effect-distributed-lock 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +90 -56
  2. package/examples/index.ts +49 -21
  3. package/package.json +2 -2
  4. package/redis-semaphore/.codeclimate.yml +5 -0
  5. package/redis-semaphore/.fossa.yml +14 -0
  6. package/redis-semaphore/.github/dependabot.yml +6 -0
  7. package/redis-semaphore/.github/workflows/branches.yml +39 -0
  8. package/redis-semaphore/.github/workflows/pull-requests.yml +35 -0
  9. package/redis-semaphore/.mocharc.yaml +6 -0
  10. package/redis-semaphore/.prettierrc +6 -0
  11. package/redis-semaphore/.snyk +4 -0
  12. package/redis-semaphore/.yarnrc.yml +2 -0
  13. package/redis-semaphore/CHANGELOG.md +70 -0
  14. package/redis-semaphore/Dockerfile +5 -0
  15. package/redis-semaphore/LICENSE +21 -0
  16. package/redis-semaphore/README.md +445 -0
  17. package/redis-semaphore/docker-compose.yml +31 -0
  18. package/redis-semaphore/eslint.config.mjs +73 -0
  19. package/redis-semaphore/package.json +79 -0
  20. package/redis-semaphore/setup-redis-servers.sh +2 -0
  21. package/redis-semaphore/src/Lock.ts +172 -0
  22. package/redis-semaphore/src/RedisMultiSemaphore.ts +56 -0
  23. package/redis-semaphore/src/RedisMutex.ts +45 -0
  24. package/redis-semaphore/src/RedisSemaphore.ts +49 -0
  25. package/redis-semaphore/src/RedlockMultiSemaphore.ts +56 -0
  26. package/redis-semaphore/src/RedlockMutex.ts +52 -0
  27. package/redis-semaphore/src/RedlockSemaphore.ts +49 -0
  28. package/redis-semaphore/src/errors/LostLockError.ts +1 -0
  29. package/redis-semaphore/src/errors/TimeoutError.ts +1 -0
  30. package/redis-semaphore/src/index.ts +23 -0
  31. package/redis-semaphore/src/misc.ts +12 -0
  32. package/redis-semaphore/src/multiSemaphore/acquire/index.ts +53 -0
  33. package/redis-semaphore/src/multiSemaphore/acquire/lua.ts +31 -0
  34. package/redis-semaphore/src/multiSemaphore/refresh/index.ts +32 -0
  35. package/redis-semaphore/src/multiSemaphore/refresh/lua.ts +31 -0
  36. package/redis-semaphore/src/multiSemaphore/release/index.ts +22 -0
  37. package/redis-semaphore/src/multiSemaphore/release/lua.ts +17 -0
  38. package/redis-semaphore/src/mutex/acquire.ts +42 -0
  39. package/redis-semaphore/src/mutex/refresh.ts +37 -0
  40. package/redis-semaphore/src/mutex/release.ts +30 -0
  41. package/redis-semaphore/src/redlockMultiSemaphore/acquire.ts +56 -0
  42. package/redis-semaphore/src/redlockMultiSemaphore/refresh.ts +68 -0
  43. package/redis-semaphore/src/redlockMultiSemaphore/release.ts +19 -0
  44. package/redis-semaphore/src/redlockMutex/acquire.ts +54 -0
  45. package/redis-semaphore/src/redlockMutex/refresh.ts +53 -0
  46. package/redis-semaphore/src/redlockMutex/release.ts +19 -0
  47. package/redis-semaphore/src/redlockSemaphore/acquire.ts +55 -0
  48. package/redis-semaphore/src/redlockSemaphore/refresh.ts +60 -0
  49. package/redis-semaphore/src/redlockSemaphore/release.ts +18 -0
  50. package/redis-semaphore/src/semaphore/acquire/index.ts +52 -0
  51. package/redis-semaphore/src/semaphore/acquire/lua.ts +25 -0
  52. package/redis-semaphore/src/semaphore/refresh/index.ts +31 -0
  53. package/redis-semaphore/src/semaphore/refresh/lua.ts +25 -0
  54. package/redis-semaphore/src/semaphore/release.ts +14 -0
  55. package/redis-semaphore/src/types.ts +63 -0
  56. package/redis-semaphore/src/utils/createEval.ts +45 -0
  57. package/redis-semaphore/src/utils/index.ts +13 -0
  58. package/redis-semaphore/src/utils/redlock.ts +7 -0
  59. package/redis-semaphore/test/init.test.ts +9 -0
  60. package/redis-semaphore/test/redisClient.ts +82 -0
  61. package/redis-semaphore/test/setup.ts +6 -0
  62. package/redis-semaphore/test/shell.test.ts +15 -0
  63. package/redis-semaphore/test/shell.ts +48 -0
  64. package/redis-semaphore/test/src/Lock.test.ts +37 -0
  65. package/redis-semaphore/test/src/RedisMultiSemaphore.test.ts +425 -0
  66. package/redis-semaphore/test/src/RedisMutex.test.ts +334 -0
  67. package/redis-semaphore/test/src/RedisSemaphore.test.ts +367 -0
  68. package/redis-semaphore/test/src/RedlockMultiSemaphore.test.ts +671 -0
  69. package/redis-semaphore/test/src/RedlockMutex.test.ts +328 -0
  70. package/redis-semaphore/test/src/RedlockSemaphore.test.ts +579 -0
  71. package/redis-semaphore/test/src/index.test.ts +22 -0
  72. package/redis-semaphore/test/src/multiSemaphore/acquire/index.test.ts +51 -0
  73. package/redis-semaphore/test/src/multiSemaphore/acquire/internal.test.ts +67 -0
  74. package/redis-semaphore/test/src/multiSemaphore/refresh/index.test.ts +52 -0
  75. package/redis-semaphore/test/src/multiSemaphore/release/index.test.ts +18 -0
  76. package/redis-semaphore/test/src/mutex/acquire.test.ts +78 -0
  77. package/redis-semaphore/test/src/mutex/refresh.test.ts +22 -0
  78. package/redis-semaphore/test/src/mutex/release.test.ts +17 -0
  79. package/redis-semaphore/test/src/redlockMutex/acquire.test.ts +90 -0
  80. package/redis-semaphore/test/src/redlockMutex/refresh.test.ts +27 -0
  81. package/redis-semaphore/test/src/redlockMutex/release.test.ts +17 -0
  82. package/redis-semaphore/test/src/semaphore/acquire/index.test.ts +49 -0
  83. package/redis-semaphore/test/src/semaphore/acquire/internal.test.ts +65 -0
  84. package/redis-semaphore/test/src/semaphore/refresh/index.test.ts +44 -0
  85. package/redis-semaphore/test/src/semaphore/release.test.ts +18 -0
  86. package/redis-semaphore/test/src/utils/eval.test.ts +22 -0
  87. package/redis-semaphore/test/src/utils/index.test.ts +19 -0
  88. package/redis-semaphore/test/src/utils/redlock.test.ts +31 -0
  89. package/redis-semaphore/test/unhandledRejection.ts +28 -0
  90. package/redis-semaphore/tsconfig.build-commonjs.json +9 -0
  91. package/redis-semaphore/tsconfig.build-es.json +9 -0
  92. package/redis-semaphore/tsconfig.json +11 -0
  93. package/redis-semaphore/yarn.lock +5338 -0
  94. package/src/Backing.ts +87 -0
  95. package/src/DistributedSemaphore.ts +384 -0
  96. package/src/Errors.ts +3 -15
  97. package/src/RedisBacking.ts +165 -59
  98. package/src/index.ts +28 -12
  99. package/src/DistributedMutex.ts +0 -356
