Repository Wiki
zai-org/ZCode

服务监督、崩溃预算与运行状态

该子系统由 Supervisor 负责服务 Core 进程的生命周期、单实例锁、控制端点、启动恢复、状态快照与更新期间的运行边界;它同时持有 CrashBudget,用于监督异常退出相关的预算策略。当前源码证据主要集中在 packages/zcode-server-cli/src/supervisor/supervisor.ts,崩溃预算的具体阈值和判定实现未在本次受限源码读取中展开。

Purpose and Scope

本文覆盖 server CLI 中 Supervisor 的启动、停止、重启、Core 子进程收口、状态持久化、更新恢复和并发生命周期操作,以及这些机制与 data-root lock、control server、release manager 的关系。页面重点是“监督与运行状态”这一边界。

更新包的具体事务格式、release 选择算法、IPC 请求契约和 CLI 参数解析属于相邻实现;如需修改这些部分,应分别阅读 ReleaseManager、control server、contracts 与 CLI 入口,而不应把它们的完整行为推断为 Supervisor 行为。CrashBudget 已由 Supervisor 构造并持有,但本页不虚构其内部窗口、阈值或重启决策,因为这些实现细节未在已读取源码中出现。

Overview

Supervisor 是一个持有状态的生命周期协调器,而不是简单的子进程启动包装器。它维护:

  • state:当前 LifecycleState,初始为 stopped;
  • generation:Core 代际计数;
  • core:被监督的 ChildProcess;
  • control:控制端点服务器;
  • DataRootLock:保证同一 data-root 只有一个有效 Supervisor;
  • ReleaseManager:确保并读取当前可执行 release;
  • persistStatusSnapshot:将 status() 结果写入 status 文件;
  • lifecycleOperation:串行化 stop、restart、update、uninstall 等生命周期操作;
  • CrashBudget:异常退出监督所需的预算对象。

设计核心是先取得单实例锁,再进行启动恢复、读取 current release、建立控制端点并启动 Core。停止时则必须观察到 Core 的 exit 或 close,否则不能释放锁;这避免旧进程仍存活时第二个 Core 被错误启动。

Architecture

Loading diagram...

Source: supervisor.ts

Supervisor 负责组合这些组件,但不把 release 事务和状态快照逻辑复制进自身:release 选择交给 ReleaseManager,启动恢复交给 recoverSupervisorStartup,状态落盘交给 createStatusPersister。这种划分使生命周期临界区仍由 Supervisor 统一控制,同时把具体存储和恢复策略隔离出去。

核心状态与对象初始化

构造函数默认解析 server layout,使用 layout 中的 lock 文件创建 DataRootLock,初始化 CrashBudget 和 ReleaseManager,并为 status 文件建立持久化闭包。状态快照失败不会直接改变核心生命周期;持久化器通过告警日志报告失败。

typescript
1export class Supervisor { 2 private readonly layout: ServerLayout; 3 private readonly lock: DataRootLock; 4 private readonly crashBudget: CrashBudget; 5 private readonly releaseManager: ReleaseManager; 6 private core: ChildProcess | undefined; 7 private control: Awaited<ReturnType<typeof createControlServer>> | undefined; 8 private state: LifecycleState = "stopped"; 9 private generation = 0; 10 private host: string | null = null; 11 private port: number | null = null; 12 private startedAt: number | null = null; 13 private runningTaskCount = 0; 14 private lastExitReason: string | null = null; 15 private readonly persistStatusSnapshot: () => Promise<void>; 16 17 public constructor(private readonly options: SupervisorOptions) { 18 this.layout = options.layout ?? resolveServerLayout(); 19 this.lock = new DataRootLock(this.layout.lockFile); 20 this.crashBudget = new CrashBudget({ now: options.now }); 21 this.releaseManager = new ReleaseManager(this.layout); 22 this.persistStatusSnapshot = createStatusPersister( 23 this.layout.statusFile, 24 () => this.status(), 25 (error) => log.warn("failed to persist status snapshot", error), 26 ); 27 } 28}

Source: supervisor.ts

启动流程与恢复边界

start() 首先处理幂等快速路径:当状态已经是 ready 或 starting 时直接返回当前状态。否则先执行 releaseManager.ensure(),再获取 data-root lock。锁必须先于启动恢复,因为恢复会回写 current.json 并删除 update transaction;如果存活 Supervisor 同时执行 apply-update,可能出现内存运行一个 release、磁盘 current pointer 却被另一个启动者回滚的分裂状态。

