docs(generator): развилка фикса задачи 20 решена после ревью постановки

- Зачем:
  - адверсарное ревью показало: генератор не обрабатывает SIGTERM
    (python — PID 1, docker compose stop ведёт к SIGKILL посреди batch),
    а вариант «ждать записи generator_batch_history» гонку не закрывает.
- Что:
  - направление фикса: минимальный SIGTERM-хендлер с дозаписью batch,
    альтернативы отклонены с доводами, bounded live mode вне скоупа.
  - критерии дополнены: красный сценарий обязан ловиться, стабильность
    обосновывается структурно, запрещены вероятностные смягчения.
  - в диагноз внесены механизм обрыва и различие device/geo vs location.
- Проверка:
  - чтение постановки: все находки ревью закрыты решением или доводом.
This commit is contained in:
2026-07-12 18:28:17 +03:00
parent 515d5ef73b
commit 55a3feb63d
@@ -51,34 +51,72 @@ location/device/geo -> DDS строит `dds.event` через `LEFT JOIN locati
Судьба гипотез:
1. Запасная ветка рождения — **опровергнута**: ветка уже хранит донора
(`generation.py:194-209`, комментарий «Запасная ветка тоже восстановима»)
(`generation.py:196-209`, комментарий «Запасная ветка тоже восстановима»)
и громко падает на неполных locations. Перепроверено координатором по коду.
2. Недетерминизм live — **подтверждена частично**: решает не сид, а настенный
момент остановки процесса и какие топики успели дописаться.
3. Проверочный SQL — **опровергнута в формулировке**: поля не «легитимно
различаются», SQL маскирует пропущенный location под «смену фактуры».
## Направление фикса (из диагноза)
Уточнение механизма обрыва (адверсарное ревью постановки, 2026-07-12;
перепроверено координатором по коду):
1. **Основное:** сделать остановку live управляемой — завершать после полного
batch/tick и flush всех топиков (bounded live mode или ожидание записи
`generator_batch_history` с полными sent-счётчиками).
- Генератор не обрабатывает 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-строки — чтобы ошибка называла реальную
причину, а не «фактура поменялась».
3. Просто увеличить sleep перед stop — отклонено: снижает вероятность,
но гонку не убирает.
причину, а не «фактура поменялась». Precheck не подменяет основную
проверку: настоящая смена фактуры внутри `click_id` обязана падать
как и раньше.
3. **Отклонены любые вероятностные смягчения**, а не только «увеличить
sleep»: settle-sleep перед прогоном, рост `WAIT_LIVE_ROWS`/`LIVE_SECONDS`,
сужение окна `GEN_LIVE_CHECK_MINUTES` и прочие способы снизить
вероятность — гонку они не убирают и фиксом не считаются.
## Acceptance criteria
- [x] Причина расхождения 8/19 найдена и названа (код, не догадка) —
см. «Диагноз» выше.
- [ ] Остановка live в runtime-check управляемая: batch дописывается во все
топики целиком до остановки (направление фикса, пункт 1).
- [ ] Генератор корректно завершается по SIGTERM: текущий batch дописывается
во все четыре топика целиком (flush + запись history), потом процесс
выходит; runtime-check дожидается фактической остановки контейнера
(направление фикса, пункт 1).
- [ ] Seam-check различает «непарные live-строки» и «смена фактуры»:
precheck называет реальную причину (пункт 2).
- [ ] `make generated-history-runtime-check` стабилен: N подряд прогонов
зелёные (N >= 3), зафиксировано в задаче.
- [ ] Красный сценарий по-прежнему ловится: настоящая смена per-event
фактуры внутри `click_id` (инъекция в тесте или контролируемое искажение
данных) валит гейт с прежним сообщением — precheck и фикс не сделали
проверку мягче.
- [ ] Стабильность обоснована структурно (гонка снята по построению:
завершение только на границе batch), а не статистикой прогонов;
`make generated-history-runtime-check` — N подряд зелёных (N >= 3)
как дымовая проверка поверх этого довода, зафиксировано в задаче.
## Blocked by