This is an automated email from the ASF dual-hosted git repository.
henry3260 pushed a commit to branch v3-3-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/v3-3-test by this push:
new 0337d127e7c Fix Calendar view hanging for Dags with high-frequency
cron schedules (#71263) (#71435)
0337d127e7c is described below
commit 0337d127e7c68477445f2bf206f146535cb4ca94
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