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,334 @@
1
+ import { expect } from 'chai'
2
+ import { Redis } from 'ioredis'
3
+ import sinon from 'sinon'
4
+ import LostLockError from '../../src/errors/LostLockError'
5
+ import Mutex from '../../src/RedisMutex'
6
+ import { TimeoutOptions } from '../../src/types'
7
+ import { delay } from '../../src/utils/index'
8
+ import { client1 as client, clientMock1 as clientMock } from '../redisClient'
9
+ import { downRedisServer, upRedisServer } from '../shell'
10
+ import {
11
+ catchUnhandledRejection,
12
+ throwUnhandledRejection,
13
+ unhandledRejectionSpy
14
+ } from '../unhandledRejection'
15
+
16
+ const timeoutOptions: TimeoutOptions = {
17
+ lockTimeout: 300,
18
+ acquireTimeout: 100,
19
+ refreshInterval: 80,
20
+ retryInterval: 10
21
+ }
22
+
23
+ describe('Mutex', () => {
24
+ it('should fail on invalid arguments', () => {
25
+ expect(() => new Mutex(null as unknown as Redis, 'key')).to.throw(
26
+ '"client" is required'
27
+ )
28
+ expect(() => new Mutex(client, '')).to.throw('"key" is required')
29
+ expect(() => new Mutex(client, 1 as unknown as string)).to.throw(
30
+ '"key" must be a string'
31
+ )
32
+ expect(() => new Mutex(client, 'key', { identifier: '' })).to.throw(
33
+ 'identifier must be not empty random string'
34
+ )
35
+ expect(
36
+ () => new Mutex(client, 'key', { acquiredExternally: true })
37
+ ).to.throw('acquiredExternally=true meanless without custom identifier')
38
+ expect(
39
+ () =>
40
+ new Mutex(client, 'key', {
41
+ externallyAcquiredIdentifier: '123',
42
+ identifier: '123'
43
+ })
44
+ ).to.throw(
45
+ 'Invalid usage. Use custom identifier and acquiredExternally: true'
46
+ )
47
+ expect(
48
+ () =>
49
+ new Mutex(client, 'key', {
50
+ externallyAcquiredIdentifier: '123',
51
+ acquiredExternally: true,
52
+ identifier: '123'
53
+ })
54
+ ).to.throw(
55
+ 'Invalid usage. Use custom identifier and acquiredExternally: true'
56
+ )
57
+ })
58
+ it('should set default options', () => {
59
+ expect(new Mutex(client, 'key', {})).to.be.ok
60
+ expect(new Mutex(client, 'key')).to.be.ok
61
+ })
62
+ it('should set random UUID as identifier', () => {
63
+ expect(new Mutex(client, 'key').identifier).to.match(
64
+ /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/
65
+ )
66
+ })
67
+ it('should add identifier suffix', () => {
68
+ expect(
69
+ new Mutex(client, 'key', { identifierSuffix: 'abc' }).identifier
70
+ ).to.match(
71
+ /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}-abc$/
72
+ )
73
+ })
74
+ it('should use custom identifier if provided', () => {
75
+ expect(
76
+ new Mutex(client, 'key', { identifier: 'abc' }).identifier
77
+ ).to.be.eql('abc')
78
+ })
79
+ it('should acquire and release lock', async () => {
80
+ const mutex = new Mutex(client, 'key')
81
+ expect(mutex.isAcquired).to.be.false
82
+ await mutex.acquire()
83
+ expect(mutex.isAcquired).to.be.true
84
+ expect(await client.get('mutex:key')).to.be.eql(mutex.identifier)
85
+ await mutex.release()
86
+ expect(mutex.isAcquired).to.be.false
87
+ expect(await client.get('mutex:key')).to.be.eql(null)
88
+ })
89
+ it('should reject after timeout', async () => {
90
+ const mutex1 = new Mutex(client, 'key', timeoutOptions)
91
+ const mutex2 = new Mutex(client, 'key', timeoutOptions)
92
+ await mutex1.acquire()
93
+ await expect(mutex2.acquire()).to.be.rejectedWith(
94
+ 'Acquire mutex mutex:key timeout'
95
+ )
96
+ await mutex1.release()
97
+ expect(await client.get('mutex:key')).to.be.eql(null)
98
+ })
99
+ it('should return false for tryAcquire after timeout', async () => {
100
+ const mutex1 = new Mutex(client, 'key', timeoutOptions)
101
+ const mutex2 = new Mutex(client, 'key', timeoutOptions)
102
+ await mutex1.acquire()
103
+ const result = await mutex2.tryAcquire()
104
+ expect(result).to.be.false
105
+ await mutex1.release()
106
+ expect(await client.get('mutex:key')).to.be.eql(null)
107
+ })
108
+ it('should return true for successful tryAcquire', async () => {
109
+ const mutex = new Mutex(client, 'key', timeoutOptions)
110
+ const result = await mutex.tryAcquire()
111
+ expect(result).to.be.true
112
+ await mutex.release()
113
+ expect(await client.get('mutex:key')).to.be.eql(null)
114
+ })
115
+ it('should refresh lock every refreshInterval ms until release', async () => {
116
+ const mutex = new Mutex(client, 'key', timeoutOptions)
117
+ await mutex.acquire()
118
+ await delay(400)
119
+ expect(await client.get('mutex:key')).to.be.eql(mutex.identifier)
120
+ await mutex.release()
121
+ expect(await client.get('mutex:key')).to.be.eql(null)
122
+ })
123
+ it('should stop refreshing lock every refreshInterval ms if stopped', async () => {
124
+ const mutex = new Mutex(client, 'key', timeoutOptions)
125
+ await mutex.acquire()
126
+ mutex.stopRefresh()
127
+ await delay(400)
128
+ expect(await client.get('mutex:key')).to.be.eql(null)
129
+ })
130
+ it('should not call _refresh if already refreshing', async () => {
131
+ const mutex = new Mutex(client, 'key', timeoutOptions)
132
+ let callCount = 0
133
+ Object.assign(mutex, {
134
+ _refresh: () =>
135
+ delay(100).then(() => {
136
+ callCount++
137
+ return true
138
+ })
139
+ })
140
+ await mutex.acquire()
141
+ await delay(400)
142
+ expect(callCount).to.be.eql(2) // not floor(400/80) = 9
143
+ })
144
+
145
+ it('should support externally acquired mutex (deprecated interface)', async () => {
146
+ const externalMutex = new Mutex(client, 'key', {
147
+ ...timeoutOptions,
148
+ refreshInterval: 0
149
+ })
150
+ const localMutex = new Mutex(client, 'key', {
151
+ ...timeoutOptions,
152
+ externallyAcquiredIdentifier: externalMutex.identifier
153
+ })
154
+ await externalMutex.acquire()
155
+ await localMutex.acquire()
156
+ await delay(400)
157
+ expect(await client.get('mutex:key')).to.be.eql(localMutex.identifier)
158
+ await localMutex.release()
159
+ expect(await client.get('mutex:key')).to.be.eql(null)
160
+ })
161
+ it('should support externally acquired mutex', async () => {
162
+ const externalMutex = new Mutex(client, 'key', {
163
+ ...timeoutOptions,
164
+ refreshInterval: 0
165
+ })
166
+ const localMutex = new Mutex(client, 'key', {
167
+ ...timeoutOptions,
168
+ identifier: externalMutex.identifier,
169
+ acquiredExternally: true
170
+ })
171
+ await externalMutex.acquire()
172
+ await localMutex.acquire()
173
+ await delay(400)
174
+ expect(await client.get('mutex:key')).to.be.eql(localMutex.identifier)
175
+ await localMutex.release()
176
+ expect(await client.get('mutex:key')).to.be.eql(null)
177
+ })
178
+ describe('lost lock case', () => {
179
+ beforeEach(() => {
180
+ catchUnhandledRejection()
181
+ })
182
+ afterEach(() => {
183
+ throwUnhandledRejection()
184
+ })
185
+ it('should throw unhandled error if lock was lost between refreshes (another instance acquired)', async () => {
186
+ const mutex = new Mutex(client, 'key', timeoutOptions)
187
+ await mutex.acquire()
188
+ expect(mutex.isAcquired).to.be.true
189
+ await client.set('mutex:key', '222') // another instance
190
+ await delay(200)
191
+ expect(mutex.isAcquired).to.be.false
192
+ expect(unhandledRejectionSpy).to.be.called
193
+ expect(unhandledRejectionSpy.firstCall.firstArg instanceof LostLockError)
194
+ .to.be.true
195
+ })
196
+ it('should throw unhandled error if lock was lost between refreshes (lock expired)', async () => {
197
+ const mutex = new Mutex(client, 'key', timeoutOptions)
198
+ await mutex.acquire()
199
+ expect(mutex.isAcquired).to.be.true
200
+ await client.del('mutex:key') // expired
201
+ await delay(200)
202
+ expect(mutex.isAcquired).to.be.false
203
+ expect(unhandledRejectionSpy).to.be.called
204
+ expect(unhandledRejectionSpy.firstCall.firstArg instanceof LostLockError)
205
+ .to.be.true
206
+ })
207
+ it('should call onLockLost callback if provided (another instance acquired)', async () => {
208
+ const onLockLostCallback = sinon.spy(function (this: Mutex) {
209
+ expect(this.isAcquired).to.be.false
210
+ })
211
+ const mutex = new Mutex(client, 'key', {
212
+ ...timeoutOptions,
213
+ onLockLost: onLockLostCallback
214
+ })
215
+ await mutex.acquire()
216
+ expect(mutex.isAcquired).to.be.true
217
+ await client.set('mutex:key', '222') // another instance
218
+ await delay(200)
219
+ expect(mutex.isAcquired).to.be.false
220
+ expect(unhandledRejectionSpy).to.not.called
221
+ expect(onLockLostCallback).to.be.called
222
+ expect(onLockLostCallback.firstCall.firstArg instanceof LostLockError).to
223
+ .be.true
224
+ })
225
+ it('should call onLockLost callback if provided (lock expired)', async () => {
226
+ const onLockLostCallback = sinon.spy(function (this: Mutex) {
227
+ expect(this.isAcquired).to.be.false
228
+ })
229
+ const mutex = new Mutex(client, 'key', {
230
+ ...timeoutOptions,
231
+ onLockLost: onLockLostCallback
232
+ })
233
+ await mutex.acquire()
234
+ expect(mutex.isAcquired).to.be.true
235
+ await client.del('mutex:key') // expired
236
+ await delay(200)
237
+ expect(mutex.isAcquired).to.be.false
238
+ expect(unhandledRejectionSpy).to.not.called
239
+ expect(onLockLostCallback).to.be.called
240
+ expect(onLockLostCallback.firstCall.firstArg instanceof LostLockError).to
241
+ .be.true
242
+ })
243
+ })
244
+ it('should be reusable', async function () {
245
+ this.timeout(10000)
246
+ const mutex = new Mutex(client, 'key', timeoutOptions)
247
+
248
+ /* Lifecycle 1 */
249
+ await mutex.acquire()
250
+ await delay(300)
251
+ expect(await client.get('mutex:key')).to.be.eql(mutex.identifier)
252
+ await mutex.release()
253
+ expect(await client.get('mutex:key')).to.be.eql(null)
254
+ await delay(300)
255
+ expect(await client.get('mutex:key')).to.be.eql(null)
256
+
257
+ await delay(300)
258
+
259
+ /* Lifecycle 2 */
260
+ await mutex.acquire()
261
+ await delay(300)
262
+ expect(await client.get('mutex:key')).to.be.eql(mutex.identifier)
263
+ await mutex.release()
264
+ expect(await client.get('mutex:key')).to.be.eql(null)
265
+ await delay(300)
266
+ expect(await client.get('mutex:key')).to.be.eql(null)
267
+
268
+ await delay(300)
269
+
270
+ /* Lifecycle 3 */
271
+ await mutex.acquire()
272
+ await delay(300)
273
+ expect(await client.get('mutex:key')).to.be.eql(mutex.identifier)
274
+ await mutex.release()
275
+ expect(await client.get('mutex:key')).to.be.eql(null)
276
+ await delay(300)
277
+ expect(await client.get('mutex:key')).to.be.eql(null)
278
+ })
279
+ describe('[Node shutdown]', () => {
280
+ beforeEach(() => {
281
+ catchUnhandledRejection()
282
+ })
283
+ afterEach(async () => {
284
+ throwUnhandledRejection()
285
+ await upRedisServer(1)
286
+ })
287
+ it('should lost lock when node become alive', async function () {
288
+ this.timeout(60000)
289
+ const onLockLostCallback = sinon.spy(function (this: Mutex) {
290
+ expect(this.isAcquired).to.be.false
291
+ })
292
+ const mutex1 = new Mutex(client, 'key', {
293
+ ...timeoutOptions,
294
+ onLockLost: onLockLostCallback
295
+ })
296
+ await mutex1.acquire()
297
+ await downRedisServer(1)
298
+
299
+ await delay(1000)
300
+ // lock expired now
301
+
302
+ await upRedisServer(1)
303
+ // mutex was expired, key was deleted in redis
304
+ // give refresh mechanism time to detect lock lost
305
+ // (includes client reconnection time)
306
+ await delay(1000)
307
+
308
+ expect(await client.get('mutex:key')).to.be.eql(null)
309
+ expect(onLockLostCallback).to.be.called
310
+ expect(onLockLostCallback.firstCall.firstArg instanceof LostLockError).to
311
+ .be.true
312
+
313
+ // lock was not reacquired by mutex1, so mutex2 can acquire the lock
314
+
315
+ const mutex2 = new Mutex(client, 'key', timeoutOptions)
316
+ await mutex2.acquire()
317
+ expect(await client.get('mutex:key')).to.be.eql(mutex2.identifier)
318
+
319
+ await Promise.all([mutex1.release(), mutex2.release()])
320
+ })
321
+ })
322
+ describe('ioredis-mock support', async () => {
323
+ it('should acquire and release lock', async () => {
324
+ const mutex = new Mutex(clientMock, 'key')
325
+ expect(mutex.isAcquired).to.be.false
326
+ await mutex.acquire()
327
+ expect(mutex.isAcquired).to.be.true
328
+ expect(await clientMock.get('mutex:key')).to.be.eql(mutex.identifier)
329
+ await mutex.release()
330
+ expect(mutex.isAcquired).to.be.false
331
+ expect(await clientMock.get('mutex:key')).to.be.eql(null)
332
+ })
333
+ })
334
+ })
@@ -0,0 +1,367 @@
1
+ import { expect } from 'chai'
2
+ import { Redis } from 'ioredis'
3
+ import sinon from 'sinon'
4
+ import LostLockError from '../../src/errors/LostLockError'
5
+ import Semaphore from '../../src/RedisSemaphore'
6
+ import { TimeoutOptions } from '../../src/types'
7
+ import { delay } from '../../src/utils/index'
8
+ import { client1 as client, clientMock1 as clientMock } from '../redisClient'
9
+ import { downRedisServer, upRedisServer } from '../shell'
10
+ import {
11
+ catchUnhandledRejection,
12
+ throwUnhandledRejection,
13
+ unhandledRejectionSpy
14
+ } from '../unhandledRejection'
15
+
16
+ const timeoutOptions: TimeoutOptions = {
17
+ lockTimeout: 300,
18
+ acquireTimeout: 100,
19
+ refreshInterval: 80,
20
+ retryInterval: 10
21
+ }
22
+
23
+ describe('Semaphore', () => {
24
+ it('should fail on invalid arguments', () => {
25
+ expect(() => new Semaphore(null as unknown as Redis, 'key', 5)).to.throw(
26
+ '"client" is required'
27
+ )
28
+ expect(() => new Semaphore(client, '', 5)).to.throw('"key" is required')
29
+ expect(() => new Semaphore(client, 1 as unknown as string, 5)).to.throw(
30
+ '"key" must be a string'
31
+ )
32
+ expect(() => new Semaphore(client, 'key', 0)).to.throw(
33
+ '"limit" is required'
34
+ )
35
+ expect(
36
+ () => new Semaphore(client, 'key', '10' as unknown as number)
37
+ ).to.throw('"limit" must be a number')
38
+ })
39
+ it('should acquire and release semaphore', async () => {
40
+ const semaphore1 = new Semaphore(client, 'key', 2)
41
+ const semaphore2 = new Semaphore(client, 'key', 2)
42
+ expect(semaphore1.isAcquired).to.be.false
43
+ expect(semaphore2.isAcquired).to.be.false
44
+
45
+ await semaphore1.acquire()
46
+ expect(semaphore1.isAcquired).to.be.true
47
+ await semaphore2.acquire()
48
+ expect(semaphore2.isAcquired).to.be.true
49
+ expect(await client.zrange('semaphore:key', 0, -1)).to.have.members([
50
+ semaphore1.identifier,
51
+ semaphore2.identifier
52
+ ])
53
+
54
+ await semaphore1.release()
55
+ expect(semaphore1.isAcquired).to.be.false
56
+ expect(await client.zrange('semaphore:key', 0, -1)).to.be.eql([
57
+ semaphore2.identifier
58
+ ])
59
+ await semaphore2.release()
60
+ expect(semaphore2.isAcquired).to.be.false
61
+ expect(await client.zcard('semaphore:key')).to.be.eql(0)
62
+ })
63
+ it('should reject after timeout', async () => {
64
+ const semaphore1 = new Semaphore(client, 'key', 1, timeoutOptions)
65
+ const semaphore2 = new Semaphore(client, 'key', 1, timeoutOptions)
66
+ await semaphore1.acquire()
67
+ await expect(semaphore2.acquire()).to.be.rejectedWith(
68
+ 'Acquire semaphore semaphore:key timeout'
69
+ )
70
+ await semaphore1.release()
71
+ expect(await client.get('semaphore:key')).to.be.eql(null)
72
+ })
73
+ it('should refresh lock every refreshInterval ms until release', async () => {
74
+ const semaphore1 = new Semaphore(client, 'key', 2, timeoutOptions)
75
+ const semaphore2 = new Semaphore(client, 'key', 2, timeoutOptions)
76
+ await semaphore1.acquire()
77
+ await semaphore2.acquire()
78
+ await delay(400)
79
+ expect(await client.zrange('semaphore:key', 0, -1)).to.have.members([
80
+ semaphore1.identifier,
81
+ semaphore2.identifier
82
+ ])
83
+ await semaphore1.release()
84
+ expect(await client.zrange('semaphore:key', 0, -1)).to.be.eql([
85
+ semaphore2.identifier
86
+ ])
87
+ await semaphore2.release()
88
+ expect(await client.zcard('semaphore:key')).to.be.eql(0)
89
+ })
90
+ it('should stop refreshing lock if stopped', async () => {
91
+ const semaphore1 = new Semaphore(client, 'key', 2, timeoutOptions)
92
+ const semaphore2 = new Semaphore(client, 'key', 2, timeoutOptions)
93
+ await semaphore1.acquire()
94
+ await semaphore2.acquire()
95
+ await semaphore1.stopRefresh()
96
+ await delay(400)
97
+ expect(await client.zrange('semaphore:key', 0, -1)).to.be.eql([
98
+ semaphore2.identifier
99
+ ])
100
+ await semaphore2.stopRefresh()
101
+ await delay(400)
102
+ expect(await client.zcard('semaphore:key')).to.be.eql(0)
103
+ })
104
+ it('should acquire maximum LIMIT semaphores', async () => {
105
+ const s = () =>
106
+ new Semaphore(client, 'key', 3, {
107
+ acquireTimeout: 1000,
108
+ lockTimeout: 50,
109
+ retryInterval: 10,
110
+ refreshInterval: 0 // disable refresh
111
+ })
112
+ const pr1 = Promise.all([s().acquire(), s().acquire(), s().acquire()])
113
+ await delay(5)
114
+ const pr2 = Promise.all([s().acquire(), s().acquire(), s().acquire()])
115
+ await pr1
116
+ const ids1 = await client.zrange('semaphore:key', 0, -1)
117
+ expect(ids1.length).to.be.eql(3)
118
+ await pr2
119
+ const ids2 = await client.zrange('semaphore:key', 0, -1)
120
+ expect(ids2.length).to.be.eql(3)
121
+ expect(ids2)
122
+ .to.not.include(ids1[0])
123
+ .and.not.include(ids1[1])
124
+ .and.not.include(ids1[2])
125
+ })
126
+ it('should support externally acquired semaphore (deprecated interface)', async () => {
127
+ const externalSemaphore = new Semaphore(client, 'key', 3, {
128
+ ...timeoutOptions,
129
+ refreshInterval: 0
130
+ })
131
+ const localSemaphore = new Semaphore(client, 'key', 3, {
132
+ ...timeoutOptions,
133
+ externallyAcquiredIdentifier: externalSemaphore.identifier
134
+ })
135
+ await externalSemaphore.acquire()
136
+ await localSemaphore.acquire()
137
+ await delay(400)
138
+ expect(await client.zrange('semaphore:key', 0, -1)).to.have.members([
139
+ localSemaphore.identifier
140
+ ])
141
+ await localSemaphore.release()
142
+ expect(await client.zcard('semaphore:key')).to.be.eql(0)
143
+ })
144
+ it('should support externally acquired semaphore', async () => {
145
+ const externalSemaphore = new Semaphore(client, 'key', 3, {
146
+ ...timeoutOptions,
147
+ refreshInterval: 0
148
+ })
149
+ const localSemaphore = new Semaphore(client, 'key', 3, {
150
+ ...timeoutOptions,
151
+ identifier: externalSemaphore.identifier,
152
+ acquiredExternally: true
153
+ })
154
+ await externalSemaphore.acquire()
155
+ await localSemaphore.acquire()
156
+ await delay(400)
157
+ expect(await client.zrange('semaphore:key', 0, -1)).to.have.members([
158
+ localSemaphore.identifier
159
+ ])
160
+ await localSemaphore.release()
161
+ expect(await client.zcard('semaphore:key')).to.be.eql(0)
162
+ })
163
+ describe('lost lock case', () => {
164
+ beforeEach(() => {
165
+ catchUnhandledRejection()
166
+ })
167
+ afterEach(() => {
168
+ throwUnhandledRejection()
169
+ })
170
+ it('should throw unhandled error if lock is lost between refreshes', async () => {
171
+ const semaphore = new Semaphore(client, 'key', 3, timeoutOptions)
172
+ await semaphore.acquire()
173
+ await client.del('semaphore:key')
174
+ await client.zadd(
175
+ 'semaphore:key',
176
+ Date.now(),
177
+ 'aaa',
178
+ Date.now(),
179
+ 'bbb',
180
+ Date.now(),
181
+ 'ccc'
182
+ )
183
+ await delay(200)
184
+ expect(unhandledRejectionSpy).to.be.called
185
+ expect(unhandledRejectionSpy.firstCall.firstArg instanceof LostLockError)
186
+ .to.be.true
187
+ })
188
+ it('should call onLockLost callback if provided', async () => {
189
+ const onLockLostCallback = sinon.spy(function (this: Semaphore) {
190
+ expect(this.isAcquired).to.be.false
191
+ })
192
+ const semaphore = new Semaphore(client, 'key', 3, {
193
+ ...timeoutOptions,
194
+ onLockLost: onLockLostCallback
195
+ })
196
+ await semaphore.acquire()
197
+ expect(semaphore.isAcquired).to.be.true
198
+ await client.del('semaphore:key')
199
+ await client.zadd(
200
+ 'semaphore:key',
201
+ Date.now(),
202
+ 'aaa',
203
+ Date.now(),
204
+ 'bbb',
205
+ Date.now(),
206
+ 'ccc'
207
+ )
208
+ await delay(200)
209
+ expect(semaphore.isAcquired).to.be.false
210
+ expect(unhandledRejectionSpy).to.not.called
211
+ expect(onLockLostCallback).to.be.called
212
+ expect(onLockLostCallback.firstCall.firstArg instanceof LostLockError).to
213
+ .be.true
214
+ })
215
+ })
216
+ describe('reusable', () => {
217
+ it('autorefresh enabled', async () => {
218
+ const semaphore1 = new Semaphore(client, 'key', 2, timeoutOptions)
219
+ const semaphore2 = new Semaphore(client, 'key', 2, timeoutOptions)
220
+
221
+ await semaphore1.acquire()
222
+ await semaphore2.acquire()
223
+ await delay(300)
224
+ await semaphore1.release()
225
+ await semaphore2.release()
226
+
227
+ await delay(300)
228
+
229
+ await semaphore1.acquire()
230
+ await semaphore2.acquire()
231
+ await delay(300)
232
+ await semaphore1.release()
233
+ await semaphore2.release()
234
+
235
+ await delay(300)
236
+
237
+ await semaphore1.acquire()
238
+ await semaphore2.acquire()
239
+ await delay(300)
240
+ await semaphore1.release()
241
+ await semaphore2.release()
242
+ })
243
+
244
+ it('autorefresh disabled', async () => {
245
+ const noRefreshOptions = {
246
+ ...timeoutOptions,
247
+ refreshInterval: 0,
248
+ acquireTimeout: 10
249
+ }
250
+ const semaphore1 = new Semaphore(client, 'key', 2, noRefreshOptions)
251
+ const semaphore2 = new Semaphore(client, 'key', 2, noRefreshOptions)
252
+ const semaphore3 = new Semaphore(client, 'key', 2, noRefreshOptions)
253
+
254
+ await semaphore1.acquire()
255
+ await semaphore2.acquire()
256
+ await delay(300)
257
+ await semaphore1.release()
258
+ await semaphore2.release()
259
+
260
+ await delay(300)
261
+
262
+ // [0/2]
263
+ await semaphore1.acquire()
264
+ // [1/2]
265
+ await delay(80)
266
+ await semaphore2.acquire()
267
+ // [2/2]
268
+ await expect(semaphore3.acquire()).to.be.rejectedWith(
269
+ 'Acquire semaphore semaphore:key timeout'
270
+ ) // rejectes after 10ms
271
+
272
+ // since semaphore1.acquire() elapsed 80ms (delay) + 10ms (semaphore3 timeout)
273
+ // semaphore1 will expire after 300 - 90 = 210ms
274
+ await delay(210)
275
+
276
+ // [1/2]
277
+ await semaphore3.acquire()
278
+ })
279
+ })
280
+ describe('[Node shutdown]', () => {
281
+ beforeEach(() => {
282
+ catchUnhandledRejection()
283
+ })
284
+ afterEach(async () => {
285
+ throwUnhandledRejection()
286
+ await upRedisServer(1)
287
+ })
288
+ it('should lost lock when node become alive', async function () {
289
+ this.timeout(60000)
290
+ const onLockLostCallbacks = [1, 2, 3].map(() =>
291
+ sinon.spy(function (this: Semaphore) {
292
+ expect(this.isAcquired).to.be.false
293
+ })
294
+ )
295
+ const semaphores1 = [1, 2, 3].map(
296
+ (n, i) =>
297
+ new Semaphore(client, 'key', 3, {
298
+ ...timeoutOptions,
299
+ onLockLost: onLockLostCallbacks[i]
300
+ })
301
+ )
302
+
303
+ await Promise.all(semaphores1.map(s => s.acquire()))
304
+
305
+ await downRedisServer(1)
306
+ console.log('SHUT DOWN')
307
+
308
+ await delay(1000)
309
+
310
+ await upRedisServer(1)
311
+ console.log('ONLINE')
312
+
313
+ // semaphore was expired, key was deleted in redis
314
+ // give refresh mechanism time to detect lock lost
315
+ // (includes reconnection time)
316
+ await delay(1000)
317
+
318
+ const data = await client.zrange('semaphore:key', 0, -1, 'WITHSCORES')
319
+ expect(data).to.be.eql([])
320
+
321
+ // console.log(data)
322
+ // expect(data).to.include(semaphore11.identifier)
323
+ // expect(data).to.include(semaphore12.identifier)
324
+ // expect(data).to.include(semaphore13.identifier)
325
+
326
+ // lock was not reacquired by semaphore1[1-3], so semaphore2 can acquire the lock
327
+
328
+ const semaphore2 = new Semaphore(client, 'key', 3, timeoutOptions)
329
+ await semaphore2.acquire()
330
+
331
+ expect(await client.zrange('semaphore:key', 0, -1)).to.have.members([
332
+ semaphore2.identifier
333
+ ])
334
+
335
+ await Promise.all([
336
+ ...semaphores1.map(s => s.release()),
337
+ semaphore2.release()
338
+ ])
339
+ })
340
+ })
341
+ describe('ioredis-mock support', async () => {
342
+ it('should acquire and release semaphore', async () => {
343
+ const semaphore1 = new Semaphore(clientMock, 'key', 2)
344
+ const semaphore2 = new Semaphore(clientMock, 'key', 2)
345
+ expect(semaphore1.isAcquired).to.be.false
346
+ expect(semaphore2.isAcquired).to.be.false
347
+
348
+ await semaphore1.acquire()
349
+ expect(semaphore1.isAcquired).to.be.true
350
+ await semaphore2.acquire()
351
+ expect(semaphore2.isAcquired).to.be.true
352
+ expect(await clientMock.zrange('semaphore:key', 0, -1)).to.have.members([
353
+ semaphore1.identifier,
354
+ semaphore2.identifier
355
+ ])
356
+
357
+ await semaphore1.release()
358
+ expect(semaphore1.isAcquired).to.be.false
359
+ expect(await clientMock.zrange('semaphore:key', 0, -1)).to.be.eql([
360
+ semaphore2.identifier
361
+ ])
362
+ await semaphore2.release()
363
+ expect(semaphore2.isAcquired).to.be.false
364
+ expect(await clientMock.zcard('semaphore:key')).to.be.eql(0)
365
+ })
366
+ })
367
+ })