cronivo
Version:
Synced job scheduler using later and Redis
139 lines (123 loc) • 4.82 kB
JavaScript
;
const _ = require('lodash');
const later = require('later');
const logger = require('winston');
/**
* Executes the action for either setInterval or setTimeout, using lock and redis to synchronize
* execution.
*
* @param {Function} action The function to be executed on the provided schedule
* @param {schedule} schedule A later schedule for the action
* @param {string} jobName The name of the job to be executed
* @private
*/
function executeAction(action, schedule, jobName) {
// Locks so other jobs wait before executing
this.redisLock(`${jobName}Lock`, (done) => {
// Gets the job next execution saved on redis
this.redisClient.get(jobName, (err, reply) => {
// If the next execution (saved on redis) is null or greater than now, then this job must
// execute, else, it was already executed
const nextExecutions = later.schedule(schedule).next(2);
// Sets milliseconds to zero since later doesn't use or set it
_.forEach(nextExecutions, execution => execution.setMilliseconds(0));
const now = new Date().getTime();
const nextExecution = now > nextExecutions[0].getTime()
? nextExecutions[1] : nextExecutions[0];
if (_.isNil(reply) || reply.toString() < now) {
// Sets the next execution time on redis so other jobs wont run
this.redisClient.set(jobName, nextExecution.getTime(), () => {
// Releases the lock since it is no longer required
done();
// Calls the method passed by the user
try {
action();
} catch (error) {
this.logger.error(error);
}
});
} else {
done();
}
});
});
}
/**
* Class to schedule synchronized jobs.
*/
class Cronivo {
/**
* Sets up the class, saving the redis client and using it to startup redisLock.
*
* @param {RedisClient} client A redis client connected to a redis instance where the jobs
* data will be saved
* @param {Logger} [userLogger] A user provided logger to be used to log errors
*/
constructor(client, userLogger) {
this.redisClient = client;
// Uses the provided client to configure redisLock
this.redisLock = require('redis-lock')(this.redisClient); // eslint-disable-line global-require
// Initialize jobs list
this.jobs = {};
// Use provided logger or default
this.logger = userLogger || logger;
}
/**
* Executes the provided function repeatedely in the provided schedule. Uses the job name as a key
* on redis, therefore it must be unique.
*
* @param {Function} action The function to be executed on the provided schedule
* @param {schedule} schedule A later schedule for the action
* @param {string} jobName The name of the job to be executed
*/
addJob(action, schedule, jobName) {
// Save the job timer so it can be cancelled
const timer = later.setInterval(() => {
executeAction.bind(this)(action, schedule, jobName);
}, schedule);
_.set(this.jobs, `${jobName}.timer`, timer);
// Save the method action so it can be ran manually
this.jobs[jobName].action = action;
}
/**
* Executes the provided function once in the provided schedule. Uses the job name as a key on
* redis, therefore it must be unique.
*
* @param {Function} action The function to be executed on the provided schedule
* @param {schedule} schedule A later schedule for the action
* @param {string} jobName The name of the job to be executed
*/
addSingleExecutionJob(action, schedule, jobName) {
// Save the job timer so it can be cancelled
_.set(this.jobs, `${jobName}.timer`, later.setTimeout(() => {
executeAction.bind(this)(action, schedule, jobName);
}, schedule));
// Save the method action so it can be ran manually
this.jobs[jobName].action = action;
}
/**
* Cancels a jobs execution, clearing all data about it.
*
* @param {string} jobName The name of the job to be cancelled.
*/
cancelJob(jobName) {
if (this.jobs[jobName]) {
this.jobs[jobName].timer.clear();
delete this.jobs[jobName];
this.redisLock(`${jobName}Lock`, (done) => {
this.redisClient.del(jobName, () => done());
});
}
}
/**
* Runs the given job, if it exists, imediately, without affecting the schedule.
*
* @param {string} jobName The name of the job to be executed
*/
runJob(jobName) {
if (this.jobs[jobName]) {
this.jobs[jobName].action();
}
}
}
module.exports = Cronivo;