T3 Code 服务器后台服务更新机制解析:launcher 仲裁、试运行提交边界与 SQLite 回滚
2026/9/15 11:47:57 网站建设 项目流程

T3 Code 服务器后台服务更新机制解析:launcher 仲裁、试运行提交边界与 SQLite 回滚

【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code

导读

本文以 docs/internals/server-updates.md 为骨架,结合 T3 Code 仓库中 serviceLauncher.ts、serviceProtocol.ts、selfUpdate.ts 等源码,系统讲解 T3 Code 后台服务(background service)的更新架构:为什么更新只能由 stable launcher 仲裁、试运行(trial)为何必须越过"提交边界"才允许对外就绪、SQLite 三文件快照如何让迁移可回滚、客户端如何通过 update ID 区分"替换成功"与"回滚"。读完你将掌握整套服务更新的状态机、IPC 协议与崩溃恢复规则,可直接对照源码逐行验证。

架构总览:谁有资格更新服务

T3 Code 的后台服务并非由服务进程自己更新自己。从源码看,整个更新流程围绕一个常驻的stable launcher(稳定启动器)展开,其完整实现位于 apps/server/src/serviceLauncher.ts:

  • launcher 是被 systemd(Linux)或 launchd(macOS)选中的服务运行时(service runtime)的唯一持有者,也是服务持久状态(durable service state)的唯一写入方
  • 服务子进程(server child)只通过继承的 IPC 通道请求更新,永不重写自己的服务定义(service definition),也不选择自己的替代者
  • 本地服务命令(如t3 service系列命令)只允许在服务停止期间替换 launcher 与状态文件;
  • 前台 CLI 进程不进行自我更新(foreground CLI processes do not self-update)。

Launcher 之所以必须"稳定",注释写得很清楚:它要跨服务版本存活("keep working across server versions"),因此 serviceLauncher.ts 只用 Node 内置模块(node:child_processnode:fsnode:crypto等),不依赖任何 Effect 运行时——它是整个可执行体中唯一不能假设"其余部分可加载"的组件。

Launcher 的入口要求环境变量T3CODE_HOME,读取<baseDir>/runtime/service-state.json(由SERVICE_STATE_FILE常量定义,见 serviceProtocol.ts)中的ServiceState后进入运行循环;状态文件无效时直接抛错,而不是猜测该启动哪个运行时。

运行时目录与"精确版本安装"

服务更新针对的是**精确版本(exact version)**的不可变运行时。每个目标版本以解压后的 release 归档形式固定在:

<baseDir>/runtime/versions/<version>/ ├── t3 # 可执行文件(Windows 为 t3.exe) └── .install-complete # 安装完成哨兵文件,内容为该版本号

路径构造见 serviceLauncher.ts 中的runtimePaths,安装事务则集中在 pinnedRuntime.ts:

  • 精确版本安装使重启不依赖 npm 缓存驱逐(cache eviction)或漂移的发布 tag;
  • 安装与预检(preflight)都在暂存(staging)阶段完成,之后才发布不可变运行时;
  • 哨兵文件只在解压与校验成功后写入——仅检查可执行文件存在不够,因为 tar 会先写出可执行文件再写出原生包,被中断的安装可能留下"看似完整实则损坏"的目录树(pinnedRuntime.ts);
  • 安装全程加信号量串行化(pinnedRuntimeInstallLock),超时上限为 10 分钟。

runtimeExists校验(serviceLauncher.ts)同时要求入口文件存在且哨兵内容与版本号一致,两者任一失败都视为该运行时缺失或不完整。

预检:为什么协议版本决定能否升级

文档强调:预检检查 launcher 协议,因为需要新回滚保证的目标版本不能安全地运行在旧 launcher 之下。升级 launcher 本身需要一次本地服务更新(local service update)。

预检实现在 servicePreflight.ts:目标运行时以__service-preflight子命令被拉起(见 selfUpdate.ts):

t3 __service-preflight --database-path <dbPath> --launcher-protocol <protocol>
  • 若传入的 launcher 协议不等于当前SERVICE_LAUNCHER_PROTOCOL,预检返回blocked,原因固定为:"This release requires a newer T3 Code service launcher. Update it on the server machine.";
  • 只有status === "ready"且返回版本与目标版本一致才算通过;返回的 JSON 需能被decodeServicePreflightResult完整解码,否则判定暂存失败。

预检是暂存阶段(installing之前的downloading阶段)的一部分,超时 30 秒。它把"目标运行时是否具备新协议要求的回滚能力"这个问题前置到切流之前解决。

提交边界(Commit Boundary):试运行的三阶段状态机

文档将更新分为三个核心阶段:pending → trial(prepared)→ committed / rolled-back / failed。对应状态机记录在 serviceProtocol.ts 的ServiceStateServiceUpdateRecord中。

1. 记录 pending 后才确认

子进程通过 IPC 发送request-update(携带targetVersiondbPath),launcher 在 serviceLauncher.ts 的#handleUpdateRequest中做完整校验:

