UNPKG

@axway/api-builder-runtime

Version:

API Builder Runtime

188 lines (164 loc) 5.94 kB
// This class handles keeping track of global API Builder stuff and process events // There's still a lot around the codebase, and we want to eventually reduce as // much of this as possible, but this can be independently tested right now. const { allSettled } = require('./utils'); class Globals { constructor() { this.state = { servers: [], stopping: false, hasBoundProcessHandlers: false }; // Since we're passing these instance methods directly to this._onStop = this._onStop.bind(this); this._onExit = this._onExit.bind(this); this._onRestart = this._onRestart.bind(this); this._onAbort = this._onAbort.bind(this); this._onUncaughtException = this._onUncaughtException.bind(this); this._onUnhandledRejection = this._onUnhandledRejection.bind(this); } // Reset the state resetState() { this.state.servers = []; this.state.stopping = false; this._unbindListeners(); } // Add a server to track and optionally bind listeners addServer(server, { bind }) { this.state.servers.push(server); if (bind) { this._bindListeners(); } } // Remove a server from being tracked globally. When there are no servers left // we will unbind all global event listeners if they are set. removeServer(server) { for (let i = 0; i < this.state.servers.length; i++) { if (this.state.servers[i] === server) { this.state.servers.splice(i, 1); break; } } if (this.state.servers.length === 0) { this._unbindListeners(); } } // Stop a running builder server (promisified for async) // This function specifically doesn't care about errors so always resolve. _stopServer(server) { return new Promise(resolve => { server.stop(resolve); }); } // Reload a running builder server (promisified for async) // This function specifically doesn't care about errors so always resolve. _reloadServer(server) { return new Promise(resolve => { server.reload(resolve); }); } // Shutdown all servers and then exit. // Even though this is async, the promise should never reject. async _shutdownAllServers() { if (this.state.stopping) { return; } this.state.stopping = true; // TODO: use Promise.allSettled when our minimum node version is > 12.9.0. await allSettled(this.state.servers.map(this._stopServer)); this.state.stopping = false; } // Restart all servers by first stopping and then restarting them. // Delegates the actual execution to `reload` function. async _reloadAllServers() { if (this.state.stopping) { return; } // TODO: use Promise.allSettled when our minimum node version is > 12.9.0. return allSettled(this.state.servers.map(this._reloadServer)); } // Called on stop signals. This will intentionally handle shutdown and // kill the process. async _onStop() { await this._shutdownAllServers(); process.exit(0); } // Called on exit event. This will set the response code but won't kill the // process. This will let other exit events fire and potentially set a different // exit code. // Note: while this is async and shuts down the servers, there's no way to reliably // wait for async events in an exit listener, so the process will exit as soon // as the event loop is clear. async _onExit(exitCode) { process.exitCode = exitCode; await this._shutdownAllServers(); } // Called on SIGUSR2 signal. This reloads all servers. async _onRestart() { // eslint-disable-next-line no-console console.log('signal received SIGUSR2 restarting'); await this._reloadAllServers(); } // Called on abort signal. This will handle shutdown then abort the process. async _onAbort() { await this._shutdownAllServers(); process.abort(); } // Called on any unhandled exceptions. async _onUncaughtException(error) { // eslint-disable-next-line no-console console.error('Uncaught Exception', error); await this._shutdownAllServers(); process.exitCode = 1; } // Called on any unhandled rejections. async _onUnhandledRejection(error) { // eslint-disable-next-line no-console console.error('Unhandled Promise Rejection', error); await this._shutdownAllServers(); process.exitCode = 1; } // Binds all events to the process. Will not bind // if there are already events bound. _bindListeners() { // Only bind listeners once if (this.state.hasBoundProcessHandlers) { return; } // normal shutdown type signals // for now, we're going to ignore SIGHUP since that can be sent on terminal // disconnect or backgrounding for (const signal of [ 'SIGINT', 'SIGTERM', 'SIGQUIT' ]) { process.on(signal, this._onStop); } // the arguments passed to `on` depend on the signal. on `exit`, `code` will // be an error code (int). otherwise, the `code` will be the signal name. process.on('exit', this._onExit); // restart if we receive the SIGUSR2 signal process.on('SIGUSR2', this._onRestart); // on SIGABRT we are going to send an abort which should core on *nix platforms process.on('SIGABRT', this._onAbort); // monitor any unhandled exceptions process.on('uncaughtException', this._onUncaughtException); process.on('unhandledRejection', this._onUnhandledRejection); this.state.hasBoundProcessHandlers = true; } // Unbind events from the process if there are any bound. _unbindListeners() { if (!this.state.hasBoundProcessHandlers) { return; } // TODO: use process.off instead of process.removeListener when our // minimum is > node 10 for (const signal of [ 'SIGINT', 'SIGTERM', 'SIGQUIT' ]) { process.removeListener(signal, this._onStop); } process.removeListener('exit', this._onExit); process.removeListener('SIGUSR2', this._onRestart); process.removeListener('SIGABRT', this._onAbort); process.removeListener('uncaughtException', this._onUncaughtException); process.removeListener('unhandledRejection', this._onUnhandledRejection); this.state.hasBoundProcessHandlers = false; } } module.exports = Globals;