effect-distributed-lock 0.0.2 → 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 +97 -66
  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 -39
  97. package/src/RedisBacking.ts +167 -165
  98. package/src/index.ts +28 -17
  99. package/src/DistributedMutex.ts +0 -304
@@ -0,0 +1,328 @@
1
+ import { expect } from 'chai'
2
+ import { Redis } from 'ioredis'
3
+ import sinon from 'sinon'
4
+ import LostLockError from '../../src/errors/LostLockError'
5
+ import RedlockMutex from '../../src/RedlockMutex'
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 expectGetAll(
30
+ key: string,
31
+ value: string | null,
32
+ clients = allClients
33
+ ) {
34
+ await expect(
35
+ Promise.all([clients[0].get(key), clients[1].get(key), clients[2].get(key)])
36
+ ).to.become([value, value, value])
37
+ }
38
+
39
+ describe('RedlockMutex', () => {
40
+ it('should fail on invalid arguments', () => {
41
+ expect(() => new RedlockMutex(null as unknown as Redis[], 'key')).to.throw(
42
+ '"clients" array is required'
43
+ )
44
+ expect(() => new RedlockMutex(allClients, '')).to.throw('"key" is required')
45
+ expect(() => new RedlockMutex(allClients, 1 as unknown as string)).to.throw(
46
+ '"key" must be a string'
47
+ )
48
+ })
49
+ it('should acquire and release lock', async () => {
50
+ const mutex = new RedlockMutex(allClients, 'key')
51
+ expect(mutex.isAcquired).to.be.false
52
+
53
+ await mutex.acquire()
54
+ expect(mutex.isAcquired).to.be.true
55
+ await expectGetAll('mutex:key', mutex.identifier)
56
+
57
+ await mutex.release()
58
+ expect(mutex.isAcquired).to.be.false
59
+ await expectGetAll('mutex:key', null)
60
+ })
61
+ it('should reject after timeout', async () => {
62
+ const mutex1 = new RedlockMutex(allClients, 'key', timeoutOptions)
63
+ const mutex2 = new RedlockMutex(allClients, 'key', timeoutOptions)
64
+ await mutex1.acquire()
65
+ await expect(mutex2.acquire()).to.be.rejectedWith(
66
+ 'Acquire redlock-mutex mutex:key timeout'
67
+ )
68
+ await mutex1.release()
69
+ await expectGetAll('mutex:key', null)
70
+ })
71
+ it('should refresh lock every refreshInterval ms until release', async () => {
72
+ const mutex = new RedlockMutex(allClients, 'key', timeoutOptions)
73
+ await mutex.acquire()
74
+ await delay(400)
75
+ await expectGetAll('mutex:key', mutex.identifier)
76
+ await mutex.release()
77
+ await expectGetAll('mutex:key', null)
78
+ })
79
+ it('should stop refreshing if stopped', async () => {
80
+ const mutex = new RedlockMutex(allClients, 'key', timeoutOptions)
81
+ await mutex.acquire()
82
+ mutex.stopRefresh()
83
+ await delay(400)
84
+ await expectGetAll('mutex:key', null)
85
+ })
86
+ it('should support externally acquired mutex (deprecated interface)', async () => {
87
+ const externalMutex = new RedlockMutex(allClients, 'key', {
88
+ ...timeoutOptions,
89
+ refreshInterval: 0
90
+ })
91
+ const localMutex = new RedlockMutex(allClients, 'key', {
92
+ ...timeoutOptions,
93
+ externallyAcquiredIdentifier: externalMutex.identifier
94
+ })
95
+ await externalMutex.acquire()
96
+ await localMutex.acquire()
97
+ await delay(400)
98
+ await expectGetAll('mutex:key', localMutex.identifier)
99
+ await localMutex.release()
100
+ await expectGetAll('mutex:key', null)
101
+ })
102
+ it('should support externally acquired mutex', async () => {
103
+ const externalMutex = new RedlockMutex(allClients, 'key', {
104
+ ...timeoutOptions,
105
+ refreshInterval: 0
106
+ })
107
+ const localMutex = new RedlockMutex(allClients, 'key', {
108
+ ...timeoutOptions,
109
+ identifier: externalMutex.identifier,
110
+ acquiredExternally: true
111
+ })
112
+ await externalMutex.acquire()
113
+ await localMutex.acquire()
114
+ await delay(400)
115
+ await expectGetAll('mutex:key', localMutex.identifier)
116
+ await localMutex.release()
117
+ await expectGetAll('mutex:key', null)
118
+ })
119
+ describe('lost lock case', () => {
120
+ beforeEach(() => {
121
+ catchUnhandledRejection()
122
+ })
123
+ afterEach(() => {
124
+ throwUnhandledRejection()
125
+ })
126
+ it('should throw unhandled error if lock is lost between refreshes', async () => {
127
+ const mutex = new RedlockMutex(allClients, 'key', timeoutOptions)
128
+ await mutex.acquire()
129
+ await Promise.all([
130
+ client1.set('mutex:key', '222'), // another instance
131
+ client2.set('mutex:key', '222'), // another instance
132
+ client3.set('mutex:key', '222') // another instance
133
+ ])
134
+ await delay(200)
135
+ expect(unhandledRejectionSpy).to.be.called
136
+ expect(unhandledRejectionSpy.firstCall.firstArg instanceof LostLockError)
137
+ .to.be.true
138
+ })
139
+ it('should call onLockLost callback if provided', async () => {
140
+ const onLockLostCallback = sinon.spy(function (this: RedlockMutex) {
141
+ expect(this.isAcquired).to.be.false
142
+ })
143
+ const mutex = new RedlockMutex(allClients, 'key', {
144
+ ...timeoutOptions,
145
+ onLockLost: onLockLostCallback
146
+ })
147
+ await mutex.acquire()
148
+ expect(mutex.isAcquired).to.be.true
149
+ await Promise.all([
150
+ client1.set('mutex:key', '222'), // another instance
151
+ client2.set('mutex:key', '222'), // another instance
152
+ client3.set('mutex:key', '222') // another instance
153
+ ])
154
+ await delay(200)
155
+ expect(mutex.isAcquired).to.be.false
156
+ expect(unhandledRejectionSpy).to.not.called
157
+ expect(onLockLostCallback).to.be.called
158
+ expect(onLockLostCallback.firstCall.firstArg instanceof LostLockError).to
159
+ .be.true
160
+ })
161
+ })
162
+ it('should be reusable', async () => {
163
+ const mutex = new RedlockMutex(allClients, 'key', timeoutOptions)
164
+
165
+ /* Lifecycle 1 */
166
+ await mutex.acquire()
167
+ await delay(100)
168
+ await expectGetAll('mutex:key', mutex.identifier)
169
+ await mutex.release()
170
+ await expectGetAll('mutex:key', null)
171
+ await delay(100)
172
+ await expectGetAll('mutex:key', null)
173
+
174
+ await delay(100)
175
+
176
+ /* Lifecycle 2 */
177
+ await mutex.acquire()
178
+ await delay(100)
179
+ await expectGetAll('mutex:key', mutex.identifier)
180
+ await mutex.release()
181
+ await expectGetAll('mutex:key', null)
182
+ await delay(100)
183
+ await expectGetAll('mutex:key', null)
184
+
185
+ await delay(100)
186
+
187
+ /* Lifecycle 3 */
188
+ await mutex.acquire()
189
+ await delay(100)
190
+ await expectGetAll('mutex:key', mutex.identifier)
191
+ await mutex.release()
192
+ await expectGetAll('mutex:key', null)
193
+ await delay(100)
194
+ await expectGetAll('mutex:key', null)
195
+ })
196
+ describe('[Node shutdown]', () => {
197
+ afterEach(async () => {
198
+ await Promise.all([upRedisServer(1), upRedisServer(2), upRedisServer(3)])
199
+ })
200
+ it('should handle server shutdown if quorum is alive', async function () {
201
+ this.timeout(60000)
202
+ const mutex1 = new RedlockMutex(allClients, 'key', timeoutOptions)
203
+ await mutex1.acquire()
204
+
205
+ // <Server1Failure>
206
+ await downRedisServer(1)
207
+ console.log('SHUT DOWN 1')
208
+
209
+ await delay(1000)
210
+
211
+ // lock survive in server2 and server3
212
+ // mutex2 will NOT be able to acquire the lock
213
+
214
+ const mutex2 = new RedlockMutex(allClients, 'key', timeoutOptions)
215
+ await expect(mutex2.acquire()).to.be.rejectedWith(
216
+ 'Acquire redlock-mutex mutex:key timeout'
217
+ )
218
+
219
+ // key in server1 has expired now
220
+
221
+ await upRedisServer(1)
222
+ console.log('ONLINE 1')
223
+
224
+ // let mutex1 to refresh lock on server1
225
+ await delay(1000)
226
+ expect(await client1.get('mutex:key')).to.be.eql(mutex1.identifier)
227
+ // </Server1Failure>
228
+
229
+ // <Server2Failure>
230
+ await downRedisServer(2)
231
+ console.log('SHUT DOWN 2')
232
+
233
+ await delay(1000)
234
+
235
+ // lock survive in server1 and server3
236
+ // mutex3 will NOT be able to acquire the lock
237
+
238
+ const mutex3 = new RedlockMutex(allClients, 'key', timeoutOptions)
239
+ await expect(mutex3.acquire()).to.be.rejectedWith(
240
+ 'Acquire redlock-mutex mutex:key timeout'
241
+ )
242
+
243
+ // key in server2 has expired now
244
+
245
+ await upRedisServer(2)
246
+ console.log('ONLINE 2')
247
+
248
+ // let mutex1 to refresh lock on server2
249
+ await delay(1000)
250
+ expect(await client2.get('mutex:key')).to.be.eql(mutex1.identifier)
251
+ // </Server2Failure>
252
+
253
+ // <Server3Failure>
254
+ await downRedisServer(3)
255
+ console.log('SHUT DOWN 3')
256
+
257
+ await delay(1000)
258
+
259
+ // lock survive in server1 and server2
260
+ // mutex4 will NOT be able to acquire the lock
261
+
262
+ const mutex4 = new RedlockMutex(allClients, 'key', timeoutOptions)
263
+ await expect(mutex4.acquire()).to.be.rejectedWith(
264
+ 'Acquire redlock-mutex mutex:key timeout'
265
+ )
266
+
267
+ // key in server3 has expired now
268
+
269
+ await upRedisServer(3)
270
+ console.log('ONLINE 3')
271
+
272
+ // let mutex1 to refresh lock on server3
273
+ await delay(1000)
274
+ expect(await client3.get('mutex:key')).to.be.eql(mutex1.identifier)
275
+ // </Server3Failure>
276
+
277
+ await mutex1.release()
278
+ })
279
+ it('should fail and release when quorum is become dead', async function () {
280
+ this.timeout(60000)
281
+ const onLockLostCallback = sinon.spy(function (this: RedlockMutex) {
282
+ expect(this.isAcquired).to.be.false
283
+ })
284
+ const mutex1 = new RedlockMutex(allClients, 'key', {
285
+ ...timeoutOptions,
286
+ onLockLost: onLockLostCallback
287
+ })
288
+ await mutex1.acquire()
289
+
290
+ await downRedisServer(1)
291
+ console.log('SHUT DOWN 1')
292
+
293
+ await downRedisServer(2)
294
+ console.log('SHUT DOWN 2')
295
+
296
+ await delay(1000)
297
+
298
+ expect(onLockLostCallback).to.be.called
299
+ expect(onLockLostCallback.firstCall.firstArg instanceof LostLockError).to
300
+ .be.true
301
+
302
+ // released lock on server3
303
+ expect(await client3.get('mutex:key')).to.be.eql(null)
304
+
305
+ // mutex2 will NOT be able to acquire the lock cause quorum is dead
306
+
307
+ const mutex2 = new RedlockMutex(allClients, 'key', timeoutOptions)
308
+ await expect(mutex2.acquire()).to.be.rejectedWith(
309
+ 'Acquire redlock-mutex mutex:key timeout'
310
+ )
311
+ })
312
+ })
313
+ describe('ioredis-mock support', () => {
314
+ it('should acquire and release lock', async () => {
315
+ const mutex = new RedlockMutex(allClientMocks, 'key')
316
+ expect(mutex.isAcquired).to.be.false
317
+
318
+ await mutex.acquire()
319
+ console.log('acquired!')
320
+ expect(mutex.isAcquired).to.be.true
321
+ await expectGetAll('mutex:key', mutex.identifier, allClientMocks)
322
+
323
+ await mutex.release()
324
+ expect(mutex.isAcquired).to.be.false
325
+ await expectGetAll('mutex:key', null, allClientMocks)
326
+ })
327
+ })
328
+ })