122 lines
3.7 KiB
TypeScript
122 lines
3.7 KiB
TypeScript
/**
|
||
* 健康检查执行器(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> | 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 }
|
||
}
|