取得锁后,Supervisor 按以下顺序工作:

  1. 调用 recoverSupervisorStartup,处理上次更新留下的启动事务;恢复失败时把状态置为 stop-failed 并持久化原因。
  2. 恢复完成后重新读取 readCurrentForExecution(),确保启动的是恢复后的 candidate,而不是恢复前缓存的 release。
  3. 创建权限收紧为 0700 的 run 目录。
  4. 创建 control server,并把请求处理器绑定到 handleControl。
  5. 进入 starting,记录启动日志,调用 launchCore()。
  6. 立即持久化状态并返回 status()。
typescript
1public async start(): Promise<ServerStatus> { 2 if (this.state === "ready" || this.state === "starting") return this.status(); 3 await this.releaseManager.ensure(); 4 await this.lock.acquire(); 5 try { 6 await recoverSupervisorStartup( 7 this.releaseManager, 8 this.layout.uninstalledFile, 9 this.layout.serverRoot, 10 async (error) => { 11 this.state = "stop-failed"; 12 this.lastExitReason = `update rollback recovery failed: ${updateErrorMessage(error)}`; 13 await this.persistStatusSnapshot(); 14 }, 15 ); 16 this.activeRelease = await this.releaseManager.readCurrentForExecution(); 17 await mkdir(this.layout.runDir, { recursive: true, mode: 0o700 }); 18 const handler: ControlHandler = (request) => this.handleControl(request); 19 this.control = await createControlServer(this.layout.controlEndpoint, handler); 20 this.state = "starting"; 21 this.launchCore(); 22 await this.persistStatusSnapshot(); 23 return this.status(); 24 } catch (error) { 25 // 启动失败时还要收口 Core 和 control,再决定是否释放锁。 26 throw error; 27 } 28}

Source: supervisor.ts

启动异常路径不是简单的 finally { releaseLock() }。Supervisor 会分别尝试停止已经启动的 Core、关闭 control server;只有两者都收口后才把状态持久化为 stopped(或保留 stop-failed)并释放锁。如果 Core 或 control 仍无法收口,则保持 stop-failed 和锁,防止重试制造第二个实例。

停止、重启与操作串行化

公开的 stop() 和 restart() 都经过 runLifecycleOperation。因此生命周期操作不是任意并发地直接修改 core、control 与 state:同一时间的 stop、restart、update、uninstall 会由统一操作门控协调。restart() 先以 restart 原因执行内部停止,再重新启动;停止阶段不会触发最终的 onStopped 回调,避免重启被观察为完整终止。

当没有 Core 时,stopInternal() 仍会将状态写为 stopped、关闭可能存在的 control server、释放锁并返回快照。这保证“部分启动”或重复停止不会遗留 control endpoint 或锁。

Core 终止与锁安全

stopCore() 将状态设为 stopping 并保存退出原因。它先等待优雅退出;超过 coreStopGraceTimeoutMs(生产默认 5 秒)后发送 SIGKILL,再等待 coreKillTimeoutMs(生产默认 2 秒)。关键约束是:发送 SIGKILL 后,只有观察到 exit 或 close 才算进程真正收口。若第二个等待窗口结束仍未观察到终态,方法抛出错误并保留 Core 引用和 data-root lock。

typescript
1private async stopCore(reason: string): Promise<void> { 2 if (!this.core) return; 3 this.state = "stopping"; 4 this.lastExitReason = reason; 5 const core = this.core; 6 await new Promise<void>((resolve, reject) => { 7 let killTimer: NodeJS.Timeout | undefined; 8 let settled = false; 9 const finish = (): void => { 10 if (settled) return; 11 settled = true; 12 cleanup(); 13 resolve(); 14 }; 15 const fail = (): void => { 16 if (settled) return; 17 settled = true; 18 cleanup(); 19 reject( 20 new Error(`Server Core pid ${core.pid ?? "unknown"} did not terminate after SIGKILL`), 21 ); 22 }; 23 const forceKill = (): void => { 24 core.kill("SIGKILL"); 25 if (!settled) killTimer = setTimeout(fail, this.options.coreKillTimeoutMs ?? 2_000); 26 }; 27 const graceTimer = setTimeout(() => { 28 forceKill(); 29 }, this.options.coreStopGraceTimeoutMs ?? 5_000); 30 core.once("exit", finish); 31 core.once("close", finish); 32 }); 33}

Source: supervisor.ts

这种等待策略的设计意图是把“发送信号”与“进程已经终止”区分开。若在未收到终态事件时释放锁,新的 Supervisor 可能在旧 Core 仍运行时取得同一 data-root,造成双 Core 或端口冲突。

Core 流程图

Loading diagram...

Source: supervisor.ts Source: supervisor.ts

API 与配置参考

constructor(options: SupervisorOptions)

创建 Supervisor,并注入启动器、版本和可选运行参数。layout、now、onStopped 等选项用于运行时替换或测试;now 被传给 CrashBudget,说明崩溃预算依赖可替换时钟,但本次读取没有看到预算计算 API。

选项类型默认值作用
layoutServerLayoutresolveServerLayout()提供 lock、run、status、control endpoint、server root 等路径。
launcherCoreLauncher必填通过 launch(generation, release?) 创建 Core 子进程。
versionstring必填启动日志中的 Supervisor 版本。
serviceRegisteredboolean未在该文件中给出显式默认值标记服务是否已注册。
coreReadyTimeoutMsnumber生产默认 15 秒注释声明用于缩短 ready 等待;具体读取位置未在当前摘录中出现。
coreStopGraceTimeoutMsnumber5,000 ms优雅停止等待时间。
coreKillTimeoutMsnumber2,000 msSIGKILL 后等待终态的时间。
now() => numberCrashBudget 默认时钟注入崩溃预算的当前时间来源。
onStopped() => void可选非 restart 停止完成后调用。

start(): Promise<ServerStatus>

启动或返回当前 Supervisor 状态。若状态为 ready 或 starting,方法直接返回当前快照;否则执行 release 确保、单实例锁获取、启动恢复、current release 读取、control server 创建和 Core 启动。

stop(reason = "requested"): Promise<ServerStatus>

通过生命周期操作门控停止服务。没有 Core 时也会清理 control server、释放锁并持久化 stopped;有 Core 时必须先完成 stopCore()。

restart(): Promise<ServerStatus>

串行执行 stopInternal("restart") 后调用 start()。restart 原因不会触发 onStopped,从而避免把重启中间态当作最终停止事件。

状态、失败模式与并发边界

启动恢复失败

恢复回滚失败会设置 stop-failed,并将 lastExitReason 写成带有 updateErrorMessage(error) 的原因。即便没有可停止的 Core 或可关闭的 control,也会保留该状态并持久化,向后续 status 查询暴露需要人工处理的事务。

Control 或 Core 无法收口

启动过程中任一环节失败时,Supervisor 分别尝试停止 Core 与关闭 control。只有 coreStopped && controlClosed 才释放锁;否则保持 stop-failed。这是本实现最重要的并发保护之一。

SIGKILL 后无终态

SIGKILL 调用成功并不等于子进程生命周期事件已经被观察到。若在 coreKillTimeoutMs 内没有 exit 或 close,操作失败,锁和 Core 引用保留,避免第二个实例接管同一 data-root。

重复 start / stop

start() 对 ready 和 starting 做幂等短路;stopInternal() 对不存在的 Core 做清理型停止。因此调用者不需要假定每次调用都伴随一次新的 OS 进程创建,但源码未表明所有 control 请求在业务层面的重复请求语义,应以 handleControl 和 contracts 实现为准。

崩溃预算

Supervisor 在构造时创建 CrashBudget({ now: options.now }),并将其作为私有成员保存。当前受限读取没有包含 crashBudget.ts 的实现,因此无法确认预算窗口、允许次数、耗尽后的状态迁移或是否触发重启。任何运营手册都不应依据本文推导具体阈值。

运行与扩展注意事项

  • 需要调整停止超时时,应优先通过 coreStopGraceTimeoutMs 和 coreKillTimeoutMs 注入,而不是改变锁释放条件。超时只能改变等待窗口,不能把未观察到终态视为成功。
  • 需要替换路径布局时,应注入 ServerLayout,并保持 lock、status、run 与 control endpoint 的一致性;Supervisor 的构造函数会把同一个 layout 交给 lock、release manager 和状态持久化器。
  • 需要测试时间相关行为时,可注入 now;这只证明时钟是可替换依赖,不代表崩溃预算本身是可配置的。
  • 需要扩展生命周期命令时,应沿用 runLifecycleOperation 的串行边界,并明确 stop、restart、update、uninstall 的状态所有者。不能通过额外后台任务绕过 data-root lock。
  • 日志等级已经按生命周期事件与高频明细区分:Supervisor 的生命周期日志使用 info,可恢复异常使用 warn,无法收口等错误使用 error;生产诊断不应把高频 heartbeat/task activity 提升为普通日志。

说明:本页基于受限读取到的 Supervisor、CLI 交叉引用和导出信息编写。CrashBudget 的内部实现、完整 status() 字段、handleControl() 路由及测试覆盖范围未在本次源码读取中展开;这些部分应在对应 sibling page 或后续源码审阅中补充,不能从名称推断行为。

Sources

(2 files)
(root)
packages/zcode-server-cli/src/supervisor