feat(generator): инкрементальные счётчики manifest без перечитки Kafka

- Зачем:
  - world_next_day перечитывал всю историю топиков Kafka ради
    накопительных счётчиков — время прогона росло с возрастом мира
    (issue #5, находка F9).
- Что:
  - счётчики засеваются при import из уже прочитанного артефакта и при
    backfill из потока; next-day продвигает их только событиями нового
    дня, полного чтения Kafka больше нет;
  - катящаяся контрольная сумма — сумма SHA-256 событий по модулю 2^256
    (инкремент равен полному пересчёту), старый формат артефакта
    принимается без изменений;
  - точные множества click_id/user_domain_id вынесены из manifest в
    цепочку контент-адресуемых фрагментов (<=10 000 ID, SHA-256-цепочка,
    отдельный топик counter_chunks) — потолок сообщения Kafka не грозит,
    предел 900 000 байт проверяется явно с понятной ошибкой;
  - порядок записи всюду: фрагменты -> manifest -> state; старое локальное
    состояние отклоняется с подсказкой перезапустить import;
  - документация manifest/state обновлена (ARCHITECTURE, OPERATIONS,
    runbook startup-history).
- Проверка:
  - make test (216+31) и make lint зелёные;
  - живая приёмка на чистом стенде: import 235 с; три прогона
    world_next_day — 716/718/716 с (плоское время, O(нового дня));
    мир 3->6 дней, 561 942 события; make generated-history-chain-check —
    все порции и стыки однородны;
  - тест равенства инкремента и полного пересчёта:
    test_incremental_counters_equal_full_recompute.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-22 23:47:02 +03:00
co-authored by Claude Fable 5
parent f4971e94ca
commit 103ac021c8
12 changed files with 845 additions and 54 deletions
+11 -10
View File
@@ -82,7 +82,6 @@ Backfill/import требуют чистый стенд: пустые data-топ
- Один запуск восстанавливает мир из state, добавляет 24 модельных часа,
запускает `etl_pipeline` с полной пересборкой и сверяет витрины с manifest.
- Расписание задано каждые 30 минут, но DAG создаётся на паузе; `catchup=False`.
Не включайте расписание до внедрения накопительных счётчиков manifest.
- `max_active_runs=1` не даёт двум доливкам выполняться параллельно.
`world_next_day` работает на непустом стенде и не использует проверку чистоты.
@@ -107,13 +106,15 @@ data-топики, state, manifest. Автоматического отката
не повторяйте доливку поверх возможного хвоста. Очистите стенд и переимпортируйте
последний исправный портативный артефакт, затем повторите запуск `world_next_day`.
Текущая версия пересчитывает накопительные счётчики и контрольные суммы по всей
доступной истории data-топиков Kafka. Поэтому время выполнения и расход памяти
каждой доливки растут вместе с историей. Стандартный срок хранения Kafka тоже
ограничивает долгую работу стенда. Для ручного учебного цикла запускайте
соседние дни без долгих пауз. Перед включением расписания отдельная задача
должна выбрать одно из решений: бессрочное хранение data-топиков или новый
формат накопительного состояния manifest.
`next-day` не перечитывает старые data-топики Kafka. Основная запись manifest
хранит суммы, катящуюся контрольную сумму и SHA-256-ссылку на цепочку точных
множеств. Сами `click_id` и `user_domain_id` разбиты на небольшие неизменяемые
фрагменты в отдельном compact-топике. Новый день записывает только новый
фрагмент, поэтому размер одной записи не растёт с возрастом мира, а загрузка
основного manifest не сканирует всю цепочку.
State ссылается на основную запись manifest, чтобы две точки продолжения нельзя
было случайно смешать. Если локальный state создан старым кодом и раздела в нём
нет, повторно запустите `world_init` с операцией `import` на чистом стенде.
Перед запуском `etl_pipeline` пульт проверяет, что DAG не стоит на паузе. Если
стоит, задача падает сразу с подсказкой снять паузу в UI или командой:
@@ -294,8 +295,8 @@ GEN_HISTORY_DURATION=2d make generated-history-analytics
что state совпадает с manifest, и стартует ровно с `T_end` без настенной дельты.
Новый backfill записывает `boundaries=[T0, T_end]`. Каждый успешный `next-day`
добавляет одну границу, сдвигает `model_t_end` и пересчитывает накопительные
счётчики и контрольные суммы по всей истории.
добавляет одну границу, сдвигает `model_t_end` и дополняет накопительные
счётчики только событиями нового дня. Полной перечитки Kafka в этом пути нет.
Если историю нужно сохранить в файл и восстановить на чистом стенде без новой
генерации, используйте [runbook стартовой истории](./runbooks/startup-history.md).