diff --git a/src/util/locking/RedisLocker.ts b/src/util/locking/RedisLocker.ts index d2bff1bfc..3e61444ba 100644 --- a/src/util/locking/RedisLocker.ts +++ b/src/util/locking/RedisLocker.ts @@ -50,11 +50,19 @@ export class RedisLocker implements ReadWriteLocker, ResourceLocker, Initializab private readonly redisRw: RedisReadWriteLock; private readonly redisLock: RedisResourceLock; private readonly attemptSettings: Required; + private readonly namespacePrefix: string; 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.attemptSettings = { ...attemptDefaults, ...attemptSettings }; + this.namespacePrefix = namespacePrefix; // Register 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. */ 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. */ private getResourceKey(identifier: ResourceIdentifier): string { - return `${PREFIX_LOCK}${identifier.path}`; + return `${this.namespacePrefix}${PREFIX_LOCK}${identifier.path}`; } /* ReadWriteLocker methods */ @@ -199,12 +207,12 @@ export class RedisLocker implements ReadWriteLocker, ResourceLocker, Initializab * Remove any lock still open */ private async clearLocks(): Promise { - const keysRw = await this.redisRw.keys(`${PREFIX_RW}*`); + const keysRw = await this.redisRw.keys(`${this.namespacePrefix}${PREFIX_RW}*`); if (keysRw.length > 0) { 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) { await this.redisLock.del(...keysLock); } diff --git a/test/unit/util/locking/RedisLocker.test.ts b/test/unit/util/locking/RedisLocker.test.ts index 7df0f6a6d..d4c70f122 100644 --- a/test/unit/util/locking/RedisLocker.test.ts +++ b/test/unit/util/locking/RedisLocker.test.ts @@ -101,6 +101,15 @@ const redis: jest.Mocked = { jest.mock('ioredis', (): any => jest.fn().mockImplementation((): Redis => redis)); describe('A RedisLocker', (): void => { + it('will generate keys with the given namespacePrefix.', async(): Promise => { + 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 => { const resource1 = { path: 'http://test.com/resource' }; const resource2 = { path: 'http://test.com/resource2' }; @@ -392,8 +401,8 @@ describe('A RedisLocker', (): void => { const emitter = new EventEmitter(); const promise = locker.withWriteLock(resource1, (): any => new Promise((resolve): any => emitter.on('release', resolve))); - await redis.releaseWriteLock(`__RW__${resource1.path}`); await flushPromises(); + await redis.releaseWriteLock(`__RW__${resource1.path}`); emitter.emit('release'); await expect(promise).rejects.toThrow('Redis operation error detected (value was null).'); });