-
-
Save michael-fmg/14064513265b338c067dd85d04e503dc to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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