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:
@@ -328,7 +328,7 @@ def target_dag_trigger_error(dag_id: str, dag_model) -> str | None:
|
||||
if dag_model.is_paused:
|
||||
return (
|
||||
f"DAG {dag_id} стоит на паузе: снимите паузу в Airflow UI, "
|
||||
"затем повторите generator_control."
|
||||
"затем повторите запуск DAG."
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
+36
-34
@@ -1,6 +1,4 @@
|
||||
"""
|
||||
Контракт DAG generator_control без запуска Airflow.
|
||||
"""
|
||||
"""Контракты DAG world_init и world_next_day без запуска Airflow."""
|
||||
|
||||
import ast
|
||||
from pathlib import Path
|
||||
@@ -8,7 +6,8 @@ from pathlib import Path
|
||||
from clickstream_generator.launch import PROFILES
|
||||
|
||||
|
||||
DAG_PATH = Path(__file__).parents[2] / "airflow" / "dags" / "generator_control_dag.py"
|
||||
DAG_PATH = Path(__file__).parents[2] / "airflow" / "dags" / "world_init_dag.py"
|
||||
NEXT_DAY_DAG_PATH = Path(__file__).parents[2] / "airflow" / "dags" / "world_next_day_dag.py"
|
||||
REPO_ROOT = Path(__file__).parents[2]
|
||||
|
||||
|
||||
@@ -65,7 +64,7 @@ def test_dag_file_exists_and_uses_dynamic_profiles():
|
||||
"""DAG берёт варианты профилей из PROFILES, а не из ручного списка."""
|
||||
text = DAG_PATH.read_text(encoding="utf-8")
|
||||
|
||||
assert "dag_id=\"generator_control\"" in text
|
||||
assert "dag_id=\"world_init\"" in text
|
||||
assert "sorted(PROFILES)" in text
|
||||
for profile in PROFILES:
|
||||
assert profile not in {"hardcoded-profile"}
|
||||
@@ -76,12 +75,13 @@ def test_trigger_form_has_expected_param_enums():
|
||||
text = DAG_PATH.read_text(encoding="utf-8")
|
||||
param_defaults = _declared_param_defaults()
|
||||
|
||||
assert 'enum=["backfill", "import", "next-day", "check"]' in text
|
||||
assert 'enum=["backfill", "import", "check"]' in text
|
||||
assert param_defaults["operation"] == "import"
|
||||
assert "enum=sorted(PROFILES)" in text
|
||||
assert param_defaults["profile"] == "daily-wave"
|
||||
assert '"duration": Param(' in text
|
||||
assert '"artifact_path": Param(' in text
|
||||
assert '"expected_t_end": Param(' in text
|
||||
assert '"expected_t_end": Param(' not in text
|
||||
|
||||
|
||||
def test_trigger_form_marks_only_optional_params_as_nullable():
|
||||
@@ -93,7 +93,6 @@ def test_trigger_form_marks_only_optional_params_as_nullable():
|
||||
"seed",
|
||||
"model_time_speed",
|
||||
"artifact_path",
|
||||
"expected_t_end",
|
||||
}:
|
||||
assert "null" in param_types[name]
|
||||
for name in {"operation", "profile"}:
|
||||
@@ -101,7 +100,7 @@ def test_trigger_form_marks_only_optional_params_as_nullable():
|
||||
|
||||
|
||||
def test_dag_branches_and_waits_for_etl_completion():
|
||||
"""Backfill/import/next-day запускают ETL и ждут завершения перед check."""
|
||||
"""Backfill/import запускают ETL и ждут завершения перед check."""
|
||||
text = DAG_PATH.read_text(encoding="utf-8")
|
||||
|
||||
assert "BranchPythonOperator" in text
|
||||
@@ -112,41 +111,43 @@ def test_dag_branches_and_waits_for_etl_completion():
|
||||
assert 'failed_states=["failed"]' in text
|
||||
|
||||
|
||||
def test_next_day_has_own_boundary_precheck_and_serial_execution():
|
||||
"""Next-day не использует clean-guard и не допускает параллельных запусков."""
|
||||
text = DAG_PATH.read_text(encoding="utf-8")
|
||||
def test_world_next_day_is_parameterless_paused_half_hour_dag():
|
||||
"""DAG следующего дня запускается пустой формой каждые полчаса."""
|
||||
text = NEXT_DAY_DAG_PATH.read_text(encoding="utf-8")
|
||||
|
||||
assert "schedule=None" in text
|
||||
assert 'dag_id="world_next_day"' in text
|
||||
assert 'schedule="*/30 * * * *"' in text
|
||||
assert "catchup=False" in text
|
||||
assert "is_paused_upon_creation=True" in text
|
||||
assert "max_active_runs=1" in text
|
||||
assert 'return "check_etl_not_paused_before_next_day"' in text
|
||||
assert "assert_next_day_snapshot" in text
|
||||
assert "assert_expected_t_end" in text
|
||||
next_day_precheck = text.split("def precheck_next_day", maxsplit=1)[1].split(
|
||||
"\ndef ", maxsplit=1
|
||||
)[0]
|
||||
assert "assert_stand_clean" not in next_day_precheck
|
||||
assert "assert_live_generator_not_running" in next_day_precheck
|
||||
assert "run_next_day" in text
|
||||
assert "params=" not in text
|
||||
assert "precheck_next_day" in text
|
||||
assert "run_next_day_task" in text
|
||||
assert "TriggerDagRunOperator" in text
|
||||
assert "wait_for_completion=True" in text
|
||||
assert "check_manifest_task" in text
|
||||
assert "check_etl_not_paused >> precheck" in text
|
||||
|
||||
|
||||
def test_generator_control_prechecks_etl_dag_not_paused_before_waiting():
|
||||
def test_world_dags_precheck_etl_dag_not_paused_before_waiting():
|
||||
"""Пульт проверяет паузу etl_pipeline до долгого ожидания."""
|
||||
text = DAG_PATH.read_text(encoding="utf-8")
|
||||
shared_tasks = (
|
||||
REPO_ROOT / "airflow" / "dags" / "utils" / "startup_history_tasks.py"
|
||||
).read_text(encoding="utf-8")
|
||||
|
||||
assert "assert_target_dag_not_paused" in text
|
||||
assert 'ETL_DAG_ID = "etl_pipeline"' in text
|
||||
assert 'op_kwargs={"dag_id": ETL_DAG_ID}' in text
|
||||
assert "session.query(DagModel)" in text
|
||||
assert "DagModel.dag_id == dag_id" in text
|
||||
assert "target_dag_trigger_error" in text
|
||||
assert "Airflow 2.10.5" in text
|
||||
assert "fail_when_dag_is_paused" in text
|
||||
assert "session.query(DagModel)" in shared_tasks
|
||||
assert "DagModel.dag_id == dag_id" in shared_tasks
|
||||
assert "target_dag_trigger_error" in shared_tasks
|
||||
assert "Airflow 2.10.5" in shared_tasks
|
||||
assert "fail_when_dag_is_paused" in shared_tasks
|
||||
assert 'return "check_etl_not_paused_before_backfill"' in text
|
||||
assert 'return "check_etl_not_paused_before_import"' in text
|
||||
assert 'return "check_etl_not_paused_before_next_day"' in text
|
||||
assert "check_etl_not_paused_before_backfill >> precheck_backfill_task" in text
|
||||
assert "check_etl_not_paused_before_import >> precheck_import_task" in text
|
||||
assert "check_etl_not_paused_before_next_day >> precheck_next_day_task" in text
|
||||
assert text.index("check_etl_not_paused_before_backfill >> precheck_backfill_task") < text.index(
|
||||
"precheck_backfill_task >> backfill_task"
|
||||
)
|
||||
@@ -170,15 +171,16 @@ def test_make_up_rebuilds_airflow_images_after_repo_update():
|
||||
assert "superset-init" not in up_block
|
||||
|
||||
|
||||
def test_readme_quick_start_lists_prerequisites_and_runs_ddl_before_generator_control():
|
||||
"""Быстрый старт называет инструменты и DDL до generator_control."""
|
||||
def test_readme_quick_start_creates_schema_via_ddl_init_before_world_init():
|
||||
"""Быстрый старт: терминал — только make up, схему создаёт ddl_init до world_init."""
|
||||
text = (REPO_ROOT / "README.md").read_text(encoding="utf-8")
|
||||
|
||||
assert "uv" in text
|
||||
assert "Docker" in text
|
||||
assert "docker compose" in text
|
||||
assert "make up\nmake ddl" in text
|
||||
assert text.index("make ddl") < text.index("generator_control")
|
||||
assert "make ddl" not in text
|
||||
quick_start = text.split("## Быстрый старт", maxsplit=1)[1]
|
||||
assert quick_start.index("ddl_init") < quick_start.index("world_init")
|
||||
|
||||
|
||||
def test_course_readme_lists_uv_before_first_command():
|
||||
Reference in New Issue
Block a user