UNPKG

pandora

Version:

A powerful and lightweight application manager for Node.js applications powered by TypeScript.

361 lines 14.2 kB
'use strict'; 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