Managing multiple cron jobs in node.js project
Tags: typescript, decorators, cronjob, npm
Published: 2022-04-11 12:25:07

## 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](https://www.npmjs.com/package/node-cron).

```typescript
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
```
The **cronExpression** describes how often the **action** should be called. Check [here](https://www.npmjs.com/package/node-cron) 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? 

```typescript
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'})
})

```
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.

```typescript
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`})
})
```

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.
```typescript
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`})
})

```

## 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](https://www.npmjs.com/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 
```bash
npm i @ose4g/cron-manager
```     
or if you're using yarn    
```bash
yarn add @ose4g/cron-manager
```
See implementation below
```typescript
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'})
})
```
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
  ```json
  "experimentalDecorators": true,
  "emitDecoratorMetadata": true,
  ```
- 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](https://github.com/Ose-4g/cron-manager). Kindly leave a star too 🙏🏾🙏🏾.

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