effect-distributed-lock 0.0.3 → 0.0.5

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 +156 -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 +448 -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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2018 Alexander Mochalin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,445 @@
1
+ # redis-semaphore
2
+
3
+ [![NPM version][npm-image]][npm-url]
4
+ [![Build status][ci-image]][ci-url]
5
+ ![FOSSA Status][typescript-image]
6
+ [![Coverage Status][coverage-image]][coverage-url]
7
+ [![Maintainability][codeclimate-image]][codeclimate-url]
8
+ [![Known Vulnerabilities][snyk-image]][snyk-url]
9
+ [![FOSSA Status][fossa-badge-image]][fossa-badge-url]
10
+
11
+ [Mutex](<https://en.wikipedia.org/wiki/Lock_(computer_science)>) and [Semaphore](<https://en.wikipedia.org/wiki/Semaphore_(programming)>) implementations based on [Redis](https://redis.io/) ready for distributed systems
12
+
13
+ ## Features
14
+
15
+ - Fail-safe (all actions performed by LUA scripts (atomic))
16
+
17
+ ## Usage
18
+
19
+ ### Installation
20
+
21
+ ```bash
22
+ npm install --save redis-semaphore ioredis
23
+ # or
24
+ yarn add redis-semaphore ioredis
25
+ ```
26
+
27
+ ioredis is the officially supported Redis client. This library's test code runs on it.
28
+
29
+ Users of other Redis clients should ensure ioredis-compatible API (see src/types.ts) when creating lock objects.
30
+
31
+ ### Mutex
32
+
33
+ > See [RedisLabs: Locks with timeouts](https://redislabs.com/ebook/part-2-core-concepts/chapter-6-application-components-in-redis/6-2-distributed-locking/6-2-5-locks-with-timeouts/)
34
+
35
+ ##### new Mutex(redisClient, key [, { lockTimeout = 10000, acquireTimeout = 10000, acquireAttemptsLimit = Number.POSITIVE_INFINITY, retryInterval = 10, refreshInterval = lockTimeout * 0.8, identifier = crypto.randomUUID() }])
36
+
37
+ - `redisClient` - **required**, configured `redis` client
38
+ - `key` - **required**, key for locking resource (final key in redis: `mutex:<key>`)
39
+ - `options` - _optional_
40
+ - `lockTimeout` - _optional_ ms, time after mutex will be auto released (expired)
41
+ - `acquireTimeout` - _optional_ ms, max timeout for `.acquire()` call
42
+ - `acquireAttemptsLimit` - _optional_ max number of attempts to be made in `.acquire()` call
43
+ - `retryInterval` - _optional_ ms, time between acquire attempts if resource locked
44
+ - `refreshInterval` - _optional_ ms, auto-refresh interval; to disable auto-refresh behaviour set `0`
45
+ - `identifier` - _optional_ uuid, custom mutex identifier. Must be unique between parallel executors, otherwise multiple locks with same identifier *can* be treated as the same lock holder. Override only if you know what you are doing (see `acquiredExternally` option).
46
+ - `acquiredExternally` - _optional_ `true`, If `identifier` provided and `acquiredExternally` is `true` then `_refresh` will be used instead of `_acquire` in `.tryAcquire()`/`.acquire()`. Useful for lock sharing between processes: acquire in scheduler, refresh and release in handler.
47
+ - `onLockLost` - _optional_ function, called when lock loss is detected due refresh cycle; default onLockLost throws unhandled LostLockError
48
+
49
+ #### Example
50
+
51
+ ```javascript
52
+ const Mutex = require('redis-semaphore').Mutex
53
+ const Redis = require('ioredis')
54
+
55
+ // TypeScript
56
+ // import { Mutex } from 'redis-semaphore'
57
+ // import Redis from 'ioredis'
58
+
59
+ const redisClient = new Redis()
60
+
61
+ async function doSomething() {
62
+ const mutex = new Mutex(redisClient, 'lockingResource')
63
+ await mutex.acquire()
64
+ try {
65
+ // critical code
66
+ } finally {
67
+ await mutex.release()
68
+ }
69
+ }
70
+ ```
71
+
72
+ #### Example with lost lock handling
73
+
74
+ ```javascript
75
+ async function doSomething() {
76
+ const mutex = new Mutex(redisClient, 'lockingResource', {
77
+ // By default onLockLost throws unhandled LostLockError
78
+ onLockLost(err) {
79
+ console.error(err)
80
+ }
81
+ })
82
+ await mutex.acquire()
83
+ try {
84
+ while (mutex.isAcquired) {
85
+ // critical cycle iteration
86
+ }
87
+ } finally {
88
+ // It's safe to always call release, because if lock is no longer belongs to this mutex, .release() will have no effect
89
+ await mutex.release()
90
+ }
91
+ }
92
+ ```
93
+
94
+ #### Example with optional lock
95
+
96
+ ```javascript
97
+ async function doSomething() {
98
+ const mutex = new Mutex(redisClient, 'lockingResource', {
99
+ acquireAttemptsLimit: 1
100
+ })
101
+ const lockAcquired = await mutex.tryAcquire()
102
+ if (!lockAcquired) {
103
+ return
104
+ }
105
+ try {
106
+ while (mutex.isAcquired) {
107
+ // critical cycle iteration
108
+ }
109
+ } finally {
110
+ // It's safe to always call release, because if lock is no longer belongs to this mutex, .release() will have no effect
111
+ await mutex.release()
112
+ }
113
+ }
114
+ ```
115
+
116
+ #### Example with temporary refresh
117
+
118
+ ```javascript
119
+ async function doSomething() {
120
+ const mutex = new Mutex(redisClient, 'lockingResource', {
121
+ lockTimeout: 120000,
122
+ refreshInterval: 15000
123
+ })
124
+ const lockAcquired = await mutex.tryAcquire()
125
+ if (!lockAcquired) {
126
+ return
127
+ }
128
+ try {
129
+ // critical cycle iteration
130
+ } finally {
131
+ // We want to let lock expire over time after operation is finished
132
+ await mutex.stopRefresh()
133
+ }
134
+ }
135
+ ```
136
+
137
+ #### Example with dynamically adjusting existing lock
138
+
139
+ ```javascript
140
+ const Mutex = require('redis-semaphore').Mutex
141
+ const Redis = require('ioredis')
142
+
143
+ // TypeScript
144
+ // import { Mutex } from 'redis-semaphore'
145
+ // import Redis from 'ioredis'
146
+
147
+ const redisClient = new Redis()
148
+
149
+ // This creates an original lock
150
+ const preMutex = new Mutex(redisClient, 'lockingResource', {
151
+ lockTimeout: 10 * 1e3, // lock for 10s
152
+ refreshInterval: 0
153
+ });
154
+
155
+ // This modifies lock with a new TTL and starts refresh
156
+ const mutex = new Mutex(redisClient, 'lockingResource', {
157
+ identifier: preMutex.identifier,
158
+ acquiredExternally: true, // required in this case
159
+ lockTimeout: 30 * 60 * 1e3, // lock for 30min
160
+ refreshInterval: 60 * 1e3
161
+ });
162
+
163
+ ```
164
+
165
+ #### Example with shared lock between scheduler and handler apps
166
+
167
+ ```javascript
168
+ const Mutex = require('redis-semaphore').Mutex
169
+ const Redis = require('ioredis')
170
+
171
+ // TypeScript
172
+ // import { Mutex } from 'redis-semaphore'
173
+ // import Redis from 'ioredis'
174
+
175
+ const redisClient = new Redis()
176
+
177
+ // scheduler app code
178
+ async function every10MinutesCronScheduler() {
179
+ const mutex = new Mutex(redisClient, 'lockingResource', {
180
+ lockTimeout: 30 * 60 * 1e3, // lock for 30min
181
+ refreshInterval: 0
182
+ })
183
+ if (await mutex.tryAcquire()) {
184
+ someQueue.publish({ mutexIdentifier: mutex.identifier })
185
+ } else {
186
+ logger.info('Job already scheduled. Do nothing in current cron cycle')
187
+ }
188
+ }
189
+
190
+ // handler app code
191
+ async function queueHandler(queueMessageData) {
192
+ const { mutexIdentifier } = queueMessageData
193
+ const mutex = new Mutex(redisClient, 'lockingResource', {
194
+ lockTimeout: 10 * 1e3, // 10sec
195
+ identifier: mutexIdentifier,
196
+ acquiredExternally: true // required in this case
197
+ })
198
+
199
+ // actually will do `refresh` with new lockTimeout instead of acquire
200
+ // if mutex was locked by another process or lock was expired - exception will be thrown (default refresh behavior)
201
+ await mutex.acquire()
202
+
203
+ try {
204
+ // critical code
205
+ } finally {
206
+ await mutex.release()
207
+ }
208
+ }
209
+ ```
210
+
211
+ ### Semaphore
212
+
213
+ > See [RedisLabs: Basic counting sempahore](https://redislabs.com/ebook/part-2-core-concepts/chapter-6-application-components-in-redis/6-3-counting-semaphores/6-3-1-building-a-basic-counting-semaphore/)
214
+
215
+ This implementation is slightly different from the algorithm described in the book, but the main idea has not changed.
216
+
217
+ `zrank` check replaced with `zcard`, so now it is fair as [RedisLabs: Fair semaphore](https://redislabs.com/ebook/part-2-core-concepts/chapter-6-application-components-in-redis/6-3-counting-semaphores/6-3-2-fair-semaphores/) (see tests).
218
+
219
+ In edge cases (node time difference is greater than `lockTimeout`) both algorithms are not fair due cleanup stage (removing expired members from sorted set), so `FairSemaphore` API has been removed (it's safe to replace it with `Semaphore`).
220
+
221
+ Most reliable way to use: `lockTimeout` is greater than possible node clock differences, `refreshInterval` is not 0 and is less enough than `lockTimeout` (by default is `lockTimeout * 0.8`)
222
+
223
+ ##### new Semaphore(redisClient, key, maxCount [, { lockTimeout = 10000, acquireTimeout = 10000, acquireAttemptsLimit = Number.POSITIVE_INFINITY, retryInterval = 10, refreshInterval = lockTimeout * 0.8 }])
224
+
225
+ - `redisClient` - **required**, configured `redis` client
226
+ - `key` - **required**, key for locking resource (final key in redis: `semaphore:<key>`)
227
+ - `maxCount` - **required**, maximum simultaneously resource usage count
228
+ - `options` _optional_ See `Mutex` options
229
+
230
+ #### Example
231
+
232
+ ```javascript
233
+ const Semaphore = require('redis-semaphore').Semaphore
234
+ const Redis = require('ioredis')
235
+
236
+ // TypeScript
237
+ // import { Semaphore } from 'redis-semaphore'
238
+ // import Redis from 'ioredis'
239
+
240
+ const redisClient = new Redis()
241
+
242
+ async function doSomething() {
243
+ const semaphore = new Semaphore(redisClient, 'lockingResource', 5)
244
+ await semaphore.acquire()
245
+ try {
246
+ // maximum 5 simultaneously executions
247
+ } finally {
248
+ await semaphore.release()
249
+ }
250
+ }
251
+ ```
252
+
253
+ ### MultiSemaphore
254
+
255
+ Same as `Semaphore` with one difference - MultiSemaphore will try to acquire multiple permits instead of one.
256
+
257
+ `MultiSemaphore` and `Semaphore` shares same key namespace and can be used together (see test/src/RedisMultiSemaphore.test.ts).
258
+
259
+ ##### new MultiSemaphore(redisClient, key, maxCount, permits [, { lockTimeout = 10000, acquireTimeout = 10000, acquireAttemptsLimit = Number.POSITIVE_INFINITY, retryInterval = 10, refreshInterval = lockTimeout * 0.8 }])
260
+
261
+ - `redisClient` - **required**, configured `redis` client
262
+ - `key` - **required**, key for locking resource (final key in redis: `semaphore:<key>`)
263
+ - `maxCount` - **required**, maximum simultaneously resource usage count
264
+ - `permits` - **required**, number of acquiring permits
265
+ - `options` _optional_ See `Mutex` options
266
+
267
+ #### Example
268
+
269
+ ```javascript
270
+ const MultiSemaphore = require('redis-semaphore').MultiSemaphore
271
+ const Redis = require('ioredis')
272
+
273
+ // TypeScript
274
+ // import { MultiSemaphore } from 'redis-semaphore'
275
+ // import Redis from 'ioredis'
276
+
277
+ const redisClient = new Redis()
278
+
279
+ async function doSomething() {
280
+ const semaphore = new MultiSemaphore(redisClient, 'lockingResource', 5, 2)
281
+
282
+ await semaphore.acquire()
283
+ try {
284
+ // make 2 parallel calls to remote service which allow only 5 simultaneously calls
285
+ } finally {
286
+ await semaphore.release()
287
+ }
288
+ }
289
+ ```
290
+
291
+ ### RedlockMutex
292
+
293
+ Distributed `Mutex` version
294
+
295
+ > See [The Redlock algorithm](https://redis.io/topics/distlock#the-redlock-algorithm)
296
+
297
+ ##### new RedlockMutex(redisClients, key [, { lockTimeout = 10000, acquireTimeout = 10000, acquireAttemptsLimit = Number.POSITIVE_INFINITY, retryInterval = 10, refreshInterval = lockTimeout * 0.8 }])
298
+
299
+ - `redisClients` - **required**, array of configured `redis` client connected to independent nodes
300
+ - `key` - **required**, key for locking resource (final key in redis: `mutex:<key>`)
301
+ - `options` _optional_ See `Mutex` options
302
+
303
+ #### Example
304
+
305
+ ```javascript
306
+ const RedlockMutex = require('redis-semaphore').RedlockMutex
307
+ const Redis = require('ioredis')
308
+
309
+ // TypeScript
310
+ // import { RedlockMutex } from 'redis-semaphore'
311
+ // import Redis from 'ioredis'
312
+
313
+ const redisClients = [
314
+ new Redis('127.0.0.1:6377'),
315
+ new Redis('127.0.0.1:6378'),
316
+ new Redis('127.0.0.1:6379')
317
+ ] // "Those nodes are totally independent, so we don’t use replication or any other implicit coordination system."
318
+
319
+ async function doSomething() {
320
+ const mutex = new RedlockMutex(redisClients, 'lockingResource')
321
+ await mutex.acquire()
322
+ try {
323
+ // critical code
324
+ } finally {
325
+ await mutex.release()
326
+ }
327
+ }
328
+ ```
329
+
330
+ ### RedlockSemaphore
331
+
332
+ Distributed `Semaphore` version
333
+
334
+ > See [The Redlock algorithm](https://redis.io/topics/distlock#the-redlock-algorithm)
335
+
336
+ ##### new RedlockSemaphore(redisClients, key, maxCount [, { lockTimeout = 10000, acquireTimeout = 10000, acquireAttemptsLimit = Number.POSITIVE_INFINITY, retryInterval = 10, refreshInterval = lockTimeout * 0.8 }])
337
+
338
+ - `redisClients` - **required**, array of configured `redis` client connected to independent nodes
339
+ - `key` - **required**, key for locking resource (final key in redis: `semaphore:<key>`)
340
+ - `maxCount` - **required**, maximum simultaneously resource usage count
341
+ - `options` _optional_ See `Mutex` options
342
+
343
+ #### Example
344
+
345
+ ```javascript
346
+ const RedlockSemaphore = require('redis-semaphore').RedlockSemaphore
347
+ const Redis = require('ioredis')
348
+
349
+ // TypeScript
350
+ // import { RedlockSemaphore } from 'redis-semaphore'
351
+ // import Redis from 'ioredis'
352
+
353
+ const redisClients = [
354
+ new Redis('127.0.0.1:6377'),
355
+ new Redis('127.0.0.1:6378'),
356
+ new Redis('127.0.0.1:6379')
357
+ ] // "Those nodes are totally independent, so we don’t use replication or any other implicit coordination system."
358
+
359
+ async function doSomething() {
360
+ const semaphore = new Semaphore(redisClients, 'lockingResource', 5)
361
+ await semaphore.acquire()
362
+ try {
363
+ // maximum 5 simultaneously executions
364
+ } finally {
365
+ await semaphore.release()
366
+ }
367
+ }
368
+ ```
369
+
370
+ ### RedlockMultiSemaphore
371
+
372
+ Distributed `MultiSemaphore` version
373
+
374
+ > See [The Redlock algorithm](https://redis.io/topics/distlock#the-redlock-algorithm)
375
+
376
+ ##### new RedlockMultiSemaphore(redisClients, key, maxCount, permits [, { lockTimeout = 10000, acquireTimeout = 10000, acquireAttemptsLimit = Number.POSITIVE_INFINITY, retryInterval = 10, refreshInterval = lockTimeout * 0.8 }])
377
+
378
+ - `redisClients` - **required**, array of configured `redis` client connected to independent nodes
379
+ - `key` - **required**, key for locking resource (final key in redis: `semaphore:<key>`)
380
+ - `maxCount` - **required**, maximum simultaneously resource usage count
381
+ - `permits` - **required**, number of acquiring permits
382
+ - `options` _optional_ See `Mutex` options
383
+
384
+ #### Example
385
+
386
+ ```javascript
387
+ const RedlockMultiSemaphore = require('redis-semaphore').RedlockMultiSemaphore
388
+ const Redis = require('ioredis')
389
+
390
+ // TypeScript
391
+ // import { RedlockMultiSemaphore } from 'redis-semaphore'
392
+ // import Redis from 'ioredis'
393
+
394
+ const redisClients = [
395
+ new Redis('127.0.0.1:6377'),
396
+ new Redis('127.0.0.1:6378'),
397
+ new Redis('127.0.0.1:6379')
398
+ ] // "Those nodes are totally independent, so we don’t use replication or any other implicit coordination system."
399
+
400
+ async function doSomething() {
401
+ const semaphore = new RedlockMultiSemaphore(
402
+ redisClients,
403
+ 'lockingResource',
404
+ 5,
405
+ 2
406
+ )
407
+
408
+ await semaphore.acquire()
409
+ try {
410
+ // make 2 parallel calls to remote service which allow only 5 simultaneously calls
411
+ } finally {
412
+ await semaphore.release()
413
+ }
414
+ }
415
+ ```
416
+
417
+ ## Development
418
+
419
+ ```shell
420
+ yarn --immutable
421
+ ./setup-redis-servers.sh
422
+ yarn dev
423
+ ```
424
+
425
+ ## License
426
+
427
+ MIT
428
+
429
+ [![FOSSA Status][fossa-large-image]][fossa-large-url]
430
+
431
+ [npm-image]: https://img.shields.io/npm/v/redis-semaphore.svg?style=flat-square
432
+ [npm-url]: https://npmjs.org/package/redis-semaphore
433
+ [ci-image]: https://github.com/swarthy/redis-semaphore/actions/workflows/branches.yml/badge.svg
434
+ [ci-url]: https://github.com/swarthy/redis-semaphore/actions/workflows/branches.yml
435
+ [codeclimate-image]: https://api.codeclimate.com/v1/badges/02778c96bb5983eb150c/maintainability
436
+ [codeclimate-url]: https://codeclimate.com/github/swarthy/redis-semaphore/maintainability
437
+ [snyk-image]: https://snyk.io/test/npm/redis-semaphore/badge.svg
438
+ [snyk-url]: https://snyk.io/test/npm/redis-semaphore
439
+ [coverage-image]: https://coveralls.io/repos/github/swarthy/redis-semaphore/badge.svg?branch=master
440
+ [coverage-url]: https://coveralls.io/r/swarthy/redis-semaphore?branch=master
441
+ [fossa-badge-image]: https://app.fossa.com/api/projects/custom%2B10538%2Fgit%40github.com%3Aswarthy%2Fredis-semaphore.git.svg?type=shield
442
+ [fossa-badge-url]: https://app.fossa.com/projects/custom%2B10538%2Fgit%40github.com%3Aswarthy%2Fredis-semaphore.git?ref=badge_shield
443
+ [fossa-large-image]: https://app.fossa.com/api/projects/custom%2B10538%2Fgit%40github.com%3Aswarthy%2Fredis-semaphore.git.svg?type=large
444
+ [fossa-large-url]: https://app.fossa.com/projects/custom%2B10538%2Fgit%40github.com%3Aswarthy%2Fredis-semaphore.git?ref=badge_large
445
+ [typescript-image]: https://badgen.net/npm/types/tslib
@@ -0,0 +1,31 @@
1
+ version: '3.7'
2
+
3
+ services:
4
+ waiter:
5
+ image: node:alpine
6
+ volumes:
7
+ - ./:/app
8
+ working_dir: /app
9
+ command: >
10
+ sh -c "
11
+ corepack enable &&
12
+ yarn wait-for --redis redis://redis1 &&
13
+ yarn wait-for --redis redis://redis2 &&
14
+ yarn wait-for --redis redis://redis3 &&
15
+ echo 'All redis instances ready!'
16
+ "
17
+
18
+ redis1:
19
+ image: redis:alpine
20
+ ports:
21
+ - 6001:6379
22
+
23
+ redis2:
24
+ image: redis:alpine
25
+ ports:
26
+ - 6002:6379
27
+
28
+ redis3:
29
+ image: redis:alpine
30
+ ports:
31
+ - 6003:6379
@@ -0,0 +1,73 @@
1
+ import typescript from '@typescript-eslint/eslint-plugin'
2
+ import typescriptParser from '@typescript-eslint/parser'
3
+ import nodePlugin from 'eslint-plugin-node'
4
+
5
+ export default [
6
+ {
7
+ ignores: ['lib/**', 'es/**', 'coverage/**', '.nyc_output/**']
8
+ },
9
+ {
10
+ files: ['**/*.ts'],
11
+ languageOptions: {
12
+ parser: typescriptParser,
13
+ parserOptions: {
14
+ ecmaVersion: 2024,
15
+ sourceType: 'module',
16
+ project: './tsconfig.json',
17
+ ecmaFeatures: {
18
+ jsx: false
19
+ }
20
+ },
21
+ globals: {
22
+ console: true,
23
+ process: true,
24
+ setTimeout: true,
25
+ clearTimeout: true,
26
+ setInterval: true,
27
+ clearInterval: true
28
+ }
29
+ },
30
+ plugins: {
31
+ '@typescript-eslint': typescript
32
+ },
33
+ rules: {
34
+ ...typescript.configs['recommended'].rules,
35
+ ...typescript.configs['recommended-requiring-type-checking'].rules,
36
+ '@typescript-eslint/explicit-function-return-type': 'error',
37
+ '@typescript-eslint/no-explicit-any': 'error',
38
+ '@typescript-eslint/no-unused-vars': [
39
+ 'error',
40
+ { argsIgnorePattern: '^_' }
41
+ ],
42
+ '@typescript-eslint/no-unused-expressions': 'off',
43
+ '@typescript-eslint/no-misused-promises': 'off',
44
+ '@typescript-eslint/require-await': 'off',
45
+ 'no-unused-expressions': 'off'
46
+ }
47
+ },
48
+ {
49
+ files: ['test/**/*.ts'],
50
+ languageOptions: {
51
+ globals: {
52
+ describe: true,
53
+ it: true,
54
+ before: true,
55
+ after: true,
56
+ beforeEach: true,
57
+ afterEach: true,
58
+ mocha: true
59
+ }
60
+ },
61
+ rules: {
62
+ '@typescript-eslint/no-explicit-any': 'off',
63
+ '@typescript-eslint/no-unsafe-assignment': 'off',
64
+ '@typescript-eslint/no-unsafe-member-access': 'off',
65
+ '@typescript-eslint/no-unsafe-call': 'off',
66
+ '@typescript-eslint/no-unsafe-return': 'off',
67
+ '@typescript-eslint/no-unsafe-argument': 'off',
68
+ '@typescript-eslint/await-thenable': 'off',
69
+ '@typescript-eslint/explicit-function-return-type': 'off',
70
+ 'no-unused-vars': 'off'
71
+ }
72
+ }
73
+ ]
@@ -0,0 +1,79 @@
1
+ {
2
+ "name": "redis-semaphore",
3
+ "version": "5.6.2",
4
+ "description": "Distributed mutex and semaphore based on Redis",
5
+ "main": "lib/index.js",
6
+ "scripts": {
7
+ "lint": "eslint --ext .js,.ts .",
8
+ "test": "mocha",
9
+ "test-ci-with-coverage": "nyc mocha && nyc report --reporter=text-lcov | coveralls",
10
+ "coverage-html": "nyc mocha && nyc report --reporter=html",
11
+ "converalls": "nyc mocha && nyc report --reporter=text-lcov | coveralls",
12
+ "dev": "mocha -w",
13
+ "build": "yarn build-commonjs",
14
+ "build-commonjs": "rm -rf lib && yarn tsc -b tsconfig.build-commonjs.json",
15
+ "build-es": "rm -rf es && yarn tsc -b tsconfig.build-es.json",
16
+ "preversion": "yarn lint && yarn test && yarn build"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git@github.com:swarthy/redis-semaphore.git"
21
+ },
22
+ "keywords": [
23
+ "redis",
24
+ "redlock",
25
+ "mutex",
26
+ "semaphore"
27
+ ],
28
+ "author": "Alexander Mochalin (horroshow@mail.ru)",
29
+ "license": "MIT",
30
+ "devDependencies": {
31
+ "@swarthy/wait-for": "^2.1.1",
32
+ "@swc-node/register": "1.10.10",
33
+ "@swc/core": "1.11.11",
34
+ "@types/chai": "^4.3.20",
35
+ "@types/chai-as-promised": "^7.1.8",
36
+ "@types/debug": "^4.1.12",
37
+ "@types/ioredis-mock": "^8.2.5",
38
+ "@types/mocha": "^10.0.10",
39
+ "@types/node": "22.13.11",
40
+ "@types/sinon": "^17.0.4",
41
+ "@types/sinon-chai": "^3.2.12",
42
+ "@typescript-eslint/eslint-plugin": "8.27.0",
43
+ "@typescript-eslint/parser": "8.27.0",
44
+ "benchmark": "^2.1.4",
45
+ "chai": "4.5.0",
46
+ "chai-as-promised": "7.1.2",
47
+ "coveralls": "^3.1.1",
48
+ "eslint": "9.23.0",
49
+ "eslint-plugin-node": "11.1.0",
50
+ "ioredis": "5.6.0",
51
+ "ioredis-mock": "8.9.0",
52
+ "mocha": "11.1.0",
53
+ "mocha-lcov-reporter": "^1.3.0",
54
+ "nyc": "^17.1.0",
55
+ "sinon": "19.0.4",
56
+ "sinon-chai": "3.7.0",
57
+ "snyk": "1.1296.0",
58
+ "ts-node": "^10.9.2",
59
+ "typescript": "^5.8.2"
60
+ },
61
+ "engines": {
62
+ "node": ">= 14.17.0"
63
+ },
64
+ "peerDependencies": {
65
+ "ioredis": "^4.1.0 || ^5"
66
+ },
67
+ "peerDependenciesMeta": {
68
+ "ioredis": {
69
+ "optional": true
70
+ }
71
+ },
72
+ "dependencies": {
73
+ "debug": "^4.4.0"
74
+ },
75
+ "packageManager": "yarn@4.1.0+sha256.81a00df816059803e6b5148acf03ce313cad36b7f6e5af6efa040a15981a6ffb",
76
+ "files": [
77
+ "lib/"
78
+ ]
79
+ }
@@ -0,0 +1,2 @@
1
+ #!/bin/sh
2
+ docker compose up -d redis1 redis2 redis3