From 28acdc19aeeac065cf9040da675ca020ea8da628 Mon Sep 17 00:00:00 2001 From: ogt Date: Wed, 22 Apr 2026 09:21:24 +0800 Subject: [PATCH] =?UTF-8?q?fix(scheduler):=20=E4=BF=AE=E5=BE=A9=20Gunicorn?= =?UTF-8?q?=204=20workers=20=E9=87=8D=E8=A4=87=E7=99=BC=E9=80=81=E6=8E=92?= =?UTF-8?q?=E7=A8=8B=E9=80=9A=E7=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因:APScheduler 在 openclaw_bot_routes.py 透過 record_once 啟動, 但 record_once 只防止同一 process 內重複;Gunicorn --workers 4 有 4 個獨立 worker process,各自啟動一個 scheduler,導致早報/晚報/Excel 每次觸發都送出 4 份。 修復:start_scheduler() 改用 fcntl.LOCK_EX|LOCK_NB 搶佔 /tmp/openclaw_scheduler.lock, 只有搶到鎖的 worker 啟動排程,其餘 3 個 worker 靜默跳過。 Co-Authored-By: Claude Sonnet 4.6 --- routes/openclaw_bot_routes.py | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/routes/openclaw_bot_routes.py b/routes/openclaw_bot_routes.py index 0c614e2..3416aab 100644 --- a/routes/openclaw_bot_routes.py +++ b/routes/openclaw_bot_routes.py @@ -3300,9 +3300,32 @@ def send_daily_excel(): sys_log.error(f"[AutoExcel] {e}") +_sched_lock_fh = None # 持有鎖的 file handle,進程退出時自動釋放 + + def start_scheduler(): - """啟動排程(Flask app 啟動後呼叫)""" - global _scheduler + """啟動排程(Flask app 啟動後呼叫) + 使用 fcntl exclusive lock 確保 Gunicorn 多 worker 環境下只有一個 worker 運行排程。 + """ + global _scheduler, _sched_lock_fh + import fcntl, atexit + + lock_path = '/tmp/openclaw_scheduler.lock' + try: + _sched_lock_fh = open(lock_path, 'w') + fcntl.flock(_sched_lock_fh.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB) + except OSError: + sys_log.info("[OpenClawBot] Scheduler lock busy — another worker owns it, skipping") + return + + @atexit.register + def _release_sched_lock(): + try: + fcntl.flock(_sched_lock_fh.fileno(), fcntl.LOCK_UN) + _sched_lock_fh.close() + except Exception: + pass + try: from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.triggers.cron import CronTrigger @@ -3315,7 +3338,7 @@ def start_scheduler(): _scheduler.add_job(send_weekly_report, CronTrigger(day_of_week='mon', hour=9, minute=0)) _scheduler.add_job(check_anomalies, CronTrigger(hour='9,12,15,18', minute=0)) _scheduler.start() - sys_log.info("[OpenClawBot] Scheduler started ✓ (competitor/morning/excel/evening/weekly/anomaly)") + sys_log.info("[OpenClawBot] Scheduler started ✓ pid=%d (competitor/morning/excel/evening/weekly/anomaly)", os.getpid()) except ImportError: sys_log.warning("[OpenClawBot] APScheduler 未安裝 — 排程功能停用") except Exception as e: