UNPKG

@travetto/runtime

Version:

Runtime for travetto applications.

105 lines (85 loc) 3.52 kB
import type { ChildProcess } from 'node:child_process'; import { Env } from './env.ts'; import { Util } from './util.ts'; import { TimeUtil } from './time.ts'; const MAPPING = [['restart', 200], ['error', 1], ['quit', 0]] as const; export type ShutdownReason = typeof MAPPING[number][0]; const REASON_TO_CODE = new Map<ShutdownReason, number>(MAPPING); const CODE_TO_REASON = new Map<number, ShutdownReason>(MAPPING.map(([k, v]) => [v, k])); type Handler = (event: Event) => unknown; type ShutdownEvent = { reason?: ShutdownReason, mode?: 'exit' | 'interrupt' }; const isShutdownEvent = (event: unknown): event is ShutdownEvent => typeof event === 'object' && event !== null && 'type' in event && event.type === 'shutdown'; const wrapped = async (handler: Handler): Promise<void> => { try { await handler(new Event('abort')); } catch (err) { console.error('Error during shutdown handler', err); } }; /** * Shutdown manager, allowing for listening for graceful shutdowns */ export class ShutdownManager { static #shouldIgnoreInterrupt = false; static #registered = new Set<Handler>(); static #controller = new AbortController(); static { this.#controller.signal.addEventListener = (_: 'abort', listener: Handler): void => { this.#registered.add(listener); }; this.#controller.signal.removeEventListener = (_: 'abort', listener: Handler): void => { this.#registered.delete(listener); }; try { process .on('message', event => { isShutdownEvent(event) && this.shutdown(event); }) .on('SIGINT', () => this.shutdown({ mode: 'interrupt' })) .on('SIGTERM', () => this.shutdown()); } catch { } } static get signal(): AbortSignal { return this.#controller.signal; } /** Disable SIGINT interrupt handling */ static disableInterrupt(): typeof ShutdownManager { this.#shouldIgnoreInterrupt = true; return this; } /** Convert exit code to a reason string */ static reasonForExitCode(code: number): ShutdownReason { return CODE_TO_REASON.get(code) ?? 'error'; } /** Trigger a watch signal signal to a subprocess */ static async shutdownChild(subprocess: ChildProcess, config?: ShutdownEvent): Promise<void> { subprocess?.send!({ type: 'shutdown', ...config }); } /** * Shutdown the application gracefully */ static async shutdown({ reason = 'quit', mode = undefined }: ShutdownEvent = {}): Promise<void> { if ((mode === 'interrupt' && this.#shouldIgnoreInterrupt) || this.#controller.signal.aborted) { return; } process // Allow shutdown if anything is still listening .removeAllListeners('message') .removeAllListeners('SIGINT') .removeAllListeners('SIGTERM'); if (mode === 'interrupt' && process.stdout.isTTY) { process.stdout.write('\n'); } process.exitCode ??= REASON_TO_CODE.get(reason); const timeout = TimeUtil.fromValue(Env.TRV_SHUTDOWN_WAIT.value) ?? 2000; const context = { reason, mode, pid: process.pid, timeout, pending: this.#registered.size }; this.#controller.abort('Shutdown started'); console.debug('Shutdown started', context); const winner = await Promise.race([ Util.nonBlockingTimeout(timeout).then(() => this), Promise.all([...this.#registered].map(wrapped)) ]); if (winner !== this) { console.debug('Shutdown completed', context); } else { console.warn('Shutdown timed out', context); } if (mode === 'exit') { process.exit(); } } }