乐于分享
好东西不私藏

Hermes 源码解析之Dispatcher 调度引擎(在一个 Tick 里发生了什么)

Hermes 源码解析之Dispatcher 调度引擎(在一个 Tick 里发生了什么)
建议先阅读 #1 了解整体生命周期。本文会深入剖析 _dispatch_once_locked()。
零、为什么单独写一篇 Dispatcher?

在上一篇文章中,笔者把 Dispatcher 比作 Kanban 的"心脏"——每 60 秒跳动一次,回收过期任务、晋升待办、孵化 Worker。

但"跳动"时内部发生了什么呢?

  • CAS 到底是怎么做到原子领取的?
  • WAL 模式为什么能保证不丢数据?
  • 四层故障恢复分别解决什么问题?
  • 容量控制是如何生效的?

这篇文章主要回答这些问题。

一、入口:`_kanban_dispatcher_watcher`

Dispatcher 不是一个独立进程。它作为 Gateway 的一个后台协程运行:

为什么需要 Dispatcher 锁?

两个 Gateway 同时调度会导致:

1. 回收频率翻倍——一个 Task 被 A 回收后又立即被 B 回收

2. Claim 事件翻倍——日志噪声

3. WAL checkpoint 竞态——SQLite 索引页可能损坏

文件锁是最后一道防线。 `dispatch_in_gateway` 配置是主控,文件锁是容错。

二、Tick 核心:`_dispatch_once_locked()`
Dispatcher Tick 流程 — 8 步顺序执行,3 个阶段。

8 个步骤必须按顺序执行,不可调换。

三、故障恢复的完整图谱

四层恢复 vs 四种故障模式

故障模式被哪一层捕获恢复行为
Worker 进程 Crash(OOM、信号)第三层detect_crashed_workersPID gone → 立即回收 → ready
Worker 卡死(死循环、死锁)第二层detect_stale_runningHeartbeat 超时 → 回收 → ready
慢模型响应(长时间推理)第一层release_stale_claimsPID 存活 → 不回收,延长 TTL
远程 Worker 失联第一层 + 第二层无 PID 检测,TTL 过期 → ready
连续孵化和 Worker 都失败第四层 熔断器failure_limit=2 → auto-blocked

四、容量控制详解

Dispatcher 支持 4 个层级的并发控制:

参数默认值作用域行为
max_spawn无限制全局running + 本次 spawn 总数上限
max_in_progress无限制全局running 状态任务总数上限
max_in_progress_per_profile无限制按 Profile单个 Profile 的 running 上限
Respawn Guard5 分钟冷却按 Task防止同一 Task 被反复孵化

三者的优先级关系

注意: `max_spawn` 是实时并发容量而非"每 Tick 孵化预算"。它会统计 `status='running'` 的所有任务 + 本 Tick 新 spawned 的任务,确保总并发不超过上限。

五、Notifier:结果如何回到用户?

Dispatcher 只负责调度。结果通知由 _kanban_notifier_watcher 完成:

Notifier 的订阅机制:

1. 谁创建的订阅?Agent 在 Worker 启动时自动创建

2. 什么时候取消? Task 达到 `done` 或 `archived` 后自动移除

3. 发送失败怎么办?同一订阅连续 3 次发送失败 → 丢弃该订阅(防止死 Chat 死循环)