From 654516400df4c7339332bac3f30f8fd23d74329e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E4=BA=AC=E5=9D=A4?= <2802629917@qq.com> Date: Sat, 24 Oct 2026 15:06:23 +0800 Subject: [PATCH] fix(cron): use elapsed time across daylight-saving transitions --- reme/components/job/cron_job.py | 3 ++- tests/unit/test_cron_delay.py | 40 +++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 tests/unit/test_cron_delay.py diff --git a/reme/components/job/cron_job.py b/reme/components/job/cron_job.py index 71948597..466d607f 100644 --- a/reme/components/job/cron_job.py +++ b/reme/components/job/cron_job.py @@ -32,7 +32,8 @@ class CronJob(BackgroundJob): from croniter import croniter nxt = croniter(self.cron_expr, now).get_next(datetime.datetime) - return max(0.0, (nxt - now).total_seconds()) + # A wait needs elapsed seconds, including any intervening UTC-offset change. + return max(0.0, nxt.timestamp() - now.timestamp()) async def _execute_steps(self) -> Response: context = RuntimeContext(**self.kwargs) diff --git a/tests/unit/test_cron_delay.py b/tests/unit/test_cron_delay.py new file mode 100644 index 00000000..b5d14965 --- /dev/null +++ b/tests/unit/test_cron_delay.py @@ -0,0 +1,40 @@ +"""Cron waits use elapsed seconds across timezone offset changes.""" + +# pylint: disable=protected-access,missing-function-docstring,missing-class-docstring + +import datetime +from types import SimpleNamespace + +import pytest + +from reme.components.job import cron_job as cron_job_module +from reme.components.job.cron_job import CronJob + + +@pytest.mark.parametrize( + "timezone,instant,expected_delay", + [ + ("America/New_York", "2026-03-08T06:30:00+00:00", 1800), + ("America/New_York", "2026-11-01T04:30:00+00:00", 12600), + ("Europe/London", "2026-03-29T00:30:00+00:00", 5400), + ("Europe/London", "2026-10-25T00:30:00+00:00", 9000), + ("Australia/Lord_Howe", "2026-10-03T15:00:00+00:00", 3600), + ("Australia/Lord_Howe", "2026-04-04T13:30:00+00:00", 10800), + ("America/New_York", "2026-01-15T06:30:00+00:00", 5400), + ("Asia/Shanghai", "2026-03-07T17:30:00+00:00", 5400), + (None, "2026-01-15T01:30:00+00:00", 5400), + ], +) +def test_cron_delay_uses_elapsed_seconds(monkeypatch, timezone, instant, expected_delay): + fixed = datetime.datetime.fromisoformat(instant) + + class FixedDatetime(datetime.datetime): + @classmethod + def now(cls, tz=None): + return fixed.astimezone(tz) if tz else fixed.replace(tzinfo=None) + + monkeypatch.setattr(cron_job_module, "datetime", SimpleNamespace(datetime=FixedDatetime)) + job = CronJob(cron="0 3 * * *") + job.app_context = SimpleNamespace(app_config=SimpleNamespace(timezone=timezone)) + + assert job._next_fire_delay() == expected_delay