Job processing microservices using bull - node.js

I would like to process scheduled jobs using node.js bull. Basically I have two processors that handle 2 types of jobs. There is one configurator that configures the jobs which will be added to the bull queue using cron.
The scheduler will be in one microservice and the each of the processor will be a separate microservice. So I will be having 3 micro services.
My question is am I using the correct pattern with bull?
index.js
const Queue = require('bull');
const fetchQueue = new Queue('MyScheduler');
fetchQueue.add("fetcher", {name: "earthQuakeAlert"}, {repeat: {cron: '1-59/2 * * * *'}, removeOnComplete: true});
fetchQueue.add("fetcher", {name: "weatherAlert"}, {repeat: {cron: '3-59/3 * * * *'}, removeOnComplete: true});
processor-configurator.js
const Queue=require('bull');
const scheduler = new Queue("MyScheduler");
scheduler.process("processor", __dirname + "/alert-processor");
fetcher-configurator.js
const Queue=require('bull');
const scheduler = new Queue("MyScheduler");
scheduler.process("fetcher", __dirname+"/fetcher");
fetcher.js
const Queue = require('bull');
const moment = require('moment');
module.exports = function (job) {
const scheduler = new Queue('MyScheduler');
console.log("Insider processor ", job.data, moment().format("YYYY-MM-DD hh:mm:ss"));
scheduler.add('processor', {'name': 'Email needs to be sent'}, {removeOnComplete: true});
return Promise.resolve()
};
alert-processor.js
const Queue = require('bull');
const moment = require('moment');
module.exports = function (job) {
const scheduler = new Queue('MyScheduler');
console.log("Insider processor ", job.data, moment().format("YYYY-MM-DD hh:mm:ss"));
scheduler.add('processor', {'name': 'Email needs to be sent'}, {removeOnComplete: true});
return Promise.resolve()
};
There will be three microservices -
node index.js
node fetcher-configurator.js
node processor-configurator.js
I see inconsistent behavior from bull. Sometimes I am getting the error Missing process handler for job type

Quoting myself with a hope this will be helpful for someone else
This is because both workers use the same queue. Worker tries to get next job from queue, receives a job with wrong type (eg "fetcher" instead of "processor") and fails because it knows how to handle "processor" and doesn't know what to do with "fetcher". Bull doesn't allow you to take only compatible jobs from queue, both workers should be able to process all types of jobs. The simplest solution would be to use two different queues, one for processors and one for fetchers. Then you can remove names from jobs and processors, it won't be needed anymore since name is defined by the queue.
https://github.com/OptimalBits/bull/issues/1481

The Bull:
expiration-queue.js
import Queue from 'bull';
import { ExpirationCompletePublisher } from '../events/publishers/expiration-complete-publisher';
import { natsWrapper } from '../nats-wrapper';
interface Payload {
orderId: string;
}
const expirationQueue = new Queue<Payload>('order:expiration', {
redis: {
host: process.env.REDIS_HOST,
},
});
expirationQueue.process(async (job) => {
console.log('Expiries order id', job.data.orderId);
new ExpirationCompletePublisher(natsWrapper.client).publish({
orderId: job.data.orderId,
});
});
export { expirationQueue };
promotionEndQueue.js
import Queue from 'bull';
import { PromotionEndedPublisher } from '../events/publishers/promotion-ended-publisher';
import { natsWrapper } from '../nats-wrapper';
interface Payload {
promotionId: string;
}
const promotionEndQueue = new Queue<Payload>('promotions:end', {
redis: {
host: process.env.REDIS_HOST, // look at expiration-depl.yaml
},
});
promotionEndQueue.process(async (job) => {
console.log('Expiries promotion id', job.data.promotionId);
new PromotionEndedPublisher(natsWrapper.client).publish({
promotionId: job.data.promotionId,
});
});
export { promotionEndQueue };
order-created-listener.js
import { Listener, OrderCreatedEvent, Subjects } from '#your-lib/common';
import { queueGroupName } from './queue-group-name';
import { Message } from 'node-nats-streaming';
import { expirationQueue } from '../../queues/expiration-queue';
export class OrderCreatedListener extends Listener<OrderCreatedEvent> {
subject: Subjects.OrderCreated = Subjects.OrderCreated;
queueGroupName = queueGroupName;
async onMessage(data: OrderCreatedEvent['data'], msg: Message) {
// delay = expiredTime - currentTime
const delay = new Date(data.expiresAt).getTime() - new Date().getTime();
// console.log("delay", delay)
await expirationQueue.add(
{
orderId: data.id,
},
{
delay,
}
);
msg.ack();
}
}
promotion-started-listener.js
import {
Listener,
PromotionStartedEvent,
Subjects,
} from '#your-lib/common';
import { queueGroupName } from './queue-group-name';
import { Message } from 'node-nats-streaming';
import { promotionEndQueue } from '../../queues/promotions-end-queue';
export class PromotionStartedListener extends Listener<PromotionStartedEvent> {
subject: Subjects.PromotionStarted = Subjects.PromotionStarted;
queueGroupName = queueGroupName;
async onMessage(data: PromotionStartedEvent['data'], msg: Message) {
// delay = expiredTime - currentTime
const delay = new Date(data.endTime).getTime() - new Date().getTime();
// console.log("delay", delay)
await promotionEndQueue.add(
{
promotionId: data.id,
},
{
delay,
}
);
msg.ack();
}
}

Related

How test listener on my custom event emitter in node typescript

I'm trying to test a service that has a listener of the a custom Event Emitter in node with typescript and mocha, sinon.
My custom emmiter;
class PublishEmitter extends EventEmitter {
publish(id: string) {
this.emit('publish', id);
}
}
My service use case:
export default class PublishVehicle {
constructor(
private findVehicle: FindVehicle, // Service that contains find methods on repository
private updateVehicle: UpdateVehicle, // Service that contains update methods on repository
private logger: ILogger,
) {
this.producer = producer;
this.logger = logger;
}
listen() {
this.logger.log('debug', 'Creating listener on PublishEmitter');
this.publishListener = this.publishListener.bind(this);
pubsub.on('publish', this.publishListener);
}
/**
* Listener on PublishEmitter.
*
* #param event
*/
async publishListener(event: string) {
try {
const vehicle = await this.findVehicle.findById(event);
if (vehicle?.state === State.PENDING_PUBLISH) {
//
const input = { state: State.PUBLISH };
await this.updateVehicle.update(vehicle.id, input);
this.logger.log('debug', `Message sent at ${Date.now() - now} ms`);
}
this.logger.log('debug', `End Vehicle's Publish Event: ${event}`);
} catch (error) {
this.logger.log('error', {
message: `publishListener: ${event}`,
stackTrace: error,
});
}
}
}
and in my test file:
import chai from 'chai';
const { expect } = chai;
import sinon from 'sinon';
import { StubbedInstance, stubInterface } from 'ts-sinon';
import pubsub from './PublishEmitter';
describe('Use Case - Publish Vehicle', function () {
let mockRepository: MockVehicleRepository;
let publishVehicle: PublishVehicle;
let findVehicleUseCase: FindVehicle;
let updateVehicleUseCase: UpdateVehicle;
before(() => {
const logger = Logger.getInstance();
mockRepository = new MockVehicleRepository();
findVehicleUseCase = new FindVehicle(mockRepository, logger);
updateVehicleUseCase = new UpdateVehicle(mockRepository);
publishVehicle = new PublishVehicle(
findVehicleUseCase,
updateVehicleUseCase,
logger,
);
});
afterEach(() => {
// Restore the default sandbox here
sinon.restore();
});
it('Should emit event to publish vehicle', async () => {
const vehicle = { ... }; // dummy data
const stubFindById = sinon
.stub(mockRepository, 'findById')
.returns(Promise.resolve(vehicle));
const stubUpdate = sinon
.stub(mockRepository, 'update')
.returns(Promise.resolve(vehicle));
const spy = sinon.spy(publishVehicle, 'publishListener');
publishVehicle.listen();
pubsub.publish(vehicle.id);
expect(spy.calledOnce).to.be.true; // OK
expect(stubFindById.calledOnce).to.be.true; // Error (0 call)
expect(stubUpdate.calledOnce).to.be.true; // Error (0 call)
});
});
When I debug this test, indeed the methods are called but they seem to be executed after it has gone through the last expect lines.
The output:
1 failing
1) Use Case - Publish Vehicle
Should emit event to publish vehicle:
AssertionError: expected false to be true
+ expected - actual
-false
+true
UPDATE
Finally I was be able to solve my problem wrapping expect lines in setTimeout.
setTimeout(() => {
expect(spy.calledOnce).to.be.true; // OK
expect(stubFindById.calledOnce).to.be.true; // OK
expect(stubUpdate.calledOnce).to.be.true; // OK
done();
}, 0);

