import cluster from "node:cluster"; import { cpus } from "node:os"; import { PlataClusterController } from './cluster/cluster-controller' import dns from 'node:dns' export class PlataClusterManager { private readonly NodeCluster = Plata.NodeCluster constructor() { if (Plata.cluster.isPrimary) { if (this.NodeCluster.eventNames().indexOf('exit') === -1) { this.NodeCluster.on('exit', ( t => t._onWorkerKilled(t) )(this)) } if (this.NodeCluster.eventNames().indexOf('message') === -1) { this.NodeCluster.on('message', ( (t) => t._onMessage(t) )(this)) } } } public async _startCluster(config: Cluster.WorkerConfig): Promise { if (!Plata.cluster.isPrimary) return { errorID: 'PBLCLWS0002', msg: 'startCluster must be called from primary process', error: { stack: new Error().stack } } if (config.workers === undefined) { if (config.env === 'prod') { config.workers = cpus().length } else { config.workers = 1 } } if (config.workers === -1) { config.workers = cpus().length } if (config.workers === 0) { return null } console.log(`Starting ${config.workers} workers on cluster ${config.name}`) const cluster: Cluster.ClusterConfig = { pids: [], config } for (let i = 0; i < config.workers; i++) { const w = this._fork(config, false) if (w.process.pid === undefined) { return { errorID: 'PBLCLWS0003', msg: `Erro while starting worker ${i} from cluster ${config.name}`, error: w.process.exitCode } } cluster.pids.push(w.process.pid) } if (config.controller !== undefined) { console.log(`Starting controller worker on cluster ${config.name}`) const w = this._fork(config, true) if (w.process.pid === undefined) { return { errorID: 'PBLCLWS0003', msg: `Erro while starting controller worker from cluster ${config.name}`, error: w.process.exitCode } } cluster.controllerPid = w.process.pid cluster.pids.push(w.process.pid) } if (config.onExit !== undefined) { this.NodeCluster.on(this.getClusterEvents(config.name).worker.exit, config.onExit) } Plata.cluster.clusters.push(cluster) return null } public isPrimary() { return this.NodeCluster.isPrimary } public _fork(config: Cluster.WorkerConfig, isController: boolean) { return this.NodeCluster.fork({ ...config.workersEnv, ENV: config.env, _PLATA_WORKER_NAME: config.name, _PLATA_WORKER_TYPE: isController ? 'CONTROLLER' : 'WORKER' }) } public getClusterEvents(clusterName: string) { const base = `plata.cluster.${clusterName}` return { worker: { exit: `${base}.worker.exit` } } } public _onWorkerKilled(t: this) { return (worker: Cluster.Worker, code: number, signal: string) => { if (worker.process.pid === undefined) return if (!Plata.cluster.isPrimary) return const clusterIndex = Plata.cluster.clusters.findIndex(cluster => cluster.pids.indexOf(worker.process.pid ?? 0) !== -1 ) if (clusterIndex !== -1) { const c = Plata.cluster.clusters[clusterIndex] console.log(`O Worker ${worker.process.pid} do cluster ${c.config.name} morreu com o codigo ${code} ${signal ?? ''}`) console.log(`Iniciando um novo worker no cluster ${c.config.name}`) cluster.emit( t.getClusterEvents(c.config.name).worker.exit, worker, code, signal, ) const wasController = c.controllerPid === worker.process.pid const w = t._fork(c.config, wasController) const pidIndex = c.pids.indexOf(worker.process.pid) c.pids[pidIndex] = w.process.pid ?? 0 if (wasController) { c.controllerPid = w.process.pid ?? 0 } if (c.controllerPid !== undefined) { if (wasController) return t._sendMsgToPid(c.controllerPid, { action: 'DEAD-WORKER-NOTIFY', msg: { clusterName: c.config.name, newPid: w.process.pid, pidDead: worker.process.pid, } } as any) } } } } public async _sendMsgToAllWorkers(msg: Cluster.Actions) { if (this.NodeCluster.workers === undefined) return for (const worker of Object.values(this.NodeCluster.workers)) { if (worker === undefined) continue worker.send(msg) } } public async sendMsgToCluster(name: string, msg: Cluster.Actions) { if (this.NodeCluster.workers === undefined) return if (!Plata.cluster.isPrimary) return const clusterIndex = Plata.cluster.clusters.findIndex(cluster => cluster.config.name === name) if (clusterIndex === -1) return if (Plata.cluster.clusters[clusterIndex].controllerPid !== undefined) { return this._sendMsgToPid(Plata.cluster.clusters[clusterIndex].controllerPid, msg) } const pids = Plata.cluster.clusters[clusterIndex].pids for (const worker of Object.values(this.NodeCluster.workers)) { if (worker === undefined) continue if (pids.includes(worker.process.pid ?? 0)) worker.send(msg) ; } } public async _sendMsgToPid(pid: number, msg: T) { if (this.NodeCluster.workers === undefined) return if (!Plata.cluster.isPrimary) return for (const worker of Object.values(this.NodeCluster.workers)) { if (worker === undefined) continue const wPid = worker.process.pid ?? 0 if (wPid === pid) worker.send(msg) ; } } public async _getClusterPids(name: string, id: string) { if (this.NodeCluster.workers === undefined) return this._sendMsgToAllWorkers({ action: `GET-PIDS-CLUSTER-RESULT-${id}`, msg: { errorID: 'PCLGETCLPIDS001', msg: 'não tem workers online', } }) ; if (!Plata.cluster.isPrimary) return this._sendMsgToAllWorkers({ action: `GET-PIDS-CLUSTER-RESULT-${id}`, msg: { errorID: 'PCLGETCLPIDS002', msg: 'não tem workers online', } }) ; const clusterIndex = Plata.cluster.clusters.findIndex(cluster => cluster.config.name === name) if (clusterIndex === -1) return this._sendMsgToAllWorkers({ action: `GET-PIDS-CLUSTER-RESULT-${id}`, msg: { errorID: 'PCLGETCLPIDS002', msg: 'Cluster não encontrado', error: name } }) ; return this._sendMsgToAllWorkers({ action: `GET-PIDS-CLUSTER-RESULT-${id}`, msg: Plata.cluster.clusters[clusterIndex].pids }) } public _onMessage(t: this) { return (worker: Cluster.Worker, actionEvent: Cluster.GetActions) => { Plata.switchUnionObject(actionEvent, 'action', { _default: async (v) => console.log(`Action inválida: ${actionEvent.action}`), "ECHO-CLUSTER": async ({ msg }) => t.sendMsgToCluster(msg.name, msg.data), 'ECHO-PID': async ({ msg }) => t._sendMsgToPid(msg.pid, msg.data), 'DEAD-WORKER-NOTIFY': async () => console.log(`Action inválida: ${actionEvent.action}`), ECHO: async ({ msg }) => t._sendMsgToAllWorkers(msg), KILL: async ({ msg }) => { console.log(`Process kill requested: ${msg}`) process.exit(0) }, RELOAD: async () => t._sendMsgToAllWorkers({ action: 'RELOAD', msg: true }), "GET-PIDS-CLUSTER": async ({ msg }) => t._getClusterPids(msg.name.toString(), msg.id), }) } } public async send(msg: Cluster.Actions): PlataPromise { if (process.send === undefined) { return { errorID: 'PBLCLWS0001', msg: `process don't have send function`, error: { stack: new Error().stack } } } return process.send(msg) } public async sendToWorkerPid(pid: number, data: Cluster.UserActions) { return this.send({ action: 'ECHO-PID', msg: { pid, data } }) } public async _onStartCluster(name: string, type: typeof Plata.config._PLATA_WORKER_TYPE) { const c: { default: Cluster.WorkerConfig } = require(`@@/clusters/${name}`) if (Plata.config._PLATA_WORKER_DEFAULTDNS === '1') { const dnsServersSet = new Set([ ...dns.getServers(), // Default DNSs '1.1.1.1', '8.8.8.8', ]) dns.setServers([...dnsServersSet]) } if (type === 'CONTROLLER' && c.default.controller !== undefined) { const controller = new PlataClusterController( this, name, ) await c.default.controller(controller) return } await c.default.onStart() } } export * from './cluster/cluster-controller'