diff --git a/.scratch/generator-model-time-startup-history/PRD.md b/.scratch/generator-model-time-startup-history/PRD.md index 53f7f9f..e9a7e68 100644 --- a/.scratch/generator-model-time-startup-history/PRD.md +++ b/.scratch/generator-model-time-startup-history/PRD.md @@ -88,16 +88,19 @@ Status: Draft узкое место — время человека на ручную приёмку; тяжелее всего проверять руками миграцию курса, поэтому она идёт последней, когда пульт уже есть): -- Основная ветка: `issues/12-...` (Airflow-DAG как пульт; после 11 вернуть на - дооформление — глаголы, профили, экспорт и импорт уже есть). -- Параллельно, независимы: `issues/09-...` (фикс браузерной фактуры на стыке) - и `issues/10-...` (читаемость гео-карты). +- Основная ветка: `issues/12-...` (Airflow-DAG как пульт; дооформлен + 2026-07-04: без Docker-доступа из Airflow, конечные операции — Python-код + генератора в тасках, live и полный сброс остаются консолью). +- Параллельно, независимы: `issues/09-...` (фикс браузерной фактуры на + стыке), `issues/10-...` (читаемость гео-карты) и `issues/14-...` (быстрый + учебный профиль: суточная волна вживую за ~24 минуты; заведён 2026-07-04, + лучше до 08 — артефакт курса рождается из этого профиля). - После фикса 09: `issues/13-...` (доливка истории от слепка; без приоритета, `needs-triage` — доливка тиражирует стыки восстановления, дооформлять после фикса). - Последней: `issues/08-...` (миграция курса на генерацию) — жёстко после 07 (уроки ссылаются на runbook), мягко после 12 (приёмку уроков человек ведёт - уже через пульт). + уже через пульт) и после 14 (артефакт курса — из быстрого профиля). ```mermaid flowchart LR @@ -105,6 +108,7 @@ flowchart LR i11 --> i12["12 DAG-пульт"] i07 --> i08["08 миграция курса"] i12 -. "приёмка через пульт" .-> i08 + i14["14 быстрый профиль"] --> i08 i09["09 фикс стыка"] --> i13["13 доливка"] i10["10 гео-карта"] ``` diff --git a/.scratch/generator-model-time-startup-history/issues/08-migrate-course-from-archive-seed.md b/.scratch/generator-model-time-startup-history/issues/08-migrate-course-from-archive-seed.md index 79889c3..1fe9c7c 100644 --- a/.scratch/generator-model-time-startup-history/issues/08-migrate-course-from-archive-seed.md +++ b/.scratch/generator-model-time-startup-history/issues/08-migrate-course-from-archive-seed.md @@ -67,6 +67,10 @@ startup-history/backfill -> Kafka -> STG -> ODS -> DDS -> DM -> Superset отдельный проект), шпаргалки удалены из репозитория 2026-07-04 (см. поправку в шапке `docs/course/PRD.md`). +Рекомендуемый режим ревью по coordinator-loop: обычный (документы и уроки, кода +генератора и state задача не трогает); человеческая приёмка — проход по урокам +через пульт (HITL). + ## Blocked by - Жёстких блокеров нет: штатный путь (`make generated-history-analytics`) уже @@ -79,3 +83,8 @@ startup-history/backfill -> Kafka -> STG -> ODS -> DDS -> DM -> Superset человеческая приёмка миграции — это проход по урокам 00–05, и вести её удобнее через DAG-пульт, а не через консоль. Поэтому эта задача идёт последней в очереди — после 12. Порядок всей очереди — в PRD, раздел «Задачи». +- Мягкая зависимость от `14-fast-teaching-profile.md` и + `09-seam-browser-fixture-not-preserved.md` (дополнено 2026-07-04): обе меняют + мир — 14 переводит профиль `daily-wave` на быстрый темп, 09 меняет схему + state. Артефакт стартовой истории для уроков должен родиться **после** них, + иначе он протухнет по антисмешиванию и проход по урокам придётся повторять. diff --git a/.scratch/generator-model-time-startup-history/issues/12-generator-control-dag.md b/.scratch/generator-model-time-startup-history/issues/12-generator-control-dag.md index c788d8f..f7cb5f1 100644 --- a/.scratch/generator-model-time-startup-history/issues/12-generator-control-dag.md +++ b/.scratch/generator-model-time-startup-history/issues/12-generator-control-dag.md @@ -1,4 +1,4 @@ -Status: needs-triage +Status: ready-for-agent # Airflow-DAG — пульт управления генератором @@ -11,45 +11,159 @@ Status: needs-triage Даже с runbook и глаголами управление генератором остаётся консольным. Идея (пользователь, 2026-06-14; приоритет поднят 2026-07-04): параметризованный DAG в Airflow как основной человеческий интерфейс стенда — форма в веб-UI, где -выбираются режим, длительность/профиль, seed, скорость. Плюсы: валидация -настроек против манифеста ещё до запуска, повторные попытки, наглядный статус. +выбираются операция, профиль, длительность. Плюсы: валидация настроек против +манифеста ещё до запуска, наглядный статус, понятные ошибки. Учебный бонус: такой DAG сам по себе учебный материал — живой пример параметризованной оркестрации (тема урока 4), что ближе к цели курса, чем устройство генератора. +## Решения (дооформление 2026-07-04) + +- **Без Docker-доступа из Airflow.** Docker socket в контейнеры Airflow не + прокидываем. Конечные операции — это обычный Python-код генератора, которому + нужны только настройки и Kafka; таски DAG выполняют его сами, в worker'е + Airflow. Урок из проекта `airflow-greenplum-solution` (пользователь): DAG + говорит с управляемой системой по сетевому протоколу, жизненный цикл + контейнеров — только хост/Makefile. +- **Границы пульта.** `make up`, `make clean` (полный сброс) и старт/стоп + live-сервиса — консоль; операции `continue` в пульте нет намеренно. + Backfill и import работают только на чистом стенде (пустые топики данных); + грязный стенд лечится `make clean` с консоли — пульт при отказе прямо + подсказывает это. +- **Генератор не автостартует.** Сейчас `make up` поднимает live-генератор + вместе со стендом (`docker-compose.yml`, `restart: unless-stopped`) — с + пультом это неверно: любой backfill был бы сразу отбит предпроверкой, а + выключить live из UI нельзя. Live становится явным действием + (`make generator-continue`); механизм — например, compose-профиль для + сервиса `generator`, выбрать при реализации и поправить make-цели. +- **Три операции, а не четыре.** `backfill` | `import` | `check`. Отдельного + `export` нет: артефакт в коде пишется только по ходу backfill + (`StartupHistoryArtifactBuilder` внутри `_run_backfill`), «выгрузки задним + числом» не существует — поэтому у backfill есть поле `artifact_path` + («сохранить артефакт», пусто — не сохранять). +- **Один DAG** с параметром «операция», а не несколько DAG по операциям. +- **После backfill/import DAG сам запускает ETL** (`TriggerDagRunOperator` на + существующий ETL-DAG) и **дожидается его завершения** перед check: + `wait_for_completion` в Airflow 2.10.5 по умолчанию `False` (сверено по + Context7, ревью Codex 2026-07-04) — простой trigger запустил бы check + раньше конца ETL. Менти видит цепочку «мир → пайплайн → витрины». + ## What to build -Черновой контур (уточнить после задач 07 и 11): +- **Код генератора доступен таскам Airflow.** Варианты: смонтировать + `generator/src` томом (как уже смонтирован `./sql`) и добавить в + `PYTHONPATH`, либо ставить пакет в `Dockerfile.airflow`. Монтировать нужно + во все Airflow-сервисы (scheduler, webserver, worker): список профилей в + форме читается из `PROFILES` при разборе DAG. Критерий выбора — правка + генератора не должна требовать лишних пересборок. +- **Выравнивание зависимостей:** версии сейчас расходятся — kafka-python + 2.0.5 у генератора против 2.0.6 в `airflow/requirements.txt`; привести к + одной. `prometheus-client` в образе Airflow нет — либо добавить, либо (лучше) + точка входа backfill не поднимает HTTP-сервер метрик (`service.py:65-66` + вызывается безусловно — понадобится небольшая склейка; это единственное + место, где честно появляется новый код, зафиксировать его в PR). +- **DAG `generator_control`** в `airflow/dags/`: `schedule=None`, + `max_active_runs=1`, форма запуска на `Param`: + - `operation`: `backfill` | `import` | `check` (выпадающий список); + - `profile`: из `PROFILES` (`launch.py`), список брать динамически; + - `duration`: строка вида `6h`/`2d`, пусто — из профиля; + - переопределения мира для backfill (минимум скорость и seed) — через + механизм `overrides` в `build_launch_env`; + - `artifact_path`: для backfill — куда сохранить артефакт (пусто — не + сохранять), для import — что импортировать; дефолт в общем томе `./data`. +- **Ветвление по операции** — `BranchPythonOperator`, в стиле существующих + DAG стенда. +- **Каждая операция начинается с валидации, падение — до любых записей:** + - backfill и import: топики данных пусты (переиспользовать + `KafkaTopicInspector.assert_data_topics_empty`) **и STG-таблицы + ClickHouse пусты** — батч-ETL пересобирает ODS из всего STG + (`sql/ods/20_stg_to_ods.sql`), поэтому пустых топиков мало: старый мир в + STG смешался бы с новым. При отказе в логе — подсказка про `make clean`; + - import дополнительно: артефакт читается, манифест совместим с выбранными + настройками (готовые проверки `startup_history_artifact.py`), в лог — + перечень разошедшихся полей; + - вспомогательно: проверка «live не работает» по метрикам + `generator:9109` внутри сети compose (см. Notes об ограничениях). +- **Тела операций — вызовы существующего кода:** `build_launch_env` + прогон + генерации до `T_end` (backfill), функции `startup_history_artifact` + (import), сверка контрольных чисел с манифестом (check). Источник манифеста + для check — compact-топик (он есть и после backfill, и после import); + артефакт — запасной вариант. +- `retries=0` у содержательных тасков: оператор должен сразу видеть ошибку + (проверенный приём из greenplum-проекта). +- **Документация:** в runbook `docs/runbooks/startup-history.md` — раздел + «Пульт в Airflow» как основной путь и явные границы пульта (что остаётся + консолью и почему, включая «continue в пульте нет намеренно»); тонкость + нестандартного мира: `make generator-continue` для мира с переопределёнными + настройками требует тех же настроек, громкий отказ подскажет разошедшиеся + поля. Ссылки из `docs/OPERATIONS.md` и `README.md`; правки compose и + Makefile описать в том же PR (правило репозитория). -- Параметризованный DAG с операциями: создать стартовую историю (backfill), - экспортировать артефакт, импортировать артефакт, сбросить мир. -- Валидация параметров против манифеста **до** запуска операции; при - несовместимости — понятная ошибка в UI, а не тихий новый мир. -- Конечные операции (backfill, экспорт, импорт, сброс) — таски DAG. - Непрерывный live — долгоживущий сервис compose: DAG его стартует и - останавливает, но не держит внутри таска. -- Под капотом DAG вызывает глаголы из задачи 11, а не собирает env-матрицу. +## Acceptance criteria -## Acceptance criteria (черновые, дооформить после 07 и 11) - -- [ ] Стартовую историю выбранной длительности можно создать из веб-UI Airflow, - не выставляя переменных окружения вручную. -- [ ] Несовместимые параметры отклоняются до запуска с указанием разошедшихся - полей. -- [ ] Импорт/экспорт артефакта доступны как операции DAG. -- [ ] Runbook дополнен разделом «пульт в Airflow» как основным путём. +- [ ] После `make up` (генератор не автостартует) стартовую историю выбранного + профиля/длительности можно создать из веб-UI Airflow, не открывая консоль и + не выставляя переменных окружения; следом ETL запускается из той же цепочки + и `check` зелёный. +- [ ] Импорт артефакта из веб-UI: несовместимый артефакт отклоняется **до** + записи в топики, в логе таска — перечень разошедшихся полей. +- [ ] Backfill/import на непустом стенде отклоняются предпроверкой с + подсказкой про `make clean`; миры не смешиваются. +- [ ] Артефакт, сохранённый при backfill из веб-UI, пригоден для консольного + импорта (формат один и тот же), и наоборот. +- [ ] Операция check сверяет контрольные числа ClickHouse с манифестом и + падает при расхождении. +- [ ] Docker недоступен из контейнеров Airflow (socket не монтируется) — это + граница решения, а не упущение. +- [ ] Консольные глаголы работают как раньше; `scripts/*` и DAG сходятся в + одном `launch.py`, дублирования логики запуска нет. +- [ ] `make up` больше не запускает live; `make generator-continue` запускает + его явно; существующие сценарии (`generated-history-analytics`, CI) не + сломаны. +- [ ] Runbook и `docs/OPERATIONS.md` описывают пульт как основной путь и его + границы. +- [ ] DAG остаётся читаемым менти: витрина параметризованной оркестрации, + сложность живёт в коде генератора. ## Notes -- Проверить API параметров DAG (params/Datasets) через MCP Context7 по правилу - репозитория: спорные API Airflow сверять с актуальной документацией. -- DAG должен остаться понятным менти: это витрина оркестрации, не место для - хитрой логики. Сложность — в глаголах генератора (задача 11), не здесь. +- Перед реализацией сверить API формы (`Param`, enum, описания полей) и + `TriggerDagRunOperator` для Airflow 2.10 через MCP Context7 — правило + репозитория. +- `Config` генератора читает env процесса при создании: в таске собирать + окружение через `build_launch_env` и применять к процессу таска (executor — + LocalExecutor, таск живёт в своём процессе). Не забыть `GEN_DATA_DIR`: + `build_launch_env` его не задаёт, дефолт `/data`, а в контейнерах Airflow + данные смонтированы в `/opt/airflow/data` — без явной установки `Config()` + упадёт, словари `*.jsonl` не найдутся. +- Права на файлы: артефакт из worker'а пишется uid'ом Airflow (50000); в + контейнере генератора `./data` смонтирован **read-only**, консольные + скрипты монтируют каталог артефактов отдельно. Чтение работает везде, + перезапись чужого файла — не всегда; одну строку об этом — в runbook. +- Проверка `generator:9109` — вспомогательная эвристика, не защита: ошибка + DNS означает «сервис не поднят» (не ошибку таска); при крашлупе после + громкого отказа порт мигает (HTTP-сервер стартует до валидации state); + эфемерные контейнеры `docker compose run` под этим именем не видны. Основная + защита от смешивания — пустота топиков данных. +- Backfill — блокирующий таск на минуты; при `max_active_runs=1` для ручного + стенда это нормально. +- Грабли greenplum-проекта, применимые здесь: DAG создаются на паузе — в + инструкции приёмки не забыть unpause перед trigger; автоматические прогоны + гонять через REST API, а не CLI внутри контейнера. +- Возможное развитие (не в скоупе): операция «очистить данные» из пульта + (прецедент удаления/пересоздания топиков из DAG уже есть в + `airflow/dags/utils/kafka_helpers.py`) — сняла бы требование `make clean` + перед новым миром. +- Артефакты `daily-wave`, сделанные до задачи 14, протухнут после неё + (антисмешивание отработает громко) — при пересечении работ это ожидаемо. +- Рекомендуемый режим ревью по coordinator-loop: обычный — state и + сериализацию задача не трогает, код генератора переиспользуется как есть. -## Blocked by +## Зависимости (проверенные предпосылки) -- `07-startup-history-portable-artifact-and-usage-docs.md` — операции - экспорта/импорта появляются там. -- `11-generator-launch-verbs-and-profiles.md` — DAG оборачивает глаголы и - профили; без них он превращается в переводчик формы в env-матрицу. +- `07-startup-history-portable-artifact-and-usage-docs.md` — сделана. +- `11-generator-launch-verbs-and-profiles.md` — сделана. +- Мягкая связь с `14-fast-teaching-profile.md`: быстрый профиль появится в + выпадашке сам, если список профилей брать из `PROFILES` динамически; + блокером не является. diff --git a/.scratch/generator-model-time-startup-history/issues/14-fast-teaching-profile.md b/.scratch/generator-model-time-startup-history/issues/14-fast-teaching-profile.md new file mode 100644 index 0000000..d38db91 --- /dev/null +++ b/.scratch/generator-model-time-startup-history/issues/14-fast-teaching-profile.md @@ -0,0 +1,94 @@ +Status: ready-for-agent + +# Быстрый учебный профиль: суточная волна за минуты занятия + +## Parent + +`.scratch/generator-model-time-startup-history/PRD.md` + +## Why + +Мир по умолчанию живёт со скоростью настенных часов (`speed=1`): чтобы +наблюдать суточную волну вживую, стенд должен непрерывно работать сутки. +Столько стенды включёнными не живут, и люди не вытерпят (пользователь, +2026-07-04). Стартовая история закрывает только половину проблемы: она даёт +закономерности **в статике**, но правый край графика ползёт 1:1 — на занятии +live выглядит мёртвой картинкой. + +Решение пользователя (2026-07-04): учебному миру нужна скорость 60 — модельный +час за настенную минуту, полная суточная волна проживается за ~24 минуты +занятия. + +## What to build + +- Перевести профиль `daily-wave` (`launch.py`) на `GEN_MODEL_TIME_SPEED=60` + при `GEN_TICK_SECONDS=1`. Модельный тик остаётся прежним: 1 настенная + секунда × 60 = 60 модельных секунд, поэтому фактура потока (крупность пачек, + бюджет тика до 72 событий в пике: 60/мин × 1.2) не отличается от + сегодняшней, и колпак `GEN_MAX_EVENTS_PER_TICK` трогать не нужно. +- Наивный вариант «speed=60 при тике 60 с» отвергнут при разборе + (2026-07-04): модельный тик стал бы часом; первые события всех визитов тика + получают один таймстемп — старт тика (`runtime.py:433`, + `generation.py:198`) — история слиплась бы в почасовые гребёнки, а пиковый + тик (4320 событий) упёрся бы в колпак 1000 и срезал бы верхушку волны. +- **Оценить цену тика в 1 Гц.** Сейчас каждый live-тик пишет state в + compact-топик, запись в историю пачек и несколько INFO-строк + (`service.py:566, 584-585, 591-600`): на тике 1 с это 86 400 сохранений и + ~полмиллиона строк лога за настенные сутки против 1 440 сегодня. Пер-тиковые + INFO приглушить до DEBUG (или логировать раз в N тиков); частоту сохранения + state менять только с оглядкой на спеку — она входит в контракт + возобновления после сбоя. +- Профиль `ci` не трогать: это дефолт CI, его мир и контрольные суммы должны + остаться прежними. +- Обновить документы, где живут предположения о старом `daily-wave`: + runbook `docs/runbooks/startup-history.md` (таблица профилей; продолжение + мира — `PROFILE=daily-wave make generator-continue`; старые артефакты + профиля становятся несовместимыми — антисмешивание отработает громко, это + ожидаемо), `docs/OPERATIONS.md` (smoke-последовательность со `sleep 130` — + ожидание двух минутных тиков — на тике 1 с теряет смысл), `README.md`, + `generator/README.md`. +- Спеку `docs/specs/2026-06-14-generator-model-time-and-startup-history.md` + дополнить: выбранная пара tick/speed и почему именно она. + +## Acceptance criteria + +- [ ] После backfill/импорта `daily-wave` и `PROFILE=daily-wave make + generator-continue` модельное время идёт ~в 60 раз быстрее настенного; на + дашборде суточная волна проживается за ~24 настенные минуты. +- [ ] Пиковые тики не упираются в `GEN_MAX_EVENTS_PER_TICK` — волна не + срезана. +- [ ] Стык «история → live» бесшовный: без дублей и дыр, антисмешивание + работает как раньше. +- [ ] Многочасовой live на тике 1 с не заливает журналы и compact-топики: + пер-тиковые INFO приглушены или их объём обоснован в PR; объём + state-записей (86 400/сутки) измерен и обоснован — либо частота сохранения + state осознанно изменена вместе с правкой спеки (контракт возобновления). +- [ ] Профиль `ci`, его контрольные суммы и поведение по умолчанию не + изменились; существующие тесты генератора проходят. +- [ ] Runbook, `docs/OPERATIONS.md`, README'и и спека обновлены. + +## Notes + +- Нагрузка по событиям: ~42–72 события в секунду (ночь–пик) — незаметно для + Kafka и ClickHouse; backfill не дорожает (число тиков на 2 суток то же, что + сейчас: модельный шаг тика не изменился). +- Если генерация тика займёт больше 1 настенной секунды, тики пойдут реже и + фактическая скорость окажется чуть ниже 60 (`service.py:623-627`) — это + допустимо, точная скорость не обещается. +- Несовместимость со старыми мирами ловится по-разному: импорт артефакта и + continue от стартовой истории сверяют полный набор настроек (включая + `tick_seconds` в манифесте), а обычный live-continue — только + seed/T0/timezone/speed (`service.py:213-230`); для этой правки хватает и + его — speed изменился. +- Альтернатива «третий профиль fast-live» отвергнута: у медленного + `daily-wave` не остаётся сценария использования, а лишний профиль усложняет + выпадашку пульта (задача 12). +- Рекомендуемый режим ревью по coordinator-loop: обычный (значения профиля и + логирование). Исключение: если по ходу решат менять частоту сохранения + state — это уже контракт возобновления, эскалировать в **гейт**. + +## Blocked by + +Нет: профили из задачи 11 сделаны. Мягкие связи: задача 12 (профиль появится +в форме пульта автоматически) и задача 08 (артефакт курса рождается из этого +профиля — эту задачу лучше сделать до него).