bull-redlock
Version:
A node.js redlock implementation for distributed redis locks
429 lines (386 loc) • 13.3 kB
JavaScript
;
var assert = require('chai').assert;
var Promise = require('bluebird');
var Redlock = require('./redlock');
test('https://www.npmjs.com/package/redis', [require('redis').createClient()]);
test('https://www.npmjs.com/package/ioredis', [new (require('ioredis'))()]);
/* istanbul ignore next */
function test(name, clients){
var redlock = new Redlock(clients, {
retryCount: 2,
retryDelay: 150,
retryJitter: 0
});
var resource = 'Redlock:test:resource';
var error = 'Redlock:test:error';
describe('Redlock: ' + name, function(){
before(function(done) {
var err;
var l = clients.length; function cb(e){ if(e) err = e; l--; if(l === 0) done(err); }
for (var i = clients.length - 1; i >= 0; i--) {
clients[i].sadd(error, 'having a set here should cause a failure', cb);
}
});
it('should throw an error if not passed any clients', function(){
assert.throws(function(){
new Redlock([], {
retryCount: 2,
retryDelay: 150,
retryJitter: 0
});
});
});
it('emits a clientError event when a client error occurs', function(done){
var emitted = 0;
function test(err) {
assert.isNotNull(err);
emitted++;
}
redlock.on('clientError', test);
redlock.lock(error, 200, function(err, lock){
redlock.removeListener('clientError', test);
assert.isNotNull(err);
assert.equal(emitted, 3);
done();
});
});
it('supports custom script functions in options', function(){
var opts = {
lockScript: function(lockScript) { return lockScript + 'and 1'; },
unlockScript: function(unlockScript) { return unlockScript + 'and 2'; },
extendScript: function(extendScript) { return extendScript + 'and 3'; }
};
var customRedlock = new Redlock(clients, opts);
var i = 1;
assert.equal(customRedlock.lockScript, redlock.lockScript + 'and ' + i++);
assert.equal(customRedlock.unlockScript, redlock.unlockScript + 'and ' + i++);
assert.equal(customRedlock.extendScript, redlock.extendScript + 'and ' + i);
});
describe('callbacks', function(){
before(function(done) {
var err;
var l = clients.length; function cb(e){ if(e) err = e; l--; if(l === 0) done(err); }
for (var i = clients.length - 1; i >= 0; i--) {
clients[i].del(resource, cb);
}
});
var one;
it('should lock a resource', function(done) {
redlock.lock(resource, 200, function(err, lock){
if(err) throw err;
assert.isObject(lock);
assert.instanceOf(lock, Redlock.Lock);
assert.isAbove(lock.expiration, Date.now()-1);
one = lock;
done();
});
});
var two;
var two_expiration;
it('should wait until a lock expires before issuing another lock', function(done) {
assert(one, 'Could not run because a required previous test failed.');
redlock.lock(resource, 800, function(err, lock){
if(err) throw err;
assert.isObject(lock);
assert.isAbove(lock.expiration, Date.now()-1);
assert.isAbove(Date.now()+1, one.expiration);
two = lock;
two_expiration = lock.expiration;
done();
});
});
it('should unlock a resource', function(done) {
assert(two, 'Could not run because a required previous test failed.');
two.unlock(done);
assert.equal(two.expiration, 0, 'Failed to immediately invalidate the lock.');
});
it('should unlock an already-unlocked resource', function(done) {
assert(two, 'Could not run because a required previous test failed.');
two.unlock(done);
});
it('should error when unable to fully release a resource', function(done) {
assert(two, 'Could not run because a required previous test failed.');
var failingTwo = Object.create(two);
failingTwo.resource = error;
failingTwo.unlock(function(err) {
assert.isNotNull(err);
done();
});
});
it('should fail to extend a lock on an already-unlocked resource', function(done) {
assert(two, 'Could not run because a required previous test failed.');
two.extend(200, function(err, lock){
assert.isNotNull(err);
assert.instanceOf(err, Redlock.LockError);
done();
});
});
var three;
it('should issue another lock immediately after a resource is unlocked', function(done) {
assert(two_expiration, 'Could not run because a required previous test failed.');
redlock.lock(resource, 800, function(err, lock){
if(err) throw err;
assert.isObject(lock);
assert.isAbove(lock.expiration, Date.now()-1);
assert.isBelow(Date.now()-1, two_expiration);
three = lock;
done();
});
});
var four;
it('should extend an unexpired lock', function(done) {
assert(three, 'Could not run because a required previous test failed.');
three.extend(800, function(err, lock){
if(err) throw err;
assert.isObject(lock);
assert.isAbove(lock.expiration, Date.now()-1);
assert.isAbove(lock.expiration, three.expiration-1);
assert.equal(three, lock);
four = lock;
done();
});
});
it('should fail after the maximum retry count is exceeded', function(done) {
assert(four, 'Could not run because a required previous test failed.');
redlock.lock(resource, 200, function(err, lock){
assert.isNotNull(err);
assert.instanceOf(err, Redlock.LockError);
done();
});
});
it('should fail to extend an expired lock', function(done) {
assert(four, 'Could not run because a required previous test failed.');
setTimeout(function(){
three.extend(800, function(err, lock){
assert.isNotNull(err);
assert.instanceOf(err, Redlock.LockError);
done();
});
}, four.expiration - Date.now() + 100);
});
it('should issue another lock immediately after a resource is expired', function(done) {
assert(four, 'Could not run because a required previous test failed.');
redlock.lock(resource, 800, function(err, lock){
if(err) throw err;
assert.isObject(lock);
assert.isAbove(lock.expiration, Date.now()-1);
done();
});
});
after(function(done) {
var err;
var l = clients.length; function cb(e){ if(e) err = e; l--; if(l === 0) done(err); }
for (var i = clients.length - 1; i >= 0; i--) {
clients[i].del(resource, cb);
}
});
});
describe('promises', function(){
before(function(done) {
var err;
var l = clients.length; function cb(e){ if(e) err = e; l--; if(l === 0) done(err); }
for (var i = clients.length - 1; i >= 0; i--) {
clients[i].del(resource, cb);
}
});
var one;
it('should lock a resource', function(done) {
redlock.lock(resource, 200)
.done(function(lock){
assert.isObject(lock);
assert.isAbove(lock.expiration, Date.now()-1);
one = lock;
done();
}, done);
});
var two;
var two_expiration;
it('should wait until a lock expires before issuing another lock', function(done) {
assert(one, 'Could not run because a required previous test failed.');
redlock.lock(resource, 800)
.done(function(lock){
assert.isObject(lock);
assert.isAbove(lock.expiration, Date.now()-1);
assert.isAbove(Date.now()+1, one.expiration);
two = lock;
two_expiration = lock.expiration;
done();
}, done);
});
it('should unlock a resource', function(done) {
assert(two, 'Could not run because a required previous test failed.');
two.unlock().done(done, done);
});
it('should unlock an already-unlocked resource', function(done) {
assert(two, 'Could not run because a required previous test failed.');
two.unlock().done(done, done);
});
it('should error when unable to fully release a resource', function(done) {
assert(two, 'Could not run because a required previous test failed.');
var failingTwo = Object.create(two);
failingTwo.resource = error;
failingTwo.unlock().done(done, function(err) {
assert.isNotNull(err);
done();
});
});
it('should fail to extend a lock on an already-unlocked resource', function(done) {
assert(two, 'Could not run because a required previous test failed.');
two.extend(200)
.done(function(){
done(new Error('Should have failed with a LockError'));
}, function(err){
assert.instanceOf(err, Redlock.LockError);
done();
});
});
var three;
it('should issue another lock immediately after a resource is unlocked', function(done) {
assert(two_expiration, 'Could not run because a required previous test failed.');
redlock.lock(resource, 800)
.done(function(lock){
assert.isObject(lock);
assert.isAbove(lock.expiration, Date.now()-1);
assert.isBelow(Date.now()-1, two_expiration);
three = lock;
done();
}, done);
});
var four;
it('should extend an unexpired lock', function(done) {
assert(three, 'Could not run because a required previous test failed.');
three.extend(800)
.done(function(lock){
assert.isObject(lock);
assert.isAbove(lock.expiration, Date.now()-1);
assert.isAbove(lock.expiration, three.expiration-1);
assert.equal(three, lock);
four = lock;
done();
}, done);
});
it('should fail after the maximum retry count is exceeded', function(done) {
assert(four, 'Could not run because a required previous test failed.');
redlock.lock(resource, 200)
.done(function(){
done(new Error('Should have failed with a LockError'));
}, function(err){
assert.instanceOf(err, Redlock.LockError);
done();
});
});
it('should fail to extend an expired lock', function(done) {
assert(four, 'Could not run because a required previous test failed.');
setTimeout(function(){
three.extend(800)
.done(function(){
done(new Error('Should have failed with a LockError'));
}, function(err){
assert.instanceOf(err, Redlock.LockError);
done();
});
}, four.expiration - Date.now() + 100);
});
after(function(done) {
var err;
var l = clients.length; function cb(e){ if(e) err = e; l--; if(l === 0) done(err); }
for (var i = clients.length - 1; i >= 0; i--) {
clients[i].del(resource, cb);
}
});
});
describe('disposer', function(){
before(function(done) {
var err;
var l = clients.length; function cb(e){ if(e) err = e; l--; if(l === 0) done(err); }
for (var i = clients.length - 1; i >= 0; i--) {
clients[i].del(resource, cb);
}
});
var one;
var one_expiration;
it('should automatically release a lock after the using block', function(done) {
Promise.using(
redlock.disposer(resource, 200),
function(lock){
assert.isObject(lock);
assert.isAbove(lock.expiration, Date.now()-1);
one = lock;
one_expiration = lock.expiration;
}
).done(done, done);
});
var two;
var two_expiration;
it('should issue another lock immediately after a resource is unlocked', function(done) {
assert(one_expiration, 'Could not run because a required previous test failed.');
Promise.using(
redlock.disposer(resource, 800),
function(lock){
assert.isObject(lock);
assert.isAbove(lock.expiration, Date.now()-1);
assert.isBelow(Date.now()-1, one_expiration);
two = lock;
two_expiration = lock.expiration;
}
).done(done, done);
});
it('should call unlockErrorHandler when unable to fully release a resource', function(done) {
assert(two, 'Could not run because a required previous test failed.');
var errs = 0;
var lock;
Promise.using(
redlock.disposer(resource, 800, function(err) {
errs++;
}),
function(l){
lock = l;
lock.resource = error;
}
).done(function() {
assert.equal(errs, 1);
lock.resource = resource;
lock.unlock().done(done, done);
}, done);
});
var three_original, three_extended;
var three_original_expiration;
var three_extended_expiration;
it('should automatically release an extended lock', function(done) {
assert(two_expiration, 'Could not run because a required previous test failed.');
Promise.using(
redlock.disposer(resource, 200),
function(lock){
assert.isObject(lock);
assert.isAbove(lock.expiration, Date.now()-1);
assert.isBelow(Date.now()-1, two_expiration);
three_original = lock;
three_original_expiration = lock.expiration;
return Promise.delay(100)
.then(function(){ return lock.extend(200); })
.then(function(extended) {
assert.isObject(extended);
assert.isAbove(extended.expiration, Date.now()-1);
assert.isBelow(Date.now()-1, three_original_expiration);
assert.isAbove(extended.expiration, three_original_expiration);
assert.equal(extended, lock);
three_extended = extended;
three_extended_expiration = extended.expiration;
});
}
)
.then(function(){
assert.equal(three_original.expiration, 0);
assert.equal(three_extended.expiration, 0);
}).done(done, done);
});
after(function(done) {
var err;
var l = clients.length; function cb(e){ if(e) err = e; l--; if(l === 0) done(err); }
for (var i = clients.length - 1; i >= 0; i--) {
clients[i].del(resource, cb);
}
});
});
});
}