UNPKG

redis-smq-common

Version:

Provides essential components and utilities shared across RedisSMQ packages.

196 lines 6.86 kB
import { randomUUID } from 'node:crypto'; import { resolve } from 'path'; import { Worker as WorkerThread } from 'worker_threads'; import { async } from '../async/index.js'; import { env } from '../env/index.js'; import { EventEmitter } from '../event/index.js'; import { WorkerIsShuttingDownError, WorkerThreadError, WorkerThreadFailureError, } from './errors/index.js'; import { EWorkerThreadChildExecutionCode, } from './types/index.js'; import { WorkerLogger } from './worker-logger.js'; const dir = env.getCurrentDir(); const allWorkers = new Set(); const cleanupAllWorkers = () => { if (allWorkers.size === 0) return; const tasks = Array.from(allWorkers).map((worker) => { return (cb) => { worker.shutdown(() => cb()); }; }); async.parallel(tasks, () => void 0); }; process.once('SIGTERM', cleanupAllWorkers); process.once('SIGINT', cleanupAllWorkers); process.once('beforeExit', cleanupAllWorkers); export class Worker extends EventEmitter { id; workerFilename; initialPayload; workerThread = null; stdoutStream = null; stderrStream = null; logger; isShuttingDown = false; constructor(workerFilename, initialPayload, logger) { super(); this.id = randomUUID(); this.workerFilename = workerFilename; this.initialPayload = initialPayload; this.logger = logger.createLogger(this.constructor.name); allWorkers.add(this); } cleanupStreams() { if (this.stdoutStream) { try { this.workerThread?.stdout?.unpipe(this.stdoutStream); this.stdoutStream.end(); this.stdoutStream.destroy(); this.stdoutStream = null; } catch (error) { this.logger.debug(`Error cleaning up stdout stream: ${error}`); } } if (this.stderrStream) { try { this.workerThread?.stderr?.unpipe(this.stderrStream); this.stderrStream.end(); this.stderrStream.destroy(); this.stderrStream = null; } catch (error) { this.logger.debug(`Error cleaning up stderr stream: ${error}`); } } } getWorkerThread() { if (this.isShuttingDown) { throw new WorkerIsShuttingDownError(); } if (!this.workerThread) { const workerThreadPath = resolve(dir, './worker-thread/worker-thread.js'); this.stdoutStream = new WorkerLogger(false); this.stderrStream = new WorkerLogger(true); const workerOptions = { workerData: { filename: this.workerFilename, initialPayload: this.initialPayload, type: this.type, }, stdout: true, stderr: true, }; this.workerThread = new WorkerThread(workerThreadPath, workerOptions); const workerStdout = this.workerThread.stdout; const workerStderr = this.workerThread.stderr; if (workerStdout && this.stdoutStream) { workerStdout.pipe(this.stdoutStream); workerStdout.on('error', (err) => { this.logger.debug(`Worker stdout error: ${err.message}`); }); } if (workerStderr && this.stderrStream) { workerStderr.pipe(this.stderrStream); workerStderr.on('error', (err) => { this.logger.debug(`Worker stderr error: ${err.message}`); }); } this.setupWorkerListeners(); this.logger.debug('Worker thread created with custom stream handling'); } return this.workerThread; } setupWorkerListeners() { if (!this.workerThread) return; this.workerThread.removeAllListeners(); this.workerThread.on('messageerror', (err) => { this.logger.error(`Message error: ${err.message}`); }); this.workerThread.on('error', (err) => { this.logger.error(`Worker error: ${err.message}`); this.emit('worker.error', err); this.cleanupStreams(); }); this.workerThread.on('exit', (code) => { this.logger.debug(`Worker exited with code ${code}`); this.cleanupStreams(); this.workerThread = null; this.emit('worker.terminated'); }); } postMessage(message, callback) { if (!callback) { callback = (err, reply) => { if (err) this.emit('worker.error', err); if (reply) this.emit('worker.data', reply); }; } if (this.isShuttingDown) { return callback(new WorkerIsShuttingDownError()); } const worker = this.getWorkerThread(); const onMessage = (msg) => { worker.removeListener('message', onMessage); worker.removeListener('exit', onExit); if (msg.code === EWorkerThreadChildExecutionCode.OK) { callback(null, msg.data); } else { callback(new WorkerThreadError({ metadata: msg })); } }; const onExit = (code) => { worker.removeListener('message', onMessage); worker.removeListener('exit', onExit); const error = code !== 0 ? new WorkerThreadFailureError({ metadata: { code, }, }) : null; callback(error); }; worker.once('message', onMessage); worker.once('exit', onExit); worker.postMessage(message); } shutdown(cb) { if (this.isShuttingDown) { return cb(new WorkerIsShuttingDownError()); } this.isShuttingDown = true; if (!this.workerThread) { this.cleanupStreams(); this.removeFromGlobalList(); return cb(null); } this.workerThread.removeAllListeners(); this.workerThread .terminate() .then(() => { this.cleanupStreams(); this.workerThread = null; this.isShuttingDown = false; this.removeFromGlobalList(); cb(null); }) .catch((err) => { this.isShuttingDown = false; cb(err); }); } removeFromGlobalList() { allWorkers.delete(this); } getId() { return this.id; } getWorkerFilename() { return this.workerFilename; } } //# sourceMappingURL=worker.js.map