feat(airflow): пульт стал world_init, добавлен DAG world_next_day (#4)

- Зачем:
  - список DAG'ов должен читаться лесенкой ddl_init → world_init →
    world_next_day, а путь менти — проходиться пустыми формами
    (issue #4, спека редизайна пути менти, решения 2–3).
- Что:
  - generator_control переименован в world_init, дефолт операции —
    import; next-day ушёл из выпадашки в отдельный DAG;
  - новый беспараметрный world_next_day: расписание */30 * * * *,
    создаётся на паузе, catchup=False, max_active_runs=1; общие
    задачи вынесены в airflow/dags/utils/startup_history_tasks.py;
  - доки и контрактные тесты обновлены синхронно; быстрый старт
    README — без make ddl, схему создаёт DAG ddl_init.
- Проверка:
  - make test (210 + 31) и make lint зелёные;
  - живая приёмка на чистом стенде: world_init пустой формой
    импортировал эталонный мир за 217 с (3 дня, 280 437 событий),
    world_next_day после снятия с паузы добавляет ровно один день
    за прогон, дашборд Superset собирается.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-22 21:45:02 +03:00
co-authored by Claude Fable 5
parent 76ff669cf7
commit ac7a973504
12 changed files with 268 additions and 192 deletions
+28 -17
View File
@@ -27,8 +27,9 @@
flowchart LR
subgraph AF["Airflow"]
DAG1["ddl_init"]
DAG2["generator_control"]
DAG3["etl_pipeline"]
DAG2["world_init"]
DAG3["world_next_day"]
DAG4["etl_pipeline"]
end
subgraph GEN["Generator"]
@@ -57,7 +58,8 @@ flowchart LR
V[витрины VIEW]
end
DAG2 -->|startup-history backfill/import| K
DAG2 -->|import/backfill| K
DAG3 -->|следующий день| K
G -->|live после make generator-continue| K
K -->|MV| S
S -->|batch| O
@@ -66,12 +68,13 @@ flowchart LR
D1 & D2 -->|VIEW| V
DAG1 -.->|DDL| STG & ODS & DDS & DM
DAG3 -.->|batch| ODS & DDS
DAG4 -.->|batch| ODS & DDS
```
В учебном стенде предусмотрены два пути загрузки:
- `startup-history`: DAG `generator_control` создаёт или импортирует историю, добавляет следующий модельный день, запускает ETL и проверяет витрины;
- `startup-history`: `world_init` импортирует или создаёт историю, а
`world_next_day` добавляет один модельный день; оба запускают ETL и проверяют витрины;
- `live`: генератор запускается явно через `make generator-continue`, когда нужна непрерывная подача новых событий.
### Слои и их назначение
@@ -80,8 +83,9 @@ flowchart LR
flowchart LR
subgraph AF["Airflow"]
DAG1["ddl_init"]
DAG2["generator_control"]
DAG3["etl_pipeline"]
DAG2["world_init"]
DAG3["world_next_day"]
DAG4["etl_pipeline"]
end
subgraph GEN["Generator"]
@@ -106,7 +110,8 @@ flowchart LR
DM_T["VIEW"]
end
DAG2 -->|startup-history| KAFKA
DAG2 -->|стартовый мир| KAFKA
DAG3 -->|следующий день| KAFKA
G -->|live| KAFKA
KAFKA -->|MV| STG_T
STG_T -->|batch| ODS_T
@@ -114,7 +119,7 @@ flowchart LR
ODS_T -.->|ошибки| DQ
DAG1 -.->|DDL| L1 & L2 & L3 & L4
DAG3 -.->|batch| ODS_T & DDS_T
DAG4 -.->|batch| ODS_T & DDS_T
```
---
@@ -387,10 +392,12 @@ sequenceDiagram
CH-->>User: ✅ Структура БД создана
alt Startup-history режим
User->>Airflow: Trigger generator_control (backfill/import)
User->>Airflow: Trigger world_init с пустой формой
Airflow->>K: события стартовой истории
Airflow->>Airflow: trigger etl_pipeline + check
K-->>User: ✅ История в Kafka и витринах
User->>Airflow: Trigger world_next_day с пустой формой
Airflow->>K: события следующего модельного дня
else Live режим
User->>Compose: make generator-continue
loop каждые 1-10 секунд
@@ -637,7 +644,8 @@ INSERT INTO dm.daily_traffic SELECT * FROM dm.v_daily_traffic;
```python
# airflow/dags/ddl_init_dag.py — создание баз/таблиц
# airflow/dags/generator_control_dag.py — backfill/import/next-day/check стартовой истории
# airflow/dags/world_init_dag.py — import/backfill/check стартового мира
# airflow/dags/world_next_day_dag.py — добавление одного модельного дня
# airflow/dags/etl_pipeline_dag.py — основной ETL (STG→ODS→DDS→DM)
# airflow/dags/kafka_load_dag.py — архивный ручной путь из JSONL, не основной контур
@@ -645,22 +653,25 @@ INSERT INTO dm.daily_traffic SELECT * FROM dm.v_daily_traffic;
# - DDL и трансформации выполняются явными SQL-task через ClickHouseOperator;
# - SQL-файлы вызываются по фиксированным путям;
# - загрузка может идти двумя путями:
# 1) startup-history через DAG `generator_control`;
# 1) стартовый мир через `world_init` и рост через `world_next_day`;
# 2) live-поток через явный `make generator-continue`.
#
# Базовый demo-сценарий:
# ddl_init -> generator_control(backfill/import) -> etl_pipeline -> check
# ddl_init -> world_init(import) -> etl_pipeline -> check
# Расширенный учебный сценарий:
# make generator-continue + периодический etl_pipeline
```
**DAG `generator_control`**:
- `backfill`: создаёт стартовую историю через генератор
- `import`: импортирует портативный артефакт стартовой истории
- `next-day`: пакетно добавляет следующий модельный день от текущего слепка мира
**DAG `world_init`**:
- `import` по умолчанию импортирует портативный артефакт стартового мира
- `backfill` создаёт стартовую историю через генератор
- `check`: сверяет ClickHouse с manifest стартовой истории
- После `backfill` и `import` запускает `etl_pipeline` с `full_refresh`
**DAG `world_next_day`** без параметров пакетно добавляет следующий модельный
день, запускает `etl_pipeline` с `full_refresh` и сверяет manifest. У него задано
расписание каждые 30 минут, но DAG создаётся на паузе и не выполняет пропущенные интервалы.
**Подключение к ClickHouse:**
- Connection: `clickhouse_default`
- URL: `clickhouse://default:123456@clickhouse:9000/default` (native TCP для Airflow plugin)
+29 -25
View File
@@ -48,45 +48,48 @@ volumes или live-генератором. Для стыка backfill/live от
## Airflow DAGs
Штатный ручной путь начинается с `generator_control`: чистый стенд получает
стартовую историю генератора, затем этот же DAG запускает ETL и проверку.
Штатный ручной путь начинается с `world_init`: пустая форма импортирует
эталонный мир, затем этот же DAG запускает ETL и проверку.
`kafka_load` остаётся для экспериментов и совместимости учебного стенда.
### `generator_control`
### `world_init`
- Запуск: ручной (`Trigger DAG`).
- Назначение: пульт стартовой истории генератора.
- Назначение: импорт или служебная сборка стартового мира.
- Операции:
- `import` — операция по умолчанию: импортировать портативный артефакт, затем
запустить `etl_pipeline` и дождаться `success`;
- `backfill` — создать стартовую историю, затем запустить `etl_pipeline` и
дождаться `success`;
- `import` — импортировать портативный артефакт, затем запустить
`etl_pipeline` и дождаться `success`;
- `next-day` — восстановить мир из state, добавить 24 модельных часа,
затем запустить `etl_pipeline` и дождаться `success`;
- `check` — сверить ClickHouse с manifest из Kafka.
- Параметры:
- `operation` (`backfill` / `import` / `next-day` / `check`);
- `operation` (`import` / `backfill` / `check`);
- `profile` — список берётся из `PROFILES` генератора;
- `duration``6h`, `2d` и т.п.; пусто означает длительность профиля;
- `seed`, `model_time_speed` — необязательные переопределения мира;
- `artifact_path` — для `backfill` путь сохранения; для `import` путь чтения.
При пустом поле импортируется эталонный мир из репозитория;
- `expected_t_end` — необязательная ожидаемая граница перед `next-day`.
При расхождении запуск показывает ожидаемое и фактическое значения.
При пустом поле импортируется эталонный мир из репозитория.
Backfill/import требуют чистый стенд: пустые data-топики Kafka и пустые
`stg.*_raw`. При отказе очистите стенд через `make clean`. Операции `continue`
в DAG нет: live-генератор — долгоживущий сервис, его запускают с консоли через
`make generator-continue`.
`next-day` работает на непустом стенде и не использует проверку чистоты.
Перед записью пульт требует manifest, state ровно на его `T_end` и остановленный
live-генератор. Настройки мира берутся из manifest; поля `profile`, `duration`,
`seed` и `model_time_speed` формы для этой операции не применяются. Один запуск
добавляет полуоткрытый диапазон `[T_end, T_end + 24h)` в UTC. Новая граница
появляется в `boundaries`; старый manifest без поля читается как `[T0, T_end]`.
Расписание остаётся выключенным (`schedule=None`), а `max_active_runs=1` не даёт
двум доливкам выполняться параллельно.
### `world_next_day`
- Запуск: вручную с пустой формой.
- Параметров нет.
- Один запуск восстанавливает мир из state, добавляет 24 модельных часа,
запускает `etl_pipeline` с полной пересборкой и сверяет витрины с manifest.
- Расписание задано каждые 30 минут, но DAG создаётся на паузе; `catchup=False`.
Не включайте расписание до внедрения накопительных счётчиков manifest.
- `max_active_runs=1` не даёт двум доливкам выполняться параллельно.
`world_next_day` работает на непустом стенде и не использует проверку чистоты.
Перед записью он требует manifest, state ровно на его `T_end` и остановленный
live-генератор. Настройки мира берутся из manifest. Один запуск добавляет
полуоткрытый диапазон `[T_end, T_end + 24h)` в UTC. Новая граница появляется в
`boundaries`; старый manifest без поля читается как `[T0, T_end]`.
После двух доливок проверьте завершённые стыки:
@@ -98,10 +101,11 @@ make generated-history-chain-check
смену browser/referer/utm внутри переходящих визитов. Она не меняет
`make generated-history-runtime-check` для стыка backfill/live.
Точка фиксации `next-day` новый manifest. Порядок записи: data-топики, state,
manifest. Автоматического отката нет. Если запуск упал до публикации manifest,
Результат запуска `world_next_day` фиксируется новым manifest. Порядок записи:
data-топики, state, manifest. Автоматического отката нет. Если запуск упал до
публикации manifest,
не повторяйте доливку поверх возможного хвоста. Очистите стенд и переимпортируйте
последний исправный портативный артефакт, затем повторите `next-day`.
последний исправный портативный артефакт, затем повторите запуск `world_next_day`.
Текущая версия пересчитывает накопительные счётчики и контрольные суммы по всей
доступной истории data-топиков Kafka. Поэтому время выполнения и расход памяти
@@ -689,8 +693,8 @@ make generated-history-runtime-check
## Быстрые проверки
- Kafka ingest: наличие данных генератора в `stg.*` и типизированных строк в `ods.*`.
- Airflow UI: `http://localhost:8080` показывает DAG `ddl_init`, `generator_control`,
`kafka_load`, `etl_pipeline`; основной ручной пульт генератора — `generator_control`.
- Airflow UI: `http://localhost:8080` показывает лестницу `ddl_init`
`world_init``world_next_day`, а также `kafka_load` и `etl_pipeline`.
- BI: витрина `dm.v_events_enriched` отвечает за разумное время при фильтре по дате.
---
+3 -1
View File
@@ -7,7 +7,9 @@
### Airflow (ручной и учебный путь запуска)
- `airflow/dags/ddl_init_dag.py` — инициализация схемы ClickHouse
- `airflow/dags/generator_control_dag.py` — Airflow-пульт стартовой истории: backfill/import/next-day/check
- `airflow/dags/world_init_dag.py` — импорт или служебная сборка стартового мира и проверка витрин
- `airflow/dags/world_next_day_dag.py` — беспараметрное добавление одного модельного дня
- `airflow/dags/utils/startup_history_tasks.py` — общие задачи DAG для роста и проверки мира
- `airflow/dags/kafka_load_dag.py` — архивная загрузка в Kafka из JSONL; не основной источник аналитики
- `airflow/dags/etl_pipeline_dag.py` — ETL процесс STG -> ODS -> DDS -> DM
- `airflow/dags/utils/kafka_helpers.py` — helper-функции для Kafka
+1 -1
View File
@@ -16,7 +16,7 @@
- Для smoke и CI явно задаём служебный профиль `ci`.
- Полный прогон выполняем отдельно через `PROFILE=daily-wave`.
- Основной ручной путь запуска — через Airflow DAG `generator_control`.
- Основной ручной путь запуска — через Airflow DAG `world_init` с пустой формой.
- Консольный чистый прогон `make generated-history-analytics` остаётся коротким
повторяемым сценарием для smoke и CI.
- Критерий успеха: не только `Success` DAG, но и проверки данных/ошибок/мониторинга.
+12 -8
View File
@@ -58,9 +58,9 @@ git add data/startup_history/reference-world.json.xz
При импорте DAG дважды читает и распаковывает артефакт: во время предпроверки и
перед записью в Kafka. Это увеличивает время импорта, но не меняет результат.
Менти в форме `generator_control` выбирает `import` и оставляет
`artifact_path` пустым. Тогда читается эталонный мир из репозитория. Из консоли
тот же импорт запускается без указания пути:
Менти запускает `world_init` с пустой формой. Тогда читается эталонный мир из
репозитория. Следующий модельный день добавляет отдельный беспараметрный DAG
`world_next_day`. Из консоли тот же импорт запускается без указания пути:
```bash
make startup-history-import
@@ -68,20 +68,24 @@ make startup-history-import
## Пульт в Airflow
Основной ручной путь — DAG `generator_control` в Airflow UI:
Основной учебный путь в Airflow UI:
1. Поднимите стенд: `make up`.
2. Если DDL ещё не применён, запустите `ddl_init`.
3. Снимите паузу с `etl_pipeline`, если он ещё paused:
`docker compose exec -T airflow-webserver airflow dags unpause etl_pipeline`.
4. Откройте `generator_control` и выберите `operation`.
4. Запустите `world_init` с пустой формой. По умолчанию он импортирует эталонный мир.
5. Когда нужен ещё один модельный день, запустите `world_next_day` с пустой формой.
Операции:
`world_next_day` имеет расписание каждые 30 минут, но по умолчанию стоит на паузе.
Не включайте расписание до внедрения накопительных счётчиков manifest.
## Операции сопровождающего
В форме `world_init` сопровождающему дополнительно доступны операции:
- `backfill` — создать стартовую историю. После записи в Kafka DAG сам запускает
`etl_pipeline`, ждёт завершения и выполняет `check`.
- `import` — прочитать артефакт из `artifact_path`. Несовместимый артефакт
отклоняется до записи в Kafka.
- `check` — сверить ClickHouse с manifest из Kafka.
Поля формы: