webserver-framework/src/controller/Shard.js
2022-11-09 11:20:06 +02:00

144 lines
4.9 KiB
JavaScript

const ChildProcess = require('node:child_process');
const { EventEmitter } = require('node:events');
const { Util } = require('../util');
class Shard extends EventEmitter {
constructor (controller, id, options = {}) {
super();
this.controller = controller;
if (typeof id !== 'number' || isNaN(id)) throw new Error('Missing ID');
this.id = id;
if (!options.path) throw new Error('Missing path to file to fork');
this.filePath = options.path;
this.args = options.args || [];
this.execArgv = options.execArgv || [];
this.env = options.env || {};
this._respawn = options.respawn || false;
this.serverOptions = options.serverOptions || {};
this.ready = false;
this.process = null;
this.fatal = false;
}
async spawn (waitForReady = false) {
if (this.fatal) throw new Error(`[shard-${this.id}] Process died fatally and cannot be restarted. Fix the issue before trying again.`);
if (this.process) throw new Error(`[shard-${this.id}] A process for this shard already exists!`);
this.process = ChildProcess.fork(this.filePath, this.args, { env: { ...this.env, SHARD_ID: this.id }, execArgv: this.execArgv })
.on('message', this._handleMessage.bind(this))
.on('exit', () => this._handleExit())
.on('disconnect', this._handleDisconnect.bind(this)); // Don't know if this is going to help, but monitoring whether this gets called whenever a process on its own closes the IPC channel
this.process.once('spawn', () => {
this.emit('spawn');
this.process.send({ _start: this.serverOptions });
});
if (!waitForReady) return;
return new Promise((resolve, reject) => {
this.once('ready', resolve);
this.once('disconnect', () => reject(new Error(`[shard-${this.id}] Shard disconnected while starting up`)));
this.once('death', () => reject(new Error(`[shard-${this.id}] Shard died while starting`)));
setTimeout(() => reject(new Error(`[shard-${this.id}] Shard timed out while starting`)), 30_000);
});
}
async respawn (delay = 500) {
await this.kill();
if (delay) await Util.wait(delay);
return this.spawn();
}
/**
* Sends a shutdown command to the shard, if it doesn't respond within 5 seconds it gets killed
* TODO: Add a check to see if the process actually ends and print out a warning if it hasn't
*
* @return {*}
* @memberof Shard
*/
kill () {
if (this.process) {
return new Promise((resolve) => {
// Clear out all other exit listeners so they don't accidentally start the process up again
this.process.removeAllListeners('exit');
// Set timeout for force kill
const to = setTimeout(() => {
this.process.kill();
resolve();
}, 5000);
// Gracefully handle exit
this.process.once('exit', () => {
clearTimeout(to);
this._handleExit(false);
resolve();
});
// Clear the force kill timeout if the process responds with a shutdown echo, allowing it time to gracefully close all connections
this.once('shutdown', () => {
clearTimeout(to);
});
this.process.send({ _shutdown: true });
});
}
this._handleExit(false);
}
send (message) {
return new Promise((resolve, reject) => {
if (this.ready && this.process) {
this.process.send(message, err => {
if (err) reject(err);
else resolve();
});
} else reject(new Error(`[shard-${this.id}] Cannot send message to dead shard.`));
});
}
_handleMessage (message) {
if (message) {
if (message._ready) {
this.ready = true;
this.emit('ready');
return;
} else if (message._shutdown) {
this.ready = false;
this.emit('shutdown');
return;
} else if (message._fatal) {
this.process.removeAllListeners();
this.ready = false;
this.fatal = true;
this._handleExit(false);
this.emit('fatal');
}
}
this.emit('message', message);
}
_handleDisconnect () {
this.emit('disconnect');
}
_handleExit (respawn = this._respawn) {
this.process.removeAllListeners();
this.emit('death');
this.ready = false;
this.process = null;
if (respawn) this.spawn().catch(err => this.emit('error', err));
}
}
module.exports = Shard;