feat(airflow): пульт мира — работники, выключатель, контейнер #83

Merged
ddmitry merged 2 commits from feat/79-world-control into main 2026-08-13 15:09:21 +03:00
Owner

Пульт мира по ADR 0009: три дага, которыми двигают ось модельного времени.

Closes #79

Что сделано

dags/world_control.py — три дага в одном файле: выключатель без своего работника не имеет смысла, и петля читается в один экран.

Даг Род Расписание Пауза при создании
world_next_day работник, день пачкой; «сколько дней» — параметр запуска нет нет
world_live_day работник, день в темпе модельного времени нет нет
world_live выключатель, своей работы нет 25 минут да

Генератор зовётся DockerOperator в образе clickstream-generator:local, в сети стенда, командой batch или live. Позицию ведёт переменная Airflow world_position — номер первого несыгранного дня; ставит её работник, сыгравший день, и только по успеху. Выключатель к переменной не прикасается.

Факты стенда — образ, сеть, брокер, топик, размер стартового мира — даги получают окружением от compose. Внутри compose они названы по разу якорями: с приходом пульта у каждого стало по два потребителя, и без якоря они однажды разошлись бы. Число дней стартового мира этим же якорем ушло из двух мест YAML в одно (второе, STARTING_DAYS в inventory.py, остаётся — YAML не читает Python).

Сокет докера отдан только планировщику: при LocalExecutor задачи исполняет он. Открывает дверь не монтирование, а group_add — у сокета права 660 и группа docker, чей GID уехал в .env как локальная настройка. Плата названа вслух в README и в «Быстром старте», потому что DOCKER_GID — единственное значение образца, неверное вне этой машины.

Ответы на «осталось проверить при исполнении» из ADR 0009

  • Версия провайдера docker. Ставить не пришлось: apache/airflow:3.3.0 несёт apache-airflow-providers-docker 4.5.7 сам. Записано в infra/airflow/Dockerfile, чтобы следующий не завёл второе место установки.
  • Имя сети стенда. Достаётся дагу без ручной подстановки: compose передаёт STAND_NETWORK: ${COMPOSE_PROJECT_NAME}_default, у второй копии стенда оно другое само собой.
  • Триггер доходит, дочерний прогон виден. У задачи выключателя работает ссылка «Triggered DAG»: /dags/world_live_day/runs/manual__2026-08-13T10:06:19.273030+00:00.
  • Два запуска подряд двигают позицию на два дня, обрыв — ни на один. Проверено, см. ниже.

Проверка

Цели make на чистом стенде: lint, config-test, smoke (20/0, 9 с), check-services (7/0, 46 с), check-clickhouse (9/9). Подъём с нуля — 2 м 20 с.

Прогон пульта — один раз при исполнении, постоянной целью не становится (карта проверок, «Интеграционная проверка постоянной целью не становится»):

  • Даги видны и разбираются. Три дага в списке, ошибок разбора нет. Работники не на паузе, выключатель на паузе — как задумано, прямо после make up.
  • Два запуска world_next_day подряд двинули позицию 8 → 9 → 10; дни D8 и D9 доехали до ODS.
  • «Дней = 3» сыграл дни 10, 11, 12 одним прогоном и поставил позицию 13; все три дня в ODS.
  • Обрыв. Контейнер живого дня убит на второй секунде: задача покраснела с DockerContainerFailedException: {'StatusCode': 137}, remember_played ушла в upstream_failed, позиция осталась 13. Повторный запуск сыграл тот же день 13 с тем же счётом событий (46 310) — детерминизм на месте.
  • Выключатель. Снят с паузы в 10:06 — сыграл два дня подряд без единого нажатия: 10:06:19–10:30:26 (24 м 07 с, позиция → 14) и 10:31:21–10:55:28 (24 м 06 с, позиция → 15). Пауза между днями — 55 секунд, ровно остаток до следующего тика. Поставлен на паузу в 10:31, посреди второго дня: день доигран до конца, тик 10:56:11 прошёл впустую, третьего прогона нет — мир встал на границе модельных суток.

Итог в ODS — пятнадцать дней вместо восьми, check-clickhouse при этом зелёный: подневная сверка утверждает только про стартовый мир.

Ревью

Две линии, обе субагентами. Взято: auto_remove переведён с success на force (у 4.5.7 удаление стоит в finally вопреки собственной документации — значение success работало правильно, но случайно); назван catchup=False; предупреждение про DOCKER_GID поднято в «Быстрый старт»; ответ про провайдер записан в образ; список томов планировщика получил пометку «правя одно, правьте второе».

Отклонено с доводами: якоря в compose — не попутный рефактор, каждый гасит дубль, который заводит этот же PR; {{ ti.xcom_pull(...) }} при переименовании ломается громко, а не молча — генератор отказывается стартовать без --day; doc_md=__doc__ не нужен, @dag берёт докстроку своей функции, и каждый даг показывает свою.

