mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-11-14 09:01:22 +00:00
fix(cron): use elapsed time across daylight-saving transitions (#671)
* fix(cron): use elapsed time across daylight-saving transitions * fix(cron): preserve local-time delays during DST rollback --------- Co-authored-by: jinli.yl <jinli.yl@alibaba-inc.com>
This commit is contained in:
parent
b784468a10
commit
e9d1561bb3
2 changed files with 69 additions and 1 deletions
|
|
@ -32,7 +32,9 @@ 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())
|
||||
# croniter may reset fold on naive local times, so retain their wall-clock arithmetic.
|
||||
delay = nxt.timestamp() - now.timestamp() if tz_name else (nxt - now).total_seconds()
|
||||
return max(0.0, delay)
|
||||
|
||||
async def _execute_steps(self) -> Response:
|
||||
context = RuntimeContext(stop_event=self._stop_event, **self.kwargs)
|
||||
|
|
|
|||
66
tests/unit/test_cron_delay.py
Normal file
66
tests/unit/test_cron_delay.py
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
"""Cron waits use elapsed seconds across timezone offset changes."""
|
||||
|
||||
# pylint: disable=protected-access,missing-function-docstring,missing-class-docstring
|
||||
|
||||
import datetime
|
||||
import time
|
||||
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
|
||||
|
||||
|
||||
@pytest.mark.skipif(not hasattr(time, "tzset"), reason="requires time.tzset")
|
||||
@pytest.mark.parametrize("with_app_context", [True, False], ids=["local-timezone", "standalone"])
|
||||
def test_cron_local_time_after_dst_fallback(monkeypatch, with_app_context):
|
||||
fixed = datetime.datetime.fromisoformat("2026-11-01T06:30:00+00:00")
|
||||
|
||||
class FixedDatetime(datetime.datetime):
|
||||
@classmethod
|
||||
def now(cls, tz=None):
|
||||
return datetime.datetime.fromtimestamp(fixed.timestamp(), tz)
|
||||
|
||||
try:
|
||||
with monkeypatch.context() as patch:
|
||||
patch.setenv("TZ", "America/New_York")
|
||||
time.tzset()
|
||||
patch.setattr(cron_job_module, "datetime", SimpleNamespace(datetime=FixedDatetime))
|
||||
job = CronJob(cron="* * * * *")
|
||||
if with_app_context:
|
||||
job.app_context = SimpleNamespace(app_config=SimpleNamespace(timezone=None))
|
||||
|
||||
assert FixedDatetime.now().fold == 1
|
||||
assert job._next_fire_delay() == 60
|
||||
finally:
|
||||
time.tzset()
|
||||
Loading…
Add table
Reference in a new issue