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

18 KiB
Raw Blame History

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

  • Глагол next-day в generator_control: батч ровно за [T_end, T_end + 24h) от T_end манифеста; после прогона манифест содержит новую границу в boundaries и новый T_end.
  • Предпроверка границы: на пустом стенде (нет манифеста) next-day громко падает; при работающем live-генераторе — громко падает; clean-guard и существующие глаголы backfill|import|check не изменены.
  • Идемпотентность: запуск с expected_t_end, не равным T_end манифеста, — громкий отказ с обеими границами в тексте; повторный запуск того же дня после успеха — тот же отказ, дублей и второго мира нет.
  • Проверка цепочки (новая make-цель): проходит по всем границам из boundaries; после N прогонов next-day в цепочке N новых границ; непарные счётчики нулевые, per-event поля визитов через каждую границу однородны (браузер, referer, utm).
  • Учебный цикл целиком и повторяемо: next-day -> прогон etl_pipeline -> в DM-витринах появились строки ровно за новый день (счётчики до/после, прирост только в диапазоне нового дня); проверено на двух next-day подряд.
  • Регрессий нет: тесты генератора и контракты корня зелёные (make test), сценарий make generated-history-runtime-check задачи 20 остаётся зелёным.
  • Документация: 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 отдельно проверяет стык, где переходящий визит гарантирован сценарием.