node-resque
Version:
an opinionated implementation of resque in node
371 lines (342 loc) • 11.4 kB
JavaScript
var util = require('util');
var connection = require(__dirname + "/connection.js").connection;
var pluginRunner = require(__dirname + "/pluginRunner.js");
var queue = function(options, jobs, callback){
var self = this;
if(typeof jobs == 'function' && callback === undefined){
callback = jobs;
jobs = {};
}
self.options = options;
self.jobs = jobs;
self.queueObject = self; // to keep plugins consistant
self.runPlugin = pluginRunner.runPlugin;
self.runPlugins = pluginRunner.runPlugins;
self.connection = new connection(options.connection);
self.connection.connect(function(err){
if(typeof callback === 'function'){ callback(err); }
});
};
queue.prototype.end = function(callback){
var self = this;
self.connection.redis.quit();
process.nextTick(function(){
if(typeof callback === 'function'){ callback(); }
});
};
queue.prototype.encode = function(q, func, args){
return JSON.stringify({
"class": func,
queue: q,
args: args || []
});
};
queue.prototype.enqueue = function(q, func, args, callback){
var self = this;
if(arguments.length === 3 && typeof args === 'function'){
callback = args;
args = [];
}else if(arguments.length < 3){
args = [];
}
args = arrayify(args);
var job = self.jobs[func];
self.runPlugins('before_enqueue', func, q, job, args, function(err, toRun){
if(toRun === false){
if(typeof callback === 'function'){ callback(err, toRun); }
}else{
self.connection.ensureConnected(callback, function(){
self.connection.redis.sadd(self.connection.key('queues'), q, function(){
self.connection.redis.rpush(self.connection.key('queue', q), self.encode(q, func, args), function(){
self.runPlugins('after_enqueue', func, q, job, args, function(){
if(typeof callback === 'function'){ callback(err, toRun); }
});
});
});
});
}
});
};
queue.prototype.enqueueAt = function(timestamp, q, func, args, callback){
// Don't run plugins here, they should be run by scheduler at the enqueue step
var self = this;
if(arguments.length === 4 && typeof args === 'function'){
callback = args;
args = [];
}else if(arguments.length < 4){
args = [];
}
args = arrayify(args);
self.connection.ensureConnected(callback, function(){
var item = self.encode(q, func, args);
var rTimestamp = Math.round(timestamp / 1000); // assume timestamp is in ms
// enqueue the encoded job into a list per timestmp to be popped and workered later
self.connection.redis.rpush(self.connection.key("delayed:" + rTimestamp), item, function(){
// save the job + args into a set so that it can be checked by plugins
self.connection.redis.sadd(self.connection.key("timestamps:" + item), self.connection.key("delayed:" + rTimestamp), function(){
// and the timestamp in question to a zset to the scheduler will know which timestamps have data to work
self.connection.redis.zadd(self.connection.key('delayed_queue_schedule'), rTimestamp, rTimestamp, function(){
if(typeof callback === 'function'){ callback(); }
});
});
});
});
};
queue.prototype.enqueueIn = function(time, q, func, args, callback){
var self = this;
if(arguments.length === 4 && typeof args === 'function'){
callback = args;
args = [];
}else if(arguments.length < 4){
args = [];
}
args = arrayify(args);
var timestamp = (new Date().getTime()) + time;
self.enqueueAt(timestamp, q, func, args, function(){
if(typeof callback === 'function'){ callback(); }
});
};
queue.prototype.queues = function(callback){
var self = this;
self.connection.ensureConnected(callback, function(){
self.connection.redis.smembers(self.connection.key('queues'), function(err, queues){
callback(err, queues);
});
});
};
queue.prototype.delQueue = function(q, callback){
var self = this;
self.connection.redis.del(self.connection.key('queue', q), function(err){
callback(err);
});
};
queue.prototype.length = function(q, callback){
var self = this;
self.connection.ensureConnected(callback, function(){
self.connection.redis.llen(self.connection.key('queue', q), function(err, length){
callback(err, length);
});
});
};
queue.prototype.del = function(q, func, args, count, callback){
var self = this;
if(typeof count === 'function' && callback === undefined){
callback = count;
count = 0;
}else if(arguments.length === 3){
if(typeof args === 'function'){
callback = args;
args = [];
}
count = 0;
}else if(arguments.length < 3){
args = [];
count = 0;
}
args = arrayify(args);
self.connection.ensureConnected(callback, function(){
self.connection.redis.lrem(self.connection.key('queue', q), count, self.encode(q, func, args), function(err, count){
if(typeof callback === 'function'){ callback(err, count); }
});
});
};
queue.prototype.delDelayed = function(q, func, args, callback){
var self = this;
if(arguments.length === 3 && typeof args === 'function'){
callback = args;
args = [];
}else if(arguments.length < 3){
args = [];
}
args = arrayify(args);
var search = self.encode(q, func, args);
self.connection.ensureConnected(callback, function(){
var timestamps = self.connection.redis.smembers(self.connection.key("timestamps:" + search), function(err, members){
if(members.length === 0 ){ if(typeof callback === 'function'){ callback(err, []); } }
else{
var started = 0;
var timestamps = [];
members.forEach(function(key){
started++;
self.connection.redis.lrem(key, 0, search, function(){
self.connection.redis.srem(self.connection.key("timestamps:" + search), key, function(){
timestamps.push(key.split(":")[key.split(":").length - 1]);
started--;
if(started === 0){
if(typeof callback === 'function'){ callback(err, timestamps); }
}
});
});
});
}
});
});
};
queue.prototype.scheduledAt = function(q, func, args, callback){
var self = this;
if(arguments.length === 3 && typeof args === 'function'){
callback = args;
args = [];
}else if(arguments.length < 3){
args = [];
}
args = arrayify(args);
var search = self.encode(q, func, args);
self.connection.ensureConnected(callback, function(){
self.connection.redis.smembers(self.connection.key("timestamps:" + search), function(err, members){
var timestamps = [];
if(members !== null){
members.forEach(function(key){
timestamps.push(key.split(":")[key.split(":").length - 1]);
});
}
if(typeof callback === 'function'){ callback(err, timestamps); }
});
});
};
queue.prototype.timestamps = function(callback){
var self = this;
var results = [];
self.connection.redis.keys(self.connection.key("delayed:*"), function(err, timestamps){
timestamps.forEach(function(timestamp){
var parts = timestamp.split(":");
results.push(parseInt(parts[(parts.length - 1)]) * 1000);
});
results.sort();
callback(err, results);
});
};
queue.prototype.delayedAt = function(timestamp, callback){
var self = this;
var rTimestamp = Math.round(timestamp / 1000); // assume timestamp is in ms
var tasks = [];
self.connection.redis.lrange(self.connection.key("delayed:" + rTimestamp), 0, -1, function(err, items){
items.forEach(function(i){
tasks.push( JSON.parse(i) );
});
callback(err, tasks, rTimestamp);
});
};
queue.prototype.allDelayed = function(callback){
var self = this;
var started = 0;
var results = {};
self.timestamps(function(err, timestamps){
if(timestamps.length === 0){
callback(err, {});
}else{
timestamps.forEach(function(timestamp){
started++;
self.delayedAt(timestamp, function(err, tasks, rTimestamp){
results[(rTimestamp * 1000)] = tasks;
started--;
if(started === 0){ callback(err, results); }
});
});
}
});
};
queue.prototype.workers = function(callback){
var self = this;
var workers = {};
self.connection.redis.smembers(self.connection.key('workers'), function(err, results){
if(results){
results.forEach(function(r){
var parts = r.split(':');
var name, queues;
if(parts.length === 1){
name = parts[0];
workers[name] = null;
}
else if(parts.length === 2){
name = parts[0];
queues = parts[1];
workers[name] = queues;
}else{
name = parts.shift() + ":" + parts.shift();
queues = parts.join(':');
workers[name] = queues;
}
});
}
if(typeof callback === 'function'){ callback(err, workers); }
});
};
queue.prototype.workingOn = function(workerName, queues, callback){
var self = this;
var fullWorkerName = workerName + ':' + queues;
self.connection.redis.get(self.connection.key('worker', fullWorkerName), function(err, data){
if(typeof callback === 'function'){ callback(err, data); }
});
};
queue.prototype.allWorkingOn = function(callback){
var self = this;
var results = {};
var counter = 0;
self.workers(function(err, workers){
if(err && typeof callback === 'function'){
callback(err, results);
}else if(!workers || hashLength(workers) === 0){
callback(null, results);
}else{
for(var w in workers){
counter++;
results[w] = 'started';
self.workingOn(w, workers[w], function(err, data){
counter--;
if(data){
data = JSON.parse(data);
results[data.worker] = data;
}
if(counter === 0 && typeof callback === 'function'){
callback(err, results);
}
});
}
}
});
};
queue.prototype.failedCount = function(callback){
var self = this;
self.connection.redis.llen(self.connection.key('failed'), function(err, length){
callback(err, length);
});
};
queue.prototype.failed = function(start, stop, callback){
var self = this;
var results = [];
self.connection.redis.lrange(self.connection.key('failed'), start, stop, function(err, data){
data.forEach(function(d){ results.push( JSON.parse(d) ); });
callback(err, results);
});
};
queue.prototype.removeFailed = function(failedJob, callback){
var self = this;
self.connection.redis.lrem(self.connection.key('failed'), 1, JSON.stringify(failedJob), callback);
};
queue.prototype.retryAndRemoveFailed = function(failedJob, callback){
var self = this;
self.removeFailed(failedJob, function(err, countFailed){
if(err){return callback(err, failedJob); }
if(countFailed < 1 ){return callback(new Error('This job is not in failed queue'), failedJob); }
self.enqueue(failedJob.queue, failedJob.payload.class, failedJob.payload.args, callback);
});
};
/////////////
// helpers //
/////////////
var arrayify = function(o){
if(Array.isArray(o)){
return o;
}else{
return [o];
}
};
var hashLength = function(obj) {
var size = 0, key;
for(key in obj){
if(obj.hasOwnProperty(key)){ size++; }
}
return size;
};
exports.queue = queue;