Files
clickstream-ch-kafka-supers…/.scratch/generator-model-time-startup-history/issues/13-backfill-top-up-from-snapshot.md
T
ddadminandClaude Fable 5 f5dee26eda feat(generator): глагол next-day — следующий модельный день от слепка
- Зачем:
  - режим кормления стенда порциями: менти триггерит «следующий день»,
    видит полный цикл DWH за один шаг (задача 13, вариант 2 — генерация
    от слепка T_end).
- Что:
  - новый ограниченный режим next-day: восстановление мира из state,
    генерация ровно [T_end, T_end+24h), публикация данные -> state ->
    манифест (манифест — точка фиксации, автоотката нет).
  - операция next-day в DAG generator_control: своя предпроверка границы
    вместо clean-guard, идемпотентность через параметр expected_t_end.
  - цепочка границ — накопительное поле boundaries в манифесте, старый
    формат читается как [T0, T_end]; импорт не изменён.
  - новая проверка цепочки (make generated-history-chain-check): непарные
    счётчики и однородность по каждой границе, явный статус нулевого
    стыка, хвост за границей по всем четырём топикам, литералы в UTC
    с микросекундами.
  - документация OPERATIONS.md: глагол, предпроверка, восстановление
    после сбоя, ограничение retention; в задаче 13 — решения двух слепых
    ревью постановки и кода с аргументами отклонений.
- Проверка:
  - make test: 204 теста генератора + 31 контракт корня, зелёные.
  - make generated-history-chain-check: зелёный, 2 внутренние границы,
    непарные счётчики нулевые; учебный цикл: DM 322 -> 10026 -> 19196
    за два next-day подряд.
  - make generated-history-runtime-check (регрессия задачи 20): зелёный.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 23:53:48 +03:00