Bull queue is getting added but never completed

I'm working on an express app that uses several Bull queues in production. We created a wrapper around BullQueue (I added a stripped down version of it down below)
import logger from '~/libs/logger'
import BullQueue from 'bull'
import Redis from '~/libs/redis'
import {report} from '~/libs/sentry'
import {ValidationError, RetryError} from '~/libs/errors'
export default class Queue {
constructor(opts={}) {
if (!opts.label) {
throw new ValidationError('Cannot create queue without label')
}
if (!this.handler) {
throw new ValidationError(`Cannot create queue ${opts.label} without handler`)
}
this.label = opts.label
this.jobOpts = Object.assign({
attempts: 3,
backoff: {
type: 'exponential',
delay: 10000,
},
concurrency: 1,
// clean up jobs on completion - prevents redis slowly filling
removeOnComplete: true
}, opts.jobOpts)
const queueOpts = Object.assign({
createClient: function (type) {
switch (type) {
case 'client':
return client
case 'subscriber':
return subscriber
default:
return new Redis().client
}
}
}, opts.queueOpts)
this.queue = new BullQueue(this.label, queueOpts)
if (process.env.NODE_ENV === 'test') {
return
}
logger.info(`Queue:${this.label} created`)
}
add(data, opts={}) {
const jobOpts = Object.assign(this.jobOpts, opts)
return this.queue.add(data, jobOpts)
}
}
Then I created a queue that is supposed to send a GET request using node-fetch
import Queue from '~/libs/queue'
import Sentry from '~/libs/sentry'
import fetch from 'node-fetch'
class IndexNowQueue extends Queue {
constructor(options = {}) {
super({
label: 'documents--index-now'
})
}
async handler(job) {
Sentry.addBreadcrumb({category: 'async'})
const {permalink} = job.data
const res = await fetch(`https://www.bing.com/indexnow?url=${permalink}&key=${process.env.INDEX_NOW_KEY}`)
if (res.status === 200) {
return
}
throw new Error(`Failed to submit url '${permalink}' to IndexNow. Status: ${res.status} ${await res.text()}`)
}
}
export default new IndexNowQueue()
And then this Queue is being added in the relevant endpoint
indexNowQueue.add({permalink: document.permalink})
In the logs I can see that the Queue is added, however, unlike the other queues (for instance aggregate-feeds) it never moves forward
No error is thrown and any debugger breakpoint I added in there never gets reached. I also tried isolating the handler function outside of the queue and it works as I would expect. What could be causing this issue? What other ways do I have to debug Bull?
It's worth mentioning that there are half a dozen queues in the projects and they are all working as expected.

Node - async function inside an imported module