校验项拒绝原因(reason)
请求方必须是 active 角色Only the active server can request an update.
请求方版本必须等于状态中的 activeVersionThe requesting server is not the selected active version.
不能已有 pending 更新Another server update is already pending.
目标必须是精确 SemVer(禁止 dist-tag/范围/路径)The requested target is not an exact version.
目标必须更新Remote updates must select a newer server version.
数据库路径必须绝对路径The requested database path is not absolute.
目标运行时必须已完整安装The requested target runtime is missing or incomplete.

通过校验后,launcher先持久化写盘ServiceState { update: pending },再回复update-accepted(携带随机生成的updateId)。也就是说:确认发生在持久化之后,防止确认后崩溃导致"以为已受理、实际无记录"。精确版本的正则与比较器见 serviceProtocol.ts 与compareExactServiceVersions(BigInt 实现,构建元数据被忽略)。

随后 launcher 等待HANDOFF_DELAY_MS = 2000ms,再终止旧子进程并启动试运行(#beginTrial#startTrial)。

2. 试运行必须越过激活门(activation gate)

文档给出了严格的就绪定义:试运行必须完成迁移、获取依赖、绑定 HTTP、把所有长驻根(long-running root)停在激活门(activation gate),之后才能上报prepared。仅靠"监听器已经起来"并不能证明运行时已具备提交条件——监听器可能在关键获取完成之前就开始接受流量。

prepared上报携带updateId(serviceProtocol.ts)。Launcher 收到后校验:角色必须是 trial、状态仍为 pending、updateId 匹配、目标版本匹配(serviceLauncher.ts)。一旦满足,launcher:

  1. 清除PREPARED_TIMEOUT_MS = 120_000ms的试运行超时定时器;
  2. 持久化提交activeVersion更新为目标版本、update.status置为committed,写盘;
  3. 丢弃数据库快照;
  4. 回复committed(携带 updateId);
  5. 此后子进程才允许释放激活门、接受命令、发布 ready。

顺序不可颠倒:先写盘提交,再放行子进程。试运行失败或超时(prepared-timeout)则回到#returnToPrevious,把状态置为rolled-back/failed并重启旧版本;提交之后目标版本成为权威版本,此后按服务管理器(systemd/launchd)的常规重启策略运行。

3. 状态写入的持久化方式

所有运行时状态转换都使用同目录替换(same-directory replacement):写入临时文件 →fsync文件 →rename覆盖 →fsync目录(serviceLauncher.ts)。syncDirectory对 Windows 做了兼容(NTFS 自行日志化 rename,目录 fsync 会以 EPERM 失败,被吞掉)。状态文件损坏或协议不匹配时,readServiceState直接抛错阻止启动,绝不猜测该启动哪个版本。

数据库回滚:SQLite 三文件快照

文档的关键点:旧子进程退出后,launcher 对 SQLite 的主文件、WAL、共享内存文件(shared-memory file)做快照,从而让试运行的迁移在没有 down migration的情况下也可逆。

实现要点(serviceLauncher.ts):

  • 三件套由DB_FILE_SUFFIXES = ["", "-wal", "-shm"]定义,备份到<baseDir>/runtime/db-backup/<updateId>/
  • 快照每个 update 只做一次backupDatabaseOnce若发现备份目录已存在则直接跳过,因为重启后的 launcher 可能面对的是同一试运行进程上次尝试留下的数据库写入,覆盖它可能把失败尝试的变更混进快照;
  • 快照先写入.staging临时目录、逐文件fsync后整体renamefsync父目录,保证快照原子可见;
  • 回滚前先写持久化恢复标记(restore marker).restore-pending,再覆盖三个文件并逐个 fsync——若恢复中途崩溃,重启后 launcher 在#recover里检测到标记就优先把恢复做完,任何版本启动前恢复都必须先收尾(对应 reasonrollback-interrupted);
  • 备份目录要保留到提交(commit)为止,或直到恢复与终态回滚记录都已持久化;提交成功后调用discardDatabaseBackup清理。

回滚的完整路径#returnToPrevious(serviceLauncher.ts):终止试运行 → 恢复数据库备份 → 持久化rolled-back/failed终态(activeVersion回到 fromVersion)→ 丢弃备份 → 启动旧版本为 active。

边界说明:附件(attachments)及其他 SQLite 之外的文件不在此回滚边界内。文档明确将快照语义限定为"让 trial migrations 可逆",而非全量文件系统回滚。

崩溃恢复矩阵(#recover)

每次 launcher 启动(包括更新中途崩溃后)都走#recover(serviceLauncher.ts):

启动时观察到的状态恢复动作
状态中无 pending 更新丢弃残留备份,直接以 activeVersion 启动
pending 且存在.restore-pending标记先完成恢复,置failed(reason:rollback-interrupted),回滚到旧版本
pending 且目标运行时缺失failed(reason:target-runtime-missing),回滚到旧版本
pending 且目标运行时完整重新进入试运行(#startTrial

启动时还会清除.service-stopping停止标记:该标记是 launcher 在显式停止前同步写入的,用于让子进程区分"服务要关停"与"launcher 即将启动我的替代者"这两种场景(serviceProtocol.ts);新 launcher 启动意味着服务重新运行,旧的停止标记必须作废。

客户端确认:update ID 而非"重连即成功"

一个被接受的更新仍是 pending。文档的提醒很关键:重连本身无法区分"替换成功"与"回滚"。因此:

  • 客户端把 launcher 的 update ID 与重连后的 ready 事件关联起来,再检查 outcome(committed / rolled-back / failed)与目标版本;
  • 老版本服务器没有 update ID,则保留仅按版本号关联(version-only correlation)的降级路径。

协议层面对此提供了完整支撑:ServiceLauncherChildMessage中的preparedServiceLauncherParentMessage中的update-accepted/committed/update-rejected全部携带updateId(serviceProtocol.ts)。子进程侧的ServiceLauncherClient(serviceLauncherClient.ts)在继承的 IPC 上做请求-应答交换,30 秒无响应判定timeoutprepareTrial仅接受committed且 updateId 与 pending 匹配的回复,否则视为不可能回复。

启动上下文通过环境变量T3_SERVICE_LAUNCHER_CONTEXT注入子进程(SERVICE_LAUNCHER_CONTEXT_ENV),decodeServiceLauncherContext会强校验childVersion与当前 package 版本一致,版本不符直接判定version-mismatch——保证子进程不会在错误的身份下启动。

拒绝原因即错误面

update-rejected携带的 reason 会原样透传:ServiceLauncherRejectedError的 message 就是 launcher 的拒绝理由(serviceLauncherClient.ts)。未安装后台服务时(capability 为null),远端更新会直接失败并提示:Remote updates require the T3 Code background service. Run t3 service install on the server machine.(见 selfUpdate.ts)。

桌面端(Desktop)更新:独立的两阶段交接

桌面更新与 boot-service 更新走完全不同的两阶段交接,原因很朴素:安装桌面应用会停掉它内置的 bundled backend,若后端先关机,唯一成功的 RPC 结果可能丢失。

实现位于 DesktopAppUpdate.ts 与 selfUpdate.ts 的withRunningThreadContinuation

  1. 准备阶段:后端(被桌面 app 启动、通过桌面遥测控制 FD 通信)调用 desktop app 的更新流程,等到ready-to-install状态上报后,返回desktopUpdateToken(即本次请求的requestId),此时连接仍然存活(DesktopAppUpdate.ts);
  2. 提交阶段:客户端只有在收到 token 之后才调用commitDesktopUpdate(requestId)提交该 token——否则后端关机可能恰好发生在"唯一的成功 RPC 结果"之前,导致结果丢失;
  3. 客户端随后必须在重连后观察 prepared 版本,确认安装生效;
  4. 若安装失败,桌面端会重启被停止的后端,并针对同一 token 回放失败(replay the failure for the same token)——token 以HashSet形式保存在desktopContinuationTokens中,未提交的 token 在失败时会重新加回集合,保证"失败的更新可重试且幂等"。

commitDesktopUpdate还通过handoffAccepted回调与不可中断块(Effect.uninterruptible)保证提交请求一旦发出就不会被中断取消;desktopUpdates订阅先于请求发出("Subscribe before sending the request so a fast first report cannot be missed"),防止快速上报被错过。

关键常量与配置速查

常量出处
SERVICE_LAUNCHER_PROTOCOL2(协议 2 支持试运行前快照 SQLite)serviceProtocol.ts
SERVICE_STATE_FILEservice-state.jsonserviceProtocol.ts
SERVICE_STOP_MARKER_FILE.service-stoppingserviceProtocol.ts
HANDOFF_DELAY_MS2_000serviceLauncher.ts
PREPARED_TIMEOUT_MS120_000serviceLauncher.ts
TERMINATE_GRACE_MS5_000(宽限后 SIGKILL)serviceLauncher.ts
IPC 应答超时30 秒serviceLauncherClient.ts
暂存预检超时30 秒selfUpdate.ts
运行时安装超时10 分钟pinnedRuntime.ts
数据库快照件数3(主文件、-wal-shmserviceLauncher.ts
恢复标记.restore-pendingserviceLauncher.ts

总结

T3 Code 的服务更新以"launcher 是状态唯一写入方"为不可动摇的根基:精确版本暂存 + 协议预检解决"能不能升";pending 先持久化再确认 + 试运行越过激活门才提交解决"何时算成功";SQLite 三文件一次性快照 + 持久恢复标记解决"失败怎么退";update ID 关联解决"客户端怎么确认"。而桌面端用 token 两阶段交接解决"安装即断连"的最后一公里。这套设计的每一处都可在上述源码文件中逐行验证,也是理解t3 service命令族与后台服务可靠性的最佳入口。

【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询