Uh oh!
There was an error while loading. Please reload this page.
Fix dag processor callback cleanup for versioned bundle files - #66484
Conversation
There was a problem hiding this comment.
Diagnosis is right and the fix is in the right shape — separating queue identity from file presence is the correct move. Three things I'd want addressed before this lands:
The same identity mismatch lives at
manager.py:1267in_add_new_files_to_queue. Both_file_statsand_processorsaccumulate versioned entries (set at L1112 / L1254). The next scan's unversionedDagFileInfodoesn't match a versioned stat, so the file is re-queued as "new" and re-parsed every cycle until the version key happens to align. Same bug family, untouched here. If you don't want to fix it in this PR, please open a follow-up issue and link it in the body.Why does
bundle_versionneed to stay inDagFileInfoidentity at all?_callback_to_execute[file_info]is a list — multiple callbacks for the same path already coexist there without needing distinct keys. The only place versioned identity matters is_processors, but L1248 (if file in self._processors: continue) currently lets two different versions each spawn a processor for the same path, which is arguably worse than collapsing them. A one-linebundle_version: str | None = field(compare=False)change would fix the bug without needing the_present_file_keymachinery. Worth a sentence in the PR body explaining why the surgical fix was preferred.Scope is broader than the title suggests. Scheduler-emitted
TaskCallbackRequestfor normally-versioned bundles flows through the same queue and hits the same orphan check. Worth saying "DAG-level and task-level callbacks" in the body so reviewers understand the actual blast radius.
Additional inline notes (anchoring failed via API; included here)
On manager.py line 1023 — the new deque(...) comprehension in purge_removed_files_from_queue:
All three orphan-check methods recompute the same present_keys set from the same input. handle_removed_files is the only caller — compute it once there and pass keys down:
defhandle_removed_files(self, known_files):
present_keys= {(f.bundle_name, f.rel_path) forvinknown_files.values() forfinv}
self.purge_removed_files_from_queue(present_keys)
self.terminate_orphan_processes(present_keys)
self.remove_orphaned_file_stats(present_keys)Minor at typical sizes, but the signature change is free and removes the duplication.
On test_manager.py line 453 — processor.kill.assert_not_called():
The three new tests only cover the "preserved" direction. None assert that an entry is correctly purged when the file is genuinely gone — a regression that made _present_file_key collapse all keys to a constant would still pass the suite.
Please add the negative case for each of the three methods. For terminate_orphan_processes specifically:
deftest_terminate_orphan_processes_kills_processor_when_file_is_truly_absent(self):
manager=DagFileProcessorManager(max_runs=1)
versioned_file=DagFileInfo(
bundle_name="testing",
rel_path=Path("callbacks.py"),
bundle_path=TEST_DAGS_FOLDER,
bundle_version="v1",
)
processor=MagicMock()
manager._processors[versioned_file] =processormanager.terminate_orphan_processes(present=set())
assertmanager._processors== {}
processor.kill.assert_called_once()Without this, assert_not_called() doesn't actually pin the behavior — it just confirms kill wasn't invoked, which is also true if kill is never reachable.
Drafted-by: Claude Code (Opus 4.7); reviewed by @ephraimbuddy before posting
Uh oh!
There was an error while loading. Please reload this page.
hkc-8010
commented
May 6, 2026
@ephraimbuddy Thanks, this was really helpful. I pushed a follow-up that addresses the points you called out:
I kept I also updated the PR description to call out the broader scope: DAG-level and task-level callbacks both flow through this path. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
hkc-8010
commented
May 9, 2026
By the way, the customer tested the PR as a patch on their current Airflow version and confirmed that the callbacks resumed functioning without any problems. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Fix dag-processor presence/orphan checks so versioned bundle files are not treated as removed or new solely because the queued/tracked
DagFileInfocarries abundle_version.This is distinct from #66301 / #66474. Those changes addressed stale serialized DAG metadata when only the bundle version changed. This fix is later in the callback pipeline: scheduler-emitted DAG-level and task-level callback requests are fetched and queued correctly, but manager-side presence checks must not purge, orphan, or re-queue the same file just because the scanned DAG file is represented without
bundle_version.The change keeps
DagFileInfoequality and hashing version-aware, but adds apresence_keyonDagFileInfoand uses that for manager-side “is this file already present / already represented?” checks. This is intentionally narrower than changing globalDagFileInfoequality withbundle_version=field(compare=False), because callback requests are tied to a specific bundle version and we do not want to collapse queue/process identity semantics outside these presence checks.That is now applied in:
purge_removed_files_from_queueterminate_orphan_processesremove_orphaned_file_stats_add_new_files_to_queueprepare_file_queue_sort_by_mtime/processed_recentlyTests added:
prepare_file_queue()and modified-time mode when versioned entries already existValidation:
pytest airflow-core/tests/unit/dag_processing/test_manager.py -qbreeze testing core-tests --backend postgres --python 3.10 --db-reset -- airflow-core/tests/unit/dag_processing/test_manager.py -qbreeze testing core-tests --backend postgres --python 3.10 --downgrade-pendulum --db-reset -- airflow-core/tests/unit/dag_processing/test_manager.py -qbreeze testing core-tests --backend sqlite --python 3.10 --force-lowest-dependencies --db-reset -- airflow-core/tests/unit/dag_processing/test_manager.py -qcloses: #66483
Was generative AI tooling used to co-author this PR?
Generated-by: Codex following the guidelines
{pr_number}.significant.rst, in airflow-core/newsfragments. You can add this file in a follow-up commit after the PR is created so you know the PR number.