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
