feat(generator): добавлен артефакт стартовой истории

- Зачем:
  - чистый стенд должен восстанавливать стартовую историю без повторной генерации.
- Что:
  - добавлены export/import артефакта через Kafka и compact-топики.
  - добавлена manifest-aware сверка ClickHouse и защита от смешения state.
  - добавлен runbook использования стартовой истории.
- Проверка:
  - uv run --with-requirements generator/requirements.txt pytest generator/tests -q.
  - bash -n scripts/check_startup_history_manifest.sh scripts/export_startup_history_artifact.sh scripts/import_startup_history_artifact.sh.
  - git diff --check.
This commit is contained in:
2026-07-04 18:23:54 +03:00
parent ee4c9f5857
commit 3c38465d89
16 changed files with 1746 additions and 177 deletions
@@ -90,6 +90,8 @@ ADR-0005 решил отвязать время генератора от реа
одну настенную секунду. Формат: положительное число, по умолчанию `1`.
- `GEN_RUN_MODE` — режим запуска: `live` или `backfill`. Значение по умолчанию —
`live`.
- `GEN_STARTUP_HISTORY_ARTIFACT` — путь к JSON-файлу, куда `backfill` дополнительно
пишет портативный артефакт стартовой истории. В обычном live-запуске не нужен.
- `GEN_SEED`, `GEN_TICK_SECONDS` и остальные настройки генерации остаются частью
контракта повторяемости. Если они отличаются, артефакт стартовой истории
считается другим.
@@ -162,12 +164,35 @@ resume_model_at =
`GEN_MODEL_TIME_SPEED` короткая настенная пауза может стать долгой модельной
паузой, и тогда просроченные активные визиты закрываются.
Если state отсутствует или повреждён, генератор стартует чисто и пишет
предупреждение. Если state читается, `GEN_STATE_RESET=false`, но настройки
продолжения несовместимы (`GEN_SEED`, `GEN_MODEL_T0`, `GEN_MODEL_TIMEZONE`,
`GEN_MODEL_TIME_SPEED`, а для стартовой истории ещё и `GEN_MODEL_T_END` или
manifest), генератор должен упасть с перечнем разошедшихся полей и подсказкой
использовать `GEN_STATE_RESET=true` для осознанного нового мира.
#### Манифест стартовой истории
Стартовая история состоит из трёх частей: события, слепок состояния и манифест.
Манифест хранится как JSON в Kafka compact-topic
В рабочем стенде манифест хранится как JSON в Kafka compact-topic
`generator_startup_history_manifest`, ключ `default`. Слепок состояния хранится
в `generator_state`, ключ `default`. Манифест минимум содержит:
в `generator_state`, ключ `default`.
Портативный файл-артефакт хранит тот же связный набор: сообщения топиков
`browser_events`, `location_events`, `device_events`, `geo_events`, слепок state
и manifest. Для событий хранится raw JSON value, чтобы импорт мог воспроизвести
Kafka-сообщения без повторной сериализации dict. Импорт артефакта воспроизводит
сообщения в Kafka и записывает state с manifest в служебные compact-топики.
Напрямую в ClickHouse импорт не пишет: ClickHouse наполняется штатным путём через
Kafka engine и Materialized View.
Импорт рассчитан на чистый стенд. Перед записью он проверяет, что data-топики
Kafka пустые. Так как текущий Python-клиент Kafka не даёт транзакционный producer
для нескольких топиков, при ошибке записи импорт удаляет import-топики Kafka,
чтобы повторный импорт не дописал дубли; если ClickHouse уже успел прочитать
частичные сообщения, стенд очищается как clean-stand сценарий.
Манифест минимум содержит:
- `manifest_version`;
- `generated_at` — настенная UTC-метка создания артефакта;
@@ -186,8 +211,8 @@ resume_model_at =
только если `state.last_batch_id`, `state.model_timestamp`, `GEN_SEED`,
`GEN_MODEL_T0`, `GEN_MODEL_T_END`, `GEN_MODEL_TIMEZONE`, `GEN_MODEL_TIME_SPEED`
и `generation_settings` совпадают. Startup-history state без подходящего
manifest считается несовместимым и ведёт к чистому старту, а не к восстановлению
по правилу live-сбоя.
manifest считается несовместимым читаемым state и даёт жёсткий отказ при
`GEN_STATE_RESET=false`.
#### Повторяемая проверка в ClickHouse