You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airflow.apache.org by "simon-hoon (via GitHub)" <gi...@apache.org> on 2026/08/06 11:16:20 UTC

[I] Calendar view computes planned runs for cron timetables in UTC, ignoring the DAG's configured timezone [airflow]

simon-hoon opened a new issue, #71234:
URL: https://github.com/apache/airflow/issues/71234

   ### Under which category would you file this issue?
   
   Airflow Core
   
   ### Apache Airflow version
   
   3.3.0
   
   ### What happened and how to reproduce it?
   
     For a DAG using a plain cron-string `schedule` (e.g. `"0 8 * * *"`) on an Airflow instance whose `[core] default_timezone` is **not UTC** (e.g. `Asia/Seoul`, UTC+9), the **Calendar view's "planned" (future, not-yet-run) cells are shown at the wrong hour** — offset by the UTC/local difference.
   
     Concretely, with `AIRFLOW__CORE__DEFAULT_TIMEZONE=Asia/Seoul` and a DAG scheduled `"0 8 * * *"`:
     - The DAG actually fires at **08:00 KST**, confirmed by `airflow dags next-execution` and by the DAG detail page's "Next Run" / "Last Run" fields (both correctly show `08:00:00`).
     - Real, already-executed runs are shown correctly on the Calendar at the 08:00 row.
     - **Future "planned" cells for the same DAG are drawn at the 17:00 row instead** (08:00 + 9h) — i.e. exactly a UTC-vs-KST offset.
   
     Historical (already-happened) runs are unaffected because they're read straight from `DagRun` rows already computed correctly by the scheduler. Only the *projected/planned* rows — computed on the fly by the Calendar API — are wrong.
   
     How to reproduce:
   
     Via the UI:
     1. Deploy Airflow with `AIRFLOW__CORE__DEFAULT_TIMEZONE=Asia/Seoul` (any non-UTC zone works; easiest to see with a schedule hour close to local midnight).
     2. Create a DAG with `schedule="0 8 * * *"` (no explicit timetable timezone override — it inherits the core default).
     3. Let it run at least once, or just open the DAG's Calendar tab.
     4. Compare the DAG detail page's "Next Run"/"Last Run" (correctly `08:00`) against the Calendar tab's *planned* cells for the same DAG (shown at `17:00`).
   
     Minimal Python repro (no webserver needed) — confirms the bad value is computed by the service itself, not introduced or fixed anywhere downstream:
   
     ```python
     from croniter import croniter
     from datetime import datetime
     import pendulum
     from airflow.timetables.trigger import CronTriggerTimetable
   
     tt = CronTriggerTimetable("0 8 * * *", timezone="Asia/Seoul")
     last_end_utc = pendulum.datetime(2026, 8, 5, 23, 0, 0, tz="UTC")  # = 2026-08-06 08:00 KST
   
     # (A) what CalendarService._calculate_cron_planned_runs does:
     next_planned = next(croniter(tt._expression, start_time=last_end_utc, ret_type=datetime))
     print(next_planned, "->", next_planned.astimezone(pendulum.timezone("Asia/Seoul")))
     # 2026-08-06 08:00:00+00:00 -> 2026-08-06 17:00:00+09:00   (WRONG)
   
     # (B) what the real scheduler uses (CronMixin._get_next):
     next_real = tt._get_next(last_end_utc)
     print(next_real, "->", next_real.in_timezone("Asia/Seoul"))
     # 2026-08-06 23:00:00+00:00 -> 2026-08-07 08:00:00+09:00   (matches `airflow dags next-execution`)
     ```
   
     I also confirmed this end-to-end by calling `CalendarService.get_calendar_data()` (the exact function `GET /ui/calendar/{dag_id}` calls) against a live instance's real `DagRun` history for a `schedule="0 8 * * *"` DAG: the one real, already-executed run came back correct (08:00 KST), while every planned/future occurrence came back exactly 9 hours later. This is the value returned by the API itself — nothing downstream corrects it. (Screenshot attached below, from a fresh local demo instance — no relation to the code snippets above.)
   
   <img width="1080" height="700" alt="Image" src="https://github.com/user-attachments/assets/6f55add7-c21c-43a9-a8e8-01a15abc7e33" />
   
   ### What you think should happen instead?
   
   Planned/future run cells in the Calendar view should be computed in the DAG's configured timetable timezone, the same way the real scheduler computes actual runs, so they land in the same hour/day as the runs that will actually execute.
   
   Root cause: `CalendarService._calculate_cron_planned_runs()` in `airflow-core/src/airflow/api_fastapi/core_api/services/ui/calendar.py`:
   
   ```python
   def _calculate_cron_planned_runs(
       self, dag, last_data_interval, year, date_filter, granularity,
   ):
       """Calculate planned runs for cron-based timetables."""
       dates: dict[datetime, int] = collections.Counter()
   
       dates_iter: Iterator[datetime | None] = croniter(
           cast("CronMixin", dag.timetable)._expression,
           start_time=last_data_interval.end,
           ret_type=datetime,
       )
       ...
   ```
   
   `last_data_interval.end` is a UTC-aware `datetime`. `croniter`'s documented contract (its README: "Be sure to init your croniter instance with a TZ aware datetime for this to work!") is that the caller localizes `start_time` to whatever timezone the cron fields should be matched against — croniter just reads the tzinfo off it (`self.tzinfo = start_time.tzinfo` in `set_current`). `_calculate_cron_planned_runs` never does that localization, so `"0 8 * * *"` gets matched against UTC wall-clock instead of `dag.timetable`'s configured `Asia/Seoul` (→ 08:00 UTC = 17:00 KST). This is croniter behaving exactly as documented — the bug is entirely on the caller's side.
   
   Contrast with how the real scheduler computes the next run — `CronMixin._get_next()` in `airflow-core/src/airflow/timetables/_cron.py`, which does the localize/delocalize correctly:
   
   ```python
   def _get_next(self, current: DateTime) -> DateTime:
       naive = make_naive(current, self._timezone)   # UTC -> local naive wall-clock
       ...                                             # croniter matches on local wall-clock
       return convert_to_utc(make_aware(scheduled, self._timezone))  # local -> UTC
   ```
   
   Notably, the sibling function in the same file, `_calculate_timetable_planned_runs()` (used for non-cron/partitioned timetables), does this correctly via `dag.timetable.next_dagrun_info_v2(...)`. Only the cron-specific fast path bypasses it. Still present, unchanged, on `main` (diffed byte-for-byte against the installed 3.3.0 function body).
   
   Related prior art (same class of bug, different surfaces):
   - #67477 (fixed) — a different layer: the React frontend ignored the user's selected UI timezone. Since it's fixed, the frontend now faithfully renders whatever value the API sends — which is why this backend bug is visible end-to-end.
   - #67717 (merged) — the same pattern (comparing UTC-parsed dates directly against a non-UTC timetable's dates) for `airflow dags clear`, fixed via a new `Timetable.resolve_day_bound()`.
   - #63631 — added the correct, timezone-aware `next_dagrun_info_v2` path for non-cron/partitioned timetables in this same `calendar.py` file; the cron fast path was left behind.
   
   Suggested fix — either:
   1. Route cron timetables through the same `next_dagrun_info_v2`-based path as `_calculate_timetable_planned_runs` (simplest, reuses already-correct logic, at some perf cost vs. croniter's fast iteration), or
   2. Keep the croniter fast path, but localize `start_time` into the timetable's own timezone before constructing `croniter` (mirroring `CronMixin._get_next`), converting results back to UTC afterward. This needs read access to the timetable's timezone from `calendar.py` — `CronMixin._timezone` is currently private; a small public accessor (in the spirit of the `resolve_day_bound()` added in #67717) would do it cleanly.
   
   ### Operating System
   
   Not specific to an OS — reproduced on macOS (local venv) and confirmed against a Kubernetes/Helm-chart deployment (Debian-based image).
   
   ### Deployment
   
   Virtualenv installation
   
   ### Apache Airflow Provider(s)
   
   _No response_
   
   ### Versions of Apache Airflow Providers
   
   _No response_
   
   ### Official Helm Chart version
   
   Not Applicable
   
   ### Kubernetes Version
   
   _No response_
   
   ### Helm Chart configuration
   
   _No response_
   
   ### Docker Image customizations
   
   _No response_
   
   ### Anything else?
   
   Deterministic, not intermittent — occurs for any cron-scheduled DAG on a non-UTC `default_timezone`, every time the Calendar view computes planned/future runs.
   
   ### Are you willing to submit PR?
   
   - [x] Yes I am willing to submit a PR!
   
   ### Code of Conduct
   
   - [x] I agree to follow this project's [Code of Conduct](https://github.com/apache/airflow/blob/main/CODE_OF_CONDUCT.md)
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@airflow.apache.org.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [I] Calendar view computes planned runs for cron timetables in UTC, ignoring the DAG's configured timezone [airflow]

Posted by "boring-cyborg[bot] (via GitHub)" <gi...@apache.org>.
boring-cyborg[bot] commented on issue #71234:
URL: https://github.com/apache/airflow/issues/71234#issuecomment-5203937225

   Thanks for opening your first issue here! Be sure to follow the issue template! If you are willing to raise PR to address this issue please do so, no need to wait for approval.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@airflow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [I] Calendar view computes planned runs for cron timetables in UTC, ignoring the DAG's configured timezone [airflow]

Posted by "pierrejeambrun (via GitHub)" <gi...@apache.org>.
pierrejeambrun closed issue #71234: Calendar view computes planned runs for cron timetables in UTC, ignoring the DAG's configured timezone
URL: https://github.com/apache/airflow/issues/71234


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@airflow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org