Compare commits

..

No commits in common. "d2a9348051cc138030b8f62138995801680358ee" and "46ad540f68869c2c1b946588ed20c166924e095f" have entirely different histories.

7 changed files with 41 additions and 114 deletions

View File

@ -3,7 +3,6 @@
"shardOptions": { "shardOptions": {
"respawn": false "respawn": false
}, },
"shardCount": 1,
"serverOptions": { "serverOptions": {
"serveFiles": "./files", "serveFiles": "./files",
"callbackURL": "/api/login", "callbackURL": "/api/login",

View File

@ -38,8 +38,6 @@ class Controller extends EventEmitter {
this.logger.error(ex.stack); this.logger.error(ex.stack);
}); });
process.on('SIGINT', this.#shutdown.bind(this));
this.on('built', () => { this.on('built', () => {
this.logger.info(`API Controller built`); this.logger.info(`API Controller built`);
this._built = true; this._built = true;
@ -92,16 +90,6 @@ class Controller extends EventEmitter {
shard.on('message', (msg) => this._handleMessage(shard, msg)); shard.on('message', (msg) => this._handleMessage(shard, msg));
} }
async #shutdown () {
this.logger.info('Received SIGINT, shutting down');
const promises = this.shards.map(shard => shard.awaitShutdown());
await Promise.all(promises);
this.logger.info(`Shutdown completed, goodbye`);
// eslint-disable-next-line no-process-exit
process.exit();
}
} }
module.exports = Controller; module.exports = Controller;

View File

@ -2,8 +2,7 @@ const ChildProcess = require('node:child_process');
const { EventEmitter } = require('node:events'); const { EventEmitter } = require('node:events');
const { Util } = require('../util'); const { Util } = require('../util');
// Give subprocess 90s to shut down before being forcibly killed
const KillTO = 90 * 1000;
class Shard extends EventEmitter { class Shard extends EventEmitter {
constructor (controller, id, options = {}) { constructor (controller, id, options = {}) {
@ -32,8 +31,6 @@ class Shard extends EventEmitter {
// Set in the spawn method // Set in the spawn method
this.spawnedAt = null; this.spawnedAt = null;
this._awaitingShutdown = null;
} }
async spawn (waitForReady = false) { async spawn (waitForReady = false) {
@ -88,7 +85,7 @@ class Shard extends EventEmitter {
const to = setTimeout(() => { const to = setTimeout(() => {
this.process.kill(); this.process.kill();
resolve(); resolve();
}, KillTO); }, 5000);
// Gracefully handle exit // Gracefully handle exit
this.process.once('exit', (code, signal) => { this.process.once('exit', (code, signal) => {
clearTimeout(to); clearTimeout(to);
@ -121,13 +118,6 @@ class Shard extends EventEmitter {
}); });
} }
awaitShutdown () {
this._respawn = false;
return new Promise((resolve) => {
this._awaitingShutdown = resolve;
});
}
_handleMessage (message) { _handleMessage (message) {
if (message) { if (message) {
if (message._ready) { if (message._ready) {
@ -135,10 +125,6 @@ class Shard extends EventEmitter {
this.emit('ready'); this.emit('ready');
return; return;
} else if (message._shutdown) { } else if (message._shutdown) {
setTimeout(() => {
if (this.process)
this.process.kill('SIGKILL');
}, KillTO);
this.ready = false; this.ready = false;
this.emit('shutdown'); this.emit('shutdown');
return; return;
@ -164,8 +150,6 @@ class Shard extends EventEmitter {
this.process.removeAllListeners(); this.process.removeAllListeners();
this.emit('death'); this.emit('death');
if (this._awaitingShutdown)
this._awaitingShutdown();
if (code !== 0) if (code !== 0)
this.crashes.push(Date.now() - this.spawnedAt); this.crashes.push(Date.now() - this.spawnedAt);

View File

@ -155,7 +155,6 @@ class Server extends EventEmitter {
this.app.use(this.#ready.bind(this)); // denies requests before the server is ready this.app.use(this.#ready.bind(this)); // denies requests before the server is ready
process.on('message', this._handleMessage.bind(this)); process.on('message', this._handleMessage.bind(this));
process.on('SIGINT', this.shutdown.bind(this));
} }
@ -210,11 +209,10 @@ class Server extends EventEmitter {
async shutdown () { async shutdown () {
this.logger.info(`Received shutdown command, initiating graceful shutdown`); this.logger.info(`Received shutdown command, initiating graceful shutdown`);
// Tells the manager the shard is shutting down, will set a timeout to kill the shard if it hangs
process.send({ _shutdown: true }); process.send({ _shutdown: true });
this._ready = false; this._ready = false; // stops any new connections
this.server.close(async () => {
await this.mongodb.close(); await this.mongodb.close();
await this.mariadb.close(); await this.mariadb.close();
this.logger.status('DB shutdowns complete.'); this.logger.status('DB shutdowns complete.');
@ -222,7 +220,6 @@ class Server extends EventEmitter {
this.logger.status('Shutdown complete. Goodbye'); this.logger.status('Shutdown complete. Goodbye');
// eslint-disable-next-line no-process-exit // eslint-disable-next-line no-process-exit
process.exit(); process.exit();
});
} }

View File

@ -3,8 +3,6 @@ const mysql = require('mysql');
const { inspect } = require('node:util'); const { inspect } = require('node:util');
const { Util } = require('../../util'); const { Util } = require('../../util');
const SAFE_TO_RETRY = [ 'ER_LOCK_DEADLOCK' ];
class MariaDB { class MariaDB {
constructor (server, config) { constructor (server, config) {
@ -41,6 +39,7 @@ class MariaDB {
this.pool = mysql.createPoolCluster(this.config.options.cluster); this.pool = mysql.createPoolCluster(this.config.options.cluster);
for (const creds of this._credentials) for (const creds of this._credentials)
this.pool.add({ ...this.config.options.client, ...creds }); this.pool.add({ ...this.config.options.client, ...creds });
} else { } else {
this.pool = mysql.createPool({ ...this.config.options.client, ...this._credentials[0] }); this.pool = mysql.createPool({ ...this.config.options.client, ...this._credentials[0] });
} }
@ -107,56 +106,30 @@ class MariaDB {
} }
getConnection () { query (query, values) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.pool.getConnection((err, conn) => {
if (err)
return reject(err);
resolve(conn);
});
});
}
/**
* Retry certain queries that are safe to retry, e.g. deadlock
*
* @private
* */
async #_query (query, values, attempts = 0) {
const connection = await this.getConnection();
try {
const result = await new Promise((resolve, reject) => {
const q = connection.query(query, values, (err, results, fields) => {
if (err)
reject(err);
else if (results)
resolve(results);
else
resolve(fields);
connection.release();
});
this.logger.debug(`Constructed query: ${q.sql}`);
});
return Promise.resolve(result);
} catch (err) {
// Retry safe errors
if (SAFE_TO_RETRY.includes(err.code) && attempts < 5) //
return this.#_query(query, values, ++attempts);
return Promise.reject(err);
}
}
async query (query, values) {
if (!this.ready) if (!this.ready)
return Promise.reject(new Error('MariaDB not ready')); return reject(new Error('MariaDB not ready'));
let batch = false; let batch = false;
if (values && typeof values.some === 'function') if (values && typeof values.some === 'function')
batch = values.some(val => val instanceof Array); batch = values.some(val => val instanceof Array);
this.logger.debug(`Incoming query (batch: ${batch})\n${query}\n${inspect(values)}`); this.logger.debug(`Incoming query (batch: ${batch})\n${query}\n${inspect(values)}`);
return this.#_query(query, values); // If connected to a cluster, pick one of the node connection pools
// By default the config is set to use round robin for picking a pool when connected to a cluster
const pool = this._cluster ? this.pool.of('*') : this.pool;
return pool.query(query, values, (err, results, fields) => {
if (err)
return reject(err);
if (results)
return resolve(results);
resolve(fields);
});
});
} }

View File

@ -11,9 +11,9 @@ class Util {
return Math.floor(Date.now() / 1000); return Math.floor(Date.now() / 1000);
} }
static wait (timeMs) { static wait (time) {
return new Promise((resolve) => { return new Promise((resolve) => {
setTimeout(resolve, timeMs); setTimeout(resolve, time);
}); });
} }

View File

@ -1,14 +0,0 @@
{
"apps": [
{
"name": "framework",
"script": "index.js",
"interpreter_args": "",
"error_file": "/dev/null",
"out_file": "/dev/null",
"kill_timeout": 90000,
"max_restarts": 5,
"restart_delay": 5000
}
]
}