pandora
Version:
A powerful and lightweight application manager for Node.js applications powered by TypeScript.
285 lines • 10.5 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const cluster = require("cluster");
const util_1 = require("util");
const $ = require("pandora-dollar");
const const_1 = require("../const");
const pandora_dollar_1 = require("pandora-dollar");
const cFork = require('../../3rd/fork');
const pathToProcessBootstrap = require.resolve('./ProcessBootstrap');
const $ProcessName = Symbol('ProcessName');
/**
* Class ScalableMaster
* For kind of the process, that's field scale great than 1
*/
class ScalableMaster {
constructor(processRepresentation) {
this.started = false;
this.workers = new Map();
this.processRepresentation = null;
this.processRepresentation = processRepresentation;
}
get workersSet() {
const workers = [];
for (let [key, value] of this.workers) {
workers.push({
pid: key,
spawnargs: value.process.spawnargs,
name: value[$ProcessName],
});
}
return workers;
}
/**
* Get all the workers
* @return {Array}
*/
getWorkers() {
return this.workersSet;
}
/**
* Start master
* @return {Promise<void>}
*/
async start() {
// Listen the events of the module cluster
this.listenEvents();
// Start to cluster.fork()
await this.forkWorker(this.processRepresentation);
// Mark self as started
this.started = true;
}
/**
* Stop master, stop all workers
* @return {Promise<void>}
*/
async stop() {
const promises = [];
for (const id of Object.keys(cluster.workers)) {
const worker = cluster.workers[id];
promises.push(this.sendShutdownToWorker(worker));
}
await Promise.all(promises);
await Promise.all(Object.keys(cluster.workers).map(async (id) => {
const worker = cluster.workers[id];
await this.killWorkerSop(worker);
}));
this.started = false;
}
/**
* Reload all workers
* @param targetName
* @return {Promise<void>}
*/
async reload(targetName) {
if (targetName !== this.processRepresentation.processName && targetName != null) {
return;
}
await this.reloadNamedWorkers(this.workers.values());
}
/**
* Realod named workers
* @param workers
* @return {Promise<void>}
*/
async reloadNamedWorkers(workers) {
const r = [];
for (let worker of workers) {
r.push(this.reloadWorker(worker));
}
await Promise.all(r);
}
/**
* Reload a worker
* @param worker
* @return {Promise<any>}
*/
reloadWorker(worker) {
return this.sendShutdownToWorker(worker).then(() => {
return new Promise((resolve) => {
let setting = worker._clusterSettings;
if (setting) {
cluster.settings = setting;
cluster.setupMaster();
}
const newWorker = cluster.fork();
newWorker.once('online', () => {
newWorker[$ProcessName] = worker[$ProcessName];
this.workers.set(newWorker.process.pid, newWorker);
// Long living server connections may block workers from disconnecting
resolve(this.killWorkerSop(worker));
});
newWorker._clusterSettings = setting;
});
});
}
sendShutdownToWorker(worker) {
return new Promise((resolve) => {
let timer = setTimeout(() => {
resolve();
}, const_1.SHUTDOWN_TIMEOUT);
// tell 3rd lib cfork, it do want to be reforked
worker._refork = false;
worker.send({ action: const_1.SHUTDOWN });
worker.on('message', message => {
if (message.action === const_1.FINISH_SHUTDOWN) {
clearTimeout(timer);
timer = null;
resolve();
}
});
});
}
/**
* Kill a worker by a SOP
* @param worker
* @return {Promise<void>}
*/
async killWorkerSop(worker) {
if (worker.isDead()) {
return;
}
worker.disconnect();
await $.promise.delay(2000);
worker.kill();
await $.promise.delay(2000);
if (worker.isDead()) {
return;
}
worker.kill('SIGKILL');
await $.promise.delay(2000);
}
/**
* Send message to all workers
* @param action
* @param data
*/
notify(action, data) {
for (let pid of this.workers.keys()) {
const worker = this.workers.get(pid);
if (worker.isConnected()) {
worker.send({ action, data });
}
}
}
/**
* Fork a new worker, keep it live
* @param {ProcessRepresentation} processRepresentation
* @return {Promise<any>}
*/
forkWorker(processRepresentation) {
const processRepresentationToWorker = Object.assign(Object.assign({}, processRepresentation), {
// Set scale to 1, so ProcessBootstrap can see it is a worker not a master
scale: 1 });
const workerArgs = ['--params', JSON.stringify(processRepresentationToWorker)]
.concat(processRepresentation.args || []);
const count = processRepresentation.scale === 'auto' ? const_1.defaultWorkerCount : (processRepresentation.scale || const_1.defaultWorkerCount);
const execArgv = process.execArgv.concat(processRepresentation.execArgv || []);
// Handing TypeScript, only for unit test
if (/\.ts$/.test(module.filename) && execArgv.indexOf('ts-node/register') === -1) {
execArgv.push('-r', 'ts-node/register', '-r', 'nyc-ts-patch');
}
cFork({
env: processRepresentation.env,
exec: pathToProcessBootstrap,
execArgv,
args: workerArgs,
silent: false,
count: count,
// TODO: Set field refork to be false when started by pandora dev, it will not be restarted.
// TODO: It will exited course by exception, easy to find bugs.
refork: true
});
let successCount = 0;
return new Promise((resolve, reject) => {
const onFork = (worker) => {
worker.once('message', (message) => {
const action = message && message.action;
if (action === const_1.PROCESS_READY) {
successCount++;
worker[$ProcessName] = processRepresentation.processName;
this.workers.set(worker.process.pid, worker);
// All process started successfully
if (successCount === count) {
cluster.removeListener('exit', onExit);
cluster.removeListener('fork', onFork);
resolve();
}
}
else if (action === const_1.PROCESS_ERROR) {
const message = util_1.format('web-worker#%s:%s start error (exitedAfterDisconnect: %s, state: %s), current workers: %j', worker.id, worker.process.pid, worker.exitedAfterDisconnect, worker.state, Object.keys(cluster.workers));
const err = new Error(message);
reject(err);
}
});
};
const onExit = (worker, code, signal) => {
worker._refork = false;
worker.removeAllListeners('message');
const workerProcess = worker.process;
const exitCode = workerProcess.exitCode;
const message = util_1.format('web-worker#%s:%s died (code: %s, signal: %s, exitedAfterDisconnect: %s, state: %s), current workers: %j', worker.id, worker.process.pid, exitCode, signal, worker.exitedAfterDisconnect, worker.state, Object.keys(cluster.workers));
const err = new Error(message);
err.name = 'WebWorkerDiedError';
reject(err);
};
cluster.on('fork', onFork);
cluster.once('exit', onExit);
});
}
/**
* Listen the events of the cluster when the master start
*/
listenEvents() {
cluster.on('exit', this.onWorkerDie.bind(this));
cluster.on('fork', this.onClusterFork.bind(this));
process.on('message', this.onProcessMessage.bind(this));
}
/**
* Handing the process message
* @param message
*/
onProcessMessage(message) {
if (message.action === const_1.RELOAD) {
this.reload(message.name).then(() => {
if (process.send) {
process.send({ action: const_1.RELOAD_SUCCESS });
}
}).catch((err) => {
pandora_dollar_1.consoleLogger.error(err);
if (process.send) {
process.send({ action: const_1.RELOAD_ERROR, error: err });
}
});
}
}
onClusterFork(worker) {
// Send a message to other workers when a new worker online
worker.once('message', (message) => {
process.nextTick(() => {
const action = message && message.action;
if (action === const_1.PROCESS_READY) {
const pid = worker.process.pid;
this.notify(const_1.WORKER_READY, {
pid: String(pid),
name: worker[$ProcessName],
});
}
});
});
}
onWorkerDie(worker, code, signal) {
// After a worker died, notify MessengerServer to remove connection of that worker
const pid = worker.process.pid;
this.workers.delete(pid);
// Tell other workers that worker died
this.notify(const_1.WORKER_EXIT, {
code,
signal,
pid: String(pid),
name: worker[$ProcessName],
});
}
}
exports.ScalableMaster = ScalableMaster;
//# sourceMappingURL=ScalableMaster.js.map