@@ -0,0 +1,579 @@
1
+ import { expect } from 'chai'
2
+ import { Redis } from 'ioredis'
3
+ import sinon from 'sinon'
4
+ import LostLockError from '../../src/errors/LostLockError'
5
+ import RedlockSemaphore from '../../src/RedlockSemaphore'
6
+ import { TimeoutOptions } from '../../src/types'
7
+ import { delay } from '../../src/utils/index'
8
+ import {
9
+ allClientMocks,
10
+ allClients,
11
+ client1,
12
+ client2,
13
+ client3
14
+ } from '../redisClient'
15
+ import { downRedisServer, upRedisServer } from '../shell'
16
+ import {
17
+ catchUnhandledRejection,
18
+ throwUnhandledRejection,
19
+ unhandledRejectionSpy
20
+ } from '../unhandledRejection'
21
+
22
+ const timeoutOptions: TimeoutOptions = {
23
+ lockTimeout: 300,
24
+ acquireTimeout: 100,
25
+ refreshInterval: 80,
26
+ retryInterval: 10
27
+ }
28
+
29
+ async function expectZRangeAllEql(key: string, values: string[]) {
30
+ const results = await Promise.all([
31
+ client1.zrange(key, 0, -1),
32
+ client2.zrange(key, 0, -1),
33
+ client3.zrange(key, 0, -1)
34
+ ])
35
+ expect(results).to.be.eql([values, values, values])
36
+ }
37
+
38
+ async function expectZRangeAllHaveMembers(key: string, values: string[]) {
39
+ const results = await Promise.all([
40
+ client1.zrange(key, 0, -1),
41
+ client2.zrange(key, 0, -1),
42
+ client3.zrange(key, 0, -1)
43
+ ])
44
+ for (const result of results) {
45
+ expect(result).to.have.members(values)
46
+ }
47
+ }
48
+
49
+ async function expectZCardAllEql(key: string, count: number) {
50
+ const results = await Promise.all([
51
+ client1.zcard(key),
52
+ client2.zcard(key),
53
+ client3.zcard(key)
54
+ ])
55
+ expect(results).to.be.eql([count, count, count])
56
+ }
57
+
58
+ describe('RedlockSemaphore', () => {
59
+ it('should fail on invalid arguments', () => {
60
+ expect(
61
+ () => new RedlockSemaphore(null as unknown as Redis[], 'key', 5)
62
+ ).to.throw('"clients" array is required')
63
+ expect(() => new RedlockSemaphore(allClients, '', 5)).to.throw(
64
+ '"key" is required'
65
+ )
66
+ expect(
67
+ () => new RedlockSemaphore(allClients, 1 as unknown as string, 5)
68
+ ).to.throw('"key" must be a string')
69
+ expect(() => new RedlockSemaphore(allClients, 'key', 0)).to.throw(
70
+ '"limit" is required'
71
+ )
72
+ expect(
73
+ () => new RedlockSemaphore(allClients, 'key', '10' as unknown as number)
74
+ ).to.throw('"limit" must be a number')
75
+ })
76
+ it('should acquire and release semaphore', async () => {
77
+ const semaphore1 = new RedlockSemaphore(allClients, 'key', 2)
78
+ const semaphore2 = new RedlockSemaphore(allClients, 'key', 2)
79
+ await semaphore1.acquire()
80
+ await semaphore2.acquire()
81
+ await expectZRangeAllHaveMembers('semaphore:key', [
82
+ semaphore1.identifier,
83
+ semaphore2.identifier
84
+ ])
85
+ await semaphore1.release()
86
+ await expectZRangeAllEql('semaphore:key', [semaphore2.identifier])
87
+ await semaphore2.release()
88
+ await expectZCardAllEql('semaphore:key', 0)
89
+ })
90
+ it('should reject after timeout', async () => {
91
+ const semaphore1 = new RedlockSemaphore(
92
+ allClients,
93
+ 'key',
94
+ 1,
95
+ timeoutOptions
96
+ )
97
+ const semaphore2 = new RedlockSemaphore(
98
+ allClients,
99
+ 'key',
100
+ 1,
101
+ timeoutOptions
102
+ )
103
+ await semaphore1.acquire()
104
+ await expect(semaphore2.acquire()).to.be.rejectedWith(
105
+ 'Acquire redlock-semaphore semaphore:key timeout'
106
+ )
107
+ await semaphore1.release()
108
+ await expectZCardAllEql('semaphore:key', 0)
109
+ })
110
+ it('should refresh lock every refreshInterval ms until release', async () => {
111
+ const semaphore1 = new RedlockSemaphore(
112
+ allClients,
113
+ 'key',
114
+ 2,
115
+ timeoutOptions
116
+ )
117
+ const semaphore2 = new RedlockSemaphore(
118
+ allClients,
119
+ 'key',
120
+ 2,
121
+ timeoutOptions
122
+ )
123
+ await semaphore1.acquire()
124
+ await semaphore2.acquire()
125
+ await delay(400)
126
+ await expectZRangeAllHaveMembers('semaphore:key', [
127
+ semaphore1.identifier,
128
+ semaphore2.identifier
129
+ ])
130
+ await semaphore1.release()
131
+ await expectZRangeAllEql('semaphore:key', [semaphore2.identifier])
132
+ await semaphore2.release()
133
+ await expectZCardAllEql('semaphore:key', 0)
134
+ })
135
+ it('should stop refreshing lock if stopped', async () => {
136
+ const semaphore1 = new RedlockSemaphore(
137
+ allClients,
138
+ 'key',
139
+ 2,
140
+ timeoutOptions
141
+ )
142
+ const semaphore2 = new RedlockSemaphore(
143
+ allClients,
144
+ 'key',
145
+ 2,
146
+ timeoutOptions
147
+ )
148
+ await semaphore1.acquire()
149
+ await semaphore2.acquire()
150
+ semaphore1.stopRefresh()
151
+ await delay(400)
152
+ await expectZRangeAllEql('semaphore:key', [semaphore2.identifier])
153
+ semaphore2.stopRefresh()
154
+ await delay(400)
155
+ await expectZCardAllEql('semaphore:key', 0)
156
+ })
157
+ it('should acquire maximum LIMIT semaphores', async () => {
158
+ const s = () =>
159
+ new RedlockSemaphore(allClients, 'key', 3, {
160
+ acquireTimeout: 1000,
161
+ lockTimeout: 50,
162
+ retryInterval: 10,
163
+ refreshInterval: 0 // disable refresh
164
+ })
165
+ const set1 = [s(), s(), s()]
166
+ const pr1 = Promise.all(set1.map(sem => sem.acquire()))
167
+ await delay(5)
168
+ const set2 = [s(), s(), s()]
169
+ const pr2 = Promise.all(set2.map(sem => sem.acquire()))
170
+ await pr1
171
+ await expectZRangeAllHaveMembers('semaphore:key', [
172
+ set1[0].identifier,
173
+ set1[1].identifier,
174
+ set1[2].identifier
175
+ ])
176
+ await expectZCardAllEql('semaphore:key', 3)
177
+ await pr2
178
+ await expectZRangeAllHaveMembers('semaphore:key', [
179
+ set2[0].identifier,
180
+ set2[1].identifier,
181
+ set2[2].identifier
182
+ ])
183
+ await expectZCardAllEql('semaphore:key', 3)
184
+ })
185
+ it('should support externally acquired semaphore (deprecated interface)', async () => {
186
+ const externalSemaphore = new RedlockSemaphore(allClients, 'key', 3, {
187
+ ...timeoutOptions,
188
+ refreshInterval: 0
189
+ })
190
+ const localSemaphore = new RedlockSemaphore(allClients, 'key', 3, {
191
+ ...timeoutOptions,
192
+ externallyAcquiredIdentifier: externalSemaphore.identifier
193
+ })
194
+ await externalSemaphore.acquire()
195
+ await localSemaphore.acquire()
196
+ await delay(400)
197
+ await expectZRangeAllEql('semaphore:key', [localSemaphore.identifier])
198
+ await localSemaphore.release()
199
+ await expectZCardAllEql('semaphore:key', 0)
200
+ })
201
+ it('should support externally acquired semaphore', async () => {
202
+ const externalSemaphore = new RedlockSemaphore(allClients, 'key', 3, {
203
+ ...timeoutOptions,
204
+ refreshInterval: 0
205
+ })
206
+ const localSemaphore = new RedlockSemaphore(allClients, 'key', 3, {
207
+ ...timeoutOptions,
208
+ identifier: externalSemaphore.identifier,
209
+ acquiredExternally: true
210
+ })
211
+ await externalSemaphore.acquire()
212
+ await localSemaphore.acquire()
213
+ await delay(400)
214
+ await expectZRangeAllEql('semaphore:key', [localSemaphore.identifier])
215
+ await localSemaphore.release()
216
+ await expectZCardAllEql('semaphore:key', 0)
217
+ })
218
+ describe('lost lock case', () => {
219
+ beforeEach(() => {
220
+ catchUnhandledRejection()
221
+ })
222
+ afterEach(() => {
223
+ throwUnhandledRejection()
224
+ })
225
+ it('should throw unhandled error if lock is lost between refreshes', async () => {
226
+ const semaphore = new RedlockSemaphore(
227
+ allClients,
228
+ 'key',
229
+ 3,
230
+ timeoutOptions
231
+ )
232
+ await semaphore.acquire()
233
+ await Promise.all(allClients.map(client => client.del('semaphore:key')))
234
+ await Promise.all(
235
+ allClients.map(client =>
236
+ client.zadd(
237
+ 'semaphore:key',
238
+ Date.now(),
239
+ 'aaa',
240
+ Date.now(),
241
+ 'bbb',
242
+ Date.now(),
243
+ 'ccc'
244
+ )
245
+ )
246
+ )
247
+ await delay(200)
248
+ expect(unhandledRejectionSpy).to.be.called
249
+ expect(unhandledRejectionSpy.firstCall.firstArg instanceof LostLockError)
250
+ .to.be.true
251
+ })
252
+ it('should call onLockLost callback if provided', async () => {
253
+ const onLockLostCallback = sinon.spy(function (this: RedlockSemaphore) {
254
+ expect(this.isAcquired).to.be.false
255
+ })
256
+ const semaphore = new RedlockSemaphore(allClients, 'key', 3, {
257
+ ...timeoutOptions,
258
+ onLockLost: onLockLostCallback
259
+ })
260
+ await semaphore.acquire()
261
+ expect(semaphore.isAcquired).to.be.true
262
+ await Promise.all(allClients.map(client => client.del('semaphore:key')))
263
+ await Promise.all(
264
+ allClients.map(client =>
265
+ client.zadd(
266
+ 'semaphore:key',
267
+ Date.now(),
268
+ 'aaa',
269
+ Date.now(),
270
+ 'bbb',
271
+ Date.now(),
272
+ 'ccc'
273
+ )
274
+ )
275
+ )
276
+ await delay(200)
277
+ expect(semaphore.isAcquired).to.be.false
278
+ expect(unhandledRejectionSpy).to.not.called
279
+ expect(onLockLostCallback).to.be.called
280
+ expect(onLockLostCallback.firstCall.firstArg instanceof LostLockError).to
281
+ .be.true
282
+ })
283
+ })
284
+ describe('reusable', () => {
285
+ it('autorefresh enabled', async () => {
286
+ const semaphore1 = new RedlockSemaphore(
287
+ allClients,
288
+ 'key',
289
+ 2,
290
+ timeoutOptions
291
+ )
292
+ const semaphore2 = new RedlockSemaphore(
293
+ allClients,
294
+ 'key',
295
+ 2,
296
+ timeoutOptions
297
+ )
298
+
299
+ await semaphore1.acquire()
300
+ await semaphore2.acquire()
301
+ await delay(100)
302
+ await semaphore1.release()
303
+ await semaphore2.release()
304
+
305
+ await delay(100)
306
+
307
+ await semaphore1.acquire()
308
+ await semaphore2.acquire()
309
+ await delay(100)
310
+ await semaphore1.release()
311
+ await semaphore2.release()
312
+
313
+ await delay(100)
314
+
315
+ await semaphore1.acquire()
316
+ await semaphore2.acquire()
317
+ await delay(100)
318
+ await semaphore1.release()
319
+ await semaphore2.release()
320
+ })
321
+
322
+ it('autorefresh disabled', async () => {
323
+ const noRefreshOptions = {
324
+ ...timeoutOptions,
325
+ refreshInterval: 0,
326
+ acquireTimeout: 10
327
+ }
328
+ const semaphore1 = new RedlockSemaphore(
329
+ allClients,
330
+ 'key',
331
+ 2,
332
+ noRefreshOptions
333
+ )
334
+ const semaphore2 = new RedlockSemaphore(
335
+ allClients,
336
+ 'key',
337
+ 2,
338
+ noRefreshOptions
339
+ )
340
+ const semaphore3 = new RedlockSemaphore(
341
+ allClients,
342
+ 'key',
343
+ 2,
344
+ noRefreshOptions
345
+ )
346
+
347
+ await semaphore1.acquire()
348
+ await semaphore2.acquire()
349
+ await delay(100)
350
+ await semaphore1.release()
351
+ await semaphore2.release()
352
+
353
+ await delay(100)
354
+
355
+ // [0/2]
356
+ await semaphore1.acquire()
357
+ // [1/2]
358
+ await delay(80)
359
+ await semaphore2.acquire()
360
+ // [2/2]
361
+ await expect(semaphore3.acquire()).to.be.rejectedWith(
362
+ 'Acquire redlock-semaphore semaphore:key timeout'
363
+ ) // rejectes after 10ms
364
+
365
+ // since semaphore1.acquire() elapsed 80ms (delay) + 10ms (semaphore3 timeout)
366
+ // semaphore1 will expire after 300 - 90 = 210ms
367
+ await delay(210)
368
+
369
+ // [1/2]
370
+ await semaphore3.acquire()
371
+ })
372
+ })
373
+ describe('[Node shutdown]', () => {
374
+ afterEach(async () => {
375
+ await Promise.all([upRedisServer(1), upRedisServer(2), upRedisServer(3)])
376
+ })
377
+ it('should handle server shutdown if quorum is alive', async function () {
378
+ this.timeout(60000)
379
+ const semaphore11 = new RedlockSemaphore(
380
+ allClients,
381
+ 'key',
382
+ 3,
383
+ timeoutOptions
384
+ )
385
+ const semaphore12 = new RedlockSemaphore(
386
+ allClients,
387
+ 'key',
388
+ 3,
389
+ timeoutOptions
390
+ )
391
+ const semaphore13 = new RedlockSemaphore(
392
+ allClients,
393
+ 'key',
394
+ 3,
395
+ timeoutOptions
396
+ )
397
+ await Promise.all([
398
+ semaphore11.acquire(),
399
+ semaphore12.acquire(),
400
+ semaphore13.acquire()
401
+ ])
402
+
403
+ // <Server1Failure>
404
+ await downRedisServer(1)
405
+ console.log('SHUT DOWN 1')
406
+
407
+ await delay(1000)
408
+
409
+ // lock survive in server2 and server3
410
+ // semaphore2 will NOT be able to acquire the lock
411
+
412
+ const semaphore2 = new RedlockSemaphore(
413
+ allClients,
414
+ 'key',
415
+ 3,
416
+ timeoutOptions
417
+ )
418
+ await expect(semaphore2.acquire()).to.be.rejectedWith(
419
+ 'Acquire redlock-semaphore semaphore:key timeout'
420
+ )
421
+
422
+ // key in server1 has expired now
423
+
424
+ await upRedisServer(1)
425
+ console.log('ONLINE 1')
426
+
427
+ // let semaphore1[1-3] to refresh lock on server1
428
+ await delay(1000)
429
+ expect(await client1.zrange('semaphore:key', 0, -1)).to.have.members([
430
+ semaphore11.identifier,
431
+ semaphore12.identifier,
432
+ semaphore13.identifier
433
+ ])
434
+ // </Server1Failure>
435
+
436
+ // <Server2Failure>
437
+ await downRedisServer(2)
438
+ console.log('SHUT DOWN 2')
439
+
440
+ await delay(1000)
441
+
442
+ // lock survive in server1 and server3
443
+ // semaphore3 will NOT be able to acquire the lock
444
+
445
+ const semaphore3 = new RedlockSemaphore(
446
+ allClients,
447
+ 'key',
448
+ 3,
449
+ timeoutOptions
450
+ )
451
+ await expect(semaphore3.acquire()).to.be.rejectedWith(
452
+ 'Acquire redlock-semaphore semaphore:key timeout'
453
+ )
454
+
455
+ // key in server2 has expired now
456
+
457
+ await upRedisServer(2)
458
+ console.log('ONLINE 2')
459
+
460
+ // let semaphore1[1-3] to refresh lock on server1
461
+ await delay(1000)
462
+ expect(await client2.zrange('semaphore:key', 0, -1)).to.have.members([
463
+ semaphore11.identifier,
464
+ semaphore12.identifier,
465
+ semaphore13.identifier
466
+ ])
467
+ // </Server2Failure>
468
+
469
+ // <Server3Failure>
470
+ await downRedisServer(3)
471
+ console.log('SHUT DOWN 3')
472
+
473
+ await delay(1000)
474
+
475
+ // lock survive in server1 and server2
476
+ // semaphore4 will NOT be able to acquire the lock
477
+
478
+ const semaphore4 = new RedlockSemaphore(
479
+ allClients,
480
+ 'key',
481
+ 3,
482
+ timeoutOptions
483
+ )
484
+ await expect(semaphore4.acquire()).to.be.rejectedWith(
485
+ 'Acquire redlock-semaphore semaphore:key timeout'
486
+ )
487
+
488
+ // key in server1 has expired now
489
+
490
+ await upRedisServer(3)
491
+ console.log('ONLINE 3')
492
+
493
+ // let semaphore1[1-3] to refresh lock on server1
494
+ await delay(1000)
495
+ expect(await client3.zrange('semaphore:key', 0, -1)).to.have.members([
496
+ semaphore11.identifier,
497
+ semaphore12.identifier,
498
+ semaphore13.identifier
499
+ ])
500
+ // </Server3Failure>
501
+
502
+ await Promise.all([
503
+ semaphore11.release(),
504
+ semaphore12.release(),
505
+ semaphore13.release()
506
+ ])
507
+ })
508
+ it('should fail and release when quorum become dead', async function () {
509
+ this.timeout(60000)
510
+ const onLockLostCallbacks = [1, 2, 3].map(() =>
511
+ sinon.spy(function (this: RedlockSemaphore) {
512
+ expect(this.isAcquired).to.be.false
513
+ })
514
+ )
515
+ const semaphores1 = [1, 2, 3].map(
516
+ (n, i) =>
517
+ new RedlockSemaphore(allClients, 'key', 3, {
518
+ ...timeoutOptions,
519
+ onLockLost: onLockLostCallbacks[i]
520
+ })
521
+ )
522
+ await Promise.all(semaphores1.map(s => s.acquire()))
523
+
524
+ expect(await client1.zrange('semaphore:key', 0, -1)).to.have.members([
525
+ semaphores1[0].identifier,
526
+ semaphores1[1].identifier,
527
+ semaphores1[2].identifier
528
+ ])
529
+ expect(await client2.zrange('semaphore:key', 0, -1)).to.have.members([
530
+ semaphores1[0].identifier,
531
+ semaphores1[1].identifier,
532
+ semaphores1[2].identifier
533
+ ])
534
+ expect(await client3.zrange('semaphore:key', 0, -1)).to.have.members([
535
+ semaphores1[0].identifier,
536
+ semaphores1[1].identifier,
537
+ semaphores1[2].identifier
538
+ ])
539
+
540
+ await downRedisServer(1)
541
+ console.log('SHUT DOWN 1')
542
+
543
+ await downRedisServer(2)
544
+ console.log('SHUT DOWN 2')
545
+
546
+ await delay(1000)
547
+
548
+ for (const lostCb of onLockLostCallbacks) {
549
+ expect(lostCb).to.be.called
550
+ expect(lostCb.firstCall.firstArg instanceof LostLockError).to.be.true
551
+ }
552
+
553
+ // released lock on server3
554
+ expect(await client3.zrange('semaphore:key', 0, -1)).to.be.eql([])
555
+
556
+ // semaphore2 will NOT be able to acquire the lock
557
+
558
+ const semaphore2 = new RedlockSemaphore(
559
+ allClients,
560
+ 'key',
561
+ 3,
562
+ timeoutOptions
563
+ )
564
+ await expect(semaphore2.acquire()).to.be.rejectedWith(
565
+ 'Acquire redlock-semaphore semaphore:key timeout'
566
+ )
567
+ })
568
+ })
569
+ describe('ioredis-mock support', () => {
570
+ it('should acquire and release semaphore', async () => {
571
+ const semaphore1 = new RedlockSemaphore(allClientMocks, 'key', 2)
572
+ const semaphore2 = new RedlockSemaphore(allClientMocks, 'key', 2)
573
+ await semaphore1.acquire()
574
+ await semaphore2.acquire()
575
+ await semaphore1.release()
576
+ await semaphore2.release()
577
+ })
578
+ })
579
+ })
@@ -0,0 +1,22 @@
1
+ import { expect } from 'chai'
2
+ import {
3
+ defaultTimeoutOptions,
4
+ MultiSemaphore,
5
+ Mutex,
6
+ RedlockMultiSemaphore,
7
+ RedlockMutex,
8
+ RedlockSemaphore,
9
+ Semaphore
10
+ } from '../../src/index'
11
+
12
+ describe('index', () => {
13
+ it('should export public API', () => {
14
+ expect(Mutex).to.be.ok
15
+ expect(Semaphore).to.be.ok
16
+ expect(MultiSemaphore).to.be.ok
17
+ expect(RedlockMutex).to.be.ok
18
+ expect(RedlockSemaphore).to.be.ok
19
+ expect(RedlockMultiSemaphore).to.be.ok
20
+ expect(defaultTimeoutOptions).to.be.ok
21
+ })
22
+ })
@@ -0,0 +1,51 @@
1
+ import { expect } from 'chai'
2
+
3
+ import {
4
+ acquireSemaphore as acquire,
5
+ Options
6
+ } from '../../../../src/multiSemaphore/acquire/index'
7
+ import { client1 as client } from '../../../redisClient'
8
+
9
+ const opts = (id: string, overrides?: Partial<Options>): Options => ({
10
+ identifier: id,
11
+ acquireTimeout: 50,
12
+ acquireAttemptsLimit: Number.POSITIVE_INFINITY,
13
+ lockTimeout: 100,
14
+ retryInterval: 10,
15
+ ...overrides
16
+ })
17
+
18
+ describe('multiSemaphore acquire', () => {
19
+ it('should return true for success acquire', async () => {
20
+ const result = await acquire(client, 'key', 1, 1, opts('111'))
21
+ expect(await client.zrange('key', 0, -1)).to.be.eql(['111_0'])
22
+ expect(result).to.be.true
23
+ })
24
+ it('should return false when timeout', async () => {
25
+ const result1 = await acquire(client, 'key', 2, 1, opts('111')) // expire after 100ms
26
+ const result2 = await acquire(client, 'key', 2, 1, opts('112')) // expire after 100ms
27
+ const result3 = await acquire(client, 'key', 2, 1, opts('113')) // timeout after 50ms
28
+
29
+ expect(result1).to.be.true
30
+ expect(result2).to.be.true
31
+ expect(result3).to.be.false
32
+ })
33
+ it('should return false after acquireAttemptsLimit', async () => {
34
+ const result1 = await acquire(client, 'key', 2, 1, opts('111')) // expire after 100ms
35
+ const result2 = await acquire(client, 'key', 2, 1, opts('112')) // expire after 100ms
36
+ const result3 = await acquire(
37
+ client,
38
+ 'key',
39
+ 2,
40
+ 1,
41
+ opts('113', {
42
+ acquireAttemptsLimit: 1,
43
+ acquireTimeout: Number.POSITIVE_INFINITY
44
+ })
45
+ ) // no timeout, attempt limit = 1
46
+
47
+ expect(result1).to.be.true
48
+ expect(result2).to.be.true
49
+ expect(result3).to.be.false
50
+ })
51
+ })