DavidAmsellem commented on issue #68004:
URL: https://github.com/apache/airflow/issues/68004#issuecomment-6314903061

   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 | 24 / 25 | 
satisfied / satisfied | satisfied / `2024-04-07T01+1030` not expected |
   | America/Santiago (midnight gap) 2024-09-08 / 2024-04-06 | 23 / 25 | 
satisfied / satisfied | satisfied / `2024-04-06T23-0300` not expected |
   | America/Havana (midnight gap) 2024-03-10 / 2024-11-03 | 23 / 25 | 
satisfied / satisfied | satisfied / `2024-11-03T00-0400` not expected |
   
   So:
   
   1. **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).
   2. **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.
   3. **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`.
   
   <details><summary>dst_check.py</summary>
   
   ```python
   import pendulum
   from airflow.partition_mappers.base import RollupMapper
   from airflow.partition_mappers.temporal import StartOfDayMapper
   from airflow.partition_mappers.window import DayWindow
   
   
   def check(tz_name, ymd, fmt):
       tz = pendulum.timezone(tz_name)
       t = pendulum.datetime(*ymd, tz="UTC").subtract(days=2)
       produced = []
       for _ in range(24 * 5):
           local = t.in_tz(tz)
           if (local.year, local.month, local.day) == ymd:
               produced.append(local.strftime(fmt))
           t = t.add(hours=1)
       mapper = RollupMapper(
           upstream_mapper=StartOfDayMapper(timezone=tz_name, 
input_format=fmt), window=DayWindow()
       )
       (down,) = {mapper.to_downstream(k) for k in produced}
       expected = mapper.to_upstream(down)
       print(
           f"{tz_name} {ymd} {fmt}: real hours={len(produced)} distinct 
produced={len(set(produced))} "
           f"expected={len(expected)} never arrive={sorted(expected - 
set(produced))} "
           f"not expected={sorted(set(produced) - expected)} 
satisfied={expected <= set(produced)}"
       )
   
   
   for fmt in ("%Y-%m-%dT%H", "%Y-%m-%dT%H%z"):
       for tz_name, ymd in [
           ("America/New_York", (2024, 3, 10)),
           ("America/New_York", (2024, 11, 3)),
           ("Europe/Madrid", (2024, 3, 31)),
           ("Europe/Madrid", (2024, 10, 27)),
           ("America/Santiago", (2024, 9, 8)),
           ("America/Santiago", (2024, 4, 6)),
       ]:
           check(tz_name, ymd, fmt)
   ```
   
   </details>
   
   ---
   
   Drafted-by: Claude Code (Opus 5.5)
   


-- 
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: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to