UNPKG

node-resque

Version:

an opinionated implementation of resque in node

647 lines (569 loc) 22.7 kB
var specHelper = require(__dirname + '/../_specHelper.js').specHelper; var should = require('should'); describe('queue', function(){ var queue; it('can connect', function(done){ queue = new specHelper.NR.queue({connection: specHelper.connectionDetails, queue: specHelper.queue}); queue.connect(function(error){ should.not.exist(error); should.exist(queue); queue.end(done); }); }); it('can provide an error if connection failed', function(done){ // Only run this test if this is using real redis if(process.env.FAKEREDIS === 'true' || process.env.FAKEREDIS === true){ return done(); } var connectionDetails = { pkg: specHelper.connectionDetails.pkg, host: 'wronghostname', password: specHelper.connectionDetails.password, port: specHelper.connectionDetails.port, database: specHelper.connectionDetails.database, namespace: specHelper.connectionDetails.namespace, }; queue = new specHelper.NR.queue({connection: connectionDetails, queue: specHelper.queue}); queue.connect(function(){ throw new Error('should not get here'); }); queue.on('error', function(error){ error.message.should.match(/getaddrinfo ENOTFOUND/); queue.end(done); }); }); describe('[with connection]', function(){ before(function(done){ specHelper.connect(function(){ queue = new specHelper.NR.queue({connection: specHelper.connectionDetails, queue: specHelper.queue}); queue.connect(done); }); }); beforeEach(function(done){ specHelper.cleanup(function(){ done(); }); }); after(function(done){ specHelper.cleanup(function(){ done(); }); }); it('can add a normal job', function(done){ queue.enqueue(specHelper.queue, 'someJob', [1, 2, 3], function(){ specHelper.popFromQueue(function(err, obj){ should.exist(obj); obj = JSON.parse(obj); obj['class'].should.equal('someJob'); obj.args.should.eql([1, 2, 3]); done(); }); }); }); it('can add delayed job (enqueueAt)', function(done){ queue.enqueueAt(10000, specHelper.queue, 'someJob', [1, 2, 3], function(error){ should.not.exist(error); specHelper.redis.zscore(specHelper.namespace + ':delayed_queue_schedule', '10', function(err, score){ String(score).should.equal('10'); specHelper.redis.lpop(specHelper.namespace + ':delayed:' + '10', function(err, obj){ should.exist(obj); obj = JSON.parse(obj); obj['class'].should.equal('someJob'); obj.args.should.eql([1, 2, 3]); done(); }); }); }); }); it('will not enqueue a delayed job at the same time with matching params', function(done){ queue.enqueueAt(10000, specHelper.queue, 'someJob', [1, 2, 3], function(error){ should.not.exist(error); queue.enqueueAt(10000, specHelper.queue, 'someJob', [1, 2, 3], function(error){ String(error).should.equal('Error: Job already enqueued at this time with same arguments'); done(); }); }); }); it('can add delayed job (enqueueIn)', function(done){ var now = Math.round(new Date().getTime() / 1000) + 5; queue.enqueueIn(5 * 1000, specHelper.queue, 'someJob', [1, 2, 3], function(){ specHelper.redis.zscore(specHelper.namespace + ':delayed_queue_schedule', now, function(err, score){ String(score).should.equal(String(now)); specHelper.redis.lpop(specHelper.namespace + ':delayed:' + now, function(err, obj){ should.exist(obj); obj = JSON.parse(obj); obj['class'].should.equal('someJob'); obj.args.should.eql([1, 2, 3]); done(); }); }); }); }); it('can get the number of jobs currently enqueued', function(done){ queue.enqueue(specHelper.queue, 'someJob', [1, 2, 3], function(){ queue.enqueue(specHelper.queue, 'someJob', [1, 2, 3], function(){ queue.length(specHelper.queue, function(err, len){ len.should.equal(2); done(); }); }); }); }); it('can get the jobs in the queue', function(done){ queue.enqueue(specHelper.queue, 'someJob', [1, 2, 3], function(){ queue.enqueue(specHelper.queue, 'someJob', [4, 5, 6], function(){ queue.queued(specHelper.queue, 0, -1, function(err, jobs){ jobs.length.should.equal(2); jobs[0].args.should.eql([1, 2, 3]); jobs[1].args.should.eql([4, 5, 6]); done(); }); }); }); }); it('can find previously scheduled jobs', function(done){ queue.enqueueAt(10000, specHelper.queue, 'someJob', [1, 2, 3], function(){ queue.scheduledAt(specHelper.queue, 'someJob', [1, 2, 3], function(err, timestamps){ timestamps.length.should.equal(1); timestamps[0].should.equal('10'); done(); }); }); }); it('will not match previously scheduled jobs with differnt args', function(done){ queue.enqueueAt(10000, specHelper.queue, 'someJob', [1, 2, 3], function(){ queue.scheduledAt(specHelper.queue, 'someJob', [3, 2, 1], function(err, timestamps){ timestamps.length.should.equal(0); done(); }); }); }); it('can deleted an enqued job', function(done){ queue.enqueue(specHelper.queue, 'someJob', [1, 2, 3], function(){ queue.length(specHelper.queue, function(err, len){ len.should.equal(1); queue.del(specHelper.queue, 'someJob', [1, 2, 3], function(){ queue.length(specHelper.queue, function(err, len){ len.should.equal(0); done(); }); }); }); }); }); it('can deleted a delayed job', function(done){ queue.enqueueAt(10000, specHelper.queue, 'someJob', [1, 2, 3], function(){ queue.delDelayed(specHelper.queue, 'someJob', [1, 2, 3], function(err, timestamps){ timestamps.length.should.equal(1); timestamps[0].should.equal('10'); done(); }); }); }); it('can delete a delayed job, and delayed queue should be empty', function(done){ queue.enqueueAt(10000, specHelper.queue, 'someJob', [1, 2, 3], function(){ queue.delDelayed(specHelper.queue, 'someJob', [1, 2, 3], function(err, timestamps){ queue.allDelayed(function(err, hash){ hash.should.be.empty(); timestamps.length.should.equal(1); timestamps[0].should.equal('10'); done(); }); }); }); }); it('can handle single arguments without explicit array', function(done){ queue.enqueue(specHelper.queue, 'someJob', 1, function(){ specHelper.popFromQueue(function(err, obj){ JSON.parse(obj).args.should.eql([1]); done(); }); }); }); it('allows omitting arguments when enqueuing', function(done){ queue.enqueue(specHelper.queue, 'noParams'); // no callback here, but in practice will finish before next enqueue calls back queue.enqueue(specHelper.queue, 'noParams', function(){ queue.length(specHelper.queue, function(err, len){ len.should.equal(2); specHelper.popFromQueue(function(err, obj){ obj = JSON.parse(obj); obj['class'].should.equal('noParams'); obj.args.should.be.empty; specHelper.popFromQueue(function(err, obj){ obj = JSON.parse(obj); obj['class'].should.equal('noParams'); obj.args.should.be.empty; done(); }); }); }); }); }); it('allows omitting arguments when deleting', function(done){ queue.enqueue(specHelper.queue, 'noParams', [], function(){ queue.enqueue(specHelper.queue, 'noParams', [], function(){ queue.length(specHelper.queue, function(err, len){ len.should.equal(2); queue.del(specHelper.queue, 'noParams', function(err, let){ should.not.exist(err); len.should.equal(2); queue.del(specHelper.queue, 'noParams', function(err, len){ should.not.exist(err); len.should.equal(0); queue.length(specHelper.queue, function(err, len){ len.should.equal(0); done(); }); }); }); }); }); }); }); it('allows omitting arguments when adding delayed job', function(done){ queue.allDelayed(function(error, hash){ should.not.exist(error); hash.should.be.empty; queue.enqueueAt(10000, specHelper.queue, 'noParams', function(error){ should.not.exist(error); queue.enqueueIn(11000, specHelper.queue, 'noParams', function(error){ should.not.exist(error); queue.enqueueAt(12000, specHelper.queue, 'noParams', function(error){ should.not.exist(error); queue.enqueueIn(13000, specHelper.queue, 'noParams', function(error){ should.not.exist(error); queue.scheduledAt(specHelper.queue, 'noParams', function(error, timestamps){ should.not.exist(error); timestamps.length.should.equal(4); queue.allDelayed(function(error, hash){ should.not.exist(error); Object.keys(hash).length.should.equal(4); for(var key in hash){ hash[key][0].args.should.be.empty; } done(); }); }); }); }); }); }); }); }); it('allows omitting arguments when deleting a delayed job', function(done){ queue.allDelayed(function(err, hash){ hash.should.be.empty; queue.enqueueAt(10000, specHelper.queue, 'noParams'); queue.enqueueAt(12000, specHelper.queue, 'noParams', function(){ queue.allDelayed(function(err, hash){ Object.keys(hash).length.should.equal(2); queue.delDelayed(specHelper.queue, 'noParams', function(){ queue.delDelayed(specHelper.queue, 'noParams', function(){ queue.allDelayed(function(err, hash){ hash.should.be.empty; done(); }); }); }); }); }); }); }); it('can load stats', function(done){ queue.connection.redis.set(specHelper.namespace + ':stat:failed', 1); queue.connection.redis.set(specHelper.namespace + ':stat:processed', 2); queue.stats(function(err, stats){ should.not.exist(err); stats.processed.should.equal('2'); stats.failed.should.equal('1'); done(); }); }); describe('locks', function(){ beforeEach(function(done){ queue.connection.redis.set(queue.connection.key('lock:lists:queueName:jobName:[{}]'), 123, done); }); beforeEach(function(done){ queue.connection.redis.set(queue.connection.key('workerslock:lists:queueName:jobName:[{}]'), 456, done); }); afterEach(function(done){ queue.connection.redis.del(queue.connection.key('lock:lists:queueName:jobName:[{}]'), done); }); afterEach(function(done){ queue.connection.redis.del(queue.connection.key('workerslock:lists:queueName:jobName:[{}]'), done); }); it('can get locks', function(done){ queue.locks(function(err, locks){ should.not.exist(err); Object.keys(locks).length.should.equal(2); locks['lock:lists:queueName:jobName:[{}]'].should.equal('123'); locks['workerslock:lists:queueName:jobName:[{}]'].should.equal('456'); done(); }); }); it('can remove locks', function(done){ queue.locks(function(err, locks){ should.not.exist(err); Object.keys(locks).length.should.equal(2); queue.delLock('workerslock:lists:queueName:jobName:[{}]', function(err, count){ should.not.exist(err); count.should.equal(1); done(); }); }); }); }); describe('failed job managment', function(){ beforeEach(function(done){ var errorPayload = function(id){ return JSON.stringify({ worker: 'busted-worker-' + id, queue: 'busted-queue', payload: { 'class': 'busted_job', queue: 'busted-queue', args: [1, 2, 3] }, exception: 'ERROR_NAME', error: 'I broke', failed_at: (new Date()).toString() }); }; queue.connection.redis.rpush(queue.connection.key('failed'), errorPayload(1), function(){ queue.connection.redis.rpush(queue.connection.key('failed'), errorPayload(2), function(){ queue.connection.redis.rpush(queue.connection.key('failed'), errorPayload(3), function(){ done(); }); }); }); }); it('can list how many failed jobs there are', function(done){ queue.failedCount(function(err, failedCount){ should.not.exist(err); failedCount.should.equal(3); done(); }); }); it('can get the body content for a collection of failed jobs', function(done){ queue.failed(1, 2, function(err, failedJobs){ should.not.exist(err); failedJobs.length.should.equal(2); failedJobs[0].worker.should.equal('busted-worker-2'); failedJobs[0].queue.should.equal('busted-queue'); failedJobs[0].exception.should.equal('ERROR_NAME'); failedJobs[0].error.should.equal('I broke'); failedJobs[0].payload.args.should.eql([1, 2, 3]); failedJobs[1].worker.should.equal('busted-worker-3'); failedJobs[1].queue.should.equal('busted-queue'); failedJobs[1].exception.should.equal('ERROR_NAME'); failedJobs[1].error.should.equal('I broke'); failedJobs[1].payload.args.should.eql([1, 2, 3]); done(); }); }); it('can remove a failed job by payload', function(done){ queue.failed(1, 1, function(err, failedJobs){ failedJobs.length.should.equal(1); queue.removeFailed(failedJobs[0], function(err, removedJobs){ should.not.exist(err); removedJobs.should.equal(1); queue.failedCount(function(err, failedCount){ failedCount.should.equal(2); done(); }); }); }); }); it('can re-enqueue a specific job, removing it from the failed queue', function(done){ queue.failed(0, 999, function(err, failedJobs){ failedJobs.length.should.equal(3); failedJobs[2].worker.should.equal('busted-worker-3'); queue.retryAndRemoveFailed(failedJobs[2], function(err, retriedJob){ should.not.exist(err); queue.failed(0, 999, function(err, failedJobs){ failedJobs.length.should.equal(2); failedJobs[0].worker.should.equal('busted-worker-1'); failedJobs[1].worker.should.equal('busted-worker-2'); done(); }); }); }); }); it('will return an error when trying to retry a job not in the failed queue', function(done){ queue.failed(0, 999, function(err, failedJobs){ failedJobs.length.should.equal(3); var failedJob = failedJobs[2]; failedJob.worker = 'a-fake-worker'; queue.retryAndRemoveFailed(failedJob, function(err, retriedJob){ String(err).should.eql('Error: This job is not in failed queue'); queue.failed(0, 999, function(err, failedJobs){ failedJobs.length.should.equal(3); done(); }); }); }); }); }); describe('delayed status', function(){ beforeEach(function(done){ queue.enqueueAt(10000, specHelper.queue, 'job1', [1, 2, 3], function(){ queue.enqueueAt(10000, specHelper.queue, 'job2', [1, 2, 3], function(){ queue.enqueueAt(20000, specHelper.queue, 'job3', [1, 2, 3], function(){ done(); }); }); }); }); it('can list the timestamps that exist', function(done){ queue.timestamps(function(err, timestamps){ should.not.exist(err); timestamps.length.should.equal(2); timestamps[0].should.equal(10000); timestamps[1].should.equal(20000); done(); }); }); it('can list the jobs delayed at a timestamp', function(done){ queue.delayedAt(10000, function(err, tasks_a){ should.not.exist(err); tasks_a.length.should.equal(2); tasks_a[0]['class'].should.equal('job1'); tasks_a[1]['class'].should.equal('job2'); queue.delayedAt(20000, function(err, tasks_b){ should.not.exist(err); tasks_b.length.should.equal(1); tasks_b[0]['class'].should.equal('job3'); done(); }); }); }); it('can also return a hash with all delayed tasks', function(done){ queue.allDelayed(function(err, hash){ Object.keys(hash).length.should.equal(2); Object.keys(hash)[0].should.equal('10000'); Object.keys(hash)[1].should.equal('20000'); hash['10000'].length.should.equal(2); hash['20000'].length.should.equal(1); done(); }); }); }); describe('worker status', function(){ var workerA; var workerB; var timeout = 500; var jobs = { 'slowJob': { perform: function(callback){ setTimeout(function(){ callback(null); }, timeout); } } }; beforeEach(function(done){ workerA = new specHelper.NR.worker({ connection: specHelper.connectionDetails, timeout: specHelper.timeout, queues: specHelper.queue, name: 'workerA' }, jobs); workerB = new specHelper.NR.worker({ connection: specHelper.connectionDetails, timeout: specHelper.timeout, queues: specHelper.queue, name: 'workerB' }, jobs); workerA.connect(function(){ workerB.connect(function(){ workerA.init(function(){ workerB.init(function(){ done(); }); }); }); }); }); afterEach(function(done){ workerA.end(function(){ workerB.end(function(){ done(); }); }); }); it('can list running workers', function(done){ queue.workers(function(err, workers){ should.not.exist(err); workers.workerA.should.equal('test_queue'); workers.workerB.should.equal('test_queue'); done(); }); }); it('we can see what workers are working on (idle)', function(done){ queue.allWorkingOn(function(err, data){ should.not.exist(err); data.should.containEql({'workerA': 'started'}); data.should.containEql({'workerB': 'started'}); done(); }); }); it('we can see what workers are working on (active)', function(done){ var listener = workerA.on('job', function(q, job, failure){ workerA.removeAllListeners('job'); queue.allWorkingOn(function(err, data){ should.not.exist(err); data.should.containEql({'workerB': 'started'}); var paylaod = data.workerA.payload; paylaod.queue.should.equal('test_queue'); paylaod['class'].should.equal('slowJob'); done(); }); }); queue.enqueue(specHelper.queue, 'slowJob'); workerA.start(); }); it('can remove stuck workers', function(done){ var age = 1; var listener = workerA.on('job', function(q, job, failure){ workerA.removeAllListeners('job'); queue.allWorkingOn(function(err, data){ var paylaod = data.workerA.payload; paylaod.queue.should.equal('test_queue'); paylaod['class'].should.equal('slowJob'); queue.cleanOldWorkers(age, function(err, data){ should.not.exist(err); Object.keys(data).length.should.equal(1); data.workerA.queue.should.equal('test_queue'); data.workerA.worker.should.equal('workerA'); data.workerA.payload['class'].should.equal('slowJob'); specHelper.redis.rpop(specHelper.namespace + ':' + 'failed', function(err, data){ data = JSON.parse(data); data.queue.should.equal(specHelper.queue); data.exception.should.equal('Worker Timeout (killed manually)'); data.error.should.equal('Worker Timeout (killed manually)'); data.payload['class'].should.equal('slowJob'); queue.allWorkingOn(function(err, data){ Object.keys(data).length.should.equal(1); data.workerB.should.equal('started'); done(); }); }); }); }); }); queue.enqueue(specHelper.queue, 'slowJob'); workerA.start(); }); it('will not remove stuck jobs within the timelimit', function(done){ var age = 999; var listener = workerA.on('job', function(q, job, failure){ workerA.removeAllListeners('job'); queue.cleanOldWorkers(age, function(err, data){ should.not.exist(err); Object.keys(data).length.should.equal(0); queue.allWorkingOn(function(err, data){ var paylaod = data.workerA.payload; paylaod.queue.should.equal('test_queue'); paylaod['class'].should.equal('slowJob'); done(); }); }); }); queue.enqueue(specHelper.queue, 'slowJob'); workerA.start(); }); }); }); });