1 | 'use strict';
|
2 |
|
3 | var assert = require('chai').assert;
|
4 | var Promise = require('bluebird');
|
5 | var Redlock = require('./redlock');
|
6 |
|
7 | test('https://www.npmjs.com/package/redis', [require('redis').createClient()]);
|
8 | test('https://www.npmjs.com/package/ioredis', [new (require('ioredis'))()]);
|
9 |
|
10 |
|
11 | function test(name, clients){
|
12 | var redlock = new Redlock(clients, {
|
13 | retryCount: 2,
|
14 | retryDelay: 150
|
15 | });
|
16 |
|
17 | var resource = 'Redlock:test:resource';
|
18 | var error = 'Redlock:test:error';
|
19 |
|
20 | describe('Redlock: ' + name, function(){
|
21 |
|
22 | before(function(done) {
|
23 | var err;
|
24 | var l = clients.length; function cb(e){ if(e) err = e; l--; if(l === 0) done(err); }
|
25 | for (var i = clients.length - 1; i >= 0; i--) {
|
26 | clients[i].sadd(error, 'having a set here should cause a failure', cb);
|
27 | }
|
28 | });
|
29 |
|
30 | it('should throw an error if not passed any clients', function(){
|
31 | assert.throws(function(){
|
32 | new Redlock([], {
|
33 | retryCount: 2,
|
34 | retryDelay: 150
|
35 | });
|
36 | });
|
37 | });
|
38 |
|
39 | it('emits a clientError event when a client error occurs', function(done){
|
40 | var emitted = 0;
|
41 | function test(err) {
|
42 | assert.isNotNull(err);
|
43 | emitted++;
|
44 | }
|
45 | redlock.on('clientError', test);
|
46 | redlock.lock(error, 200, function(err, lock){
|
47 | redlock.removeListener('clientError', test);
|
48 | assert.isNotNull(err);
|
49 | assert.equal(emitted, 3);
|
50 | done();
|
51 | });
|
52 | });
|
53 |
|
54 | describe('callbacks', function(){
|
55 | before(function(done) {
|
56 | var err;
|
57 | var l = clients.length; function cb(e){ if(e) err = e; l--; if(l === 0) done(err); }
|
58 | for (var i = clients.length - 1; i >= 0; i--) {
|
59 | clients[i].del(resource, cb);
|
60 | }
|
61 | });
|
62 |
|
63 | var one;
|
64 | it('should lock a resource', function(done) {
|
65 | redlock.lock(resource, 200, function(err, lock){
|
66 | if(err) throw err;
|
67 | assert.isObject(lock);
|
68 | assert.isAbove(lock.expiration, Date.now()-1);
|
69 | one = lock;
|
70 | done();
|
71 | });
|
72 | });
|
73 |
|
74 | var two;
|
75 | var two_expiration;
|
76 | it('should wait until a lock expires before issuing another lock', function(done) {
|
77 | assert(one, 'Could not run because a required previous test failed.');
|
78 | redlock.lock(resource, 800, function(err, lock){
|
79 | if(err) throw err;
|
80 | assert.isObject(lock);
|
81 | assert.isAbove(lock.expiration, Date.now()-1);
|
82 | assert.isAbove(Date.now()+1, one.expiration);
|
83 | two = lock;
|
84 | two_expiration = lock.expiration;
|
85 | done();
|
86 | });
|
87 | });
|
88 |
|
89 | it('should unlock a resource', function(done) {
|
90 | assert(two, 'Could not run because a required previous test failed.');
|
91 | two.unlock(done);
|
92 | });
|
93 |
|
94 | it('should unlock an already-unlocked resource', function(done) {
|
95 | assert(two, 'Could not run because a required previous test failed.');
|
96 | two.unlock(done);
|
97 | });
|
98 |
|
99 | it('should error when unable to fully release a resource', function(done) {
|
100 | assert(two, 'Could not run because a required previous test failed.');
|
101 | var failingTwo = Object.create(two);
|
102 | failingTwo.resource = error;
|
103 | failingTwo.unlock(function(err) {
|
104 | assert.isNotNull(err);
|
105 | done();
|
106 | });
|
107 | });
|
108 |
|
109 | it('should fail to extend a lock on an already-unlocked resource', function(done) {
|
110 | assert(two, 'Could not run because a required previous test failed.');
|
111 | two.extend(200, function(err, lock){
|
112 | assert.isNotNull(err);
|
113 | assert.instanceOf(err, Redlock.LockError);
|
114 | done();
|
115 | });
|
116 | });
|
117 |
|
118 | var three;
|
119 | it('should issue another lock immediately after a resource is unlocked', function(done) {
|
120 | assert(two_expiration, 'Could not run because a required previous test failed.');
|
121 | redlock.lock(resource, 800, function(err, lock){
|
122 | if(err) throw err;
|
123 | assert.isObject(lock);
|
124 | assert.isAbove(lock.expiration, Date.now()-1);
|
125 | assert.isBelow(Date.now()-1, two_expiration);
|
126 | three = lock;
|
127 | done();
|
128 | });
|
129 | });
|
130 |
|
131 | var four;
|
132 | it('should extend an unexpired lock', function(done) {
|
133 | assert(three, 'Could not run because a required previous test failed.');
|
134 | three.extend(800, function(err, lock){
|
135 | if(err) throw err;
|
136 | assert.isObject(lock);
|
137 | assert.isAbove(lock.expiration, Date.now()-1);
|
138 | assert.isAbove(lock.expiration, three.expiration-1);
|
139 | assert.equal(three, lock);
|
140 | four = lock;
|
141 | done();
|
142 | });
|
143 | });
|
144 |
|
145 | it('should fail after the maximum retry count is exceeded', function(done) {
|
146 | assert(four, 'Could not run because a required previous test failed.');
|
147 | redlock.lock(resource, 200, function(err, lock){
|
148 | assert.isNotNull(err);
|
149 | assert.instanceOf(err, Redlock.LockError);
|
150 | done();
|
151 | });
|
152 | });
|
153 |
|
154 | it('should fail to extend an expired lock', function(done) {
|
155 | assert(four, 'Could not run because a required previous test failed.');
|
156 | setTimeout(function(){
|
157 | three.extend(800, function(err, lock){
|
158 | assert.isNotNull(err);
|
159 | assert.instanceOf(err, Redlock.LockError);
|
160 | done();
|
161 | });
|
162 | }, four.expiration - Date.now() + 100);
|
163 | });
|
164 |
|
165 | it('should issue another lock immediately after a resource is expired', function(done) {
|
166 | assert(four, 'Could not run because a required previous test failed.');
|
167 | redlock.lock(resource, 800, function(err, lock){
|
168 | if(err) throw err;
|
169 | assert.isObject(lock);
|
170 | assert.isAbove(lock.expiration, Date.now()-1);
|
171 | done();
|
172 | });
|
173 | });
|
174 |
|
175 | after(function(done) {
|
176 | var err;
|
177 | var l = clients.length; function cb(e){ if(e) err = e; l--; if(l === 0) done(err); }
|
178 | for (var i = clients.length - 1; i >= 0; i--) {
|
179 | clients[i].del(resource, cb);
|
180 | }
|
181 | });
|
182 | });
|
183 |
|
184 | describe('promises', function(){
|
185 | before(function(done) {
|
186 | var err;
|
187 | var l = clients.length; function cb(e){ if(e) err = e; l--; if(l === 0) done(err); }
|
188 | for (var i = clients.length - 1; i >= 0; i--) {
|
189 | clients[i].del(resource, cb);
|
190 | }
|
191 | });
|
192 |
|
193 | var one;
|
194 | it('should lock a resource', function(done) {
|
195 | redlock.lock(resource, 200)
|
196 | .done(function(lock){
|
197 | assert.isObject(lock);
|
198 | assert.isAbove(lock.expiration, Date.now()-1);
|
199 | one = lock;
|
200 | done();
|
201 | }, done);
|
202 | });
|
203 |
|
204 | var two;
|
205 | var two_expiration;
|
206 | it('should wait until a lock expires before issuing another lock', function(done) {
|
207 | assert(one, 'Could not run because a required previous test failed.');
|
208 | redlock.lock(resource, 800)
|
209 | .done(function(lock){
|
210 | assert.isObject(lock);
|
211 | assert.isAbove(lock.expiration, Date.now()-1);
|
212 | assert.isAbove(Date.now()+1, one.expiration);
|
213 | two = lock;
|
214 | two_expiration = lock.expiration;
|
215 | done();
|
216 | }, done);
|
217 | });
|
218 |
|
219 | it('should unlock a resource', function(done) {
|
220 | assert(two, 'Could not run because a required previous test failed.');
|
221 | two.unlock().done(done, done);
|
222 | });
|
223 |
|
224 | it('should unlock an already-unlocked resource', function(done) {
|
225 | assert(two, 'Could not run because a required previous test failed.');
|
226 | two.unlock().done(done, done);
|
227 | });
|
228 |
|
229 | it('should error when unable to fully release a resource', function(done) {
|
230 | assert(two, 'Could not run because a required previous test failed.');
|
231 | var failingTwo = Object.create(two);
|
232 | failingTwo.resource = error;
|
233 | failingTwo.unlock().done(done, function(err) {
|
234 | assert.isNotNull(err);
|
235 | done();
|
236 | });
|
237 | });
|
238 |
|
239 | it('should fail to extend a lock on an already-unlocked resource', function(done) {
|
240 | assert(two, 'Could not run because a required previous test failed.');
|
241 | two.extend(200)
|
242 | .done(function(){
|
243 | done(new Error('Should have failed with a LockError'));
|
244 | }, function(err){
|
245 | assert.instanceOf(err, Redlock.LockError);
|
246 | done();
|
247 | });
|
248 | });
|
249 |
|
250 | var three;
|
251 | it('should issue another lock immediately after a resource is unlocked', function(done) {
|
252 | assert(two_expiration, 'Could not run because a required previous test failed.');
|
253 | redlock.lock(resource, 800)
|
254 | .done(function(lock){
|
255 | assert.isObject(lock);
|
256 | assert.isAbove(lock.expiration, Date.now()-1);
|
257 | assert.isBelow(Date.now()-1, two_expiration);
|
258 | three = lock;
|
259 | done();
|
260 | }, done);
|
261 | });
|
262 |
|
263 | var four;
|
264 | it('should extend an unexpired lock', function(done) {
|
265 | assert(three, 'Could not run because a required previous test failed.');
|
266 | three.extend(800)
|
267 | .done(function(lock){
|
268 | assert.isObject(lock);
|
269 | assert.isAbove(lock.expiration, Date.now()-1);
|
270 | assert.isAbove(lock.expiration, three.expiration-1);
|
271 | assert.equal(three, lock);
|
272 | four = lock;
|
273 | done();
|
274 | }, done);
|
275 | });
|
276 |
|
277 | it('should fail after the maximum retry count is exceeded', function(done) {
|
278 | assert(four, 'Could not run because a required previous test failed.');
|
279 | redlock.lock(resource, 200)
|
280 | .done(function(){
|
281 | done(new Error('Should have failed with a LockError'));
|
282 | }, function(err){
|
283 | assert.instanceOf(err, Redlock.LockError);
|
284 | done();
|
285 | });
|
286 | });
|
287 |
|
288 | it('should fail to extend an expired lock', function(done) {
|
289 | assert(four, 'Could not run because a required previous test failed.');
|
290 | setTimeout(function(){
|
291 | three.extend(800)
|
292 | .done(function(){
|
293 | done(new Error('Should have failed with a LockError'));
|
294 | }, function(err){
|
295 | assert.instanceOf(err, Redlock.LockError);
|
296 | done();
|
297 | });
|
298 | }, four.expiration - Date.now() + 100);
|
299 | });
|
300 |
|
301 | after(function(done) {
|
302 | var err;
|
303 | var l = clients.length; function cb(e){ if(e) err = e; l--; if(l === 0) done(err); }
|
304 | for (var i = clients.length - 1; i >= 0; i--) {
|
305 | clients[i].del(resource, cb);
|
306 | }
|
307 | });
|
308 | });
|
309 |
|
310 | describe('disposer', function(){
|
311 | before(function(done) {
|
312 | var err;
|
313 | var l = clients.length; function cb(e){ if(e) err = e; l--; if(l === 0) done(err); }
|
314 | for (var i = clients.length - 1; i >= 0; i--) {
|
315 | clients[i].del(resource, cb);
|
316 | }
|
317 | });
|
318 |
|
319 | var one;
|
320 | var one_expiration;
|
321 | it('should automatically release a lock after the using block', function(done) {
|
322 | Promise.using(
|
323 | redlock.disposer(resource, 200),
|
324 | function(lock){
|
325 | assert.isObject(lock);
|
326 | assert.isAbove(lock.expiration, Date.now()-1);
|
327 | one = lock;
|
328 | one_expiration = lock.expiration;
|
329 | }
|
330 | ).done(done, done);
|
331 | });
|
332 |
|
333 | var two;
|
334 | var two_expiration;
|
335 | it('should issue another lock immediately after a resource is unlocked', function(done) {
|
336 | assert(one_expiration, 'Could not run because a required previous test failed.');
|
337 | Promise.using(
|
338 | redlock.disposer(resource, 800),
|
339 | function(lock){
|
340 | assert.isObject(lock);
|
341 | assert.isAbove(lock.expiration, Date.now()-1);
|
342 | assert.isBelow(Date.now()-1, one_expiration);
|
343 | two = lock;
|
344 | two_expiration = lock.expiration;
|
345 | }
|
346 | ).done(done, done);
|
347 | });
|
348 |
|
349 | it('should call unlockErrorHandler when unable to fully release a resource', function(done) {
|
350 | assert(two, 'Could not run because a required previous test failed.');
|
351 | var errs = 0;
|
352 | var lock;
|
353 | Promise.using(
|
354 | redlock.disposer(resource, 800, function(err) {
|
355 | errs++;
|
356 | }),
|
357 | function(l){
|
358 | lock = l;
|
359 | lock.resource = error;
|
360 | }
|
361 | ).done(function() {
|
362 | assert.equal(errs, 1);
|
363 | lock.resource = resource;
|
364 | lock.unlock().done(done, done);
|
365 | }, done);
|
366 | });
|
367 |
|
368 | var three_original, three_extended;
|
369 | var three_original_expiration;
|
370 | var three_extended_expiration;
|
371 | it('should automatically release an extended lock', function(done) {
|
372 | assert(two_expiration, 'Could not run because a required previous test failed.');
|
373 | Promise.using(
|
374 | redlock.disposer(resource, 200),
|
375 | function(lock){
|
376 | assert.isObject(lock);
|
377 | assert.isAbove(lock.expiration, Date.now()-1);
|
378 | assert.isBelow(Date.now()-1, two_expiration);
|
379 | three_original = lock;
|
380 | three_original_expiration = lock.expiration;
|
381 |
|
382 | return Promise.delay(100)
|
383 | .then(function(){ return lock.extend(200); })
|
384 | .then(function(extended) {
|
385 | assert.isObject(extended);
|
386 | assert.isAbove(extended.expiration, Date.now()-1);
|
387 | assert.isBelow(Date.now()-1, three_original_expiration);
|
388 | assert.isAbove(extended.expiration, three_original_expiration);
|
389 | assert.equal(extended, lock);
|
390 | three_extended = extended;
|
391 | three_extended_expiration = extended.expiration;
|
392 | });
|
393 | }
|
394 | )
|
395 | .then(function(){
|
396 | assert.equal(three_original.expiration, 0);
|
397 | assert.equal(three_extended.expiration, 0);
|
398 | }).done(done, done);
|
399 | });
|
400 |
|
401 | after(function(done) {
|
402 | var err;
|
403 | var l = clients.length; function cb(e){ if(e) err = e; l--; if(l === 0) done(err); }
|
404 | for (var i = clients.length - 1; i >= 0; i--) {
|
405 | clients[i].del(resource, cb);
|
406 | }
|
407 | });
|
408 | });
|
409 |
|
410 | });
|
411 | }
|