task-98: 真实启动、健康检查、核心请求和外部依赖调用验证执行器

This commit is contained in:
2026-08-30 23:15:31 +08:00
parent 6e9a9f7347
commit 38e94e1f53
2 changed files with 260 additions and 0 deletions
@@ -0,0 +1,121 @@
/**
* 健康检查执行器(Task 98)。
*
* 把真实启动、健康检查、核心请求、外部依赖调用四个验收探针收敛为
* 可注入探针的确定性流程:探针按序执行,全部产出结果与耗时。
*
* 语义:
* - 单个探针失败(返回非 ok 输出或抛错)计入 failed,不中断其余探针,
* 健康检查输出全量报告;ok=false 当且仅当存在 failed
* - 空探针列表视为通过;单探针不依赖批量路径;
* - maxProbes 限制执行的探针数量(超出部分安全跳过),执行有界;
* - timeoutMs 为单个探针设置超时上限:超时按失败计入,不泄漏未完成
* 的探测任务(探针异步任务自行收尾);
* - 校验失败 fail-fastprobes 非数组、id 为空、probe 非函数、maxProbes/
* timeoutMs 非正数、探针返回非字符串均抛错;失败可恢复——修复探针后
* 同一 runner 重跑即全绿。
*/
export interface HealthProbe {
id: string
label: string
probe: () => Promise<string> | 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<HealthCheckResult>
}
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<ProbeResult> {
const start = Date.now()
let output = ''
let failed = false
let timer: ReturnType<typeof setTimeout> | null = null
try {
const raw = await Promise.race([
Promise.resolve(probe.probe()),
new Promise<string>((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<HealthCheckResult> {
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 }
}