Files
health/node-health/src/readiness.ts

252 lines
7.2 KiB
TypeScript

/** Return type of a readiness check function */
export type ReadinessFunctionReturn = {
/** Status of the readiness check */
status: ReadinessStatus;
/** Optional message providing additional information about the readiness check */
message?: string;
};
/** Function that performs a readiness check */
export type ReadinessFunction = (check: ReadinessCheck) => Promise<ReadinessFunctionReturn>;
/** Status of a readiness check */
export type ReadinessStatus = 'ok' | 'error' | 'degraded';
const aggregateStatus = (statuses: ReadinessStatus[]): ReadinessStatus => {
if (statuses.includes('error')) return 'error';
if (statuses.includes('degraded')) return 'degraded';
return 'ok';
};
/** Represents a readiness check with an optional timeout */
export type ReadinessCheck = {
/** Name of the readiness check */
name: string;
/** Function that performs the readiness check */
fn: ReadinessFunction;
/** Timeout in milliseconds for the readiness check (default: 5000) */
timeout?: number;
};
/** Result of a system readiness check */
export type ReadinessResult = {
/**
* Status of the system readiness check, aggregated as the worst status
* among individual checks. 'unknown' is a special case indicating that
* no checks were performed, used by ScheduledReadiness before the first run.
* */
status: ReadinessStatus | 'unknown';
/** Start time of the system readiness check in milliseconds since the Unix epoch */
start: number;
/** Duration of the system readiness check in milliseconds */
duration: number;
/** Details of individual readiness checks */
details: ReadinessDetail[];
};
/** Detail of an individual readiness check */
export type ReadinessDetail = {
/** Name of the readiness check */
name: string;
/** Status of the readiness check */
status: ReadinessStatus;
/** Duration of the readiness check in milliseconds */
duration: number;
/** Message providing additional information about the readiness check */
message?: string;
};
/**
* Performs a readiness check by executing the provided readiness functions.
* @param checks - An array of readiness functions to execute.
* @returns A Promise that resolves to a ReadinessResult object.
*/
export const readiness = async (checks: ReadinessCheck[]): Promise<ReadinessResult> => {
const start = Date.now();
const t0 = performance.now();
const details: ReadinessDetail[] = [];
for (const check of checks) {
const checkt0 = performance.now();
try {
const result = await withTimeout(
check.fn(check),
check.timeout ?? 5000,
`Readiness check '${check.name}' timed out after ${check.timeout ?? 5000} ms`,
);
details.push({
name: check.name,
status: result.status,
message: result.message,
duration: performance.now() - checkt0,
});
} catch (err) {
details.push({
name: check.name,
status: 'error',
message: err instanceof Error ? err.message : String(err),
duration: performance.now() - checkt0,
});
}
}
const duration = performance.now() - t0;
return {
status: aggregateStatus(details.map((d) => d.status)),
start,
duration,
details,
};
};
/**
* Creates a handler function for readiness HTTP requests. Warning: this runs all
* checks on each request and may be slow.
* @param checks - An array of readiness functions to execute.
* @returns A function that returns a Response object with ReadinessResult in JSON format.
*/
export const createReadinessHandler = (checks: ReadinessCheck[]): (() => Promise<Response>) => {
return async () => {
const result = await readiness(checks);
return respondWithResult(result);
};
};
/**
* Class that schedules periodic readiness checks.
*/
export class ScheduledReadiness {
private checks: ReadinessCheck[];
private interval: number;
private started: boolean = false;
private timer: NodeJS.Timeout | null = null;
private latestResult: ReadinessResult | null = null;
private nextResult: Promise<ReadinessResult> | null = null;
/**
* Creates an instance of ScheduledReadiness.
* @param checks - An array of readiness functions to execute.
* @param interval - Interval in milliseconds between readiness checks.
*/
constructor(checks: ReadinessCheck[], interval: number) {
this.checks = checks;
this.interval = interval;
}
/** Starts the scheduled readiness checks */
async start() {
if (this.started) return; // Already started
this.started = true;
const runCheck = async () => {
// Prevent concurrent runs
if (this.nextResult) return;
this.nextResult = readiness(this.checks);
try {
this.latestResult = await this.nextResult;
} finally {
this.nextResult = null;
}
};
await runCheck(); // Initial run
this.timer = setInterval(runCheck, this.interval);
}
/** Stops the scheduled readiness checks */
stop() {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
this.started = false;
}
/** Gets the next readiness result, waiting if a check is in progress */
async getNextResult(): Promise<ReadinessResult | null> {
if (this.nextResult) {
return await this.nextResult;
}
return this.latestResult;
}
/** Gets the latest readiness result without waiting */
getResult(): ReadinessResult | null {
return this.latestResult;
}
/**
* Sets the interval for readiness checks and restarts the timer if already started.
* @param ms - Interval in milliseconds.
*/
setInterval(ms: number) {
this.interval = ms;
if (this.timer) {
this.stop();
this.start();
}
}
/**
* Creates a handler function for readiness HTTP requests using the latest scheduled result.
* Scheduled handler always returns the most recent result, or 'unknown' if no checks have run yet.
* @returns A function that returns a Response object with ReadinessResult in JSON format.
*/
createHandler(): () => Promise<Response> {
return async () => {
const result = await this.getNextResult();
if (!result) {
return new Response(
JSON.stringify({
status: 'unknown',
start: Date.now(),
duration: 0,
details: [],
} as ReadinessResult),
{
status: httpStatusFromReadiness('unknown'),
headers: { 'Content-Type': 'application/json' },
},
);
}
return respondWithResult(result);
};
}
}
/** Returns a Response object with the given ReadinessResult in JSON format */
const respondWithResult = (result: ReadinessResult) => {
return new Response(JSON.stringify(result), {
status: httpStatusFromReadiness(result.status),
headers: { 'Content-Type': 'application/json' },
});
};
/** Returns the HTTP status code corresponding to a given readiness status */
const httpStatusFromReadiness = (status: ReadinessStatus | 'unknown'): number => {
if (status === 'ok') return 200;
if (status === 'degraded') return 200; // 206 could also be suitable, but let's avoid false alarms
if (status === 'error') return 503;
return 200; // unknown, treat as ok to avoid false alarms
};
const withTimeout = async <T>(
promise: Promise<T>,
ms: number,
timeoutMessage: string,
): Promise<T> => {
let timeoutHandle: NodeJS.Timeout;
const timeoutPromise = new Promise<never>((_, reject) => {
timeoutHandle = setTimeout(() => {
reject(new Error(timeoutMessage));
}, ms);
});
return Promise.race([promise, timeoutPromise]).finally(() => {
clearTimeout(timeoutHandle);
});
};