From 0e51ed8175e4cb74561bfc3788a042084eb98665 Mon Sep 17 00:00:00 2001 From: Brent Hoover Date: Mon, 17 Jun 2024 08:14:03 +0800 Subject: [PATCH] feat: add bullmq workers and queues --- bun.lockb | Bin 198066 -> 206650 bytes package.json | 1 + src/config/config.ts | 4 ++ src/index.ts | 20 ++++++- .../auth-plugin/resolvers/auth-resolver.ts | 3 +- src/plugins/global-context.ts | 2 + src/plugins/plugin-loader.ts | 34 ++++++++++++ src/plugins/sample-plugin/index.ts | 29 +++++++++- .../sample-plugin/services/sample-service.ts | 15 +++++- .../sample-plugin/services/sample-worker.ts | 50 ++++++++++++++++++ src/worker.ts | 31 +++++++++++ 11 files changed, 184 insertions(+), 5 deletions(-) create mode 100644 src/plugins/sample-plugin/services/sample-worker.ts create mode 100644 src/worker.ts diff --git a/bun.lockb b/bun.lockb index 88c0753450aacd0139fad5e4e9451906cae4b42a..b0f6b4764327c534cf662d6693754145fcc5fa06 100755 GIT binary patch delta 32237 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 delta 27316 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 diff --git a/package.json b/package.json index 5b59e95..c41b75a 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@typegoose/typegoose": "^12.5.0", "apollo-server-express": "^3.13.0", "bcrypt": "^5.1.1", + "bullmq": "^5.8.2", "casbin": "^5.30.0", "casbin-mongoose-adapter": "^5.3.1", "dotenv": "^16.4.5", diff --git a/src/config/config.ts b/src/config/config.ts index ee7decd..0c0db54 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -8,6 +8,10 @@ const env = cleanEnv(process.env, { MONGO_URI: str({ desc: 'MongoDB connection string' }), PORT: port({ default: 4000 }), JWT_SECRET: str({ desc: 'Secret key for JWT token' }), + JTW_EXPIRY: str({ default: '1y' }), + MODE: str({ choices: ['server', 'worker', 'dev'], default: 'dev' }), + REDIS_HOST: str({ default: 'localhost' }), + REDIS_PORT: port({ default: 6379 }), }); export default env; diff --git a/src/index.ts b/src/index.ts index 2ebc004..33a19eb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,6 +11,7 @@ import { isIntrospectionQuery } from './utils/introspection-check'; import { shouldBypassAuth } from './utils/should-bypass-auth'; import { bootstrap } from './plugins/auth-plugin/bootstrap'; import sanitizeLog from './sanitize-log'; +import { startWorker } from './worker'; const loggerCtx = { context: 'index' }; @@ -95,4 +96,21 @@ async function startServer() { } } -startServer(); +async function startApp() { + switch (env.MODE) { + case 'server': + await startServer(); + break; + case 'worker': + await startWorker(); + break; + case 'dev': + await startServer(); + await startWorker(); + break; + default: + logger.error('Unknown mode specified. Please set MODE to "server", "worker", or "dev".', loggerCtx); + } +} + +startApp(); diff --git a/src/plugins/auth-plugin/resolvers/auth-resolver.ts b/src/plugins/auth-plugin/resolvers/auth-resolver.ts index 13140ce..f002cba 100644 --- a/src/plugins/auth-plugin/resolvers/auth-resolver.ts +++ b/src/plugins/auth-plugin/resolvers/auth-resolver.ts @@ -6,6 +6,7 @@ import { UserService } from '../services/user-service.ts'; import { Service } from 'typedi'; import jwt from 'jsonwebtoken'; import { getEnforcer } from '../../../rbac'; +import env from '../../../config/config.ts'; @Service() // Register AuthResolver with Typedi @Resolver() @@ -39,7 +40,7 @@ export class AuthResolver { throw new Error('Invalid credentials'); } - const token = jwt.sign({ role: user.role, id: user._id }, process.env.JWT_SECRET!, { expiresIn: '1h' }); + const token = jwt.sign({ role: user.role, id: user._id }, process.env.JWT_SECRET!, { expiresIn: env.JTW_EXPIRY }); return token; } diff --git a/src/plugins/global-context.ts b/src/plugins/global-context.ts index d379fe7..12feaa1 100644 --- a/src/plugins/global-context.ts +++ b/src/plugins/global-context.ts @@ -1,4 +1,5 @@ import { Schema } from 'mongoose'; +import { type WorkerOptions } from 'bullmq'; export type ResolverMap = { [resolverName: string]: Function; @@ -10,4 +11,5 @@ export interface GlobalContext { extendModel: (name: string, extension: (schema: Schema) => void) => void; extendResolvers: (name: string, extension: Function[]) => void; wrapResolver: (name: string, resolver: string, wrapper: Function) => void; + queues: { [key: string]: { processor: (job: any) => Promise; options: WorkerOptions } }; } diff --git a/src/plugins/plugin-loader.ts b/src/plugins/plugin-loader.ts index 0309543..26cf1a6 100644 --- a/src/plugins/plugin-loader.ts +++ b/src/plugins/plugin-loader.ts @@ -8,12 +8,15 @@ import mongoose, { Schema } from 'mongoose'; import { type GlobalContext } from './global-context'; import { type Plugin } from './plugin-interface'; import pluginsList from './plugins-list'; +import { Queue, Worker, QueueEvents, type WorkerOptions } from 'bullmq'; +import env from '../config/config'; const loggerCtx = { context: 'plugin-loader' }; class PluginLoader { private plugins: Plugin[] = []; context: GlobalContext = this.createInitialContext(); + private queues: Record = {}; private createInitialContext(): GlobalContext { return { @@ -22,6 +25,7 @@ class PluginLoader { extendModel: this.extendModel.bind(this), extendResolvers: this.extendResolvers.bind(this), wrapResolver: this.wrapResolver.bind(this), + queues: {} }; } @@ -114,6 +118,36 @@ class PluginLoader { mongoose.model(modelName, this.context.models[modelName].schema); }); } + + initializeQueues(): void { + Object.keys(this.context.queues).forEach((queueName) => { + const { processor, options } = this.context.queues[queueName]; + const queue = new Queue(queueName, options); + const worker = new Worker(queueName, processor, options); + const queueEvents = new QueueEvents(queueName, options); + + queueEvents.on('completed', (job) => { + logger.info(`Job ${job.jobId} in queue ${queueName} completed!`, loggerCtx); + }); + + queueEvents.on('failed', (job, err) => { + const errorMessage = this.extractErrorMessage(err); + logger.error(`Job ${job.jobId} in queue ${queueName} failed with error: ${errorMessage}`, loggerCtx);; + }); + + this.queues[queueName] = queue; + }); + } + + private extractErrorMessage(err: unknown): string { + if (typeof err === 'string') { + return err; + } + if (err instanceof Error) { + return err.message; + } + return JSON.stringify(err); + } } export default PluginLoader; diff --git a/src/plugins/sample-plugin/index.ts b/src/plugins/sample-plugin/index.ts index 2407abc..494049f 100644 --- a/src/plugins/sample-plugin/index.ts +++ b/src/plugins/sample-plugin/index.ts @@ -5,6 +5,12 @@ import { Sample } from './models/sample'; import { SampleResolver } from './resolvers/sample-resolver'; import { SampleService } from './services/sample-service'; import KafkaEventService from '../../event/kafka-event-service'; +import { Queue, Job } from 'bullmq'; + +const sampleJobProcessor = async (job: Job) => { + console.log(`Processing job ${job.id}`); + // Add job processing logic here +}; export default { name: 'sample-plugin', @@ -15,8 +21,19 @@ export default { context.models['Sample'] = { schema: SampleModel.schema, model: SampleModel }; container.set('SampleModel', SampleModel); + // Define and register the queue for this plugin + const sampleQueue = new Queue('sampleQueue', { + connection: { + host: process.env.REDIS_HOST || 'localhost', + port: Number(process.env.REDIS_PORT) || 6379, + }, + }); + + // Register sampleQueue in the container + container.set('sampleQueue', sampleQueue); + // Register SampleService and KafkaEventService with typedi - container.set(SampleService, new SampleService(Container.get(KafkaEventService))); + container.set(SampleService, new SampleService(Container.get(KafkaEventService), sampleQueue)); // Ensure SampleResolver is added to context resolvers context.resolvers['Sample'] = [SampleResolver]; @@ -30,5 +47,15 @@ export default { console.log('Sample created:', sample); // Additional handling logic here }); + + context.queues['sampleQueue'] = { + processor: sampleJobProcessor, + options: { + connection: { + host: process.env.REDIS_HOST || 'localhost', + port: Number(process.env.REDIS_PORT) || 6379, + }, + }, + }; }, }; diff --git a/src/plugins/sample-plugin/services/sample-service.ts b/src/plugins/sample-plugin/services/sample-service.ts index f147dd1..a0ecd62 100644 --- a/src/plugins/sample-plugin/services/sample-service.ts +++ b/src/plugins/sample-plugin/services/sample-service.ts @@ -1,11 +1,14 @@ -// src/services/sample-service.ts import { Service } from 'typedi'; import { Sample, SampleModel } from '../models/sample'; import KafkaEventService from '../../../event/kafka-event-service'; +import { Queue } from 'bullmq'; @Service() export class SampleService { - constructor(private eventService: KafkaEventService) {} + constructor( + private eventService: KafkaEventService, + private sampleQueue: Queue + ) {} async getAllSamples(): Promise { return SampleModel.find().exec(); @@ -15,6 +18,14 @@ export class SampleService { const sample = new SampleModel({ name }); const savedSample = await sample.save(); await this.eventService.emitEvent('sampleCreated', savedSample); // Emit event using the centralized service + + // Add job to the sampleQueue + await this.sampleQueue.add('processSample', { sampleId: savedSample.id }); + return savedSample; } + + async getSampleById(id: string): Promise { + return SampleModel.findById(id).exec(); + } } diff --git a/src/plugins/sample-plugin/services/sample-worker.ts b/src/plugins/sample-plugin/services/sample-worker.ts new file mode 100644 index 0000000..cf3684a --- /dev/null +++ b/src/plugins/sample-plugin/services/sample-worker.ts @@ -0,0 +1,50 @@ +import { Worker, Job } from 'bullmq'; +import { Container } from 'typedi'; +import env from '../../../config/config'; +import logger from '../../../config/logger'; +import { SampleService } from './sample-service'; + +const loggerCtx = { context: 'sample-worker' }; + +const sampleWorker = new Worker( + 'sampleQueue', + async (job: Job | undefined) => { + if (!job) { + logger.error('Received an undefined job', loggerCtx); + return; + } + + const sampleService = Container.get(SampleService); + const sampleId = job.data.sampleId; + + // Process the sample + const sample = await sampleService.getSampleById(sampleId); + logger.info(`Processing sample: ${sample?.name}`); + // Add your processing logic here + }, + { + connection: { + host: env.REDIS_HOST, + port: env.REDIS_PORT, + }, + } +); + +sampleWorker.on('completed', (job: Job | undefined) => { + if (job) { + logger.info(`Job ${job.id} in sampleQueue completed!`, loggerCtx); + } else { + logger.error('Completed job is undefined', loggerCtx); + } +}); + +sampleWorker.on('failed', (job: Job | undefined, err: unknown) => { + const errorMessage = typeof err === 'string' ? err : (err instanceof Error ? err.message : JSON.stringify(err)); + if (job) { + logger.error(`Job ${job.id} in sampleQueue failed with error: ${errorMessage}`, loggerCtx); + } else { + logger.error(`A job failed with error: ${errorMessage}`, loggerCtx); + } +}); + +export default sampleWorker; diff --git a/src/worker.ts b/src/worker.ts new file mode 100644 index 0000000..9a03132 --- /dev/null +++ b/src/worker.ts @@ -0,0 +1,31 @@ +import { connectToDatabase } from './config/database'; +import logger from './config/logger'; +import { initEnforcer } from './rbac'; +import { bootstrap } from './plugins/auth-plugin/bootstrap'; +import PluginLoader from './plugins/plugin-loader'; + +const loggerCtx = { context: 'worker' }; + +export async function startWorker() { + try { + await connectToDatabase(); + await initEnforcer(); // Initialize Casbin + await bootstrap(); // Bootstrap the application with a superuser + + const pluginLoader = new PluginLoader(); + pluginLoader.loadPlugins(); + + // Register models before initializing plugins + pluginLoader.registerModels(); + + // Initialize plugins (extend models and resolvers) + pluginLoader.initializePlugins(); + + // Initialize queues + pluginLoader.initializeQueues(); + + logger.info('Worker started and ready to process jobs', loggerCtx); + } catch (error) { + logger.error('Failed to start worker:', error, loggerCtx); + } +}