/** * [ref] A10:composition root 分段 —— 进程收尾(hardShutdown / drain / 三个信号处理器)。 * * 纯搬运:函数体逐字来自 `main.ts`(原 4385-4488 行),缩进不变;新增的只有 import 与 * `installShutdownHandlers(ctx)` 包壳。 * * ⚠️ **位置即契约**(搬运不得改次序): * 1. 本段必须在 `server.listen()` **之后**调用 —— `process.on("SIGTERM"|"SIGINT"|"SIGHUP")` 的注册 * 时点即"从此刻起信号可被优雅处理";提前注册会让一个尚未 listen 的进程走 drain 分支。 * 2. 三个 `process.on` 的**相对次序**保持 SIGTERM → SIGINT → SIGHUP(与原文件逐字一致); * 它们互不覆盖,但 handler 之间共享 `closing`/`draining` 两个 latch,次序变化会改变 * "第二个信号"的语义读数。 * 3. `clearInterval(reaper)` 必须是 hardShutdown 的第一件事(在 `server.close()` 之前),否则 * 收尾期还会有 reaper tick 打向正在关闭的池。 * 4. **进程级监听一律成对**([ref] 二轮件4):本函数装的五只(三信号 + [ref]① 双钩)只能经返回的 * {@link ShutdownHandle} 摘 —— 调用方手写「摘三信号」的收尾会把双钩漏在进程上,而陈旧的那只 * rejection 钩仍握着 `process.exit(1)`,会对后续无关的 rejection 把进程打掉。 */ import { Runner, type RunnerDeps } from "@sema-agent/core"; import type { ServiceConfig } from "../config.js"; import type { createHttpServer } from "../http/server.js"; import type { RetentionLaneController } from "./retention-lane.js"; import type { Logger } from "../observability/logger.js"; import type { startOtlpExporter } from "../observability/otel-exporter.js"; import type { CostQuota } from "../observability/cost-quota.js"; import type { RateLimiter } from "../observability/rate-limit.js"; import type { startFleetClientFromEnv } from "../fleet-client.js"; import type { BreakerStateStore, CostQuotaStore, RateLimiterStore, StoreBackend } from "../plugins/store-backend.js"; import type { WorkflowNotifyJournalStore } from "../orchestration/workflow-notify-journal.js"; export interface ShutdownCtx { config: ServiceConfig; /** [ref]①:崩溃遗言的 version 键(与 /v1/capabilities.version 同源=main 的 serviceVersion())。 */ version: string; logger: Logger; server: ReturnType; reaper: NodeJS.Timeout; /** [ref]:fleet 存活对账腿(60s 定时器 + 总线订阅)。与 `reaper` 同列文件头契约 3 —— 收尾期不该再有 * 对账 tick 打向正在关闭的池,订阅也要摘(否则 bus 的扇出还在往一个已停的观察者写)。 */ fleetReconcile: { timer: NodeJS.Timeout; reconciler: { stop(): void; }; }; /** [ref] 车2:托管留存 sweep lane 的**控制器**(`undefined` = lane 关着,压根没建定时器)。与 `reaper` * 同列文件头契约 3,但严格更强:那些腿只要停表就够了,本腿的 tick 打的是跨十余张表的**破坏性**事务 * ⇒ 必须 `await stop()` 等在飞那一轮真正落地,**然后**才让租、才关连接池(见 hardShutdown 内的三步注)。 */ retentionLane: RetentionLaneController | undefined; /** [ref] 车2:主动让租(把 sweep 租约的到期时间归零)—— 优雅停机后另一副本不必等满一个 TTL 才接手。 * best-effort:一次失败只意味着"下一位等 TTL",绝不阻塞停机。缺席 = lane 关着。 */ releaseRetentionLease: (() => Promise) | undefined; otelExporter: ReturnType | undefined; breakerState: ReturnType | undefined; costQuota: CostQuota | CostQuotaStore | undefined; rateLimiter: RateLimiter | RateLimiterStore | undefined; runner: Runner; subRunner: Runner; lspManager: RunnerDeps["lspManager"]; workflowNotifyJournal: WorkflowNotifyJournalStore | undefined; fleetClient: ReturnType; backend: StoreBackend | undefined; drainState: { draining: boolean; since?: number; inflight?: () => number; lastActivityAt?: () => number; attachedStreams?: () => number; lastAttachEndedAt?: () => number; }; /** [ref]-2:store 活体探针(main 建)——不停的话 hardShutdown 后仍对正在关闭的池发探针,刷假 * store_probe_dead 告警(文件头契约 3 的同族漏网)。缺席 = 该部署形没建探针。 */ storeLiveProbe: { stop(): void; } | undefined; /** * device lane 的 WS 汇聚端(§8-R7 发版排空,车A-4)。缺席 = 本部署没有 device 车道。 * * 🔴 排空联动**必须**由装配层驱动(`http/server.ts` 的 `deviceHub` 键头注逐字):§8-R7 的顺序 * (新提交 503 → 新 upgrade 拒 → 停发新指令 → 在途结果照收 → 才断连)是**部署编排**的语义, * HTTP 层不能自作主张。所以 `drainState.draining` 翻真的那一刻,这里同步调 `beginDrain()`。 */ deviceHub: import("../device-ws-hub.js").DeviceWsHub | undefined; /** [ref]-2:config 60s 刷新环(config-center startRefreshLoop)——不停的话停机中途还可能热应用 * 一份新配置。缺席 = 纯 env 部署没起环。 */ configCenter: { stopRefreshLoop(): void; } | undefined; } /** * `installShutdownHandlers` 的**摘除把手**([ref] 二轮重扫件4)。 * * 存在理由:本函数装的是**进程级**监听(三信号 + [ref]① 双钩),而进程级注册表是全局单例 —— 一个进程里 * 装两次就是两套 handler 同时在场。生产进程只装一次、装到死,所以这把手服务的是**同进程内反复装配**的 * 那一族调用方(测试):此前它们各自手写「只摘三信号」的收尾,双钩每次净泄漏两只,而陈旧 handler 会对 * **后续无关**的 rejection 执行 `process.exit(1)`。注册/摘除配对因此必须由装配方给,不许调用方各猜一份。 */ export interface ShutdownHandle { /** 摘除**本次调用**注册的全部进程级监听(三信号 + 双钩)并停掉本次装的 parent 监视腿。幂等。 */ dispose(): void; } /** 注册 SIGTERM/SIGINT/SIGHUP 收尾链。**必须在 listen 之后调用**(见文件头「位置即契约」)。 * 返回 {@link ShutdownHandle} —— 同进程内反复装配的调用方(测试)必须用它收尾。 */ export declare function installShutdownHandlers(ctx: ShutdownCtx): ShutdownHandle; //# sourceMappingURL=shutdown.d.ts.map