diff --git a/astrbot/core/cron/manager.py b/astrbot/core/cron/manager.py index 9e6feb37f6..c42b7197ba 100644 --- a/astrbot/core/cron/manager.py +++ b/astrbot/core/cron/manager.py @@ -103,22 +103,28 @@ 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: 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: 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 242312754b..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,52 @@ 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 + ): + """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."""