- Зачем:
- follow-up задачи после ревью 2026-06-14 лежали без триажа; решения по
артефакту, громкому отказу и интерфейсу приняты 2026-07-04 и должны
попасть в задачи до передачи исполнителю.
- Что:
- задачи 07 (миграция курса) и 08 (артефакт) поменяны местами — номера
отражают порядок; ссылки обновлены.
- 07 (артефакт + runbook) дооформлен: импорт строго через Kafka (напрямую
в ClickHouse не пишет), громкий отказ при несовместимом state с правкой
спеки, граница runbook «использование, не устройство»; ready-for-agent.
- 08 дооформлен: устройство генератора вне пути менти, реальный объём
(make data во всех уроках 00-05), демо вне скоупа; ready-for-agent.
- новые задачи: 11 глаголы/длительность/профили (после 07, до 12),
12 Airflow-DAG как пульт (приоритет поднят), 13 доливка (после 09).
- задачи 01-06 переведены в done (стояли ошибочные ready-for-human);
PRD фичи дополнен списком задач 7-13 с порядком и зависимостями.
- Проверка:
- head -1 .scratch/generator-model-time-startup-history/issues/*.md;
grep по старым именам файлов ничего не находит вне handoff.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
115 lines
8.4 KiB
Markdown
115 lines
8.4 KiB
Markdown
Status: done
|
||
|
||
# Восстановление state v2 от модельной точки
|
||
|
||
## Parent
|
||
|
||
`.scratch/generator-model-time-startup-history/PRD.md`
|
||
|
||
## What to build
|
||
|
||
Привести восстановление state v2 к модельному времени. Один и тот же слепок
|
||
должен уметь восстанавливаться в двух разных случаях: после сбоя, где время
|
||
действительно прошло, и при старте из стартовой истории, где продолжение идёт
|
||
от `T_end` без искусственного разрыва.
|
||
|
||
Срез должен доказать поведение не только локальными тестами состояния, но и
|
||
данными в ClickHouse: активные визиты продолжаются или закрываются по правилам
|
||
модельного времени.
|
||
|
||
## Acceptance criteria
|
||
|
||
- [x] State сохраняет достаточно данных, чтобы после сбоя вычислить модельную
|
||
точку возобновления при ×K.
|
||
- [x] Восстановление после короткого модельного сбоя продолжает активные визиты
|
||
и досылает созревшие события с исходными модельными метками.
|
||
- [x] Восстановление после долгого модельного сбоя закрывает сильно просроченные
|
||
активные визиты без досылки остатка.
|
||
- [x] Восстановление из стартовой истории использует `T_end` как точку
|
||
возобновления и не обрывает активные визиты из-за настенного простоя.
|
||
- [x] Визит, переживший восстановление, остаётся однородным: события до и после
|
||
восстановления не меняют контекст визита и не создают второй путь генерации
|
||
внутри одного `click_id`.
|
||
- [x] Повреждённый или несовместимый state не валит сервис: генератор стартует
|
||
с чистого листа и пишет предупреждение.
|
||
- [x] ClickHouse-проверка подтверждает, что на стыке восстановления нет дублей
|
||
событий и нет разрыва `click_id` внутри продолжающегося визита.
|
||
- [x] После реализации выполнено саморевью worker-а и отдельное reviewer-ревью,
|
||
потому что задача меняет state/serialization и сервисное восстановление.
|
||
- [x] Если исправление после ревью меняет формат state или способ восстановления,
|
||
выполнен повторный reviewer-круг.
|
||
|
||
## Blocked by
|
||
|
||
- `.scratch/generator-model-time-startup-history/issues/03-model-speed-and-day-factor.md`
|
||
- Review gate из `PRD.md`: сквозной инвариант времени после задачи 3
|
||
|
||
## Решение
|
||
|
||
- State v2 теперь хранит `model_timestamp`, `wall_timestamp`,
|
||
`model_time_speed`, `model_timezone`, `model_t0` и `gen_seed`.
|
||
- Старый v2 без этих полей считается несовместимым: `from_dict_safe` возвращает
|
||
`None`, сервис пишет предупреждение и стартует с чистого состояния.
|
||
- Live-восстановление считает точку продолжения по формуле
|
||
`model_timestamp + max(0, wall_now_utc - wall_timestamp) * model_time_speed`.
|
||
- Перед live-восстановлением сервис сверяет state с текущим `Config` по
|
||
`gen_seed`, `model_t0`, `model_timezone` и `model_time_speed`. Несовпадение
|
||
считается несовместимым state: сервис пишет warning и стартует с чистого
|
||
состояния.
|
||
- Восстановление из стартовой истории вынесено в минимальный метод
|
||
`GeneratorService.restore_from_startup_history(state, model_t_end)`: он берёт
|
||
переданный `T_end` напрямую и не применяет wall-дельту.
|
||
- Сохранение state после успешного тика пишет модельную точку следующего тика,
|
||
а не настенное время. Это убирает повторный старт с уже обработанной модельной
|
||
точки.
|
||
|
||
## Проверки
|
||
|
||
- Точечный прогон после проверки reviewer-а:
|
||
`docker run --rm -v ... generator:test pytest tests/test_state.py::TestGeneratorState tests/test_state.py::TestGeneratorStateValidation::test_from_dict_safe_returns_state_on_valid tests/test_state.py::TestGeneratorStateValidation::test_from_dict_safe_returns_none_on_invalid_gen_seed tests/test_service.py::TestGeneratorServiceStateV2 -v`
|
||
— 15 passed.
|
||
- `make generator-test` — 126 passed.
|
||
- `uv run pytest ...` и `uv run python -m pytest ...` не запускались до тестов:
|
||
в текущем uv-окружении нет `pytest`.
|
||
- `make lint` недоступен: в `Makefile` нет такой цели.
|
||
|
||
## ClickHouse-проверка
|
||
|
||
Выполнены два чистых стендовых сценария. Оба начинались со сброса ClickHouse,
|
||
Kafka и state генератора.
|
||
|
||
1. Короткое восстановление, `GEN_MODEL_TIME_SPEED=1`.
|
||
Первый запуск с `GEN_STATE_RESET=true` записал один тик, второй запуск с
|
||
`GEN_STATE_RESET=false` восстановился из state и продолжил активные визиты.
|
||
Проверка ClickHouse:
|
||
- `dds.event`: `rows=332`, `uniq_events=332`, `duplicate_events=0`.
|
||
- `continued_from_first_tick=67` для визитов, начатых на
|
||
`2026-01-01 10:00:00`.
|
||
- Для этих `click_id` нет смены `user_domain_id`, `device_type`, `os_name`,
|
||
`geo_country`, `geo_region_name` и `geo_timezone`.
|
||
2. Долгое восстановление, `GEN_MODEL_TIME_SPEED=3600`.
|
||
Первый запуск с `GEN_STATE_RESET=true` записал один тик на
|
||
`2026-01-01 10:00:00`, второй запуск с `GEN_STATE_RESET=false`
|
||
восстановился на модельной точке `2026-01-03T22:51:51.552000+00:00`.
|
||
Проверка ClickHouse:
|
||
- `dds.event`: `rows=200`, `uniq_events=200`, `duplicate_events=0`.
|
||
- диапазон `event_ts`: от `2026-01-01 10:00:00.000000` до
|
||
`2026-01-03 22:51:51.552000`.
|
||
- `continued_old_clicks=0`: старые `click_id` с первого тика не продолжились
|
||
после долгой модельной паузы.
|
||
|
||
## Риски
|
||
|
||
- Старые записи state v2 без модельной связки больше не восстанавливаются. Это
|
||
намеренно: без этих полей нельзя корректно посчитать точку продолжения.
|
||
- Полноценный манифест и backfill не реализованы: добавлен только минимальный
|
||
интерфейс восстановления от переданного `T_end`.
|
||
- `restore_from_startup_history` пока принимает state и `T_end` напрямую, без
|
||
проверки manifest. Это риск issue 05, где появится полноценная стартовая
|
||
история и запрет смешивания артефактов.
|
||
- `GeneratorState.__post_init__` оставляет in-memory совместимость для прямого
|
||
создания state в тестах и коде. JSON/Kafka путь остаётся строгим: отсутствие
|
||
новых полей не маскируется и ведёт к fresh start.
|
||
- После reviewer-fix формат state снова изменился (`gen_seed`). Повторный
|
||
reviewer-круг выполнен, новых блокирующих находок нет.
|