I have a Node 14 server which is initialized like this:
import express, { Express } from 'express';
import kafkaConsumer from './modules/kafkaConsumer';
async function bootstrap(): Promise<Express> {
kafkaConsumer();
const app = express();
app.get('/health', (_req, res) => {
res.send('ok');
});
return app;
}
export default bootstrap;
kafkaConsumer code:
import logger from './logger.utils';
import KafkaConnector from '../connectors/kafkaConnector';
// singleton
const connectorInstance: KafkaConnector = new KafkaConnector('kafka endpoints', 'consumer group name');
// creating consumer and producer outside of main function in order to not initialize a new consumer producer per each new call.
(async () => {
await connectorInstance.createConsumer('consumer group name');
})();
const kafkaConsumer = async (): Promise<void> => {
const kafkaConsumer = connectorInstance.getConsumer();
await kafkaConsumer.connect();
await kafkaConsumer.subscribe({ topic: 'topic1', fromBeginning: true });
await kafkaConsumer.run({
autoCommit: false, // cancel auto commit in order to control committing
eachMessage: async ({ topic, partition, message }) => {
const messageContent = message.value ? message.value.toString() : '';
logger.info('received message', {
partition,
offset: message.offset,
value: messageContent,
topic
});
// commit message once finished all processing
await kafkaConsumer.commitOffsets([ { topic, partition, offset: message.offset } ]);
}
});
};
export default kafkaConsumer;
You can see that in the kafkaConsumer module there's an async function which is called at the begging to initialize the consumer instance.
How can I guarantee that it successfully passed when importing the module?
In addition, when importing the module, does this mean that the kafkaConsumer default function, is automatically called? won't it cause the server to be essentially stuck at startup?
Would appreciate some guidance here, thanks in advance.
Twicked the Kafka initalzation, and tested with local Kafka. Everything works as expected.

Nodejs Mongoose 'Operation `XXX.find()` buffering timed out after 10000ms'

index.ts is the entry point of this NodeJS program.
This is the code in index.ts:
import JobWorker from "./worker";
import { SwitchPlan } from "./jobs";
const worker = new JobWorker();
worker.addJob(SwitchPlan);
This is worker.ts:
import { CronJob } from "cron";
import mongoose from "mongoose";
import Config from "./config";
import logger from "./logger";
export default class JobWorker {
private jobs: CronJob[];
private config: {
NAME: string;
MONGO_URL: string;
};
constructor() {
this.config = Config;
this.connectDB();
this.jobs = [];
}
public async connectDB(): Promise<void> {
try {
await mongoose.connect(this.config.MONGO_URL,
{ useUnifiedTopology: true, useNewUrlParser: true, useCreateIndex: true },
);
logger.info("\nMONGODB has been connected\n");
} catch(err) {
logger.error("ERROR occurred while connecting to the database");
}
}
addJob(cronJob: CronJob) {
this.jobs.push(cronJob);
}
}
This is jobs.ts:
import moment from "moment";
import {
DatabaseOperations, Vehicle,
Plan1Doc, Plan1, VehicleDoc
} from "common-lib";
import logger from "../logger";
import { CronJob } from "cron";
const vehicleOps = new DatabaseOperations(Vehicle);
const SwitchPlan = new CronJob("25 * * * * *", async (): Promise<void> => {
const date: Date = moment(new Date()).startOf("date").toDate();
const expiringVehicles: VehicleDoc[] = vehicleOps.getAllDocuments(
{ "inspection.startTime": {
"$gte": date, "$lte": moment(date).startOf("date").add(1, "day").toDate()
}
},
{}, { pageNo: 0, limit: 0 }
).then((result: any) => {
logger.info("dsada");
}).catch((err: any) => {
logger.info("ssd");
});
});
SwitchPlan.start();
export { SwitchPlan };
I have omitted parts of code which are irrelevant to this problem. I ran this code through a debugger and there's no issue with the config. MonggoDB connected is getting printed at the start of the program. However the then block after getAllDocuments in jobs.ts is never reached and it always goes in the error block with the message, Operation vehicleinventories.find() buffering timed out after 10000ms. The getAllDocuments uses MongoDB's find() method and is working correctly because I am using this method in other projects where I have no such issues.
So far I have tried, deleting Mongoose from node_modules and reinstalling, tried connecting to MongoDB running on localhost, but the issue remains unsolved.
EDIT: DatabaseOperations class:
import { Model, Schema } from "mongoose";
class DatabaseOperations {
private dbModel: Model<any>;
constructor(dbModel: Model<any>) {
this.dbModel = dbModel;
}
getAllDocuments(
query: any,
projections: any,
options: { pageNo: number; limit: number },
sort?: any
): any {
const offset = options.limit * options.pageNo;
return this.dbModel
.find(query, projections)
.skip(offset)
.limit(options.limit)
.sort(sort ? sort : { createdAt: -1 })
.lean();
}
}
in your jobs.ts file you have the following line
SwitchToTier1Plan.start();
This line is called the moment you required the class file, hence before mongoose is connected, and all the models defined. Could this be the issue?
Another thing I noted is u are using mongoose.connect which may be wrong since mongoose.connect creates a global connection.
which means each new Worker you will be attempting to override the mongoose property with previous connection
Though i'm not sure what the implication is, but it could be because your .find could be using the old connection.
Since you are writing class, I would recommend using mongoose.createConnection which creates a new connection for each class initiation.

