|
45 | 45 | from airflow._shared.timezones import timezone |
46 | 46 | from airflow.callbacks.callback_requests import DagCallbackRequest |
47 | 47 | from airflow.dag_processing.bundles.base import BaseDagBundle |
| 48 | +from airflow.dag_processing.bundles.local import LocalDagBundle |
48 | 49 | from airflow.dag_processing.bundles.manager import DagBundlesManager |
49 | 50 | from airflow.dag_processing.dagbag import DagBag |
50 | 51 | from airflow.dag_processing.manager import ( |
|
83 | 84 |
|
84 | 85 | logger = logging.getLogger(__name__) |
85 | 86 | TEST_DAG_FOLDER = Path(__file__).parents[1].resolve() / "dags" |
| 87 | + |
| 88 | + |
| 89 | +class _HealthControlledDagBundle(LocalDagBundle): |
| 90 | + """LocalDagBundle that fails to instantiate unless its health file contains "ok".""" |
| 91 | + |
| 92 | + def __init__(self, *, health_file: str, **kwargs) -> None: |
| 93 | + state = Path(health_file).read_text().strip() |
| 94 | + if state != "ok": |
| 95 | + raise RuntimeError(f"simulated {state}") |
| 96 | + super().__init__(**kwargs) |
| 97 | + |
| 98 | + |
86 | 99 | DEFAULT_DATE = timezone.datetime(2016, 1, 1) |
87 | 100 |
|
88 | 101 |
|
@@ -1072,6 +1085,64 @@ def test_deactivate_stale_dags_marks_dags_in_inactive_bundles(self, session): |
1072 | 1085 | ) |
1073 | 1086 | assert is_stale_by_dag == {"dag_in_inactive_bundle": True, "dag_in_active_bundle": False} |
1074 | 1087 |
|
| 1088 | + def test_bundle_creation_error_keeps_bundle_active_and_dags_fresh(self, tmp_path): |
| 1089 | + """A bundle failing to instantiate at startup must not be deactivated nor its Dags marked stale.""" |
| 1090 | + dags_dir = tmp_path / "dags" |
| 1091 | + dags_dir.mkdir() |
| 1092 | + (dags_dir / "good_dag.py").write_text( |
| 1093 | + textwrap.dedent( |
| 1094 | + """ |
| 1095 | + from airflow.providers.standard.operators.empty import EmptyOperator |
| 1096 | + from airflow.sdk import DAG |
| 1097 | +
|
| 1098 | + with DAG(dag_id="dag_in_flaky_bundle", schedule=None): |
| 1099 | + EmptyOperator(task_id="noop") |
| 1100 | + """ |
| 1101 | + ) |
| 1102 | + ) |
| 1103 | + (dags_dir / "broken_dag.py").write_text("an invalid airflow DAG") |
| 1104 | + health_file = tmp_path / "health.txt" |
| 1105 | + health_file.write_text("ok") |
| 1106 | + |
| 1107 | + bundle_config = [ |
| 1108 | + { |
| 1109 | + "name": "flaky-bundle", |
| 1110 | + "classpath": f"{__name__}._HealthControlledDagBundle", |
| 1111 | + "kwargs": { |
| 1112 | + "path": str(dags_dir), |
| 1113 | + "health_file": str(health_file), |
| 1114 | + "refresh_interval": 0, |
| 1115 | + }, |
| 1116 | + } |
| 1117 | + ] |
| 1118 | + |
| 1119 | + def get_db_state(): |
| 1120 | + with create_session() as session: |
| 1121 | + active = session.scalar( |
| 1122 | + select(DagBundleModel.active).where(DagBundleModel.name == "flaky-bundle") |
| 1123 | + ) |
| 1124 | + fresh_dags = session.scalar( |
| 1125 | + select(func.count()) |
| 1126 | + .select_from(DagModel) |
| 1127 | + .where(DagModel.bundle_name == "flaky-bundle", ~DagModel.is_stale) |
| 1128 | + ) |
| 1129 | + import_errors = session.scalar(select(func.count()).select_from(ParseImportError)) |
| 1130 | + return active, fresh_dags, import_errors |
| 1131 | + |
| 1132 | + with conf_vars({("dag_processor", "dag_bundle_config_list"): json.dumps(bundle_config)}): |
| 1133 | + DagFileProcessorManager(max_runs=1, processor_timeout=365 * 86_400).run() |
| 1134 | + assert get_db_state() == (True, 1, 1) |
| 1135 | + |
| 1136 | + # "Restart" the dag-processor while bundle creation fails |
| 1137 | + health_file.write_text("transient connection error") |
| 1138 | + DagFileProcessorManager(max_runs=1, processor_timeout=365 * 86_400).run() |
| 1139 | + assert get_db_state() == (True, 1, 1) |
| 1140 | + |
| 1141 | + # Removing the still-failing bundle from config must remove it from DB |
| 1142 | + with conf_vars({("dag_processor", "dag_bundle_config_list"): json.dumps([])}): |
| 1143 | + DagFileProcessorManager(max_runs=1, processor_timeout=365 * 86_400).run() |
| 1144 | + assert get_db_state() == (False, 0, 0) |
| 1145 | + |
1075 | 1146 | @mock.patch("airflow.dag_processing.manager.is_lock_not_available_error") |
1076 | 1147 | @pytest.mark.usefixtures("testing_dag_bundle") |
1077 | 1148 | def test_deactivate_stale_dags_handles_lock_timeout(self, mock_is_lock_not_available, session, caplog): |
|
0 commit comments