Skip to content

Commit 82d9a77

Browse files
committed
merged v3.0.0
2 parents dd371e9 + 9fa6d04 commit 82d9a77

File tree

8 files changed

+268
-47
lines changed

8 files changed

+268
-47
lines changed

CHANGELOG.md

+10
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
v.3.0.0-alpha.4
2+
===============
3+
4+
- Implemented repeatable jobs. #252.
5+
6+
v.3.0.0-alpha.3
7+
===============
8+
9+
- Simplified global events #501.
10+
111
v.3.0.0-alpha.2
212
===============
313

README.md

+12-3
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,11 @@ Features:
3535
- Minimal CPU usage by poll-free design.
3636
- Robust design based on Redis.
3737
- Delayed jobs.
38+
- Schedule and repeat jobs according to a cron specification.
3839
- Retries.
3940
- Priority.
4041
- Concurrency.
42+
- Multiple job types per queue.
4143
- Pause/resume (globally or locally).
4244
- Automatic recovery from process crashes.
4345

@@ -53,12 +55,9 @@ There are a few third party UIs that can be used for easier administration of th
5355
Roadmap:
5456
--------
5557

56-
- Multiple job types per queue.
57-
- Scheduling jobs as a cron specification.
5858
- Rate limiter for jobs.
5959
- Parent-child jobs relationships.
6060

61-
6261
Install:
6362
--------
6463

