Skip to content

Commit 32f1cec

Browse files
committed
feat: added acquireAttemptsLimit
1 parent ce8faa0 commit 32f1cec

File tree

14 files changed

+184
-48
lines changed

14 files changed

+184
-48
lines changed

README.md

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,14 @@ yarn add redis-semaphore ioredis
2828

2929
> 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/)
3030
31-
##### new Mutex(redisClient, key [, { lockTimeout = 10000, acquireTimeout = 10000, retryInterval = 10, refreshInterval = lockTimeout * 0.8 }])
31+
##### new Mutex(redisClient, key [, { lockTimeout = 10000, acquireTimeout = 10000, acquireAttemptsLimit = Number.POSITIVE_INFINITY, retryInterval = 10, refreshInterval = lockTimeout * 0.8 }])
3232

3333
- `redisClient` - **required**, configured `redis` client
3434
- `key` - **required**, key for locking resource (final key in redis: `mutex:<key>`)
3535
- `options` - _optional_
3636
- `lockTimeout` - _optional_ ms, time after mutex will be auto released (expired)
3737
- `acquireTimeout` - _optional_ ms, max timeout for `.acquire()` call
38+
- `acquireAttemptsLimit` - _optional_ max number of attempts to be made in `.acquire()` call
3839
- `retryInterval` - _optional_ ms, time between acquire attempts if resource locked
3940
- `refreshInterval` - _optional_ ms, auto-refresh interval; to disable auto-refresh behaviour set `0`
4041
- `onLockLost` - _optional_ function, called when lock loss is detected due refresh cycle; default onLockLost throws unhandled LostLockError
@@ -88,7 +89,9 @@ async function doSomething() {
8889

8990
```javascript
9091
async function doSomething() {
91-
const mutex = new Mutex(redisClient, 'lockingResource', { acquireTimeout: 0 })
92+
const mutex = new Mutex(redisClient, 'lockingResource', {
93+
acquireAttemptsLimit: 1
94+
})
9295
const lockAcquired = await mutex.tryAcquire()
9396
if (!lockAcquired) {
9497
return
@@ -116,7 +119,7 @@ In edge cases (node time difference is greater than `lockTimeout`) both algorith
116119

117120
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`)
118121

119-
##### new Semaphore(redisClient, key, maxCount [, { lockTimeout = 10000, acquireTimeout = 10000, retryInterval = 10, refreshInterval = lockTimeout * 0.8 }])
122+
##### new Semaphore(redisClient, key, maxCount [, { lockTimeout = 10000, acquireTimeout = 10000, acquireAttemptsLimit = Number.POSITIVE_INFINITY, retryInterval = 10, refreshInterval = lockTimeout * 0.8 }])
120123

121124
- `redisClient` - **required**, configured `redis` client
122125
- `key` - **required**, key for locking resource (final key in redis: `semaphore:<key>`)
@@ -152,7 +155,7 @@ Same as `Semaphore` with one difference - MultiSemaphore will try to acquire mul
152155

153156
`MultiSemaphore` and `Semaphore` shares same key namespace and can be used together (see test/src/RedisMultiSemaphore.test.ts).
154157

155-
##### new MultiSemaphore(redisClient, key, maxCount, permits [, { lockTimeout = 10000, acquireTimeout = 10000, retryInterval = 10, refreshInterval = lockTimeout * 0.8 }])
158+
##### new MultiSemaphore(redisClient, key, maxCount, permits [, { lockTimeout = 10000, acquireTimeout = 10000, acquireAttemptsLimit = Number.POSITIVE_INFINITY, retryInterval = 10, refreshInterval = lockTimeout * 0.8 }])
156159

157160
- `redisClient` - **required**, configured `redis` client
158161
- `key` - **required**, key for locking resource (final key in redis: `semaphore:<key>`)
@@ -190,7 +193,7 @@ Distributed `Mutex` version
190193

