fix(generator): устранён флаки runtime-гейта стыка backfill/live

- Зачем:
  - гейт generated-history-runtime-check падал через раз: docker compose
    stop убивал генератор SIGKILL'ом посреди batch (SIGTERM не ловился),
    непарные строки маскировались под «смену фактуры» (задача 20).
- Что:
  - генератор грациозно завершается по SIGTERM: текущий batch дописывается
    во все топики с flush и записью history; compose даёт минуту grace.
  - runtime-check ждёт пересекающий визит в STG (предусловие проверки),
    seam-check получил precheck непарных live-строк; в STG-запросах
    закреплён 'UTC' против сдвига наивных меток в поясе сервера.
  - контрактные тесты усилены, задача 20 закрыта, блокер задачи 13 снят.
- Проверка:
  - тесты: 192 passed (generator), 21 passed (контракты);
  - стенд: 3 подряд зелёных make generated-history-runtime-check;
    красный сценарий (искажение referer_url пересекающего визита) валит
    гейт прежним сообщением при нулевых непарных счётчиках.
This commit is contained in:
2026-07-12 19:33:28 +03:00
parent 55a3feb63d
commit dab120a07e
9 changed files with 433 additions and 30 deletions
@@ -81,8 +81,9 @@ Status: ready-for-agent
## Blocked by
- `20-flaky-runtime-seam-check.md` — доверенный стабильный гейт стыка нужен
до навешивания на него цепочки границ; фикс 20 идёт первым.
- Нет. Последний блокер снят 2026-07-12: `20-flaky-runtime-seam-check.md`
закрыта, гейт стыка стабилен (3 подряд зелёных прогона, красный сценарий
ловится).
- Исторические блокеры закрыты: `15-world-boundary-after-cross-review.md`
(граница миров) и `17-trusted-checks-startup-history-superset.md`
(доверенные проверки) — done.
@@ -1,4 +1,4 @@
Status: ready-for-agent
Status: done
# Runtime-проверка стыка нестабильна: фактура меняется через раз
@@ -99,24 +99,54 @@ location/device/geo -> DDS строит `dds.event` через `LEFT JOIN locati
сужение окна `GEN_LIVE_CHECK_MINUTES` и прочие способы снизить
вероятность — гонку они не убирают и фиксом не считаются.
## Принятый остаточный риск (решение координатора, 2026-07-12)
Ревью отметило: `stop_grace_period: 1m` не ограничивает публикацию по
времени — при зависании Kafka дольше минуты SIGKILL всё ещё оборвёт batch.
Риск принят: штатный тик публикуется за секунды (минута — многократный
запас), а ограничивать публикацию таймером значило бы рвать batch уже по
построению. Ключевое отличие от исходного флаки: такой обрыв больше не
маскируется под «смену фактуры» — precheck непарных строк назовёт его
явно, гейт упадёт громко и честно.
## Acceptance criteria
- [x] Причина расхождения 8/19 найдена и названа (код, не догадка) —
см. «Диагноз» выше.
- [ ] Генератор корректно завершается по SIGTERM: текущий batch дописывается
- [x] Генератор корректно завершается по SIGTERM: текущий batch дописывается
во все четыре топика целиком (flush + запись history), потом процесс
выходит; runtime-check дожидается фактической остановки контейнера
(направление фикса, пункт 1).
- [ ] Seam-check различает «непарные live-строки» и «смена фактуры»:
(направление фикса, пункт 1). Плюс `stop_grace_period: 1m` в compose.
- [x] Seam-check различает «непарные live-строки» и «смена фактуры»:
precheck называет реальную причину (пункт 2).
- [ ] Красный сценарий по-прежнему ловится: настоящая смена per-event
фактуры внутри `click_id` (инъекция в тесте или контролируемое искажение
данных) валит гейт с прежним сообщением — precheck и фикс не сделали
проверку мягче.
- [ ] Стабильность обоснована структурно (гонка снята по построению:
завершение только на границе batch), а не статистикой прогонов;
`make generated-history-runtime-check` — N подряд зелёных (N >= 3)
как дымовая проверка поверх этого довода, зафиксировано в задаче.
- [x] Красный сценарий по-прежнему ловится: контролируемое искажение
`referer_url` у события пересекающего визита в `dds.event` уронило гейт
с прежним сообщением «per-event фактура меняется на стыке: 18/19» при
нулевых счётчиках непарных строк (стендовая приёмка 2026-07-12).
- [x] Стабильность обоснована структурно (SIGTERM ставит флаг, тик
дописывает все четыре топика + flush + history и выходит на границе
batch — тест `test_sigterm_during_publish_finishes_current_batch`);
дымовая проверка поверх довода: 3 подряд зелёных
`make generated-history-runtime-check` (2026-07-12, прогоны 19:25,
19:28, 19:30).
## Находки стендовой приёмки (2026-07-12, исправлены в этой же задаче)
Ревью по чтению кода их поймать не могло — вскрылись только прогонами:
1. **Гейт жил на побочном эффекте бага.** Пересекающие визиты для проверки
стыка появлялись только потому, что генератор игнорировал SIGTERM и
дописывал ~10 секунд данных до SIGKILL. После фикса live-хвост стал
коротким и пересекающих визитов могло не быть вовсе. Фикс: шаг 6
runtime-check ждёт не «любую новую STG-строку», а появления
пересекающего визита в STG (детерминированное предусловие проверки,
то же окно, что у seam-SQL).
2. **Часовой пояс в STG-запросах.** Сырые `event_timestamp` наивные и
означают UTC, сервер ClickHouse — Europe/Moscow: сравнение с границей,
заданной с `+00:00`, уезжало на 3 часа, precheck был зелёным вакуумно
(не видел ни одной live-строки). Фикс: явный `'UTC'` в
`parseDateTime64BestEffort*` с обеих сторон сравнения (4 места),
закреплено контрактными тестами.
## Blocked by