From aadbe80a23e53db2e2a08f0319f6dc694d3bca5d Mon Sep 17 00:00:00 2001 From: steven_li Date: Tue, 16 Jun 2026 09:40:57 +0800 Subject: [PATCH] =?UTF-8?q?fix(cron=5Fservice):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=BB=BB=E5=8A=A1=E5=90=AF=E7=94=A8=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E6=97=B6=E7=9A=84=E6=AD=BB=E9=94=81=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 当定时任务服务正在运行时,更新任务的启用状态可能导致死锁。 现在通过改进锁的使用方式来避免这个问题。 在update_enabled方法中添加了正确的变量初始化, 并在循环逻辑中进行了优化以确保正确释放锁。 同时添加了专门的测试用例来验证在并发场景下不会发生死锁。 --- .../backend/beaver/services/cron_service.py | 9 ++++--- .../backend/tests/unit/test_cron_service.py | 26 +++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/app-instance/backend/beaver/services/cron_service.py b/app-instance/backend/beaver/services/cron_service.py index 21678a9..9072ec6 100644 --- a/app-instance/backend/beaver/services/cron_service.py +++ b/app-instance/backend/beaver/services/cron_service.py @@ -134,6 +134,7 @@ class CronService: return job def update_enabled(self, job_id: str, enabled: bool) -> CronJob | None: + updated_job: CronJob | None = None with self._lock: jobs = self._load_jobs_unlocked() for job in jobs: @@ -143,9 +144,11 @@ class CronService: job.updated_at_ms = _now_ms() job.next_run_at_ms = compute_next_run(job.schedule) if job.enabled else None self._save_jobs_unlocked() - self._arm_timer() - return job - return None + updated_job = job + break + if updated_job is not None: + self._arm_timer() + return updated_job def remove_job(self, job_id: str) -> bool: with self._lock: diff --git a/app-instance/backend/tests/unit/test_cron_service.py b/app-instance/backend/tests/unit/test_cron_service.py index 3bbe880..b6476a0 100644 --- a/app-instance/backend/tests/unit/test_cron_service.py +++ b/app-instance/backend/tests/unit/test_cron_service.py @@ -1,4 +1,5 @@ import asyncio +import threading from beaver.foundation.models import CronExecutionResult, CronRunRecord, CronSchedule from beaver.tools.base import ToolContext @@ -108,6 +109,31 @@ def test_persisted_interval_job_keeps_schedule_and_next_run(tmp_path) -> None: assert reloaded.next_run_at_ms == job.next_run_at_ms +def test_running_scheduler_can_disable_job_without_deadlock(tmp_path) -> None: + service = CronService(tmp_path / "jobs.json") + job = service.add_job( + name="Hydration reminder", + message="Drink water", + schedule=CronSchedule(kind="every", every_ms=30 * 60 * 1000), + ) + service._running = True + completed = threading.Event() + enabled_values: list[bool] = [] + + def disable_job() -> None: + updated = service.update_enabled(job.id, False) + if updated is not None: + enabled_values.append(updated.enabled) + completed.set() + + worker = threading.Thread(target=disable_job, daemon=True) + worker.start() + + assert completed.wait(0.5), "disabling a running cron job should not deadlock" + assert enabled_values == [False] + assert service.get_job(job.id).enabled is False + + def test_cron_tool_uses_runtime_service(tmp_path) -> None: service = CronService(tmp_path / "jobs.json") tool = CronTool()