服务监督、崩溃预算与运行状态
该子系统由 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
Source: supervisor.ts
Supervisor 负责组合这些组件,但不把 release 事务和状态快照逻辑复制进自身:release 选择交给 ReleaseManager,启动恢复交给 recoverSupervisorStartup,状态落盘交给 createStatusPersister。这种划分使生命周期临界区仍由 Supervisor 统一控制,同时把具体存储和恢复策略隔离出去。
核心状态与对象初始化
构造函数默认解析 server layout,使用 layout 中的 lock 文件创建 DataRootLock,初始化 CrashBudget 和 ReleaseManager,并为 status 文件建立持久化闭包。状态快照失败不会直接改变核心生命周期;持久化器通过告警日志报告失败。
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 按以下顺序工作:
- 调用
recoverSupervisorStartup,处理上次更新留下的启动事务;恢复失败时把状态置为stop-failed并持久化原因。 - 恢复完成后重新读取
readCurrentForExecution(),确保启动的是恢复后的 candidate,而不是恢复前缓存的 release。 - 创建权限收紧为
0700的 run 目录。 - 创建 control server,并把请求处理器绑定到
handleControl。 - 进入
starting,记录启动日志,调用launchCore()。 - 立即持久化状态并返回
status()。
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。
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 流程图
Source: supervisor.ts Source: supervisor.ts
API 与配置参考
constructor(options: SupervisorOptions)
创建 Supervisor,并注入启动器、版本和可选运行参数。layout、now、onStopped 等选项用于运行时替换或测试;now 被传给 CrashBudget,说明崩溃预算依赖可替换时钟,但本次读取没有看到预算计算 API。
| 选项 | 类型 | 默认值 | 作用 |
|---|---|---|---|
layout | ServerLayout | resolveServerLayout() | 提供 lock、run、status、control endpoint、server root 等路径。 |
launcher | CoreLauncher | 必填 | 通过 launch(generation, release?) 创建 Core 子进程。 |
version | string | 必填 | 启动日志中的 Supervisor 版本。 |
serviceRegistered | boolean | 未在该文件中给出显式默认值 | 标记服务是否已注册。 |
coreReadyTimeoutMs | number | 生产默认 15 秒 | 注释声明用于缩短 ready 等待;具体读取位置未在当前摘录中出现。 |
coreStopGraceTimeoutMs | number | 5,000 ms | 优雅停止等待时间。 |
coreKillTimeoutMs | number | 2,000 ms | SIGKILL 后等待终态的时间。 |
now | () => number | CrashBudget 默认时钟 | 注入崩溃预算的当前时间来源。 |
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 提升为普通日志。
Related Links
说明:本页基于受限读取到的 Supervisor、CLI 交叉引用和导出信息编写。
CrashBudget的内部实现、完整status()字段、handleControl()路由及测试覆盖范围未在本次源码读取中展开;这些部分应在对应 sibling page 或后续源码审阅中补充,不能从名称推断行为。