relyq
Version:
A reliable redis-backed queue.
395 lines (334 loc) • 10.8 kB
JavaScript
// relyq
// A reliable task queue
// builtin
var util = require('util'),
EventEmitter = require('events').EventEmitter;
// vendor
var async = require('async'),
_ = require('underscore'),
simpleq = require('simpleq'),
uuid = require('uuid');
// local
var DeferredTaskList = require('./deferred'),
RecurringTaskList = require('./recurring');
// -- Master Type: Q --
// The master type, a task queue
function Q(redis, preopts) {
// handle forgetting a 'new'
if (!(this instanceof Q)) {
return new Q(redis, preopts);
}
if (preopts === undefined) preopts = redis, redis = preopts.redis;
if (!redis && preopts.createRedis) {
this._redis = redis = preopts.createRedis()
} else {
console.error('WARNING: passing a redis instance to relyq is deprecated. Please pass options.createRedis() function.')
this._redis = redis
}
this._options = typeof preopts === 'string' ? {prefix: preopts, redis: redis} : preopts
this._delimeter = preopts.delimeter || ':';
this._idfield = preopts.idfield || 'id';
this._prefix = preopts.prefix || preopts;
this._clean_finish = preopts.clean_finish === undefined || preopts.clean_finish;
this._keep_storage = preopts.clean_finish === 'keep_storage';
this.todo = new simpleq.Q(redis, this._prefix + this._delimeter + 'todo');
this.doing = new simpleq.Q(redis, this._prefix + this._delimeter + 'doing');
this.failed = new simpleq.Q(redis, this._prefix + this._delimeter + 'failed');
if (!this._clean_finish) {
this.done = new simpleq.Q(redis, this._prefix + this._delimeter + 'done');
}
if (preopts.allow_defer && preopts.createRedis) {
this.deferred = new DeferredTaskList(this, {
polling_interval: preopts.defer_polling_interval,
key: this._prefix + this._delimeter + 'deferred',
redis: preopts.createRedis(),
});
}
if (preopts.allow_recur && preopts.createRedis) {
this.recurring = new RecurringTaskList(this, {
polling_interval: preopts.recur_polling_interval,
key: this._prefix + this._delimeter + 'recurring',
redis: preopts.createRedis(),
});
}
EventEmitter.call(this);
var rq = this;
if (this._redis.ready) {
setImmediate(function () {
rq.emit('ready')
})
} else {
this._redis.once('ready', function () {
rq.emit('ready')
})
}
}
util.inherits(Q, EventEmitter);
// @overridable
// Get a task object from its object
Q.prototype.get = function get(taskref, callback) {
callback(null, taskref);
};
// @overridable
// Set a task object and return its reference ID
Q.prototype.set = function set(taskobj, taskref, callback) {
callback();
};
// @overridable
// Delete the task obj and return its reference ID
Q.prototype.del = function del(taskref, callback) {
callback();
};
// End the listeners that might be listening
Q.prototype.end = function (callback) {
var redis = this._redis
if (this.deferred) {
this.deferred.end();
}
if (this.recurring) {
this.recurring.end();
}
if (this._sql) {
this._sql.once('end', cb).end();
} else {
cb();
}
function cb(err) {
redis.end();
callback(err);
}
}
// -- Superclass methods ---
// refs get put into the queue
Q.prototype.ref = function (task) {
return task[this._idfield] || (task[this._idfield] = uuid.v4());
};
Q.prototype.getclean = function getclean(taskref, callback) {
var self = this;
async.waterfall([
_.bind(this.get, this, taskref),
function (taskobj, cb) {
delete(taskobj[self._idfield]);
cb(null, taskobj);
}
], callback);
}
Q.prototype.push = function push(task, callback) {
var ref = this.ref(task);
async.parallel([
_.bind(this.set, this, task, ref),
_.bind(this.todo.push, this.todo, ref)
], function (err, results) {
callback(err, results && results.length === 2 && results[1]);
});
};
Q.prototype.defer = function defer(task, when, callback) {
if (!this.deferred) {
throw new Error('Must use option allow_defer to allow defer calls.');
}
var ref = this.ref(task);
async.parallel([
_.bind(this.set, this, task, ref),
_.bind(this.deferred.defer, this.deferred, ref, when),
], function (err, results) {
callback(err, results && results.length === 2 && results[1]);
});
};
// Remove from deferred list and delete
Q.prototype.undefer_remove = function undefer_remove(taskref, callback) {
if (!this.deferred) {
throw new Error('Must use option allow_defer to allow undefer calls.');
}
async.parallel([
_.bind(this.del, this, taskref),
_.bind(this.deferred.eliminate, this.deferred, taskref),
], function (err, results) {
callback(err, results && results.length === 2 && results[1]);
});
};
// Remove from deferred list and immediately process
Q.prototype.undefer_push = function undefer_push(taskref, callback) {
if (!this.deferred) {
throw new Error('Must use option allow_defer to allow undefer calls.');
}
this.deferred.immediate(taskref, callback)
};
Q.prototype.recur = function recur(task, every, callback) {
if (!this.recurring) {
throw new Error('Must use option allow_recur to allow recur calls.');
}
var ref = this.ref(task);
async.parallel([
_.bind(this.set, this, task, ref),
_.bind(this.recurring.recur, this.recurring, ref, every),
], function (err, results) {
callback(err, results && results.length === 2 && results[1]);
});
};
Q.prototype.process = function process(callback) {
async.waterfall([
_.bind(this.todo.poppipe, this.todo, this.doing),
_.bind(this.get, this)
], callback);
};
Q.prototype.bprocess = function bprocess(timeout, callback) {
if (callback === undefined) {
callback = timeout;
timeout = 0;
}
async.waterfall([
_.bind(this.todo.bpoppipe, this.todo, this.doing, timeout),
_.bind(this.get, this)
], callback);
};
Q.prototype.finish = function finish() {
if (this._clean_finish) {
this._finish_clean.apply(this, arguments);
} else {
this._finish_dirty.apply(this, arguments);
}
};
Q.prototype.fail = function fail(task, optional_error, callback) {
if (callback === undefined) callback = optional_error, optional_error = undefined;
if (optional_error) task.error = optional_error instanceof Error ? optional_error.stack : optional_error;
var ref = this.ref(task);
async.parallel([
_.bind(this.set, this, task, ref),
_.bind(this.doing.spullpipe, this.doing, this.failed, ref)
], function (err, results) {
if (err) {
return callback(err);
}
if (results && results[1] === 0) {
return callback(new Error('Element ' + (_.isObject(task) ? JSON.stringify(task) : task.toString()) + ' is not currently processing.'));
}
callback(null, results[1]);
});
};
Q.prototype.remove = function remove(from, task, dontdel, callback) {
if (callback === undefined) {
callback = dontdel;
dontdel = false;
}
var ref = this.ref(task);
if (dontdel) {
return this[from].pull(ref, callback);
}
async.parallel([
_.bind(this.del, this, ref),
_.bind(this[from].pull, this[from], ref)
], function (err, results) {
callback(err, results && results.length === 2 && results[1]);
});
};
// Start a process listener
/*
Example Usage
var listener = rq.listen({
max_out: 10, // maximum tasks to emit at one time
})
.on('error', function (err, optional_taskref) {
if (taskref) {...}
else {...}
})
.on('task', function (task, done) {
// do task
done(error_or_not); // This will call rq.fail or rq.finish!
});
// some time later
listener.end();
*/
Q.prototype.listen = function rqlistener(opts) {
if (!this._options.createRedis) {
throw new Error('createRedis function must be passed in as an option for listening to be used.')
}
var rq = this,
sql = this._sql = rq.todo.poppipelisten(rq.doing, _.extend(opts||{}, {redisClone: this._options.createRedis()}));
sql.on('message', function (taskref, done) {
async.waterfall([
function (cb) {
rq.get(taskref, function (err, obj) {
cb(err, obj||null)
})
},
function (taskobj, cb) {
if (!taskobj) {
return cb(new Error('storage did not return a valid task object for reference: ' + taskref))
}
sql.emit('task', taskobj, newdone);
var called = false;
function newdone (err) {
if (called) {
return;
}
called = true;
if (err) {
rq.fail(taskobj, err, cb);
} else {
rq.finish(taskobj, cb);
}
}
}
], function (err) {
if (err) {
sql.emit('error', err, taskref);
}
done();
});
});
return sql;
};
module.exports = Q;
// -- Finish Helpers --
Q.prototype._finish_clean = function finish_clean(task, dontCheckFailed, callback) {
if (callback === undefined) callback = dontCheckFailed, dontCheckFailed = false;
var ref = this.ref(task),
self = this;
async.auto({
setTask: this._keep_storage ? this.set.bind(this, task, ref) : this.del.bind(this, ref),
sPullPipe: _.bind(this.doing.pull, this.doing, ref),
checkFailed: ['sPullPipe', function (cb, results) {
if (results.sPullPipe === 0) {
if (dontCheckFailed) {
return cb(null, 0);
}
return self.failed.pull(ref, cb);
}
cb(null, results.sPullPipe);
}],
last: ['checkFailed', function (cb, results) {
if (results.checkFailed === 0) {
return callback(new Error('Element ' + (_.isObject(task) ? JSON.stringify(task) : task.toString()) + ' is not currently processing or failed.'));
}
callback(null);
}]
}, function (err, results) {
callback(err, results.checkFailed);
});
};
Q.prototype._finish_dirty = function finish_dirty(task, dontCheckFailed, callback) {
if (callback === undefined) callback = dontCheckFailed, dontCheckFailed = false;
var ref = this.ref(task),
self = this;
async.auto({
setTask: _.bind(this.set, this, task, ref),
sPullPipe: _.bind(this.doing.spullpipe, this.doing, this.done, ref),
checkFailed: ['sPullPipe', function (cb, results) {
if (results.sPullPipe === 0) {
if (dontCheckFailed) {
return cb(null, 0);
}
return self.failed.spullpipe(self.done, ref, cb);
}
cb(null, results.sPullPipe);
}],
last: ['checkFailed', function (cb, results) {
if (results.checkFailed === 0) {
return callback(new Error('Element ' + (_.isObject(task) ? JSON.stringify(task) : task.toString()) + ' is not currently processing or failed.'));
}
callback(null);
}]
}, function (err, results) {
callback(err, results.checkFailed);
});
};