@hokify/agenda
Version:
Light weight job scheduler for Node.js
379 lines • 14.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Job = void 0;
const date = require("date.js");
const debug = require("debug");
const child_process_1 = require("child_process");
const JobParameters_1 = require("./types/JobParameters");
const priority_1 = require("./utils/priority");
const nextRunAt_1 = require("./utils/nextRunAt");
const log = debug('agenda:job');
/**
* @class
*/
class Job {
getCanceledMessage() {
var _a;
return typeof this.canceled === 'object'
? ((_a = this.canceled) === null || _a === void 0 ? void 0 : _a.message) || this.canceled
: this.canceled;
}
cancel(error) {
this.agenda.emit(`cancel:${this.attrs.name}`, this);
this.canceled = error || true;
if (this.forkedChild) {
try {
this.forkedChild.send({
type: 'cancel',
error: this.canceled instanceof Error ? this.canceled.message : this.canceled
});
// eslint-disable-next-line no-console
console.info('send canceled child', this.attrs.name, this.attrs._id);
}
catch (err) {
// eslint-disable-next-line no-console
console.log('cannot send cancel to child');
}
}
}
constructor(agenda, args, byJobProcessor = false) {
this.agenda = agenda;
this.byJobProcessor = byJobProcessor;
// Set attrs to args
this.attrs = {
...args,
// Set defaults if undefined
priority: (0, priority_1.parsePriority)(args.priority),
nextRunAt: args.nextRunAt === undefined ? new Date() : args.nextRunAt,
type: args.type
};
}
/**
* Given a job, turn it into an JobParameters object
*/
toJson() {
const result = {};
for (const key of Object.keys(this.attrs)) {
if (Object.hasOwnProperty.call(this.attrs, key)) {
result[key] =
JobParameters_1.datefields.includes(key) && this.attrs[key]
? new Date(this.attrs[key])
: this.attrs[key];
}
}
return result;
}
/**
* Sets a job to repeat every X amount of time
* @param interval
* @param options
*/
repeatEvery(interval, options = {}) {
this.attrs.repeatInterval = interval;
this.attrs.repeatTimezone = options.timezone;
if (options.skipImmediate) {
// Set the lastRunAt time to the nextRunAt so that the new nextRunAt will be computed in reference to the current value.
this.attrs.lastRunAt = this.attrs.nextRunAt || new Date();
this.computeNextRunAt();
this.attrs.lastRunAt = undefined;
}
else {
this.computeNextRunAt();
}
return this;
}
/**
* Sets a job to repeat at a specific time
* @param time
*/
repeatAt(time) {
this.attrs.repeatAt = time;
return this;
}
/**
* if set, a job is forked via node child process and runs in a seperate/own
* thread
* @param enableForkMode
*/
forkMode(enableForkMode) {
this.attrs.fork = enableForkMode;
return this;
}
/**
* Prevents the job from running
*/
disable() {
this.attrs.disabled = true;
return this;
}
/**
* Allows job to run
*/
enable() {
this.attrs.disabled = false;
return this;
}
/**
* Data to ensure is unique for job to be created
* @param unique
* @param opts
*/
unique(unique, opts) {
this.attrs.unique = unique;
this.attrs.uniqueOpts = opts;
return this;
}
/**
* Schedules a job to run at specified time
* @param time
*/
schedule(time) {
const d = new Date(time);
this.attrs.nextRunAt = Number.isNaN(d.getTime()) ? date(time) : d;
return this;
}
/**
* Sets priority of the job
* @param priority priority of when job should be queued
*/
priority(priority) {
this.attrs.priority = (0, priority_1.parsePriority)(priority);
return this;
}
/**
* Fails the job with a reason (error) specified
*
* @param reason
*/
fail(reason) {
this.attrs.failReason = reason instanceof Error ? reason.message : reason;
this.attrs.failCount = (this.attrs.failCount || 0) + 1;
const now = new Date();
this.attrs.failedAt = now;
this.attrs.lastFinishedAt = now;
log('[%s:%s] fail() called [%d] times so far', this.attrs.name, this.attrs._id, this.attrs.failCount);
return this;
}
async fetchStatus() {
const dbJob = await this.agenda.db.getJobs({ _id: this.attrs._id });
if (!dbJob || dbJob.length === 0) {
// @todo: should we just return false instead? a finished job could have been removed from database,
// and then this would throw...
throw new Error(`job with id ${this.attrs._id} not found in database`);
}
this.attrs.lastRunAt = dbJob[0].lastRunAt;
this.attrs.lockedAt = dbJob[0].lockedAt;
this.attrs.lastFinishedAt = dbJob[0].lastFinishedAt;
}
/**
* A job is running if:
* (lastRunAt exists AND lastFinishedAt does not exist)
* OR
* (lastRunAt exists AND lastFinishedAt exists but the lastRunAt is newer [in time] than lastFinishedAt)
* @returns Whether or not job is running at the moment (true for running)
*/
async isRunning() {
if (!this.byJobProcessor || this.attrs.fork) {
// we have no job definition, therfore we are not the job processor, but a client call
// so we get the real state from database
await this.fetchStatus();
}
if (!this.attrs.lastRunAt) {
return false;
}
if (!this.attrs.lastFinishedAt) {
return true;
}
if (this.attrs.lockedAt &&
this.attrs.lastRunAt.getTime() > this.attrs.lastFinishedAt.getTime()) {
return true;
}
return false;
}
/**
* Saves a job to database
*/
async save() {
if (this.agenda.forkedWorker) {
const warning = new Error('calling save() on a Job during a forkedWorker has no effect!');
console.warn(warning.message, warning.stack);
return this;
}
// ensure db connection is ready
await this.agenda.ready;
return this.agenda.db.saveJob(this);
}
/**
* Remove the job from database
*/
remove() {
return this.agenda.cancel({ _id: this.attrs._id });
}
async isDead() {
return this.isExpired();
}
async isExpired() {
if (!this.byJobProcessor || this.attrs.fork) {
// we have no job definition, therfore we are not the job processor, but a client call
// so we get the real state from database
await this.fetchStatus();
}
const definition = this.agenda.definitions[this.attrs.name];
const lockDeadline = new Date(Date.now() - definition.lockLifetime);
// This means a job has "expired", as in it has not been "touched" within the lockoutTime
// Remove from local lock
if (this.attrs.lockedAt && this.attrs.lockedAt < lockDeadline) {
return true;
}
return false;
}
/**
* Updates "lockedAt" time so the job does not get picked up again
* @param progress 0 to 100
*/
async touch(progress) {
if (this.canceled) {
throw new Error(`job ${this.attrs.name} got canceled already: ${this.canceled}!`);
}
this.attrs.lockedAt = new Date();
this.attrs.progress = progress;
await this.agenda.db.saveJobState(this);
}
computeNextRunAt() {
try {
if (this.attrs.repeatInterval) {
this.attrs.nextRunAt = (0, nextRunAt_1.computeFromInterval)(this.attrs);
log('[%s:%s] nextRunAt set to [%s]', this.attrs.name, this.attrs._id, new Date(this.attrs.nextRunAt).toISOString());
}
else if (this.attrs.repeatAt) {
this.attrs.nextRunAt = (0, nextRunAt_1.computeFromRepeatAt)(this.attrs);
log('[%s:%s] nextRunAt set to [%s]', this.attrs.name, this.attrs._id, this.attrs.nextRunAt.toISOString());
}
else {
this.attrs.nextRunAt = null;
}
}
catch (error) {
this.attrs.nextRunAt = null;
this.fail(error);
}
return this;
}
async run() {
this.attrs.lastRunAt = new Date();
log('[%s:%s] setting lastRunAt to: %s', this.attrs.name, this.attrs._id, this.attrs.lastRunAt.toISOString());
this.computeNextRunAt();
await this.agenda.db.saveJobState(this);
try {
this.agenda.emit('start', this);
this.agenda.emit(`start:${this.attrs.name}`, this);
log('[%s:%s] starting job', this.attrs.name, this.attrs._id);
if (this.attrs.fork) {
if (!this.agenda.forkHelper) {
throw new Error('no forkHelper specified, you need to set a path to a helper script');
}
const { forkHelper } = this.agenda;
await new Promise((resolve, reject) => {
this.forkedChild = (0, child_process_1.fork)(forkHelper.path, [
this.attrs.name,
this.attrs._id.toString(),
this.agenda.definitions[this.attrs.name].filePath || ''
], forkHelper.options);
let childError;
this.forkedChild.on('close', code => {
if (code) {
// eslint-disable-next-line no-console
console.info('fork parameters', forkHelper, this.attrs.name, this.attrs._id, this.agenda.definitions[this.attrs.name].filePath);
const error = new Error(`child process exited with code: ${code}`);
console.warn(error.message, childError || this.canceled);
reject(childError || this.canceled || error);
}
else {
resolve();
}
});
this.forkedChild.on('message', message => {
// console.log(`Message from child.js: ${message}`, JSON.stringify(message));
if (typeof message === 'string') {
try {
childError = JSON.parse(message);
}
catch (errJson) {
childError = message;
}
}
else {
childError = message;
}
});
});
}
else {
await this.runJob();
}
this.attrs.lastFinishedAt = new Date();
this.agenda.emit('success', this);
this.agenda.emit(`success:${this.attrs.name}`, this);
log('[%s:%s] has succeeded', this.attrs.name, this.attrs._id);
}
catch (error) {
log('[%s:%s] unknown error occurred', this.attrs.name, this.attrs._id);
this.fail(error);
this.agenda.emit('fail', error, this);
this.agenda.emit(`fail:${this.attrs.name}`, error, this);
log('[%s:%s] has failed [%s]', this.attrs.name, this.attrs._id, error.message);
}
finally {
this.forkedChild = undefined;
this.attrs.lockedAt = undefined;
try {
await this.agenda.db.saveJobState(this);
log('[%s:%s] was saved successfully to MongoDB', this.attrs.name, this.attrs._id);
}
catch (err) {
// in case this fails, we ignore it
// this can e.g. happen if the job gets removed during the execution
log('[%s:%s] was not saved to MongoDB', this.attrs.name, this.attrs._id, err);
}
this.agenda.emit('complete', this);
this.agenda.emit(`complete:${this.attrs.name}`, this);
log('[%s:%s] job finished at [%s] and was unlocked', this.attrs.name, this.attrs._id, this.attrs.lastFinishedAt);
}
}
async runJob() {
const definition = this.agenda.definitions[this.attrs.name];
if (!definition) {
log('[%s:%s] has no definition, can not run', this.attrs.name, this.attrs._id);
throw new Error('Undefined job');
}
if (definition.fn.length === 2) {
log('[%s:%s] process function being called', this.attrs.name, this.attrs._id);
await new Promise((resolve, reject) => {
try {
const result = definition.fn(this, error => {
if (error) {
reject(error);
return;
}
resolve();
});
if (this.isPromise(result)) {
result.catch((error) => reject(error));
}
}
catch (error) {
reject(error);
}
});
}
else {
log('[%s:%s] process function being called', this.attrs.name, this.attrs._id);
await definition.fn(this);
}
}
isPromise(value) {
return !!(value && typeof value.then === 'function');
}
}
exports.Job = Job;
//# sourceMappingURL=Job.js.map