From 292302e161951baf2a0ca1ec70115d6b9a878c48 Mon Sep 17 00:00:00 2001 From: Dmitry Dementiev Date: Thu, 13 Aug 2026 13:57:59 +0300 Subject: [PATCH] =?UTF-8?q?fix(airflow):=20=D0=BF=D1=80=D0=B0=D0=B2=D0=BA?= =?UTF-8?q?=D0=B8=20=D0=BF=D1=83=D0=BB=D1=8C=D1=82=D0=B0=20=D0=BC=D0=B8?= =?UTF-8?q?=D1=80=D0=B0=20=D0=BF=D0=BE=20=D0=B4=D0=B2=D1=83=D0=BC=20=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=B8=D1=8F=D0=BC=20=D1=80=D0=B5=D0=B2=D1=8C=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Зачем: линия дефектов нашла опору на дефект провайдера, линия постановки — незаписанный ответ на вопрос 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 --- README.md | 5 ++++- compose.yaml | 1 + dags/world_control.py | 20 +++++++++----------- infra/airflow/Dockerfile | 3 +++ 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index f474c34..ad7ce26 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,10 @@ make check-services Дальше, в рабочей петле, обычно хватает `make smoke`. `.env.example` — образец обязательной локальной настройки. В нём живут имя -экземпляра, внешние порты, учебные учётные данные и ключи. `compose.yaml` не +экземпляра, внешние порты, учебные учётные данные и ключи. Одно значение образца +верно не везде — `DOCKER_GID`: у группы `docker` на каждой машине свой номер, и +свой подскажет `getent group docker`. Пока он не сойдётся, стенд работает весь, +кроме пульта мира — о нём ниже. `compose.yaml` не дублирует их умолчаниями: если значения нет в `.env`, Compose сразу предложит скопировать образец. Внутренние адреса, имена топиков и версии образов описывают сам стенд, поэтому записаны литералами в `compose.yaml` и Dockerfile. diff --git a/compose.yaml b/compose.yaml index fdb22f2..916aee4 100644 --- a/compose.yaml +++ b/compose.yaml @@ -376,6 +376,7 @@ services: group_add: - ${DOCKER_GID:?Скопируйте .env.example в .env} # Тома перечислены заново: список службы общий не дополняет, а заменяет. + # Появится новый том у остальных служб Airflow — добавьте и сюда. volumes: - /var/run/docker.sock:/var/run/docker.sock - ./dags:/opt/airflow/dags:ro diff --git a/dags/world_control.py b/dags/world_control.py index 704a8ff..fcad3a4 100644 --- a/dags/world_control.py +++ b/dags/world_control.py @@ -28,8 +28,7 @@ from airflow.sdk import Param, Variable, dag, get_current_context, task # Факты стенда — образ генератора, сеть, адрес брокера, топик и размер # стартового мира — приходят окружением, и называет их compose: тот же, что -# называет их разовой службе генератора. Генератор для пульта — отдельная и -# заменяемая сущность, и знает о нём даг ровно то, что здесь перечислено. +# называет их разовой службе генератора. GENERATOR_IMAGE = os.environ["GENERATOR_IMAGE"] STAND_NETWORK = os.environ["STAND_NETWORK"] GENERATOR_ENVIRONMENT = { @@ -41,11 +40,10 @@ STARTING_DAYS = int(os.environ["WORLD_STARTING_DAYS"]) # Позиция на оси: номер первого несыгранного дня. Переменной нет — мир в # стартовом состоянии, и играть надо сразу за ним. # -# Позиция именно ставится, а не увеличивается на единицу. Наложись один прогон -# на другой, худшее при таком правиле — сыгранный дважды день: номера событий -# детерминированы, и повтор схлопнет ReplacingMergeTree. Увеличение в том же -# случае молча съело бы день, и в мире осталась бы дыра, которой никто не -# заметит. +# Позиция ставится, а не увеличивается. Наложись один прогон на другой, худшее +# при таком правиле — сыгранный дважды день, а повтор схлопнет +# ReplacingMergeTree. Увеличение молча съело бы день, и в мире осталась бы +# дыра, которой никто не заметит. WORLD_POSITION = "world_position" # Тик выключателя. Каденцию задаёт не он, а сама длина живого дня — около @@ -75,10 +73,9 @@ def _play(task_id: str, command: list[str]) -> DockerOperator: command=command, network_mode=STAND_NETWORK, environment=GENERATOR_ENVIRONMENT, - # Контейнер убирается за собой в любом исходе — вопреки имени - # значения: оператор сносит его в `finally`. Терять при этом нечего, - # вывод генератора он уже перелил в журнал задачи. - auto_remove="success", + # Контейнер убирается в любом исходе: вывод генератора оператор уже + # перелил в журнал задачи, а мёртвые контейнеры копить незачем. + auto_remove="force", # По умолчанию оператор монтирует контейнеру временный каталог. Здесь # это ловушка: путь он заводит внутри Airflow, а монтирует демон с # хоста, где такого пути нет. Генератору временный каталог не нужен. @@ -157,6 +154,7 @@ def world_live_day(): dag_id="world_live", schedule=LIVE_TICK, start_date=START_DATE, + catchup=False, is_paused_upon_creation=True, max_active_runs=1, tags=TAGS, diff --git a/infra/airflow/Dockerfile b/infra/airflow/Dockerfile index a82e296..b6f1f7b 100644 --- a/infra/airflow/Dockerfile +++ b/infra/airflow/Dockerfile @@ -4,6 +4,9 @@ USER root # Официальный провайдер Kafka использует тот же confluent-kafka. Пробникам # не нужны его подключения и обёртки, поэтому в образ добавлен сам клиент. +# +# Провайдера docker, которым пульт мира зовёт генератор, ставить не нужно: +# база несёт его сама — 4.5.7 к Airflow 3.3.0, замер 13 августа 2026 года. RUN uv pip install --python /home/airflow/.local/bin/python --no-cache \ "clickhouse-connect==1.6.0" \ "confluent-kafka==2.15.0" \