redis-smq-common
Version:
Provides essential components and utilities shared across RedisSMQ packages.
196 lines • 6.86 kB
JavaScript
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