Node.js Job Scheduling with Agenda

December 19, 2018 • node, javascript, agenda.js

I have a freelance project I’ve been working on that needed to be setup to allow tasks to be retried automatically. The application depends upon an integration between two 3rd-party systems, and those systems only sync up at certain intervals. If one system didn’t have the information I needed to wait until it did. So I started looking at different scheduling and queueing systems to see if something already existed that would work for my situation. I came across Agenda.js, and it seemed to fit the bill well. I already had a MongoDB in place for my client and this wouldn’t require me using any new services.

I installed the npm package as usual and then went about setting everything up. I wanted to keep as much of the code for agenda and the tasks separate as possible so I created a new directory for all the code, in my case server/lib/processingQueue.

I added an index.js file where I would do most of the setup. I don’t expect to have many items using agenda for the moment so I turned the concurrency way down.

const Agenda = require('agenda');

const agenda = new Agenda({
    db: { address: process.env.MONGODB_URI },
    maxConcurrency: 5,
    defaultConcurrency: 1
});

agenda.on('ready', function () {
    agenda.define('myJobName', function(job, done) {
        // ...
        done();
    });

    agenda.start();
});

module.exports = {
    agenda,
}

This was a good start, but I didn’t want to clutter the ready event listener with function definitions when I added more tasks in the future. So I created another file in the server/lib/processingQueue directory and gave it the same name as the job. This is where I would define the job and have all the logic.

const { tryDoTheThing } = require('../myLibraryFile');
const { db } = require('../db');
const JOB_NAME = 'myJobName';

function defineJob(agenda) {
    console.log(`Defining ${JOB_NAME} job`);
    agenda.define(JOB_NAME, jobFunction);
}

async function jobFunction(job, done) {
    let { itemId } = job.attrs.data;
    if (!itemId) {
        return done();
    }
    let item = db.findById(itemId);

    let success = await tryDoTheThing(item);

    if (!success) {
        throw new Error(`Failed to do the thing in myJobName job, itemId ${itemId}`);
    }
    done(success);
}


function scheduleJob(agenda, itemId) {
    var job = agenda.create(JOB_NAME, { itemId });
    job.schedule('in 1 hour');
    job.unique({'data.itemId': 'itemId'});
    job.save(function (err) {
        if(err) {
            console.error(`Error saving job for itemId ${itemId}`, err);
        }
        console.log(`Job successfully saved for itemId ${itemId}`);
    });
}

module.exports = {
    JOB_NAME,
    defineJob,
    scheduleJob,
};

After that I went back to my main file for Agenda and updated it to use the new module.

const Agenda = require('agenda');
const myJobName = require('./myJobName');

const agenda = new Agenda({
    db: { address: process.env.MONGODB_URI },
    maxConcurrency: 5,
    defaultConcurrency: 1
});

agenda.on('ready', function () {
    myJobName.defineJob(agenda);

    agenda.start();
});

module.exports = {
    agenda,
    scheduleMyJobName: async (itemId) => myJobName.scheduleJob(agenda, itemId),
}

Note the exports at the bottom. I’m exporting an arrow function that calls myJobName.scheduleJob. I did this so that no matter where I was I’d only have to import one file to get the Agenda tasks. This works out very nicely because it also meant that if I forgot to import the application in my main JavaScript file it would still start Agenda anywhere. Importing Agenda in multiple places won’t be a problem like it might seem at first because Node only evaluates the file once, then it just passes the same reference after that first import.


Jason Butz's profile picture

Jason Butz is a software engineer and cloud architect with a strong focus on JavaScript, TypeScript, Node, and AWS. Jason has a love for open-soure software and a passion for building scalable, secure, and reliable applications.