Files
clickstream-ch-kafka-supers…/.scratch/generator-model-time-startup-history/issues/20-flaky-runtime-seam-check.md
T
ddadmin 55a3feb63d docs(generator): развилка фикса задачи 20 решена после ревью постановки
- Зачем:
  - адверсарное ревью показало: генератор не обрабатывает SIGTERM
    (python — PID 1, docker compose stop ведёт к SIGKILL посреди batch),
    а вариант «ждать записи generator_batch_history» гонку не закрывает.
- Что:
  - направление фикса: минимальный SIGTERM-хендлер с дозаписью batch,
    альтернативы отклонены с доводами, bounded live mode вне скоупа.
  - критерии дополнены: красный сценарий обязан ловиться, стабильность
    обосновывается структурно, запрещены вероятностные смягчения.
  - в диагноз внесены механизм обрыва и различие device/geo vs location.
- Проверка:
  - чтение постановки: все находки ревью закрыты решением или доводом.
2026-07-12 18:28:17 +03:00

129 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
Status: ready-for-agent
# Runtime-проверка стыка нестабильна: фактура меняется через раз
## Parent
`.scratch/generator-model-time-startup-history/PRD.md`
## Что нашли
Два подряд прогона `make generated-history-runtime-check` на одном и том же
стенде (2026-07-07, проверка пути менти) дали разный результат:
- Прогон 1 (20:1820:20): **красный**
`Ошибка: per-event фактура меняется на стыке: 8/19`
(у 8 из 19 визитов, переживших границу backfill->live, поменялась
per-event фактура). При этом `duplicate_events=0`, events=2776.
- Прогон 2 (21:5x, та же команда, без изменений кода): **зелёный**
«runtime-проверка startup-history/live seam прошла», конфликтов 0,
events=2815.
Конфигурация проверки: `GEN_LAUNCH_PROFILE=daily-wave`,
`GEN_HISTORY_DURATION=1h`, `GEN_MODEL_T_END=2026-01-01T01:00:00+00:00`
(зашита в `scripts/run_generated_history_runtime_check.sh`).
## Почему это важно
- Это гейт, которому мы доверяем стык backfill->live (задача 17); флаки-гейт
ничего не гарантирует: красный пугает зря, зелёный ничего не доказывает.
- Смена фактуры на стыке — класс дефекта задачи 09, который считается
закрытым (донор фактуры сохранён в state, тихие fallback'и заменены на
ошибки). Либо фикс неполон, либо есть второй источник расхождения.
- Задача 13 (глагол next-day) навешивает на этот же механизм цепочку границ —
ей нужен доверенный, стабильный гейт.
## Диагноз (Codex, 2026-07-07; ключевой факт перепроверен координатором)
**Корень: гонка остановки live-генератора, а не смена фактуры.** Цепочка:
`run_generated_history_runtime_check.sh` останавливает live сразу после первых
новых STG-строк -> генератор публикует batch по четырём топикам
последовательно, без атомарности -> `docker compose stop generator` может
оборвать процесс между топиками -> часть browser-событий остаётся без парных
location/device/geo -> DDS строит `dds.event` через `LEFT JOIN location`,
и такие строки получают `NULL` в referer/utm -> seam-SQL считает это «сменой
фактуры внутри click_id».
Эмпирика: в красном прогоне `browser_raw=2776`, но `location_raw=2736`
40 live-событий без пары; в зелёном `browser_raw=location_raw=2815`, поэтому
проверка прошла (хотя device/geo и там отстали: 2781).
Судьба гипотез:
1. Запасная ветка рождения — **опровергнута**: ветка уже хранит донора
(`generation.py:196-209`, комментарий «Запасная ветка тоже восстановима»)
и громко падает на неполных locations. Перепроверено координатором по коду.
2. Недетерминизм live — **подтверждена частично**: решает не сид, а настенный
момент остановки процесса и какие топики успели дописаться.
3. Проверочный SQL — **опровергнута в формулировке**: поля не «легитимно
различаются», SQL маскирует пропущенный location под «смену фактуры».
Уточнение механизма обрыва (адверсарное ревью постановки, 2026-07-12;
перепроверено координатором по коду):
- Генератор не обрабатывает SIGTERM: ловится только `KeyboardInterrupt`
(`service.py:175`), а `docker compose stop` шлёт именно SIGTERM. В compose
у сервиса generator нет `init:`/`stop_signal:`, python работает PID 1 —
SIGTERM игнорируется, и через grace-период прилетает SIGKILL: жёсткий
обрыв посреди batch, без `finally` и без flush.
- Почему отставание device/geo не валит гейт, а location валит: device/geo
привязаны к `click_id` (одно значение на визит) — при пропуске весь визит
однородно NULL, массив уникальных значений длины 1, проверка проходит;
location привязан к `event_id` — частичный пропуск даёт смешанный массив
(реальное значение + NULL) и «смену фактуры». Догонять device/geo фикс
не обязан — важна граница batch.
## Направление фикса (развилка решена после ревью постановки 2026-07-12)
1. **Основное: корректная обработка SIGTERM в генераторе.** Минимальный
хендлер: по SIGTERM выставить `_running = False`, дать текущему tick'у
дописаться (все четыре топика + flush + запись batch history), затем
штатный `finally`/`stop()`. Runtime-check после `docker compose stop`
дожидается фактического завершения контейнера. Так генератор завершается
только на границе batch — гонка снята по построению, а не вероятностно.
- **Отклонено: «ожидание записи `generator_batch_history` перед stop»
как самостоятельный фикс** — запись history подтверждает только
ПРОШЕДШИЙ batch (`service.py:586-603`, пишется после цикла публикации);
следующий batch к моменту stop уже может быть в полёте, и SIGKILL
оборвёт его так же. Довод — ревью постановки, перепроверен по коду.
- Bounded live mode (генератор сам останавливается по лимиту) — более
тяжёлая альтернатива; в скоуп не входит, возвращаться к ней только
если SIGTERM-хендлера окажется недостаточно (с доводом в задаче).
2. **Дополнительно:** precheck в seam-check на непарные
browser/location/device/geo live-строки — чтобы ошибка называла реальную
причину, а не «фактура поменялась». Precheck не подменяет основную
проверку: настоящая смена фактуры внутри `click_id` обязана падать
как и раньше.
3. **Отклонены любые вероятностные смягчения**, а не только «увеличить
sleep»: settle-sleep перед прогоном, рост `WAIT_LIVE_ROWS`/`LIVE_SECONDS`,
сужение окна `GEN_LIVE_CHECK_MINUTES` и прочие способы снизить
вероятность — гонку они не убирают и фиксом не считаются.
## Acceptance criteria
- [x] Причина расхождения 8/19 найдена и названа (код, не догадка) —
см. «Диагноз» выше.
- [ ] Генератор корректно завершается по SIGTERM: текущий batch дописывается
во все четыре топика целиком (flush + запись history), потом процесс
выходит; runtime-check дожидается фактической остановки контейнера
(направление фикса, пункт 1).
- [ ] Seam-check различает «непарные live-строки» и «смена фактуры»:
precheck называет реальную причину (пункт 2).
- [ ] Красный сценарий по-прежнему ловится: настоящая смена per-event
фактуры внутри `click_id` (инъекция в тесте или контролируемое искажение
данных) валит гейт с прежним сообщением — precheck и фикс не сделали
проверку мягче.
- [ ] Стабильность обоснована структурно (гонка снята по построению:
завершение только на границе batch), а не статистикой прогонов;
`make generated-history-runtime-check` — N подряд зелёных (N >= 3)
как дымовая проверка поверх этого довода, зафиксировано в задаче.
## Blocked by
- Нет. Диагноз можно начинать сразу; стенд воспроизводит через раз.
Связано: `09-seam-browser-fixture-not-preserved.md` (класс дефекта и решение
про донора), `13-backfill-top-up-from-snapshot.md` (нуждается в доверенном
гейте на цепочке границ), `17-trusted-checks-startup-history-superset.md`
(появление этого гейта).