From 31a71f93eac0c7e4b9c11bfe9c79743a92d02728 Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Sun, 14 Jun 2026 16:30:15 +0300 Subject: [PATCH] =?UTF-8?q?docs(generator):=20=D0=B7=D0=B0=D1=84=D0=B8?= =?UTF-8?q?=D0=BA=D1=81=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BD=D1=82=D1=80=D0=B0=D0=BA=D1=82=20=D0=BC=D0=BE=D0=B4=D0=B5?= =?UTF-8?q?=D0=BB=D1=8C=D0=BD=D0=BE=D0=B3=D0=BE=20=D0=B2=D1=80=D0=B5=D0=BC?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - нужно закрепить принятое человеком HITL-решение до кодовых задач 02-06. - Что: - добавлен рабочий контракт настроек, хода часов, state, манифеста и ClickHouse-проверки. - выбран один живой ход часов через фиксированный модельный шаг без отдельной матрицы драйверов. - уточнена граница стартовой истории как [T0, T_end) и закрыт чек-лист issue 01. - Проверка: - git diff --cached --check. --- .../PRD.md | 13 +- .../01-time-and-startup-history-contract.md | 45 ++++-- ...-startup-history-backfill-to-clickhouse.md | 2 +- ...enerator-model-time-and-startup-history.md | 149 ++++++++++++++++-- 4 files changed, 177 insertions(+), 32 deletions(-) diff --git a/.scratch/generator-model-time-startup-history/PRD.md b/.scratch/generator-model-time-startup-history/PRD.md index 971beab..ef1a66f 100644 --- a/.scratch/generator-model-time-startup-history/PRD.md +++ b/.scratch/generator-model-time-startup-history/PRD.md @@ -35,12 +35,16 @@ Status: Draft ## Сквозные инварианты +- Рабочий контракт реализации живёт в + `docs/specs/2026-06-14-generator-model-time-and-startup-history.md`, раздел + «Рабочий контракт реализации». Задачи 02–06 берут имена настроек, формат + манифеста, границу `T_end` и правила проверки оттуда. - `event_timestamp` — модельное время, а не настенные часы компьютера. - Операционные метки сервиса, история пачек, метрики здоровья и длительность тика остаются настенным временем, если отдельная задача не докажет обратное. -- Контракт задачи 1 должен разделить, где повторяемость точная, а где - статистическая: промотка прошлого должна быть точной, живой режим зависит от - выбранного драйвера часов. +- Живой ход часов идёт фиксированным модельным шагом + `GEN_TICK_SECONDS * GEN_MODEL_TIME_SPEED`; промотка прошлого тоже повторяется + точно при тех же настройках и чистом состоянии. - При ×K модельное время и событийный бюджет идут по модельной длительности тика, а не по реальной длительности сна процесса. - Часовой пояс модельных часов явно задан в контракте; дневной коэффициент @@ -49,7 +53,8 @@ Status: Draft настенного времени, а не простым `datetime.now()`. - Стартовая история — это события плюс слепок состояния плюс манифест, чтобы не смешать данные от разных `GEN_SEED`, `T0` и `T_end`. -- На стыке `[T0, T_end]` и живого продолжения не должно быть дублей и дыр. +- Стартовая история покрывает `[T0, T_end)`, живое продолжение начинается из + слепка на `T_end`; на стыке не должно быть дублей и дыр. - Повторная проверка на чистом стенде должна быть воспроизводимой: либо команда явно чистит ClickHouse, Kafka-топики данных и состояние генератора, либо процесс идемпотентен. diff --git a/.scratch/generator-model-time-startup-history/issues/01-time-and-startup-history-contract.md b/.scratch/generator-model-time-startup-history/issues/01-time-and-startup-history-contract.md index dc7674b..e09c5cf 100644 --- a/.scratch/generator-model-time-startup-history/issues/01-time-and-startup-history-contract.md +++ b/.scratch/generator-model-time-startup-history/issues/01-time-and-startup-history-contract.md @@ -19,32 +19,47 @@ Status: ready-for-human ## Acceptance criteria -- [ ] Выбраны имена и формат настроек для `T0`, скорости ×K и режима промотки +- [x] Выбраны имена и формат настроек для `T0`, скорости ×K и режима промотки прошлого. -- [ ] Описан драйвер живых часов: модельное время идёт фиксированным шагом - `tick_seconds * K` или по измеренному настенному интервалу. Для каждого режима - записано, что именно считается повторяемым. -- [ ] Разделены уровни повторяемости: точная повторяемость промотки прошлого и - повторяемость живого режима по правилам выбранного драйвера часов. -- [ ] Описано, как после сбоя вычисляется модельная точка возобновления при ×K: +- [x] Описан живой ход часов: модельное время идёт фиксированным шагом + `tick_seconds * K`; измеренный настенный интервал между тиками не входит в + текущий контракт. +- [x] Разделены уровни повторяемости: точная повторяемость промотки прошлого и + точная повторяемость живого режима при тех же настройках и числе успешных + тиков. +- [x] Описано, как после сбоя вычисляется модельная точка возобновления при ×K: из сохранённой модельной метки, сохранённой настенной метки и скорости. -- [ ] Описано, что короткий и долгий простой считаются по модельному времени: +- [x] Описано, что короткий и долгий простой считаются по модельному времени: при большом ×K короткий настенный простой может стать долгим модельным. -- [ ] Описан манифест стартовой истории: минимум `GEN_SEED`, `T0`, `T_end`, +- [x] Описан манифест стартовой истории: минимум `GEN_SEED`, `T0`, `T_end`, настройки генерации, версия state и контрольные числа. -- [ ] Описана граница `T_end`: где заканчивается прошлое и с какой метки +- [x] Описана граница `T_end`: где заканчивается прошлое и с какой метки начинается живое продолжение, без дублей и дыр. -- [ ] Разделены модельные метки событий и настенные операционные метки сервиса. -- [ ] Зафиксирован часовой пояс модельных часов для дневного коэффициента. -- [ ] Решено, как координатор будет получать повторяемую ClickHouse-проверку: +- [x] Разделены модельные метки событий и настенные операционные метки сервиса. +- [x] Зафиксирован часовой пояс модельных часов для дневного коэффициента. +- [x] Решено, как координатор будет получать повторяемую ClickHouse-проверку: через очистку данных или идемпотентный прогон. -- [ ] Если выбран чистый прогон, перечислены поверхности сброса: таблицы +- [x] Если выбран чистый прогон, перечислены поверхности сброса: таблицы ClickHouse, Kafka-топики данных и состояние генератора (`generator_state` или явный сброс состояния при старте). -- [ ] Контракт записан в durable-документ: обновление `PRD.md`, короткий +- [x] Контракт записан в durable-документ: обновление `PRD.md`, короткий design note в `.scratch/generator-model-time-startup-history/` или уточнение спеки. Сам 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 None - can start immediately diff --git a/.scratch/generator-model-time-startup-history/issues/05-startup-history-backfill-to-clickhouse.md b/.scratch/generator-model-time-startup-history/issues/05-startup-history-backfill-to-clickhouse.md index ec2eb8b..e7d01da 100644 --- a/.scratch/generator-model-time-startup-history/issues/05-startup-history-backfill-to-clickhouse.md +++ b/.scratch/generator-model-time-startup-history/issues/05-startup-history-backfill-to-clickhouse.md @@ -19,7 +19,7 @@ Status: ready-for-agent ## Acceptance criteria -- [ ] Промотка прошлого создаёт события за `[T0, T_end]`, слепок состояния и +- [ ] Промотка прошлого создаёт события за `[T0, T_end)`, слепок состояния и манифест с контрольными данными. - [ ] При одинаковых `GEN_SEED`, `T0`, `T_end` и настройках артефакт промотки прошлого повторяем точно; проверки в ClickHouse следуют правилам допуска из diff --git a/docs/specs/2026-06-14-generator-model-time-and-startup-history.md b/docs/specs/2026-06-14-generator-model-time-and-startup-history.md index d3179bf..ba860e4 100644 --- a/docs/specs/2026-06-14-generator-model-time-and-startup-history.md +++ b/docs/specs/2026-06-14-generator-model-time-and-startup-history.md @@ -12,9 +12,9 @@ [`06-state-v2-and-restart`](../../.scratch/feature-data-generator/issues/06-state-v2-and-restart.md) (сохранение состояния построено на настенных часах). -Уровень документа — **модель и правила**, как в мат-спеке. Код, имена настроек, -формат конфигурации и сам способ управления ходом часов — за исполнителем, в -рамках правил этой спеки (как условились в ADR-0005). +Уровень документа — **модель и правила**, как в мат-спеке. Раздел «Рабочий +контракт реализации» ниже фиксирует внешний интерфейс для задач 02–06. Внутренние +классы, функции и разбиение кода остаются за исполнителем. ## Проблема @@ -65,18 +65,144 @@ ADR-0005 решил отвязать время генератора от реа - **Не описываем здесь перестройку процесса** на новый сид (загрузка `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 и минимум содержит: + +- `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`** (задаётся в настройках). В метку события (`event_timestamp`) пишется это внутреннее время. Скорость, с которой оно идёт относительно реальных часов, -переключается (в ADR-0005 — «драйвер часов»): +задаётся режимами из ADR-0005: - **×1** — как реальное время; - **×K** — в `K` раз быстрее; @@ -86,16 +212,15 @@ ADR-0005 решил отвязать время генератора от реа ### Повторяемость При одном и том же зерне `GEN_SEED`, одной и той же `T0` и одной скорости -генератор каждый раз даёт **один и тот же поток**. Это убирает прежнюю оговорку -мат-спеки «запуск в другой час даёт другой поток»: дневной коэффициент -(день/ночь) теперь считается по внутреннему времени от `T0`, а не по реальным -часам. Так раздел «Воспроизводимость» мат-спеки приводится в соответствие с -модельным временем. +генератор каждый раз даёт **один и тот же поток** в `backfill` и в живом режиме +при одинаковом числе успешных тиков. Дневной коэффициент считается по модельному +времени в `GEN_MODEL_TIMEZONE`, а не по реальным часам. Так раздел +«Воспроизводимость» мат-спеки приводится в соответствие с модельным временем. ### Заливка прошлого и стартовая история «Промотать» прошлое — значит прогнать время без пауз от `T0` до момента `T_end` и -сгенерировать события этого отрезка. На выходе — события за `[T0, T_end]` и +сгенерировать события этого отрезка. На выходе — события за `[T0, T_end)` и **слепок состояния** генератора на момент `T_end`. События плюс слепок и есть **стартовая история** («стартовый сид» или «новый сид» —