diff --git a/awx/main/models/schedules.py b/awx/main/models/schedules.py index 0afb546f90..5ab57e1746 100644 --- a/awx/main/models/schedules.py +++ b/awx/main/models/schedules.py @@ -72,10 +72,10 @@ def _fast_forward_rrule(rrule, ref_dt=None): if ref_dt is None: ref_dt = now() - ref_dt = ref_dt.astimezone(datetime.timezone.utc) + dtstart_tz = rrule._dtstart.tzinfo + ref_dt = ref_dt.astimezone(dtstart_tz) - rrule_dtstart_utc = rrule._dtstart.astimezone(datetime.timezone.utc) - if rrule_dtstart_utc > ref_dt: + if rrule._dtstart > ref_dt: return rrule interval = rrule._interval if rrule._interval else 1 @@ -84,20 +84,14 @@ def _fast_forward_rrule(rrule, ref_dt=None): elif rrule._freq == dateutil.rrule.MINUTELY: interval *= 60 - # if after converting to seconds the interval is still a fraction, - # just return original rrule if isinstance(interval, float) and not interval.is_integer(): return rrule - seconds_since_dtstart = (ref_dt - rrule_dtstart_utc).total_seconds() + seconds_since_dtstart = (ref_dt - rrule._dtstart).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_utc + interval_aligned_offset - new_rrule = rrule.replace(dtstart=new_start.astimezone(rrule._dtstart.tzinfo)) + new_start = rrule._dtstart + interval_aligned_offset + new_rrule = rrule.replace(dtstart=new_start) return new_rrule diff --git a/awx/main/tests/unit/utils/test_schedule_fast_forward.py b/awx/main/tests/unit/utils/test_schedule_fast_forward.py index be1bdae53e..b5050a935b 100644 --- a/awx/main/tests/unit/utils/test_schedule_fast_forward.py +++ b/awx/main/tests/unit/utils/test_schedule_fast_forward.py @@ -7,7 +7,7 @@ from django.utils.timezone import now from awx.main.models.schedules import _fast_forward_rrule, Schedule from dateutil.rrule import HOURLY, MINUTELY, MONTHLY -REF_DT = datetime.datetime(2024, 1, 1, tzinfo=datetime.timezone.utc) +REF_DT = datetime.datetime(2026, 4, 16, tzinfo=datetime.timezone.utc) @pytest.mark.parametrize( @@ -20,6 +20,10 @@ REF_DT = datetime.datetime(2024, 1, 1, tzinfo=datetime.timezone.utc) 'DTSTART;TZID=America/New_York:20201118T200000 RRULE:FREQ=MINUTELY;INTERVAL=5;WKST=SU;BYMONTH=2,3;BYMONTHDAY=18;BYHOUR=5;BYMINUTE=35;BYSECOND=0', id='every-5-minutes-at-5:35:00-am-on-the-18th-day-of-feb-or-march-with-week-starting-on-sundays', ), + pytest.param( + 'DTSTART;TZID=America/New_York:20251211T130000 RRULE:FREQ=HOURLY;INTERVAL=4;WKST=MO;BYDAY=MO,TU,WE,TH,FR;BYHOUR=1,5,9,13,17,21;BYMINUTE=0', + id='every-4-hours-at-1-5-9-13-17-21-am-on-monday-through-friday-with-week-starting-on-monday', + ), pytest.param( 'DTSTART;TZID=America/New_York:20201118T200000 RRULE:FREQ=HOURLY;INTERVAL=5;WKST=SU;BYMONTH=2,3;BYHOUR=5', id='every-5-hours-at-5-am-in-feb-or-march-with-week-starting-on-sundays', @@ -48,6 +52,7 @@ def test_fast_forwarded_rrule_matches_original_occurrence(rrulestr): [ 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.param(datetime.datetime(2024, 11, 3, 6, 30, tzinfo=datetime.timezone.utc), id='ref-dt-fall-back-day'), ], ) @pytest.mark.parametrize( @@ -58,6 +63,8 @@ def test_fast_forwarded_rrule_matches_original_occurrence(rrulestr): 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' ), + pytest.param('DTSTART;TZID=America/New_York:20230313T005800 RRULE:FREQ=MINUTELY;INTERVAL=7', id='rrule-post-dst-7min'), + pytest.param('DTSTART;TZID=America/New_York:20230313T005800 RRULE:FREQ=MINUTELY;INTERVAL=13', id='rrule-post-dst-13min'), ], ) def test_fast_forward_across_dst(rrulestr, ref_dt):