From 5601d190cc43bcd280bd092c9ab8b7cff52b973e Mon Sep 17 00:00:00 2001 From: Matvii Sakhnenko Date: Tue, 7 Apr 2026 13:03:57 +0200 Subject: [PATCH] fix: set misfire_grace_time=None for reliable heartbeats With misfire_grace_time=300, heartbeats were still missed when the bot was busy for >5 minutes (observed: 8m54s miss). Setting to None guarantees every job fires exactly once, combined with coalesce=True to prevent duplicate execution. --- src/scheduler/scheduler.py | 7 +++- .../test_scheduler/test_misfire_config.py | 37 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 tests/unit/test_scheduler/test_misfire_config.py diff --git a/src/scheduler/scheduler.py b/src/scheduler/scheduler.py index 98d90a55..8cbca1d6 100644 --- a/src/scheduler/scheduler.py +++ b/src/scheduler/scheduler.py @@ -32,7 +32,12 @@ def __init__( self.event_bus = event_bus self.db_manager = db_manager self.default_working_directory = default_working_directory - self._scheduler = AsyncIOScheduler() + self._scheduler = AsyncIOScheduler( + job_defaults={ + "misfire_grace_time": None, # Always run, no matter how late + "coalesce": True, # Merge multiple missed runs into one + } + ) async def start(self) -> None: """Load persisted jobs and start the scheduler.""" diff --git a/tests/unit/test_scheduler/test_misfire_config.py b/tests/unit/test_scheduler/test_misfire_config.py new file mode 100644 index 00000000..9f3f5f89 --- /dev/null +++ b/tests/unit/test_scheduler/test_misfire_config.py @@ -0,0 +1,37 @@ +"""Tests for scheduler misfire configuration.""" + +from unittest.mock import MagicMock + +import pytest + +from src.events.bus import EventBus +from src.scheduler.scheduler import JobScheduler +from src.storage.database import DatabaseManager + + +class TestSchedulerMisfireConfig: + """Verify APScheduler is configured for resilient job execution.""" + + @pytest.fixture + def scheduler(self, tmp_path): + event_bus = EventBus() + db_manager = MagicMock(spec=DatabaseManager) + return JobScheduler( + event_bus=event_bus, + db_manager=db_manager, + default_working_directory=tmp_path, + ) + + def test_misfire_grace_time_is_none(self, scheduler): + """misfire_grace_time=None ensures jobs always run, no matter how late.""" + job_defaults = scheduler._scheduler._job_defaults + assert job_defaults.get("misfire_grace_time") is None, ( + "misfire_grace_time must be None to guarantee late jobs still fire" + ) + + def test_coalesce_is_enabled(self, scheduler): + """coalesce=True merges multiple missed runs into a single execution.""" + job_defaults = scheduler._scheduler._job_defaults + assert job_defaults.get("coalesce") is True, ( + "coalesce must be True to prevent spam-firing missed runs" + )