209 lines
18 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: done
# Глагол next-day: собрать следующий модельный день
## Parent
`.scratch/generator-model-time-startup-history/PRD.md`
## Why
Рамка пересмотрена 2026-07-07 (обсуждение с пользователем). Было: «удлинить
готовую историю» (2 суток -> 7) — этот мотив закрыл портативный артефакт
(задача 07), приоритет был низкий. Стало: **режим кормления стенда порциями**.
Ценность не в длине истории, а в дискретности подачи:
- **Урок.** Менти триггерит «следующий день» -> в Kafka падает батч за новый
модельный день -> прогон `etl_pipeline` -> витрины и дашборды сдвинулись.
Полный цикл DWH виден за один шаг; повторяемо и управляемо, в отличие от
непрерывного live.
- **Имитация жизни.** Тот же механизм по расписанию — стенд «живёт».
В этой задаче делаем глагол готовым к расписанию (идемпотентность,
громкие отказы), но само включение расписания — отдельная задача
(решение по скоупу 2026-07-12, см. «Решения по ревью постановки»).
- Удлинение истории остаётся как частный случай (несколько next-day подряд).
## Интерфейс
Новый глагол `next-day` в существующем DAG `generator_control`
(`airflow/dags/generator_control_dag.py`), не отдельный DAG: менти уже знает,
где ручки генератора. Pause-check `etl_pipeline` переиспользуется
(`generator_control_dag.py:153-164` — подтверждено ревью). Guard чистого
стенда **не** переиспользуется — у next-day своя предпроверка границы
(см. решение 3 ниже).
## Развилка реализации — решена (пользователь, 2026-07-07)
**Выбран вариант 2: генерация следующего дня от слепка `T_end`.**
Обоснование. Диагноз задачи 20 показал, что генератор на стыке ничего не
ломает — красный гейт был гонкой в самой проверке. Оба варианта одинаково
требуют цепочку границ и расширение проверок, а вариант 1 сверх того требует
новый режим среза в импорте («всё или ничего» сейчас). Вариант 2 не дороже,
честнее (жизнь стенда не ограничена длиной артефакта) и переиспользует
закалённый restore-механизм (задача 09: донор в state, громкие ошибки,
детерминированная запасная ветка).
Отклонено: вариант 1 (резать готовый артефакт на дневные порции) — не даёт
неограниченной жизни и требует своей новой механики импорта.
## Решения по ревью постановки (2026-07-12, два слепых ревью)
Оба ревью (Codex и Claude, свежие сессии) вернули CHANGES_REQUIRED.
Все находки внесены решениями ниже; полные индексы — в обменном каталоге
сессии (в репо не хранятся).
1. **next-day — новый ограниченный режим запуска, а не переиспользование
существующего глагола.** Сейчас `backfill` — свежая генерация от `T0`
с чистым завершением, `continue` — бесконечный live
(`generator/src/clickstream_generator/launch.py:96-128`,
`service.py:96-128`). next-day = восстановление мира из state
+ генерация ровно одного дня с самозавершением + сдвиг манифеста.
Это основная работа задачи, «переиспользуется» только restore-механизм.
2. **Один модельный день = ровно 24 часа, полуоткрытый диапазон
`[T_end, T_end + 24h)` в модельном времени (UTC).** Календарные сутки и
`GEN_MODEL_TIMEZONE` в границах не участвуют; литералы времени в
проверках — по образцу `clickhouse_datetime_literal` (урок задачи 20).
3. **Предпроверка границы вместо guard'а чистого стенда.**
`assert_stand_clean` в DAG требует пустые Kafka и STG
(`airflow/dags/lib/airflow_control.py:99-109`) — next-day по построению
доливает в непустой стенд. Своя предпроверка: манифест существует,
state согласован с `T_end` манифеста, live-генератор остановлен.
Образец «непустого» режима — `assert_stand_clean.sh continue`
(в Python-версии guard'а такого режима нет — добавить именно для
next-day, `clean`-режим не менять).
4. **Идемпотентность — через явный параметр.** У DAG-запуска next-day есть
необязательный параметр `expected_t_end`: если задан и не совпадает с
`T_end` манифеста — громкий отказ, в тексте обе границы (ожидаемая и
фактическая). Повторный триггер «того же дня» с прежним `expected_t_end`
после успешного прогона ловится этим же сравнением. Без параметра —
генерация от текущей границы (режим «жизни»); от двойного параллельного
запуска защищает `max_active_runs=1`.
5. **Точка фиксации — публикация манифеста, автоотката нет.** Порядок
публикации дня: топики данных -> state -> манифест последним. Сбой до
манифеста оставляет «хвост» за границей — его ловит проверка цепочки
(непарные счётчики), восстановление — переимпорт артефакта
(задокументировать в OPERATIONS.md). Отклонено: инкрементальный откат
через `rollback_import_topics` — он удаляет топики целиком
(`startup_history_artifact.py:250-270`) и для доливки непригоден.
6. **Цепочка границ живёт внутри единственной записи манифеста.**
Манифест остаётся одной записью compact-топика с ключом `default`
(`kafka_io.py:205-274`); добавляется необязательное поле `boundaries`
накопительный список границ `[T0, d1, ..., T_end]`. Старый манифест без
поля читается как `[T0, T_end]`. Итоговые счётчики и контрольные суммы
остаются накопительными за всю историю. Формат артефакта меняется только
этим необязательным полем — без новой версии и без изменения импорта.
7. **Проверка цепочки — новый батчевый режим, live-гейт не трогать.**
Текущий seam-гейт (`scripts/check_generated_analytics.sh:334-531`)
привязан к одной границе и живому генератору — литерально «расширить»
его нельзя. Новый режим (отдельная make-цель): по каждой границе из
`boundaries` — непарные счётчики и однородность per-event полей у
визитов, переживших границу (класс проверки задачи 09). Существующий
сценарий `make generated-history-runtime-check` остаётся зелёным как был.
8. **Расписание — вне этой задачи.** `schedule=None` у DAG не меняется;
задача гарантирует только готовность глагола к расписанию (пункты 4–5).
Включение «жизни» по расписанию — отдельная задача-продолжение (в ней же
решить: как расписание задаёт `operation=next-day`, ведь параметр DAG по
умолчанию — `backfill`, и планового запуска с дефолтом быть не должно).
9. **Исправлены ссылки:** запасная ветка рождения визита —
`generation.py:196-209` (строка 194 — основная ветка); донор
сериализуется в `runtime.py:178-226`.
## Acceptance criteria
- [x] Глагол `next-day` в `generator_control`: батч ровно за
`[T_end, T_end + 24h)` от `T_end` манифеста; после прогона манифест
содержит новую границу в `boundaries` и новый `T_end`.
- [x] Предпроверка границы: на пустом стенде (нет манифеста) next-day
громко падает; при работающем live-генераторе — громко падает;
`clean`-guard и существующие глаголы `backfill|import|check` не изменены.
- [x] Идемпотентность: запуск с `expected_t_end`, не равным `T_end`
манифеста, — громкий отказ с обеими границами в тексте; повторный запуск
того же дня после успеха — тот же отказ, дублей и второго мира нет.
- [x] Проверка цепочки (новая make-цель): проходит по всем границам из
`boundaries`; после N прогонов next-day в цепочке N новых границ;
непарные счётчики нулевые, per-event поля визитов через каждую границу
однородны (браузер, referer, utm).
- [x] Учебный цикл целиком и повторяемо: next-day -> прогон `etl_pipeline`
-> в DM-витринах появились строки ровно за новый день (счётчики до/после,
прирост только в диапазоне нового дня); проверено на двух next-day подряд.
- [x] Регрессий нет: тесты генератора и контракты корня зелёные
(`make test`), сценарий `make generated-history-runtime-check`
задачи 20 остаётся зелёным.
- [x] Документация: OPERATIONS.md — глагол, предпроверка, поведение при
сбое до фиксации манифеста (восстановление переимпортом артефакта).
## Границы (что не трогать)
- Поведение существующих глаголов `backfill`, `import`, `check` и
`clean`-guard'а.
- Формат артефакта — кроме необязательного поля `boundaries` (решение 6);
механику импорта не менять.
- Live seam-гейт задачи 20 (`generated-history-runtime-check`) — не
редактировать, только прогонять как регрессию.
## Blocked by
- Нет. Последний блокер снят 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.
Исторический контекст: `09-seam-browser-fixture-not-preserved.md` — шов
рождается при любом прохождении мира через сериализованный state (рестарт,
сбой, доливка — один код восстановления); фикс 09 положил донора фактуры в
state и закрыл тихие fallback'и. Прежняя редакция этой задачи называлась
«Доливка стартовой истории кусочком от слепка». Проверенные 2026-07-07
факты (import «всё или ничего», донор задачи 09) подтверждены обоими ревью
2026-07-12 по коду ветки.
## Решения по слепым ревью реализации (2026-07-12)
### Полная перечитка Kafka и конечный срок хранения — отклонено в задаче 13
Накопительные `visits` и `users` требуют точного объединения идентификаторов,
а текущая контрольная сумма — SHA-256 последовательности событий. Из одних
итоговых чисел и готовой контрольной суммы нельзя точно добавить новый день. Для
инкрементального расчёта пришлось бы хранить множества идентификаторов и новое
состояние hash в manifest. Это изменило бы формат артефакта сверх разрешённого
поля `boundaries` и нарушило бы решение 6.
Поэтому в задаче 13 остаётся точная пересборка по доступной истории Kafka.
Её проверяет
`test_next_day_publishes_24h_then_state_then_cumulative_manifest`: два дня,
точные накопительные счётчики и суммы. Риск принят только для ручного учебного
цикла. Ограничение по времени, памяти и сроку хранения записано в OPERATIONS.md.
Задача про расписание из решения 8 до включения обязана выбрать бессрочное
хранение data-топиков или новый согласованный формат накопительного состояния.
### Device/geo min/max timestamps после пересборки — отклонено
Эти два диапазона являются диагностическими полями статистики топика и не
участвуют в `compare_clickhouse_stats_to_manifest`, проверке цепочки или
критериях приёмки. Контракт задачи требует накопительные строки и контрольные
суммы; они пересчитываются точно и проверяются тестом двух `next-day`.
Выравнивание
device/geo min/max с потактовым backfill потребовало бы сохранять связь каждой
повторной строки с исходным browser-событием, которой в этих сообщениях нет.
Менять формат ради неиспользуемых диагностических полей в задаче 13 не следует.
### Нулевой стык — допустим с явным статусом
Диагноз текущего стенда подтвердил естественный ночной провал, а не потерю
визитов при восстановлении. На границе `2026-01-02T01:00:00+00:00` DDS и STG
дали `crossing_visits=0`. За предыдущий час завершилось 27 визитов, за
следующий началось 24. Последний визит закончился в `00:56:57.766327`, новый
начался в `01:00:00`; пауза составила 183 секунды. State на границе содержал
`active_visits=0`, поэтому restore не мог отбросить активный визит. Для
контроля: на предыдущей границе state содержал один активный визит, и DDS/STG
нашли один переходящий визит.
Поэтому нулевой стык проходит с явным статусом «однородность неприменима».
Пустая порция, хвост за последней границей и непарные строки по-прежнему дают
отказ. Остаточный слепой участок принят: однородность нельзя проверить без
переходящего визита. Restore закреплён тестом двух последовательных `next-day`
и детерминизмом задачи 09, а live-гейт задачи 20 отдельно проверяет стык, где
переходящий визит гарантирован сценарием.