fixed queue close

This commit is contained in:
Boki 2025-06-20 17:13:10 -04:00
parent c048e00d7f
commit dbfa80b2a2
2 changed files with 84 additions and 121 deletions

View file

@ -1,4 +1,4 @@
import { Queue as BullQueue, Worker, QueueEvents, type Job } from 'bullmq';
import { Queue as BullQueue, QueueEvents, Worker, type Job } from 'bullmq';
import { getLogger } from '@stock-bot/logger';
import { handlerRegistry } from './handler-registry';
import type { JobData, JobOptions, QueueStats, RedisConfig } from './types';
@ -24,14 +24,14 @@ export class Queue {
private redisConfig: RedisConfig;
constructor(
queueName: string,
redisConfig: RedisConfig,
queueName: string,
redisConfig: RedisConfig,
defaultJobOptions: JobOptions = {},
config: QueueWorkerConfig = {}
) {
this.queueName = queueName;
this.redisConfig = redisConfig;
const connection = getRedisConnection(redisConfig);
// Initialize BullMQ queue
@ -59,10 +59,10 @@ export class Queue {
this.startWorkers(config.workers, config.concurrency || 1);
}
logger.trace('Queue created', {
queueName,
logger.trace('Queue created', {
queueName,
workers: config.workers || 0,
concurrency: config.concurrency || 1
concurrency: config.concurrency || 1,
});
}
@ -84,12 +84,10 @@ export class Queue {
/**
* Add multiple jobs to the queue in bulk
*/
async addBulk(
jobs: Array<{ name: string; data: JobData; opts?: JobOptions }>
): Promise<Job[]> {
logger.trace('Adding bulk jobs', {
queueName: this.queueName,
jobCount: jobs.length
async addBulk(jobs: Array<{ name: string; data: JobData; opts?: JobOptions }>): Promise<Job[]> {
logger.trace('Adding bulk jobs', {
queueName: this.queueName,
jobCount: jobs.length,
});
return await this.bullQueue.addBulk(jobs);
}
@ -98,9 +96,9 @@ export class Queue {
* Add a scheduled job with cron-like pattern
*/
async addScheduledJob(
name: string,
data: JobData,
cronPattern: string,
name: string,
data: JobData,
cronPattern: string,
options: JobOptions = {}
): Promise<Job> {
const scheduledOptions: JobOptions = {
@ -112,15 +110,15 @@ export class Queue {
...options.repeat,
},
};
logger.info('Adding scheduled job', {
queueName: this.queueName,
jobName: name,
logger.info('Adding scheduled job', {
queueName: this.queueName,
jobName: name,
cronPattern,
repeatKey: scheduledOptions.repeat?.key,
immediately: scheduledOptions.repeat?.immediately
immediately: scheduledOptions.repeat?.immediately,
});
return await this.bullQueue.add(name, data, scheduledOptions);
}
@ -195,8 +193,8 @@ export class Queue {
* Clean completed and failed jobs
*/
async clean(
grace: number = 0,
limit: number = 100,
grace: number = 0,
limit: number = 100,
type: 'completed' | 'failed' = 'completed'
): Promise<void> {
await this.bullQueue.clean(grace, limit, type);
@ -210,62 +208,30 @@ export class Queue {
await this.bullQueue.waitUntilReady();
}
/**
* Close the queue (cleanup resources)
*/
/**
* Close the queue (cleanup resources)
*/
async close(): Promise<void> {
try {
// Close workers first with timeout
if (this.workers.length > 0) {
const workerClosePromises = this.workers.map((worker) => {
return new Promise<void>((resolve) => {
const timeout = setTimeout(() => {
resolve();
}, 50);
worker.close().then(() => {
clearTimeout(timeout);
resolve();
}).catch(() => {
clearTimeout(timeout);
resolve();
});
});
});
await Promise.all(workerClosePromises);
this.workers = [];
logger.debug('Workers closed', { queueName: this.queueName });
}
// Close the queue itself
await this.bullQueue.close();
logger.info('Queue closed', { queueName: this.queueName });
// Close queue events with timeout
// Close queue events
if (this.queueEvents) {
const eventsClosePromise = this.queueEvents.close();
const eventsTimeoutPromise = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('Queue events close timeout')), 50)
);
try {
await Promise.race([eventsClosePromise, eventsTimeoutPromise]);
} catch (error) {
// Silently ignore timeout
}
await this.queueEvents.close();
logger.debug('Queue events closed', { queueName: this.queueName });
}
// Close the queue itself with timeout
const queueClosePromise = this.bullQueue.close();
const queueTimeoutPromise = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('BullQueue close timeout')), 50)
);
try {
await Promise.race([queueClosePromise, queueTimeoutPromise]);
} catch (error) {
// Silently ignore timeout
// Close workers first
if (this.workers.length > 0) {
await Promise.all(this.workers.map(worker => worker.close()));
this.workers = [];
logger.debug('Workers closed', { queueName: this.queueName });
}
logger.info('Queue closed', { queueName: this.queueName });
} catch (error) {
logger.error('Error closing queue', { queueName: this.queueName, error });
throw error;
@ -279,19 +245,15 @@ export class Queue {
const connection = getRedisConnection(this.redisConfig);
for (let i = 0; i < workerCount; i++) {
const worker = new Worker(
`{${this.queueName}}`,
this.processJob.bind(this),
{
connection,
concurrency,
maxStalledCount: 3,
stalledInterval: 30000,
}
);
const worker = new Worker(`{${this.queueName}}`, this.processJob.bind(this), {
connection,
concurrency,
maxStalledCount: 3,
stalledInterval: 30000,
});
// Setup worker event handlers
worker.on('completed', (job) => {
worker.on('completed', job => {
logger.trace('Job completed', {
queueName: this.queueName,
jobId: job.id,
@ -310,7 +272,7 @@ export class Queue {
});
});
worker.on('error', (error) => {
worker.on('error', error => {
logger.error('Worker error', {
queueName: this.queueName,
workerId: i,
@ -385,4 +347,4 @@ export class Queue {
getBullQueue(): BullQueue {
return this.bullQueue;
}
}
}