UNPKG

@dazejs/framework

Version:

Daze.js - A powerful web framework for Node.js

232 lines (215 loc) 7.73 kB
/** * Copyright (c) 2018 Chan Zewail * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ import cluster, { Worker as ClusterWorker } from 'cluster'; import debuger from 'debug'; import net from 'net'; import hash from 'string-hash'; import { Deferred } from '../utils/defered'; import { RELOAD_SIGNAL, STIKCY_CONNECTION, WORKER_DID_FORKED, WORKER_DISCONNECT, WORKER_DYING, DAZE_PROCESS_TYPE } from './const'; import { getAlivedWorkers, parseMasterOpts } from './helpers'; const debug = debuger('daze-framework:cluster'); export interface MasterOptions { port: number; workers: number; sticky: boolean; } const defaultOptions: MasterOptions = { port: 0, workers: 0, sticky: false, }; export class Master { /** * master options */ options: MasterOptions; /** * server connections */ connections: { [key: string]: net.Socket; } = {}; constructor(opts: MasterOptions) { this.options = Object.assign({}, defaultOptions, parseMasterOpts(opts)); } /** * Fork a work process */ forkWorker(env = {}) { const worker = cluster.fork(env); debug(`worker is forked, use pid: ${worker.process.pid}`); (worker as any)[DAZE_PROCESS_TYPE] = 'worker'; const deferred = new Deferred<ClusterWorker>(); // Accepts the disconnection service signal sent by the work process, // indicating that the work process is about to // stop the service and needs to be replaced by a new work process // 接受工作进程发送的断开服务信号,表示该工作进程即将停止服务,需要 fork 一个新的工作进程来替代 worker.on('message', (message: string) => { if (Reflect.getMetadata(WORKER_DYING, worker)) return; // if (worker[WORKER_DYING]) return; if (message === WORKER_DISCONNECT) { debug('refork worker, receive message \'daze-worker-disconnect\''); Reflect.defineMetadata(WORKER_DYING, true, worker); // worker[WORKER_DYING] = true; // The signal that tells the worker process that it has fork after fork, // and lets it end the service // fork 完毕后通知工作进程已 fork 的信号,让其结束服务 this.forkWorker(env).then(() => worker.send(WORKER_DID_FORKED)).catch(() => { // }); } }); // Emitted after the worker IPC channel has disconnected // Automatically fork a new work process after the IPC pipeline is detected to be disconnected worker.once('disconnect', () => { if (Reflect.getMetadata(WORKER_DYING, worker)) return; // if (worker[WORKER_DYING]) return; debug(`worker disconnect: ${worker.process.pid}`); Reflect.defineMetadata(WORKER_DYING, true, worker); // worker[WORKER_DYING] = true; debug('worker will fork'); this.forkWorker(env); }); // The cluster module will trigger an 'exit' event when any worker process is closed worker.once('exit', (code: number, signal: string) => { // if (worker[WORKER_DYING]) return; if (Reflect.getMetadata(WORKER_DYING, worker)) return; debug(`worker exit, code: ${code}, signal: ${signal}`); Reflect.defineMetadata(WORKER_DYING, true, worker); // worker[WORKER_DYING] = true; this.forkWorker(env); }); // listening event worker.once('listening', (address: string) => { debug(`listening, address: ${JSON.stringify(address)}`); deferred.resolve(worker); }); return deferred.promise; } /** * Work processes corresponding to the fork, depending on the configuration or number of cpus * fork 对应的工作进程,取决于cpu的数量或配置参数 */ forkWorkers() { const { workers } = this.options; const promises: Promise<any>[] = []; const env = { DAZE_PROCESS_TYPE: 'worker' }; for (let i = 0; i < workers; i += 1) { promises.push(this.forkWorker(env)); } return Promise.all(promises); } /** * Fork a separate process * fork 一条独立进程 */ forkAgent() { const env = { DAZE_PROCESS_TYPE: 'agent' }; const agent = cluster.fork(env); (agent as any)[DAZE_PROCESS_TYPE] = 'agent'; // 在工作服务断开后触发 // 检测到工作服务断开后,自动 fork 新的工作进程 agent.once('disconnect', () => { if (Reflect.getMetadata(WORKER_DYING, agent)) return; // if (worker[WORKER_DYING]) return; debug(`agent disconnect: ${agent.process.pid}`); Reflect.defineMetadata(WORKER_DYING, true, agent); // worker[WORKER_DYING] = true; debug('agent will fork'); this.forkAgent(); }); // 当任何工作进程关闭时,群集模块将触发 'exit' 事件 agent.once('exit', (code: number, signal: string) => { // if (worker[WORKER_DYING]) return; if (Reflect.getMetadata(WORKER_DYING, agent)) return; debug(`agent exit, code: ${code}, signal: ${signal}`); Reflect.defineMetadata(WORKER_DYING, true, agent); // worker[WORKER_DYING] = true; cluster.fork(env); }); } /** * Create sticky sessions for websocket communication * 创建粘性会话,适用于 websocket 通信 * reference https://github.com/uqee/sticky-cluster */ cteateStickyServer() { const deferred = new Deferred<ClusterWorker[]>(); const server = net.createServer({ pauseOnConnect: true }, (connection) => { const signature = `${connection.remoteAddress}:${connection.remotePort}`; this.connections[signature] = connection; connection.on('close', () => { delete this.connections[signature]; }); const index = hash(connection.remoteAddress || '') % this.options.workers; let current = -1; getAlivedWorkers().some((worker) => { // eslint-disable-next-line no-plusplus if (index === ++current) { worker.send(STIKCY_CONNECTION, connection); return true; } return false; }); }); server.listen(this.options.port, () => { this.forkWorkers().then((data) => { deferred.resolve(data); }); }); return deferred.promise; } /** * Send a reload signal to all work processes * 给所有工作进程发送 reload 信号 */ reloadWorkers() { for (const worker of getAlivedWorkers()) { worker.send(RELOAD_SIGNAL); } return this; } /** * Capture all restart work process signals * 捕获所有重启工作进程的信号 */ catchSignalToReload() { // After the master process receives the reload signal // it traverses the surviving worker processes // and sends the reload instruction to each worker process // 主进程接收到 reload 信号后,遍历存活的工作进程,给每个工作进程发送 reload 指令 process.once(RELOAD_SIGNAL, () => { debug(`Start smooth restart, signal: ${RELOAD_SIGNAL}`); this.reloadWorkers(); }); // Receives the daze-restart restart instruction // sent by the work process to restart all the work processes // 接收工作进程发送的 daze-restart 重启指令,重启所有工作进程 cluster.on('message', (_worker, message) => { if (message !== 'daze-restart') return; this.reloadWorkers(); }); } /** * Start the service * 启动服务 */ async run() { debug(`current master process id [${process.pid}]`); const serverPromise = this.options.sticky ? this.cteateStickyServer() : this.forkWorkers(); const workers = serverPromise.then((res) => { // do something this.catchSignalToReload(); return res; }); return workers; } }