Comparing version 3.1.2 to 4.0.0
{ | ||
"name": "redlock", | ||
"version": "3.1.2", | ||
"version": "4.0.0", | ||
"description": "A node.js redlock implementation for distributed redis locks", | ||
@@ -29,7 +29,7 @@ "main": "redlock.js", | ||
"devDependencies": { | ||
"chai": "^3.5.0", | ||
"coveralls": "^2.11.8", | ||
"ioredis": "^3.0.0", | ||
"chai": "^4.2.0", | ||
"coveralls": "^3.0.4", | ||
"ioredis": "^4.10.0", | ||
"istanbul": "^0.4.2", | ||
"mocha": "^2.4.5", | ||
"mocha": "^6.1.4", | ||
"redis": "^2.7.1" | ||
@@ -40,2 +40,5 @@ }, | ||
}, | ||
"engines": { | ||
"node": ">=8.0.0" | ||
}, | ||
"config": { | ||
@@ -42,0 +45,0 @@ "blanket": { |
@@ -13,2 +13,3 @@ [![npm version](https://badge.fury.io/js/redlock.svg)](https://www.npmjs.com/package/redlock) | ||
- [Usage (Callback Style)](#usage-callback-style) | ||
- [Locking multiple resources](#locking-multiple-resources) | ||
- [API Docs](#api-docs) | ||
@@ -24,2 +25,5 @@ | ||
### Using Cluster/Sentinel | ||
***Please make sure to use a client with built-in cluster support, such as [ioredis](https://github.com/luin/ioredis).*** | ||
It is completely possible to use a *single* redis cluster or sentinal configuration by passing one preconfigured client to redlock. While you do gain high availability and vastly increased throughput under this scheme, the failure modes are a bit different, and it becomes theoretically possible that a lock is acquired twice: | ||
@@ -52,3 +56,3 @@ | ||
A redlock object is instantiated with an array of at least one redis client and an optional `options` object. Properties of the Redlock object should NOT be changed after it is firstused, as doing so could have unintended consequences for live locks. | ||
A redlock object is instantiated with an array of at least one redis client and an optional `options` object. Properties of the Redlock object should NOT be changed after it is first used, as doing so could have unintended consequences for live locks. | ||
@@ -115,3 +119,3 @@ ```js | ||
// the maximum amount of time you want the resource locked, | ||
// the maximum amount of time you want the resource locked in milliseconds, | ||
// keeping in mind that you can extend the lock up until | ||
@@ -309,2 +313,38 @@ // the point when it expires | ||
## Locking multiple resources | ||
Multiple resources can be locked by providing an `Array` of strings to `Redlock.prototype.lock` call. Internally a single attempt is made to `redis` by evaluating script which executes lock statements. For more details about atomicity of scripts please see [redis reference](https://redis.io/commands/eval#atomicity-of-scripts). | ||
There are however some limitations of which you need to be aware of: | ||
- When requesting a lock it will fail if any of requested resources is already set | ||
- If lock attempt fails for any resource (due to whatever reason) an attempt for removing already set resources is made. However there are no guarantees that it will succeed (`redis` doesn't provide them) | ||
- Releasing lock will fail if any of requested resources is missing | ||
- Extending lock will fail if any of requested resources is missing | ||
Example: | ||
```js | ||
redlock.lock(['locks:account:322456', 'locks:account:322457', 'locks:account:322458'], 1000).then(function(lock) { | ||
// ...do something here... | ||
// if you need more time, you can continue to extend | ||
// the lock as long as you never let it expire | ||
// this will extend the lock so that it expires | ||
// approximitely 1s from when `extend` is called | ||
return lock.extend(1000).then(function(lock){ | ||
// ...do something here... | ||
// unlock your resource when you are done | ||
return lock.unlock() | ||
.catch(function(err) { | ||
// we weren't able to reach redis; your lock will eventually | ||
// expire, but you probably want to log this error | ||
console.error(err); | ||
}); | ||
}); | ||
}); | ||
``` | ||
API Docs | ||
@@ -314,3 +354,3 @@ -------- | ||
### `Redlock.prototype.lock(resource, ttl, ?callback) => Promise<Lock>` | ||
- `resource (string)` resource to be locked | ||
- `resource (string or string[])` resource(s) to be locked | ||
- `ttl (number)` time in ms until the lock expires | ||
@@ -337,3 +377,3 @@ - `callback (function)` callback returning: | ||
### `Redlock.prototype.disposer(resource, ttl, ?unlockErrorHandler)` | ||
- `resource (string)` resource to be locked | ||
- `resource (string or string[])` resource(s) to be locked | ||
- `ttl (number)` time in ms to extend the lock's expiration | ||
@@ -340,0 +380,0 @@ - `callback (function)` error handler called with: |
166
redlock.js
'use strict'; | ||
var util = require('util'); | ||
var crypto = require('crypto'); | ||
var Promise = require('bluebird'); | ||
var EventEmitter = require('events'); | ||
const util = require('util'); | ||
const crypto = require('crypto'); | ||
const Promise = require('bluebird'); | ||
const EventEmitter = require('events'); | ||
// support the event library provided by node < 0.11.0 | ||
if(typeof EventEmitter.EventEmitter === 'function') | ||
EventEmitter = EventEmitter.EventEmitter; | ||
// constants | ||
const lockScript = ` | ||
-- Return 0 if an entry already exists. | ||
for i, key in ipairs(KEYS) do | ||
if redis.call("exists", key) == 1 then | ||
return 0 | ||
end | ||
end | ||
-- Create an entry for each provided key. | ||
for i, key in ipairs(KEYS) do | ||
redis.call("set", key, ARGV[1], "PX", ARGV[2]) | ||
end | ||
// constants | ||
var lockScript = 'return redis.call("set", KEYS[1], ARGV[1], "NX", "PX", ARGV[2])'; | ||
var unlockScript = 'if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end'; | ||
var extendScript = 'if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("pexpire", KEYS[1], ARGV[2]) else return 0 end'; | ||
-- Return the number of entries added. | ||
return table.getn(KEYS) | ||
`; | ||
const unlockScript = ` | ||
local count = 0 | ||
for i, key in ipairs(KEYS) do | ||
-- Only remove entries for *this* lock value. | ||
if redis.call("get", key) == ARGV[1] then | ||
redis.pcall("del", key) | ||
count = count + 1 | ||
end | ||
end | ||
-- Return the number of entries removed. | ||
return count | ||
`; | ||
const extendScript = ` | ||
-- Return 0 if an entry exists with a *different* lock value. | ||
for i, key in ipairs(KEYS) do | ||
if redis.call("get", key) ~= ARGV[1] then | ||
return 0 | ||
end | ||
end | ||
-- Update the entry for each provided key. | ||
for i, key in ipairs(KEYS) do | ||
redis.call("set", key, ARGV[1], "PX", ARGV[2]) | ||
end | ||
-- Return the number of entries updated. | ||
return table.getn(KEYS) | ||
`; | ||
// defaults | ||
var defaults = { | ||
const defaults = { | ||
driftFactor: 0.01, | ||
@@ -88,7 +127,6 @@ retryCount: 10, | ||
this.retryDelay = typeof options.retryDelay === 'number' ? options.retryDelay : defaults.retryDelay; | ||
this.retryJitter = typeof options.retryJitter === 'number' ? options.retryJitter : defaults.retryJitter; | ||
this.retryJitter = typeof options.retryJitter === 'number' ? options.retryJitter : defaults.retryJitter; | ||
this.lockScript = typeof options.lockScript === 'function' ? options.lockScript(lockScript) : lockScript; | ||
this.unlockScript = typeof options.unlockScript === 'function' ? options.unlockScript(unlockScript) : unlockScript; | ||
this.extendScript = typeof options.extendScript === 'function' ? options.extendScript(extendScript) : extendScript; | ||
// set the redis servers from additional arguments | ||
@@ -175,4 +213,9 @@ this.servers = clients; | ||
Redlock.prototype.unlock = function unlock(lock, callback) { | ||
var self = this; | ||
const self = this; | ||
// array of locked resources | ||
const resource = Array.isArray(lock.resource) | ||
? lock.resource | ||
: [lock.resource]; | ||
// immediately invalidate the lock | ||
@@ -183,14 +226,22 @@ lock.expiration = 0; | ||
// the number of votes needed for consensus | ||
const quorum = Math.floor(self.servers.length / 2) + 1; | ||
// the number of servers which have agreed to release this lock | ||
var votes = 0; | ||
let votes = 0; | ||
// the number of votes needed for consensus | ||
var quorum = Math.floor(self.servers.length / 2) + 1; | ||
// the number of async redis calls still waiting to finish | ||
var waiting = self.servers.length; | ||
let waiting = self.servers.length; | ||
// release the lock on each server | ||
self.servers.forEach(function(server){ | ||
server.eval(self.unlockScript, 1, lock.resource, lock.value, loop); | ||
return server.eval( | ||
[ | ||
self.unlockScript, | ||
resource.length, | ||
...resource, | ||
lock.value | ||
], | ||
loop | ||
) | ||
}); | ||
@@ -201,12 +252,9 @@ | ||
// - if the lock was released by this call, it will return 1 | ||
// - if the lock has already been released, it will return 0 | ||
// - it may have been re-acquired by another process | ||
// - it may hava already been manually released | ||
// - it may have expired | ||
// - If the response is less than the resource length, than one or | ||
// more resources failed to unlock: | ||
// - It may have been re-acquired by another process; | ||
// - It may hava already been manually released; | ||
// - It may have expired; | ||
if(typeof response === 'string') | ||
response = parseInt(response); | ||
if(response === 0 || response === 1) | ||
if(response === resource.length || response === '' + resource.length) | ||
votes++; | ||
@@ -234,3 +282,3 @@ | ||
Redlock.prototype.extend = function extend(lock, ttl, callback) { | ||
var self = this; | ||
const self = this; | ||
@@ -286,10 +334,13 @@ // the lock has expired | ||
Redlock.prototype._lock = function _lock(resource, value, ttl, callback) { | ||
var self = this; | ||
const self = this; | ||
// array of locked resources | ||
resource = Array.isArray(resource) ? resource : [resource]; | ||
return new Promise(function(resolve, reject) { | ||
var request; | ||
let request; | ||
// the number of times we have attempted this lock | ||
var attempts = 0; | ||
let attempts = 0; | ||
// create a new lock | ||
@@ -299,3 +350,12 @@ if(value === null) { | ||
request = function(server, loop){ | ||
return server.eval(self.lockScript, 1, resource, value, ttl, loop); | ||
return server.eval( | ||
[ | ||
self.lockScript, | ||
resource.length, | ||
...resource, | ||
value, | ||
ttl | ||
], | ||
loop | ||
); | ||
}; | ||
@@ -307,3 +367,12 @@ } | ||
request = function(server, loop){ | ||
return server.eval(self.extendScript, 1, resource, value, ttl, loop); | ||
return server.eval( | ||
[ | ||
self.extendScript, | ||
resource.length, | ||
...resource, | ||
value, | ||
ttl | ||
], | ||
loop | ||
); | ||
}; | ||
@@ -316,23 +385,23 @@ } | ||
// the time when this attempt started | ||
var start = Date.now(); | ||
const start = Date.now(); | ||
// the number of votes needed for consensus | ||
const quorum = Math.floor(self.servers.length / 2) + 1; | ||
// the number of servers which have agreed to this lock | ||
var votes = 0; | ||
let votes = 0; | ||
// the number of votes needed for consensus | ||
var quorum = Math.floor(self.servers.length / 2) + 1; | ||
// the number of async redis calls still waiting to finish | ||
var waiting = self.servers.length; | ||
let waiting = self.servers.length; | ||
function loop(err, response) { | ||
if(err) self.emit('clientError', err); | ||
if(response) votes++; | ||
if(response === resource.length || response === '' + resource.length) votes++; | ||
if(waiting-- > 1) return; | ||
// Add 2 milliseconds to the drift to account for Redis expires precision, which is 1 ms, | ||
// plus the configured allowable drift factor | ||
var drift = Math.round(self.driftFactor * ttl) + 2; | ||
var lock = new Lock(self, resource, value, start + ttl - drift, attempts); | ||
const drift = Math.round(self.driftFactor * ttl) + 2; | ||
const lock = new Lock(self, resource, value, start + ttl - drift, attempts); | ||
// SUCCESS: there is concensus and the lock is not expired | ||
@@ -372,3 +441,2 @@ if(votes >= quorum && lock.expiration > Date.now()) | ||
module.exports = Redlock; |
454
test.js
@@ -20,4 +20,5 @@ 'use strict'; | ||
var resource = 'Redlock:test:resource'; | ||
var error = 'Redlock:test:error'; | ||
var resourceString = 'Redlock:test:resource'; | ||
var resourceArray = ['Redlock:test:resource1','Redlock:test:resource2']; | ||
var error = 'Redlock:test:error'; | ||
@@ -63,3 +64,3 @@ describe('Redlock: ' + name, function(){ | ||
unlockScript: function(unlockScript) { return unlockScript + 'and 2'; }, | ||
extendScript: function(extendScript) { return extendScript + 'and 3'; } | ||
extendScript: function(extendScript) { return extendScript + 'and 3'; }, | ||
}; | ||
@@ -70,3 +71,3 @@ var customRedlock = new Redlock(clients, opts); | ||
assert.equal(customRedlock.unlockScript, redlock.unlockScript + 'and ' + i++); | ||
assert.equal(customRedlock.extendScript, redlock.extendScript + 'and ' + i); | ||
assert.equal(customRedlock.extendScript, redlock.extendScript + 'and ' + i++); | ||
}); | ||
@@ -79,3 +80,3 @@ | ||
for (var i = clients.length - 1; i >= 0; i--) { | ||
clients[i].del(resource, cb); | ||
clients[i].del(resourceString, cb); | ||
} | ||
@@ -86,3 +87,3 @@ }); | ||
it('should lock a resource', function(done) { | ||
redlock.lock(resource, 200, function(err, lock){ | ||
redlock.lock(resourceString, 200, function(err, lock){ | ||
if(err) throw err; | ||
@@ -102,3 +103,3 @@ assert.isObject(lock); | ||
assert(one, 'Could not run because a required previous test failed.'); | ||
redlock.lock(resource, 800, function(err, lock){ | ||
redlock.lock(resourceString, 800, function(err, lock){ | ||
if(err) throw err; | ||
@@ -123,3 +124,6 @@ assert.isObject(lock); | ||
assert(two, 'Could not run because a required previous test failed.'); | ||
two.unlock(done); | ||
two.unlock(function(err) { | ||
assert.isNotNull(err) | ||
done(); | ||
}); | ||
}); | ||
@@ -150,3 +154,3 @@ | ||
assert(two_expiration, 'Could not run because a required previous test failed.'); | ||
redlock.lock(resource, 800, function(err, lock){ | ||
redlock.lock(resourceString, 800, function(err, lock){ | ||
if(err) throw err; | ||
@@ -179,3 +183,3 @@ assert.isObject(lock); | ||
assert(four, 'Could not run because a required previous test failed.'); | ||
redlock.lock(resource, 200, function(err, lock){ | ||
redlock.lock(resourceString, 200, function(err, lock){ | ||
assert.isNotNull(err); | ||
@@ -202,3 +206,3 @@ assert.instanceOf(err, Redlock.LockError); | ||
assert(four, 'Could not run because a required previous test failed.'); | ||
redlock.lock(resource, 800, function(err, lock){ | ||
redlock.lock(resourceString, 800, function(err, lock){ | ||
if(err) throw err; | ||
@@ -216,3 +220,3 @@ assert.isObject(lock); | ||
for (var i = clients.length - 1; i >= 0; i--) { | ||
clients[i].del(resource, cb); | ||
clients[i].del(resourceString, cb); | ||
} | ||
@@ -227,3 +231,3 @@ }); | ||
for (var i = clients.length - 1; i >= 0; i--) { | ||
clients[i].del(resource, cb); | ||
clients[i].del(resourceString, cb); | ||
} | ||
@@ -234,3 +238,3 @@ }); | ||
it('should lock a resource', function(done) { | ||
redlock.lock(resource, 200) | ||
redlock.lock(resourceString, 200) | ||
.done(function(lock){ | ||
@@ -249,3 +253,3 @@ assert.isObject(lock); | ||
assert(one, 'Could not run because a required previous test failed.'); | ||
redlock.lock(resource, 800) | ||
redlock.lock(resourceString, 800) | ||
.done(function(lock){ | ||
@@ -269,3 +273,7 @@ assert.isObject(lock); | ||
assert(two, 'Could not run because a required previous test failed.'); | ||
two.unlock().done(done, done); | ||
two.unlock().done(function(result) { | ||
done(new Error('Expected an error.')); | ||
}, function(err) { | ||
done(); | ||
}); | ||
}); | ||
@@ -298,3 +306,3 @@ | ||
assert(two_expiration, 'Could not run because a required previous test failed.'); | ||
redlock.lock(resource, 800) | ||
redlock.lock(resourceString, 800) | ||
.done(function(lock){ | ||
@@ -327,3 +335,3 @@ assert.isObject(lock); | ||
assert(four, 'Could not run because a required previous test failed.'); | ||
redlock.lock(resource, 200) | ||
redlock.lock(resourceString, 200) | ||
.done(function(){ | ||
@@ -356,3 +364,3 @@ done(new Error('Should have failed with a LockError')); | ||
for (var i = clients.length - 1; i >= 0; i--) { | ||
clients[i].del(resource, cb); | ||
clients[i].del(resourceString, cb); | ||
} | ||
@@ -367,3 +375,3 @@ }); | ||
for (var i = clients.length - 1; i >= 0; i--) { | ||
clients[i].del(resource, cb); | ||
clients[i].del(resourceString, cb); | ||
} | ||
@@ -376,3 +384,3 @@ }); | ||
Promise.using( | ||
redlock.disposer(resource, 200), | ||
redlock.disposer(resourceString, 200), | ||
function(lock){ | ||
@@ -393,3 +401,3 @@ assert.isObject(lock); | ||
Promise.using( | ||
redlock.disposer(resource, 800), | ||
redlock.disposer(resourceString, 800), | ||
function(lock){ | ||
@@ -411,3 +419,3 @@ assert.isObject(lock); | ||
Promise.using( | ||
redlock.disposer(resource, 800, function(err) { | ||
redlock.disposer(resourceString, 800, function(err) { | ||
errs++; | ||
@@ -421,3 +429,3 @@ }), | ||
assert.equal(errs, 1); | ||
lock.resource = resource; | ||
lock.resource = resourceString; | ||
lock.unlock().done(done, done); | ||
@@ -433,3 +441,3 @@ }, done); | ||
Promise.using( | ||
redlock.disposer(resource, 200), | ||
redlock.disposer(resourceString, 200), | ||
function(lock){ | ||
@@ -466,7 +474,401 @@ assert.isObject(lock); | ||
for (var i = clients.length - 1; i >= 0; i--) { | ||
clients[i].del(resource, cb); | ||
clients[i].del(resourceString, cb); | ||
} | ||
}); | ||
}); | ||
describe('callbacks - multi', 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--) { | ||
for (var j = resourceArray.length - 1; j >= 0; j--) { | ||
clients[i].del(resourceArray[j], cb); | ||
} | ||
} | ||
}); | ||
var one; | ||
it('should lock a multivalue resource', function(done) { | ||
redlock.lock(resourceArray, 200, function(err, lock){ | ||
if(err) throw err; | ||
assert.isObject(lock); | ||
assert.instanceOf(lock, Redlock.Lock); | ||
assert.isAbove(lock.expiration, Date.now()-1); | ||
assert.equal(lock.attempts, 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(resourceArray, 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); | ||
assert.isAbove(lock.attempts, 1); | ||
two = lock; | ||
two_expiration = lock.expiration; | ||
done(); | ||
}); | ||
}); | ||
it('should unlock a multivalue 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 multivalue resource', function(done) { | ||
assert(two, 'Could not run because a required previous test failed.'); | ||
two.unlock(function(err) { | ||
assert.isNotNull(err) | ||
done(); | ||
}); | ||
}); | ||
it('should error when unable to fully release a multivalue 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 multivalue 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); | ||
assert.equal(err.attempts, 0); | ||
done(); | ||
}); | ||
}); | ||
var three; | ||
it('should issue another lock immediately after a multivalue resource is unlocked', function(done) { | ||
assert(two_expiration, 'Could not run because a required previous test failed.'); | ||
redlock.lock(resourceArray, 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); | ||
assert.equal(lock.attempts, 1); | ||
three = lock; | ||
done(); | ||
}); | ||
}); | ||
var four; | ||
it('should extend an unexpired multivalue 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(lock.attempts, 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(resourceArray, 200, function(err, lock){ | ||
assert.isNotNull(err); | ||
assert.instanceOf(err, Redlock.LockError); | ||
assert.equal(err.attempts, 3); | ||
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); | ||
assert.equal(err.attempts, 0); | ||
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(resourceArray, 800, function(err, lock){ | ||
if(err) throw err; | ||
assert.isObject(lock); | ||
assert.isAbove(lock.expiration, Date.now()-1); | ||
assert.equal(lock.attempts, 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--) { | ||
for (var j = resourceArray.length - 1; j >= 0; j--) { | ||
clients[i].del(resourceArray[j], cb); | ||
} | ||
} | ||
}); | ||
}); | ||
describe('promises - multi', 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--) { | ||
for (var j = resourceArray.length - 1; j >= 0; j--) { | ||
clients[i].del(resourceArray[j], cb); | ||
} | ||
} | ||
}); | ||
var one; | ||
it('should lock a multivalue resource', function(done) { | ||
redlock.lock(resourceArray, 200) | ||
.done(function(lock){ | ||
assert.isObject(lock); | ||
assert.isAbove(lock.expiration, Date.now()-1); | ||
assert.equal(lock.attempts, 1); | ||
one = lock; | ||
done(); | ||
}, done); | ||
}); | ||
var two; | ||
var two_expiration; | ||
it('should wait until a multivalue lock expires before issuing another lock', function(done) { | ||
assert(one, 'Could not run because a required previous test failed.'); | ||
redlock.lock(resourceArray, 800) | ||
.done(function(lock){ | ||
assert.isObject(lock); | ||
assert.isAbove(lock.expiration, Date.now()-1); | ||
assert.isAbove(Date.now()+1, one.expiration); | ||
assert.isAbove(lock.attempts, 1); | ||
two = lock; | ||
two_expiration = lock.expiration; | ||
done(); | ||
}, done); | ||
}); | ||
it('should unlock a multivalue 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 multivalue resource', function(done) { | ||
assert(two, 'Could not run because a required previous test failed.'); | ||
two.unlock().done(function(result) { | ||
done(new Error('Expected an error.')); | ||
}, function(err) { | ||
done(); | ||
}); | ||
}); | ||
it('should error when unable to fully release a multivalue 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 multivalue 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); | ||
assert.equal(err.attempts, 0); | ||
done(); | ||
}); | ||
}); | ||
var three; | ||
it('should issue another lock immediately after a multivalue resource is unlocked', function(done) { | ||
assert(two_expiration, 'Could not run because a required previous test failed.'); | ||
redlock.lock(resourceArray, 800) | ||
.done(function(lock){ | ||
assert.isObject(lock); | ||
assert.isAbove(lock.expiration, Date.now()-1); | ||
assert.isBelow(Date.now()-1, two_expiration); | ||
assert.equal(lock.attempts, 1); | ||
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(lock.attempts, 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(resourceArray, 200) | ||
.done(function(){ | ||
done(new Error('Should have failed with a LockError')); | ||
}, function(err){ | ||
assert.instanceOf(err, Redlock.LockError); | ||
assert.equal(err.attempts, 3); | ||
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); | ||
assert.equal(err.attempts, 0); | ||
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--) { | ||
for (var j = resourceArray.length - 1; j >= 0; j--) { | ||
clients[i].del(resourceArray[j], cb); | ||
} | ||
} | ||
}); | ||
}); | ||
describe('disposer - multi', 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--) { | ||
for (var j = resourceArray.length - 1; j >= 0; j--) { | ||
clients[i].del(resourceArray[j], cb); | ||
} | ||
} | ||
}); | ||
var one; | ||
var one_expiration; | ||
it('should automatically release a lock after the using block', function(done) { | ||
Promise.using( | ||
redlock.disposer(resourceArray, 200), | ||
function(lock){ | ||
assert.isObject(lock); | ||
assert.isAbove(lock.expiration, Date.now()-1); | ||
assert.equal(lock.attempts, 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(resourceArray, 800), | ||
function(lock){ | ||
assert.isObject(lock); | ||
assert.isAbove(lock.expiration, Date.now()-1); | ||
assert.isBelow(Date.now()-1, one_expiration); | ||
assert.equal(lock.attempts, 1); | ||
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(resourceArray, 800, function(err) { | ||
errs++; | ||
}), | ||
function(l){ | ||
lock = l; | ||
lock.resource = error; | ||
} | ||
).done(function() { | ||
assert.equal(errs, 1); | ||
lock.resource = resourceArray; | ||
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(resourceArray, 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(lock.attempts, 1); | ||
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--) { | ||
for (var j = resourceArray.length - 1; j >= 0; j--) { | ||
clients[i].del(resourceArray[j], cb); | ||
} | ||
} | ||
}); | ||
}); | ||
describe('quit', function() { | ||
@@ -473,0 +875,0 @@ it('should quit all clients', function(done){ |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
53874
9
1130
394