diff --git a/src/sentry/monitors/models.py b/src/sentry/monitors/models.py index ab74d6378294..c6cadd13fe39 100644 --- a/src/sentry/monitors/models.py +++ b/src/sentry/monitors/models.py @@ -4,7 +4,7 @@ import uuid import zoneinfo from collections.abc import Sequence -from datetime import datetime +from datetime import UTC, datetime from functools import cache from typing import TYPE_CHECKING, ClassVar, Self, override from uuid import uuid4 @@ -353,7 +353,8 @@ def get_next_expected_checkin_latest(self, last_checkin: datetime) -> datetime: """ from sentry.monitors.utils import get_checkin_margin - next_checkin = self.get_next_expected_checkin(last_checkin) + # Add the margin in UTC: local wall-clock arithmetic drops the DST fold + next_checkin = self.get_next_expected_checkin(last_checkin).astimezone(UTC) return next_checkin + get_checkin_margin(self.config.get("checkin_margin")) def update_config(self, config_payload, validated_config): diff --git a/tests/sentry/monitors/test_models.py b/tests/sentry/monitors/test_models.py index 6d2c52c81bfd..ef3026dc1524 100644 --- a/tests/sentry/monitors/test_models.py +++ b/tests/sentry/monitors/test_models.py @@ -71,6 +71,28 @@ def test_next_run_latest_crontab_with_margin(self) -> None: 2019, 1, 1, 1, 16, tzinfo=timezone.utc ) + def test_next_run_latest_crontab_during_dst_fold(self) -> None: + # Europe/Berlin repeats 02:00-02:59 local time on 2024-10-27 from 01:00 UTC + ts = datetime(2024, 10, 27, 1, 0, 3, tzinfo=timezone.utc) + monitor = Monitor( + config={ + "schedule_type": ScheduleType.CRONTAB, + "schedule": "* * * * *", + "timezone": "Europe/Berlin", + "checkin_margin": 5, + "max_runtime": None, + } + ) + monitor_environment = MonitorEnvironment(monitor=monitor, last_checkin=ts) + + # PEP 495: inter-zone == is always False for a time with fold=1 + assert monitor_environment.monitor.get_next_expected_checkin(ts).astimezone( + timezone.utc + ) == datetime(2024, 10, 27, 1, 1, tzinfo=timezone.utc) + assert monitor_environment.monitor.get_next_expected_checkin_latest(ts) == datetime( + 2024, 10, 27, 1, 6, tzinfo=timezone.utc + ) + def test_next_run_crontab_with_timezone(self) -> None: ts = datetime(2019, 1, 1, 1, 10, 20, tzinfo=timezone.utc) monitor = Monitor(