Managing multiple cron jobs in node.js project

ose4g

Osemudiamen Itua

Posted on April 11, 2022

Managing multiple cron jobs in node.js project

What is a cron job?

A cron job is some piece of code or action that you want to take at specific intervals or specific times. Some examples could be

  • Checking your app once everyday for users whose birthday is that day and sending them emails.
  • Taking statistics/metrics of your app on the 1st of every month and storing it to the database or sending the data to an admin.

Basically any action you want to take at various intervals is called a cron job

Cron jobs in node.js using typescript.

Multiple packages are available to create cron jobs in Node.js, but we'll work with node-cron.

import cron from 'node-cron'

const cronExpression = '* * * * * *';

function action(){
    console.log('This cron job will run every second')
}

const job = cron.schedule(cronExpression, action, {scheduled:false})

job.start()//starts the job

//job.stop() //stops the job
Enter fullscreen mode Exit fullscreen mode

The cronExpression describes how often the action should be called. Check here for a description of cronExpression.

The Problem

There might be times when we want to pause or restart a cron job. Then we need to find a way to manage it. Say in our express server we wanted a way to start or stop a cron-job. How would we do it?

import cron from 'node-cron'
import express, { Request, Response } from 'express'

const cronExpression = '* * * * * *';

function action(){
    console.log('This cron job will run every second')
}

const job = cron.schedule(cronExpression, action, {scheduled:false})

const app = express()
app.use(express.json())
app.use(express.urlencoded({ extended: true }))

app.post('/start-job',(req:Request,res:Response)=>{
    job.start();
    res.status(200).json({message:'job started successfully'})
})

app.post('/stop-job',(req:Request,res:Response)=>{
    job.stop();
    res.status(200).json({message:'job stopped successfully'})
})

Enter fullscreen mode Exit fullscreen mode

We could do it this way when working with just a few jobs.
But say we're working with many jobs, and we don't want to create an endpoint for every one of them.
An approach would be to associate each job with a unique key, and we could map the key to the job.
See the implementation below.

import cron from 'node-cron'
import express, { Request, Response } from 'express'

//hash map to map keys to jobs
const jobMap: Map<string, cron.ScheduledTask> = new Map();

//jobs
const metricsJob = cron.schedule('0 0 0 1 * *',()=>{
    console.log('There are 5 users in the application')
}, {scheduled:false})

const birthdayJob = cron.schedule('0 0 0 * * *',()=>{
    console.log('20 users have their birthday today')
}, {scheduled:false})


//set the key to map to the job
jobMap.set('metrics',metricsJob)
jobMap.set('birthday',birthdayJob)


//express api and simple routes
const app = express()
app.use(express.json())
app.use(express.urlencoded({ extended: true }))


app.post('/start-job',(req:Request,res:Response)=>{
    const {jobName} = req.body
    const job = jobMap.get(jobName)

    if(!job) return res.status(400).json({message: 'invalid job name'})
    else job.start()
    res.status(200).json({message:`job ${jobName} started successfully`})
})

app.post('/stop-job',(req:Request,res:Response)=>{
    const {jobName} = req.body
    const job = jobMap.get(jobName)

    if(!job) return res.status(400).json({message: 'invalid job name'})
    else job.start()
    res.status(200).json({message:`job ${jobName} stoppeed successfully`})
})
Enter fullscreen mode Exit fullscreen mode

It works right. All well and good. Another use case would be wanting to start or stop related jobs.
Say you had two jobs related to authentication and another two jobs related to application metrics. You would want a way to start or stop all jobs under auth or metrics without having to start each job individually.
You could use another map to achieve this too.

See the implementation below.

import cron from 'node-cron'
import express, { Request, Response } from 'express'

//hash map to map keys to jobs
const jobMap: Map<string, cron.ScheduledTask> = new Map();
const jobGroupsMap: Map<string, cron.ScheduledTask[]> = new Map();

//jobs

//default jobs
const metricsJob = cron.schedule('0 0 0 1 * *',()=>{
    console.log('There are 5 users in the application')
}, {scheduled:false})

const birthdayJob = cron.schedule('0 0 0 * * *',()=>{
    console.log('20 users have their birthday today')
}, {scheduled:false})

// jobs related to auth
const countLoggedInUsersJob = cron.schedule('0 * * * * *',()=>{
    console.log('There are 100 users currently logged in')
}, {scheduled:false})

const autoUnbanUsersJob = cron.schedule('0 0 * * * *',()=>{
    console.log('unbanning users whose ban has expired')
},{scheduled:false})


//set the key to map to the job
jobMap.set('metrics',metricsJob)
jobMap.set('birthday',birthdayJob)
jobMap.set('countUsers',countLoggedInUsersJob)
jobMap.set('unbanUsers',autoUnbanUsersJob)

jobGroupsMap.set('default',[metricsJob, birthdayJob])
jobGroupsMap.set('auth',[countLoggedInUsersJob, autoUnbanUsersJob])

//express api and simple routes
const app = express()
app.use(express.json())
app.use(express.urlencoded({ extended: true }))


app.post('/start-job',(req:Request,res:Response)=>{
    const {jobName} = req.body
    const job = jobMap.get(jobName)

    if(!job) return res.status(400).json({message: 'invalid job name'})
    else job.start()
    res.status(200).json({message:`job ${jobName} started successfully`})
})

app.post('/stop-job',(req:Request,res:Response)=>{
    const {jobName} = req.body
    const job = jobMap.get(jobName)

    if(!job) return res.status(400).json({message: 'invalid job name'})
    else job.start()
    res.status(200).json({message:`job ${jobName} stoppeed successfully`})
})

