Skip to content

Commit cc85455

Browse files
committed
update readme
1 parent 7ef1c9a commit cc85455

File tree

1 file changed

+125
-29
lines changed

1 file changed

+125
-29
lines changed

readme.md

Lines changed: 125 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -27,29 +27,34 @@
2727
</a>
2828
</p>
2929

30-
Implements mutex lock using redis as described in [redis docs](https://redis.io/commands/set#patterns). The term **simple** is opposed to the more complex **Redlock**, that was also proposed by Redis in their [docs](https://redis.io/topics/distlock) for use in case of distributed redis instances.
30+
Implements distributed mutex lock using redis as described in [redis docs](https://redis.io/commands/set#patterns). The term **simple** is opposed to the more complex **Redlock**, that was also proposed by Redis in their [docs](https://redis.io/topics/distlock) for use in case of distributed redis instances.
3131

32-
This implementation of redis lock introduces some fine tuning features to the lock such as lock expire time, and acquire retry time, and acquire timeout (all described below).
33-
34-
Also acquiring the lock in a **FIFO** manner is supported by just providing a boolean option `fifo: true`, this will make the lock behave like a mutex queue and allow only one instance to acquire the lock at a time, and once the lock is released the first one to wait for it is the first one to acquire it and so on.
32+
Locks have timeout (expire time) and fail after options. Also, Redis Pub/Sub is used so that released lock can be immediately acquired by another waiting process instead of depending on polling. Manual polling is still supported though in case lock expires.
3533

3634
## Install
3735

3836
Install the package using `npm`.
39-
4037
```bash
4138
npm i simple-redis-mutex
4239
```
4340

41+
Or with bun
42+
```bash
43+
bun add simple-redis-mutex
44+
```
45+
4446
## Examples
4547

4648
```js
47-
const { lock } = require('simple-redis-mutex');
48-
const Redis = require('ioredis');
49+
import { lock, tryLock } from 'simple-redis-mutex';
50+
import { createClient, RedisClientType } from 'redis';
4951

50-
// Connect to redis using ioredis
51-
redis = new Redis(process.env.REDIS_URI);
52+
// Connect to redis
53+
const redis = await createClient()
54+
.on('error', (err) => console.log('Redis Client Error', err))
55+
.connect();
5256

57+
// Using blocking lock
5358
async function someFunction() {
5459
// Acquire the lock, by passing redis client and the resource name (all settings are optional)
5560
const release = await lock(redis, 'resource-name');
@@ -60,40 +65,131 @@ async function someFunction() {
6065
// Release the lock
6166
await release();
6267
}
68+
69+
// Using tryLock
70+
async function someOtherFunction() {
71+
const [hasLock, release] = await tryLock(redis, 'resource-name');
72+
if (!hasLock) return; // Lock is already acquired
73+
74+
// Do some operations that require mutex lock
75+
await doSomeCriticalOperations();
76+
77+
// Release the lock
78+
await release();
79+
}
6380
```
6481

6582
## Usage
6683

67-
To acquire the lock you just call the `lock` function exported from the package, and pass to it [ioredis](https://github.com/luin/ioredis) client, and the resource name for the lock. You can also pass any optional options to fine-tune the lock as needed (see API below).
84+
There are 2 methods to acquire a lock:
85+
- `lock`: which attempts to acquire the lock in a blocking way, if lock is already acquired, it blocks until lock is available.
86+
- `tryLock`: which attempts to acquire the lock, if lock is already acquired it returns immediately.
6887

6988
## API
7089

71-
The package exports one named function `lock`, that acquires the lock and returns another function that releases the lock. The API for the `lock` function is as follows ...
72-
73-
```js
74-
lock(
75-
client,
76-
lockName,
77-
{ retryTimeMillis = 100, timeoutMillis, failAfterMillis, fifo }
78-
): Promise<Function>
90+
### `lock`
91+
As per the code:
92+
```typescript
93+
/**
94+
* Attempts to acquire lock, if lock is already acquired it will block until it can acquire the lock.
95+
* Returns lock release function.
96+
*
97+
* Lock timeout is used to expire the lock if it's not been released before `timeout`.
98+
* This is to prevent crashed processes holding the lock indefinitely.
99+
*
100+
* When a lock is released redis Pub/Sub is used to publish that the lock has been released
101+
* so that other processes waiting for the lock can attempt to acquire it.
102+
*
103+
* Manual polling is also implemented to attempt to acquire the lock in case the holder crashed and did not release the lock.
104+
* It is controlled by `pollingInterval`.
105+
*
106+
* Application logic should not depend on lock timeout and polling interval. They are meant to be a safe net when things fail.
107+
* Depending on them is inefficient and an anti-pattern, in such case application logic should be revised and refactored.
108+
*
109+
* If process fails to acquire the lock before `failAfter` milliseconds, it will throw an error and call `onFail` if provided.
110+
* If `failAfter` is not provided, process will block indefinitely waiting for the lock to be released.
111+
*
112+
* @param redis redis client
113+
* @param lockName lock name
114+
* @param options lock options
115+
* @param options.timeout lock timeout in milliseconds, default: 30 seconds
116+
* @param options.pollingInterval how long between manual polling for lock status milliseconds, default: 10 seconds
117+
* @param options.failAfter time to fail after if lock is still not acquired milliseconds
118+
* @param options.onFail called when failed to acquire lock before `failAfter`
119+
* @returns release function
120+
*/
121+
function lock(
122+
redis: RedisClient,
123+
lockName: string,
124+
{ timeout = DEFAULT_TIMEOUT, pollingInterval = DEFAULT_POLLING_INTERVAL, failAfter, onFail }: LockOptions = {},
125+
): Promise<ReleaseFunc>
79126
```
80127

81-
- **client** \<ioredis client>: [ioredis](https://www.npmjs.com/package/ioredis) client.
82-
- **lockName** \<String>: This is the name of the lock, and this is what distinguishes one lock from another, so that the part that needs mutual exclusion would always require a lock with the same name to be acquired by any process that attempts to enter that part. The key in redis database will be derived from this name.
83-
- **retryTimeMillis** \<Number>: (default `100`) This defines how much should a process wait before trying to acquire the same lock again, provided time is milliseconds, this time cannot be null.
84-
- **timeoutMillis** \<Number>: (default `null`) This defines the expiry time of the lock after it's acquired, so after that expiry time another process can acquire the lock even if the current holder did not release it, time provided is in milliseconds, `null` timeout value means that the lock will never expire.
85-
- **failAfterMillis** \<Number>: (default `null`) This defines the maximum time a process should wait for the lock until it can acquire it, when this time has passes and the process has not acquired the lock yet, the function will throw an Error saying that the lock could not be acquired in the given time, the provided time is in milliseconds, `null` value means that the function will not fail until it has acquired the lock.
86-
- **fifo** \<Boolean>: (default `false`) If this is set the waiting instances will be acquire the lock in a FIFO manner, i.e. the first one to wait will be the first one to acquire the lock once it's released and so on.
87-
- Return type \<`Promise<Function>`>: The `unlock` function, that is an async function, and should be called to release the lock.
128+
### `tryLock`
129+
As per the code:
130+
```typescript
131+
/**
132+
* Try to acquire the lock, if failed will return immediately.
133+
* Returns whether or not the lock was acquired, and a release function.
134+
*
135+
* If the lock was acquired, release function is idempotent,
136+
* calling it after the first time has no effect.
137+
*
138+
* If lock was not acquired, release function is a no-op.
139+
*
140+
* @param redis redis client
141+
* @param lockName lock name
142+
* @param options lock options
143+
* @param options.timeout lock timeout in milliseconds, default: 30 seconds
144+
* @returns whether or not the lock was acquired and release function.
145+
*/
146+
function tryLock(
147+
redis: RedisClient,
148+
lockName: string,
149+
{ timeout = DEFAULT_TIMEOUT }: TryLockOptions = {},
150+
): Promise<[boolean, ReleaseFunc]>
151+
```
88152

89153
## Notes
90154

91-
- This package has **Peer Dependency** on [ioredis](https://github.com/luin/ioredis).
92-
- It's taken into account the case that process A acquires the lock, then it expires, then process B acquires the lock. When process A try to release the lock, it will not be released, as it's now acquired by B.
93-
- The same lock can be acquired with different options each time, so one time it can have an expiry time, and the next acquire it can lock indefinitely, the same with all the other options, although this behavior is not encouraged as it can be hard to debug.
155+
### Redis Client
156+
This package has **Peer Dependency** on [redis](https://www.npmjs.com/package/redis), the is the redis client that must be passed to lock functions.
94157

95-
## Contribution
158+
Same client must always be provided within same process, this is because pub/sub depends on the provided client and its lifecycle.
159+
160+
### Lock options
161+
The same lock can be acquired with different options each time, and it can be acquired using `lock` and `tryLock` in different places or under different circumstances (actually `lock` internally uses `tryLock` to acquire the lock). You can mix and match as you see fit, but I recommend always using the same options in same places for more consistency and to make debugging easier.
162+
163+
`timeout` and `pollingInterval` have default value and user is not allowed to provide nullish values for those 2. This is for encouraging best practices. If you really want your lock to lock indefinitely for whatever reason, you can force-pass `null` for `timeout` and disable `pollingInterval` by also passing `null` (note that passing `undefined` will use the default values). Typescript will complain but you can just disable it for that line, something like so...
164+
```typescript
165+
// @ts-ignore
166+
await lock(redis, 'some-lock', { timeout: null, pollingInterval: null });
167+
```
168+
But I really advice against it. If lock-holding process crashes, there is no way to recover that lock other than removing the redis key manually from redis.
169+
170+
### Lock Release
171+
Once a lock is released a pub/sub channel is used to notify any process waiting for the lock. This makes waiting for lock more efficient and removes the need for frequent polling to check the status of the lock.
96172

173+
A dedicated subscriber is created and managed in the background to manage subscribing to the pub/sub channel. It is created as a duplicate of provided redis client, and it stops whenever the provided client stops.
174+
175+
Only one subscriber is created at a time. If the client stops and reconnects for whatever reason, then subscriber will stop with it and will reconnect on next lock use.
176+
177+
### Double Releasing
178+
Once `release` function has been called all following calls are no-op, so same function cannot release the lock again from a different holder.
179+
180+
It's also taken into consideration that an expired lock cannot be released so it does not release the lock from another holder. i.e. if process A acquires the lock, then it expires, then process B acquires the lock. When process A tries to release the lock, it will not be released, as it's now acquired by B.
181+
182+
### Migration from v1.x
183+
Breaking Changes in v2:
184+
- Redis client is now `redis` and not `ioredis`
185+
- options have been renamed:
186+
- `timeoutMillis` -> `timeout`
187+
- `retryTimeMillis` -> `pollingInterval` -- and it is now only used for expired locks, other wise pub/sub is used with released locks
188+
- `failAfterMillis` -> `failAfter`
189+
- FIFO option has been removed: existing implementation was wrong, it failed on lock-holder crash or failing to acquire the lock, and I could not come up with an implementation that would retain the functionality using redis only -- I sincerely apologize to anyone who have used it.
190+
- `timeout` and `pollingInterval` have defaults. Locks are now allowed to lock indefinitely (except with work around mentioned in "Lock Options" section above).
191+
192+
## Contribution
97193
You are welcome to [open a ticket](https://github.com/AmrSaber/simple-redis-mutex/issues) anytime, if you find a bug or have a feature request.
98194

99195
Also feel free to create a PR to **dev** branch for bug fixes or feature suggestions.

0 commit comments

Comments
 (0)