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

10 KiB
Raw Blame History

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

  • Причина расхождения 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 (появление этого гейта).