93 lines
No EOL
2.5 KiB
TypeScript
93 lines
No EOL
2.5 KiB
TypeScript
import { getLogger } from '@stock-bot/logger';
|
|
import { SHUTDOWN_DEFAULTS } from './constants';
|
|
import type { ShutdownCallback, ShutdownOptions } from './types';
|
|
|
|
interface CallbackEntry {
|
|
callback: ShutdownCallback;
|
|
priority: number;
|
|
name?: string;
|
|
}
|
|
|
|
export class Shutdown {
|
|
private static instance: Shutdown | null = null;
|
|
private readonly logger = getLogger('shutdown');
|
|
private isShuttingDown = false;
|
|
private shutdownTimeout: number;
|
|
private callbacks: CallbackEntry[] = [];
|
|
private signalHandlersRegistered = false;
|
|
|
|
constructor(options: ShutdownOptions = {}) {
|
|
this.shutdownTimeout = options.timeout || SHUTDOWN_DEFAULTS.TIMEOUT;
|
|
if (options.autoRegister !== false) {
|
|
this.setupSignalHandlers();
|
|
}
|
|
}
|
|
|
|
static getInstance(options?: ShutdownOptions): Shutdown {
|
|
if (!Shutdown.instance) {
|
|
Shutdown.instance = new Shutdown(options);
|
|
}
|
|
return Shutdown.instance;
|
|
}
|
|
|
|
/**
|
|
* Register a cleanup callback
|
|
*/
|
|
onShutdown(callback: ShutdownCallback, priority: number = SHUTDOWN_DEFAULTS.MEDIUM_PRIORITY, name?: string): void {
|
|
if (this.isShuttingDown) { return };
|
|
this.callbacks.push({ callback, priority, name });
|
|
}
|
|
|
|
/**
|
|
* Initiate graceful shutdown
|
|
*/
|
|
async shutdown(): Promise<void> {
|
|
if (this.isShuttingDown) { return };
|
|
|
|
this.isShuttingDown = true;
|
|
|
|
const timeout = new Promise<never>((_, reject) =>
|
|
setTimeout(() => reject(new Error('Shutdown timeout')), this.shutdownTimeout)
|
|
);
|
|
|
|
try {
|
|
await Promise.race([this.executeCallbacks(), timeout]);
|
|
} catch (error) {
|
|
this.logger.error('Shutdown failed', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
private async executeCallbacks(): Promise<void> {
|
|
const sorted = [...this.callbacks].sort((a, b) => a.priority - b.priority);
|
|
|
|
for (const { callback, name } of sorted) {
|
|
try {
|
|
await callback();
|
|
} catch (error) {
|
|
this.logger.error(`Shutdown callback failed: ${name || 'unnamed'}`, error);
|
|
}
|
|
}
|
|
}
|
|
|
|
private setupSignalHandlers(): void {
|
|
if (this.signalHandlersRegistered) { return };
|
|
|
|
const signals: NodeJS.Signals[] = ['SIGTERM', 'SIGINT'];
|
|
|
|
signals.forEach(signal => {
|
|
process.once(signal, async () => {
|
|
if (!this.isShuttingDown) {
|
|
try {
|
|
await this.shutdown();
|
|
process.exit(0);
|
|
} catch {
|
|
process.exit(1);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
this.signalHandlersRegistered = true;
|
|
}
|
|
} |