Перед приёмкой — проход на вычитание: срезаны три комментария, пересказывавших ADR.

Осталось решить

Тикет просит у выключателя «ожидание завершения и явные состояния отказа». Стоит только wait_for_completion=True: замером — провайдер сам подставляет allowed_states=[success] и failed_states=[failed], то есть ровно требуемое. Поведение из тикета выполнено, буква — нет. Скажешь — верну три строки.

Границы

Заказов, etl_pipeline, сенсоров и второго выключателя здесь нет; triggerer не заводится, deferrable не применяется.

Пульт мира по [ADR 0009](https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/adr/0009-world-control.md): три дага, которыми двигают ось модельного времени. Closes #79 ## Что сделано `dags/world_control.py` — три дага в одном файле: выключатель без своего работника не имеет смысла, и петля читается в один экран. | Даг | Род | Расписание | Пауза при создании | | --- | --- | --- | --- | | `world_next_day` | работник, день пачкой; «сколько дней» — параметр запуска | нет | нет | | `world_live_day` | работник, день в темпе модельного времени | нет | нет | | `world_live` | выключатель, своей работы нет | 25 минут | да | Генератор зовётся `DockerOperator` в образе `clickstream-generator:local`, в сети стенда, командой `batch` или `live`. Позицию ведёт переменная Airflow `world_position` — номер первого несыгранного дня; ставит её работник, сыгравший день, и только по успеху. Выключатель к переменной не прикасается. Факты стенда — образ, сеть, брокер, топик, размер стартового мира — даги получают окружением от compose. Внутри compose они названы по разу якорями: с приходом пульта у каждого стало по два потребителя, и без якоря они однажды разошлись бы. Число дней стартового мира этим же якорем ушло из двух мест YAML в одно (второе, `STARTING_DAYS` в `inventory.py`, остаётся — YAML не читает Python). Сокет докера отдан **только планировщику**: при LocalExecutor задачи исполняет он. Открывает дверь не монтирование, а `group_add` — у сокета права 660 и группа `docker`, чей GID уехал в `.env` как локальная настройка. Плата названа вслух в README и в «Быстром старте», потому что `DOCKER_GID` — единственное значение образца, неверное вне этой машины. ## Ответы на «осталось проверить при исполнении» из ADR 0009 - **Версия провайдера docker.** Ставить не пришлось: `apache/airflow:3.3.0` несёт `apache-airflow-providers-docker` 4.5.7 сам. Записано в `infra/airflow/Dockerfile`, чтобы следующий не завёл второе место установки. - **Имя сети стенда.** Достаётся дагу без ручной подстановки: compose передаёт `STAND_NETWORK: ${COMPOSE_PROJECT_NAME}_default`, у второй копии стенда оно другое само собой. - **Триггер доходит, дочерний прогон виден.** У задачи выключателя работает ссылка «Triggered DAG»: `/dags/world_live_day/runs/manual__2026-08-13T10:06:19.273030+00:00`. - **Два запуска подряд двигают позицию на два дня, обрыв — ни на один.** Проверено, см. ниже. ## Проверка Цели `make` на чистом стенде: `lint`, `config-test`, `smoke` (20/0, 9 с), `check-services` (7/0, 46 с), `check-clickhouse` (9/9). Подъём с нуля — 2 м 20 с. Прогон пульта — один раз при исполнении, постоянной целью не становится ([карта проверок](https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/testing.md), «Интеграционная проверка постоянной целью не становится»): - **Даги видны и разбираются.** Три дага в списке, ошибок разбора нет. Работники не на паузе, выключатель на паузе — как задумано, прямо после `make up`. - **Два запуска `world_next_day` подряд** двинули позицию 8 → 9 → 10; дни D8 и D9 доехали до ODS. - **«Дней = 3»** сыграл дни 10, 11, 12 одним прогоном и поставил позицию 13; все три дня в ODS. - **Обрыв.** Контейнер живого дня убит на второй секунде: задача покраснела с `DockerContainerFailedException: {'StatusCode': 137}`, `remember_played` ушла в `upstream_failed`, позиция осталась 13. Повторный запуск сыграл тот же день 13 с тем же счётом событий (46 310) — детерминизм на месте. - **Выключатель.** Снят с паузы в 10:06 — сыграл два дня подряд без единого нажатия: 10:06:19–10:30:26 (24 м 07 с, позиция → 14) и 10:31:21–10:55:28 (24 м 06 с, позиция → 15). Пауза между днями — 55 секунд, ровно остаток до следующего тика. Поставлен на паузу в 10:31, посреди второго дня: день доигран до конца, тик 10:56:11 прошёл впустую, третьего прогона нет — мир встал на границе модельных суток. Итог в ODS — пятнадцать дней вместо восьми, `check-clickhouse` при этом зелёный: подневная сверка утверждает только про стартовый мир. ## Ревью Две линии, обе субагентами. Взято: `auto_remove` переведён с `success` на `force` (у 4.5.7 удаление стоит в `finally` вопреки собственной документации — значение `success` работало правильно, но случайно); назван `catchup=False`; предупреждение про `DOCKER_GID` поднято в «Быстрый старт»; ответ про провайдер записан в образ; список томов планировщика получил пометку «правя одно, правьте второе». Отклонено с доводами: якоря в compose — не попутный рефактор, каждый гасит дубль, который заводит этот же PR; `{{ ti.xcom_pull(...) }}` при переименовании ломается громко, а не молча — генератор отказывается стартовать без `--day`; `doc_md=__doc__` не нужен, `@dag` берёт докстроку своей функции, и каждый даг показывает свою. Перед приёмкой — проход на вычитание: срезаны три комментария, пересказывавших ADR. ## Осталось решить Тикет просит у выключателя «ожидание завершения **и явные состояния отказа**». Стоит только `wait_for_completion=True`: замером — провайдер сам подставляет `allowed_states=[success]` и `failed_states=[failed]`, то есть ровно требуемое. Поведение из тикета выполнено, буква — нет. Скажешь — верну три строки. ## Границы Заказов, `etl_pipeline`, сенсоров и второго выключателя здесь нет; `triggerer` не заводится, `deferrable` не применяется.
ddmitry added 2 commits 2026-08-13 14:11:27 +03:00
Зачем: модельный день прогонялся только руками, и владельцу нечем было
проверять процессы стенда вживую. Пульт нужен раньше этапа 3 и независимо
от него: он обкатывает то, что приёму заказов понадобится готовым — вызов
генератора из задачи Airflow.

