zh-jn commented on issue #58554:
URL: https://github.com/apache/airflow/issues/58554#issuecomment-5535044661

   Adding a root-cause data point for future readers landing here. On Airflow 
**3.2.2** the same symptom (backfill dag_run `state=running`, all task 
instances `state=NULL`, `last_scheduling_decision=NULL` forever) reproduces 
deterministically whenever running 
   non-backfill dag_runs ≥ `max_dagruns_per_loop_to_schedule`. Raising the 
config does mask it, but the underlying cause is a sort-direction bug in 
`get_running_dag_runs_to_examine`.
   
   **Root cause** — `airflow-core/src/airflow/models/dagrun.py:619`:
   
   ```python
   .order_by(
     nulls_first(cast("ColumnElement[Any]", BackfillDagRun.sort_ordinal), 
session=session),   # ← reversed
     nulls_first(cast("ColumnElement[Any]", cls.last_scheduling_decision), 
session=session),
     cls.run_after,
   )
   ```
   
   `sort_ordinal` lives on the LEFT-JOINed `backfill_dag_run` row and is `NULL` 
for every non-backfill dag_run. `nulls_first` therefore sorts *non-backfill* 
runs before backfill runs, exactly the opposite of the field's apparent intent. 
With
   `.limit(DEFAULT_DAGRUNS_TO_EXAMINE)` (default 20) and any healthy multi-DAG 
scheduled workload, backfill dag_runs never fit into the window and are 
silently skipped — no log, no `scheduled_by_job_id`, no 
`last_scheduling_decision`.
   
   **Reproducer** (read-only, in the scheduler pod):
   
   ```python
   # Production query — backfill dag_run absent
   from airflow.models.dagrun import DagRun
   from airflow.utils.session import create_session
   with create_session() as s:
     runs = list(DagRun.get_running_dag_runs_to_examine(session=s))
     print(len(runs), sum(1 for r in runs if r.backfill_id is not None))
   
   # Same query with nullslast(sort_ordinal) — backfill dag_runs jump to the top
   from sqlalchemy import select, false, func, nullslast
   from airflow.models.backfill import BackfillDagRun
   from airflow.models.dag import DagModel
   from airflow.utils.state import DagRunState
   with create_session() as s:
     q = (select(DagRun)
           .where(DagRun.state == DagRunState.RUNNING)
           .join(DagModel, DagModel.dag_id == DagRun.dag_id)
           .join(BackfillDagRun, BackfillDagRun.dag_run_id == DagRun.id, 
isouter=True)
           .where(DagModel.is_paused == false(), DagModel.is_stale == false())
           .order_by(nullslast(BackfillDagRun.sort_ordinal), DagRun.run_after)
           .where(DagRun.run_after <= func.now())
           .limit(DagRun.DEFAULT_DAGRUNS_TO_EXAMINE))
     runs = list(s.scalars(q).unique())
     print(len(runs), sum(1 for r in runs if r.backfill_id is not None))
   ```
   
   On the same session we saw production return 20 runs / 0 backfill, and 
`nullslast` return 16 runs / **2 backfill** (both stuck ones, top of the list).
   
   **One-line fix** — swap `nulls_first` → `nulls_last` on the `sort_ordinal` 
column only (keep `nulls_first` on `last_scheduling_decision`, that one is 
intentional):
   
   ```diff
   - nulls_first(cast("ColumnElement[Any]", BackfillDagRun.sort_ordinal), 
session=session),
   + nulls_last (cast("ColumnElement[Any]", BackfillDagRun.sort_ordinal), 
session=session),
   ```
   
   Because this issue is closed I'll open a fresh one referencing here + #49508 
+ #69658 so the fix has a place to land. Wanted to leave the diagnosis in this 
thread since it's the top search hit for the symptom.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to