You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Make RollupMapper calendar windows DST-aware #68004
RollupMapper with a DayWindow always expects 24 hourly keys, so a local-timezone upstream mapper breaks on DST days: spring-forward (23h) leaves one expected key that never arrives and the run is held forever; fall-back (25h) drops the extra hour's events.
Current workaround (in the partition docs / known limitations): use UTC partition keys.
This tracks making the calendar windows DST-aware so the expected key set matches the real number of hours, instead of relying on a UTC workaround.
Before working on this I measured what DayWindow does today on DST days, and the spring-forward case behaves differently from what the docstring, assets.rst and the strict-xfail test say.
I simulated an hourly producer: one key per real local hour of the day, encoded with the upstream mapper's input_format. Then I compared those keys with RollupMapper(upstream_mapper=StartOfDayMapper(timezone=..., input_format=...), window=DayWindow()).to_upstream(day) on main:
zone, day
real hours
%Y-%m-%dT%H (no offset)
%Y-%m-%dT%H%z (with offset)
America/New_York 2024-03-10 (spring)
23
23 expected = 23 produced, satisfied
23 = 23, satisfied
America/New_York 2024-11-03 (fall)
25
24 distinct produced, 24 expected, satisfied
25 distinct produced, 24 expected; 2024-11-03T01-0400 not expected
Europe/Madrid 2024-03-31 / 2024-10-27
23 / 25
satisfied / satisfied
satisfied / 2024-10-27T02+0200 not expected
Australia/Lord_Howe (30 min shift) 2024-10-06 / 2024-04-07
Spring-forward is not held. The nonexistent hour's naive step is localised forward onto the next real hour, the two steps collapse into one key, and to_upstream returns exactly the 23 keys an hourly producer emits. test_day_window_rollup_under_yields_on_spring_forward_with_local_tz asserts len(upstream_keys) == 24, but 23 is the right answer for a 23-hour day, so the strict xfail encodes the wrong expectation. The same holds where the gap is at midnight (Santiago, Havana).
Fall-back without an offset in the key is fine. The repeated hour produces the same key twice, so 24 distinct keys arrive and 24 are expected.
Fall-back with an offset in the key is the real gap. The repeated hour produces two distinct keys, and the window only yields one of them. With WaitForAll the run still fires (every expected key arrives, the last one at the end of the day), but one real hour isn't part of the expected set, so it doesn't count towards MinimumCount and the UI's required keys show 24 for a 25-hour day.
If that matches your understanding, I'd split it in two:
A small PR now: replace the strict xfail with tests that check the rollup is satisfied on spring-forward and fall-back days for a local-timezone mapper without an offset, add one that pins the fall-back gap with an offset, and correct the DayWindow docstring and the assets.rst paragraph (the "use UTC for any rollup that crosses a DST boundary" advice is only needed for offset-bearing keys).
This issue then tracks only case 3. One way: have RollupMapper.to_upstream localise the decoded period start with the upstream mapper's timezone and let the temporal windows step through the aware range to the next local period start, so the repeated hour yields both instants and encode_upstream (which already handles aware datetimes) renders both keys. That changes the "windows don't touch timezones" contract, so I'd like your view before writing it.
The script I used is below; it runs against main with uv run --project airflow-core python dst_check.py.
Activity
Make RollupMapper calendar windows DST-aware (apache#68004)
Make RollupMapper calendar windows DST-aware (apache#68004)
DavidAmsellem commentedon Oct 21, 2026
Before working on this I measured what
DayWindowdoes today on DST days, and the spring-forward case behaves differently from what the docstring,assets.rstand the strict-xfail test say.I simulated an hourly producer: one key per real local hour of the day, encoded with the upstream mapper's
input_format. Then I compared those keys withRollupMapper(upstream_mapper=StartOfDayMapper(timezone=..., input_format=...), window=DayWindow()).to_upstream(day)onmain:%Y-%m-%dT%H(no offset)%Y-%m-%dT%H%z(with offset)2024-11-03T01-0400not expected2024-10-27T02+0200not expected2024-04-07T01+1030not expected2024-04-06T23-0300not expected2024-11-03T00-0400not expectedSo:
to_upstreamreturns exactly the 23 keys an hourly producer emits.test_day_window_rollup_under_yields_on_spring_forward_with_local_tzassertslen(upstream_keys) == 24, but 23 is the right answer for a 23-hour day, so the strict xfail encodes the wrong expectation. The same holds where the gap is at midnight (Santiago, Havana).WaitForAllthe run still fires (every expected key arrives, the last one at the end of the day), but one real hour isn't part of the expected set, so it doesn't count towardsMinimumCountand the UI's required keys show 24 for a 25-hour day.If that matches your understanding, I'd split it in two:
DayWindowdocstring and theassets.rstparagraph (the "use UTC for any rollup that crosses a DST boundary" advice is only needed for offset-bearing keys).RollupMapper.to_upstreamlocalise the decoded period start with the upstream mapper's timezone and let the temporal windows step through the aware range to the next local period start, so the repeated hour yields both instants andencode_upstream(which already handles aware datetimes) renders both keys. That changes the "windows don't touch timezones" contract, so I'd like your view before writing it.The script I used is below; it runs against
mainwithuv run --project airflow-core python dst_check.py.dst_check.py
Drafted-by: Claude Code (Opus 5.5)