Skip to content

Retry scheduled DagRun creation after database errors - #74761

Open
boqiang wants to merge 10 commits into
apache:mainfrom
boqiang:codex/retry-dagrun-db-errors
Open

boqiang wants to merge 10 commits into
apache:mainfrom
boqiang:codex/retry-dagrun-db-errors

Conversation

@boqiang

@boqiang boqiang commented Oct 14, 2026 •

Copy link
Copy Markdown

Database and ORM flush errors while loading a serialized Dag or creating a scheduled DagRun are caught by per-Dag handlers. Continuing with the failed SQLAlchemy Session can mask the original retryable error with PendingRollbackError and interrupt scheduling.

Propagate both DBAPIError and StaleDataError to the existing _create_dagruns_for_dags retry boundary, matching the error categories handled by retry_db_transaction. The owner rolls back before re-querying candidates and reacquiring their row locks. Ordinary per-Dag error handling, retry limits, and CommitProhibitorGuard remain unchanged. Persistent errors still propagate after retry exhaustion; this does not provide per-Dag isolation for persistent failures.

Regression tests cover transient failures and retry exhaustion at both interception points using real failed flushes: a duplicate Pool constraint violation and a zero-row Pool UPDATE that raises SQLAlchemy's StaleDataError. They verify the original exception on exhaustion, bounded retries, an active Session after rollback, successful DagRun creation after recovery, and restoration of the Pool data.

The MySQL-only test uses two real InnoDB transactions to cause deadlock 1213 during pending Dag metadata flush, with autoflush enabled and disabled, after a DagRun has already been created in the batch. It checks a fresh locked candidate query, exactly one run per scheduled Dag, and preservation of concurrent metadata updates.

Related: #59120

Validation for the StaleDataError follow-up, through Breeze on Python 3.11 with the official Linux amd64 CI image:

Check Result
Complete MySQL 8.4 scheduler test file 570 passed, 2 skipped
Expanded SQLite regression cases 8 passed, 2 MySQL-only cases skipped
Expanded PostgreSQL regression cases 8 passed, 2 MySQL-only cases skipped
Restore the pre-review production handlers while retaining the new tests All 4 StaleDataError cases fail with PendingRollbackError after the native zero-row UPDATE error
Ruff, airflow-core mypy, fast and manual prek checks Passed

For the initial contribution, reverting the DBAPIError propagation made all 6 original MySQL cases fail with PendingRollbackError after the original constraint error or deadlock. The initial broader selective checks had seven failures that were independently reproduced without the PR: five plugin-count failures and two LangSDK file-descriptor failures. Those broader selective checks and documentation build have not been rerun for this follow-up; the initial documentation build passed.

These tests exercise the transaction boundary directly and do not establish multi-scheduler process acceptance.


Was generative AI tooling used to co-author this PR?
  • Yes — Codex (GPT-6)

Generated-by: Codex (GPT-6) following the guidelines

Let the owning transaction retry the original database exception instead of continuing with a failed Session and raising PendingRollbackError. Cover transient failures, retry exhaustion, and real MySQL deadlock recovery without changing HA commit guards.
@boring-cyborg

boring-cyborg Bot commented Oct 14, 2026

Copy link
Copy Markdown

Congratulations on your first Pull Request and welcome to the Apache Airflow community! If you have any issues or are unsure about any anything please check our Contributors' Guide
Here are some useful points:

  • Pay attention to the quality of your code (ruff, mypy and type annotations). Our prek-hooks will help you with that.
  • In case of a new feature add useful documentation (in docstrings or in docs/ directory). Adding a new operator? Check this short guide Consider adding an example Dag that shows how users should use it.
  • Consider using Breeze environment for testing locally, it's a heavy docker but it ships with a working Airflow and a lot of integrations.
  • Be patient and persistent. It might take some time to get a review or get the final approval from Committers.
  • Please follow ASF Code of Conduct for all communication including (but not limited to) comments on Pull Requests, Mailing list and Slack.
  • Be sure to read the Airflow Coding style.
  • Always keep your Pull Requests rebased, otherwise your build might fail due to changes not related to your commits.
    Apache Airflow is a community-driven project and together we are making it better 🚀.
    In case of doubts contact the developers at:
    Mailing List: [email protected]
    Slack: https://s.apache.org/airflow-slack

@boqiang
boqiang marked this pull request as ready for review October 14, 2026 20:02
@boqiang
boqiang requested review from XD-DENG and ashb as code owners October 14, 2026 20:02
@boqiang

boqiang commented Oct 15, 2026

Copy link
Copy Markdown
Author

Could a maintainer please review and approve the pending CI workflows? All three workflows are awaiting approval and have not started. This PR only changes scheduler error propagation and regression tests; it does not modify workflow files. Thank you.

@SEPURI-SAI-KRISHNA SEPURI-SAI-KRISHNA left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ran the patch locally since the GitHub Actions workflows have not run yet. On SQLite your four non-MySQL tests pass and the two MySQL ones skip. Reverting only the production change and keeping the tests, all four fail with PendingRollbackError after the original IntegrityError, so this reproduces on SQLite and is not MySQL specific. Full test_scheduler_job.py: 473 passed before the patch, 477 after, with the same pre-existing 2 failures (statsd missing) and 75 errors (airflow_shared missing) in both.

Coverage looks complete. _create_dagruns_for_dags is already decorated @retry_db_transaction and run_with_db_retries rolls the session back before each attempt, so the recovery path existed and the per-Dag handlers were making it unreachable. _create_dag_runs_asset_triggered has no except around its create_dagrun, and both paths reach _get_current_dag, which the first hunk covers.

One gap: run_with_db_retries retries on (DBAPIError, StaleDataError), but both new handlers re-raise DBAPIError only. StaleDataError is not a DBAPIError subclass, so it still lands in the broad except Exception and leaves the session in the state this PR fixes. It appears nowhere in scheduler_job_runner.py today so I have no reproduction, but if the boundary retries it the handler should too: except (DBAPIError, StaleDataError): raise.

ORM flush failures can leave the Session unusable just like driver errors. Keeping both retryable error categories visible to the owning transaction lets its existing rollback and retry policy recover the batch instead of masking the original failure.
@boqiang

boqiang commented Oct 16, 2026

Copy link
Copy Markdown
Author

Thanks for catching the StaleDataError gap. Confirmed with a real failed ORM flush, and addressed in a53b3bc.

Both handlers now propagate (DBAPIError, StaleDataError), matching the existing retry boundary. The expanded regression uses a pending Pool UPDATE whose row has been deleted through the raw connection, so SQLAlchemy itself raises StaleDataError and invalidates the Session. It covers both interception points, transient recovery and retry exhaustion, the original exception identity, bounded retries, and restoration of the Pool data after rollback.

With only the production handlers restored to their pre-review version, all four new cases fail with PendingRollbackError after the original StaleDataError. With the fix, the expanded cases pass on SQLite and PostgreSQL (8 passed, 2 MySQL-only cases skipped on each). The complete MySQL 8.4 scheduler test file passes: 570 passed, 2 skipped, including both native InnoDB deadlock/autoflush cases. Ruff, airflow-core mypy, and fast/manual prek checks also pass.

CommitProhibitorGuard and the retry policy are unchanged. The PR description now includes these results and keeps the persistent-failure isolation and multi-scheduler validation limits explicit.

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.

2 participants