kaxil commented on code in PR #64109:
URL: https://github.com/apache/airflow/pull/64109#discussion_r4243926254


##########
airflow-core/src/airflow/models/dagrun.py:
##########
@@ -811,16 +811,32 @@ def get_queued_dag_runs_to_set_running(cls, session: 
Session) -> ScalarResult[Da
             .subquery()
         )
 
-        query = (
-            select(cls)
-            .where(cls.state == DagRunState.QUEUED)
+        available_dagruns_rn = (
+            select(
+                DagRun.dag_id,
+                DagRun.id,
+                running_drs.c.num_running,
+                func.row_number()
+                .over(
+                    partition_by=[DagRun.dag_id, DagRun.backfill_id],
+                    order_by=[
+                        nulls_first(cast("ColumnElement[Any]", 
BackfillDagRun.sort_ordinal), session=session),
+                        nulls_first(
+                            cast("ColumnElement[Any]", 
cls.last_scheduling_decision), session=session
+                        ),
+                        DagRun.run_after,
+                    ],
+                )
+                .label("rn"),
+            )
+            .where(DagRun.state == DagRunState.QUEUED)

Review Comment:
   The window ranks every queued run in the partition, but `run_after <= now()` 
is only applied on the outer query (line 888), after `rn <= cap - num_running`. 
So a run that isn't due yet can take a slot and then get dropped, and the ready 
run ranked after it never comes back.
   
   That's reachable on main today: a reverse backfill on a partitioned 
timetable can have `to_date` in the future (validation in `backfill.py` only 
rejects when both bounds are in the future, and `_get_info_list` keeps future 
partitions for partitioned timetables). Reversing gives those future partitions 
`sort_ordinal` 1..N, which is the first window key. With the default backfill 
`max_active_runs=10` and ten or more future partitions, the backfill returns 
nothing until they mature, where main starts the newest past partitions right 
away.
   
   Moving the `run_after` filter into this subquery fixes it, and the comment 
at line 873 would then be true. Since you'd be touching the subquery anyway, 
I'd move the paused/stale `DagModel` join in here as well. I don't think the 
planner can push a join condition into a subquery that computes a window 
function, so as written the ranking also runs over queued runs of paused Dags.



##########
airflow-core/tests/unit/jobs/test_scheduler_job.py:
##########
@@ -4477,6 +4480,94 @@ def 
test_runs_are_created_after_max_active_runs_was_reached(self, dag_maker, ses
         dag_runs = DagRun.find(dag_id=dag.dag_id, session=session)
         assert len(dag_runs) == 2
 
+    def test_runs_are_not_starved_by_max_active_runs_limit(self, dag_maker, 
session):

Review Comment:
   This one also passes with `dagrun.py` reverted to main (checked in breeze), 
same as ashb's point on the test below. The three Dags share `start_date` and 
schedule, so their queued runs interleave by `run_after`, main's top 20 holds 
about 7 per Dag, and the Python guard still starts 3 of each.
   
   To fail on main, one Dag's backlog has to sort entirely ahead of a Dag that 
has capacity. For example: Dag A with `max_active_runs=1` and 25 queued runs, 
Dag B starting a day later with `max_active_runs=3` and 5 queued runs, then 
assert `{A: 1, B: 3}` running. Main gives `{A: 1}` and this branch passes.



##########
airflow-core/src/airflow/models/dagrun.py:
##########
@@ -830,37 +846,42 @@ def get_queued_dag_runs_to_set_running(cls, session: 
Session) -> ScalarResult[Da
                 ),
                 isouter=True,
             )
-            .join(Backfill, isouter=True)
+            .subquery()
+        )
+
+        query = (
+            select(cls)
             .join(
-                running_drs,
+                available_dagruns_rn,
+                available_dagruns_rn.c.id == DagRun.id,
+            )
+            .join(
+                DagModel,
                 and_(
-                    running_drs.c.dag_id == DagRun.dag_id,
-                    coalesce(running_drs.c.backfill_id, text("-1"))
-                    == coalesce(DagRun.backfill_id, text("-1")),
+                    DagModel.dag_id == cls.dag_id,
+                    DagModel.is_paused == false(),
+                    DagModel.is_stale == false(),
                 ),
-                isouter=True,
             )
+            .order_by(

Review Comment:
   On main the outer `ORDER BY` started with `sort_ordinal` NULLS FIRST so 
backfill runs sort after every non-backfill run. That key now only lives in the 
window, so it no longer orders anything across partitions. Backfill runs 
usually have an older `run_after`, so they now win the 20-row limit over 
scheduled runs. In a breeze run with one backfill (cap 3) and a regular Dag 
with 30 queued runs, main's query started 20 scheduled runs and this one 
started 3 backfill plus 17 scheduled.
   
   Could you select `sort_ordinal` from the subquery and put it back as the 
first key here? 
`test_backfill_runs_are_started_with_lower_priority_catchup_true` can't catch 
this as rewritten, since 1 + 3 + 16 startable runs exactly fill the limit. It 
needs more startable runs than `DEFAULT_DAGRUNS_TO_EXAMINE`, and dag2's 
`max_active_runs` set explicitly instead of relying on the 16 from config.



##########
airflow-core/tests/unit/jobs/test_scheduler_job.py:
##########
@@ -7752,41 +7845,55 @@ def _running_counts():
             EmptyOperator(task_id="mytask")
 
         dr = dag_maker.create_dagrun(run_type=DagRunType.SCHEDULED, 
state=State.QUEUED)
-        for _ in range(9):
+        for _ in range(29):
             dr = dag_maker.create_dagrun_after(dr, 
run_type=DagRunType.SCHEDULED, state=State.QUEUED)
 
         # initial state -- nothing is running
         assert dag1_non_b_running == 0
         assert dag1_b_running == 0
         assert total_running == 0
-        assert session.scalar(select(func.count(DagRun.id))) == 46
+        assert session.scalar(select(func.count(DagRun.id))) == 66
         assert session.scalar(select(func.count()).where(DagRun.dag_id == 
dag1_dag_id)) == 36
 
         # now let's run it once
         self.job_runner._start_queued_dagruns(session)
         session.flush()
 
         # after running the scheduler one time, observe that only one dag run 
is started

Review Comment:
   This still says only one dag run is started, but the assert below is now 20. 
The `session.flush()` right after `session.commit()` on line 7883 doesn't do 
anything either.



-- 
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