pandora
Version:
A powerful and lightweight application manager for Node.js applications powered by TypeScript.
396 lines (357 loc) • 11.7 kB
text/typescript
;
import {ApplicationRepresentation, ApplicationStructureRepresentation, ProcessRepresentation} from '../domain';
import cluster = require('cluster');
import {cpus} from 'os';
import {format} from 'util';
import * as $ from 'pandora-dollar';
import Base = require('sdk-base');
import {consoleLogger} from '../universal/LoggerBroker';
import {ProcfileReconciler} from './ProcfileReconciler';
import {
READY, WORKER_READY, APP_START_SUCCESS, RELOAD, SHUTDOWN, WORKER_EXIT, ERROR,
RELOAD_SUCCESS, RELOAD_ERROR, SHUTDOWN_TIMEOUT, FINISH_SHUTDOWN
} from '../const';
const cFork = require('../../3rd/fork');
const PathWorkerProcessBootstrap = require.resolve('./WorkerProcessBootstrap');
const $ProcessName = Symbol('ProcessName');
export const defaultWorkerCount = process.env.DEFAULT_WORKER_COUNT ? parseInt(process.env.DEFAULT_WORKER_COUNT) : cpus().length;
/**
* Class Master
*/
export class ProcessMaster extends Base {
public started: boolean = false;
private workers: Map<any, any> = new Map();
private appRepresentation: ApplicationRepresentation = null;
private procfileReconciler: ProcfileReconciler = null;
private get workersSet() {
const workers = [];
for (let [key, value] of this.workers) {
workers.push({
pid: key,
spawnargs: value.process.spawnargs,
name: value[$ProcessName],
});
}
return workers;
}
constructor(appRepresentation: ApplicationRepresentation) {
super();
this.appRepresentation = appRepresentation;
this.procfileReconciler = new ProcfileReconciler(appRepresentation);
}
/**
* Get all the workers
* @return {Array}
*/
getWorkers() {
return this.workersSet;
}
/**
* Start master
* @return {Promise<void>}
*/
async start() {
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: ApplicationStructureRepresentation = this.procfileReconciler.getApplicationStructure();
const {process: processRepresentSet} = appStructRepresent;
for (const processRepresent of processRepresentSet) {
await this.forkWorker({
...this.appRepresentation,
...processRepresent
});
}
// Mark self started
this.started = true;
// Notify all workers the application successful started
this.notify(APP_START_SUCCESS, {workers: this.workersSet});
}
/**
* 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.sendWorkerShutdown(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>}
*/
public async reload(targetName?) {
/*
* 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]) {
await this.reloadNamedWorkers(workers[targetName]);
return;
}
const appStructRepresent: ApplicationStructureRepresentation =
this.procfileReconciler.getApplicationStructure();
const {process: processRepresentSet} = appStructRepresent;
for (const processRepresent of processRepresentSet) {
await this.reloadNamedWorkers(workers[processRepresent.processName]);
}
}
/**
* Realod named workers
* @param workers
* @return {Promise<void>}
*/
private 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>}
*/
private reloadWorker(worker): Promise<any> {
return this.sendWorkerShutdown(worker).then(() => {
return new Promise((resolve) => {
let setting = worker._clusterSettings;
if (setting) {
cluster.settings = setting;
cluster.setupMaster();
}
const newWorker: any = 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;
});
});
}
private sendWorkerShutdown(worker): Promise<any> {
return new Promise((resolve) => {
let timer = setTimeout(() => {
resolve();
}, SHUTDOWN_TIMEOUT);
// Mark it doesn't want to refork by 3rd lib cfork
worker._refork = false;
worker.send({action: SHUTDOWN});
worker.on('message', message => {
if (message.action === FINISH_SHUTDOWN) {
clearTimeout(timer);
timer = null;
resolve();
}
});
});
}
/**
* Kill a worker by in a SOP way
* @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 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>}
*/
private forkWorker(processRepresentation: ProcessRepresentation): Promise<any> {
const workerArgs = ['--params', JSON.stringify(processRepresentation)];
const count = processRepresentation.scale === 'auto' ? defaultWorkerCount : (processRepresentation.scale || 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 === 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 === ERROR) {
const message = 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 = 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
*/
private 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) {
consoleLogger.info(`Application's master receive a signal ${sig}, exit with code 0, pid ${process.pid}`);
this.stop().then(() => {
process.exit(0);
}).catch((err) => {
consoleLogger.error(err);
process.exit(1);
});
}
/**
* Handing the process message
* @param message
*/
onProcessMessage(message) {
if (message.action === RELOAD) {
this.reload(message.name).then(() => {
if (process.send) {
process.send({action: RELOAD_SUCCESS});
}
}).catch((err) => {
consoleLogger.error(err);
if (process.send) {
process.send({action: 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 === READY) {
const pid = worker.process.pid;
this.notify(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(WORKER_EXIT, {
code,
signal,
pid: String(pid),
name: worker[$ProcessName],
});
}
}
export default (appRepresentation: ApplicationRepresentation, done) => {
const master = new ProcessMaster(appRepresentation);
master.start().then(() => {
done();
}).catch(err => {
done(err);
});
};