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