docs(issues): дооформлены задачи 12 и 14, обновлён граф бэклога генератора
- Зачем:
- подготовить цепочку 12 -> 14 -> 09 -> 10 -> 08 к передаче в coordinator-loop:
задаче 12 не хватало решений после 07 и 11, задача 14 родилась из обсуждения
учебного UX (суточная волна вживую).
- Что:
- issue 12 (DAG-пульт): решения 2026-07-04 — без Docker-доступа из Airflow,
операции как Python-код генератора в тасках, генератор без автостарта,
границы пульта, предпроверки чистоты (топики + STG), ожидание ETL перед
check; учтены находки адверсарного ревью и ревью Codex.
- issue 14 (новая): профиль daily-wave переводится на speed=60 при тике 1 с —
модельный час за настенную минуту без изменения фактуры мира.
- issue 08: мягкие зависимости от 14 и 09 (артефакт курса рождается после
них), режим ревью; PRD: задача 14 в списке и графе, 12 помечена дооформленной.
- Проверка:
- вычитка: статусы задач ready-for-agent, порядок в PRD и Blocked by
согласованы; mermaid-граф рендерится.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+9
@@ -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. Артефакт стартовой истории для уроков должен родиться **после** них,
|
||||
иначе он протухнет по антисмешиванию и проход по урокам придётся повторять.
|
||||
|
||||
@@ -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` динамически;
|
||||
блокером не является.
|
||||
|
||||
@@ -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 (артефакт курса рождается из этого
|
||||
профиля — эту задачу лучше сделать до него).
|
||||
Reference in New Issue
Block a user