From 4d16957b9b29df5a24d6d23f3fbdf811f3f8de1b Mon Sep 17 00:00:00 2001 From: gomico Date: Tue, 28 Jul 2026 01:30:12 +0800 Subject: [PATCH 1/2] fix: sync persistent cron jobs after early scheduler start --- astrbot/core/cron/manager.py | 9 ++++++--- tests/unit/test_cron_manager.py | 25 +++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/astrbot/core/cron/manager.py b/astrbot/core/cron/manager.py index 9e6feb37f6..aefa09bd1d 100644 --- a/astrbot/core/cron/manager.py +++ b/astrbot/core/cron/manager.py @@ -103,15 +103,18 @@ def __init__(self, db: BaseDatabase) -> None: self._basic_handlers: dict[str, Callable[..., Any]] = {} self._lock = asyncio.Lock() self._started = False + self._db_synced = False async def start(self, ctx: "Context") -> None: self.ctx: Context = ctx # star context async with self._lock: - if self._started: + if self._db_synced: return - self.scheduler.start() - self._started = True + if not self._started: + self.scheduler.start() + self._started = True await self.sync_from_db() + self._db_synced = True async def shutdown(self) -> None: async with self._lock: diff --git a/tests/unit/test_cron_manager.py b/tests/unit/test_cron_manager.py index 242312754b..cca5ec0896 100644 --- a/tests/unit/test_cron_manager.py +++ b/tests/unit/test_cron_manager.py @@ -95,6 +95,31 @@ async def test_start_idempotent(self, cron_manager, mock_db, mock_context): # Should only sync once assert mock_db.list_cron_jobs.call_count == 1 + @pytest.mark.asyncio + async def test_start_syncs_after_scheduler_started_early( + self, cron_manager, mock_db, mock_context, sample_cron_job + ): + """Test that early scheduler startup does not skip database sync.""" + mock_db.create_cron_job.return_value = sample_cron_job + mock_db.list_cron_jobs.return_value = [sample_cron_job] + + await cron_manager.add_basic_job( + name="Early Job", + cron_expression="0 9 * * *", + handler=MagicMock(), + enabled=True, + persistent=False, + ) + + await cron_manager.start(mock_context) + + assert cron_manager._started is True + assert cron_manager._db_synced is True + assert cron_manager.scheduler.get_job(sample_cron_job.job_id) is not None + assert mock_db.list_cron_jobs.call_count == 1 + + await cron_manager.shutdown() + class TestCronJobManagerShutdown: """Tests for CronJobManager.shutdown method.""" From c7ac67b9051ac1f1158130e755d81693c78ec923 Mon Sep 17 00:00:00 2001 From: gomico Date: Tue, 28 Jul 2026 02:03:08 +0800 Subject: [PATCH 2/2] fix: resync cron jobs after manager shutdown --- astrbot/core/cron/manager.py | 3 +++ tests/unit/test_cron_manager.py | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/astrbot/core/cron/manager.py b/astrbot/core/cron/manager.py index aefa09bd1d..c42b7197ba 100644 --- a/astrbot/core/cron/manager.py +++ b/astrbot/core/cron/manager.py @@ -103,6 +103,7 @@ def __init__(self, db: BaseDatabase) -> None: self._basic_handlers: dict[str, Callable[..., Any]] = {} self._lock = asyncio.Lock() self._started = False + # The scheduler may start early via _schedule_job; track DB sync separately. self._db_synced = False async def start(self, ctx: "Context") -> None: @@ -121,7 +122,9 @@ async def shutdown(self) -> None: if not self._started: return self.scheduler.shutdown(wait=False) + await asyncio.sleep(0) self._started = False + self._db_synced = False async def sync_from_db(self) -> None: jobs = await self.db.list_cron_jobs() diff --git a/tests/unit/test_cron_manager.py b/tests/unit/test_cron_manager.py index cca5ec0896..47f97ef445 100644 --- a/tests/unit/test_cron_manager.py +++ b/tests/unit/test_cron_manager.py @@ -69,6 +69,7 @@ def test_init(self, mock_db): assert manager.db == mock_db assert manager._basic_handlers == {} assert manager._started is False + assert manager._db_synced is False class TestCronJobManagerStart: @@ -95,6 +96,27 @@ async def test_start_idempotent(self, cron_manager, mock_db, mock_context): # Should only sync once assert mock_db.list_cron_jobs.call_count == 1 + @pytest.mark.asyncio + async def test_start_resyncs_after_shutdown( + self, cron_manager, mock_db, mock_context + ): + """Test that restarting the manager resyncs the database.""" + mock_db.list_cron_jobs.return_value = [] + + await cron_manager.start(mock_context) + await cron_manager.shutdown() + + assert cron_manager._started is False + assert cron_manager._db_synced is False + + await cron_manager.start(mock_context) + + assert mock_db.list_cron_jobs.call_count == 2 + assert cron_manager._started is True + assert cron_manager._db_synced is True + + await cron_manager.shutdown() + @pytest.mark.asyncio async def test_start_syncs_after_scheduler_started_early( self, cron_manager, mock_db, mock_context, sample_cron_job