Skip to content

Stop the scheduler deadlocking with the triggerer on trigger timeouts - #75962

Open
rjgoyln wants to merge 5 commits into
apache:mainfrom
rjgoyln:fix-trigger-timeout-sweep-deadlock
Open

rjgoyln wants to merge 5 commits into
apache:mainfrom
rjgoyln:fix-trigger-timeout-sweep-deadlock

Conversation

@rjgoyln

@rjgoyln rjgoyln commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Continues #72595, closed by the open PR limit.

Summary

The deferred-task timeout sweep fails every timed-out task instance in one unbounded UPDATE. On MySQL that statement can wait on rows the triggerer is writing while the triggerer waits on rows the sweep holds, and InnoDB kills one side.

The sweep's retry does not prevent this: under the same triggerer traffic the cycle can form again, and once the retries run out the exception ends the scheduler loop, which is the crash #56670 reports.

closes: #56670

Change

  • Claim the timed-out rows with FOR UPDATE SKIP LOCKED and update only those.
  • Claim at most 500 task instances per tick.
  • Roll back a failed attempt before retrying, as adopt_or_reset_orphaned_tasks does.

With SKIP LOCKED the sweep no longer waits on rows the triggerer holds, and the UPDATE touches only rows its own transaction already claimed. Where with_row_locks is a no-op (SQLite, MariaDB, use_row_level_locking = False), the rollback is what lets the retry start from a clean transaction.

Tests

MySQL 8.0.46, 450 timed-out deferred task instances, four writers re-deferring ten rows per transaction, three 30-second runs. The writers lock rows in descending id order: one order across writers, so they do not deadlock each other, and the reverse of the sweep's scan.

InnoDB 1213 deadlocks sweep rolled back
before 1886 / 1743 / 1868 482 / 457 / 454
after 0 / 0 / 0 0 / 0 / 0

With one of three timed-out rows locked by another transaction, the sweep now skips that row and returns in 0.03s. Before, it waited out innodb_lock_wait_timeout and raised 1205 without failing any of the three.

Behavior change

A tick fails at most 500 timed-out task instances; a larger backlog drains over later ticks.


Was generative AI tooling used to co-author this PR?
  • Yes — Claude Code (Opus 5.5)

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

…meouts

The timeout sweep issued one blind, unbounded UPDATE over every deferred
task instance past its trigger timeout. On MySQL that statement waits on
rows the triggerer is writing while the triggerer waits on rows the sweep
already holds, so InnoDB breaks the cycle by killing one of them. The
retry the sweep already had does not save it: the deadlock recurs on
every tick, and once the retries are exhausted the exception ends the
scheduler loop. Deployments running more than one triggerer hit this
often enough to lose runs.

Claiming the rows up front with SKIP LOCKED means the sweep never waits
on a row another writer holds -- it leaves those for the next tick -- so
it can no longer be one side of a lock cycle. The claim also bounds how
many rows a single tick may lock, so a large backlog of timed-out tasks
cannot stall the scheduler loop, and rolling back a failed attempt lets
the existing retry start from a clean transaction on the backends where
row locks are unavailable and a cycle is still possible.
@rjgoyln
rjgoyln marked this pull request as ready for review October 29, 2026 13:18
@rjgoyln
rjgoyln requested review from XD-DENG and ashb as code owners October 29, 2026 13:18
Comment thread airflow-core/tests/unit/jobs/test_scheduler_job.py Outdated
Comment thread airflow-core/newsfragments/75962.bugfix.rst Outdated
Comment thread airflow-core/src/airflow/jobs/scheduler_job_runner.py

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MySql Deadlock in check_trigger_timeouts

2 participants