This is an automated email from the ASF dual-hosted git repository.

vatsrahul1001 pushed a commit to branch v3-3-test
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit 551c0b0eb0e1c1ba24d39a6c47ab48512eeb4851
Author: Pierre Jeambrun <[email protected]>
AuthorDate: Tue Aug 11 15:43:00 2026 +0200

    Fix Calendar view hanging for Dags with high-frequency cron schedules 
(#71263) (#71435)
    
    The planned-runs computation for cron timetables iterates croniter until the
    year boundary with no cap, while the generic-timetable path stops at
    MAX_PLANNED_RUNS. For "*/5 * * * *" that is ~105k iterations (~2s) per 
calendar
    request, ~520k (~7s) for a minutely cron, and ~31M (minutes of CPU) for a
    seconds-resolution cron - enough for any user with Dag read access to pin an
    API server worker just by opening the Calendar tab. Apply the same
    MAX_PLANNED_RUNS cap the generic path has used since the endpoint was added.
    
    (cherry picked from commit d1e5f1a61340581c0a5b3bc0bf7af57cbb95ec58)
    
    Co-authored-by: Jonathan Brown 
<[email protected]>
---
 .../api_fastapi/core_api/services/ui/calendar.py   |  6 +++-
 .../core_api/routes/ui/test_calendar.py            | 39 ++++++++++++++++++++++
 2 files changed, 44 insertions(+), 1 deletion(-)

diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/services/ui/calendar.py 
b/airflow-core/src/airflow/api_fastapi/core_api/services/ui/calendar.py
index ebd5a38453e..3c22472f734 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/services/ui/calendar.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/services/ui/calendar.py
@@ -17,6 +17,7 @@
 from __future__ import annotations
 
 import collections
+import itertools
 from collections.abc import Iterator, Sequence
 from datetime import datetime
 from typing import Literal, cast
@@ -218,7 +219,10 @@ class CalendarService:
             ret_type=datetime,
         )
 
-        for dt in dates_iter:
+        # Cap the iteration like _calculate_timetable_planned_runs does; a 
high-frequency
+        # expression (e.g. "* * * * *", or a seconds-resolution cron) would 
otherwise take
+        # hundreds of thousands of steps before hitting the year boundary.
+        for dt in itertools.islice(dates_iter, self.MAX_PLANNED_RUNS):
             if dt is None or dt.year != year:
                 break
             if dag.end_date and dt > dag.end_date:
diff --git 
a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_calendar.py 
b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_calendar.py
index 7bd0d69153c..5c218e599c2 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_calendar.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_calendar.py
@@ -23,6 +23,7 @@ import pendulum
 import pytest
 from sqlalchemy.orm import Session
 
+from airflow.api_fastapi.core_api.services.ui.calendar import CalendarService
 from airflow.providers.standard.operators.empty import EmptyOperator
 from airflow.sdk import CronPartitionTimetable
 from airflow.utils.session import NEW_SESSION, provide_session
@@ -320,3 +321,41 @@ class TestPartitionedCalendar:
         assert response.status_code == 200
         body = response.json()
         assert body == result
+
+
+class TestCalendarPlannedRunsCap:
+    """A high-frequency cron must stop at MAX_PLANNED_RUNS instead of 
iterating to the year boundary."""
+
+    DAG_NAME = "test_minutely_dag"
+
+    @pytest.fixture(autouse=True)
+    @provide_session
+    def setup_dag_runs(self, dag_maker, *, session: Session = NEW_SESSION) -> 
None:
+        clear_db_runs()
+        clear_db_dags()
+        with dag_maker(
+            self.DAG_NAME,
+            schedule="* * * * *",
+            start_date=datetime(2025, 6, 1),
+            catchup=False,
+            serialized=True,
+            session=session,
+        ):
+            EmptyOperator(task_id="test_task1")
+        dag_maker.create_dagrun(
+            run_id="run_1",
+            state=DagRunState.SUCCESS,
+            logical_date=datetime(2025, 6, 1),
+        )
+        dag_maker.sync_dagbag_to_db()
+        session.commit()
+
+    def teardown_method(self) -> None:
+        clear_db_runs()
+        clear_db_dags()
+
+    def test_planned_runs_capped_for_high_frequency_cron(self, test_client):
+        response = test_client.get(f"/calendar/{self.DAG_NAME}")
+        assert response.status_code == 200
+        planned = [r for r in response.json()["dag_runs"] if r["state"] == 
"planned"]
+        assert sum(r["count"] for r in planned) == 
CalendarService.MAX_PLANNED_RUNS

Reply via email to