app.post('/start-job-group',(req:Request,res:Response)=>{
    const {groupName} = req.body
    const jobs = jobGroupsMap.get(groupName)

    if(!jobs) return res.status(400).json({message: 'invalid group name'})
    else{
        jobs.forEach(job=>{
            job.start()
        })
    }
    res.status(200).json({message:`jobs in group ${groupName} started successfully`})
})

app.post('/stop-job-group',(req:Request,res:Response)=>{
    const {groupName} = req.body
    const jobs = jobGroupsMap.get(groupName)

    if(!jobs) return res.status(400).json({message: 'invalid group name'})
    else{
        jobs.forEach(job=>{
            job.stop()
        })
    }
    res.status(200).json({message:`jobs in group ${groupName} stopped successfully`})
})

Enter fullscreen mode Exit fullscreen mode

The Proposed Solution

As you can see, handling multiple cron jobs in node.js could become a hassle as the user has to write code to cater for each new cron job they create.

To solve this problem, I created a package. @ose4g/cron-manager.

It covers all use cases specified above

  • starting and stopping all jobs
  • starting and stopping a specific job
  • starting and stopping a group of related jobs.

To use first of all install the package using

npm i @ose4g/cron-manager
Enter fullscreen mode Exit fullscreen mode

or if you're using yarn

yarn add @ose4g/cron-manager
Enter fullscreen mode Exit fullscreen mode

See implementation below

import { cronGroup, cronJob, CronManager } from "@ose4g/cron-manager";
import express, { Request, Response } from 'express'

@cronGroup('default')
class DefaultJobs{

    @cronJob('0 0 0 1 * *','metrics')
    metricsCount(){
        console.log('There are 5 users in the application')
    }

    @cronJob('0 0 0 * * *','birthday')
    countBirthdays(){
        console.log('20 users have their birthday today')
    }
}

@cronGroup('auth')
class AuthJobs{

    @cronJob('0 * * * * *','countUsers')
    countLoggedInUsers(){
        console.log('There are 100 users currently logged in')
    }

    @cronJob('0 0 * * * *','unbanUsers')
    autoUnbanUsers(){

    }
}

const cronManager = new CronManager()

//registers the jobs
cronManager.register(DefaultJobs, new DefaultJobs())
cronManager.register(AuthJobs, new AuthJobs())


//express api and simple routes
const app = express()
app.use(express.json())
app.use(express.urlencoded({ extended: true }))

app.post('/start-job',(req:Request,res:Response)=>{
    const {jobName} = req.body
    try {
       cronManager.startHandler(jobName) //throw error for invalid jobName.
        res.status(200).json({message:`job ${jobName} started successfully`})
    } catch (error) {
        return res.status(400).json({message: 'invalid job name'})
    }
})

app.post('/stop-job',(req:Request,res:Response)=>{
    const {jobName} = req.body
    try {
       cronManager.stopHandler(jobName) //throw error for invalid jobName.
        res.status(200).json({message:`job ${jobName} stopped successfully`})
    } catch (error) {
        return res.status(400).json({message: 'invalid job name'})
    }
})

app.post('/start-job-group',(req:Request,res:Response)=>{
    const {groupName} = req.body
    try {
       cronManager.stopHandler(groupName) //throw error for invalid jobName.
        res.status(200).json({message:`jobs in group ${groupName} started successfully`})
    } catch (error) {
        return res.status(400).json({message: 'invalid group name'})
    }
})

app.post('/stop-job-group',(req:Request,res:Response)=>{
    const {groupName} = req.body
    try {
       cronManager.stopHandler(groupName) //throw error for invalid jobName.
        res.status(200).json({message:`jobs in group ${groupName} stopped successfully`})
    } catch (error) {
        return res.status(400).json({message: 'invalid group name'})
    }
})

app.post('/start-all',(req:Request, res:Response)=>{
   cronManager.startAll();
    return res.status(200).json({message: 'successfully started all jobs'})
})

app.post('/stop-all', (req:Request, res:Response)=>{
   cronManager.stopAll();
    return res.status(200).json({message: 'successfully stopped all jobs'})
})
Enter fullscreen mode Exit fullscreen mode

So to use the package, there are a few things you must do first.

  • Add decorator support in your tsconfig.json file Ensure the following is in your tsconfig.json file
  "experimentalDecorators": true,
  "emitDecoratorMetadata": true,
Enter fullscreen mode Exit fullscreen mode
  • Create your job class and annotate it with the @cronGroup tag. It takes a single parameter which is the string for the job groupName.
  • Create methods in the class and annotate the required methods with @cronJob(). It takes 2 parameters. The first is the cron expression while the second is the jobName (unique string to identify that job).
  • Create an instance of cronManager and register an instance of each Job class to the cronManager.It would throw an error if classes are not registered and you try to run the jobs.
  • start or stop jobs. The following functions are available
    • startAll(): starts all jobs defined in the app.
    • stopAll(): stops all jobs defined in the app.
    • startHandler(jobName): starts a specific job with jobName.
    • stopHandler(jobName): stops a specific job with jobName
    • startGroup(groupName): starts all jobs under group with groupName
    • startGroup(groupName): starts all jobs under group with groupName.
  • getGroups(): Lists all groupNames in the application
  • getHandlers(): Lists all handler names in the application

So using this package you can focus more on the logic of the cron jobs and write less code for managing your cron jobs.

Check out the source code for the package here. Kindly leave a star too πŸ™πŸΎπŸ™πŸΎ.

I hope you found this article insightful and helpful.
See you on the next one. Stay Awesome

πŸ’– πŸ’ͺ πŸ™… 🚩
ose4g
Osemudiamen Itua

Posted on April 11, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related