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.
- package/README.md +97 -66
- 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 -39
- package/src/RedisBacking.ts +167 -165
- package/src/index.ts +28 -17
- package/src/DistributedMutex.ts +0 -304
|
@@ -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
|
+
})
|