Files
clickstream-ch-kafka-supers…/docs/specs/2026-06-14-generator-model-time-and-startup-history.md
T
ddadmin d5408f28e9 feat(generator): добавлена стартовая история через backfill
- Зачем:
  - стенду нужна повторяемая история с живым продолжением от модельной границы без дублей и разрыва визитов.
- Что:
  - добавлен backfill-режим с `GEN_MODEL_T_END`, manifest и state на `T_end`.
  - live-запуск восстанавливается из manifest без настенной дельты и проверяет совместимость state.
  - добавлены SQL-проверки формы данных, повторяемости и стыка backfill с live.
- Проверка:
  - make generator-test.
  - два чистых ClickHouse-прогона backfill дали одинаковые manifest checksums и digest.
  - reviewer gate issue 05 пройден после исправлений state/manifest.
2026-06-14 19:45:58 +03:00

332 lines
26 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.
# Модельное время на практике: сохранение состояния, заливка прошлого и стартовая история
Дата: 2026-06-14
Статус: Draft
Связано: [ADR-0005](../adr/0005-generator-model-clock.md) (решение про модельные
часы — эта спека его дорабатывает), [ADR-0006](../adr/0006-generation-as-sole-analytics-source.md)
(стартовая история как источник аналитики), мат-спека
[`2026-06-10-generator-math-model.md`](./2026-06-10-generator-math-model.md)
(её разделы «Персистентность через рестарты» и «Воспроизводимость» здесь
**приводятся в соответствие** с модельным временем — не повторяются, а
переописываются ссылкой), [`CONTEXT.md`](../../CONTEXT.md), задача
[`06-state-v2-and-restart`](../../.scratch/feature-data-generator/issues/06-state-v2-and-restart.md)
(сохранение состояния построено на настенных часах).
Уровень документа — **модель и правила**, как в мат-спеке. Раздел «Рабочий
контракт реализации» ниже фиксирует внешний интерфейс для задач 02–06. Внутренние
классы, функции и разбиение кода остаются за исполнителем.
## Проблема
ADR-0005 решил отвязать время генератора от реальных часов и ввёл три скорости
его хода: обычную (×1), ускоренную (×K) и мгновенную «промотку» прошлого. Но два
правила старой модели — как генератор сохраняет состояние между перезапусками и
как добивается повторяемости — описаны от реальных часов. А ADR-0006 сделал
стартовую историю (готовое сгенерированное прошлое) единственным источником
данных стенда.
Осталось довести модель до конца: как работает промотка прошлого, его заморозка и
непрерывный запуск стенда с этого момента; как при этом меняются правила
сохранения состояния и повторяемости; и как всё это проверить. Всё это — **один
механизм**: промотать прошлое → заморозить → продолжить живьём. Это один и тот же
путь сохранения и восстановления, просто с разных сторон.
## Цели
- **Дать генератору собственные часы и сделать поток повторяемым.** Генератор
должен отсчитывать время от своей точки отсчёта (ниже — «стартовая модельная
точка `T0`»), а не от реальных часов компьютера. Тогда при одних и тех же
настройках он каждый раз порождает один и тот же поток — это нужно для тестов и
для повторяемых уроков.
- **Переписать правила сохранения и восстановления состояния по этим часам.**
Сейчас они завязаны на реальное время. Главное правило — «если посетитель молчал
дольше 30 минут, считаем, что он ушёл» — должно мерить эти 30 минут по часам
генератора. И поток больше не должен зависеть от того, в котором часу реального
дня запущен генератор.
- **Научиться быстро «проматывать» прошлое и замораживать его как стартовый набор
данных.** Генератор прокручивает время без пауз от точки отсчёта до нужного
момента, порождает события прошлого и сохраняет «слепок» своего состояния. Этот
замороженный набор — то, с чего свежий стенд начинает жить, уже имея историю:
здоровую пирамиду «пользователей меньше, чем визитов, а визитов меньше, чем
событий» с первой минуты.
- **Описать, как всё это проверить — в два шага.** Сначала числами: агент
поднимает стенд и сверяет данные в ClickHouse. Потом глазами: человек смотрит на
дашборды и видит, что распределение похоже на задуманное, а стенд «дышит» во
времени.
## Чего здесь не делаем
- **Не учим генератор придумывать «фактуру» сам** (браузеры, гео, устройства,
метки кампаний). Это отдельная спека — следствие ADR-0006. Пока её нет,
стартовая история берёт фактуру из статического сида; это осознанно временно.
- **Не описываем здесь перестройку процесса** на новый сид (загрузка
`kafka_load_dag`, витрины/Superset, уроки) — это решено в ADR-0006 и
проектируется отдельно. Эта спека — только про сам генератор.
- **Не фиксируем** внутренние имена классов и функций — это за исполнителем.
- **Не добавляем** новые типы событий и инкрементальную загрузку ETL.
## Модель
### Рабочий контракт реализации
Этот раздел — источник истины для задач 02–06. Если кодовая задача меняет любое
имя, формат state, манифест или правило проверки, сначала обновляется этот
контракт.
#### Настройки
- `GEN_MODEL_T0` — стартовая модельная точка `T0`. Формат: ISO 8601 с часовым
поясом, например `2026-01-01T00:00:00+00:00`. Внутри генератора метка
нормализуется к UTC.
- `GEN_MODEL_T_END` — граница стартовой истории `T_end`. Формат такой же, как у
`GEN_MODEL_T0`. Обязательна только для режима `backfill`.
- `GEN_MODEL_TIMEZONE` — часовой пояс модельных часов для дневного коэффициента.
Формат: имя IANA, например `UTC` или `Europe/Moscow`. Значение по умолчанию —
`UTC`.
- `GEN_MODEL_TIME_SPEED` — скорость `K`: сколько модельных секунд проходит за
одну настенную секунду. Формат: положительное число, по умолчанию `1`.
- `GEN_RUN_MODE` — режим запуска: `live` или `backfill`. Значение по умолчанию —
`live`.
- `GEN_SEED`, `GEN_TICK_SECONDS` и остальные настройки генерации остаются частью
контракта повторяемости. Если они отличаются, артефакт стартовой истории
считается другим.
#### Ход часов
В живом режиме модельное время идёт фиксированным шагом. На чистом старте оно
равно `GEN_MODEL_T0`. После каждого успешного тика оно сдвигается на
`GEN_TICK_SECONDS * GEN_MODEL_TIME_SPEED`. Событийный бюджет считается по этой
модельной длительности, а не по тому, сколько процесс реально спал.
При одинаковых `GEN_SEED`, `GEN_MODEL_T0`, `GEN_MODEL_TIME_SPEED`, настройках
генерации и числе успешных тиков живой поток повторяется точно. Это выбранный
путь для текущей цепочки: он ближе к учебной цели и не добавляет новую публичную
матрицу режимов поверх ADR-0005.
`backfill` не спит и не измеряет настенные интервалы. Генератор детерминированно
проматывает модельное время от `GEN_MODEL_T0` до `GEN_MODEL_T_END`. Артефакт
повторяется точно при тех же настройках и чистом состоянии.
Измеренный настенный интервал между тиками не входит в текущий контракт живого
режима. Если он понадобится позже, это отдельное изменение спеки или ADR, потому
что оно меняет уровень повторяемости стенда.
#### Граница `T_end`
Стартовая история покрывает полуоткрытый отрезок `[T0, T_end)`: в неё попадают
события с `event_timestamp >= T0` и `event_timestamp < T_end`. Слепок состояния
сохраняется на модельной границе `T_end`.
Живое продолжение начинается из этого слепка с модельной точки `T_end`. Событие,
которое запланировано ровно на `T_end`, не входит в стартовую историю и может
быть выпущено первым живым тиком. Так на стыке нет дублей и дыр.
#### Модельные и настенные метки
Модельными считаются:
- `event_timestamp` во всех событиях;
- метки внутри визитов и популяции: начало визита, запланированные смещения,
`last_finished_at`;
- `GEN_MODEL_T0`, `GEN_MODEL_T_END`, точка возобновления и метка слепка state.
Настенными остаются операционные метки: время записи логов, метрики здоровья,
длительность тика, история отправки пачек, время сохранения state в Kafka и
служебная метка `generated_at` в манифесте. Они помогают обслуживать сервис, но
не должны менять `event_timestamp` и бизнес-логику визитов.
#### Восстановление после сбоя
Следующая версия state должна хранить связку:
- `model_timestamp` — модельная точка последнего сохранённого состояния;
- `wall_timestamp` — настенная UTC-метка, когда это состояние было сохранено;
- `model_time_speed`, `model_timezone` и `model_t0`.
При восстановлении живого режима после сбоя модельная точка считается так:
```text
resume_model_at =
state.model_timestamp
+ max(0, wall_now_utc - state.wall_timestamp) * state.model_time_speed
```
Для восстановления из стартовой истории эта формула не применяется:
`resume_model_at = manifest.model_t_end`. Иначе долгий простой между созданием
артефакта и запуском стенда искусственно оборвёт активные визиты.
Короткий или долгий простой считается только по модельному времени. При большом
`GEN_MODEL_TIME_SPEED` короткая настенная пауза может стать долгой модельной
паузой, и тогда просроченные активные визиты закрываются.
#### Манифест стартовой истории
Стартовая история состоит из трёх частей: события, слепок состояния и манифест.
Манифест хранится как JSON в Kafka compact-topic
`generator_startup_history_manifest`, ключ `default`. Слепок состояния хранится
в `generator_state`, ключ `default`. Манифест минимум содержит:
- `manifest_version`;
- `generated_at` — настенная UTC-метка создания артефакта;
- `gen_seed`;
- `model_t0`, `model_t_end`, `model_timezone`;
- `run_mode = "backfill"`;
- настройки генерации, влияющие на поток, в `generation_settings`;
- `state_version` и ссылку на запись слепка состояния;
- контрольные числа по каждому топику: количество строк, минимум и максимум
`event_timestamp`, контрольная сумма;
- итоговые контрольные числа для проверки в ClickHouse: события, визиты,
пользователи и диапазон модельного времени.
Нельзя смешивать события, state и манифест от разных `GEN_SEED`, `T0`, `T_end`
или настроек генерации. Live-запуск использует manifest как стартовую историю
только если `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-сбоя.
#### Повторяемая проверка в ClickHouse
Для этой цепочки выбираем чистый прогон, а не идемпотентную дозаливку. Перед
повторной проверкой нужно сбросить:
- таблицы ClickHouse в слоях STG, ODS, DDS и DM, куда попадают события стенда;
- Kafka-топики данных: `browser_events`, `location_events`, `device_events`,
`geo_events`;
- состояние генератора: compact topic `generator_state` или явный сброс
состояния при старте, если он гарантированно не читает старую запись.
После такого сброса один и тот же артефакт стартовой истории должен давать те же
контрольные числа в ClickHouse. Если проверка запускается без чистки, это уже
другой сценарий и его нужно описывать отдельно.
### Точка отсчёта и скорость хода часов
У генератора своя точка отсчёта времени — **стартовая модельная точка `T0`**
(задаётся в настройках). В метку события (`event_timestamp`) пишется это
внутреннее время. Скорость, с которой оно идёт относительно реальных часов,
задаётся режимами из ADR-0005:
- **×1** — как реальное время;
- **×K** — в `K` раз быстрее;
- **заливка прошлого** — время гонится без пауз от `T0` до нужного момента
(особый случай очень большого `K`).
### Повторяемость
При одном и том же зерне `GEN_SEED`, одной и той же `T0` и одной скорости
генератор каждый раз даёт **один и тот же поток** в `backfill` и в живом режиме
при одинаковом числе успешных тиков. Дневной коэффициент считается по модельному
времени в `GEN_MODEL_TIMEZONE`, а не по реальным часам. Так раздел
«Воспроизводимость» мат-спеки приводится в соответствие с модельным временем.
### Заливка прошлого и стартовая история
«Промотать» прошлое — значит прогнать время без пауз от `T0` до момента `T_end` и
сгенерировать события этого отрезка. На выходе — события за `[T0, T_end)` и
**слепок состояния** генератора на момент `T_end`.
События плюс слепок и есть **стартовая история** («стартовый сид» или «новый сид» —
синонимы, новое значение слова «сид» не заводим). При создании стенда это прошлое заливается, и
генератор готов продолжить ровно с `T_end`. Стенд сразу живёт с готовой историей —
с настоящей пирамидой «пользователей меньше, чем визитов, визитов меньше, чем
событий», без вырождения статического сида, где пользователей ровно столько же,
сколько визитов.
### Сохранение и восстановление состояния
Правило «молчал дольше 30 минут — посетитель ушёл» остаётся; меняется только **от
какого момента отсчитывать эти 30 минут**:
- **после сбоя** — от реального «сейчас» (время и правда прошло) → как сегодня;
- **при запуске со стартовой истории** — от метки слепка (`T_end`) → разрыва
почти нет, активные визиты не обрываются, мир продолжается без шва.
Паузы между визитами одного пользователя и обязательная пауза перед возвратом
тоже считаются по внутренним меткам времени, а не по числу тактов. Если
сохранённое состояние повреждено или его нет — генератор стартует с чистого листа
(свежая популяция от `GEN_SEED`) и пишет предупреждение в лог. Так раздел
«Персистентность» мат-спеки приводится в соответствие с модельным временем.
### С какой скоростью стенд живёт дальше
После старта стенд продолжает жить на выбранной скорости. Скорость — это **ручка
под задачу**: чтобы увидеть медленные вещи (возвраты, сдвиг воронки после правки
таблицы переходов, «дыхание» суточной нагрузки) за учебное время, нужно ускорение
(×K) — иначе суточная волна разворачивается реальные сутки. Какой скорость будет
по умолчанию (×1 «как настоящий сайт» или ускоренная «учебная») — решает
урок/исполнитель; спека лишь фиксирует, что это ручка и что именно на ней держится
наблюдаемость медленных явлений.
### Временная опора на сид (фактура)
Пока генератор не умеет придумывать фактуру сам (ADR-0006), стартовая история
одевает события в данные из статического сида (браузер, гео, устройство, метки
кампаний). Это временно и не мешает: механизм времени и стартовой истории не ждёт
синтеза фактуры, а сид до его появления остаётся кладовкой готовых значений — но
уже не источником аналитики.
## Проверка
Два шага — это разделение труда: первый объективный и повторяемый (его делает
агент или CI), второй — человеческий взгляд.
### Шаг 1 — агент сверяет числа в ClickHouse
На чистом стенде с фиксированными `GEN_SEED` и `T0` залить стартовую историю,
прогнать STG→DM и проверить:
- **пирамида:** уникальных пользователей меньше, чем визитов, а визитов меньше,
чем событий;
- **воронка** убывает по шагам `/home → товары → /cart → /payment →
/confirmation`; доля дошедших до `/confirmation` — в нужном коридоре;
- **возвраты:** у части пользователей больше одного визита;
- **длина визита** и доля коротких визитов — в коридорах мат-спеки.
Главное: проверки **повторяемы** (тот же `GEN_SEED` и `T0` → те же числа в
пределах допуска) и записаны как команды и запросы, чтобы их мог прогнать агент
или CI без человека. Сами запросы и числовые коридоры — при реализации.
### Шаг 2 — человек смотрит на дашборды
Глазами убедиться, что:
- **распределение похоже на задуманное** — на наши спроектированные распределения
(мат-спека), а не на профиль чужого сида;
- **стенд «дышит»** — видна суточная волна нагрузки, копятся возвраты и воронка
(заметно на ×K).
Здесь всплывает нехватка: дашборда с распределением сгенерированных данных пока
нет. Superset построен на сиде, а Grafana показывает только скорость работы
сервиса, не форму данных. Закрыть можно двумя способами: переключить Superset на
генерацию (часть большой миграции — позже) или добавить простую панель
«распределение из ClickHouse» в Grafana. Пошаговые действия шага 2 — в будущий
runbook «проверка генератора на стенде».
## Решения и отклонённые варианты
- **Приняли:** одна спека на время, сохранение состояния и стартовую историю —
это один механизм, а не три задачи.
- **Приняли:** своя точка отсчёта `T0` как опора повторяемости; дневной
коэффициент считается по внутреннему времени.
- **Приняли:** проверка в два шага (агент — числа, человек — глаза).
- **Отклонили:** жёстко подгонять генерацию под профиль сида (см. ADR-0006).
- **Отклонили:** приводить сохранение состояния в соответствие с модельным
временем отдельно от стартовой истории — это разрезало бы один механизм надвое.
## Влияние на документацию
- **Мат-спека:** разделы «Персистентность через рестарты» и «Воспроизводимость»
пометить как переописанные в модельном времени этой спекой (ссылкой, без
повтора).
- **ADR-0005:** направление «стартовая история» — отмечено как запущенное.
- **CONTEXT.md:** «стартовый сид» / «новый сид» = «стартовая история стенда»
(синонимы, не новое значение); роль статического сида — по ADR-0006.
- **Будущий runbook** «проверка генератора на стенде» — шаги шага 2.
- **`generator/README.md`, `docs/OPERATIONS.md`** — при реализации: скорость хода
часов, ×K, стартовая история, проверки шага 1.