@@ -528,6 +527,8 @@ interface JobOpts{
528527
delay: number; // An amount of miliseconds to wait until this job can be processed. Note that for accurate delays, both
529528
// server and clients should have their clocks synchronized. [optional].
530529

530+
repeat: RepeatOpts; // Define repeat options for adding jobs according to a Cron specification.
531+
531532
attempts: number; // The total number of attempts to try the job until it completes.
532533

533534
backoff: number | BackoffOpts; // Backoff setting for automatic retries if the job fails
@@ -549,6 +550,14 @@ interface JobOpts{
549550
}
550551
```
551552

553+
```typescript
554+
interface RepeatOpts{
555+
cron: string; // Cron expression. See https://github.com/harrisiirak/cron-parser for details.
556+
endDate?: number | Date; // Stop repeating jobs after this date.
557+
tz?: string; // Timezone. For example: 'Europe/Athens'
558+
}
559+
```
560+
552561
```typescript
553562
interface BackoffOpts{
554563
type: string; // Backoff type, which can be either `fixed` or `exponential`

lib/commands/moveToActive-4.lua

-3
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66
expiration time. The worker is responsible of keeping the lock fresh
77
so that no other worker picks this job again.
88
9-
Note: This command only works in non-distributed redis deployments.
10-
119
Input:
1210
KEYS[1] wait key
1311
KEYS[2] active key
@@ -35,4 +33,3 @@ if jobId then
3533

3634
return {redis.call("HGETALL", jobKey), jobId} -- get job data
3735
end
38-

lib/commands/updateDelaySet-4.lua

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
--[[
2-
Updates the delay set
2+
Updates the delay set, by picking a delayed job that should
3+
be processed now.
34
45
Input:
56
KEYS[1] 'delayed'
@@ -18,7 +19,7 @@ local jobId = RESULT[1]
1819
local score = RESULT[2]
1920
if (score ~= nil) then
2021
score = score / 0x1000
21-
if (score <= tonumber(ARGV[2])) then
22+
if (math.floor(score) <= tonumber(ARGV[2])) then
2223
redis.call("ZREM", KEYS[1], jobId)
2324
redis.call("LREM", KEYS[2], 0, jobId)
2425
redis.call("LPUSH", KEYS[3], jobId) -- not sure if it is better to move the job at the begining of the queue with LPUSH

lib/queue.js

+89-34
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ var uuid = require('uuid');
1919

2020
var commands = require('./commands/');
2121

22+
Promise.config({
23+
cancellation: true
24+
});
25+
2226
/**
2327
Gets or creates a new Queue with the given name.
2428
@@ -140,7 +144,7 @@ var Queue = function Queue(name, url, opts){
140144
this.eclient = createClient('subscriber', redisOpts);
141145

142146
this.handlers = {};
143-
this.delayTimer = null;
147+
this.delayTimer = Promise.resolve();
144148
this.processing = [];
145149
this.retrieving = 0;
146150

@@ -246,33 +250,16 @@ Queue.prototype.off = Queue.prototype.removeListener;
246250

247251
Queue.prototype._init = function(name){
248252
var _this = this;
249-
var initializers = [this.client, this.eclient].map(function (client) {
250-
var _resolve, errorHandler;
251-
return new Promise(function(resolve, reject) {
252-
_resolve = resolve;
253-
errorHandler = function(err){
254-
if(err.code !== 'ECONNREFUSED'){
255-
reject(err);
256-
}
257-
};
258-
client.once('ready', resolve);
259-
client.on('error', errorHandler);
260-
}).finally(function(){
261-
client.removeListener('ready', _resolve);
262-
client.removeListener('error', errorHandler);
263-
});
264-
});
265253

266-
this._initializing = Promise.all(initializers).then(function(){
267-
return _this.eclient.psubscribe(_this.toKey('') + '*');
268-
}).then(function(){
269-
return commands(_this.client);
270-
}).then(function(){
271-
debuglog(name + ' queue ready');
272-
}, function(err){
273-
_this.emit('error', err, 'Error initializing queue');
274-
throw err;
275-
});
254+
this._initializing = _this.eclient.psubscribe(_this.toKey('') + '*')
255+
.then(function(){
256+
return commands(_this.client);
257+
}).then(function(){
258+
debuglog(name + ' queue ready');
259+
}, function(err){
260+
_this.emit('error', err, 'Error initializing queue');
261+
throw err;
262+
});
276263
};
277264

278265
Queue.prototype._setupQueueEventListeners = function(){
@@ -375,7 +362,7 @@ Queue.prototype.close = function( doNotWaitJobs ){
375362
_.each(_this.errorRetryTimer, function(timer){
376363
clearTimeout(timer);
377364
});
378-
clearTimeout(_this.delayTimer);
365+
_this.delayTimer.cancel();
379366
clearInterval(_this.guardianTimer);
380367
clearInterval(_this.moveUnlockedJobsToWaitInterval);
381368
_this.timers.clearAll();
@@ -423,6 +410,58 @@ Queue.prototype.process = function(name, concurrency, handler){
423410
});
424411
};
425412

413+
//
414+
// This code will be called everytime a job is going to be processed if the job has a repeat option. (from delay -> active).
415+
//
416+
var parser = require('cron-parser');
417+
418+
function nextRepeatableJob(queue, name, data, opts){
419+
var repeat = opts.repeat;
420+
var repeatKey = queue.toKey('repeat') + ':' + name + ':' + repeat.cron;
421+
422+
//
423+
// Get millis for this repeatable job.
424+
//
425+
return queue.client.get(repeatKey).then(function(millis){
426+
if(millis){
427+
return parseInt(millis);
428+
}else{
429+
return Date.now();
430+
}
431+
}).then(function(millis){
432+
var interval = parser.parseExpression(repeat.cron, _.defaults({
433+
currentDate: new Date(millis)
434+
}, repeat));
435+
var nextMillis;
436+
try{
437+
nextMillis = interval.next();
438+
} catch(e){
439+
// Ignore error
440+
}
441+
442+
if(nextMillis){
443+
nextMillis = nextMillis.getTime();
444+
var delay = nextMillis - millis;
445+
446+
//
447+
// Generate unique job id for this iteration.
448+
//
449+
var customId = 'repeat:' + name + ':' + nextMillis;
450+
451+
//
452+
// Set key and add job should be atomic.
453+
//
454+
return queue.client.set(repeatKey, nextMillis).then(function(){
455+
return Job.create(queue, name, data, _.extend(_.clone(opts), {
456+
jobId: customId,
457+
delay: delay < 0 ? 0 : delay,
458+
timestamp: Date.now()
459+
}));
460+
});
461+
}
462+
});
463+
};
464+
426465
Queue.prototype.start = function(concurrency){
427466
var _this = this;
428467
return this.run(concurrency).catch(function(err){
@@ -449,6 +488,11 @@ Queue.prototype.setHandler = function(name, handler){
449488
interface JobOptions
450489
{
451490
attempts: number;
491+
492+
repeat: {
493+
tz?: string,
494+
endDate?: Date | string | number
495+
}
452496
}
453497
*/
454498

@@ -459,7 +503,11 @@ interface JobOptions
459503
@param opts: JobOptions Options for this job.
460504
*/
461505
Queue.prototype.add = function(name, data, opts){
462-
return Job.create(this, name, data, opts);
506+
if(opts && opts.repeat){
507+
return nextRepeatableJob(this, name || DEFAULT_JOB_NAME, data, opts);
508+
}else{
509+
return Job.create(this, name, data, opts);
510+
}
463511
};
464512

465513
/**
@@ -588,14 +636,15 @@ Queue.prototype.run = function(concurrency){
588636
*/
589637
Queue.prototype.updateDelayTimer = function(newDelayedTimestamp){
590638
var _this = this;
639+
newDelayedTimestamp = Math.round(newDelayedTimestamp);
591640
if(newDelayedTimestamp < _this.delayedTimestamp && newDelayedTimestamp < (MAX_TIMEOUT_MS + Date.now())){
592-
clearTimeout(this.delayTimer);
641+
this.delayTimer.cancel();
593642
this.delayedTimestamp = newDelayedTimestamp;
594643

595644
var nextDelayedJob = newDelayedTimestamp - Date.now();
596-
nextDelayedJob = nextDelayedJob < 0 ? 0 : nextDelayedJob;
645+
var delay = nextDelayedJob <= 0 ? Promise.resolve() : Promise.delay(nextDelayedJob);
597646

598-
this.delayTimer = setTimeout(function(){
647+
this.delayTimer = delay.then(function(){
599648
scripts.updateDelaySet(_this, _this.delayedTimestamp).then(function(nextTimestamp){
600649
if(nextTimestamp){
601650
nextTimestamp = nextTimestamp < Date.now() ? Date.now() : nextTimestamp;
@@ -607,7 +656,7 @@ Queue.prototype.updateDelayTimer = function(newDelayedTimestamp){
607656
_this.emit('error', err, 'Error updating the delay timer');
608657
});
609658
_this.delayedTimestamp = Number.MAX_VALUE;
610-
}, nextDelayedJob);
659+
});
611660
}
612661
};
613662

@@ -791,7 +840,13 @@ Queue.prototype.getNextJob = function() {
791840

792841
return scripts.moveToActive(this).spread(function(jobData, jobId){
793842
if(jobData){
794-
return Job.fromData(_this, jobData, jobId);
843+
var job = Job.fromData(_this, jobData, jobId);
844+
if(job.opts.repeat){
845+
return nextRepeatableJob(_this, job.name, job.data, job.opts).then(function(){
846+
return job;
847+
});
848+
}
849+
return job;
795850
}else{
796851
return newJobs;
797852
}

package.json

+17-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
{
22
"name": "bull",
3+
<<<<<<< HEAD
34
"version": "3.0.0-alpha.3",
5+
=======
6+
"version": "3.0.0-alpha.4",
7+
>>>>>>> 9fa6d0445c5c3cf5d09d049ea45d0923871adb58
48
"description": "Job manager",
59
"main": "./lib/queue",
610
"repository": {
@@ -18,16 +22,18 @@
1822
"readmeFilename": "README.md",
1923
"dependencies": {
2024
"bluebird": "^3.5.0",
25+
"cron-parser": "^2.4.0",
2126
"debuglog": "^1.0.0",
22-
"ioredis": "^3.0.0-1",
27+
"ioredis": "^3.0.0-2",
2328
"lodash": "^4.17.4",
2429
"semver": "^5.3.0",
2530
"uuid": "^3.0.1"
2631
},
2732
"devDependencies": {
33+
"chai": "^3.5.0",
2834
"eslint": "^2.13.1",
2935
"expect.js": "^0.3.1",
30-
"mocha": "^2.5.3",
36+
"mocha": "^3.3.0",
3137
"sinon": "^1.17.7"
3238
},
3339
"scripts": {
@@ -52,7 +58,15 @@
5258
"camelcase": 1,
5359
"no-unused-vars": 1,
5460
"no-alert": 1,
55-
"no-console": [2, {"allow": ["warn", "error"]}],
61+
"no-console": [
62+
2,
63+
{
64+
"allow": [
65+
"warn",
66+
"error"
67+
]
68+
}
69+
],
5670
"quotes": [
5771
2,
5872
"single"

test/test_job.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ describe('Job', function(){
9797
});
9898
});
9999

100-
it('fails to remove a locked job', function(done) {
100+
it('fails to remove a locked job', function() {
101101
return Job.create(queue, 1, {foo: 'bar'}).then(function(job) {
102102
return job.takeLock().then(function(lock) {
103103
expect(lock).to.be.truthy;
@@ -108,7 +108,7 @@ describe('Job', function(){
108108
}).then(function() {
109109
throw new Error('Should not be able to remove a locked job');
110110
}).catch(function(/*err*/) {
111-
done();
111+
// Good!
112112
});
113113
});
114114
});

0 commit comments

Comments
 (0)