Repository navigation
Handle concurrent XCom writes with savepoint and UPDATE fallback to avoid 409 - #69959
nagasrisai wants to merge 15 commits into
Conversation
|
@nagasrisai This PR has a few issues that need to be addressed before it can be reviewed — please see our Pull Request quality criteria. Issues found:
What to do next:
There is no rush — take your time and work at your own pace. We appreciate your contribution and are happy to wait for updates. If you have questions, feel free to ask on the Airflow Slack. Note: This comment was drafted by an AI-assisted triage tool and may contain mistakes. Once you have addressed the points above, an Apache Airflow maintainer — a real person — will take the next look at your PR. We use this two-stage triage process so that our maintainers' limited time is spent where it matters most: the conversation with you. |
Catching all IntegrityError was too broad: a FK violation (e.g. the task_instance cascade-deleted between the dag_run_id lookup and the flush) would also land here, causing the fallback UPDATE to match zero rows while still returning 200 — silently losing the write. Narrow the guard to unique-constraint messages per dialect (matching the pattern in _UniqueConstraintErrorHandler) and re-raise everything else.
|
@kaxil - Thanks for the pointers! I've pushed two follow-up commits:
Will also look into getting the static checks clean. |
…m_set Adds two tests to TestXComsSetEndpoint: * test_xcom_set_concurrent_write_uses_update_fallback — pre-seeds the row, patches XComModel.set to raise a unique-constraint IntegrityError once, and asserts the endpoint returns 201 with the latest value stored via the UPDATE fallback. * test_xcom_set_fk_integrity_error_propagates — patches XComModel.set to raise an FK IntegrityError and asserts it propagates as a 500 rather than being silently swallowed.
…ASGI EndOfStream crash
potiuk
left a comment
There was a problem hiding this comment.
Thanks for chasing this. The race is real. The task SDK client retries a POST that timed out client-side (_should_retry_api_request retries httpx.RequestError) while the first request is still running on the server. XComModel.set does DELETE then INSERT, so the second insert hits the PK, and the generic _UniqueConstraintErrorHandler turns that into a 409 the SDK never handles. Making that write succeed is a bug fix and does not need a new Execution API version.
The branch now conflicts with main, though, and the conflict changes the picture:
-
Rebase onto #73005.
task_mapwas removed and its length now lives inxcom.mapped_length, passed throughXComModel.set(..., mapped_length=...). The savepoint here was justified by protecting theTaskMapmerge, which no longer exists. As written, the UPDATE fallback (.values(value=value, dag_result=dag_result)) would also leave a stalemapped_lengthandtimestampon the row. -
Please move the fix into
XComModel.setas a single upsert.airflow.utils.sqlalchemy.build_upsert_stmtalready builds the dialect-specificINSERT … ON CONFLICT DO UPDATE/ON DUPLICATE KEY UPDATE, and its docstring describes this race. Replacing the DELETE+INSERT inairflow/models/xcom.pywith an upsert on(dag_run_id, task_id, map_index, key), updatingvalue,dag_result,mapped_lengthandtimestamp:- fixes every caller, not only this route;
- drops the error-string matching and the savepoint;
- avoids a problem the savepoint approach still has on MySQL. With three or more concurrent writers (the issue reports three), the losers each hold a shared lock on the duplicate row, and their UPDATEs deadlock (1213). That rolls back the whole transaction, not just the savepoint.
-
Don't build a 500 with
e.origin the detail.HTTPException(500, detail=f"Unexpected database error: {e.orig}")sends the raw driver message back to the caller and bypasses the[api] expose_stacktraceredaction that_DatabaseErrorHandlerapplies. If anyIntegrityErrorhandling remains, a bareraiselets the registered handlers produce the usual redacted response. -
Tests should hit the database for real. Patching
XComModel.setto raise a hand-builtIntegrityErrorwith a Postgres message never enters the savepoint (or the upsert) with a real conflict, and never checks the MySQL or SQLite paths. A test that causes an actual PK conflict, for example by seeding a committed row and making the first write collide with it, would run on all three DB backends in CI. Please also move thepatch/IntegrityErrorimports to module level. -
Please drop
airflow-core/newsfragments/69959.bugfix.rst. As with #64614, the commit message is enough for this fix.
Drafted-by: Claude Code (Opus 5); reviewed by @potiuk before posting
Closes #69956
Summary
When two task runners (concurrent execution or task retry) write the same XCom key at roughly the same time, both call
XComModel.setwhich does a DELETE followed by an INSERT. Both DELETEs succeed (they each see the record that was just written by the peer or see nothing), then both try to INSERT the same primary key. The second INSERT hits the unique constraint and the Execution API converts theIntegrityErrorto an HTTP 409, causing the task to fail.Fix
Wrap the
XComModel.setcall in a savepoint (session.begin_nested()) so that a constraint violation only rolls back the XCom write and not any earlier work in the same request (e.g. thetask_mapmerge). TheIntegrityErroris caught and handled with an explicit UPDATE that overwrites the existing entry with the latest value, which is the correct semantic for a retry.Changes
airflow-core/src/airflow/api_fastapi/execution_api/routes/xcoms.pyupdateto thesqlalchemyimportIntegrityErrorimport fromsqlalchemy.excXComModel.setinsession.begin_nested()and catchIntegrityErrorwith an UPDATE fallbackImportant
🛠️ Maintainer triage note for @nagasrisai · by
@potiuk· 2026-07-28 16:11 UTCHelpful heads-up from the maintainers — please address before this PR can be reviewed:
The ball is in your court — you've been assigned to this PR. Fix the above, then mark it Ready for review.
See the Pull Request quality criteria for how to fix each item. There is no rush.
Note: your branch is 270 commits behind
main— please rebase and push again to get up-to-date CI results.Automated triage — may be imperfect; a maintainer takes the next look. We use this two-stage triage process so maintainers' limited time goes to the conversation with you.