node-resque
Version:
an opinionated implementation of resque in node
257 lines (229 loc) • 7.34 kB
JavaScript
// To read notes about the master locking scheme, check out:
// https://github.com/resque/resque-scheduler/blob/master/lib/resque/scheduler/locking.rb
var EventEmitter = require('events').EventEmitter;
var util = require('util');
var os = require('os');
var connection = require(__dirname + '/connection.js').connection;
var queue = require(__dirname + '/queue.js').queue;
var scheduler = function(options, jobs){
var self = this;
if(!jobs){ jobs = {}; }
var defaults = self.defaults();
for(var i in defaults){
if(options[i] === null || options[i] === undefined){
options[i] = defaults[i];
}
}
self.options = options;
self.name = self.options.name;
self.master = false;
self.running = false;
self.processing = false;
self.queue = new queue({connection: options.connection}, jobs);
self.queue.on('error', function(error){
self.emit('error', error);
});
};
util.inherits(scheduler, EventEmitter);
scheduler.prototype.defaults = function(){
var self = this;
return {
timeout: 5000, // in ms
masterLockTimeout: 60 * 3, // in seconds
name: os.hostname() + ':' + process.pid, // assumes only one worker per node process
};
};
scheduler.prototype.connect = function(callback){
var self = this;
self.queue.connect(function(){
self.connection = self.queue.connection;
if(typeof callback === 'function'){ callback(); }
});
};
scheduler.prototype.start = function(){
var self = this;
self.processing = false;
if(!self.running){
self.emit('start');
self.running = true;
self.timer = setTimeout((function(){
self.poll();
}), self.options.timeout);
}
};
scheduler.prototype.end = function(callback){
var self = this;
self.running = false;
clearTimeout(self.timer);
if(self.processing === false){
self.releaseMasterLock(function(error, wasMaster){
if(error){ self.emit('error', error); }
self.queue.end(function(){
self.emit('end');
process.nextTick(function(){
if(typeof callback === 'function'){ callback(); }
});
});
});
}
else{
setTimeout(function(){
self.end(callback);
}, (self.options.timeout / 2));
}
};
scheduler.prototype.poll = function(callback){
var self = this;
self.processing = true;
clearTimeout(self.timer);
self.tryForMaster(function(error, isMaster){
if(error){ self.emit('error', error); }
if(isMaster){
if(!self.master){
self.master = true;
self.emit('master');
}
self.emit('poll');
self.nextDelayedTimestamp(function(error, timestamp){
if(!error && timestamp){
self.emit('working_timestamp', timestamp);
self.enqueueDelayedItemsForTimestamp(timestamp, function(error){
if(error){ self.emit('error', error); }
self.poll(callback);
});
}else{
if(error){ self.emit('error', error); }
self.processing = false;
self.pollAgainLater();
if(typeof callback === 'function'){ callback(); }
}
});
}else{
self.master = false;
self.processing = false;
self.pollAgainLater();
if(typeof callback === 'function'){ callback(); }
}
});
};
scheduler.prototype.pollAgainLater = function(){
var self = this;
if(self.running === true){
self.timer = setTimeout(function(){
self.poll();
}, self.options.timeout);
}
};
scheduler.prototype.masterKey = function(){
var self = this;
return self.connection.key('resque_scheduler_master_lock');
};
scheduler.prototype.tryForMaster = function(callback){
var self = this;
if(!self.connection || !self.connection.redis){
return callback();
}
self.connection.redis.setnx(self.masterKey(), self.options.name, function(error, locked){
if(error){ return callback(error); }
else if(locked === true || locked === 1){
self.connection.redis.expire(self.masterKey(), self.options.masterLockTimeout, function(error){
return callback(error, true);
});
}else{
self.connection.redis.get(self.masterKey(), function(error, value){
if(error){ return callback(error); }
else if(value === self.options.name){
self.connection.redis.expire(self.masterKey(), self.options.masterLockTimeout, function(error){
return callback(error, true);
});
}else{
return callback(null, false);
}
});
}
});
};
scheduler.prototype.releaseMasterLock = function(callback){
var self = this;
if(self.connection){
self.tryForMaster(function(error, isMaster){
if(error){ return callback(error); }
else if(!isMaster){ return callback(null, false); }
else{
self.connection.redis.del(self.masterKey(), function(error, delted){
self.master = false;
return callback(error, (delted === 1));
});
}
});
}else{
return callback();
}
};
scheduler.prototype.nextDelayedTimestamp = function(callback){
var self = this;
var time = Math.round(new Date().getTime() / 1000);
self.connection.redis.zrangebyscore(self.connection.key('delayed_queue_schedule'), '-inf', time, 'limit', 0, 1, function(error, items){
if(error || items === null || items.length === 0){
return callback(error);
}else{
return callback(null, items[0]);
}
});
};
scheduler.prototype.enqueueDelayedItemsForTimestamp = function(timestamp, callback){
var self = this;
self.nextItemForTimestamp(timestamp, function(error, job){
if(!error && job){
self.transfer(timestamp, job, function(){
self.enqueueDelayedItemsForTimestamp(timestamp, callback);
});
}else{
return callback(error);
}
});
};
scheduler.prototype.nextItemForTimestamp = function(timestamp, callback){
var self = this;
var key = self.connection.key('delayed:' + timestamp);
self.connection.redis.lpop(key, function(error, job){
if(error){
return callback(error);
}else{
self.connection.redis.srem(self.connection.key('timestamps:' + job), ('delayed:' + timestamp), function(error){
self.cleanupTimestamp(timestamp, function(){
if(error){
return callback(error);
}else{
return callback(null, JSON.parse(job));
}
});
});
}
});
};
scheduler.prototype.transfer = function(timestamp, job, callback){
var self = this;
self.queue.enqueue(job.queue, job['class'], job.args, function(error){
if(error){ self.emit('error', error); }
self.emit('transferred_job', timestamp, job);
return callback();
});
};
scheduler.prototype.cleanupTimestamp = function(timestamp, callback){
var self = this;
var key = self.connection.key('delayed:' + timestamp);
self.connection.redis.llen(key, function(error, len){
if(error){ return callback(error); }
if(len === 0){
self.connection.redis.del(key, function(error){
if(error){ return callback(error); }
self.connection.redis.zrem(self.connection.key('delayed_queue_schedule'), timestamp, function(error){
return callback(error);
});
});
}
else{ return callback(); }
});
};
exports.scheduler = scheduler;