Skip to content

Find Lang-SDK task handler artifacts and decide which to probe - #74032

Draft
jason810496 wants to merge 6 commits into
jason/core-taskhandler-refactor/10-task-handler-bindings-reconcilefrom
jason/core-taskhandler-refactor/11-candidate-listing-fast-path
Draft

jason810496 wants to merge 6 commits into
jason/core-taskhandler-refactor/10-task-handler-bindings-reconcilefrom
jason/core-taskhandler-refactor/11-candidate-listing-fast-path

Conversation

@jason810496

@jason810496 jason810496 commented Oct 1, 2026 •

Copy link
Copy Markdown
Member

Stack (bottom to top): #73970, #73971, #73972, #73973, #73974, #73975, #73976, #73977, #74030, #74031, #74032, #74033, #74067

Why

The Dag-file parse will resolve each @task.stub task against the artifacts in its coordinator's Dag bundle (ADR-0013 Flow 1, steps 4 and 5). To do that it must find the artifacts without running them, and skip the probe subprocess for an artifact whose recorded answer still holds. This PR adds both. Nothing calls them yet; a later PR wires them into _parse_file.

What changes

coordinator = get_coordinator_manager().get_coordinator("jdk-17")
candidates = coordinator.list_task_handler_candidates(bundle_path)
# [TaskHandlerCandidate(rel_path="airflow-java-sdk-scala-spark-example.jar", size_bytes=21135,
#                       cache_digest="067c98e6...", error=None)]

plan = plan_task_handler_probes(
    bundle_name="java-task-handlers", candidates=candidates, known_artifacts=request.known_artifacts
)
plan.cached    # recorded answers (TaskHandlerArtifact) that still hold
plan.probe     # candidates to probe
plan.rejected  # candidates the listing found unusable
  • Listing. BaseCoordinator.list_task_handler_candidates(bundle_path) raises NotImplementedError by default. SubprocessCoordinator walks the bundle in sorted order and asks a new per-language hook, _read_task_handler_candidate(path, *, rel_path), about each file. A file reached through symlinks is listed once, under the first path walked, and a coordinator without the hook raises NotImplementedError before walking. The hook reads what marks the artifact and its stored digest, takes the size from the same open file, and never hashes the artifact.
    • Go: any file that ends in the AFBNDL01 trailer, whatever its executable bit. A bundle without the bit, or with a trailer this runtime cannot run, is listed with an error. Object-store Dag bundles drop the bit, and such a bundle used to be skipped silently.
    • Java: a .jar whose manifest carries Airflow-Cache-Digest. In the Scala Spark example bundle, 6 of 167 JARs declare Main-Class, 5 of them dependencies, and only the bundle JAR carries the digest. A digest JAR without Main-Class is listed with an error. A JAR whose manifest cannot be read (an encrypted entry, an unsupported compression, corrupt LZMA data) is skipped, like any other unreadable JAR.
    • TS: a .min.mjs whose first line starts with the //# airflowBundle= marker. One whose layout line is invalid is listed with an error. A .min.mjs without the marker may be an unrelated minified module, so it is skipped.
  • Digests. Go's read_cache_digest now shares its reader with the listing. The new TS read_cache_digest is the SHA-256 of the whole layout line, which records every region's SHA-256, so head -n1 bundle.min.mjs | sha256sum reproduces it. Java reads the manifest attribute. A stored digest says "unchanged", not "intact", so the size is compared too, and execution's integrity check is unchanged. A digest longer than 128 characters, the column's length, becomes None, so that artifact is probed on every parse.
  • Decision. plan_task_handler_probes decides each candidate on its own, with no database:
    • a candidate with a listing error is rejected, even when a recorded answer matches it;
    • a candidate with the same path, size and non-null stored digest as a known artifact of its bundle reuses that artifact's whole answer;
    • any other candidate, including one that stores no digest, is probed.
  • Docs. A contributor-guide subsection for the hook, a cache digest paragraph in ts-bundle-spec.rst, one sentence each in the JavaCoordinator and ExecutableCoordinator docstrings, and ADR-0013's fast path, Flow 1 steps 4 to 8 and Consequences, plus the Java marker in ADR-0014.

How to test

uv run --project task-sdk pytest task-sdk/tests/task_sdk/coordinators task-sdk/tests/task_sdk/execution_time/test_coordinator.py -q
uv run --project airflow-core --with-editable shared/secrets_masker pytest airflow-core/tests/unit/dag_processing/test_task_handler_fast_path.py -q

