Skip to content

Commit cb6c190

Browse files
committed
add refreshTimeout
1 parent 9ca7787 commit cb6c190

File tree

3 files changed

+56
-12
lines changed

3 files changed

+56
-12
lines changed

readme.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,12 +179,17 @@ A dedicated subscriber is created and managed in the background to manage subscr
179179

180180
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.
181181

182+
### Refresh lock timeout
183+
At any point when using the lock if you need more time before the lock expires you can call `await release.refreshTimeout()` to reset the lock's timeout. e.g. if lock timeout was 5 seconds, and after 4 seconds you realize that you need more time to finish the task and you call `refreshTimeout` then lock timeout is reset to 5 seconds again.
184+
185+
If a process holds a lock and it is released or expired then that process calling `refreshTimeout` has no effect. Same thing if lock was not acquired in the first place (with `tryLock`) then `refreshTimeout` will have no effect.
186+
182187
### Fencing Token
183188
A fencing token is an increasing number that is used to identify the order at which locks are acquired, and is used for further safety with writes in distributed systems. See "Making the lock safe with fencing" section from [this article](https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html) for more info about fencing tokens.
184189

185-
If the lock is successfully acquired then a fencing token is sure to be assigned, otherwise no fencing token will be issued if the lock is not acquired.
190+
If the lock is successfully acquired then a fencing token is issued, otherwise no fencing token will be issued or assigned if the lock is not acquired.
186191

187-
Fencing tokens can be access from `release` function like `release.fencingToken`, it is undefined only if lock was not acquired.
192+
Fencing tokens can be access from `release` function like `release.fencingToken`, it is -1 only if lock was not acquired.
188193

189194
Fencing tokens are global across all locks issued and not scoped with lock name. Application logic should only depend on the fencing token increasing and not care about the exact value of the token.
190195

src/index.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ type RedisClient = RedisClientType<any, any, any> | RedisClusterType<any, any, a
55
type ReleaseCallbackFn = () => void;
66
type ReleaseCallback = { lockKey: string; callback: ReleaseCallbackFn };
77

8-
export type ReleaseFunc = (() => Promise<void>) & { fencingToken?: number };
8+
export type ReleaseFunc = (() => Promise<void>) & { fencingToken: number; refreshTimeout: () => Promise<void> };
99
export type TryLockOptions = { timeout?: number };
1010
export type LockOptions = TryLockOptions & {
1111
pollingInterval?: number;
@@ -129,7 +129,13 @@ export async function tryLock(
129129
PX: timeout,
130130
});
131131

132-
if (result != REDIS_OK) return [false, async () => {}];
132+
if (result != REDIS_OK) {
133+
const dummyRelease: ReleaseFunc = () => Promise.resolve();
134+
dummyRelease.refreshTimeout = () => Promise.resolve();
135+
dummyRelease.fencingToken = -1;
136+
137+
return [false, dummyRelease];
138+
}
133139

134140
let released = false;
135141
const release: ReleaseFunc = async function () {
@@ -163,6 +169,14 @@ export async function tryLock(
163169
};
164170

165171
release.fencingToken = await redis.incr(REDIS_FENCING_TOKENS_COUNTER);
172+
release.refreshTimeout = async () => {
173+
if (released || (await redis.get(lockKey)) != lockValue) {
174+
released = true;
175+
return; // Check if lock is released
176+
}
177+
178+
await redis.pExpire(lockKey, timeout);
179+
};
166180

167181
return [true, release];
168182
}

unit.test.ts

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,19 +36,19 @@ describe('Lock tests', () => {
3636
[hasLock, release] = await tryLock(redis, lockName);
3737
expect(hasLock).toEqual(false);
3838

39-
expect(release.fencingToken).not.toBeDefined();
39+
expect(release.fencingToken).toEqual(-1);
4040
});
4141

4242
test('it issues monotonic fencing tokens', async () => {
4343
let lastToken: number | null = null;
4444

45-
for (let i = 0; i < 10; i++) {
45+
for (let i = 0; i < 25; i++) {
4646
let [hasLock, release] = await tryLock(redis, lockName);
4747
expect(hasLock).toEqual(true);
4848
await release();
4949

50-
if (lastToken != null) expect(lastToken).toBeLessThan(release.fencingToken!);
51-
lastToken = release.fencingToken!;
50+
if (lastToken != null) expect(lastToken).toBeLessThan(release.fencingToken);
51+
lastToken = release.fencingToken;
5252
}
5353
});
5454

@@ -88,15 +88,24 @@ describe('Lock tests', () => {
8888
});
8989

9090
test('lock expiration', async () => {
91-
let [hasLock] = await tryLock(redis, lockName, { timeout: 50 });
91+
const options: TryLockOptions = { timeout: 25 };
92+
93+
let [hasLock, release] = await tryLock(redis, lockName, options);
9294
expect(hasLock).toEqual(true);
9395

94-
[hasLock] = await tryLock(redis, lockName);
96+
[hasLock] = await tryLock(redis, lockName, options);
9597
expect(hasLock).toEqual(false);
9698

97-
await sleep(55);
99+
await sleep(30);
98100

99-
[hasLock] = await tryLock(redis, lockName);
101+
[hasLock] = await tryLock(redis, lockName, options);
102+
expect(hasLock).toEqual(true);
103+
104+
await sleep(10);
105+
await release.refreshTimeout(); // should has no effect
106+
await sleep(20);
107+
108+
[hasLock] = await tryLock(redis, lockName, options);
100109
expect(hasLock).toEqual(true);
101110
});
102111

@@ -126,6 +135,22 @@ describe('Lock tests', () => {
126135
expect(hasLock).toEqual(true);
127136
await release();
128137
});
138+
139+
test('refresh expire', async () => {
140+
const [, release] = await tryLock(redis, lockName, { timeout: 50 });
141+
142+
await sleep(35);
143+
await release.refreshTimeout();
144+
145+
let [hasLock] = await tryLock(redis, lockName);
146+
expect(hasLock).toEqual(false);
147+
148+
await sleep(35);
149+
await release.refreshTimeout();
150+
151+
[hasLock] = await tryLock(redis, lockName);
152+
expect(hasLock).toEqual(false);
153+
});
129154
});
130155

131156
describe('lock', () => {

0 commit comments

Comments
 (0)