@axway/api-builder-runtime
Version:
API Builder Runtime
188 lines (164 loc) • 5.94 kB
JavaScript
// 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;