bull
Version:
Job manager
848 lines (729 loc) • 21.4 kB
JavaScript
/// <reference path='../typings/mocha/mocha.d.ts'/>
/*eslint-env node */
;
var Queue = require('../lib/priority-queue');
var expect = require('expect.js');
var Promise = require('bluebird');
var sinon = require('sinon');
var _ = require('lodash');
var uuid = require('node-uuid');
var redis = require('redis');
var STD_QUEUE_NAME = 'test queue';
function buildQueue(name){
var qName = name || STD_QUEUE_NAME;
return new Queue(qName, 6379, '127.0.0.1');
}
function cleanupQueue(queue){
return queue.empty().then(queue.close.bind(queue));
}
describe('Priority queue', function(){
var queue;
var sandbox = sinon.sandbox.create();
beforeEach(function(){
var client = redis.createClient();
return client.flushdbAsync();
});
afterEach(function(){
if(queue){
return cleanupQueue(queue).then(function(){
queue = undefined;
});
}
sandbox.restore();
});
it('allow custom clients', function(){
var clients = 0;
queue = new Queue(STD_QUEUE_NAME, {
redis: {
opts: {
createClient: function(){
clients++;
return redis.createClient();
}
}
}
});
expect(clients).to.be(15);
});
describe('.close', function(){
var testQueue;
beforeEach(function(){
testQueue = buildQueue('test close');
});
it('should return a promise', function(){
var closePromise = testQueue.close().then(function(){
expect(closePromise).to.be.a(Promise);
});
});
describe('should be callable from within', function(){
it('a job handler that takes a callback', function(done){
this.timeout(6000);
testQueue.process(function(job, jobDone){
expect(job.data.foo).to.be('bar');
testQueue.close().then(function(){
done();
});
jobDone();
});
testQueue.add({
foo: 'bar'
}).then(function(job){
expect(job.jobId).to.be.ok();
expect(job.data.foo).to.be('bar');
});
});
it('a job handler that returns a promise', function(done){
this.timeout(6000);
testQueue.process(function(job){
expect(job.data.foo).to.be('bar');
testQueue.close().then(function(){
done();
});
return Promise.resolve();
});
testQueue.add({
foo: 'bar'
}).then(function(job){
expect(job.jobId).to.be.ok();
expect(job.data.foo).to.be('bar');
});
});
});
});
it('creates a queue with dots in its name', function(){
queue = new Queue('using. dots. in.name.');
return queue.add({
foo: 'bar'
}).then(function(job){
expect(job.jobId).to.be.ok();
expect(job.data.foo).to.be('bar');
}).then(function(){
queue.process(function(job, jobDone){
expect(job.data.foo).to.be.equal('bar');
jobDone();
});
});
});
it('processes jobs by priority', function(done){
queue = buildQueue();
var normalPriority = [],
mediumPriority = [],
highPriority = [];
// for the current strategy this number should not exceed 8 (2^2*2)
// this is done to maitain a deterministic output.
var numJobsPerPriority = 6;
for(var i = 0; i < numJobsPerPriority; i++){
normalPriority.push(queue.add({p: 1}, {priority: 'normal'}));
mediumPriority.push(queue.add({p: 2}, {priority: 'medium'}));
highPriority.push(queue.add({p: 3}, {priority: 'high'}));
}
// wait for all jobs to enter the queue and then start processing
Promise
.all(normalPriority, mediumPriority, highPriority)
.then(function(){
var currentPriority = 3;
var counter = 0;
queue.process(function(job, jobDone){
expect(job.jobId).to.be.ok();
expect(job.data.p).to.be(currentPriority);
jobDone();
if(++counter === numJobsPerPriority){
currentPriority--;
counter = 0;
if(currentPriority < 1){
done();
}
}
});
});
});
it('process a job', function(done){
queue = buildQueue();
queue.process(function(job, jobDone){
expect(job.data.foo).to.be.equal('bar');
jobDone();
done();
});
queue.add({
foo: 'bar'
}).then(function(job){
expect(job.jobId).to.be.ok();
expect(job.data.foo).to.be('bar');
}).catch(done);
});
it('process a job that updates progress', function(done){
queue = buildQueue();
queue.process(function(job, jobDone){
expect(job.data.foo).to.be.equal('bar');
job.progress(42);
jobDone();
});
queue.add({
foo: 'bar'
}).then(function(job){
expect(job.jobId).to.be.ok();
expect(job.data.foo).to.be('bar');
}).catch(done);
queue.on('progress', function(job, progress){
expect(job).to.be.ok();
expect(progress).to.be.eql(42);
done();
});
});
it('process a job that returns data in the process handler', function(done){
queue = buildQueue();
queue.process(function(job, jobDone){
expect(job.data.foo).to.be.equal('bar');
jobDone(null, 37);
});
queue.add({
foo: 'bar'
}).then(function(job){
expect(job.jobId).to.be.ok();
expect(job.data.foo).to.be('bar');
}).catch(done);
queue.on('completed', function(job, data){
expect(job).to.be.ok();
expect(data).to.be.eql(37);
done();
});
});
it('process stalled jobs when starting a queue', function(done){
this.timeout(16000);
var queueStalled = buildQueue('test queue stalled');
queueStalled.setLockRenewTime(10);
var jobs = [
queueStalled.add({
bar: 'baz'
}),
queueStalled.add({
bar1: 'baz1'
}),
queueStalled.add({
bar2: 'baz2'
}),
queueStalled.add({
bar3: 'baz3'
})
];
queueStalled.empty().then(function(){
Promise.all(jobs).then(function(){
return queueStalled.process(function(){
// instead of completing we just force-close the queue to simulate a crash.
return queueStalled.close( true ).then(function(){
var queue2 = buildQueue('test queue stalled');
queue2.once('ready', function() {
var doneAfterFour = _.after(4, function(){
queue2.close().then(function(){
done()
}, done);
});
queue2.on('completed', function(){
doneAfterFour();
});
queue2.process(function(job, jobDone){
jobDone();
});
});
});
// The sudden simulated crash will throw,
// catch it here to let the tests continue normally
}).catch(function () {
return Promise.resolve();
});
}).catch(done);
});
});
it('processes jobs that were added before the queue backend started', function(){
var queueStalled = buildQueue('test queue added before');
queueStalled.setLockRenewTime(10);
var jobs = [
queueStalled.add({
bar: 'baz'
}),
queueStalled.add({
bar1: 'baz1'
}),
queueStalled.add({
bar2: 'baz2'
}),
queueStalled.add({
bar3: 'baz3'
})
];
return Promise.all(jobs)
.then(queueStalled.close.bind(queueStalled))
.then(function(){
queue = buildQueue('test queue added before');
queue.process(function(job, jobDone){
jobDone();
});
return new Promise(function(resolve){
var resolveAfterAllJobs = _.after(jobs.length, resolve);
queue.on('completed', resolveAfterAllJobs);
});
});
});
it.skip('processes several stalled jobs when starting several queues', function(done){
this.timeout(5000);
var NUM_QUEUES = 5;
var NUM_JOBS_PER_QUEUE = 10;
var stalledQueues = [];
var jobs = [];
for(var i = 0; i < NUM_QUEUES; i++){
var stalledQueue = buildQueue('test queue stalled 2');
stalledQueues.push(stalledQueue);
stalledQueue.setLockRenewTime(10);
for(var j = 0; j < NUM_JOBS_PER_QUEUE; j++){
jobs.push(stalledQueue.add({
job: j
}));
}
}
Promise.all(jobs).then(function(){
var processed = 0;
var procFn = function(){
// instead of completing we just force-close the queue to simulate a crash.
this.disconnect().then(function(){
processed++;
if(processed === stalledQueues.length){
setTimeout(function(){
var queue2 = buildQueue('test queue stalled 2');
queue2.process(function(job2, jobDone){
jobDone();
});
var counter = 0;
queue2.on('completed', function(){
counter++;
if(counter === NUM_QUEUES * NUM_JOBS_PER_QUEUE){
queue2.close().then(function(){
done();
});
}
});
}, 100);
}
});
};
for(var k = 0; k < stalledQueues.length; k++){
stalledQueues[k].process(procFn);
}
});
});
it('does not process a job that is being processed when a new queue starts', function(done){
this.timeout(5000);
var err = null;
var anotherQueue;
var queueName = uuid();
queue = buildQueue(queueName);
queue.add({
foo: 'bar'
}).then(function(addedJob){
queue.process(function(job, jobDone){
expect(job.data.foo).to.be.equal('bar');
if(addedJob.jobId !== job.jobId){
err = new Error('Processed job id does not match that of added job');
}
anotherQueue = buildQueue(queueName);
anotherQueue.process(function(job2, jobDone2){
err = new Error('The second queue should not have received a job to process');
jobDone2();
});
setTimeout(jobDone, 100);
});
queue.on('completed', function(){
cleanupQueue(anotherQueue).then(done.bind(null, err));
});
});
});
it.skip('process stalled jobs without requiring a queue restart');
it('process a job that fails', function(done){
var jobError = new Error('Job Failed');
queue = buildQueue();
queue.process(function(job, jobDone){
expect(job.data.foo).to.be.equal('bar');
jobDone(jobError);
});
queue.add({
foo: 'bar'
}).then(function(job){
expect(job.jobId).to.be.ok();
expect(job.data.foo).to.be('bar');
}, function(err){
done(err);
});
queue.once('failed', function(job, err){
expect(job.jobId).to.be.ok();
expect(job.data.foo).to.be('bar');
expect(err).to.be.eql(jobError);
done();
});
});
it('process a job that throws an exception', function(done){
var jobError = new Error('Job Failed');
queue = buildQueue();
queue.process(function(job){
expect(job.data.foo).to.be.equal('bar');
throw jobError;
});
queue.add({
foo: 'bar'
}).then(function(job){
expect(job.jobId).to.be.ok();
expect(job.data.foo).to.be('bar');
}, function(err){
done(err);
});
queue.once('failed', function(job, err){
expect(job.jobId).to.be.ok();
expect(job.data.foo).to.be('bar');
expect(err).to.be.eql(jobError);
done();
});
});
it('process several jobs serially', function(done){
var counter = 1;
var maxJobs = 100;
queue = buildQueue();
queue.process(function(job, jobDone){
expect(job.data.num).to.be.equal(counter);
expect(job.data.foo).to.be.equal('bar');
jobDone();
if(counter === maxJobs){
done();
}
counter++;
});
for(var i = 1; i <= maxJobs; i++){
queue.add({
foo: 'bar',
num: i
});
}
});
it('count added, unprocessed jobs', function(){
var maxJobs = 100;
var added = [];
queue = buildQueue();
for(var i = 1; i <= maxJobs; i++){
added.push(queue.add({
foo: 'bar',
num: i
}));
}
return Promise.all(added)
.then(queue.count.bind(queue))
.then(function(count){
expect(count).to.be(100);
})
.then(queue.empty.bind(queue))
.then(queue.count.bind(queue))
.then(function(count){
expect(count).to.be(0);
});
});
it('add jobs to a paused queue', function(done){
var ispaused = false,
counter = 2;
queue = buildQueue();
queue.process(function(job, jobDone){
expect(ispaused).to.be(false);
expect(job.data.foo).to.be.equal('paused');
jobDone();
counter--;
if(counter === 0){
done();
}
});
queue.pause();
ispaused = true;
queue.add({
foo: 'paused'
});
queue.add({
foo: 'paused'
});
setTimeout(function(){
ispaused = false;
queue.resume();
}, 100); // We hope that this was enough to trigger a process if
// we were not paused.
});
it('paused a running queue', function(done){
var ispaused = false,
isresumed = true,
first = true;
queue = buildQueue();
queue.process(function(job, jobDone){
expect(ispaused).to.be(false);
expect(job.data.foo).to.be.equal('paused');
jobDone();
if(first){
first = false;
queue.pause();
ispaused = true;
} else{
expect(isresumed).to.be(true);
done();
}
});
queue.add({
foo: 'paused'
});
queue.add({
foo: 'paused'
});
queue.on('paused', function(){
setTimeout(function(){
ispaused = false;
queue.resume();
}, 100); // We hope that this was enough to trigger a process if
});
queue.on('resumed', function(){
isresumed = true;
});
});
it('process a lifo queue', function(done){
var currentValue = 0,
first = true;
queue = new Queue('test lifo');
queue.once('ready', function(){
queue.process(function(job, jobDone){
// Catching the job before the pause
if(first){
expect(job.data.count).to.be.equal(0);
first = false;
return jobDone();
}
expect(job.data.count).to.be.equal(currentValue--);
jobDone();
if(currentValue === 0){
done();
}
});
// Add a job to pend proccessing
queue.add({
'count': 0
}).then(function(){
Promise.delay(500).then(function(){
queue.pause().then(function(){
// Add a series of jobs in a predictable order
var fn = function(cb){
queue.add({
'count': ++currentValue
}, {
'lifo': true
}).then(cb);
};
fn(fn(fn(fn(function(){
queue.resume();
}))));
});
});
});
});
});
describe('Jobs getters', function(){
it('should get waitting jobs', function(done){
queue = buildQueue();
Promise.join(queue.add({
foo: 'bar'
}), queue.add({
baz: 'qux'
})).then(function(){
queue.getWaiting().then(function(jobs){
expect(jobs).to.be.a('array');
expect(jobs.length).to.be.equal(2);
expect(jobs[1].data.foo).to.be.equal('bar');
expect(jobs[0].data.baz).to.be.equal('qux');
done();
});
});
});
it('should get active jobs', function(done){
queue = buildQueue();
queue.process(function(job, jobDone){
queue.getActive().then(function(jobs){
expect(jobs).to.be.a('array');
expect(jobs.length).to.be.equal(1);
expect(jobs[0].data.foo).to.be.equal('bar');
done();
});
jobDone();
});
queue.add({
foo: 'bar'
});
});
it('should get completed jobs', function(done){
var counter = 2;
queue = buildQueue();
queue.process(function(job, jobDone){
jobDone();
});
queue.on('completed', function(){
counter--;
if(counter === 0){
queue.getCompleted().then(function(jobs){
expect(jobs).to.be.a('array');
// We need a 'empty completed' kind of function.
//expect(jobs.length).to.be.equal(2);
done();
});
}
});
queue.add({
foo: 'bar'
});
queue.add({
baz: 'qux'
});
});
it('should get failed jobs', function(done){
var counter = 2;
queue = buildQueue();
queue.process(function(job, jobDone){
jobDone(new Error('Forced error'));
});
queue.on('failed', function(){
counter--;
if(counter === 0){
queue.getFailed().then(function(jobs){
expect(jobs).to.be.a('array');
done();
});
}
});
queue.add({
foo: 'bar'
});
queue.add({
baz: 'qux'
});
});
it('fails jobs that exceed their specified timeout', function(done){
queue = buildQueue();
queue.process(function(job, jobDone){
setTimeout(jobDone, 150);
});
queue.on('failed', function(job, error){
expect(error).to.be.a(Promise.TimeoutError);
done();
});
queue.on('completed', function(){
var error = new Error('The job should have timed out');
done(error);
});
queue.add({
some: 'data'
}, {
timeout: 100
});
});
});
describe('Cleaner', function () {
beforeEach(function () {
queue = buildQueue('cleaner' + uuid());
});
it('should reject the cleaner with no grace', function(done){
queue.clean().then(function () {
done(new Error('Promise should not resolve'));
}, function (err) {
expect(err).to.be.a(Error);
done();
});
});
it('should reject the cleaner an unknown type', function (done) {
queue.clean(0, 'bad').then(function () {
done(new Error('Promise should not resolve'));
}, function (e) {
expect(e).to.be.a(Error);
done();
});
});
it('should clean an empty queue', function (done) {
queue.clean(0);
queue.on('error', function (err) {
done(err);
});
queue.on('cleaned', function (jobs, type) {
expect(type).to.be('completed');
expect(jobs.length).to.be(0);
done();
});
});
it('should clean two jobs from the queue', function (done) {
queue.add({some: 'data'}, {priority: 'normal'});
queue.add({some: 'data'}, {priority: 'normal'});
queue.process(function (job, jobDone) {
jobDone();
});
Promise.delay(100).then(function () {
return queue.clean(0);
}).then(function (jobs) {
expect(jobs.length).to.be(2);
done();
}, function (err) {
done(err);
});
});
it('should only remove a job outside of the grace period', function (done) {
queue.process(function (job, jobDone) {
jobDone();
});
queue.add({some: 'data'}, {priority: 'normal'});
queue.add({some: 'data'}, {priority: 'normal'});
Promise.delay(200).then(function () {
queue.add({some: 'data'});
queue.clean(100);
}).delay(100).then(function () {
return queue.getCompleted();
}).then(function (jobs) {
expect(jobs.length).to.be(1);
return queue.empty();
}).then(function () {
queue = undefined;
done();
});
});
it('should clean all failed jobs', function (done) {
queue.add({some: 'data'}, {priority: 'normal'});
queue.add({some: 'data'}, {priority: 'normal'});
queue.process(function (job, jobDone) {
jobDone(new Error('It failed'));
});
Promise.delay(100).then(function () {
return queue.clean(0, 'failed');
}).then(function (jobs) {
expect(jobs.length).to.be(2);
return queue.count();
}).then(function(len) {
expect(len).to.be(0);
queue = undefined;
done();
});
});
it('should clean a job without a timestamp', function (done) {
var client = redis.createClient(6379, '127.0.0.1', {});
queue.add({some: 'data'}, {priority: 'normal'});
queue.add({some: 'data'}, {priority: 'normal'});
queue.process(function (job, jobDone) {
jobDone(new Error('It failed'));
});
Promise.delay(100).then(function () {
return new Promise(function(resolve) {
client.hdel('bull:' + queue.name + ':1', 'timestamp', resolve);
});
}).then(function() {
return queue.clean(0, 'failed');
}).then(function (jobs) {
expect(jobs.length).to.be(2);
return queue.getFailed();
}).then(function(failed) {
expect(failed.length).to.be(0);
queue = undefined;
done();
});
});
});
});