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:
2026-07-04 20:42:46 +03:00
co-authored by Claude Fable 5
parent 0a240e3077
commit c672ed0329
4 changed files with 255 additions and 34 deletions
@@ -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` динамически;
блокером не является.