Ran:

  • task-sdk coordinator and test_coordinator.py tests: 299 passed.
  • The airflow-core dag_processing suite on SQLite: 680 passed, 13 skipped (the opt-in real probes and a Postgres/MySQL-only lock test).
  • Listing the locally built bundles: the Scala Spark example (167 JARs) gives one candidate in 182 ms, each fat-JAR bundle (example, e2e, k8s) one, and the TS fixture bundle-v1.min.mjs one, whose digest equals head -n1 | sha256sum.
  • mypy-task-sdk and mypy-airflow-core on all files, and the prek pre-commit hooks on the changed files, passed.

Was generative AI tooling used to co-author this PR?
  • Yes (please specify the tool below)

Generated-by: Claude Code (Opus 5.5) following the guidelines

@boring-cyborg boring-cyborg Bot added the area:coordinator Coordinator: The interface to spawn Lang-SDK subprocesses label Oct 1, 2026
@jason810496
jason810496 added this pull request to stack #73978 October 1, 2026 13:34
@jason810496 jason810496 changed the title jason/core taskhandler refactor/11 candidate listing fast path Find Lang-SDK task handler artifacts and decide which to probe Oct 1, 2026
@jason810496
jason810496 force-pushed the jason/core-taskhandler-refactor/10-task-handler-bindings-reconcile branch from 568fa9f to fead951 Compare October 1, 2026 14:44
@jason810496
jason810496 force-pushed the jason/core-taskhandler-refactor/11-candidate-listing-fast-path branch from 4392e00 to efd9eb0 Compare October 1, 2026 14:44
@jason810496
jason810496 force-pushed the jason/core-taskhandler-refactor/10-task-handler-bindings-reconcile branch from fead951 to a937384 Compare October 2, 2026 00:54
@jason810496
jason810496 force-pushed the jason/core-taskhandler-refactor/11-candidate-listing-fast-path branch from efd9eb0 to 3e03718 Compare October 2, 2026 00:54
@jason810496
jason810496 force-pushed the jason/core-taskhandler-refactor/10-task-handler-bindings-reconcile branch from a937384 to 391143b Compare October 2, 2026 05:48
@jason810496
jason810496 force-pushed the jason/core-taskhandler-refactor/11-candidate-listing-fast-path branch from 3e03718 to 1c5b1ca Compare October 2, 2026 05:48
@jason810496
jason810496 force-pushed the jason/core-taskhandler-refactor/10-task-handler-bindings-reconcile branch from 391143b to dc3fd66 Compare October 2, 2026 11:36
@jason810496
jason810496 force-pushed the jason/core-taskhandler-refactor/11-candidate-listing-fast-path branch from 1c5b1ca to 19e73b5 Compare October 2, 2026 11:36
The Dag processor must find the task-handler artifacts in a coordinator's Dag bundle before it can ask them anything, and only the coordinator knows which files are its artifacts. A hook next to the command hooks lets each SDK say so, together with the fingerprint an artifact stores, without running or hashing it.
The AFBNDL01 trailer is what marks a file as a Go bundle, so the Dag processor lists by it and not by the executable bit. An object-store Dag bundle drops that bit, and such a bundle used to be silently skipped; it is now listed with an error the Dag processor can report. The stored cache digest is read from the metadata region without hashing the binary.
A Java bundle also holds its dependency JARs, and some of them declare Main-Class (five of six in the Scala Spark example), so Main-Class cannot tell a handler JAR apart. The Airflow-Cache-Digest attribute that the Gradle plugin writes can, and it is the fingerprint the Dag processor compares. A JAR that carries it without Main-Class cannot be run, so it is listed with an error rather than skipped.
The Dag processor needs a fingerprint it can compare on every parse without hashing a bundle. A TS bundle's layout line already records the SHA-256 of every region, so hashing that one line changes whenever the bundle does and stays stable across packs of the same inputs. A .min.mjs file is listed as a task handler bundle only when its first line carries the layout marker, since any minified module may carry that suffix. One whose layout line is invalid is listed with an error, so the Dag processor can report the broken bundle instead of a missing handler.
Probing an artifact starts a subprocess, and every Dag file is re-parsed every 30 seconds by default, so a parse must reuse a recorded answer whenever it still holds. Because a probe now asks for every handler, its answer depends only on the artifact, and the size plus stored cache digest it was recorded with is a sound key for every Dag file, not only the one that probed. The decision is a pure function, so the wiring in _parse_file can use it without a database.
The ADR still described the fast path that compared each Dag file's stored params and required an unchanged candidate set, which was unsound across files. With the whole answer recorded per artifact, a candidate is skipped when its own fingerprint matches, a new artifact is probed by the first file that lists it, and Java JARs are recognised by their cache digest rather than by Main-Class.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:coordinator Coordinator: The interface to spawn Lang-SDK subprocesses area:DAG-processing area:dev-tools area:task-sdk

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant