Skip to content

Commit 6530e91

Browse files
committed
Fix release on expired locks
1 parent b720d5f commit 6530e91

File tree

3 files changed

+98
-12
lines changed

3 files changed

+98
-12
lines changed

src/index.ts

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -313,12 +313,7 @@ export default class Redlock extends EventEmitter {
313313
settings
314314
);
315315

316-
// Add 2 milliseconds to the drift to account for Redis expires precision,
317-
// which is 1 ms, plus the configured allowable drift factor.
318-
const drift =
319-
Math.round(
320-
(settings?.driftFactor ?? this.settings.driftFactor) * duration
321-
) + 2;
316+
const drift = this.calculateDrift(duration, settings);
322317

323318
return new Lock(
324319
this,
@@ -351,6 +346,12 @@ export default class Redlock extends EventEmitter {
351346
lock: Lock,
352347
settings?: Partial<Settings>
353348
): Promise<ExecutionResult> {
349+
// Check if lock already expired and quit early
350+
if (lock.expiration < Date.now()) {
351+
const attempts: Promise<ExecutionStats>[] = [];
352+
return Promise.resolve({ attempts });
353+
}
354+
354355
// Immediately invalidate the lock.
355356
lock.expiration = 0;
356357

@@ -363,6 +364,22 @@ export default class Redlock extends EventEmitter {
363364
);
364365
}
365366

367+
/**
368+
* This method calculates drift for the provided `duration`.
369+
*/
370+
public calculateDrift(
371+
duration: number,
372+
settings?: Partial<Settings>
373+
): number {
374+
// Add 2 milliseconds to the drift to account for Redis expires precision,
375+
// which is 1 ms, plus the configured allowable drift factor.
376+
return (
377+
Math.round(
378+
(settings?.driftFactor ?? this.settings.driftFactor) * duration
379+
) + 2
380+
);
381+
}
382+
366383
/**
367384
* This method extends a valid lock by the provided `duration`.
368385
*/
@@ -392,12 +409,7 @@ export default class Redlock extends EventEmitter {
392409
// Invalidate the existing lock.
393410
existing.expiration = 0;
394411

395-
// Add 2 milliseconds to the drift to account for Redis expires precision,
396-
// which is 1 ms, plus the configured allowable drift factor.
397-
const drift =
398-
Math.round(
399-
(settings?.driftFactor ?? this.settings.driftFactor) * duration
400-
) + 2;
412+
const drift = this.calculateDrift(duration, settings);
401413

402414
const replacement = new Lock(
403415
this,

src/multi.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import test, { ExecutionContext } from "ava";
33
import Redis, { Redis as Client, Cluster } from "ioredis";
44
import Redlock, { ExecutionError, ResourceLockedError } from "./index.js";
55

6+
async function wait(ms: number): Promise<number> {
7+
return new Promise((resolve) => setTimeout(() => resolve(ms), ms));
8+
}
9+
610
async function fail(
711
t: ExecutionContext<unknown>,
812
error: unknown
@@ -174,6 +178,28 @@ function run(
174178
}
175179
});
176180

181+
test(`${namespace} - releasing expired lock doesn't fail`, async (t) => {
182+
try {
183+
const redlock = new Redlock([redisA, redisB, redisC]);
184+
185+
const duration = 1000;
186+
187+
// Acquire a lock.
188+
const lock = await redlock.acquire(["{redlock}a"], duration);
189+
190+
// Wait for duration + drift to be sure that lock has expired
191+
await wait(duration + redlock.calculateDrift(duration));
192+
193+
// Release the lock.
194+
await lock.release();
195+
t.is(await redisA.get("{redlock}a"), null, "The lock was not released");
196+
t.is(await redisB.get("{redlock}a"), null, "The lock was not released");
197+
t.is(await redisC.get("{redlock}a"), null, "The lock was not released");
198+
} catch (error) {
199+
fail(t, error);
200+
}
201+
});
202+
177203
test(`${namespace} - succeeds when a minority of clients fail`, async (t) => {
178204
try {
179205
const redlock = new Redlock([redisA, redisB, redisC]);

src/single.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import test, { ExecutionContext } from "ava";
33
import Redis, { Redis as Client, Cluster } from "ioredis";
44
import Redlock, { ExecutionError, ResourceLockedError } from "./index.js";
55

6+
async function wait(ms: number): Promise<number> {
7+
return new Promise((resolve) => setTimeout(() => resolve(ms), ms));
8+
}
9+
610
async function fail(
711
t: ExecutionContext<unknown>,
812
error: unknown
@@ -135,6 +139,26 @@ function run(namespace: string, redis: Client | Cluster): void {
135139
}
136140
});
137141

142+
test(`${namespace} - releasing expired lock doesn't fail`, async (t) => {
143+
try {
144+
const redlock = new Redlock([redis]);
145+
146+
const duration = 1000;
147+
148+
// Acquire a lock.
149+
const lock = await redlock.acquire(["{redlock}a"], duration);
150+
151+
// Wait for duration + drift to be sure that lock has expired
152+
await wait(duration + redlock.calculateDrift(duration));
153+
154+
// Release the lock.
155+
await lock.release();
156+
t.is(await redis.get("{redlock}a"), null, "The lock was not released");
157+
} catch (error) {
158+
fail(t, error);
159+
}
160+
});
161+
138162
test(`${namespace} - acquires, extends, and releases a multi-resource lock`, async (t) => {
139163
try {
140164
const redlock = new Redlock([redis]);
@@ -199,6 +223,30 @@ function run(namespace: string, redis: Client | Cluster): void {
199223
}
200224
});
201225

226+
test(`${namespace} - releasing expired multi-resource lock doesn't fail`, async (t) => {
227+
try {
228+
const redlock = new Redlock([redis]);
229+
230+
const duration = 1000;
231+
232+
// Acquire a lock.
233+
const lock = await redlock.acquire(
234+
["{redlock}a1", "{redlock}a2"],
235+
duration
236+
);
237+
238+
// Wait for duration + drift to be sure that lock has expired
239+
await wait(duration + redlock.calculateDrift(duration));
240+
241+
// Release the lock.
242+
await lock.release();
243+
t.is(await redis.get("{redlock}a1"), null, "The lock was not released");
244+
t.is(await redis.get("{redlock}a2"), null, "The lock was not released");
245+
} catch (error) {
246+
fail(t, error);
247+
}
248+
});
249+
202250
test(`${namespace} - locks fail when redis is unreachable`, async (t) => {
203251
try {
204252
const redis = new Redis({

0 commit comments

Comments
 (0)