feat: add support for key namespacePrefixes in a RedisLocker instance

This commit is contained in:
Thomas Dupont 2022-10-27 15:29:28 +02:00 committed by Joachim Van Herwegen
parent 09afebbc84
commit d690cc7ed0
2 changed files with 23 additions and 6 deletions

View File

@ -50,11 +50,19 @@ export class RedisLocker implements ReadWriteLocker, ResourceLocker, Initializab
private readonly redisRw: RedisReadWriteLock; private readonly redisRw: RedisReadWriteLock;
private readonly redisLock: RedisResourceLock; private readonly redisLock: RedisResourceLock;
private readonly attemptSettings: Required<AttemptSettings>; private readonly attemptSettings: Required<AttemptSettings>;
private readonly namespacePrefix: string;
private finalized = false; private finalized = false;
public constructor(redisClient = '127.0.0.1:6379', attemptSettings: AttemptSettings = {}) { /**
* Creates a new RedisClient
* @param redisClient - Redis connection string of a standalone Redis node
* @param attemptSettings - Override default AttemptSettings
* @param namespacePrefix - Override default namespacePrefixes (used to prefix keys in Redis)
*/
public constructor(redisClient = '127.0.0.1:6379', attemptSettings: AttemptSettings = {}, namespacePrefix = '') {
this.redis = this.createRedisClient(redisClient); this.redis = this.createRedisClient(redisClient);
this.attemptSettings = { ...attemptDefaults, ...attemptSettings }; this.attemptSettings = { ...attemptDefaults, ...attemptSettings };
this.namespacePrefix = namespacePrefix;
// Register lua scripts // Register lua scripts
for (const [ name, script ] of Object.entries(REDIS_LUA_SCRIPTS)) { for (const [ name, script ] of Object.entries(REDIS_LUA_SCRIPTS)) {
@ -94,7 +102,7 @@ export class RedisLocker implements ReadWriteLocker, ResourceLocker, Initializab
* @returns A scoped Redis key that allows cleanup afterwards without affecting other keys. * @returns A scoped Redis key that allows cleanup afterwards without affecting other keys.
*/ */
private getReadWriteKey(identifier: ResourceIdentifier): string { private getReadWriteKey(identifier: ResourceIdentifier): string {
return `${PREFIX_RW}${identifier.path}`; return `${this.namespacePrefix}${PREFIX_RW}${identifier.path}`;
} }
/** /**
@ -103,7 +111,7 @@ export class RedisLocker implements ReadWriteLocker, ResourceLocker, Initializab
* @returns A scoped Redis key that allows cleanup afterwards without affecting other keys. * @returns A scoped Redis key that allows cleanup afterwards without affecting other keys.
*/ */
private getResourceKey(identifier: ResourceIdentifier): string { private getResourceKey(identifier: ResourceIdentifier): string {
return `${PREFIX_LOCK}${identifier.path}`; return `${this.namespacePrefix}${PREFIX_LOCK}${identifier.path}`;
} }
/* ReadWriteLocker methods */ /* ReadWriteLocker methods */
@ -199,12 +207,12 @@ export class RedisLocker implements ReadWriteLocker, ResourceLocker, Initializab
* Remove any lock still open * Remove any lock still open
*/ */
private async clearLocks(): Promise<void> { private async clearLocks(): Promise<void> {
const keysRw = await this.redisRw.keys(`${PREFIX_RW}*`); const keysRw = await this.redisRw.keys(`${this.namespacePrefix}${PREFIX_RW}*`);
if (keysRw.length > 0) { if (keysRw.length > 0) {
await this.redisRw.del(...keysRw); await this.redisRw.del(...keysRw);
} }
const keysLock = await this.redisLock.keys(`${PREFIX_LOCK}*`); const keysLock = await this.redisLock.keys(`${this.namespacePrefix}${PREFIX_LOCK}*`);
if (keysLock.length > 0) { if (keysLock.length > 0) {
await this.redisLock.del(...keysLock); await this.redisLock.del(...keysLock);
} }

View File

@ -101,6 +101,15 @@ const redis: jest.Mocked<Redis & RedisResourceLock & RedisReadWriteLock> = {
jest.mock('ioredis', (): any => jest.fn().mockImplementation((): Redis => redis)); jest.mock('ioredis', (): any => jest.fn().mockImplementation((): Redis => redis));
describe('A RedisLocker', (): void => { describe('A RedisLocker', (): void => {
it('will generate keys with the given namespacePrefix.', async(): Promise<void> => {
const identifier = { path: 'http://test.com/resource' };
const lockerPrefixed = new RedisLocker('6379', {}, 'MY_PREFIX');
await lockerPrefixed.acquire(identifier);
const allLocksPrefixed = Object.keys(store.internal).every((key): boolean => key.startsWith('MY_PREFIX'));
await lockerPrefixed.release(identifier);
expect(allLocksPrefixed).toBeTruthy();
});
describe('with Read-Write logic', (): void => { describe('with Read-Write logic', (): void => {
const resource1 = { path: 'http://test.com/resource' }; const resource1 = { path: 'http://test.com/resource' };
const resource2 = { path: 'http://test.com/resource2' }; const resource2 = { path: 'http://test.com/resource2' };
@ -392,8 +401,8 @@ describe('A RedisLocker', (): void => {
const emitter = new EventEmitter(); const emitter = new EventEmitter();
const promise = locker.withWriteLock(resource1, (): any => const promise = locker.withWriteLock(resource1, (): any =>
new Promise<void>((resolve): any => emitter.on('release', resolve))); new Promise<void>((resolve): any => emitter.on('release', resolve)));
await redis.releaseWriteLock(`__RW__${resource1.path}`);
await flushPromises(); await flushPromises();
await redis.releaseWriteLock(`__RW__${resource1.path}`);
emitter.emit('release'); emitter.emit('release');
await expect(promise).rejects.toThrow('Redis operation error detected (value was null).'); await expect(promise).rejects.toThrow('Redis operation error detected (value was null).');
}); });