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` не применяется.
Зачем: модельный день прогонялся только руками, и владельцу нечем было
проверять процессы стенда вживую. Пульт нужен раньше этапа 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 main2026-08-13 15:09:21 +03:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Пульт мира по ADR 0009: три дага, которыми двигают ось модельного времени.
Closes #79
Что сделано
dags/world_control.py— три дага в одном файле: выключатель без своего работника не имеет смысла, и петля читается в один экран.world_next_dayworld_live_dayworld_liveГенератор зовётся
DockerOperatorв образеclickstream-generator:local, в сети стенда, командойbatchилиlive. Позицию ведёт переменная Airflowworld_position— номер первого несыгранного дня; ставит её работник, сыгравший день, и только по успеху. Выключатель к переменной не прикасается.Факты стенда — образ, сеть, брокер, топик, размер стартового мира — даги получают окружением от compose. Внутри compose они названы по разу якорями: с приходом пульта у каждого стало по два потребителя, и без якоря они однажды разошлись бы. Число дней стартового мира этим же якорем ушло из двух мест YAML в одно (второе,
STARTING_DAYSвinventory.py, остаётся — YAML не читает Python).Сокет докера отдан только планировщику: при LocalExecutor задачи исполняет он. Открывает дверь не монтирование, а
group_add— у сокета права 660 и группаdocker, чей GID уехал в.envкак локальная настройка. Плата названа вслух в README и в «Быстром старте», потому чтоDOCKER_GID— единственное значение образца, неверное вне этой машины.Ответы на «осталось проверить при исполнении» из ADR 0009
apache/airflow:3.3.0несётapache-airflow-providers-docker4.5.7 сам. Записано вinfra/airflow/Dockerfile, чтобы следующий не завёл второе место установки.STAND_NETWORK: ${COMPOSE_PROJECT_NAME}_default, у второй копии стенда оно другое само собой./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.DockerContainerFailedException: {'StatusCode': 137},remember_playedушла вupstream_failed, позиция осталась 13. Повторный запуск сыграл тот же день 13 с тем же счётом событий (46 310) — детерминизм на месте.Итог в 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не применяется.