Scheduled task: According to the time rule, the system executes corresponding tasks in the background. Scheduled tasks are essential functions for projects, such as sending notifications to users regularly and data integration and processing regularly.

A few days ago, scheduled tasks were needed to realize business logic in the project, so I sorted out the design and implementation of scheduled tasks

The project was written in TS and constructed based on KOA2

Basic parameters

These basic parameters are required for a scheduled task

/** * @description * task object * @interface scheduleInfo */
export interface IScheduleInfo {
    /** * Timing rule */
    corn: string;
    /** * Task name */
    name: string;
    /** * Task switch */
    switch: boolean;
}
Copy the code

Name is the name of a scheduled task, and the name of the task should be unique. Two tasks cannot use the same name. The function of this name will be explained later

Creating a Scheduled Task

We use the node-schedule library to create scheduled tasks in a simple way

var schedule = require('node-schedule');

var j = schedule.scheduleJob('42 * * * *'.function(){
  console.log('The answer to life, the universe, and everything! ');
});
Copy the code

node-schedule

Transaction lock control

In distributed deployment, you need to ensure that the same scheduled task can run only once, so you need to use transaction locks to control it. I have written about this in the past

Node.js uses Redis to implement distributed transaction locks

Encapsulation abstract

According to the object-oriented design, we abstract the timing task from the parent class, and put the creation task, transaction control, etc., into the parent class. The subclasses that inherit the parent class only need to implement the business logic

import * as schedule from 'node-schedule';
import { IScheduleInfo } from './i_schedule_info';

/** * @description * @export * @class AbstractSchedule */
export abstract class AbstractSchedule {
    /** * Task object */
    public scheduleInfo: IScheduleInfo;

    public redLock;

    public name: string;

    public app: Core;

    /** * redLock expiration time */
    private _redLockTTL: number;
    constructor(app) {
        this.app = app;
        this.redLock = app.redLock;
        this._redLockTTL = 60000;
    }

    /** * @description * To synchronize tasks * @private * @param {any} lock * @returns * @memberof AbstractSchedule */
    private async _execTask(lock) {
        this.app.logger.info('Execute scheduled task, task name:The ${this.scheduleInfo.name}; Execution time:The ${new Date()}`);
        await this.task();
        await this._sleep(6000);
        return lock.unlock()
            .catch((err) = > {
                console.error(err);
            });
    }

    /** * @description * delay * @private * @param {any} ms * @returns * @memberof AbstractSchedule */
    private _sleep(ms) {
        return new Promise((resolve) = > {
            setTimeout((a)= > {
                resolve();
            }, ms);
        });
    }

    /** * @description * Start task, use redis lock, * @private * @param IScheduleInfo scheduleInfo * @param {Function} callback * @param {*} name * @returns * @memberof AbstractSchedule */
    public startSchedule() {
        return schedule.scheduleJob(this.scheduleInfo.corn, () => {
            this.redLock.lock(this.scheduleInfo.name, this._redLockTTL).then((lock) = > {
                this._execTask(lock);
            }, err => this.app.logger.info('This instance does not perform scheduled tasks:The ${this.scheduleInfo.name}, executed by other instances));
        });
    }

    /** * @description * @author lizc * @abstract * @memberof AbstractSchedule */
    public start() {
        this.startSchedule();
    }

    /** * @description defines the task * @abstract * @memberof AbstractSchedule */
    public abstract task();

}
Copy the code

The abstract class has an abstract method task in which subclasses implement concrete logical code

The subclass implementation

There are two scenarios for a scheduled task

  1. The configuration parameters of the task are written directly in the code
export default class TestSchedule extends AbstractSchedule {

    constructor(app: Core) {
        super(app);

        this.scheduleInfo = {
            corn: '* */30 * * * *'.// Updates every 30 minutes
            name: 'test'.switch: true
        };

    }
    /** * Business implementation */
    public task() { }

}
Copy the code
  1. Task parameters are controlled by the configuration center, and configuration parameters are imported from outside
export default class TestSchedule extends AbstractSchedule {

    constructor(app: Core, scheduleInfo: IScheduleInfo) {
        super(app);
        this.scheduleInfo = scheduleInfo;
    }
    /** * Business implementation */
    public task() { }

}
Copy the code

Start to realize

Locally configured tasks, very easy to start, will create instances on the line. To associate remote configuration tasks with implementation classes, make the following conventions:

  1. The file name of the task is as follows${name}_schedule.tsThis format
  2. The task name of remote configuration should be${name}_schedule.tsIn the correspondingname

For example, if there is a scheduled task associated with a user, the name of the file is user_schedule.ts, then the task name in the remote configuration is name=user

This object can then be exported and created by importing (${name}_schedule.ts)

With this convention, we can associate configuration items with corresponding tasks

The complete implementation code is as follows


import { AbstractSchedule } from './abstract_schedule';

export class ScheduleHelper {

    private scheduleList: Array<AbstractSchedule> = [];
    private app;

    constructor(app) {
        this.app = app;
        this.initStaticTask();
    }

    /** * Locally configured scheduled task */
    private initStaticTask() {
        this.scheduleList.push(new TestSchedule(this.app));
    }

    /** * Remote configured scheduled task */
    private async initTaskFromConfig() {
        const taskList: Array<IScheduleInfo> = this.app.config.scheduleConfig.taskList;

        for (const taskItem of taskList) {
            const path = `The ${this.app.config.rootPath}/schedule/task/${taskItem.name}_schedule`;

            import(path).then((taskBusiness) = > {
                const scheduleItem: AbstractSchedule = new taskBusiness.default(this.app, taskItem);
                this.scheduleList.push(scheduleItem);
            }, (err) => {
                console.error('[schedule] initialization failed. Configuration file could not be found${err.message}`); }); }}/** * start entry */
    public async taskListRun() {
        await this.initTaskFromConfig();
        for (const schedule of this.scheduleList) {
            if(schedule.scheduleInfo.switch) { schedule.start(); }}}}Copy the code

Configuration center

The configuration center in the project uses Apollo, developed by Ctrip

summary

Design comes from the scene, the current design basically meets the current business, 100% of the scene is still not satisfied, welcome friends to discuss better design scheme ~