Repository navigation
Apply the configured XCom backend to language SDK task XComs - #75184
jason810496 wants to merge 13 commits into
Conversation
5fa6540 to
e77b09a
Compare
462f4c9 to
848611a
Compare
|
Having a whole separate process to handle xcom uplodads feels overkill. Why can't we do it from the coordinator process directly? The "why" in the PR desc doesn't address this |
Good point, I've added this to the Why in the description:
The helper is also lazy and per task: it starts on the first call the configured backend handles and serves all of that task's XCom calls, so tasks on the default It is also not meant for XCom only. Any worker-side backend that runs in a Python task's process and keeps in-memory state should run in this helper for the same isolation, so the |
|
My 2 cents... I like the shape of keeping the backend out of the supervisor. But making this new XCom process a mini event loop (heartbeat, its own execution_timeout) feels kinda heavy... Is it possible to make the supervisor just hand the call to the helper and go back to its normal loop? Then the only thing the XCom process does is run the backend call itself, and we can still monitor the XCom helper process from the same selector we use for the task runner socket. The supervisor can also get the result by listening on the XCom helper's socket, and reply to the task runner once it's done. Maybe there's a reason this doesn't work -- curious what you ran into. |
848611a to
a0675cc
Compare
9dd3243 to
54db61b
Compare
jason810496
left a comment
There was a problem hiding this comment.
Thanks for the review, I just updated this PR and #75942 which this PR depends on.
b0bc7b1 to
c1c646f
Compare
| --- | ||
| sdk: java | ||
|
|
||
| min_airflow_version: "3.4.0" |
There was a problem hiding this comment.
I'd like to it as-is. Explained in the comment below.
There was a problem hiding this comment.
On 3.4 this isn't only about the XCom backend. The 3.4 bundle has no 2027-02-28, so resolve_version rejects it and every Java task fails, not just XCom backend calls. That's why main moved Go and TS to 3.5.0 with the same pin in #75387.
The Java pin can stay at 2026-10-30, since nothing here needs XCOM_BACKEND_ERROR on the Java side and #75942 decouples the pin from the vendored schema. If you'd rather bump the pin, min_airflow_version must be 3.5.0.
2ffad21 to
57b2c39
Compare
The vendored copy was refreshed by downloading the version named in gradle.properties. The remote only publishes released versions, so an in-progress one cannot be fetched, and a schema change that leaves api_version alone moved nothing, so the copy went stale unnoticed. It is now copied from the schema the Python Task SDK owns and compared by content, the way sync-go-sdk-schemas already does for the Go SDK, so any change to the source moves it in the same commit. The download stays as the fallback for a build with no monorepo around it, which is what the source distribution needs. The refreshed copy is the content it never received while it was refreshed by version; api_version is unchanged.
The pin is the supervisor schema version the Java SDK speaks; the vendored schema.json is a copy of whatever the bundle carries. Requiring the two to be equal made the pin follow main every time a supervisor version was cut. The prek hook now copies the source by content and only checks that the pin is a version the bundle defines, and the Gradle task copies the in-repo source whenever it exists instead of falling back to the download on a version mismatch, so the build and the hook agree.
TypeScript, Go and Java tasks sent their XComs to the API server without the configured XCom backend. They read the stored reference of a value that a Python task wrote, such as an S3 path, instead of the value, their own values stayed in the metadata database, and clearing a try left stored objects behind. The backend runs in a helper process started like a Python task process, so the long-lived worker process keeps none of its state, such as a cached filesystem that a task process forked from it later cannot use.
No e2e test checked that TypeScript, Go and Java tasks store and read their XComs through the configured XCom backend. The new lang_sdk_xcom_object_storage mode runs one Dag definition in each language SDK with XComObjectStorageBackend on LocalStack, and checks the stored objects and the values the tasks read back. Changes to the XCom backend sources also run the language SDK e2e jobs.
A runtime sends its XCom requests without waiting for the previous answer, so a backend call can start while another runs. The supervisor kept one pending call, so a second request dropped the first one's answer and the runtime waited for a response that never came. The supervisor now keeps the pending calls in a queue and answers them in the order the backend process runs them, which is the order they were sent.
The XCom backend process now owns the callback of each call it is sent, so the supervisor keeps no queue of its own and nothing has to stay in sync. A process that has exited, or that does not answer a call the supervisor waits for, is stopped and replaced rather than reused. A value the backend cannot be given, and a row it cannot load, now fail the request. Keeping them in the XCom row instead put the payload in the database, which is what the backend is configured to avoid. Every backend but the default one runs in the helper process, rather than the helper deciding from the hooks a backend happens to override. Operator extra links keep bypassing the backend for a language SDK task, where the key is all the supervisor has to go on, but no longer for a Python task, which already writes them past the backend.
A failure of the configured XCom backend reached the runtime as API_SERVER_ERROR, which points a user at the API server rather than at their own backend. It now has its own XCOM_BACKEND_ERROR, and the loop guard answers with it instead of raising. A runtime pinned to a supervisor schema version that has no type for it gets GENERIC_ERROR, through the version change this adds.
2026-10-30 shipped with 3.4.0rc1, so it no longer takes changes: per contributing-docs/19_execution_api_versioning.rst a migration may only be added to an unreleased version. The execution API already cut 2027-02-28 for the same reason; the supervisor bundle had not followed. AddXComBackendErrorType moves to the new version, so a runtime pinned to 2026-10-30 keeps seeing the generic error type it knows. api_version follows the bundle's newest version, so the Go, TypeScript and Java copies and the version each SDK declares move with it.
XCom.delete guards neither the read nor the purge, and the task runner clears the XComs of an earlier try inside the block that fails the task, so for a Python task any of the three failing fails the task. The supervisor caught all three and only warned, so the same backend failure stopped a Python task and let a language SDK task run on. All three now propagate: the per-try clear fails the try, and a DeleteXCom request is answered with XCOM_BACKEND_ERROR, which fails the task from the runtime. _call_xcom_backend loses its on_error hook, which nothing else used.
- Reap a helper process that is known to be dead without servicing the selector it shares with the task, which would otherwise dispatch the task's own requests re-entrantly and start a second helper. - Fail the calls a stopped process never answered, and the call whose send raises, instead of leaving the runtime waiting for an answer. - Give the lang_sdk_xcom_object_storage e2e mode its own file group and run-lang-sdk-xcom-e2e-tests output, so a change to the XCom backend no longer runs the Java, Go and TypeScript SDK e2e jobs as well. - Declare a custom-xcom-backend capability in the three SDK matrices, supported since Airflow 3.5.0. - Drop comments that restate the code or belong in the PR description.
At task end a call can still be running in the helper. Its answer handed the helper the next queued call, so a store could write an object for a task that was already over, and no exit was sent, so stopping waited out the full timeout. Answering a runtime that had already exited raised OSError out of the supervisor's monitor loop, which skipped reporting the task state and uploading its logs.
Upstream added HITLDetailResponseResult to the task-sdk supervisor schema, so the Java SDK copy vendored from it drifted after the rebase.
57b2c39 to
e8db061
Compare
Several comments went on to explain the consequences of the code they annotate, which the review asked to drop.
Changes since the third review, which leaves out the rebase so only the review response shows.
3ec4021is the reviewed tree replayed onto the current base.Why
Run the configured
[core] xcom_backendat the supervisor, so TypeScript, Go and Java tasks store, read and purge XComs through it like Python tasks.The backend runs in a helper process rather than in the supervisor itself:
XComObjectStorageBackendcaches, breaks Python tasks forked from it later (This class is not fork-safe).execution_timeoutor when the server stops the task.The helper starts on the first call the configured backend handles, so tasks on the default
BaseXComnever start it.How
SetXCom,GetXCom,GetXComSequenceItem,GetXComSequenceSlice,DeleteXCom) and on the XComs it clears when a try starts. With the defaultBaseXCom, requests go to the API server unchanged.[core] execute_tasks_new_python_interpreter). So the long-lived worker process keeps none of the backend's state, such as a cachedS3FileSystemthat a Python task forked from it later could not use.store_value, and a loaded value is encoded with serde again.skipmixin_keybypasses the backend for every task, since the scheduler reads it from the XCom row as it is. Operator extra-link keys (_link_*) bypass it for language SDK tasks only, so Python tasks keep the behaviour they have today.execution_timeout.XCOM_BACKEND_ERRORrather thanAPI_SERVER_ERROR, which would point a user at the API server instead of at their own backend.lang_sdk_xcom_object_storagee2e mode runs a Dag definition in each of TypeScript, Go and Java withXComObjectStorageBackendon LocalStack.The backend needs no SDK change, since all three runtimes only talk to the supervisor. With this config, TypeScript, Go and Java tasks read and write offloaded XComs like Python tasks:
Lifecycle
sequenceDiagram participant Runtime as Runtime (TS, Go, Java) participant Supervisor participant Backend as XCom backend process participant API as API server participant Storage as Object storage Note over Supervisor,Backend: The XCom backend process starts on the first call the backend handles,<br/>by fork, or by exec on macOS or with execute_tasks_new_python_interpreter Supervisor->>API: get and delete the XComs of the earlier try Supervisor->>Backend: purge(row) Backend->>Storage: delete object Runtime->>Supervisor: SetXCom(key, value) Supervisor->>Backend: store(coordinate, key, value) opt a Connection the backend's own secrets backends do not have Backend->>Supervisor: GetConnection Supervisor-->>Backend: Connection, read from the API server end Backend->>Storage: write value Backend-->>Supervisor: s3://... reference Supervisor->>API: set the XCom row to the reference Runtime->>Supervisor: GetXCom(task_id, key) Supervisor->>API: get the XCom row Supervisor->>Backend: load(row) Backend->>Storage: read value Backend-->>Supervisor: value Supervisor-->>Runtime: value Note over Supervisor,Backend: The supervisor answers the runtime once the call returns, and goes on heartbeating<br/>and enforcing execution_timeout while it runs Runtime->>Supervisor: exits Supervisor->>Backend: exitWas generative AI tooling used to co-author this PR?
Generated-by: Claude Code (Opus 5.5) following the guidelines