191194
> See [The Redlock algorithm](https://redis.io/topics/distlock#the-redlock-algorithm)
192195
193-
##### new RedlockMutex(redisClients, key [, { lockTimeout = 10000, acquireTimeout = 10000, retryInterval = 10, refreshInterval = lockTimeout * 0.8 }])
196+
##### new RedlockMutex(redisClients, key [, { lockTimeout = 10000, acquireTimeout = 10000, acquireAttemptsLimit = Number.POSITIVE_INFINITY, retryInterval = 10, refreshInterval = lockTimeout * 0.8 }])
194197

195198
- `redisClients` - **required**, array of configured `redis` client connected to independent nodes
196199
- `key` - **required**, key for locking resource (final key in redis: `mutex:<key>`)
@@ -229,7 +232,7 @@ Distributed `Semaphore` version
229232

230233
> See [The Redlock algorithm](https://redis.io/topics/distlock#the-redlock-algorithm)
231234
232-
##### new RedlockSemaphore(redisClients, key, maxCount [, { lockTimeout = 10000, acquireTimeout = 10000, retryInterval = 10, refreshInterval = lockTimeout * 0.8 }])
235+
##### new RedlockSemaphore(redisClients, key, maxCount [, { lockTimeout = 10000, acquireTimeout = 10000, acquireAttemptsLimit = Number.POSITIVE_INFINITY, retryInterval = 10, refreshInterval = lockTimeout * 0.8 }])
233236

234237
- `redisClients` - **required**, array of configured `redis` client connected to independent nodes
235238
- `key` - **required**, key for locking resource (final key in redis: `semaphore:<key>`)
@@ -269,7 +272,7 @@ Distributed `MultiSemaphore` version
269272

270273
> See [The Redlock algorithm](https://redis.io/topics/distlock#the-redlock-algorithm)
271274
272-
##### new RedlockMultiSemaphore(redisClients, key, maxCount, permits [, { lockTimeout = 10000, acquireTimeout = 10000, retryInterval = 10, refreshInterval = lockTimeout * 0.8 }])
275+
##### new RedlockMultiSemaphore(redisClients, key, maxCount, permits [, { lockTimeout = 10000, acquireTimeout = 10000, acquireAttemptsLimit = Number.POSITIVE_INFINITY, retryInterval = 10, refreshInterval = lockTimeout * 0.8 }])
273276

274277
- `redisClients` - **required**, array of configured `redis` client connected to independent nodes
275278
- `key` - **required**, key for locking resource (final key in redis: `semaphore:<key>`)

src/Lock.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,12 @@ import { v4 as uuid4 } from 'uuid'
44
import LostLockError from './errors/LostLockError'
55
import TimeoutError from './errors/TimeoutError'
66
import { defaultOnLockLost, defaultTimeoutOptions } from './misc'
7-
import { LockLostCallback, LockOptions } from './types'
7+
import { AcquireOptions, LockLostCallback, LockOptions } from './types'
88

99
const REFRESH_INTERVAL_COEF = 0.8
1010

1111
const debug = createDebug('redis-semaphore:instance')
1212

13-
interface AcquireOptions {
14-
identifier: string
15-
lockTimeout: number
16-
acquireTimeout: number
17-
retryInterval: number
18-
}
19-
2013
export abstract class Lock {
2114
protected abstract _kind: string
2215
protected abstract _key: string
@@ -35,6 +28,7 @@ export abstract class Lock {
3528
constructor({
3629
lockTimeout = defaultTimeoutOptions.lockTimeout,
3730
acquireTimeout = defaultTimeoutOptions.acquireTimeout,
31+
acquireAttemptsLimit = defaultTimeoutOptions.acquireAttemptsLimit,
3832
retryInterval = defaultTimeoutOptions.retryInterval,
3933
refreshInterval = Math.round(lockTimeout * REFRESH_INTERVAL_COEF),
4034
onLockLost = defaultOnLockLost
@@ -43,6 +37,7 @@ export abstract class Lock {
4337
this._acquireOptions = {
4438
lockTimeout,
4539
acquireTimeout,
40+
acquireAttemptsLimit,
4641
retryInterval,
4742
identifier: this._identifier
4843
}

src/misc.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import LostLockError from '../src/errors/LostLockError'
33
export const defaultTimeoutOptions = {
44
lockTimeout: 10000,
55
acquireTimeout: 10000,
6+
acquireAttemptsLimit: Number.POSITIVE_INFINITY,
67
retryInterval: 10
78
}
89

src/multiSemaphore/acquire/index.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export interface Options {
1010
identifier: string
1111
lockTimeout: number
1212
acquireTimeout: number
13+
acquireAttemptsLimit: number
1314
retryInterval: number
1415
}
1516

@@ -20,11 +21,18 @@ export async function acquireSemaphore(
2021
permits: number,
2122
options: Options
2223
) {
23-
const { identifier, lockTimeout, acquireTimeout, retryInterval } = options
24+
const {
25+
identifier,
26+
lockTimeout,
27+
acquireTimeout,
28+
acquireAttemptsLimit,
29+
retryInterval
30+
} = options
31+
let attempt = 0
2432
const end = Date.now() + acquireTimeout
2533
let now
26-
while ((now = Date.now()) < end) {
27-
debug(key, identifier, limit, lockTimeout)
34+
while ((now = Date.now()) < end && ++attempt <= acquireAttemptsLimit) {
35+
debug(key, identifier, limit, lockTimeout, 'attempt', attempt)
2836
const result = await acquireLua(client, [
2937
key,
3038
limit,
@@ -41,5 +49,6 @@ export async function acquireSemaphore(
4149
await delay(retryInterval)
4250
}
4351
}
52+
debug(key, identifier, limit, lockTimeout, 'timeout or reach limit')
4453
return false
4554
}

src/mutex/acquire.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export interface Options {
99
identifier: string
1010
lockTimeout: number
1111
acquireTimeout: number
12+
acquireAttemptsLimit: number
1213
retryInterval: number
1314
}
1415

@@ -17,10 +18,17 @@ export async function acquireMutex(
1718
key: string,
1819
options: Options
1920
) {
20-
const { identifier, lockTimeout, acquireTimeout, retryInterval } = options
21+
const {
22+
identifier,
23+
lockTimeout,
24+
acquireTimeout,
25+
acquireAttemptsLimit,
26+
retryInterval
27+
} = options
28+
let attempt = 0
2129
const end = Date.now() + acquireTimeout
22-
while (Date.now() < end) {
23-
debug(key, identifier, 'attempt')
30+
while (Date.now() < end && ++attempt <= acquireAttemptsLimit) {
31+
debug(key, identifier, 'attempt', attempt)
2432
const result = await client.set(key, identifier, 'PX', lockTimeout, 'NX')
2533
debug('result', typeof result, result)
2634
if (result === 'OK') {
@@ -30,6 +38,6 @@ export async function acquireMutex(
3038
await delay(retryInterval)
3139
}
3240
}
33-
debug(key, identifier, 'timeout')
41+
debug(key, identifier, 'timeout or reach limit')
3442
return false
3543
}

src/redlockMultiSemaphore/acquire.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export interface Options {
1111
identifier: string
1212
lockTimeout: number
1313
acquireTimeout: number
14+
acquireAttemptsLimit: number
1415
retryInterval: number
1516
}
1617

@@ -21,12 +22,19 @@ export async function acquireRedlockMultiSemaphore(
2122
permits: number,
2223
options: Options
2324
) {
24-
const { identifier, lockTimeout, acquireTimeout, retryInterval } = options
25+
const {
26+
identifier,
27+
lockTimeout,
28+
acquireTimeout,
29+
acquireAttemptsLimit,
30+
retryInterval
31+
} = options
32+
let attempt = 0
2533
const end = Date.now() + acquireTimeout
2634
const quorum = getQuorum(clients.length)
2735
let now: number
28-
while ((now = Date.now()) < end) {
29-
debug(key, identifier, 'attempt')
36+
while ((now = Date.now()) < end && ++attempt <= acquireAttemptsLimit) {
37+
debug(key, identifier, 'attempt', attempt)
3038
const promises = clients.map(client =>
3139
acquireLua(client, [key, limit, permits, identifier, lockTimeout, now])
3240
.then(result => +result)
@@ -44,6 +52,6 @@ export async function acquireRedlockMultiSemaphore(
4452
await delay(retryInterval)
4553
}
4654
}
47-
debug(key, identifier, 'timeout')
55+
debug(key, identifier, 'timeout or reach limit')
4856
return false
4957
}

src/redlockMutex/acquire.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export interface Options {
1111
identifier: string
1212
lockTimeout: number
1313
acquireTimeout: number
14+
acquireAttemptsLimit: number
1415
retryInterval: number
1516
}
1617

@@ -19,11 +20,18 @@ export async function acquireRedlockMutex(
1920
key: string,
2021
options: Options
2122
) {
22-
const { identifier, lockTimeout, acquireTimeout, retryInterval } = options
23+
const {
24+
identifier,
25+
lockTimeout,
26+
acquireTimeout,
27+
acquireAttemptsLimit,
28+
retryInterval
29+
} = options
30+
let attempt = 0
2331
const end = Date.now() + acquireTimeout
2432
const quorum = getQuorum(clients.length)
25-
while (Date.now() < end) {
26-
debug(key, identifier, 'attempt')
33+
while (Date.now() < end && ++attempt <= acquireAttemptsLimit) {
34+
debug(key, identifier, 'attempt', attempt)
2735
const promises = clients.map(client =>
2836
client
2937
.set(key, identifier, 'PX', lockTimeout, 'NX')
@@ -42,6 +50,6 @@ export async function acquireRedlockMutex(
4250
await delay(retryInterval)
4351
}
4452
}
45-
debug(key, identifier, 'timeout')
53+
debug(key, identifier, 'timeout or reach limit')
4654
return false
4755
}

src/redlockSemaphore/acquire.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export interface Options {
1111
identifier: string
1212
lockTimeout: number
1313
acquireTimeout: number
14+
acquireAttemptsLimit: number
1415
retryInterval: number
1516
}
1617

@@ -20,12 +21,19 @@ export async function acquireRedlockSemaphore(
2021
limit: number,
2122
options: Options
2223
) {
23-
const { identifier, lockTimeout, acquireTimeout, retryInterval } = options
24+
const {
25+
identifier,
26+
lockTimeout,
27+
acquireTimeout,
28+
acquireAttemptsLimit,
29+
retryInterval
30+
} = options
31+
let attempt = 0
2432
const end = Date.now() + acquireTimeout
2533
const quorum = getQuorum(clients.length)
2634
let now: number
27-
while ((now = Date.now()) < end) {
28-
debug(key, identifier, 'attempt')
35+
while ((now = Date.now()) < end && ++attempt <= acquireAttemptsLimit) {
36+
debug(key, identifier, 'attempt', attempt)
2937
const promises = clients.map(client =>
3038
acquireLua(client, [key, limit, identifier, lockTimeout, now])
3139
.then(result => +result)
@@ -43,6 +51,6 @@ export async function acquireRedlockSemaphore(
4351
await delay(retryInterval)
4452
}
4553
}
46-
debug(key, identifier, 'timeout')
54+
debug(key, identifier, 'timeout or reach limit')
4755
return false
4856
}

src/semaphore/acquire/index.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export interface Options {
1010
identifier: string
1111
lockTimeout: number
1212
acquireTimeout: number
13+
acquireAttemptsLimit: number
1314
retryInterval: number
1415
}
1516

@@ -19,11 +20,18 @@ export async function acquireSemaphore(
1920
limit: number,
2021
options: Options
2122
) {
22-
const { identifier, lockTimeout, acquireTimeout, retryInterval } = options
23+
const {
24+
identifier,
25+
lockTimeout,
26+
acquireTimeout,
27+
acquireAttemptsLimit,
28+
retryInterval
29+
} = options
30+
let attempt = 0
2331
const end = Date.now() + acquireTimeout
2432
let now
25-
while ((now = Date.now()) < end) {
26-
debug(key, identifier, limit, lockTimeout)
33+
while ((now = Date.now()) < end && ++attempt <= acquireAttemptsLimit) {
34+
debug(key, identifier, limit, lockTimeout, 'attempt', attempt)
2735
const result = await acquireLua(client, [
2836
key,
2937
limit,
@@ -40,5 +48,6 @@ export async function acquireSemaphore(
4048
await delay(retryInterval)
4149
}
4250
}
51+
debug(key, identifier, limit, lockTimeout, 'timeout or reach limit')
4352
return false
4453
}

src/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,19 @@ export interface LockLostCallback {
88
export interface TimeoutOptions {
99
lockTimeout?: number
1010
acquireTimeout?: number
11+
acquireAttemptsLimit?: number
1112
retryInterval?: number
1213
refreshInterval?: number
1314
}
1415

1516
export interface LockOptions extends TimeoutOptions {
1617
onLockLost?: LockLostCallback
1718
}
19+
20+
export interface AcquireOptions {
21+
identifier: string
22+
lockTimeout: number
23+
acquireTimeout: number
24+
acquireAttemptsLimit: number
25+
retryInterval: number
26+
}

0 commit comments

Comments
 (0)