Skip to content

API: Return 503 when SQLite locks during backfill creation - #67900

Merged
pierrejeambrun merged 1 commit into
apache:mainfrom
lohitkolluri:fix/66726-backfill-sqlite-lock-error
Jul 9, 2026
Merged

API: Return 503 when SQLite locks during backfill creation#67900
pierrejeambrun merged 1 commit into
apache:mainfrom
lohitkolluri:fix/66726-backfill-sqlite-lock-error

Conversation

@lohitkolluri

@lohitkolluri lohitkolluri commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Closes: #66726

On SQLite, creating a backfill through the API can fail with a "database is locked" error when the scheduler, dag-processor, and API server all share the same database.

Before this PR, this caused two problems:

  • The API returned HTTP 500 with a confusing stack trace
  • The backfill row was left in the database with no actual runs, so retrying returned 409 "already running"

After this PR:

  • The API returns HTTP 503 with a clear "database is locked, retry or use Postgres/MySQL" message
  • The half-created backfill is cleaned up so you can retry safely

Changes

Create route — When the database is locked, return 503 with a clear message instead of letting the error bubble up as a 500.

Dry-run route — Same 503 handling, so previewing a backfill doesn't return a 500 either.

Cleanup of partial state — If the lock happens partway through creating a backfill, the half-created backfill is removed so the user can retry:

  • removes the orphan backfill row
  • removes any partial dag-run rows (and their TaskInstances via cascade)
  • safe to call — if the cleanup itself fails, the original error still surfaces and the route still returns 503

SQLite detection — Teach the lock checker to recognize SQLite's "database is locked" message, since SQLite uses a human-readable string instead of a numeric error code like Postgres or MySQL.

OpenAPI / TypeScript — The 503 response is documented in the API spec and the generated UI client.

Tests

  • test_create_backfill_database_locked — checks the 503 mapping works for the create route
  • test_create_backfill_dry_run_database_locked — same check for the dry-run route
  • test_create_backfill_cleans_up_orphan_on_lock_error — checks the cleanup removes the half-created backfill from a real partial state
  • test_create_backfill_cleans_up_after_failed_transaction — checks the cleanup works when SQLAlchemy has already marked the session as needing a rollback
  • test_create_backfill_cleanup_removes_partial_dag_runs — checks partial DagRuns, BackfillDagRun rows, and TaskInstances are all removed
  • All existing backfill endpoint tests still pass
Was generative AI tooling used to co-author this PR?

@23tae 23tae 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.

Thanks for working on this.

#66726 also reports that the backfill may still be created even though the request fails.

This PR seems to improve the error response from 500 to 503, but it is not clear whether it changes the partial-success behavior. Could we clarify this in the PR description?

Comment thread airflow-core/src/airflow/api_fastapi/core_api/routes/public/backfills.py Outdated
@lohitkolluri
lohitkolluri force-pushed the fix/66726-backfill-sqlite-lock-error branch from d9ed03e to 8962d5e Compare June 3, 2026 06:56
@lohitkolluri
lohitkolluri requested review from XD-DENG and ashb as code owners June 3, 2026 06:56
@lohitkolluri
lohitkolluri force-pushed the fix/66726-backfill-sqlite-lock-error branch from 8962d5e to 2c511e9 Compare June 3, 2026 07:03
@lohitkolluri
lohitkolluri requested a review from 23tae June 3, 2026 07:06
@lohitkolluri lohitkolluri changed the title fix(backfill): Return 503 instead of 500 on SQLite lock error fix(backfill): Handle SQLite lock errors with atomic creation and graceful 503 Jun 3, 2026

@23tae 23tae 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.

Thanks for the update. I left a few comments.

Comment thread airflow-core/src/airflow/api_fastapi/core_api/routes/public/backfills.py Outdated
Comment thread airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_backfills.py Outdated
@lohitkolluri
lohitkolluri force-pushed the fix/66726-backfill-sqlite-lock-error branch from c1af990 to cc00931 Compare June 3, 2026 16:01
@lohitkolluri
lohitkolluri requested a review from 23tae June 3, 2026 16:05

@henry3260 henry3260 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.

Thanks for the fix!

Comment thread airflow-core/src/airflow/models/backfill.py Outdated
Comment thread airflow-core/newsfragments/67900.bugfix.rst Outdated
@lohitkolluri
lohitkolluri requested a review from henry3260 June 3, 2026 17:49
@lohitkolluri
lohitkolluri force-pushed the fix/66726-backfill-sqlite-lock-error branch 3 times, most recently from 250fc2e to 3358470 Compare June 4, 2026 03:00

@23tae 23tae 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.

I left a couple of comments.

Since implementing a retry loop in the API route introduces edge cases and makes the code complex, how about we just remove it entirely? Simply catching the OperationalError and raising an HTTP 503 on a database lock seems like a much cleaner approach.

Comment thread airflow-core/src/airflow/api_fastapi/core_api/routes/public/backfills.py Outdated
@lohitkolluri
lohitkolluri force-pushed the fix/66726-backfill-sqlite-lock-error branch from 3358470 to 8950a80 Compare June 4, 2026 04:30
@lohitkolluri
lohitkolluri requested a review from 23tae June 4, 2026 04:31
@lohitkolluri
lohitkolluri force-pushed the fix/66726-backfill-sqlite-lock-error branch from 8950a80 to dc36dae Compare June 4, 2026 04:45
@lohitkolluri
lohitkolluri force-pushed the fix/66726-backfill-sqlite-lock-error branch 2 times, most recently from 4e546d7 to 67d50e1 Compare June 9, 2026 20:25
@lohitkolluri
lohitkolluri force-pushed the fix/66726-backfill-sqlite-lock-error branch 4 times, most recently from a4cabdb to 2482bdd Compare June 28, 2026 19:03
@lohitkolluri

Copy link
Copy Markdown
Contributor Author

@pierrejeambrun

Reproduced the 500 on macOS — 20 concurrent POST /backfills, 2/2 runs hit the lock + orphan. Repro steps and the full traceback posted on #66726.

Also caught an edge case in the cleanup: SQLAlchemy deactivates the session on flush failure, so the first session.execute() in the previous cleanup raised InvalidRequestError silently. Fixed with a session.rollback() at the top of the helper + a test that reproduces the real failed-session state.

Ready for re-review.

@pierrejeambrun pierrejeambrun left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for iterating on this. The direction is reasonable for the SQLite dev case, and I confirmed locally that the production path does return a clean 503 (Database is locked ... use PostgreSQL or MySQL) instead of the 500 — nice.

Two things block it though, both inline: the two ...database_locked tests actually fail as written, and the cleanup path that removes partial runs isn't really tested. A couple of smaller notes too.

Comment thread airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_backfills.py Outdated
Comment thread airflow-core/newsfragments/67900.bugfix.rst Outdated
Comment thread airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_backfills.py Outdated
Comment thread airflow-core/src/airflow/models/backfill.py Outdated
Comment thread airflow-core/src/airflow/models/backfill.py Outdated
@lohitkolluri
lohitkolluri force-pushed the fix/66726-backfill-sqlite-lock-error branch 2 times, most recently from d62d484 to dab61df Compare July 1, 2026 15:36
@lohitkolluri lohitkolluri changed the title fix(backfill): Return 503 with retryable message on SQLite lock errors API: Return 503 when SQLite locks during backfill creation Jul 1, 2026
@lohitkolluri
lohitkolluri force-pushed the fix/66726-backfill-sqlite-lock-error branch 3 times, most recently from 346d1b4 to 3504a1b Compare July 2, 2026 20:38
@lohitkolluri

lohitkolluri commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

Hey! A few CI checks were failing, so I pushed a patch to address them.

SQLite backfill creation via the REST API could return HTTP 500 and
leave an orphan Backfill row when the scheduler and API server contend
for the database, blocking retries with 409. Map lock errors to HTTP 503
with a clear message and clean up partial DagRuns and TaskInstances so
users can retry against a clean slate.

Signed-off-by: Lohit Kolluri <lohitkolluri@gmail.com>
@lohitkolluri
lohitkolluri force-pushed the fix/66726-backfill-sqlite-lock-error branch from b99435e to 9354632 Compare July 6, 2026 18:37

@pierrejeambrun pierrejeambrun left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM

@pierrejeambrun pierrejeambrun added the backport-to-v3-3-test Backport to v3-3-test label Jul 9, 2026
@pierrejeambrun pierrejeambrun added this to the Airflow 3.3.1 milestone Jul 9, 2026
@pierrejeambrun
pierrejeambrun merged commit 2fb2321 into apache:main Jul 9, 2026
148 checks passed
@boring-cyborg

boring-cyborg Bot commented Jul 9, 2026

Copy link
Copy Markdown

Awesome work, congrats on your first merged pull request! You are invited to check our Issue Tracker for additional contributions.

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Backport successfully created: v3-3-test

Note: As of Merging PRs targeted for Airflow 3.X
the committer who merges the PR is responsible for backporting the PRs that are bug fixes (generally speaking) to the maintenance branches.

In matter of doubt please ask in #release-management Slack channel.

Status Branch Result
v3-3-test PR Link

potiuk pushed a commit that referenced this pull request Jul 13, 2026
#67900) (#69659)

SQLite backfill creation via the REST API could return HTTP 500 and
leave an orphan Backfill row when the scheduler and API server contend
for the database, blocking retries with 409. Map lock errors to HTTP 503
with a clear message and clean up partial DagRuns and TaskInstances so
users can retry against a clean slate.
(cherry picked from commit 2fb2321)

Signed-off-by: Lohit Kolluri <lohitkolluri@gmail.com>
Co-authored-by: Lohit Kolluri <lohitkolluri@gmail.com>
vatsrahul1001 pushed a commit that referenced this pull request Aug 5, 2026
#67900) (#69659)

SQLite backfill creation via the REST API could return HTTP 500 and
leave an orphan Backfill row when the scheduler and API server contend
for the database, blocking retries with 409. Map lock errors to HTTP 503
with a clear message and clean up partial DagRuns and TaskInstances so
users can retry against a clean slate.
(cherry picked from commit 2fb2321)

Signed-off-by: Lohit Kolluri <lohitkolluri@gmail.com>
Co-authored-by: Lohit Kolluri <lohitkolluri@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:API Airflow's REST/HTTP API backport-to-v3-3-test Backport to v3-3-test

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Creating a backfill via Airflow API results in internal server error if SQLite is used

4 participants