Skip to content

Instantly share code, notes, and snippets.

@michael-fmg
Last active December 4, 2025 13:27
Show Gist options
  • Select an option

  • Save michael-fmg/14064513265b338c067dd85d04e503dc to your computer and use it in GitHub Desktop.

Select an option

Save michael-fmg/14064513265b338c067dd85d04e503dc to your computer and use it in GitHub Desktop.
import { getLogger } from '@feedfm/feed-resources';
import { eachPromise } from '@feedfm/feed-domain';
export interface DurableServerTask {
id: number;
startedAt: Date;
description: string;
details: any;
end: () => void;
}
const log = getLogger();
export const trappableSignals: Partial<Record<NodeJS.Signals, number>> = {
SIGINT: 2,
SIGTERM: 15,
};
export const reportOnlySignals: NodeJS.Signals[] = [
'SIGABRT', 'SIGALRM', 'SIGBREAK', 'SIGBUS', 'SIGCHLD', 'SIGCONT', 'SIGFPE', 'SIGHUP', 'SIGILL', 'SIGINFO',
'SIGIO', 'SIGIOT', 'SIGLOST', 'SIGPIPE', 'SIGPOLL', 'SIGPWR', 'SIGQUIT', 'SIGSEGV', 'SIGSTKFLT', 'SIGSYS',
'SIGTRAP', 'SIGTSTP', 'SIGTTIN', 'SIGTTOU', 'SIGUNUSED', 'SIGURG', 'SIGUSR1', 'SIGUSR2', 'SIGVTALRM',
'SIGWINCH', 'SIGXCPU', 'SIGXFSZ', 'SIGBREAK', 'SIGLOST', 'SIGINFO',
];
export class TerminationHandler {
checkRateSeconds = 0.5;
reportRateSeconds = 30;
nextId = 0;
tasks: DurableServerTask[];
shutDownHandlers: Array<() => Promise<unknown>>;
reportRateIterations: number;
shuttingDownSince: null | Date;
shuttingDownTaskCount: null | number;
constructor() {
this.tasks = [];
this.shuttingDownTaskCount = null;
this.shuttingDownSince = null;
this.shutDownHandlers = [];
this.reportRateIterations = this.reportRateSeconds / this.checkRateSeconds;
}
addShutdownHandler( handler: () => unknown ) {
const asyncWrapper = async () => handler();
this.shutDownHandlers.push( asyncWrapper );
}
startHandling() {
const signals = Object.keys( trappableSignals ) as NodeJS.Signals[];
signals.forEach( ( signal ) => process.on( signal, () => this.handleSignal( signal ) ) );
reportOnlySignals.forEach( ( signal ) => {
process.on( signal, () => log.debug( `(PID ${process.pid}/${process.ppid}) Received signal ${signal}` ) );
} );
}
beginDurableTask( description: string, details = {} ) {
const newTask: DurableServerTask = {
id: this.nextId++,
description,
details,
startedAt: new Date(),
end: () => this._endDurableTask( newTask ),
};
this.tasks.push( newTask );
log.info( `New durable task: ${this.formatTask( newTask )}` );
return newTask;
}
_endDurableTask( endingTask: DurableServerTask ) {
if ( this.shuttingDownSince === null ) {
log.info( `Ending durable task: ${this.formatTask( endingTask )}` );
} else {
log.warn( `Ending durable task: ${this.formatTask( endingTask )}` );
}
if ( !this.tasks.includes( endingTask ) ) {
log.warn(
`(PID ${process.pid}/${process.ppid}) Attempted to end the same task twice. This indicates a bug in the code beginning and ending the task. `,
);
}
this.tasks = this.tasks.filter( ( existingTask ) => existingTask !== endingTask );
}
async handleSignal( signal: NodeJS.Signals ) {
if ( this.shuttingDownSince ) {
log.warn(
`(PID ${process.pid}/${process.ppid}) Received ${signal} while already gracefully shutting down, so displaying shutdown status instead:`,
);
this.report();
} else {
log.warn( `(PID ${process.pid}/${process.ppid}) Received ${signal}. Beginning graceful termination.` );
this.shuttingDownSince = new Date();
this.shuttingDownTaskCount = this.tasks.length;
await eachPromise( this.shutDownHandlers );
await this.waitForTasks();
const signalValue = trappableSignals[signal]!;
process.exit( 128 + signalValue );
}
}
async waitForTasks() {
let iteration = 0;
if ( this.tasks.length > 0 ) {
while ( this.tasks.length > 0 ) {
if ( iteration % this.reportRateIterations === 0 ) {
this.report();
}
await new Promise( ( resolve ) => setTimeout( resolve, this.checkRateSeconds * 1000 ) );
iteration++;
}
}
this.report();
}
formatDate( date: Date ) {
return date.toLocaleDateString( undefined, {
day: '2-digit',
month: '2-digit',
year: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
} );
}
formatTask( task: DurableServerTask ) {
const {
description, details, id, startedAt,
} = task;
const taskSinceText = `started ${this.formatDate( startedAt )}`;
const detailsText = details ? JSON.stringify( details ) : '(no details)';
return `Task #${id}: ${description} (${taskSinceText}) ${detailsText}`;
}
report() {
const shuttingDownSince = this.formatDate( new Date() );
if ( this.tasks.length ) {
log.warn(
`[${shuttingDownSince}] Terminating: (PID ${process.pid}/${process.ppid}) waiting for tasks to complete, ${this.tasks.length}/${this.shuttingDownTaskCount} remaining. This message will repeat every ${this.reportRateSeconds} seconds.`,
);
this.tasks.forEach( ( task, index ) => {
const taskText = this.formatTask( task );
log.warn( `\t[${index + 1}/${this.tasks.length}] ${taskText}` );
} );
} else {
const deltaSeconds = Math.floor( ( Date.now() - this.shuttingDownSince!.getTime() ) / 100 ) / 10;
log.warn(
`[${shuttingDownSince}] Termination: (PID ${process.pid}/${process.ppid}) complete, ${this.shuttingDownTaskCount}/${this.shuttingDownTaskCount} tasks ended gracefully in ${deltaSeconds} seconds.`,
);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment