mirror of
https://github.com/ansible/awx.git
synced 2026-01-09 23:12:08 -03:30
Fix rrule fast forwarding across DST boundaries (#6815)
Fixes an issue where schedules were not running at the correct time. Details: DST is Daylights Saving Time If the rrule dtstart is "in" a DST period (i.e., March to November) and the current date is outside of the DST, then the fast forwarding is not correct. This is because datetime timedeltas do not honor DST boundaries The Fix: Convert the rrule dtstart to UTC before doing operations. Then, convert back to the original timezone at the end. Signed-off-by: Seth Foster <fosterbseth@gmail.com>
This commit is contained in:
parent
b7b15584af
commit
386f85c59f
@ -59,8 +59,13 @@ def _fast_forward_rrule(rrule, ref_dt=None):
|
||||
The operation ensures that the original occurrences (based on the original dtstart)
|
||||
will match the occurrences after changing the dtstart.
|
||||
|
||||
All datetime operations (subtracting dates and adding timedeltas) should be
|
||||
in UTC to avoid DST issues. As such, the rrule dtstart is converted to UTC
|
||||
then back to the original timezone at the end.
|
||||
|
||||
Returns a new rrule with a new dtstart
|
||||
'''
|
||||
|
||||
if rrule._freq not in {dateutil.rrule.HOURLY, dateutil.rrule.MINUTELY}:
|
||||
return rrule
|
||||
|
||||
@ -70,7 +75,10 @@ def _fast_forward_rrule(rrule, ref_dt=None):
|
||||
if ref_dt is None:
|
||||
ref_dt = now()
|
||||
|
||||
if rrule._dtstart > ref_dt:
|
||||
ref_dt = ref_dt.astimezone(datetime.timezone.utc)
|
||||
|
||||
rrule_dtstart_utc = rrule._dtstart.astimezone(datetime.timezone.utc)
|
||||
if rrule_dtstart_utc > ref_dt:
|
||||
return rrule
|
||||
|
||||
interval = rrule._interval if rrule._interval else 1
|
||||
@ -84,15 +92,15 @@ def _fast_forward_rrule(rrule, ref_dt=None):
|
||||
if isinstance(interval, float) and not interval.is_integer():
|
||||
return rrule
|
||||
|
||||
seconds_since_dtstart = (ref_dt - rrule._dtstart).total_seconds()
|
||||
seconds_since_dtstart = (ref_dt - rrule_dtstart_utc).total_seconds()
|
||||
|
||||
# it is important to fast forward by a number that is divisible by
|
||||
# interval. For example, if interval is 7 hours, we fast forward by 7, 14, 21, etc. hours.
|
||||
# Otherwise, the occurrences after the fast forward might not match the ones before.
|
||||
# x // y is integer division, lopping off any remainder, so that we get the outcome we want.
|
||||
interval_aligned_offset = datetime.timedelta(seconds=(seconds_since_dtstart // interval) * interval)
|
||||
new_start = rrule._dtstart + interval_aligned_offset
|
||||
new_rrule = rrule.replace(dtstart=new_start)
|
||||
new_start = rrule_dtstart_utc + interval_aligned_offset
|
||||
new_rrule = rrule.replace(dtstart=new_start.astimezone(rrule._dtstart.tzinfo))
|
||||
return new_rrule
|
||||
|
||||
|
||||
|
||||
@ -43,6 +43,44 @@ def test_fast_forwarded_rrule_matches_original_occurrence(rrulestr):
|
||||
assert occurrences == orig_occurrences
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'ref_dt',
|
||||
[
|
||||
pytest.param(datetime.datetime(2024, 12, 1, 0, 0, tzinfo=datetime.timezone.utc), id='ref-dt-out-of-dst'),
|
||||
pytest.param(datetime.datetime(2024, 6, 1, 0, 0, tzinfo=datetime.timezone.utc), id='ref-dt-in-dst'),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
'rrulestr',
|
||||
[
|
||||
pytest.param('DTSTART;TZID=America/New_York:20240118T200000 RRULE:FREQ=MINUTELY;INTERVAL=10', id='rrule-out-of-dst'),
|
||||
pytest.param('DTSTART;TZID=America/New_York:20240318T000000 RRULE:FREQ=MINUTELY;INTERVAL=10', id='rrule-in-dst'),
|
||||
pytest.param(
|
||||
'DTSTART;TZID=Europe/Lisbon:20230703T005800 RRULE:INTERVAL=10;FREQ=MINUTELY;BYHOUR=9,10,11,12,13,14,15,16,17,18,19,20,21', id='rrule-in-dst-by-hour'
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_fast_forward_across_dst(rrulestr, ref_dt):
|
||||
'''
|
||||
Ensure fast forward works across daylight savings boundaries
|
||||
"in dst" means between March and November
|
||||
"out of dst" means between November and March the following year
|
||||
|
||||
Assert that the resulting fast forwarded date is included in the original rrule
|
||||
occurrence list
|
||||
'''
|
||||
rruleset = Schedule.rrulestr(rrulestr, ref_dt=ref_dt)
|
||||
|
||||
gen = rruleset.xafter(ref_dt, count=200)
|
||||
occurrences = [i for i in gen]
|
||||
|
||||
orig_rruleset = dateutil.rrule.rrulestr(rrulestr, forceset=True)
|
||||
gen = orig_rruleset.xafter(ref_dt, count=200)
|
||||
orig_occurrences = [i for i in gen]
|
||||
|
||||
assert occurrences == orig_occurrences
|
||||
|
||||
|
||||
def test_fast_forward_rrule_hours():
|
||||
'''
|
||||
Generate an rrule for each hour of the day
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user