Files
crawler-plugin/frontend-vue/src/shared/health-check-runner.ts
T

122 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 健康检查执行器(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 }
}