Scheduler 定时执行
让 Agent 定时/周期性自动运行("每天早上 8 点跑")。今天读 executor/scheduler.py:基于 APScheduler、持久化、cron 兼容、孤儿任务自愈。
👶 小白:服务半夜重启了,我之前设的"每天 8 点跑"的定时任务会不会就丢了?
👨🏫 老师:不会。如果闹钟只记在内存里,一断电(重启)确实全没了;所以平台把定时任务持久化进 JobStore(数据库)——相当于写进了日程本。服务重启后会从日程本把闹钟重新加载回来,接着到点触发。这就是"持久化"要解决的核心问题:状态不随进程消失。
为什么要定时
回忆 Day 01 的例子:"每天早上把新闻头条总结后发到我邮箱"。这个"每天早上"就靠 Scheduler——让 Agent 无需人工触发、按时间表自动跑。这正是"持续运行的 AI Agent"(AutoGPT 的定位)的关键。
APScheduler 基础
while True: sleep(60)?重启就全丢、错过了不补、时区还容易错。class Scheduler(AppService)(scheduler.py:1255),内部揣着一个 BackgroundScheduler(scheduler.py:22)。你用 cron 表达式登记一个闹钟(CronTrigger.from_crontab,scheduler.py:1208),到点它就调 execute_graph(scheduler.py:159)——最终汇入 Day 05 那条"发 MQ → 引擎执行"的主链路。闹钟只负责"掐点喊一嗓子",真正跑图还是执行引擎。Scheduler 基于 APScheduler(Python 成熟的定时任务库)的 BackgroundScheduler,是一个独立的 RPC 服务(AppService)。它支持几种触发器:
- cron 触发:按 cron 表达式("0 8 * * *" = 每天 8 点)。
- interval 触发:按固定间隔(每 30 分钟)。
- date 触发:在某个具体时刻跑一次。
"0 9 * * 1-5"(工作日每天 9:00):from_crontab("0 9 * * 1-5")(scheduler.py:1208)→ 生成一个 CronTrigger → APScheduler 算出下一次触发时刻 周一 09:00,到点调 execute_graph,跑完再算下一次 周二 09:00……任务参数(跑哪张图、哪个用户、输入是什么)打包进
GraphExecutionJobArgs(scheduler.py:1035)随任务存下来。持久化 JobStore
定时任务用 SQLAlchemyJobStore 存进 Postgres(scheduler.py:1302,表 apscheduler_jobs)——进程重启不丢。job_defaults(:1308)有几个关键设置:
coalesce=True # 错过的重复任务只补跑最新一次(别补一堆)
max_instances=1000
misfire_grace_time=None # 永不因错过而丢弃
同步/异步桥接
APScheduler 跑在线程池(同步),但平台业务逻辑是 async 的。run_async(scheduler.py:148)用 run_coroutine_threadsafe 把协程丢到一个常驻 event loop 线程执行——搭起"同步调度器 ↔ 异步业务"的桥。
async 的(异步世界)。两个世界不能直接互调——同步线程不能直接 await 协程。run_coroutine_threadsafe 就是那座桥:把异步协程"提交"到一个专门的异步事件循环去跑,同步这边等它结果。混合同步库(APScheduler)和异步业务是常见场景,这个桥接模式很实用。(CrewAI 的调度器也是同样处理。)触发即回主链路
定时时刻到了,APScheduler 调 execute_graph(scheduler.py:159)→ _execute_graph → 调 add_graph_execution——又回到 Day 05 那条"发 MQ → 引擎执行"的主链路!
add_graph_execution 发 MQ、由执行引擎消费执行。Scheduler 只是"按时间自动调用"那个入口,执行逻辑完全复用。add_graph_execution)。这意味着:定时执行和手动执行走完全一样的流程(扣费、拓扑执行、实时推送、计费对账)——不用为定时执行单独写一套逻辑。"多种触发源 → 同一执行入口"是清晰架构的标志。定时任务参数用 kind 字段区分(graph 执行 vs copilot 任务),是"多态判别器"设计。cron 兼容坑
一个真实的坑(scheduler.py:1143 的 _normalize_cron_day_of_week):APScheduler 的星期编号(0=周一)和 Unix cron(0=周日)不一致,需要转换,还要处理范围回绕(如 6-2 跨周末)。
孤儿任务自愈
定时任务运行时,如果它要跑的图已经被删了(或移出库、校验失败),怎么办?Scheduler 会捕获异常并自动删除这个孤儿定时任务(scheduler.py:189、_cleanup_orphaned_schedules_for_graph,:372)。
validate_and_construct_node_execution_input 校验图能跑(:1556)——防止"定时任务运行时才炸",把错误提前到设置时。这些自愈/前置校验让定时系统健壮、不留垃圾。今日小结 + 动手
🧠 今天你应该能回答
- 定时为什么是"持续 Agent"的核心?
- 持久化 JobStore + coalesce + misfire_grace 各解决什么异常?
- 同步/异步桥接为什么需要?
- 定时触发和手动触发为什么殊途同归?
- cron 星期编号坑是什么?孤儿任务怎么自愈?
✋ 动手
P=autogpt_platform/backend/backend
grep -n 'SQLAlchemyJobStore\|coalesce\|misfire_grace\|run_async' $P/executor/scheduler.py | head
grep -n 'def _execute_graph\|def add_graph_execution_schedule\|_normalize_cron_day_of_week\|_cleanup_orphaned' $P/executor/scheduler.py