Skip to content

Fix backfill completion race - #68729

Open
PrithviBadiga wants to merge 2 commits into
apache:mainfrom
PrithviBadiga:prithvi/backfill-complete-race
Open

Fix backfill completion race#68729
PrithviBadiga wants to merge 2 commits into
apache:mainfrom
PrithviBadiga:prithvi/backfill-complete-race

Conversation

@PrithviBadiga

Copy link
Copy Markdown
Contributor

Fixes a backfill completion race where a backfill could be marked complete before all associated work had reached a terminal state.

Updates:

  • make _mark_backfills_complete check unresolved BackfillDagRun associations rather than only unfinished DagRun.backfill_id rows
  • preserve dag_run_id on BackfillDagRun rows when a matching DagRun already exists or is in-flight
  • add a regression test covering an in-flight association linked to a queued DagRun

This keeps backfills open until all tracked backfill work has actually resolved.

This closes: #68721

@boring-cyborg boring-cyborg Bot added the area:Scheduler including HA (high availability) scheduler label Jun 18, 2026
@PrithviBadiga
PrithviBadiga force-pushed the prithvi/backfill-complete-race branch from 8ec4798 to cfc9794 Compare June 18, 2026 21:58
@potiuk potiuk added the ready for maintainer review Set after triaging when all criteria pass. label Jun 22, 2026
@PrithviBadiga

Copy link
Copy Markdown
Contributor Author

@ashb @XD-DENG @potiuk Can you check it out ?

@potiuk potiuk 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 — this is a genuine bug and a subtle one: an IN_FLIGHT association with no dag_run_id yet points at work that hasn't finished, so the old ~exists over DagRun alone would happily complete a backfill out from under it. Driving the check off BackfillDagRun instead is the right pivot.

Two things I verified while reading, so you don't need to re-check them: State.unfinished_dr_states is exactly {QUEUED, RUNNING}, so swapping out the local tuple is behaviour-preserving; and _mark_backfills_complete runs on a 30-second call_regular_interval rather than every scheduler pass, so this is a periodic query rather than a hot-loop one. Dropping the redundant dag_run_id=None kwargs is fine too — that's the column default.

Also worth noting this is not a duplicate of #68125 despite the similar title — that one is about the creation race, this one about completion. Different bug, different code path.

Notes inline. And the same two asks I've been making today: this wants a second maintainer's review given it rewrites a scheduler predicate, and please do your own manual verification rather than relying on my read — my review was AI-assisted, and multi-level correlated subqueries are precisely where that goes wrong confidently. Seeing the actual generated SQL and an EXPLAIN from a real backfill would be more convincing than either of our analyses.


Drafted-by: Claude Code (Opus 5); reviewed by @potiuk before posting

and_(
BackfillDagRun.dag_run_id.is_(None),
BackfillDagRun.exception_reason == BackfillDagRunExceptionReason.IN_FLIGHT,
exists(

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.

This is a correlated EXISTS containing a second correlated EXISTS, with the inner one referencing BackfillDagRun.logical_date / .partition_key from two levels out. SQLAlchemy's auto-correlation usually gets this right, but two-level correlation is a classic place for it to silently correlate against the wrong FROM and produce a subtly different predicate.

Could you paste the compiled SQL (print(stmt.compile(compile_kwargs={"literal_binds": True}))) into the PR description for at least Postgres? It'd let a reviewer confirm the correlation is what you intend without reconstructing it mentally.


Drafted-by: Claude Code (Opus 5); reviewed by @potiuk before posting

and_(
BackfillDagRun.logical_date.is_(None),
BackfillDagRun.partition_key.is_not(None),
DagRun.partition_key == BackfillDagRun.partition_key,

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.

The outer correlation is fine — BackfillDagRun has a unique constraint on (backfill_id, dag_run_id), so filtering by backfill_id is indexed.

The inner one I'm less sure about: it scans DagRun filtered by dag_id + state + either logical_date or partition_key. Is DagRun.partition_key indexed? If not, this runs every 30 seconds against what is typically the largest table in the deployment. Worth checking the plan on an instance with a large dag_run table before this lands.


Drafted-by: Claude Code (Opus 5); reviewed by @potiuk before posting

~exists(
select(DagRun.id).where(
and_(DagRun.backfill_id == Backfill.id, DagRun.state.in_(unfinished_states))
select(BackfillDagRun.id)

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.

The test covers the logical_date branch well, including the transition from "stays active" to "completes once the run succeeds". The partition_key branch — logical_date IS NULL with a non-null partition_key — has no coverage, and it's the harder of the two to reason about.

Since the partitioned path is the reason that branch exists, a companion test there would be worth adding.


Drafted-by: Claude Code (Opus 5); reviewed by @potiuk before posting

@eladkal eladkal added this to the Airflow 3.3.1 milestone Jul 30, 2026
@eladkal eladkal added the backport-to-v3-3-test Backport to v3-3-test label Jul 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:Scheduler including HA (high availability) scheduler backport-to-v3-3-test Backport to v3-3-test ready for maintainer review Set after triaging when all criteria pass.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Backfill premature completion (confirmed on Airflow 3.2.2)

4 participants