Управление несколькими заданиями cron в проекте node.js

Что такое задание cron?

Задание cron — это часть кода или действия, которые вы хотите выполнять через определенные промежутки времени или в определенное время. Некоторые примеры могут быть следующими

  • Проверка вашего приложения один раз в день на наличие пользователей, у которых день рождения в этот день, и отправка им электронных писем.
  • Получение статистики/метрических показателей вашего приложения 1-го числа каждого месяца и сохранение их в базе данных или отправка данных администратору.

В принципе, любое действие, которое вы хотите выполнять через различные промежутки времени, называется заданием cron.

Cron-задания в node.js с помощью typescript.

Для создания заданий cron в Node.js доступно несколько пакетов, но мы будем работать с 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
Вход в полноэкранный режим Выйти из полноэкранного режима

Выражение cronExpression описывает, как часто должно вызываться действие. Описание cronExpression смотрите здесь.

Проблема

Могут возникнуть ситуации, когда мы захотим приостановить или перезапустить задание cron. Тогда нам нужно найти способ управлять этим. Скажем, в нашем экспресс-сервере нам нужен способ запуска или остановки cron-job. Как бы мы это сделали?

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

Войти в полноэкранный режим Выйти из полноэкранного режима

Мы могли бы сделать это таким образом, если бы работали всего с несколькими заданиями.
Но, скажем, мы работаем с большим количеством заданий и не хотим создавать конечную точку для каждого из них.
Можно использовать подход, при котором каждое задание ассоциируется с уникальным ключом, а ключ привязывается к заданию.
Смотрите реализацию ниже.

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`})
})
Вход в полноэкранный режим Выход из полноэкранного режима

Все работает правильно. Все хорошо и отлично. Другим вариантом использования может быть запуск или остановка связанных заданий.
Скажем, у вас есть два задания, связанных с аутентификацией, и еще два задания, связанных с метриками приложения. Вам нужен способ запуска или остановки всех заданий в разделе auth или metrics без необходимости запускать каждое задание по отдельности.
Для этого можно использовать другую карту.

Смотрите реализацию ниже.

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

Вход в полноэкранный режим Выход из полноэкранного режима

Предлагаемое решение

Как вы видите, работа с несколькими заданиями cron в node.js может стать хлопотной, поскольку пользователю приходится писать код для каждого нового задания cron, которое он создает.

Чтобы решить эту проблему, я создал пакет. @ose4g/cron-manager.

Он охватывает все случаи использования, указанные выше

  • запуск и остановка всех заданий
  • запуск и остановка конкретного задания
  • запуск и остановка группы связанных заданий.

Для использования, прежде всего, установите пакет, используя

npm i @ose4g/cron-manager
Войти в полноэкранный режим Выйти из полноэкранного режима

или если вы используете yarn

yarn add @ose4g/cron-manager
Войти в полноэкранный режим Выйти из полноэкранного режима

Смотрите реализацию ниже

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'})
})
Вход в полноэкранный режим Выйти из полноэкранного режима

Итак, чтобы использовать пакет, необходимо сделать несколько вещей.

  • Добавьте поддержку декораторов в ваш файл tsconfig.jsonУбедитесь, что в вашем файле tsconfig.json есть следующие параметры
  "experimentalDecorators": true,
  "emitDecoratorMetadata": true,
Вход в полноэкранный режим Выйти из полноэкранного режима
  • Создайте класс задания и аннотируйте его тегом @cronGroup. Он принимает один параметр, который является строкой для имени группы заданий (job groupName).
  • Создайте методы в классе и аннотируйте необходимые методы тегом @cronJob(). Он принимает 2 параметра. Первый — это выражение cron, а второй — jobName (уникальная строка для идентификации задания).
  • Создайте экземпляр cronManager и зарегистрируйте экземпляр каждого класса Job в cronManager. Если классы не зарегистрированы и вы пытаетесь запустить задания, будет выдана ошибка.
  • запуск или остановка заданий. Доступны следующие функции
    • startAll(): запускает все задания, определенные в приложении.
    • stopAll(): останавливает все задания, определенные в приложении.
    • startHandler(jobName): запускает определенное задание с именем jobName.
    • stopHandler(jobName): останавливает определенное задание с именем jobName.
    • startGroup(groupName): запускает все задания под группой с именем groupName
    • startGroup(groupName): запускает все задания в группе с именем groupName.
  • getGroups(): Перечисляет все имена групп в приложении
  • getHandlers(): Перечисляет все имена обработчиков в приложении

Таким образом, используя этот пакет, вы можете больше сосредоточиться на логике работы cron-заданий и писать меньше кода для управления заданиями cron.

Ознакомьтесь с исходным кодом пакета здесь. Пожалуйста, оставьте звезду 🙏🏾🙏🏾.

Я надеюсь, что эта статья была для вас полезной и содержательной.
Увидимся в следующей. Оставайтесь потрясающими

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *