Skip to content

Commit 9c40d21

Browse files
[v3-1-test] Fix custom timetable generate_run_id not called for manual triggers (#56373) (#56699)
(cherry picked from commit c28b211) Co-authored-by: Nils Werner <[email protected]>
1 parent d2f3c45 commit 9c40d21

File tree

3 files changed

+80
-15
lines changed

3 files changed

+80
-15
lines changed

airflow-core/src/airflow/api/common/trigger_dag.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,10 @@ def _trigger_dag(
9292
else:
9393
data_interval = None
9494

95-
run_id = run_id or DagRun.generate_run_id(
95+
run_id = run_id or dag.timetable.generate_run_id(
9696
run_type=DagRunType.MANUAL,
97-
logical_date=coerced_logical_date,
9897
run_after=timezone.coerce_datetime(run_after),
98+
data_interval=data_interval,
9999
)
100100

101101
# This intentionally does not use 'session' in the current scope because it

airflow-core/src/airflow/api_fastapi/core_api/datamodels/dag_run.py

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
from airflow._shared.timezones import timezone
2727
from airflow.api_fastapi.core_api.base import BaseModel, StrictBaseModel
2828
from airflow.api_fastapi.core_api.datamodels.dag_versions import DagVersionResponse
29-
from airflow.models import DagRun
3029
from airflow.timetables.base import DataInterval
3130
from airflow.utils.state import DagRunState
3231
from airflow.utils.types import DagRunTriggeredByType, DagRunType
@@ -129,10 +128,10 @@ def validate_context(self, dag: SerializedDAG) -> dict:
129128
)
130129
run_after = data_interval.end
131130

132-
run_id = self.dag_run_id or DagRun.generate_run_id(
133-
run_type=DagRunType.SCHEDULED,
134-
logical_date=coerced_logical_date,
135-
run_after=run_after,
131+
run_id = self.dag_run_id or dag.timetable.generate_run_id(
132+
run_type=DagRunType.MANUAL,
133+
run_after=timezone.coerce_datetime(run_after),
134+
data_interval=data_interval,
136135
)
137136
return {
138137
"run_id": run_id,
@@ -143,14 +142,6 @@ def validate_context(self, dag: SerializedDAG) -> dict:
143142
"note": self.note,
144143
}
145144

146-
@model_validator(mode="after")
147-
def validate_dag_run_id(self):
148-
if not self.dag_run_id:
149-
self.dag_run_id = DagRun.generate_run_id(
150-
run_type=DagRunType.MANUAL, logical_date=self.logical_date, run_after=self.run_after
151-
)
152-
return self
153-
154145

155146
class DAGRunsBatchBody(StrictBaseModel):
156147
"""List DAG Runs body for batch endpoint."""

airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_run.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from airflow.providers.standard.operators.empty import EmptyOperator
3434
from airflow.sdk.definitions.asset import Asset
3535
from airflow.sdk.definitions.param import Param
36+
from airflow.timetables.interval import CronDataIntervalTimetable
3637
from airflow.utils.session import provide_session
3738
from airflow.utils.state import DagRunState, State
3839
from airflow.utils.types import DagRunTriggeredByType, DagRunType
@@ -50,9 +51,44 @@
5051

5152
if TYPE_CHECKING:
5253
from airflow.models.dag_version import DagVersion
54+
from airflow.timetables.base import DataInterval
5355

5456
pytestmark = pytest.mark.db_test
5557

58+
59+
class CustomTimetable(CronDataIntervalTimetable):
60+
"""Custom timetable that generates custom run IDs."""
61+
62+
def generate_run_id(
63+
self,
64+
*,
65+
run_type: DagRunType,
66+
run_after,
67+
data_interval: DataInterval | None,
68+
**kwargs,
69+
) -> str:
70+
if data_interval:
71+
return f"custom_{data_interval.start.strftime('%Y%m%d%H%M%S')}"
72+
return f"custom_manual_{run_after.strftime('%Y%m%d%H%M%S')}"
73+
74+
75+
@pytest.fixture
76+
def custom_timetable_plugin(monkeypatch):
77+
"""Fixture to register CustomTimetable for serialization."""
78+
from airflow import plugins_manager
79+
from airflow.utils.module_loading import qualname
80+
81+
timetable_class_name = qualname(CustomTimetable)
82+
existing_timetables = getattr(plugins_manager, "timetable_classes", None) or {}
83+
84+
monkeypatch.setattr(plugins_manager, "initialize_timetables_plugins", lambda: None)
85+
monkeypatch.setattr(
86+
plugins_manager,
87+
"timetable_classes",
88+
{**existing_timetables, timetable_class_name: CustomTimetable},
89+
)
90+
91+
5692
DAG1_ID = "test_dag1"
5793
DAG1_DISPLAY_NAME = "test_dag1"
5894
DAG2_ID = "test_dag2"
@@ -1772,6 +1808,44 @@ def test_should_respond_200_with_null_logical_date(self, test_client):
17721808
"note": None,
17731809
}
17741810

1811+
@time_machine.travel("2025-10-02 12:00:00", tick=False)
1812+
@pytest.mark.usefixtures("custom_timetable_plugin")
1813+
def test_custom_timetable_generate_run_id_for_manual_trigger(self, dag_maker, test_client, session):
1814+
"""Test that custom timetable's generate_run_id is used for manual triggers (issue #55908)."""
1815+
custom_dag_id = "test_custom_timetable_dag"
1816+
with dag_maker(
1817+
dag_id=custom_dag_id,
1818+
schedule=CustomTimetable("0 0 * * *", timezone="UTC"),
1819+
session=session,
1820+
serialized=True,
1821+
):
1822+
EmptyOperator(task_id="test_task")
1823+
1824+
session.commit()
1825+
1826+
logical_date = datetime(2025, 10, 1, 0, 0, 0, tzinfo=timezone.utc)
1827+
response = test_client.post(
1828+
f"/dags/{custom_dag_id}/dagRuns",
1829+
json={"logical_date": logical_date.isoformat()},
1830+
)
1831+
assert response.status_code == 200
1832+
run_id_with_logical_date = response.json()["dag_run_id"]
1833+
assert run_id_with_logical_date.startswith("custom_")
1834+
1835+
run = session.query(DagRun).filter(DagRun.run_id == run_id_with_logical_date).one()
1836+
assert run.dag_id == custom_dag_id
1837+
1838+
response = test_client.post(
1839+
f"/dags/{custom_dag_id}/dagRuns",
1840+
json={"logical_date": None},
1841+
)
1842+
assert response.status_code == 200
1843+
run_id_without_logical_date = response.json()["dag_run_id"]
1844+
assert run_id_without_logical_date.startswith("custom_manual_")
1845+
1846+
run = session.query(DagRun).filter(DagRun.run_id == run_id_without_logical_date).one()
1847+
assert run.dag_id == custom_dag_id
1848+
17751849

17761850
class TestWaitDagRun:
17771851
# The way we init async engine does not work well with FastAPI app init.

0 commit comments

Comments
 (0)