You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
238 lines
5.9 KiB
238 lines
5.9 KiB
'use strict';
|
|
|
|
const _ = require('lodash');
|
|
const parser = require('cron-parser');
|
|
const crypto = require('crypto');
|
|
|
|
const Job = require('./job');
|
|
|
|
module.exports = function(Queue) {
|
|
Queue.prototype.nextRepeatableJob = function(
|
|
name,
|
|
data,
|
|
opts,
|
|
skipCheckExists
|
|
) {
|
|
const client = this.client;
|
|
const repeat = opts.repeat;
|
|
const prevMillis = opts.prevMillis || 0;
|
|
|
|
if (!prevMillis && opts.jobId) {
|
|
repeat.jobId = opts.jobId;
|
|
}
|
|
|
|
const currentCount = repeat.count ? repeat.count + 1 : 1;
|
|
|
|
if (!_.isUndefined(repeat.limit) && currentCount > repeat.limit) {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
let now = Date.now();
|
|
|
|
if (!_.isUndefined(repeat.endDate) && now > new Date(repeat.endDate)) {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
now = prevMillis < now ? now : prevMillis;
|
|
|
|
const nextMillis = getNextMillis(now, repeat);
|
|
if (nextMillis) {
|
|
const jobId = repeat.jobId ? repeat.jobId + ':' : ':';
|
|
const repeatKey = getRepeatKey(name, repeat, jobId);
|
|
|
|
const createNextJob = () => {
|
|
return client.zadd(this.keys.repeat, nextMillis, repeatKey).then(() => {
|
|
//
|
|
// Generate unique job id for this iteration.
|
|
//
|
|
const customId = getRepeatJobId(
|
|
name,
|
|
jobId,
|
|
nextMillis,
|
|
md5(repeatKey)
|
|
);
|
|
now = Date.now();
|
|
const delay = nextMillis - now;
|
|
|
|
return Job.create(
|
|
this,
|
|
name,
|
|
data,
|
|
_.defaultsDeep(
|
|
{
|
|
repeat: {
|
|
count: currentCount,
|
|
key: repeatKey
|
|
},
|
|
jobId: customId,
|
|
delay: delay < 0 ? 0 : delay,
|
|
timestamp: now,
|
|
prevMillis: nextMillis
|
|
},
|
|
opts
|
|
)
|
|
);
|
|
});
|
|
};
|
|
|
|
if (skipCheckExists) {
|
|
return createNextJob();
|
|
}
|
|
|
|
// Check that the repeatable job hasn't been removed
|
|
// TODO: a lua script would be better here
|
|
return client
|
|
.zscore(this.keys.repeat, repeatKey)
|
|
.then(repeatableExists => {
|
|
// The job could have been deleted since this check
|
|
if (repeatableExists) {
|
|
return createNextJob();
|
|
}
|
|
return Promise.resolve();
|
|
});
|
|
} else {
|
|
return Promise.resolve();
|
|
}
|
|
};
|
|
|
|
Queue.prototype.removeRepeatable = function(name, repeat) {
|
|
if (typeof name !== 'string') {
|
|
repeat = name;
|
|
name = Job.DEFAULT_JOB_NAME;
|
|
}
|
|
|
|
return this.isReady().then(() => {
|
|
const jobId = repeat.jobId ? repeat.jobId + ':' : ':';
|
|
const repeatJobKey = getRepeatKey(name, repeat, jobId);
|
|
const repeatJobId = getRepeatJobId(name, jobId, '', md5(repeatJobKey));
|
|
const queueKey = this.keys[''];
|
|
return this.client.removeRepeatable(
|
|
this.keys.repeat,
|
|
this.keys.delayed,
|
|
repeatJobId,
|
|
repeatJobKey,
|
|
queueKey
|
|
);
|
|
});
|
|
};
|
|
|
|
Queue.prototype.removeRepeatableByKey = function(repeatJobKey) {
|
|
const repeatMeta = this._keyToData(repeatJobKey);
|
|
const queueKey = this.keys[''];
|
|
|
|
const jobId = repeatMeta.id ? repeatMeta.id + ':' : ':';
|
|
const repeatJobId = getRepeatJobId(
|
|
repeatMeta.name || Job.DEFAULT_JOB_NAME,
|
|
jobId,
|
|
'',
|
|
md5(repeatJobKey)
|
|
);
|
|
|
|
return this.isReady().then(() => {
|
|
return this.client.removeRepeatable(
|
|
this.keys.repeat,
|
|
this.keys.delayed,
|
|
repeatJobId,
|
|
repeatJobKey,
|
|
queueKey
|
|
);
|
|
});
|
|
};
|
|
|
|
Queue.prototype._keyToData = function(key) {
|
|
const data = key.split(':');
|
|
|
|
return {
|
|
key: key,
|
|
name: data[0],
|
|
id: data[1] || null,
|
|
endDate: parseInt(data[2]) || null,
|
|
tz: data[3] || null,
|
|
cron: data[4]
|
|
};
|
|
};
|
|
|
|
Queue.prototype.getRepeatableJobs = function(start, end, asc) {
|
|
const key = this.keys.repeat;
|
|
start = start || 0;
|
|
end = end || -1;
|
|
return (asc
|
|
? this.client.zrange(key, start, end, 'WITHSCORES')
|
|
: this.client.zrevrange(key, start, end, 'WITHSCORES')
|
|
).then(result => {
|
|
const jobs = [];
|
|
for (let i = 0; i < result.length; i += 2) {
|
|
const data = this._keyToData(result[i]);
|
|
jobs.push({
|
|
key: data.key,
|
|
name: data.name,
|
|
id: data.id,
|
|
endDate: data.endDate,
|
|
tz: data.cron ? data.tz : null,
|
|
cron: data.cron || null,
|
|
every: !data.cron ? parseInt(data.tz) : null,
|
|
next: parseInt(result[i + 1])
|
|
});
|
|
}
|
|
return jobs;
|
|
});
|
|
};
|
|
|
|
Queue.prototype.getRepeatableCount = function() {
|
|
return this.client.zcard(this.toKey('repeat'));
|
|
};
|
|
|
|
function getRepeatJobId(name, jobId, nextMillis, namespace) {
|
|
return 'repeat:' + md5(name + jobId + namespace) + ':' + nextMillis;
|
|
}
|
|
|
|
function getRepeatKey(name, repeat, jobId) {
|
|
const endDate = repeat.endDate
|
|
? new Date(repeat.endDate).getTime() + ':'
|
|
: ':';
|
|
const tz = repeat.tz ? repeat.tz + ':' : ':';
|
|
const suffix = repeat.cron ? tz + repeat.cron : String(repeat.every);
|
|
|
|
return name + ':' + jobId + endDate + suffix;
|
|
}
|
|
|
|
function getNextMillis(millis, opts) {
|
|
if (opts.cron && opts.every) {
|
|
throw new Error(
|
|
'Both .cron and .every options are defined for this repeatable job'
|
|
);
|
|
}
|
|
|
|
if (opts.every) {
|
|
return Math.floor(millis / opts.every) * opts.every + opts.every;
|
|
}
|
|
|
|
const currentDate =
|
|
opts.startDate && new Date(opts.startDate) > new Date(millis)
|
|
? new Date(opts.startDate)
|
|
: new Date(millis);
|
|
const interval = parser.parseExpression(
|
|
opts.cron,
|
|
_.defaults(
|
|
{
|
|
currentDate
|
|
},
|
|
opts
|
|
)
|
|
);
|
|
|
|
try {
|
|
return interval.next().getTime();
|
|
} catch (e) {
|
|
// Ignore error
|
|
}
|
|
}
|
|
|
|
function md5(str) {
|
|
return crypto
|
|
.createHash('md5')
|
|
.update(str)
|
|
.digest('hex');
|
|
}
|
|
};
|