|
|
@@ -10,13 +10,22 @@ import {
|
|
|
Logger,
|
|
|
PaginatedList,
|
|
|
} from '@vendure/core';
|
|
|
-import Bull, { ConnectionOptions, JobType, Processor, Queue, Worker, WorkerOptions } from 'bullmq';
|
|
|
+import Bull, {
|
|
|
+ ConnectionOptions,
|
|
|
+ JobType,
|
|
|
+ Processor,
|
|
|
+ Queue,
|
|
|
+ Worker,
|
|
|
+ WorkerOptions,
|
|
|
+ Job as BullJob,
|
|
|
+} from 'bullmq';
|
|
|
import { EventEmitter } from 'events';
|
|
|
import { Cluster, Redis, RedisOptions } from 'ioredis';
|
|
|
|
|
|
import { ALL_JOB_TYPES, BULLMQ_PLUGIN_OPTIONS, loggerCtx } from './constants';
|
|
|
import { RedisHealthIndicator } from './redis-health-indicator';
|
|
|
-import { BullMQPluginOptions } from './types';
|
|
|
+import { getJobsByType } from './scripts/get-jobs-by-type';
|
|
|
+import { BullMQPluginOptions, CustomScriptDefinition } from './types';
|
|
|
|
|
|
const QUEUE_NAME = 'vendure-job-queue';
|
|
|
const DEFAULT_CONCURRENCY = 3;
|
|
|
@@ -53,6 +62,8 @@ export class BullMQJobQueueStrategy implements InspectableJobQueueStrategy {
|
|
|
? this.connectionOptions
|
|
|
: new Redis(this.connectionOptions);
|
|
|
|
|
|
+ this.defineCustomLuaScripts();
|
|
|
+
|
|
|
const redisHealthIndicator = injector.get(RedisHealthIndicator);
|
|
|
Logger.info('Checking Redis connection...', loggerCtx);
|
|
|
const health = await redisHealthIndicator.isHealthy('redis');
|
|
|
@@ -137,8 +148,8 @@ export class BullMQJobQueueStrategy implements InspectableJobQueueStrategy {
|
|
|
}
|
|
|
|
|
|
async findMany(options?: JobListOptions): Promise<PaginatedList<Job>> {
|
|
|
- const start = options?.skip ?? 0;
|
|
|
- const end = start + (options?.take ?? 10);
|
|
|
+ const skip = options?.skip ?? 0;
|
|
|
+ const take = options?.take ?? 10;
|
|
|
let jobTypes: JobType[] = ALL_JOB_TYPES;
|
|
|
const stateFilter = options?.filter?.state;
|
|
|
if (stateFilter?.eq) {
|
|
|
@@ -170,26 +181,31 @@ export class BullMQJobQueueStrategy implements InspectableJobQueueStrategy {
|
|
|
? ['completed', 'failed']
|
|
|
: ['wait', 'waiting-children', 'active', 'repeat', 'delayed', 'paused'];
|
|
|
}
|
|
|
+
|
|
|
let items: Bull.Job[] = [];
|
|
|
- let jobCounts: { [index: string]: number } = {};
|
|
|
- try {
|
|
|
- items = await this.queue.getJobs(jobTypes, start, end);
|
|
|
- } catch (e: any) {
|
|
|
- Logger.error(e.message, loggerCtx, e.stack);
|
|
|
- }
|
|
|
+ let totalItems = 0;
|
|
|
+
|
|
|
try {
|
|
|
- jobCounts = await this.queue.getJobCounts(...jobTypes);
|
|
|
+ const [total, jobIds] = await this.callCustomScript(getJobsByType, [
|
|
|
+ skip,
|
|
|
+ take,
|
|
|
+ options?.filter?.queueName?.eq ?? '',
|
|
|
+ ...jobTypes,
|
|
|
+ ]);
|
|
|
+ items = (
|
|
|
+ await Promise.all(
|
|
|
+ jobIds.map(id => {
|
|
|
+ return BullJob.fromId(this.queue, id);
|
|
|
+ }),
|
|
|
+ )
|
|
|
+ ).filter(notNullOrUndefined);
|
|
|
+ totalItems = total;
|
|
|
} catch (e: any) {
|
|
|
- Logger.error(e.message, loggerCtx, e.stack);
|
|
|
+ throw new InternalServerError(e.message);
|
|
|
}
|
|
|
- const totalItems = Object.values(jobCounts).reduce((sum, count) => sum + count, 0);
|
|
|
|
|
|
return {
|
|
|
- items: await Promise.all(
|
|
|
- items
|
|
|
- .sort((a, b) => b.timestamp - a.timestamp)
|
|
|
- .map(bullJob => this.createVendureJob(bullJob)),
|
|
|
- ),
|
|
|
+ items: await Promise.all(items.map(bullJob => this.createVendureJob(bullJob))),
|
|
|
totalItems,
|
|
|
};
|
|
|
}
|
|
|
@@ -252,6 +268,7 @@ export class BullMQJobQueueStrategy implements InspectableJobQueueStrategy {
|
|
|
}
|
|
|
|
|
|
private stopped = false;
|
|
|
+
|
|
|
async stop<Data extends JobData<Data> = object>(
|
|
|
queueName: string,
|
|
|
process: (job: Job<Data>) => Promise<any>,
|
|
|
@@ -308,4 +325,31 @@ export class BullMQJobQueueStrategy implements InspectableJobQueueStrategy {
|
|
|
throw new InternalServerError('Could not determine job state');
|
|
|
// TODO: how to handle "cancelled" state? Currently when we cancel a job, we simply remove all record of it.
|
|
|
}
|
|
|
+
|
|
|
+ private callCustomScript<T, Args extends any[]>(
|
|
|
+ scriptDef: CustomScriptDefinition<T, Args>,
|
|
|
+ args: Args,
|
|
|
+ ): Promise<T> {
|
|
|
+ return new Promise<T>((resolve, reject) => {
|
|
|
+ (this.redisConnection as any)[scriptDef.name](
|
|
|
+ `bull:${this.queue.name}:`,
|
|
|
+ ...args,
|
|
|
+ (err: any, result: any) => {
|
|
|
+ if (err) {
|
|
|
+ reject(err);
|
|
|
+ } else {
|
|
|
+ resolve(result);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ );
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ private defineCustomLuaScripts() {
|
|
|
+ const redis = this.redisConnection;
|
|
|
+ redis.defineCommand(getJobsByType.name, {
|
|
|
+ numberOfKeys: getJobsByType.numberOfKeys,
|
|
|
+ lua: getJobsByType.script,
|
|
|
+ });
|
|
|
+ }
|
|
|
}
|