backend: Refactor room deletion methods to handle multiple rooms and improve logging
This commit is contained in:
parent
814f75c4a1
commit
404d21b8d5
@ -80,6 +80,8 @@ export const MEET_API_USER = 'api-user';
|
||||
export const MEET_S3_ROOMS_PREFIX = 'rooms';
|
||||
export const MEET_S3_RECORDINGS_PREFIX = 'recordings';
|
||||
|
||||
export const MEET_ROOM_GC_INTERVAL: ms.StringValue = '1h';
|
||||
|
||||
// Time to live for the active recording lock in Redis
|
||||
export const MEET_RECORDING_LOCK_TTL: ms.StringValue = '6h';
|
||||
export const MEET_RECORDING_STARTED_TIMEOUT: ms.StringValue = '30s';
|
||||
|
||||
@ -107,7 +107,7 @@ export class LiveKitService {
|
||||
await this.getRoom(roomName);
|
||||
} catch (error) {
|
||||
this.logger.warn(`Livekit Room ${roomName} not found. Skipping deletion.`);
|
||||
return;
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
await this.roomClient.deleteRoom(roomName);
|
||||
|
||||
@ -220,16 +220,15 @@ export class RedisService extends EventEmitter {
|
||||
/**
|
||||
* Deletes a key from Redis.
|
||||
* @param key - The key to delete.
|
||||
* @param hashKey - The hash key to delete. If provided, it will delete the hash key from the hash stored at the given key.
|
||||
* @returns A promise that resolves to the number of keys deleted.
|
||||
*/
|
||||
delete(key: string, hashKey?: string): Promise<number> {
|
||||
delete(keys: string | string[]): Promise<number> {
|
||||
try {
|
||||
if (hashKey) {
|
||||
return this.redisPublisher.hdel(key, hashKey);
|
||||
} else {
|
||||
return this.redisPublisher.del(key);
|
||||
if (typeof keys === 'string') {
|
||||
keys = [keys];
|
||||
}
|
||||
|
||||
return this.redisPublisher.del(keys);
|
||||
} catch (error) {
|
||||
throw internalError(`Error deleting key from Redis ${error}`);
|
||||
}
|
||||
|
||||
@ -7,11 +7,11 @@ import { MeetStorageService } from './storage/storage.service.js';
|
||||
import { MeetRoom, MeetRoomFilters, MeetRoomOptions, MeetRoomPreferences, ParticipantRole } from '@typings-ce';
|
||||
import { MeetRoomHelper } from '../helpers/room.helper.js';
|
||||
import { SystemEventService } from './system-event.service.js';
|
||||
import { TaskSchedulerService } from './task-scheduler.service.js';
|
||||
import { IScheduledTask, TaskSchedulerService } from './task-scheduler.service.js';
|
||||
import { errorParticipantUnauthorized } from '../models/error.model.js';
|
||||
import { OpenViduComponentsAdapterHelper } from '../helpers/index.js';
|
||||
import { uid } from 'uid/single';
|
||||
import { MEET_NAME_ID } from '../environment.js';
|
||||
import { MEET_NAME_ID, MEET_ROOM_GC_INTERVAL } from '../environment.js';
|
||||
import ms from 'ms';
|
||||
import { UtilsHelper } from '../helpers/utils.helper.js';
|
||||
|
||||
@ -29,28 +29,14 @@ export class RoomService {
|
||||
@inject(LiveKitService) protected livekitService: LiveKitService,
|
||||
@inject(SystemEventService) protected systemEventService: SystemEventService,
|
||||
@inject(TaskSchedulerService) protected taskSchedulerService: TaskSchedulerService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Initializes the room service.
|
||||
*
|
||||
* This method sets up the room garbage collector and event listeners.
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
this.systemEventService.onRedisReady(async () => {
|
||||
// try {
|
||||
// await this.deleteOpenViduExpiredRooms();
|
||||
// } catch (error) {
|
||||
// this.logger.error('Error deleting OpenVidu expired rooms:', error);
|
||||
// }
|
||||
// await Promise.all([
|
||||
// //TODO: Livekit rooms should not be created here. They should be created when a user joins a room.
|
||||
// this.restoreMissingLivekitRooms().catch((error) =>
|
||||
// this.logger.error('Error restoring missing rooms:', error)
|
||||
// ),
|
||||
// this.taskSchedulerService.startRoomGarbageCollector(this.deleteExpiredRooms.bind(this))
|
||||
// ]);
|
||||
});
|
||||
) {
|
||||
const roomGarbageCollectorTask: IScheduledTask = {
|
||||
name: 'roomGarbageCollector',
|
||||
type: 'cron',
|
||||
scheduleOrDelay: MEET_ROOM_GC_INTERVAL,
|
||||
callback: this.deleteExpiredRooms.bind(this)
|
||||
};
|
||||
this.taskSchedulerService.registerTask(roomGarbageCollectorTask);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -173,62 +159,31 @@ export class RoomService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes OpenVidu and LiveKit rooms.
|
||||
* Bulk deletes rooms from both storage (e.g., S3) and LiveKit.
|
||||
*
|
||||
* This method deletes rooms from both LiveKit and OpenVidu services.
|
||||
*
|
||||
* @param roomIds - An array of room names to be deleted.
|
||||
* @returns A promise that resolves with an array of successfully deleted room names.
|
||||
* @param roomIds - An array with the IDs of the rooms to delete.
|
||||
*/
|
||||
async bulkDeleteRooms(roomIds: string[]): Promise<string[]> {
|
||||
const [openViduResults, livekitResults] = await Promise.all([
|
||||
this.deleteOpenViduRooms(roomIds),
|
||||
Promise.allSettled(roomIds.map((roomId) => this.livekitService.deleteRoom(roomId)))
|
||||
]);
|
||||
async bulkDeleteRooms(roomIds: string[]): Promise<void> {
|
||||
try {
|
||||
// Delete rooms from storage and LiveKit in parallel
|
||||
const meetRoomPromise = this.storageService.deleteMeetRooms(roomIds);
|
||||
const livekitResults = await Promise.allSettled(
|
||||
roomIds.map((roomId) => this.livekitService.deleteRoom(roomId))
|
||||
);
|
||||
|
||||
// Log errors from LiveKit deletions
|
||||
livekitResults.forEach((result, index) => {
|
||||
if (result.status === 'rejected') {
|
||||
this.logger.error(`Failed to delete LiveKit room "${roomIds[index]}": ${result.reason}`);
|
||||
}
|
||||
});
|
||||
// Wait for the meet rooms deletion to complete
|
||||
await meetRoomPromise;
|
||||
|
||||
// Combine successful deletions
|
||||
const successfullyDeleted = new Set(openViduResults);
|
||||
|
||||
livekitResults.forEach((result, index) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
successfullyDeleted.add(roomIds[index]);
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(successfullyDeleted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes OpenVidu rooms.
|
||||
*
|
||||
* @param roomIds - List of room names to delete.
|
||||
* @returns A promise that resolves with an array of successfully deleted room names.
|
||||
*/
|
||||
protected async deleteOpenViduRooms(roomIds: string[]): Promise<string[]> {
|
||||
const results = await Promise.allSettled(roomIds.map((roomId) => this.storageService.deleteMeetRoom(roomId)));
|
||||
|
||||
const successfulRooms: string[] = [];
|
||||
|
||||
results.forEach((result, index) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
successfulRooms.push(roomIds[index]);
|
||||
} else {
|
||||
this.logger.error(`Failed to delete OpenVidu room "${roomIds[index]}": ${result.reason}`);
|
||||
}
|
||||
});
|
||||
|
||||
if (successfulRooms.length === roomIds.length) {
|
||||
this.logger.verbose('All OpenVidu rooms have been deleted.');
|
||||
// Log results of LiveKit room deletions
|
||||
livekitResults.forEach((result, index) => {
|
||||
if (result.status === 'rejected') {
|
||||
this.logger.error(`Failed to delete LiveKit room "${roomIds[index]}": ${result.reason}`);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error('Error deleting rooms:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return successfulRooms;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -307,12 +262,9 @@ export class RoomService {
|
||||
**/
|
||||
protected async deleteExpiredRooms(): Promise<void> {
|
||||
try {
|
||||
const ovExpiredRooms = await this.deleteOpenViduExpiredRooms();
|
||||
const ovExpiredRooms = await this.deleteExpiredMeetRooms();
|
||||
|
||||
if (ovExpiredRooms.length === 0) {
|
||||
this.logger.verbose('No expired rooms found to delete.');
|
||||
return;
|
||||
}
|
||||
if (ovExpiredRooms.length === 0) return;
|
||||
|
||||
const livekitResults = await Promise.allSettled(
|
||||
ovExpiredRooms.map((roomId) => this.livekitService.deleteRoom(roomId))
|
||||
@ -337,78 +289,34 @@ export class RoomService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes expired OpenVidu rooms.
|
||||
* Deletes expired Meet rooms by iterating through all paged results.
|
||||
*
|
||||
* This method checks for rooms that have an expiration date earlier than the current time
|
||||
* and attempts to delete them.
|
||||
*
|
||||
* @returns {Promise<void>} A promise that resolves when the operation is complete.
|
||||
* @returns A promise that resolves with an array of room IDs that were successfully deleted.
|
||||
*/
|
||||
protected async deleteOpenViduExpiredRooms(): Promise<string[]> {
|
||||
// const now = Date.now();
|
||||
// this.logger.verbose(`Checking OpenVidu expired rooms at ${new Date(now).toISOString()}`);
|
||||
// const rooms = await this.getAllMeetRooms();
|
||||
// const expiredRooms = rooms
|
||||
// .filter((room) => room.expirationDate && room.expirationDate < now)
|
||||
// .map((room) => room.roomId);
|
||||
protected async deleteExpiredMeetRooms(): Promise<string[]> {
|
||||
const now = Date.now();
|
||||
this.logger.verbose(`Checking Meet expired rooms at ${new Date(now).toISOString()}`);
|
||||
let nextPageToken: string | undefined;
|
||||
const deletedRooms: string[] = [];
|
||||
|
||||
// if (expiredRooms.length === 0) {
|
||||
// this.logger.verbose('No OpenVidu expired rooms to delete.');
|
||||
// return [];
|
||||
// }
|
||||
do {
|
||||
const { rooms, nextPageToken: token } = await this.getAllMeetRooms({ maxItems: 100, nextPageToken });
|
||||
nextPageToken = token;
|
||||
|
||||
// this.logger.info(`Deleting ${expiredRooms.length} OpenVidu expired rooms: ${expiredRooms.join(', ')}`);
|
||||
const expiredRoomIds = rooms
|
||||
.filter((room) => room.expirationDate && room.expirationDate < now)
|
||||
.map((room) => room.roomId);
|
||||
|
||||
// return await this.deleteOpenViduRooms(expiredRooms);
|
||||
return [];
|
||||
}
|
||||
if (expiredRoomIds.length > 0) {
|
||||
this.logger.verbose(
|
||||
`Deleting ${expiredRoomIds.length} expired Meet rooms: ${expiredRoomIds.join(', ')}`
|
||||
);
|
||||
// const deletedOnPage = await this.deleteMeetRooms(expiredRooms);
|
||||
await this.storageService.deleteMeetRooms(expiredRoomIds);
|
||||
deletedRooms.push(...expiredRoomIds);
|
||||
}
|
||||
} while (nextPageToken);
|
||||
|
||||
/**
|
||||
* Restores missing Livekit rooms by comparing the list of rooms from Livekit and OpenVidu.
|
||||
* If any rooms are missing in Livekit, they will be created.
|
||||
*
|
||||
* @returns {Promise<void>} A promise that resolves when the restoration process is complete.
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
protected async restoreMissingLivekitRooms(): Promise<void> {
|
||||
// this.logger.verbose(`Checking missing Livekit rooms ...`);
|
||||
// const [lkResult, ovResult] = await Promise.allSettled([
|
||||
// this.livekitService.listRooms(),
|
||||
// this.getAllMeetRooms()
|
||||
// ]);
|
||||
// let lkRooms: Room[] = [];
|
||||
// let ovRooms: MeetRoom[] = [];
|
||||
// if (lkResult.status === 'fulfilled') {
|
||||
// lkRooms = lkResult.value;
|
||||
// } else {
|
||||
// this.logger.error('Failed to list Livekit rooms:', lkResult.reason);
|
||||
// }
|
||||
// if (ovResult.status === 'fulfilled') {
|
||||
// ovRooms = ovResult.value;
|
||||
// } else {
|
||||
// this.logger.error('Failed to list OpenVidu rooms:', ovResult.reason);
|
||||
// }
|
||||
// const missingRooms: MeetRoom[] = ovRooms.filter(
|
||||
// (ovRoom) => !lkRooms.some((room) => room.name === ovRoom.roomId)
|
||||
// );
|
||||
// if (missingRooms.length === 0) {
|
||||
// this.logger.verbose('All OpenVidu rooms are present in Livekit. No missing rooms to restore. ');
|
||||
// return;
|
||||
// }
|
||||
// this.logger.info(`Restoring ${missingRooms.length} missing rooms`);
|
||||
// const creationResults = await Promise.allSettled(
|
||||
// missingRooms.map(({ roomId }: MeetRoom) => {
|
||||
// this.logger.debug(`Restoring room: ${roomId}`);
|
||||
// this.createLivekitRoom(roomId);
|
||||
// })
|
||||
// );
|
||||
// creationResults.forEach((result, index) => {
|
||||
// if (result.status === 'rejected') {
|
||||
// this.logger.error(`Failed to restore room "${missingRooms[index].roomId}": ${result.reason}`);
|
||||
// } else {
|
||||
// this.logger.info(`Restored room "${missingRooms[index].roomId}"`);
|
||||
// }
|
||||
// });
|
||||
return deletedRooms;
|
||||
}
|
||||
}
|
||||
|
||||
@ -240,15 +240,15 @@ export class S3StorageProvider<G extends GlobalPreferences = GlobalPreferences,
|
||||
}
|
||||
}
|
||||
|
||||
async deleteMeetRoom(roomId: string): Promise<void> {
|
||||
const s3RoomPath = `${MEET_S3_ROOMS_PREFIX}/${roomId}/${roomId}.json`;
|
||||
const redisKey = RedisKeyName.ROOM + roomId;
|
||||
async deleteMeetRooms(roomIds: string[]): Promise<void> {
|
||||
const roomsToDelete = roomIds.map((id) => `${MEET_S3_ROOMS_PREFIX}/${id}/${id}.json`);
|
||||
const redisKeysToDelete = roomIds.map((id) => RedisKeyName.ROOM + id);
|
||||
|
||||
try {
|
||||
await Promise.all([this.s3Service.deleteObject(s3RoomPath), this.redisService.delete(redisKey)]);
|
||||
this.logger.verbose(`Room ${roomId} deleted successfully from S3 and Redis`);
|
||||
await Promise.all([this.s3Service.deleteObjects(roomsToDelete), this.redisService.delete(redisKeysToDelete)]);
|
||||
this.logger.verbose(`Rooms deleted successfully: ${roomIds.join(', ')}`);
|
||||
} catch (error) {
|
||||
this.handleError(error, `Error deleting Room preferences for room ${roomId}`);
|
||||
this.handleError(error, `Error deleting rooms: ${roomIds.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -72,10 +72,10 @@ export interface StorageProvider<T extends GlobalPreferences = GlobalPreferences
|
||||
saveMeetRoom(ovRoom: R): Promise<R>;
|
||||
|
||||
/**
|
||||
* Deletes the OpenVidu Meet Room for a given room name.
|
||||
* Deletes OpenVidu Meet Rooms.
|
||||
*
|
||||
* @param roomId - The name of the room whose should be deleted.
|
||||
* @param roomIds - The room names to delete.
|
||||
* @returns A promise that resolves when the room have been deleted.
|
||||
**/
|
||||
deleteMeetRoom(roomId: string): Promise<void>;
|
||||
deleteMeetRooms(roomIds: string[]): Promise<void>;
|
||||
}
|
||||
|
||||
@ -117,8 +117,8 @@ export class MeetStorageService<G extends GlobalPreferences = GlobalPreferences,
|
||||
return meetRoom as R;
|
||||
}
|
||||
|
||||
async deleteMeetRoom(roomId: string): Promise<void> {
|
||||
return this.storageProvider.deleteMeetRoom(roomId);
|
||||
async deleteMeetRooms(roomIds: string[]): Promise<void> {
|
||||
return this.storageProvider.deleteMeetRooms(roomIds);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user