- Add OpenAPI components for creating and responding to AI assistant requests. - Implement AI assistant service for managing live captions capability. - Create routes and controllers for AI assistant operations (create and cancel). - Introduce request validation middleware for AI assistant requests. - Update Redis helper to manage AI assistant locks. - Integrate AI assistant cleanup in webhook service. - Enhance LiveKit service to manage agent dispatch for AI assistants. - Update token service to remove unnecessary parameters related to captions. - Add typings for AI assistant requests and responses.
184 lines
6.0 KiB
TypeScript
184 lines
6.0 KiB
TypeScript
import { Lock, Redlock } from '@sesamecare-oss/redlock';
|
|
import { inject, injectable } from 'inversify';
|
|
import ms from 'ms';
|
|
import { MeetLock } from '../helpers/redis.helper.js';
|
|
import { LoggerService } from './logger.service.js';
|
|
import { RedisService } from './redis.service.js';
|
|
|
|
export type RedisLock = Lock;
|
|
@injectable()
|
|
export class MutexService {
|
|
protected redlockWithoutRetry: Redlock;
|
|
protected readonly TTL_MS = ms('1m');
|
|
|
|
constructor(
|
|
@inject(RedisService) protected redisService: RedisService,
|
|
@inject(LoggerService) protected logger: LoggerService
|
|
) {
|
|
// Create a Redlock instance with no retry strategy
|
|
this.redlockWithoutRetry = this.redisService.createRedlock(0);
|
|
}
|
|
|
|
/**
|
|
* Acquires a lock for the specified resource.
|
|
* This method uses the Redlock library to acquire a distributed lock on a resource identified by the key.
|
|
* The request will return null if the lock cannot be acquired.
|
|
*
|
|
* @param key The resource to acquire a lock for.
|
|
* @param ttl The time-to-live (TTL) for the lock in milliseconds. Defaults to the TTL value of the MutexService.
|
|
* @returns A Promise that resolves to the acquired Lock object. If the lock cannot be acquired, it resolves to null.
|
|
*/
|
|
async acquire(key: string, ttl: number = this.TTL_MS): Promise<Lock | null> {
|
|
const registryKey = MeetLock.getRegistryLock(key);
|
|
|
|
try {
|
|
this.logger.debug(`Requesting lock: ${key}`);
|
|
const lock = await this.redlockWithoutRetry.acquire([key], ttl);
|
|
|
|
// Store Lock data in Redis registry for support HA and release lock
|
|
await this.redisService.set(
|
|
registryKey,
|
|
JSON.stringify({
|
|
resources: lock.resources,
|
|
value: lock.value,
|
|
expiration: lock.expiration,
|
|
createdAt: Date.now()
|
|
}),
|
|
true
|
|
);
|
|
return lock;
|
|
} catch (error) {
|
|
this.logger.warn('Error acquiring lock:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Releases a lock on a resource.
|
|
*
|
|
* @param key - The resource to release the lock on.
|
|
* @returns A Promise that resolves when the lock is released.
|
|
*/
|
|
async release(key: string): Promise<void> {
|
|
const registryKey = MeetLock.getRegistryLock(key);
|
|
const lock = await this.getLockData(registryKey);
|
|
|
|
if (!lock) {
|
|
this.logger.warn(`Lock not found for resource: ${key}. May be expired or released by another process.`);
|
|
return;
|
|
}
|
|
|
|
if (lock) {
|
|
try {
|
|
await lock.release();
|
|
this.logger.verbose(`Lock ${key} successfully released.`);
|
|
} catch (error) {
|
|
this.logger.error(`Error releasing lock for key ${key}:`, error);
|
|
} finally {
|
|
await this.redisService.delete(registryKey);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Retrieves all locks for a given prefix.
|
|
*
|
|
* This method retrieves all keys from Redis that match the specified prefix and returns an array of `Lock` instances.
|
|
*
|
|
* @param pattern - The prefix to filter the keys in Redis.
|
|
* @returns A promise that resolves to an array of `Lock` instances.
|
|
*/
|
|
async getLocksByPrefix(pattern: string): Promise<Lock[]> {
|
|
const registryPattern = MeetLock.getRegistryLock(pattern);
|
|
const keys = await this.redisService.getKeys(registryPattern);
|
|
this.logger.debug(`Found ${keys.length} registry keys for pattern "${pattern}".`);
|
|
|
|
if (keys.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
const lockPromises: Promise<Lock | null>[] = keys.map((key) => this.getLockData(key));
|
|
|
|
const locksResult = await Promise.all(lockPromises);
|
|
|
|
const locks = locksResult.filter((lock): lock is Lock => lock !== null);
|
|
return locks;
|
|
}
|
|
|
|
/**
|
|
* Attempts to acquire a lock, retrying up to `maxAttempts` times with a fixed delay between
|
|
* attempts. Intended for fire-and-forget flows (e.g. webhooks) where the caller has no
|
|
* opportunity to retry externally and a missed lock acquisition would leave the system in an
|
|
* inconsistent state.
|
|
*
|
|
* @param key - The resource to acquire a lock for.
|
|
* @param ttl - The time-to-live for the lock in milliseconds.
|
|
* @param maxAttempts - Maximum number of acquisition attempts. Defaults to 3.
|
|
* @param delayMs - Fixed delay in milliseconds between attempts. Defaults to 200.
|
|
* @returns A Promise that resolves to the acquired Lock, or null if all attempts fail.
|
|
*/
|
|
async acquireWithRetry(key: string, ttl: number = this.TTL_MS, maxAttempts = 3, delayMs = 200): Promise<Lock | null> {
|
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
const lock = await this.acquire(key, ttl);
|
|
|
|
if (lock) return lock;
|
|
|
|
if (attempt < maxAttempts) {
|
|
this.logger.warn(`Lock '${key}' attempt ${attempt}/${maxAttempts} failed. Retrying in ${delayMs}ms...`);
|
|
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
lockExists(key: string): Promise<boolean> {
|
|
const registryKey = MeetLock.getRegistryLock(key);
|
|
return this.redisService.exists(registryKey);
|
|
}
|
|
|
|
/**
|
|
* Retrieves the creation timestamp of a lock identified by the given key.
|
|
*
|
|
* @param key - The unique identifier for the lock
|
|
* @returns A Promise that resolves to the creation timestamp (as a number) of the lock, or null if the lock doesn't exist or has expired
|
|
*/
|
|
async getLockCreatedAt(key: string): Promise<number | null> {
|
|
const registryKey = MeetLock.getRegistryLock(key);
|
|
|
|
const redisLockData = await this.redisService.get(registryKey);
|
|
|
|
if (!redisLockData) {
|
|
this.logger.warn(
|
|
`Lock not found for resource: ${registryKey}. May be expired or released by another process.`
|
|
);
|
|
return null;
|
|
}
|
|
|
|
const { createdAt } = JSON.parse(redisLockData);
|
|
return createdAt;
|
|
}
|
|
|
|
/**
|
|
* Retrieves the lock data for a given resource key.
|
|
*
|
|
* @param registryKey - The resource key to retrieve the lock data for.
|
|
* @returns A promise that resolves to a `Lock` instance or null if not found.
|
|
*/
|
|
protected async getLockData(registryKey: string): Promise<Lock | null> {
|
|
try {
|
|
// Try to get lock from Redis
|
|
const redisLockData = await this.redisService.get(registryKey);
|
|
|
|
if (!redisLockData) {
|
|
return null;
|
|
}
|
|
|
|
const { resources, value, expiration } = JSON.parse(redisLockData);
|
|
return new Lock(this.redlockWithoutRetry, resources, value, [], expiration);
|
|
} catch (error) {
|
|
return null;
|
|
}
|
|
}
|
|
}
|