11.3 Scheduler:定时任务调度

模型: claude-opus-4-6 (anthropic/claude-opus-4-6) 生成日期: 2026-02-17


除了事件驱动的实时通信,OpenCode 还需要一些周期性的后台任务——例如定时清理过期的快照数据、删除过时的工具输出文件。这些任务不需要立即执行,但需要定期运行以维护系统健康。Scheduler 模块为此提供了一个轻量级的定时任务调度器。

11.3.1 Scheduler.register()——任务注册

Scheduler 的完整实现只有 62 行代码,但涵盖了任务调度的核心功能:

// scheduler/index.ts
export namespace Scheduler {
  const log = Log.create({ service: "scheduler" })

  export type Task = {
    id: string                      // 任务唯一标识
    interval: number                // 执行间隔(毫秒)
    run: () => Promise<void>        // 任务执行函数
    scope?: "instance" | "global"   // 作用域
  }

  export function register(task: Task) {
    const scope = task.scope ?? "instance"
    const entry = scope === "global" ? shared : state()

    // 全局任务只注册一次
    const current = entry.timers.get(task.id)
    if (current && scope === "global") return

    // 实例任务则替换旧的定时器
    if (current) clearInterval(current)

    entry.tasks.set(task.id, task)

    // 立即执行一次
    void run(task)

    // 设置周期定时器
    const timer = setInterval(() => {
      void run(task)
    }, task.interval)
    timer.unref()  // 关键:不阻止进程退出
    entry.timers.set(task.id, timer)
  }
}

register() 的行为有以下特点:

1. 即时首次执行:任务注册后会立即执行一次(void run(task)),而不是等到第一个 interval 周期结束。这确保了初始清理在系统启动时就执行。

2. 幂等注册:对于全局任务,如果已经注册过相同 ID 的任务,register() 会直接返回(if (current && scope === "global") return)。对于实例任务,则会清除旧定时器并重新注册。

3. timer.unref():这是 Node.js 中一个重要的 API。默认情况下,活跃的定时器会阻止 Node.js 进程退出——进程会一直等待,直到所有定时器被清除。unref() 告诉运行时"这个定时器不重要,不要因为它而保持进程存活"。这对于 CLI 工具尤为重要——当用户按 Ctrl+C 退出时,清理任务的定时器不应该阻止进程终止。

衍生解释:timer.unref() 与事件循环

Node.js(和 Bun)的事件循环(Event Loop)会持续运行,只要还有"待处理的异步操作"——包括活跃的定时器、未完成的 I/O、待处理的 Promise 等。当所有操作完成后,事件循环退出,进程终止。

setInterval() 创建的定时器默认会让事件循环保持活跃。如果一个定时器每小时执行一次,那么进程会至少运行一个小时才会退出。

timer.unref() 标记这个定时器为"不重要的"——事件循环在决定是否退出时会忽略它。如果除了 unref() 的定时器之外没有其他待处理的操作,进程可以立即退出。

11.3.2 实例级(instance)vs 全局级(global)任务

Scheduler 支持两种作用域,对应不同的存储位置和生命周期:

特性
Instance 级
Global 级

存储位置

Instance.state()

模块顶层变量 shared

生命周期

随 Instance 创建/销毁

随进程启动/退出

多次注册

替换旧任务(允许重注册)

忽略重复注册

多 Instance

每个 Instance 有独立的任务集

所有 Instance 共享一个

自动清理

Instance 销毁时自动 clearInterval

进程退出时自动回收

选择哪种作用域取决于任务的性质:

  • Instance 级适用于与特定项目绑定的任务(如快照清理——每个项目有独立的快照仓库)。

  • Global 级适用于系统级的公共任务(如工具输出清理——所有项目共享同一个输出目录)。

11.3.3 错误处理:静默容错

任务执行函数被一个 catch 包裹,确保单次执行失败不会影响后续调度:

这种"记录错误但继续运行"的策略适合清理类任务——如果某次快照清理因为磁盘临时不可用而失败,一小时后再试很可能就成功了。

11.3.4 实际应用场景

截至当前版本,Scheduler 有两个实际注册的任务:

快照清理(Instance 级)

快照清理使用 git gc --prune=7.days 清除超过 7 天的 Git 对象。这个任务是 Instance 级的,因为每个项目有独立的快照仓库(参见 10.1 节),需要各自清理。

工具输出清理(Global 级)

工具输出清理扫描 ~/.local/share/opencode/tool-output/ 目录,删除超过 7 天的输出文件。回顾第 5 章,当工具的输出超过 2000 行或 50KB 时,完整输出会被保存到文件中,并向 LLM 返回截断版本和文件路径。这些临时文件需要定期清理。

这个任务是 Global 级的——所有项目的工具输出都存储在同一个目录中,只需要一个清理任务即可。

时序示意

11.3.5 小结

Scheduler 模块以极简的代码量(62 行)实现了一个功能完备的定时任务调度器:

特性
实现

即时首次执行

注册后立即 run(task)

周期执行

setInterval(fn, interval)

非阻塞退出

timer.unref()

作用域隔离

Instance 级 vs Global 级

幂等注册

全局任务不重复,实例任务可替换

自动清理

Instance.state() 销毁回调中 clearInterval

错误容忍

catch + 日志,不中断后续调度

与更复杂的任务调度框架(如 node-cron)相比,Scheduler 没有 cron 表达式、优先级队列、依赖关系等高级功能。但对于 OpenCode 当前的需求——两个每小时执行一次的清理任务——这种极简设计正好足够,没有引入不必要的复杂性。这体现了 OpenCode 代码库中反复出现的设计哲学:够用即可,不过度设计

Last updated