Repository navigation
Record probed artifacts and task handler bindings from a Dag parse - #74031
Draft
jason810496 wants to merge 12 commits into
Conversation
jason810496
added this pull request to stack #73978
October 1, 2026 13:34
1 task done
This was referenced Oct 1, 2026
jason810496
force-pushed
the
jason/core-taskhandler-refactor/10-task-handler-bindings-reconcile
branch
from
October 1, 2026 14:44
568fa9f to
fead951
Compare
jason810496
force-pushed
the
jason/core-taskhandler-refactor/09-known-artifacts-push-down
branch
from
October 1, 2026 14:44
a19f24f to
34aabf6
Compare
jason810496
force-pushed
the
jason/core-taskhandler-refactor/10-task-handler-bindings-reconcile
branch
from
October 2, 2026 00:54
fead951 to
a937384
Compare
jason810496
force-pushed
the
jason/core-taskhandler-refactor/09-known-artifacts-push-down
branch
2 times, most recently
from
October 2, 2026 05:48
7b0b329 to
4ca5e76
Compare
jason810496
force-pushed
the
jason/core-taskhandler-refactor/10-task-handler-bindings-reconcile
branch
from
October 2, 2026 05:48
a937384 to
391143b
Compare
1 task done
jason810496
force-pushed
the
jason/core-taskhandler-refactor/10-task-handler-bindings-reconcile
branch
from
October 2, 2026 11:36
391143b to
dc3fd66
Compare
jason810496
force-pushed
the
jason/core-taskhandler-refactor/09-known-artifacts-push-down
branch
from
October 2, 2026 11:36
4ca5e76 to
7e11dfa
Compare
The manager has to persist which artifact runs each stub task, so a worker no longer searches for it (ADR-0013), and the answers the parse probed, so no other parse probes those artifacts again. None tells a parse that did not evaluate task handlers apart from one that found none, so a failed or skipped evaluation cannot wipe the recorded bindings. The answers travel apart from the bindings because they are worth keeping when a Dag fails validation and has none, and a binding names only its artifact, whose fingerprint and answer belong to the artifact rather than to the stub task. DagFileParsingResult already existed at 2026-06-16, so each new field needs a VersionChange: a runtime pinned there is upgraded with the bindings unset, which means "leave them alone", and no probed artifacts, which records nothing.
check-ts-sdk-supervisor-schema fails whenever supervisor.ts lags the snapshot, and CI runs it over all files on any ts-sdk change. The shifted numeric suffixes are the generator's own renumbering; no hand-written TS code refers to them.
The vendored copy tracks the monorepo snapshot while both declare the same api_version, and sync-java-sdk-supervisor-schema rewrites it on the next Java SDK change otherwise.
check-go-sdk-generated-drift regenerates the Go SDK's coordinator-protocol models from the supervisor schema snapshot and fails on any difference, so DagFileParsingResult must carry task_handler_bindings and probed_artifacts on the Go side too. go-jsonschema moves some type blocks, so the diff is larger than the new fields.
A handler row belongs to a Dag, not to the file that last wrote it, so the rows are reconciled by the result's Dag ids: a Dag that moved files keeps its rows, and a parse that yields fewer Dags cannot delete rows it did not evaluate. The Dag rows are already locked for the persist, so handler rows need no upsert. Artifact rows are shared across files and Dag processors and hold answers that other files trust, so only a probe writes them, only within the Dag file's scope, and in a transaction of its own before the persist: its exclusive locks are released before the reconcile takes shared ones, so two Dag processors cannot deadlock on crossed artifacts, and an answer survives a failed persist. The reconcile only looks artifacts up, under a shared lock that keeps the orphan sweep from deleting one before its handler rows are in; a Dag whose artifact has no row or lies outside the scope keeps its rows, and the next parse probes again.
The orphan sweep deletes unreferenced artifact rows only once last_probed_at is past a grace period, and a batched cleanup must filter on indexed columns. It is a separate migration because the tables' migration already ships in an earlier, separately reviewed PR.
A parse never evicts artifact rows, because one artifact serves Dags in many files, so the Dag processor reclaims them itself; db clean leaves the table alone. The grace period keeps rows for probed artifacts that bind nothing, which later parses reuse instead of probing again. Batches commit on their own so the sweep never holds locks across the whole table, and SKIP LOCKED keeps it from waiting on, or deleting under, a parse that is binding an artifact.
A binding names only its artifact, and the probed answers travel in a field of their own, because an answer belongs to the artifact and is worth keeping even when a Dag fails validation. Writing them in their own transaction before the persist takes the upsert out of the reconcile, which then only looks artifacts up, so the ADR has to say what happens to a Dag whose artifact is missing or outside the file's scope. An empty list deletes only the rows of the Dags in the result, since the reconcile is keyed by Dag id rather than by file. db clean excludes the artifact table, so the orphan sweep is the only reclaim path.
create_session() hands back the thread's scoped session, so the session standing in for a second Dag processor shared the test's transaction: on Postgres the sweep then deleted a row the "parse" had locked, because the lock was its own. A non-scoped session is a separate transaction, as a second processor would be.
A Dag rejected for another team's plugin class already gets an import error on every parse, so warning about its dropped bindings as well was noise that buried the case worth a warning: a binding for a Dag the parse never produced, which points at a bug in the parse.
The sweep's interval check now reads like the bundle-version cleanup next to it, and its deleted-row count no longer hides a missing rowcount behind a default that can never apply. The parsing_cleanup_interval description said the artifact deletion in a clause that did not parse cleanly.
TaskHandlerParam no longer has required. A model dump no longer carries it, so the expected result bodies must drop it, and the constructors would otherwise pass a keyword pydantic only ignores.
jason810496
force-pushed
the
jason/core-taskhandler-refactor/09-known-artifacts-push-down
branch
from
October 2, 2026 14:57
7e11dfa to
00586db
Compare
jason810496
force-pushed
the
jason/core-taskhandler-refactor/10-task-handler-bindings-reconcile
branch
from
October 2, 2026 14:57
dc3fd66 to
aa3f771
Compare
This was referenced Oct 3, 2026
jason810496
removed this pull request from stack #73978
October 6, 2026 02:27
1 task done
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stack (bottom to top): #73970, #73971, #73972, #73973, #73974, #73975, #73976, #73977, #74030, #74031, #74032, #74033, #74067
Why
The Dag processor has to persist which artifact runs each stub task, so a worker stops searching for it, and each probed artifact's answer, so no other parse probes that artifact again while it is unchanged (ADR-0013 "Flow 1"). This PR adds both to the parse result, writes them into the two tables from #73971, and reclaims artifact rows nothing uses. Nothing produces them yet (a later layer probes and validates), so in production the bindings are always
None,probed_artifactsis empty, and nothing is written.What changes
DagFileParsingResultgains the resolved bindings and the probed artifacts (TaskHandlerArtifactfrom the previous PR):probed_artifactsholds every artifact the parse probed, with its answer, even when the bindings areNone, so a Dag that fails validation is not probed again on every parse. It is the only thing that writes an answer.handle_parsing_resultrecords them first, in a transaction of their own, and skips that when the list is empty. Each is upserted by(bundle_name, relative_fileloc_hash)with its size, digest, answer andlast_probed_at, in key order (ON CONFLICT DO UPDATEon Postgres and SQLite,ON DUPLICATE KEY UPDATEon MySQL). An artifact outside the Dag file's scope (the bundles the previous PR sends the file: the named bundles of its team, and its own Dag bundle when a coordinator falls back to it) is dropped with a warning.persist_probed_task_handler_artifacts()can be overridden likepersist_parsing_result().Noneleaves the recorded bindings alone. A list replaces the bindings of each Dag inserialized_dags, so[]deletes the rows of this result's Dags and nothing else. Themax_lengthmatches the column, so an oversized path fails where the child builds the result, not in the manager's transaction.update_dag_parsing_results_in_db, after the Dag rows and before the serialized Dags, and is retried with them onOperationalError. It runs in a savepoint: on anIntegrityErrororDataErroronly the bindings are rolled back, with a warning naming the file, and the serialized Dags, code and import errors are still written.persist_parsing_resultpasses the bindings, the dispatched file path and the file's scope; bindings without a path or a scope raiseValueError. Because the bindings are written first, a throttled or failed serialized-Dag write can leave them briefly ahead of the serialized Dag; nothing reads the bindings yet, and the scheduler-read layer decides how to handle that.dag_relative_fileloc_hash. Rows of Dags the result lacks stay. A Dag removed from its file is only marked stale, so its rows, which keep their artifacts from the sweep, stay until thedagrow is deleted, and then go by cascade. The Dag rows are already lockedFOR UPDATEfor the persist, so these writes need no upsert.FOR KEY SHARE/FOR SHARE, in key order) held until commit. The read also selectssize_bytes, which is not in the unique index, so MySQL locks the row the sweep checks and not only the index entry; without it theSKIP LOCKEDtest fails on MySQL. A Dag bound to an artifact outside the scope, or to one with no row (swept concurrently, or its probed-artifact write failed), keeps its rows, with a warning; the next parse finds no recorded answer and probes again. A steady-state parse with bindings issues two SELECTs inside a savepoint and no writes.[scheduler] parsing_cleanup_interval, in batches of 1000, each in its own transaction.FOR UPDATE SKIP LOCKEDpasses over rows a parse is binding. A row goes only once itslast_probed_atis older than five cleanup intervals, so a probed artifact that binds nothing keeps its answer for the files parsed next.delete_unreferenced_task_handler_artifacts()can be overridden likecleanup_stale_bundle_versions().0143, onelast_probed_atindex for the sweep) because the tables' migration ships in Add Lang-SDK task handler binding tables #73971, a separately reviewed PR.AddTaskHandlerBindingsToDagFileParsingResultandAddProbedArtifactsToDagFileParsingResultin the in-progress2026-10-30version drop the fields for a runtime pinned to2026-06-16, and upgrade that runtime's result with no bindings and no probed artifacts. They followmain'sAddDagDefinitionsToDagFileParsingResultin that version. The snapshot,ts-sdk/src/generated/supervisor.ts, the vendoredjava-sdk/sdk/schema/schema.jsonand the Go genmodels are regenerated, sincecheck-go-sdk-generated-drift(Go SDK: bring the coordinator-protocol models back to the supervisor schema #73963) fails when the Go genmodels lag the snapshot.probed_artifactssketches, the probed-artifact write ahead of the persist, the missing and out-of-scope rule,[]narrowed to this result's Dags, a stale Dag's rows kept until itsdagrow is deleted, and "or bydb clean" dropped (the artifact table is excluded fromdb clean).How to test
Ran:
airflow-core/tests/unit/dag_processing/andtest_lang_sdk_task_handler.py, 678 passed, 13 skipped (Postgres-only budgets, the Postgres/MySQL-onlySKIP LOCKEDtest, opt-in real probes).test_db_cleanup.py: 98 passed, 3 skipped. Supervisor schema and coordinator tests: 93 passed.TestRecordProbedTaskHandlerArtifacts,TestTaskHandlerBindingReconcile, the rejected-Dag binding test,TestKnownTaskHandlerArtifacts,TestTaskHandlerBindings,TestTaskHandlerArtifactSweep(with theSKIP LOCKEDtest) and the model tests, 65 passed each, and again after0143was downgraded (index gone) and upgraded. The Postgres statement-budget tests passed unchanged.IntegrityErrorand injects aDataError, on all three backends; without the savepoint both fail the persist.prek run --from-ref <parent> --stage pre-commitpassed, includingcheck-supervisor-schemas-versionswithout SKIP,mypy-airflow-core,mypy-task-sdkandcheck-core-imports.prek run migration-round-trip --hook-stage manual,compile-ts-sdkand./gradlew -p java-sdk :sdk:test --tests '*TaskHandlerParseTest*'passed.AddProbedArtifactsToDagFileParsingResultin the bundle, a downgrade to2026-06-16keeps the field, which the migrator test catches.Was generative AI tooling used to co-author this PR?
Generated-by: Claude Code (Opus 5.5) following the guidelines