diff --git a/backend/src/services/mutex.service.ts b/backend/src/services/mutex.service.ts index 49b962e..e760967 100644 --- a/backend/src/services/mutex.service.ts +++ b/backend/src/services/mutex.service.ts @@ -1,7 +1,7 @@ -import Redlock, { Lock } from 'redlock'; -import { RedisService } from './redis.service.js'; -import { inject, injectable } from 'inversify'; import ms from 'ms'; +import Redlock, { Lock } from 'redlock'; +import { inject, injectable } from 'inversify'; +import { RedisService } from './redis.service.js'; import { LoggerService } from './logger.service.js'; export type RedisLock = Lock; @@ -11,6 +11,7 @@ export class MutexService { protected locks: Map; protected readonly TTL_MS = ms('1m'); protected LOCK_KEY_PREFIX = 'ov_meet_lock:'; + protected LOCK_REGISTRY_PREFIX = 'ov_meet_lock_registry:'; constructor( @inject(RedisService) protected redisService: RedisService, @@ -28,12 +29,25 @@ export class MutexService { * @returns A Promise that resolves to the acquired Lock object. */ async acquire(resource: string, ttl: number = this.TTL_MS): Promise { - const key = this.LOCK_KEY_PREFIX + resource; + const key = this.getLockKey(resource); + const registryKey = this.getLockRegistryKey(resource); try { this.logger.debug(`Acquiring lock for resource: ${resource}`); const lock = await this.redlockWithoutRetry.acquire([key], ttl); + + // Store the lock in memory for easy access (optional) this.locks.set(key, lock); + // Store Lock metadata in Redis registry for support HA release + await this.redisService.set( + registryKey, + JSON.stringify({ + resources: lock.resources, + value: lock.value, + expiration: lock.expiration + }), + true + ); return lock; } catch (error) { this.logger.error('Error acquiring lock:', error); @@ -48,8 +62,13 @@ export class MutexService { * @returns A Promise that resolves when the lock is released. */ async release(resource: string): Promise { - const key = this.LOCK_KEY_PREFIX + resource; - const lock = this.locks.get(key); + const key = this.getLockKey(resource); + + const lock = await this.getLockData(resource); + + if (!lock) { + return; + } if (lock) { this.logger.debug(`Releasing lock for resource: ${resource}`); @@ -60,7 +79,64 @@ export class MutexService { this.logger.error(`Error releasing lock for key ${key}:`, error); } finally { this.locks.delete(key); + await this.redisService.delete(this.getLockRegistryKey(resource)); } } } + + /** + * Returns the complete key used to acquire the lock in Redis. + */ + protected getLockKey(resource: string): string { + return `${this.LOCK_KEY_PREFIX}${resource}`; + } + + /** + * Generates a unique key for the lock registry by combining a predefined prefix + * with the specified resource identifier. + */ + protected getLockRegistryKey(resource: string): string { + return `${this.LOCK_REGISTRY_PREFIX}${resource}`; + } + + /** + * Retrieves the lock data for a given resource. + * + * This method first attempts to retrieve the lock from memory. If the lock is not found in memory, + * it then tries to fetch the lock data from Redis. If the lock data is successfully retrieved from Redis, + * it constructs a new `Lock` instance and returns it. If the lock data cannot be found in either memory + * or Redis, the method returns `null`. + * + * @param resource - The identifier of the resource for which the lock data is being retrieved. + * @returns A promise that resolves to the `Lock` instance if found, or `null` if the lock data is not available. + */ + protected async getLockData(resource: string): Promise { + const key = this.getLockKey(resource); + const registryKey = this.getLockRegistryKey(resource); + + // Try to get lock from memory + let lock = this.locks.get(key); + + if (lock) { + return lock; + } + + try { + this.logger.debug(`Getting lock data in Redis for resource: ${resource}`); + // Try to get lock from Redis + const redisLockData = await this.redisService.get(registryKey); + + if (!redisLockData) { + this.logger.error(`Cannot release lock. Lock not found for resource: ${resource}.`); + return null; + } + + const { resources, value, expiration } = JSON.parse(redisLockData); + lock = new Lock(this.redlockWithoutRetry, resources, value, [], expiration); + return lock; + } catch (error) { + this.logger.error(`Cannot release lock. Lock not found for resource: ${resource}.`); + return null; + } + } }