Why isn't this EventEmitter pubsub singleton interface working in node.js?

I am trying to separate publisher and subscriber in node.js to be able to send data to each other through a shared EventEmitter instance as bus.
My bus follows the singleton method discussed [HERE][1]
bus.js file
// https://derickbailey.com/2016/03/09/creating-a-true-singleton-in-node-js-with-es6-symbols/
// create a unique, global symbol name
// -----------------------------------
const FOO_KEY = Symbol.for("test.exchanges.bus");
const EventEmitter = require("events");
// check if the global object has this symbol
// add it if it does not have the symbol, yet
// ------------------------------------------
var globalSymbols = Object.getOwnPropertySymbols(global);
var hasFoo = (globalSymbols.indexOf(FOO_KEY) > -1);
if (!hasFoo){
global[FOO_KEY] = {
foo: new EventEmitter()
};
}
// define the singleton API
// ------------------------
var singleton = {};
Object.defineProperty(singleton, "instance", {
get: function(){
return global[FOO_KEY];
}
});
// ensure the API is never changed
// -------------------------------
Object.freeze(singleton);
// export the singleton API only
// -----------------------------
module.exports = singleton;
My understanding is that when I require this file in different modules, the same foo Object should be made available. Isn't that the purpose of having a singleton?
pub.js file
const bus = require("./bus");
class Publisher {
constructor(emitter) {
this.emitter = emitter;
console.log(this.emitter);
this.test();
}
test() {
setInterval(() => {
this.emitter.emit("test", Date.now());
}, 1000);
}
}
module.exports = Publisher;
console.log(bus.instance.foo);
sub.js file
const bus = require("./bus");
class Subscriber {
constructor(emitter) {
this.emitter = emitter;
console.log(this.emitter);
this.emitter.on("test", this.handleTest);
}
handleTest(data) {
console.log("handling test", data);
}
}
module.exports = Subscriber;
console.log(bus.instance.foo);
When I run pub.js and sub.js on 2 separate terminal windows, sub.js finished executing immediately as if publisher is not pushing the messages to it. Could anyone kindly point how to separate the publisher and subscriber to work with the same event bus?
You may consider re-designing your bus module. I recommend creating it as a class which extends EventEmitter and then returning an instantiated instance of this class.
Now when this file is loaded the first time, the class code will run and an object will be instantiated and then exported back. require will cache this instance and next time when this file is loaded, it will get the same object back. this makes it a singleton and you can now use this as a common bus.
here is some code to demonstrate this point:
bus.js
const {EventEmitter} = require('events');
class Bus extends EventEmitter {
constructor(){
super();
setTimeout(() => this.emit('foo', (new Date()).getTime()), 1000);
}
}
module.exports = new Bus();
bus.spec.js
const bus1 = require('../src/util/bus');
const bus2 = require('../src/util/bus');
describe('bus', () => {
it('is the same object', () => {
expect(bus1).toEqual(bus2);
expect(bus1 === bus2).toEqual(true);
});
it('calls the event handler on bus2 when event is emitted on bus1', done => {
bus2.on('chocolate', flavor => {
expect(flavor).toBe('dark');
done()
});
bus1.emit('chocolate', 'dark');
}, 20)
});

Resources