torotask
Version:
Task queue processing in NodeJS based on BullMQ and Redis
116 lines • 5.45 kB
JavaScript
import { ToroTask } from './client.js';
import { filterGroups } from './utils/filter-groups.js';
import { isControlError } from './utils/is-control-error.js';
const LOGGER_NAME = 'TaskServer';
/**
* A server class responsible for managing the lifecycle of BullMQ workers
* based on TaskGroup definitions from the client package.
* Provides features like centralized worker start/stop and optional global error handling.
*
* @template TAllTaskGroupsDefs The type of all task group definitions provided to the server
* @template TGroups The type of task groups derived from the task group definitions
*/
export class TaskServer extends ToroTask {
options;
// Store bound handlers to remove them later
unhandledRejectionListener;
uncaughtExceptionListener;
constructor(options, taskGroupDefs) {
// Refined Logger Initialization
options.loggerName = options.loggerName ?? LOGGER_NAME;
options.handleGlobalErrors = options.handleGlobalErrors ?? true;
super(options, taskGroupDefs);
this.options = options;
this.logger.debug('TaskServer initialized');
}
/**
* Starts server and workers based on the provided filter.
*
* @param filter Optional filter to target specific groups or tasks.
* @param workerOptions Optional default WorkerOptions to pass down.
*/
async start(filter, workerOptions) {
this.logger.info('Starting TaskServer workers...');
// Attach global handlers if configured
if (this.options.handleGlobalErrors) {
this.attachGlobalErrorHandlers();
}
// TODO Add event dispatcher options somewhere, possibly create dedicated method on client
await this.events.startWorker();
const groupsToProcess = filterGroups(this, filter, 'starting workers');
if (groupsToProcess.length === 0) {
this.logger.info('No groups to start workers for based on the filter.');
return;
}
const mergedOptions = {
prefix: this.queuePrefix,
connection: this.connectionOptions,
...workerOptions,
};
await Promise.allSettled(groupsToProcess.map(async (group) => {
await group.startWorkers(undefined, mergedOptions);
}));
this.logger.info('Finished request to start workers');
}
/**
* Stops server and workers based on the provided filter.
*
* @param filter Optional filter to target specific groups or tasks.
* @returns A promise that resolves when all targeted workers have been requested to stop.
*/
async stop(filter) {
this.logger.info({ filter }, 'Stopping workers across task groups');
const groupsToProcess = filterGroups(this, filter, 'stopping workers');
if (groupsToProcess.length === 0) {
this.logger.info('No groups to stop workers for based on the filter.');
return;
}
await Promise.allSettled(groupsToProcess.map(async (group) => {
await group.stopWorkers();
}));
this.logger.info('Finished request to stop workers');
// Detach global handlers if we attached them
this.detachGlobalErrorHandlers();
await this.close();
this.logger.info('TaskServer stopped.');
}
attachGlobalErrorHandlers() {
if (this.unhandledRejectionListener || this.uncaughtExceptionListener) {
this.logger.warn('Global error handlers already attached, detaching first.');
this.detachGlobalErrorHandlers();
}
this.logger.debug('Attaching global error handlers (unhandledRejection, uncaughtException)');
// Bind `this` to ensure logger is accessible
this.unhandledRejectionListener = this.handleUnhandledRejection.bind(this);
this.uncaughtExceptionListener = this.handleUncaughtException.bind(this);
process.on('unhandledRejection', this.unhandledRejectionListener);
process.on('uncaughtException', this.uncaughtExceptionListener);
}
detachGlobalErrorHandlers() {
if (this.unhandledRejectionListener) {
process.off('unhandledRejection', this.unhandledRejectionListener);
this.unhandledRejectionListener = undefined;
this.logger.info('Detached unhandledRejection listener.');
}
if (this.uncaughtExceptionListener) {
process.off('uncaughtException', this.uncaughtExceptionListener);
this.uncaughtExceptionListener = undefined;
this.logger.info('Detached uncaughtException listener.');
}
}
handleUnhandledRejection(error, _promise) {
// Check for BullMQ flow control errors (missing awaits)
if (isControlError(error)) {
this.logger.error({ error, errorName: error.name, errorMessage: error.message }, `Likely missing await detected: ${error.name} was thrown but not caught. This is usually caused by not awaiting a promise that throws flow control errors.`);
}
else {
this.logger.error({ error }, 'Unhandled Promise Rejection detected by TaskServer');
}
}
handleUncaughtException(error, _origin) {
this.logger.error({ err: error }, 'Uncaught Exception detected by TaskServer');
// Optional: Exit strategy (often recommended for uncaught exceptions)
// process.exit(1);
}
}
//# sourceMappingURL=server.js.map