Что: `dags/world_control.py` — три дага по ADR 0009. Работники
`world_next_day` (день пачкой, «сколько дней» параметром) и `world_live_day`
(день в темпе) живут без расписания и без паузы; выключатель `world_live`
создаётся на паузе, тикает раз в 25 минут и дёргает работника живого дня
с ожиданием конца. Генератор зовётся `DockerOperator` в каноническом
контейнере: сокет докера отдан планировщику, потому что при LocalExecutor
задачи исполняет он, а GID группы `docker` уехал в `.env` как локальная
настройка. Позицию на оси ведёт переменная `world_position` — её ставит
сыгравший день работник и только по успеху. Факты стенда — образ, сеть,
брокер, топик, размер стартового мира — даги получают окружением от compose;
внутри compose они названы по разу якорями, иначе разошлись бы с разовой
службой генератора. README получил раздел про пульт с названной вслух платой
за сокет.

Проверка: `make lint`, `make config-test`, `make smoke` (20/0), `make
check-services` (7/0), `make check-clickhouse` (9/9) — зелёные. На чистом
стенде: два прогона `world_next_day` подряд двигают позицию на два дня,
«дней = 3» — на три, все пять дней доехали в ODS; обрыв контейнера позицию
не двигает, повторный запуск играет тот же день с тем же счётом событий.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Зачем: линия дефектов нашла опору на дефект провайдера, линия постановки —
незаписанный ответ на вопрос ADR 0009 и переменную образца, чьё значение на
чужой машине неверно, а узнаёт об этом читатель через двести строк.

Что: `auto_remove` у прогона генератора переведён с `success` на `force`.
Значение `success` тоже убирало контейнер в любом исходе, но случайно —
удаление у провайдера 4.5.7 стоит в `finally` вопреки собственной
документации; `force` то же поведение называет прямо и переживёт починку.
У выключателя назван `catchup=False`: умолчание Airflow 3 то же самое, но
у дага с тиком в 25 минут и `start_date` в январе это первый вопрос
читателя. «Быстрый старт» предупреждает про `DOCKER_GID` — единственное
значение образца, неверное вне этой машины. В образе Airflow записано, что
провайдер docker приходит с базой (4.5.7 к 3.3.0), — это ответ на вопрос,
который ADR 0009 оставил тикету. Список томов планировщика получил ту же
пометку «правя одно, правьте второе», что стоит у числа дней. Проход на
вычитание срезал три комментария, пересказывавших ADR.

Проверка: `make lint`, `make config-test`, `make smoke` (20/0),
`make check-clickhouse` (9/9) — зелёные. Живой пульт на чистом стенде:
снятый с паузы `world_live` сыграл два дня подряд без нажатия (24 м 07 с и
24 м 06 с, пауза между ними 55 секунд), дочерний прогон виден ссылкой из
задачи выключателя; пауза остановила мир на границе суток — тик прошёл,
третьего прогона нет. Контейнер после прогона с `force` не остался.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ddmitry merged commit f51048dd23 into main 2026-08-13 15:09:21 +03:00
ddmitry deleted branch feat/79-world-control 2026-08-13 15:09:23 +03:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: ddmitry/clickstream-data-platform#83