feat(ops): expose liveness and readiness probes
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { checkReadiness } from "./readiness";
|
||||
|
||||
describe("readiness checks", () => {
|
||||
it("reports ready only when every dependency succeeds", async () => {
|
||||
await expect(
|
||||
checkReadiness({ database: async () => true, redis: async () => "PONG" }),
|
||||
).resolves.toEqual({
|
||||
status: "ready",
|
||||
checks: { database: "ok", redis: "ok" },
|
||||
});
|
||||
|
||||
await expect(
|
||||
checkReadiness({
|
||||
database: async () => true,
|
||||
redis: async () => {
|
||||
throw new Error("offline");
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
status: "not-ready",
|
||||
checks: { database: "ok", redis: "failed" },
|
||||
});
|
||||
});
|
||||
|
||||
it("bounds a stalled dependency", async () => {
|
||||
vi.useFakeTimers();
|
||||
const result = checkReadiness(
|
||||
{ database: () => new Promise(() => undefined) },
|
||||
100,
|
||||
);
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
await expect(result).resolves.toEqual({
|
||||
status: "not-ready",
|
||||
checks: { database: "failed" },
|
||||
});
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
export type ReadinessStatus = "ready" | "not-ready";
|
||||
|
||||
export interface ReadinessResult {
|
||||
status: ReadinessStatus;
|
||||
checks: Record<string, "ok" | "failed">;
|
||||
}
|
||||
|
||||
async function withinTimeout(
|
||||
check: () => Promise<unknown>,
|
||||
timeoutMs: number,
|
||||
): Promise<void> {
|
||||
let timeout: ReturnType<typeof setTimeout> | undefined;
|
||||
try {
|
||||
await Promise.race([
|
||||
check().then(() => undefined),
|
||||
new Promise<never>((_, reject) => {
|
||||
timeout = setTimeout(
|
||||
() => reject(new Error("Dependency check timed out.")),
|
||||
timeoutMs,
|
||||
);
|
||||
}),
|
||||
]);
|
||||
} finally {
|
||||
if (timeout) clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkReadiness(
|
||||
checks: Record<string, () => Promise<unknown>>,
|
||||
timeoutMs = 2_500,
|
||||
): Promise<ReadinessResult> {
|
||||
const boundedTimeout = Math.max(100, Math.min(10_000, timeoutMs));
|
||||
const entries = await Promise.all(
|
||||
Object.entries(checks).map(async ([name, check]) => {
|
||||
try {
|
||||
await withinTimeout(check, boundedTimeout);
|
||||
return [name, "ok"] as const;
|
||||
} catch {
|
||||
return [name, "failed"] as const;
|
||||
}
|
||||
}),
|
||||
);
|
||||
const result = Object.fromEntries(entries);
|
||||
return {
|
||||
status: Object.values(result).every((value) => value === "ok")
|
||||
? "ready"
|
||||
: "not-ready",
|
||||
checks: result,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user