agronholm / apscheduler

Task scheduling library for Python
MIT License
6.28k stars 708 forks source link

Bi-hourly `CronTrigger` runs into infinite loop at daylight savings time boundary #980

Open timon-k opened 2 weeks ago

timon-k commented 2 weeks ago

Things to check first

Version

4.0.0a5

What happened?

When using a bi-hourly CronTrigger as given below, starting at a point before the daylight savings time transition, the trigger fails to compute the sequence of next trigger times. It always returns the same static time+date.

The output of the example code given below is:

Trigger starts at 2024-10-27 01:00:00+01:00
Next trigger time is 2024-10-27 01:00:00+01:00
Next trigger time is 2024-10-27 01:00:00+00:00
Next trigger time is 2024-10-27 01:00:00+00:00
Next trigger time is 2024-10-27 01:00:00+00:00
Trigger is in future after daylight saving time transition: False

Together with the aync scheduler logic here, this leads to an infinite loop, since the computed next fire time will never become bigger than now:

                        while True:
                            try:
                                fire_time = calculate_next()
                            except Exception:
                                ...
                                break

                            # Stop if the calculated fire time is in the future
                            if fire_time is None or fire_time > now:
                                next_fire_time = fire_time
                                break

The root cause is that the local time 1:00 really exists twice in the given timezone: Once before (01:00:00+01:00) and once after the daylight savings time transition (01:00:00+00:00).

The trigger stays stuck on the occurrence after the transition.

How can we reproduce the bug?

Run this example

import datetime
import zoneinfo

from apscheduler.triggers.cron import CronTrigger

start_time = datetime.datetime(
    2024, 10, 27, 1, 0, 0, 0, tzinfo=zoneinfo.ZoneInfo(key="Europe/Lisbon")
)
print(f"Trigger starts at {start_time}")
trigger = CronTrigger(
    year="*",
    month="*",
    day="*",
    week="*",
    day_of_week="*",
    hour="1,3,5,7,9,11,13,15,17,19,21,23",
    minute="0",
    second="0",
    start_time=start_time,
    timezone="Europe/Lisbon",
)

print(f"Next trigger time is {trigger.next()}")
print(f"Next trigger time is {trigger.next()}")
print(f"Next trigger time is {trigger.next()}")
print(f"Next trigger time is {trigger.next()}")
now_time_after_dst_change = datetime.datetime(
    2024, 10, 27, 14, 0, 0, 0, tzinfo=zoneinfo.ZoneInfo(key="UTC")
)
print(f"Trigger is in future after daylight saving time transition: {trigger.next() > now_time_after_dst_change}")
agronholm commented 2 weeks ago

This is a persistent problem that has defied my attempts to fix it. A PR (that doesn't radically alter anything else) would be much appreciated!