/** * 健康检查执行器(Task 98)。 * * 把真实启动、健康检查、核心请求、外部依赖调用四个验收探针收敛为 * 可注入探针的确定性流程:探针按序执行,全部产出结果与耗时。 * * 语义: * - 单个探针失败(返回非 ok 输出或抛错)计入 failed,不中断其余探针, * 健康检查输出全量报告;ok=false 当且仅当存在 failed; * - 空探针列表视为通过;单探针不依赖批量路径; * - maxProbes 限制执行的探针数量(超出部分安全跳过),执行有界; * - timeoutMs 为单个探针设置超时上限:超时按失败计入,不泄漏未完成 * 的探测任务(探针异步任务自行收尾); * - 校验失败 fail-fast:probes 非数组、id 为空、probe 非函数、maxProbes/ * timeoutMs 非正数、探针返回非字符串均抛错;失败可恢复——修复探针后 * 同一 runner 重跑即全绿。 */ export interface HealthProbe { id: string label: string probe: () => Promise | string } export interface HealthCheckOptions { probes: HealthProbe[] /** 执行的探针数量上限,必须为正数,默认 100 */ maxProbes?: number /** 单个探针超时(毫秒),必须为正数,默认 30_000 */ timeoutMs?: number } export interface ProbeResult { id: string label: string ok: boolean output: string durationMs: number } export interface HealthCheckResult { ok: boolean passed: ProbeResult[] failed: ProbeResult[] totalDurationMs: number } export interface HealthCheckRunner { runAll: () => Promise } export function createHealthCheckRunner(options: HealthCheckOptions): HealthCheckRunner { if (!Array.isArray(options.probes)) { throw new Error('probes 必须是数组') } for (const probe of options.probes) { if (typeof probe.id !== 'string' || probe.id.length === 0) { throw new Error('probe id 不能为空') } if (typeof probe.probe !== 'function') { throw new Error('probe 必须是函数') } } const maxProbes = options.maxProbes ?? 100 const timeoutMs = options.timeoutMs ?? 30_000 if (!(maxProbes > 0)) { throw new Error('maxProbes 必须为正数: ' + maxProbes) } if (!(timeoutMs > 0)) { throw new Error('timeoutMs 必须为正数: ' + timeoutMs) } async function runOne(probe: HealthProbe): Promise { const start = Date.now() let output = '' let failed = false let timer: ReturnType | null = null try { const raw = await Promise.race([ Promise.resolve(probe.probe()), new Promise((resolve) => { timer = setTimeout(() => resolve(''), timeoutMs) }), ]) if (timer != null) clearTimeout(timer) output = raw } catch (error) { if (timer != null) clearTimeout(timer) failed = true output = error instanceof Error ? error.message : String(error) } const durationMs = Date.now() - start if (typeof output !== 'string') { throw new Error('output 必须是字符串') } const ok = !failed && output !== '' return { id: probe.id, label: probe.label, ok, output, durationMs } } async function runAll(): Promise { const start = Date.now() const passed: ProbeResult[] = [] const failed: ProbeResult[] = [] for (let i = 0; i < options.probes.length; i++) { if (i >= maxProbes) break const result = await runOne(options.probes[i]) if (result.ok) { passed.push(result) } else { failed.push(result) } } return { ok: failed.length === 0, passed, failed, totalDurationMs: Date.now() - start, } } return { runAll } }