pandora
Version:
A powerful and lightweight application manager for Node.js applications powered by TypeScript.
361 lines • 14.2 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const cluster = require("cluster");
const os_1 = require("os");
const util_1 = require("util");
const $ = require("pandora-dollar");
const Base = require("sdk-base");
const LoggerBroker_1 = require("../universal/LoggerBroker");
const ProcfileReconciler_1 = require("./ProcfileReconciler");
const const_1 = require("../const");
const cFork = require('../../3rd/fork');
const PathWorkerProcessBootstrap = require.resolve('./WorkerProcessBootstrap');
const $ProcessName = Symbol('ProcessName');
exports.defaultWorkerCount = process.env.DEFAULT_WORKER_COUNT ? parseInt(process.env.DEFAULT_WORKER_COUNT) : os_1.cpus().length;
/**
* Class Master
*/
class ProcessMaster extends Base {
constructor(appRepresentation) {
super();
this.started = false;
this.workers = new Map();
this.appRepresentation = null;
this.procfileReconciler = null;
this.appRepresentation = appRepresentation;
this.procfileReconciler = new ProcfileReconciler_1.ProcfileReconciler(appRepresentation);
}
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>}
*/
start() {
return __awaiter(this, void 0, void 0, function* () {
this.procfileReconciler.discover();
if (!this.procfileReconciler.getComplexApplicationStructureRepresentation().mount.length) {
throw new Error(`Can't got any process at ${this.appRepresentation.appDir}`);
}
// Pass appId to child process through process.env
process.env.appId = process.pid;
// Listen the events of the cluster
this.listenEvents();
// Start process
const appStructRepresent = this.procfileReconciler.getApplicationStructure();
const { process: processRepresentSet } = appStructRepresent;
for (const processRepresent of processRepresentSet) {
yield this.forkWorker(Object.assign({}, this.appRepresentation, processRepresent));
}
// Mark self started
this.started = true;
// Notify all workers the application successful started
this.notify(const_1.APP_START_SUCCESS, { workers: this.workersSet });
});
}
/**
* Stop master, stop all workers
* @return {Promise<void>}
*/
stop() {
return __awaiter(this, void 0, void 0, function* () {
const promises = [];
for (const id of Object.keys(cluster.workers)) {
const worker = cluster.workers[id];
promises.push(this.sendWorkerShutdown(worker));
}
yield Promise.all(promises);
yield Promise.all(Object.keys(cluster.workers).map((id) => __awaiter(this, void 0, void 0, function* () {
const worker = cluster.workers[id];
yield this.killWorkerSop(worker);
})));
this.started = false;
});
}
/**
* Reload all workers
* @param targetName
* @return {Promise<void>}
*/
reload(targetName) {
return __awaiter(this, void 0, void 0, function* () {
/*
* The main logic is
* 1. Send a shutdown message to workers, the workers will to do some processing after receive that message
* 2. Refork the worker after receive reply message or timeout
* @param name {string} appName
* @return {Promise}
*/
const workers = {};
for (let pid of this.workers.keys()) {
const worker = this.workers.get(pid);
const name = worker[$ProcessName];
if (!workers[name]) {
workers[name] = [];
}
workers[name].push(worker);
}
if (workers[targetName]) {
yield this.reloadNamedWorkers(workers[targetName]);
return;
}
const appStructRepresent = this.procfileReconciler.getApplicationStructure();
const { process: processRepresentSet } = appStructRepresent;
for (const processRepresent of processRepresentSet) {
yield this.reloadNamedWorkers(workers[processRepresent.processName]);
}
});
}
/**
* Realod named workers
* @param workers
* @return {Promise<void>}
*/
reloadNamedWorkers(workers) {
return __awaiter(this, void 0, void 0, function* () {
const r = [];
for (let worker of workers) {
r.push(this.reloadWorker(worker));
}
yield Promise.all(r);
});
}
/**
* Reload a worker
* @param worker
* @return {Promise<any>}
*/
reloadWorker(worker) {
return this.sendWorkerShutdown(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;
});
});
}
sendWorkerShutdown(worker) {
return new Promise((resolve) => {
let timer = setTimeout(() => {
resolve();
}, const_1.SHUTDOWN_TIMEOUT);
// Mark it doesn't want to refork by 3rd lib cfork
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 in a SOP way
* @param worker
* @return {Promise<void>}
*/
killWorkerSop(worker) {
return __awaiter(this, void 0, void 0, function* () {
if (worker.isDead()) {
return;
}
worker.disconnect();
yield $.promise.delay(2000);
worker.kill();
yield $.promise.delay(2000);
if (worker.isDead()) {
return;
}
worker.kill('SIGKILL');
yield $.promise.delay(2000);
});
}
/**
* Send message to all the 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 that live
* @param {ProcessRepresentation} processRepresentation
* @return {Promise<any>}
*/
forkWorker(processRepresentation) {
const workerArgs = ['--params', JSON.stringify(processRepresentation)];
const count = processRepresentation.scale === 'auto' ? exports.defaultWorkerCount : (processRepresentation.scale || exports.defaultWorkerCount);
const execArgv = process.execArgv.concat(processRepresentation.argv || []);
// 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: PathWorkerProcessBootstrap,
execArgv,
args: workerArgs,
silent: false,
count: count,
// TODO: Set the refork to be false in the local environment, it will not restart child process that exited by exception. It's 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.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.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));
process.once('SIGQUIT', this.onProcessTerm.bind(this, 'SIGQUIT'));
process.once('SIGTERM', this.onProcessTerm.bind(this, 'SIGTERM'));
process.once('SIGINT', this.onProcessTerm.bind(this, 'SIGINT'));
}
/**
* Handing the process term signal
* @param sig
*/
onProcessTerm(sig) {
LoggerBroker_1.consoleLogger.info(`Application's master receive a signal ${sig}, exit with code 0, pid ${process.pid}`);
this.stop().then(() => {
process.exit(0);
}).catch((err) => {
LoggerBroker_1.consoleLogger.error(err);
process.exit(1);
});
}
/**
* 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) => {
LoggerBroker_1.consoleLogger.error(err);
if (process.send) {
process.send({ action: const_1.RELOAD_ERROR });
}
});
}
}
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.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.ProcessMaster = ProcessMaster;
exports.default = (appRepresentation, done) => {
const master = new ProcessMaster(appRepresentation);
master.start().then(() => {
done();
}).catch(err => {
done(err);
});
};
//# sourceMappingURL=ProcessMaster.js.map