Cloudflare Agents 持久化调度能力(Scheduler)实战:在纯 Durable Object 上构建定时提醒服务
发布时间:2026/9/18 16:09:45 锦皓数字建站
实战:在纯 Durable Object 上构建定时提醒服务`)
Cloudflare Agents 持久化调度能力Scheduler实战在纯 Durable Object 上构建定时提醒服务【免费下载链接】agentsBuild and deploy AI Agents on Cloudflare项目地址: https://gitcode.com/GitHub_Trending/agents1/agents本篇技术指南围绕agents/schedules导出的Scheduler能力展开演示如何不继承Agent或任何 SDK 基类仅将Scheduler作为可复用的 Lifecycle 能力capability安装到一个普通的 CloudflareDurableObject上实现带持久化存储、可跨实例唤醒执行、支持一次性延迟与 cron 周期的定时提醒服务。读完本文你将掌握Scheduler的安装方式、set()/every()/get()/list()/cancel()完整 API 用法、底层 job queue 与物理 alarm 的协作机制以及如何用 curl 直接演练一个可运行的示例。示例概览examples/next/schedules本仓库的 examples/next/schedules/README.md 提供了一个 early-access 的服务端示例在普通DurableObjectReminderObject上安装Scheduler它不继承Agent或其它 SDK 基类是理解“能力capability组合”这条路线的最小完整范例。完整源码位于 examples/next/schedules/src/index.ts依赖配置见 examples/next/schedules/package.jsonWrangler 配置见 examples/next/schedules/wrangler.jsonc。示例的核心结构如下与文档一致并附上源码中实际的注释与类型import { DurableObject } from cloudflare:workers; import { routeAgentRequest } from agents; import { getCurrentAgent, Lifecycle } from agents/lifecycle; import { Scheduler, type Schedule } from agents/schedules; type ReminderPayload { message: string; }; /** A plain Durable Object with the Scheduler capability installed. */ export class ReminderObject extends DurableObjectEnv { readonly scheduler new Scheduler({ callbacks: { /** * Runs when a reminder schedule fires — with this object available * through getCurrentAgent(), even when the alarm wakes a fresh * instance. Registered callbacks are typed where they are declared and * where they are scheduled. */ deliverReminder: ( payload: ReminderPayload, schedule: ScheduleReminderPayload ) { const { agent } getCurrentAgentReminderObject(); this.ctx.storage.sql.exec( INSERT OR REPLACE INTO delivered_reminders (schedule_id, message, delivered_at, delivered_by) VALUES (?, ?, ?, ?), schedule.id, payload.message, new Date().toISOString(), agent?.lifecycle.name ?? null ); } } }); readonly lifecycle Lifecycle.install(this).use(this.scheduler); // ... onStart() 建表、onRequest() 路由等 }从Scheduler的构造与安装可以看到三条关键事实回调在构造器中注册callbacks是一个具名回调映射callback map每条调度记录持久化保存的是回调名callback name。能力通过 Lifecycle 安装Lifecycle.install(this).use(this.scheduler)一行完成接线。无需任何额外 wiring存储、alarm 协调、宿主调用边界host invocation boundary和事件系统全部来自其安装所在的 Lifecycle。安装与接线为什么“零 wiring”README 明确指出The Scheduler takes no wiring: storage, alarm coordination, the host invocation boundary, and events all come from the Lifecycle it is installed on.这句话对应的是源码层面的能力基类设计。在 packages/agents/src/lifecycle/capability.ts 中Scheduler继承自LifecycleCapability抽象基类。基类在Lifecycle.use()安装时通过bindLifecycleCapability()注入一组标准服务LifecycleServices包括storageDurable Object 的存储句柄jobsLifecycle 持有的作业队列任何入队操作都会自动重新挂载re-arm物理 alarmevents能力事件发布通道routes跨 Lifecycle 的消息路由runInHostContext进入宿主调用上下文的唯一边界能力钩子默认在宿主上下文之外运行回调执行必须通过它进入。在 packages/agents/src/schedules/scheduler.ts 的类注释中写得很明确Scheduler validates schedules and pushes them into the Lifecycle job queue; Lifecycle owns the physical alarm and the alarm event loop, and Scheduler runs registered callbacks through Lifecycles host invocation boundary when its jobs come due.Scheduler 校验并推送作业到 Lifecycle 作业队列Lifecycle 拥有物理 alarm 与 alarm 事件循环当作业到期时 Scheduler 通过 Lifecycle 的宿主调用边界运行回调。因此Scheduler本身不直接持有自己的物理 alarm而是把自己的最早待执行行贡献给 Lifecycle 的共享物理 alarm从而可以与其它同样需要唤醒wake-up的能力例如 Tasks自然组合互不冲突。配置示例Wrangler 侧需要什么Scheduler不依赖额外的 runtime binding示例的 examples/next/schedules/wrangler.jsonc 就是一个标准的新式 SQLite Durable Object 配置{ $schema: ./node_modules/wrangler/config-schema.json, name: next-schedules, main: src/index.ts, compatibility_date: 2026-06-11, compatibility_flags: [nodejs_compat], durable_objects: { bindings: [ { name: ReminderObject, class_name: ReminderObject } ] }, migrations: [ { tag: v1, new_sqlite_classes: [ReminderObject] } ], observability: { enabled: true } }要点说明compatibility_flags: [nodejs_compat]本示例代码运行所依赖的 Node 兼容标志migrations使用new_sqlite_classesScheduler的持久化建立在 Durable Object 的 SQLite 存储之上因此需要 SQLite 迁移标签依赖仅需agentspackage.json中运行时依赖只有agents: *说明能力由该包统一提供。持久化模型待执行与已交付都跨唤醒存活README 提到两个关键持久化事实Scheduler 拥有自己的cf_agents_schedules表并把自己的最早待执行行贡献给 Lifecycle 的共享物理 alarm已交付的提醒记录在宿主自己的 SQL 表中因此待执行与已完成的工作在 Durable Object 离开内存后依然存在。从当前源码看该模型已经演进为v2调度行直接存进 Lifecycle 作业队列SCHEDULE_SCHEMA_VERSION 2见 packages/agents/src/schedules/scheduler.ts。源码中保留了从旧版cf_agents_schedules表到作业队列的幂等迁移逻辑#migrateLegacyScheduleTable读取旧表行、跳过历史遗留的_cf_keepAliveHeartbeat心跳行、把时间戳从秒换算成毫秒、逐行push进作业队列后DROP TABLE并写入 schema 版本标记。迁移后的每条调度本质上是作业队列中的一个 jobjob 的fn是回调名job 的 payload 里携带调度时序词汇type、delayInSeconds、cron、intervalSeconds、owner_path、retry等即SchedulerJobPayload因此get()/list()/cancel()都是对作业队列的按 owner 过滤读写而不是直接查自定义表。示例宿主侧则用onStart()自行创建自己的已交付表用于记录“已执行完成的提醒”实现文档所述的 delivered 查询onStart(): void { // Delivered reminders live in the hosts own table, so they survive the // Durable Object leaving memory just like the pending schedule rows do. this.ctx.storage.sql.exec( CREATE TABLE IF NOT EXISTS delivered_reminders ( schedule_id TEXT PRIMARY KEY, message TEXT NOT NULL, delivered_at TEXT NOT NULL, delivered_by TEXT ) ); }delivered_by记录的是通过getCurrentAgent()拿到当前宿主agent?.lifecycle.name当 alarm 唤醒的是一个全新实例时也能正确回填。核心 APIset / every / get / list / cancelREADME 明确set()和every()会同时针对注册的回调映射对回调名和 payload 做类型检查get()、list()和cancel()用于管理待执行的调度。set()一次性延迟 / 定点或 cron 周期在 packages/agents/src/schedules/scheduler.ts 中set()的签名是async setName extends keyof Handlers string( when: Date | string | number, callback: Name, payload?: SchedulerPayloadHandlers[Name], options?: ScheduleOptions ): PromiseScheduleSchedulerPayloadHandlers[Name]when的三种取值对应三种调度类型解析逻辑见 packages/agents/src/schedules/schedule-timing.ts 的parseWhenwhen取值类型行为number秒delayed从当前时间起 N 秒后执行一次time now NDatescheduled在指定时刻执行一次stringcron 表达式cron按 cron 周期重复执行首次执行时间为下一个匹配时刻示例 HTTP 接口把 cron 字符串或延迟秒数统一交给set()注释说明二者会生成同一种持久化调度行const schedule await this.scheduler.set( body.cron ?? body.seconds ?? 5, deliverReminder, { message } );cron 解析基于cron-schedule包见 packages/agents/src/schedules/schedule-timing.ts 的nextCronTimeMs无法解析的表达式会在创建时直接抛错。every()固定间隔every(intervalSeconds, callback, payload, options?)创建interval类型的周期调度首次执行在一个间隔之后。间隔校验规则同样在 schedule-timing.ts 中必须为正数且不能超过 30 天MAX_INTERVAL_SECONDS 30 * 24 * 60 * 60。// 每 60 秒触发一次 deliverReminder await this.scheduler.every(60, deliverReminder, { message: tick });查询与管理get(id)按 ID 取回一条调度查询不到返回undefined。list(criteria?)列出匹配条件的调度。ScheduleCriteria支持id、type和timeRange起止Date详见 packages/agents/src/schedules/types.ts。cancel(id)取消一条待执行调度返回boolean表示是否命中。Schedule对象本身是一个联合类型按type分为scheduled/delayed/cron/interval四种统一携带id、callback、payload与可选retry类型定义完整见 packages/agents/src/schedules/types.ts。类型安全回调名与 payload 的联动校验README 强调set()andevery()type both the callback name and the payload against the registered callbacks map.这是本示例最值得注意的工程特性。其机制是SchedulerHandlers的泛型参数来自构造器传入的callbacks映射setName extends keyof Handlers string中Name被约束为已注册的回调名payload 类型通过条件类型SchedulerPayloadHandlers[Name]从对应回调函数的第一个参数反推见 packages/agents/src/schedules/types.ts因此回调声明处与调度处都得到类型检查拼错回调名、传错 payload 结构都会在编译期报错。运行时还有一层守卫#validateSchedule与#validateInterval都会校验回调名确实存在于注册映射或组合根 resolver 提供的兜底解析中否则抛出Unknown scheduled callback xxx: not registered on this Scheduler。源码注释指出Agent的历史按名字调度 APIthis.schedule(60, methodName)正是通过setSchedulerCallbackResolver这个内部兜底通道保持兼容的详见 packages/agents/src/schedules/scheduler.ts 中setSchedulerCallbackResolver的说明。重试、幂等与事件SchedulerOptions见 packages/agents/src/schedules/options.ts还提供若干策略选项选项默认值说明callbacks{}具名回调映射必须在每次 Durable Object 唤醒时无条件注册retrymaxAttempts: 3, baseDelayMs: 100, maxDelayMs: 3000回调执行的默认重试策略RetryOptions可在单条调度上覆盖hungScheduleTimeoutSeconds30执行中的 interval 被判定为“悬挂/废弃”的超时秒数onError无观察回调的终态错误作为能力代码运行不在宿主上下文内调度执行与重试过程会通过lifecycle.events.emit发布能力事件SchedulerEventTypeschedule:create、schedule:execute、schedule:retry、schedule:error、schedule:cancel、schedule:duplicate_warning。周期调度cron/interval执行完成后通过#recurrenceOutcome返回rescheduleAt让作业队列安排下一次执行一次性调度执行完则直接完成。ScheduleOptions.idempotent控制去重cron 与 interval 周期调度默认去重idempotent ! false一次性调度默认不去重必须显式传idempotent: true。若在启动流程如onStart()中创建非幂等的一次性调度Scheduler 会打印一条警告提示这可能随每次重启重复建行导致重复执行。运行与演练示例的 examples/next/schedules/package.json 提供了标准脚本pnpm run devwrangler dev、pnpm run deploywrangler deploy、pnpm run typechecktsc --noEmit。在示例目录下启动pnpm install pnpm run dev随后对具名对象demo演练路径形式为/agents/reminder-object/name/...# 创建一个 5 秒后到期的一次性提醒 curl -X POST http://localhost:8787/agents/reminder-object/demo/reminders \ -H content-type: application/json \ -d {message: stand up, seconds: 5} # 或一个每分钟触发的 cron 周期提醒 curl -X POST http://localhost:8787/agents/reminder-object/demo/reminders \ -H content-type: application/json \ -d {message: tick, cron: * * * * *} # 查看待执行调度与已交付提醒 curl http://localhost:8787/agents/reminder-object/demo # 按 id 取消一条待执行调度 curl -X DELETE http://localhost:8787/agents/reminder-object/demo/reminders/id对应的 HTTP 处理逻辑在 examples/next/schedules/src/index.ts 的onRequest中POST .../reminders读取{ message, seconds, cron }seconds缺省为5创建调度后返回201与{ created: schedule }DELETE .../reminders/id调用scheduler.cancel(id)命中返回200未命中返回404GET ...返回name、pendingscheduler.list()结果与按delivered_at倒序排列的delivered列表。验证要点README 强调创建一次性提醒后等待几秒再请求该对象可以看到对应调度行从pending中消失、并以回调打上的时间戳出现在delivered中——即便回调触发的瞬间是 alarm 唤醒的全新实例由于调度行与交付记录都在持久化存储中这一结果依然成立。适用边界与说明Scheduler、SchedulerOptions与回调映射类型在 packages/agents/src/schedules/index.ts 中明确标注为experimentalAPI 表面在稳定前可能调整而Schedule、ScheduleCriteria、ScheduleOptions与Agent的稳定调度方法共享同一套类型相对稳定。本示例属于examples/next目录下的 early-access 示例使用的是Lifecycle能力组合路线与Agent内置调度 APIthis.schedule(...)/this.scheduleCron(...)在语义上同源但面向不继承Agent的场景。若要深入理解底层作业队列与 alarm 事件循环、重试策略tryN、平台级失败分类、memory-limit breaker 等以及能力组合机制可继续阅读 packages/agents/src/schedules/scheduler.ts、packages/agents/src/lifecycle/capability.ts 以及测试目录 packages/agents/src/tests/schedules/capability.test.ts 中的用例。【免费下载链接】agentsBuild and deploy AI Agents on Cloudflare项目地址: https://gitcode.com/GitHub_Trending/agents1/agents创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。