docs(generator): зафиксирован контракт модельного времени
- Зачем: - нужно закрепить принятое человеком HITL-решение до кодовых задач 02-06. - Что: - добавлен рабочий контракт настроек, хода часов, state, манифеста и ClickHouse-проверки. - выбран один живой ход часов через фиксированный модельный шаг без отдельной матрицы драйверов. - уточнена граница стартовой истории как [T0, T_end) и закрыт чек-лист issue 01. - Проверка: - git diff --cached --check.
This commit is contained in:
@@ -35,12 +35,16 @@ Status: Draft
|
|||||||
|
|
||||||
## Сквозные инварианты
|
## Сквозные инварианты
|
||||||
|
|
||||||
|
- Рабочий контракт реализации живёт в
|
||||||
|
`docs/specs/2026-06-14-generator-model-time-and-startup-history.md`, раздел
|
||||||
|
«Рабочий контракт реализации». Задачи 02–06 берут имена настроек, формат
|
||||||
|
манифеста, границу `T_end` и правила проверки оттуда.
|
||||||
- `event_timestamp` — модельное время, а не настенные часы компьютера.
|
- `event_timestamp` — модельное время, а не настенные часы компьютера.
|
||||||
- Операционные метки сервиса, история пачек, метрики здоровья и длительность
|
- Операционные метки сервиса, история пачек, метрики здоровья и длительность
|
||||||
тика остаются настенным временем, если отдельная задача не докажет обратное.
|
тика остаются настенным временем, если отдельная задача не докажет обратное.
|
||||||
- Контракт задачи 1 должен разделить, где повторяемость точная, а где
|
- Живой ход часов идёт фиксированным модельным шагом
|
||||||
статистическая: промотка прошлого должна быть точной, живой режим зависит от
|
`GEN_TICK_SECONDS * GEN_MODEL_TIME_SPEED`; промотка прошлого тоже повторяется
|
||||||
выбранного драйвера часов.
|
точно при тех же настройках и чистом состоянии.
|
||||||
- При ×K модельное время и событийный бюджет идут по модельной длительности
|
- При ×K модельное время и событийный бюджет идут по модельной длительности
|
||||||
тика, а не по реальной длительности сна процесса.
|
тика, а не по реальной длительности сна процесса.
|
||||||
- Часовой пояс модельных часов явно задан в контракте; дневной коэффициент
|
- Часовой пояс модельных часов явно задан в контракте; дневной коэффициент
|
||||||
@@ -49,7 +53,8 @@ Status: Draft
|
|||||||
настенного времени, а не простым `datetime.now()`.
|
настенного времени, а не простым `datetime.now()`.
|
||||||
- Стартовая история — это события плюс слепок состояния плюс манифест, чтобы не
|
- Стартовая история — это события плюс слепок состояния плюс манифест, чтобы не
|
||||||
смешать данные от разных `GEN_SEED`, `T0` и `T_end`.
|
смешать данные от разных `GEN_SEED`, `T0` и `T_end`.
|
||||||
- На стыке `[T0, T_end]` и живого продолжения не должно быть дублей и дыр.
|
- Стартовая история покрывает `[T0, T_end)`, живое продолжение начинается из
|
||||||
|
слепка на `T_end`; на стыке не должно быть дублей и дыр.
|
||||||
- Повторная проверка на чистом стенде должна быть воспроизводимой: либо команда
|
- Повторная проверка на чистом стенде должна быть воспроизводимой: либо команда
|
||||||
явно чистит ClickHouse, Kafka-топики данных и состояние генератора, либо
|
явно чистит ClickHouse, Kafka-топики данных и состояние генератора, либо
|
||||||
процесс идемпотентен.
|
процесс идемпотентен.
|
||||||
|
|||||||
+30
-15
@@ -19,32 +19,47 @@ Status: ready-for-human
|
|||||||
|
|
||||||
## Acceptance criteria
|
## Acceptance criteria
|
||||||
|
|
||||||
- [ ] Выбраны имена и формат настроек для `T0`, скорости ×K и режима промотки
|
- [x] Выбраны имена и формат настроек для `T0`, скорости ×K и режима промотки
|
||||||
прошлого.
|
прошлого.
|
||||||
- [ ] Описан драйвер живых часов: модельное время идёт фиксированным шагом
|
- [x] Описан живой ход часов: модельное время идёт фиксированным шагом
|
||||||
`tick_seconds * K` или по измеренному настенному интервалу. Для каждого режима
|
`tick_seconds * K`; измеренный настенный интервал между тиками не входит в
|
||||||
записано, что именно считается повторяемым.
|
текущий контракт.
|
||||||
- [ ] Разделены уровни повторяемости: точная повторяемость промотки прошлого и
|
- [x] Разделены уровни повторяемости: точная повторяемость промотки прошлого и
|
||||||
повторяемость живого режима по правилам выбранного драйвера часов.
|
точная повторяемость живого режима при тех же настройках и числе успешных
|
||||||
- [ ] Описано, как после сбоя вычисляется модельная точка возобновления при ×K:
|
тиков.
|
||||||
|
- [x] Описано, как после сбоя вычисляется модельная точка возобновления при ×K:
|
||||||
из сохранённой модельной метки, сохранённой настенной метки и скорости.
|
из сохранённой модельной метки, сохранённой настенной метки и скорости.
|
||||||
- [ ] Описано, что короткий и долгий простой считаются по модельному времени:
|
- [x] Описано, что короткий и долгий простой считаются по модельному времени:
|
||||||
при большом ×K короткий настенный простой может стать долгим модельным.
|
при большом ×K короткий настенный простой может стать долгим модельным.
|
||||||
- [ ] Описан манифест стартовой истории: минимум `GEN_SEED`, `T0`, `T_end`,
|
- [x] Описан манифест стартовой истории: минимум `GEN_SEED`, `T0`, `T_end`,
|
||||||
настройки генерации, версия state и контрольные числа.
|
настройки генерации, версия state и контрольные числа.
|
||||||
- [ ] Описана граница `T_end`: где заканчивается прошлое и с какой метки
|
- [x] Описана граница `T_end`: где заканчивается прошлое и с какой метки
|
||||||
начинается живое продолжение, без дублей и дыр.
|
начинается живое продолжение, без дублей и дыр.
|
||||||
- [ ] Разделены модельные метки событий и настенные операционные метки сервиса.
|
- [x] Разделены модельные метки событий и настенные операционные метки сервиса.
|
||||||
- [ ] Зафиксирован часовой пояс модельных часов для дневного коэффициента.
|
- [x] Зафиксирован часовой пояс модельных часов для дневного коэффициента.
|
||||||
- [ ] Решено, как координатор будет получать повторяемую ClickHouse-проверку:
|
- [x] Решено, как координатор будет получать повторяемую ClickHouse-проверку:
|
||||||
через очистку данных или идемпотентный прогон.
|
через очистку данных или идемпотентный прогон.
|
||||||
- [ ] Если выбран чистый прогон, перечислены поверхности сброса: таблицы
|
- [x] Если выбран чистый прогон, перечислены поверхности сброса: таблицы
|
||||||
ClickHouse, Kafka-топики данных и состояние генератора (`generator_state` или
|
ClickHouse, Kafka-топики данных и состояние генератора (`generator_state` или
|
||||||
явный сброс состояния при старте).
|
явный сброс состояния при старте).
|
||||||
- [ ] Контракт записан в durable-документ: обновление `PRD.md`, короткий
|
- [x] Контракт записан в durable-документ: обновление `PRD.md`, короткий
|
||||||
design note в `.scratch/generator-model-time-startup-history/` или уточнение
|
design note в `.scratch/generator-model-time-startup-history/` или уточнение
|
||||||
спеки. Сам issue 01 только ссылается на источник истины.
|
спеки. Сам issue 01 только ссылается на источник истины.
|
||||||
|
|
||||||
|
## Решение
|
||||||
|
|
||||||
|
Контракт записан в durable-документ:
|
||||||
|
`docs/specs/2026-06-14-generator-model-time-and-startup-history.md`, раздел
|
||||||
|
«Рабочий контракт реализации».
|
||||||
|
|
||||||
|
PRD ссылается на этот раздел в сквозных инвариантах. Для следующих задач важная
|
||||||
|
граница: стартовая история покрывает `[T0, T_end)`, а живое продолжение стартует
|
||||||
|
из слепка на `T_end`.
|
||||||
|
|
||||||
|
Дополнительную матрицу «фиксированный шаг / измеренная настенная дельта» не
|
||||||
|
вводим: ADR-0005 не требовала такого публичного выбора, а текущей учебной
|
||||||
|
цепочке нужен один повторяемый живой ход часов.
|
||||||
|
|
||||||
## Blocked by
|
## Blocked by
|
||||||
|
|
||||||
None - can start immediately
|
None - can start immediately
|
||||||
|
|||||||
+1
-1
@@ -19,7 +19,7 @@ Status: ready-for-agent
|
|||||||
|
|
||||||
## Acceptance criteria
|
## Acceptance criteria
|
||||||
|
|
||||||
- [ ] Промотка прошлого создаёт события за `[T0, T_end]`, слепок состояния и
|
- [ ] Промотка прошлого создаёт события за `[T0, T_end)`, слепок состояния и
|
||||||
манифест с контрольными данными.
|
манифест с контрольными данными.
|
||||||
- [ ] При одинаковых `GEN_SEED`, `T0`, `T_end` и настройках артефакт промотки
|
- [ ] При одинаковых `GEN_SEED`, `T0`, `T_end` и настройках артефакт промотки
|
||||||
прошлого повторяем точно; проверки в ClickHouse следуют правилам допуска из
|
прошлого повторяем точно; проверки в ClickHouse следуют правилам допуска из
|
||||||
|
|||||||
@@ -12,9 +12,9 @@
|
|||||||
[`06-state-v2-and-restart`](../../.scratch/feature-data-generator/issues/06-state-v2-and-restart.md)
|
[`06-state-v2-and-restart`](../../.scratch/feature-data-generator/issues/06-state-v2-and-restart.md)
|
||||||
(сохранение состояния построено на настенных часах).
|
(сохранение состояния построено на настенных часах).
|
||||||
|
|
||||||
Уровень документа — **модель и правила**, как в мат-спеке. Код, имена настроек,
|
Уровень документа — **модель и правила**, как в мат-спеке. Раздел «Рабочий
|
||||||
формат конфигурации и сам способ управления ходом часов — за исполнителем, в
|
контракт реализации» ниже фиксирует внешний интерфейс для задач 02–06. Внутренние
|
||||||
рамках правил этой спеки (как условились в ADR-0005).
|
классы, функции и разбиение кода остаются за исполнителем.
|
||||||
|
|
||||||
## Проблема
|
## Проблема
|
||||||
|
|
||||||
@@ -65,18 +65,144 @@ ADR-0005 решил отвязать время генератора от реа
|
|||||||
- **Не описываем здесь перестройку процесса** на новый сид (загрузка
|
- **Не описываем здесь перестройку процесса** на новый сид (загрузка
|
||||||
`kafka_load_dag`, витрины/Superset, уроки) — это решено в ADR-0006 и
|
`kafka_load_dag`, витрины/Superset, уроки) — это решено в ADR-0006 и
|
||||||
проектируется отдельно. Эта спека — только про сам генератор.
|
проектируется отдельно. Эта спека — только про сам генератор.
|
||||||
- **Не фиксируем** имена настроек, формат конфигурации и сам алгоритм — это за
|
- **Не фиксируем** внутренние имена классов и функций — это за исполнителем.
|
||||||
исполнителем.
|
|
||||||
- **Не добавляем** новые типы событий и инкрементальную загрузку ETL.
|
- **Не добавляем** новые типы событий и инкрементальную загрузку 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 и минимум содержит:
|
||||||
|
|
||||||
|
- `manifest_version`;
|
||||||
|
- `generated_at` — настенная UTC-метка создания артефакта;
|
||||||
|
- `gen_seed`;
|
||||||
|
- `model_t0`, `model_t_end`, `model_timezone`;
|
||||||
|
- `run_mode = "backfill"`;
|
||||||
|
- настройки генерации, влияющие на поток;
|
||||||
|
- `state_version` и ссылку на файл или запись слепка состояния;
|
||||||
|
- контрольные числа по каждому топику: количество строк, минимум и максимум
|
||||||
|
`event_timestamp`, контрольная сумма;
|
||||||
|
- итоговые контрольные числа для проверки в ClickHouse: события, визиты,
|
||||||
|
пользователи и диапазон модельного времени.
|
||||||
|
|
||||||
|
Нельзя смешивать события, state и манифест от разных `GEN_SEED`, `T0`, `T_end`
|
||||||
|
или настроек генерации. Такое смешивание считается ошибкой запуска.
|
||||||
|
|
||||||
|
#### Повторяемая проверка в ClickHouse
|
||||||
|
|
||||||
|
Для этой цепочки выбираем чистый прогон, а не идемпотентную дозаливку. Перед
|
||||||
|
повторной проверкой нужно сбросить:
|
||||||
|
|
||||||
|
- таблицы ClickHouse в слоях STG, ODS, DDS и DM, куда попадают события стенда;
|
||||||
|
- Kafka-топики данных: `browser_events`, `location_events`, `device_events`,
|
||||||
|
`geo_events`;
|
||||||
|
- состояние генератора: compact topic `generator_state` или явный сброс
|
||||||
|
состояния при старте, если он гарантированно не читает старую запись.
|
||||||
|
|
||||||
|
После такого сброса один и тот же артефакт стартовой истории должен давать те же
|
||||||
|
контрольные числа в ClickHouse. Если проверка запускается без чистки, это уже
|
||||||
|
другой сценарий и его нужно описывать отдельно.
|
||||||
|
|
||||||
### Точка отсчёта и скорость хода часов
|
### Точка отсчёта и скорость хода часов
|
||||||
|
|
||||||
У генератора своя точка отсчёта времени — **стартовая модельная точка `T0`**
|
У генератора своя точка отсчёта времени — **стартовая модельная точка `T0`**
|
||||||
(задаётся в настройках). В метку события (`event_timestamp`) пишется это
|
(задаётся в настройках). В метку события (`event_timestamp`) пишется это
|
||||||
внутреннее время. Скорость, с которой оно идёт относительно реальных часов,
|
внутреннее время. Скорость, с которой оно идёт относительно реальных часов,
|
||||||
переключается (в ADR-0005 — «драйвер часов»):
|
задаётся режимами из ADR-0005:
|
||||||
|
|
||||||
- **×1** — как реальное время;
|
- **×1** — как реальное время;
|
||||||
- **×K** — в `K` раз быстрее;
|
- **×K** — в `K` раз быстрее;
|
||||||
@@ -86,16 +212,15 @@ ADR-0005 решил отвязать время генератора от реа
|
|||||||
### Повторяемость
|
### Повторяемость
|
||||||
|
|
||||||
При одном и том же зерне `GEN_SEED`, одной и той же `T0` и одной скорости
|
При одном и том же зерне `GEN_SEED`, одной и той же `T0` и одной скорости
|
||||||
генератор каждый раз даёт **один и тот же поток**. Это убирает прежнюю оговорку
|
генератор каждый раз даёт **один и тот же поток** в `backfill` и в живом режиме
|
||||||
мат-спеки «запуск в другой час даёт другой поток»: дневной коэффициент
|
при одинаковом числе успешных тиков. Дневной коэффициент считается по модельному
|
||||||
(день/ночь) теперь считается по внутреннему времени от `T0`, а не по реальным
|
времени в `GEN_MODEL_TIMEZONE`, а не по реальным часам. Так раздел
|
||||||
часам. Так раздел «Воспроизводимость» мат-спеки приводится в соответствие с
|
«Воспроизводимость» мат-спеки приводится в соответствие с модельным временем.
|
||||||
модельным временем.
|
|
||||||
|
|
||||||
### Заливка прошлого и стартовая история
|
### Заливка прошлого и стартовая история
|
||||||
|
|
||||||
«Промотать» прошлое — значит прогнать время без пауз от `T0` до момента `T_end` и
|
«Промотать» прошлое — значит прогнать время без пауз от `T0` до момента `T_end` и
|
||||||
сгенерировать события этого отрезка. На выходе — события за `[T0, T_end]` и
|
сгенерировать события этого отрезка. На выходе — события за `[T0, T_end)` и
|
||||||
**слепок состояния** генератора на момент `T_end`.
|
**слепок состояния** генератора на момент `T_end`.
|
||||||
|
|
||||||
События плюс слепок и есть **стартовая история** («стартовый сид» или «новый сид» —
|
События плюс слепок и есть **стартовая история** («стартовый сид» или «новый сид» —
|
||||||
|
|||||||
Reference in New Issue
Block a user