@nivinjoseph/n-data
Version:
Data access library for Postgres based on Knex
224 lines (174 loc) • 6.81 kB
text/typescript
import { given } from "@nivinjoseph/n-defensive";
import { Delay, Disposable, DisposableWrapper, Duration } from "@nivinjoseph/n-util";
import { test, after, before, describe } from "node:test";
import { createClient } from "redis";
import { DistributedLockService, RedisDistributedLockService } from "../src/index.js";
import assert from "node:assert";
import { UnableToAcquireDistributedLockException } from "../src/distributed-lock/redis-distributed-lock-service.js";
class Synchronized
{
private readonly _lockService: DistributedLockService;
private readonly _values = new Array<number>();
private readonly _events = new Array<string>();
public get values(): ReadonlyArray<number> { return this._values; }
public get events(): ReadonlyArray<string> { return this._events; }
public constructor(lockService: DistributedLockService)
{
given(lockService, "lockService").ensureHasValue().ensureIsObject();
this._lockService = lockService;
}
public async execute(ms: number): Promise<void>
{
const lock = await this._lockService.lock("testing");
try
{
// if (ms === 3000)
// throw new Error("boom");
this._events.push(`Started ${ms}`);
console.log(ms);
await Delay.milliseconds(ms);
this._values.push(ms);
this._events.push(`Finished ${ms}`);
}
finally
{
await lock.release();
}
}
}
await describe("DistributedLock tests", async () =>
{
let service: RedisDistributedLockService;
let connectionDisposable: Disposable;
before(async () =>
{
const redisClient = await createClient<any, any, any, any, any>().connect();
service = new RedisDistributedLockService(redisClient);
connectionDisposable = new DisposableWrapper(async () =>
{
await service.dispose();
await redisClient.close();
});
});
after(async () =>
{
await service.dispose();
await connectionDisposable.dispose();
});
await test("Basics", async () =>
{
const synchronized = new Synchronized(service);
const promises = new Array<Promise<void>>();
for (let i = 3; i > 0; i--)
{
console.log("exec", i);
promises.push(synchronized.execute(i * 1000));
await Delay.milliseconds(200);
}
await Promise.all(promises);
assert.strictEqual(synchronized.values[0], 3000);
assert.strictEqual(synchronized.values[1], 2000);
assert.strictEqual(synchronized.values[2], 1000);
});
await test("Long ttl", async () =>
{
const synchronized = new Synchronized(service);
const promises = new Array<Promise<void>>();
const opDuration = Duration.fromSeconds(1).toMilliSeconds();
for (let i = 3; i > 0; i--)
{
// console.log("exec", 0);
await Delay.seconds(1);
promises.push(synchronized.execute(opDuration + i));
}
await Promise.all(promises);
assert.strictEqual(synchronized.values[0], 1003);
assert.strictEqual(synchronized.values[1], 1002);
assert.strictEqual(synchronized.values[2], 1001);
});
await test(`
Given a lock
When acquired by 3 simultaneous operations that record the events 'Started 'opsNumber'' and 'Finished 'opsNumber''
Then the events should be correct and in order
`, async () =>
{
const synchronized = new Synchronized(service);
const promises = new Array<Promise<void>>();
const opDuration = Duration.fromSeconds(1).toMilliSeconds();
for (let i = 3; i > 0; i--)
{
// console.log("exec", 0);
await Delay.seconds(1);
promises.push(synchronized.execute(opDuration + i));
}
await Promise.all(promises);
assert.strictEqual(synchronized.events.length, 6);
for (let i = 0; i < synchronized.events.length; i = i + 2)
{
const firstEvent = synchronized.events[i];
const secondEvent = synchronized.events[i + 1];
assert.ok(firstEvent.startsWith("Started"), "should be the Started event");
assert.ok(secondEvent.startsWith("Finished"), "should be the Finished event");
const firstEventValue = firstEvent.split(" ").takeLast();
const secondEventValue = secondEvent.split(" ").takeLast();
assert.ok(firstEventValue === secondEventValue, "value for Started and Finished event should be the same");
}
});
await test(`
Given a lock that is already released
When attempting to release the lock again
Then no error should be thrown
`, async () =>
{
const lock = await service.lock("test-lock");
await lock.release();
assert.doesNotThrow(() => lock.release());
});
await test(`
Given a lock that is already expired
When attempting to release the lock
Then no error should be thrown
`, async () =>
{
const lock = await service.lock("test-lock", Duration.fromMilliSeconds(500));
await Delay.milliseconds(600);
assert.doesNotThrow(() => lock.release());
});
await test(`
Given a lock that is already expired
When attempting to acquire the lock again
Then the lock should be acquired immediately
`, async () =>
{
await service.lock("test-lock", Duration.fromMilliSeconds(200));
await Delay.milliseconds(300);
const acquireAttempt = Date.now();
const lock2 = await service.lock("test-lock", Duration.fromMilliSeconds(100));
const acquiredTime = Date.now();
const acquiringDuration = acquiredTime - acquireAttempt;
assert.ok(acquiringDuration < 5, "acquiring time should be less than 5ms");
await lock2.release();
});
await test(`
Given a lock that was acquired with a very long ttl
When attempting to acquire the lock again with default delay and retries
Then UnableToAcquireDistributedLockException should be thrown
`, async () =>
{
const lock1 = await service.lock("test-lock", Duration.fromSeconds(200));
let errorThrow = false;
try
{
await service.lock("test-lock", Duration.fromMilliSeconds(100));
}
catch (e)
{
if (e instanceof UnableToAcquireDistributedLockException)
errorThrow = true;
else
throw e;
}
assert.ok(errorThrow);
await lock1.release();
});
});