You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airflow.apache.org by ra...@apache.org on 2026/08/13 08:08:45 UTC
(airflow) 24/32: Fix Calendar view hanging for Dags with high-frequency cron schedules (#71263) (#71435)
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 <pi...@gmail.com>
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 <16...@users.noreply.github.com>
---
.../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