backend: enhance room deletion logic with new policies for meetings and recordings
This commit is contained in:
parent
632d36a470
commit
4802f48ba6
@ -61,10 +61,10 @@ export const registerDependencies = () => {
|
||||
|
||||
container.bind(FrontendEventService).toSelf().inSingletonScope();
|
||||
container.bind(LiveKitService).toSelf().inSingletonScope();
|
||||
container.bind(RecordingService).toSelf().inSingletonScope();
|
||||
container.bind(RoomService).toSelf().inSingletonScope();
|
||||
container.bind(ParticipantNameService).toSelf().inSingletonScope();
|
||||
container.bind(ParticipantService).toSelf().inSingletonScope();
|
||||
container.bind(RecordingService).toSelf().inSingletonScope();
|
||||
container.bind(OpenViduWebhookService).toSelf().inSingletonScope();
|
||||
container.bind(LivekitWebhookService).toSelf().inSingletonScope();
|
||||
};
|
||||
|
||||
@ -1,4 +1,12 @@
|
||||
import { MeetRoomFilters, MeetRoomOptions, MeetRoomRoleAndPermissions, ParticipantRole } from '@typings-ce';
|
||||
import {
|
||||
MeetRoomDeletionPolicyWithMeeting,
|
||||
MeetRoomDeletionPolicyWithRecordings,
|
||||
MeetRoomDeletionSuccessCode,
|
||||
MeetRoomFilters,
|
||||
MeetRoomOptions,
|
||||
MeetRoomRoleAndPermissions,
|
||||
ParticipantRole
|
||||
} from '@typings-ce';
|
||||
import { Request, Response } from 'express';
|
||||
import { container } from '../config/index.js';
|
||||
import INTERNAL_CONFIG from '../config/internal-config.js';
|
||||
@ -63,21 +71,26 @@ export const deleteRoom = async (req: Request, res: Response) => {
|
||||
const roomService = container.get(RoomService);
|
||||
|
||||
const { roomId } = req.params;
|
||||
const { force } = req.query;
|
||||
const forceDelete = force === 'true';
|
||||
const { withMeeting, withRecordings } = req.query as {
|
||||
withMeeting: MeetRoomDeletionPolicyWithMeeting;
|
||||
withRecordings: MeetRoomDeletionPolicyWithRecordings;
|
||||
};
|
||||
|
||||
try {
|
||||
logger.verbose(`Deleting room '${roomId}'`);
|
||||
const response = await roomService.deleteMeetRoom(roomId, withMeeting, withRecordings);
|
||||
|
||||
const { deleted } = await roomService.bulkDeleteRooms([roomId], forceDelete);
|
||||
// Determine the status code based on the success code
|
||||
// If the room action is scheduled, return 202. Otherwise, return 200.
|
||||
const scheduledSuccessCodes = [
|
||||
MeetRoomDeletionSuccessCode.ROOM_WITH_ACTIVE_MEETING_SCHEDULED_TO_BE_DELETED,
|
||||
MeetRoomDeletionSuccessCode.ROOM_WITH_ACTIVE_MEETING_SCHEDULED_TO_BE_CLOSED,
|
||||
MeetRoomDeletionSuccessCode.ROOM_WITH_ACTIVE_MEETING_AND_RECORDINGS_SCHEDULED_TO_BE_DELETED
|
||||
];
|
||||
const statusCode = scheduledSuccessCodes.includes(response.successCode) ? 202 : 200;
|
||||
|
||||
if (deleted.length > 0) {
|
||||
// Room was deleted
|
||||
return res.status(204).send();
|
||||
}
|
||||
|
||||
// Room was marked as deleted
|
||||
return res.status(202).json({ message: `Room '${roomId}' marked for deletion` });
|
||||
logger.info(response.message);
|
||||
return res.status(statusCode).json(response);
|
||||
} catch (error) {
|
||||
handleError(res, error, `deleting room '${roomId}'`);
|
||||
}
|
||||
@ -86,34 +99,30 @@ export const deleteRoom = async (req: Request, res: Response) => {
|
||||
export const bulkDeleteRooms = async (req: Request, res: Response) => {
|
||||
const logger = container.get(LoggerService);
|
||||
const roomService = container.get(RoomService);
|
||||
const { roomIds, force } = req.query;
|
||||
const forceDelete = force === 'true';
|
||||
logger.verbose(`Deleting rooms: ${roomIds}`);
|
||||
|
||||
const { roomIds, withMeeting, withRecordings } = req.query as {
|
||||
roomIds: string[];
|
||||
withMeeting: MeetRoomDeletionPolicyWithMeeting;
|
||||
withRecordings: MeetRoomDeletionPolicyWithRecordings;
|
||||
};
|
||||
|
||||
try {
|
||||
const roomIdsArray = roomIds as string[];
|
||||
logger.verbose(`Deleting rooms: ${roomIds}`);
|
||||
const { successful, failed } = await roomService.bulkDeleteMeetRooms(roomIds, withMeeting, withRecordings);
|
||||
|
||||
const { deleted, markedForDeletion } = await roomService.bulkDeleteRooms(roomIdsArray, forceDelete);
|
||||
logger.info(
|
||||
`Bulk delete operation - Successfully processed rooms: ${successful.length}, failed to process: ${failed.length}`
|
||||
);
|
||||
|
||||
logger.info(`Deleted rooms: ${deleted.length}, marked for deletion: ${markedForDeletion.length}`);
|
||||
|
||||
// All rooms were deleted
|
||||
if (deleted.length > 0 && markedForDeletion.length === 0) {
|
||||
return res.sendStatus(204);
|
||||
if (failed.length === 0) {
|
||||
// All rooms were successfully processed
|
||||
return res.status(200).json({ message: 'All rooms successfully processed for deletion', successful });
|
||||
} else {
|
||||
// Some rooms failed to process
|
||||
return res
|
||||
.status(400)
|
||||
.json({ message: `${failed.length} room(s) failed to process while deleting`, successful, failed });
|
||||
}
|
||||
|
||||
// All room were marked for deletion
|
||||
if (deleted.length === 0 && markedForDeletion.length > 0) {
|
||||
const message =
|
||||
markedForDeletion.length === 1
|
||||
? `Room '${markedForDeletion[0]}' marked for deletion`
|
||||
: `Rooms '${markedForDeletion.join(', ')}' marked for deletion`;
|
||||
|
||||
return res.status(202).json({ message });
|
||||
}
|
||||
|
||||
// Mixed result (some rooms deleted, some marked for deletion)
|
||||
return res.status(200).json({ deleted, markedForDeletion });
|
||||
} catch (error) {
|
||||
handleError(res, error, `deleting rooms`);
|
||||
}
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import { MeetRoomDeletionErrorCode } from '@typings-ce';
|
||||
import { Response } from 'express';
|
||||
import { z } from 'zod';
|
||||
import { container } from '../config/index.js';
|
||||
import { LoggerService } from '../services/index.js';
|
||||
import { z } from 'zod';
|
||||
|
||||
type StatusError = 400 | 401 | 402 | 403 | 404 | 409 | 415 | 416 | 422 | 500 | 503;
|
||||
export class OpenViduMeetError extends Error {
|
||||
@ -220,6 +221,10 @@ export const errorInvalidRoomSecret = (roomId: string, secret: string): OpenVidu
|
||||
return new OpenViduMeetError('Room Error', `Secret '${secret}' is not recognized for room '${roomId}'`, 400);
|
||||
};
|
||||
|
||||
export const errorDeletingRoom = (errorCode: MeetRoomDeletionErrorCode, message: string): OpenViduMeetError => {
|
||||
return new OpenViduMeetError(errorCode, message, 409);
|
||||
};
|
||||
|
||||
// Participant errors
|
||||
|
||||
export const errorParticipantNotFound = (participantIdentity: string, roomId: string): OpenViduMeetError => {
|
||||
|
||||
@ -12,9 +12,9 @@ export * from './auth.service.js';
|
||||
|
||||
export * from './livekit.service.js';
|
||||
export * from './frontend-event.service.js';
|
||||
export * from './recording.service.js';
|
||||
export * from './room.service.js';
|
||||
export * from './participant-name.service.js';
|
||||
export * from './participant.service.js';
|
||||
export * from './recording.service.js';
|
||||
export * from './openvidu-webhook.service.js';
|
||||
export * from './livekit-webhook.service.js';
|
||||
|
||||
@ -31,7 +31,6 @@ import {
|
||||
MeetStorageService,
|
||||
MutexService,
|
||||
RedisLock,
|
||||
RoomService,
|
||||
TaskSchedulerService
|
||||
} from './index.js';
|
||||
|
||||
@ -39,7 +38,6 @@ import {
|
||||
export class RecordingService {
|
||||
constructor(
|
||||
@inject(LiveKitService) protected livekitService: LiveKitService,
|
||||
@inject(RoomService) protected roomService: RoomService,
|
||||
@inject(MutexService) protected mutexService: MutexService,
|
||||
@inject(TaskSchedulerService) protected taskSchedulerService: TaskSchedulerService,
|
||||
@inject(DistributedEventService) protected systemEventService: DistributedEventService,
|
||||
@ -552,6 +550,22 @@ export class RecordingService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to check if a room has recordings
|
||||
*
|
||||
* @param roomId - The ID of the room to check
|
||||
* @returns A promise that resolves to true if the room has recordings, false otherwise
|
||||
*/
|
||||
async hasRoomRecordings(roomId: string): Promise<boolean> {
|
||||
try {
|
||||
const response = await this.storageService.getAllRecordings(roomId, 1);
|
||||
return response.recordings.length > 0;
|
||||
} catch (error) {
|
||||
this.logger.warn(`Error checking recordings for room '${roomId}': ${error}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async getRecordingAsStream(
|
||||
recordingId: string,
|
||||
rangeHeader?: string
|
||||
@ -584,7 +598,7 @@ export class RecordingService {
|
||||
}
|
||||
|
||||
protected async validateRoomForStartRecording(roomId: string): Promise<void> {
|
||||
const room = await this.roomService.getMeetRoom(roomId);
|
||||
const room = await this.storageService.getMeetRoom(roomId);
|
||||
|
||||
if (!room) throw errorRoomNotFound(roomId);
|
||||
|
||||
|
||||
@ -2,6 +2,10 @@ import {
|
||||
MeetingEndAction,
|
||||
MeetRecordingAccess,
|
||||
MeetRoom,
|
||||
MeetRoomDeletionErrorCode,
|
||||
MeetRoomDeletionPolicyWithMeeting,
|
||||
MeetRoomDeletionPolicyWithRecordings,
|
||||
MeetRoomDeletionSuccessCode,
|
||||
MeetRoomFilters,
|
||||
MeetRoomOptions,
|
||||
MeetRoomPreferences,
|
||||
@ -19,10 +23,12 @@ import { MEET_NAME_ID } from '../environment.js';
|
||||
import { MeetRoomHelper, UtilsHelper } from '../helpers/index.js';
|
||||
import { validateRecordingTokenMetadata } from '../middlewares/index.js';
|
||||
import {
|
||||
errorDeletingRoom,
|
||||
errorInvalidRoomSecret,
|
||||
errorRoomMetadataNotFound,
|
||||
errorRoomNotFound,
|
||||
internalError
|
||||
internalError,
|
||||
OpenViduMeetError
|
||||
} from '../models/error.model.js';
|
||||
import {
|
||||
DistributedEventService,
|
||||
@ -31,6 +37,7 @@ import {
|
||||
LiveKitService,
|
||||
LoggerService,
|
||||
MeetStorageService,
|
||||
RecordingService,
|
||||
TaskSchedulerService,
|
||||
TokenService
|
||||
} from './index.js';
|
||||
@ -46,6 +53,7 @@ export class RoomService {
|
||||
constructor(
|
||||
@inject(LoggerService) protected logger: LoggerService,
|
||||
@inject(MeetStorageService) protected storageService: MeetStorageService,
|
||||
@inject(RecordingService) protected recordingService: RecordingService,
|
||||
@inject(LiveKitService) protected livekitService: LiveKitService,
|
||||
@inject(DistributedEventService) protected distributedEventService: DistributedEventService,
|
||||
@inject(FrontendEventService) protected frontendEventService: FrontendEventService,
|
||||
@ -235,45 +243,382 @@ export class RoomService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes multiple rooms in bulk, with the option to force delete or gracefully handle rooms with active participants.
|
||||
* For rooms with participants, when `forceDelete` is false, the method performs a "graceful deletion"
|
||||
* by marking the room for deletion without disrupting active sessions.
|
||||
* However, if `forceDelete` is true, it will also end the meetings by removing the rooms from LiveKit.
|
||||
* Deletes a room based on the specified policies for handling active meetings and recordings.
|
||||
*
|
||||
* @param roomIds - Array of room identifiers to be deleted
|
||||
* @param forceDelete - If true, deletes rooms even if they have active participants.
|
||||
* If false, rooms with participants will be marked for deletion instead of being deleted immediately.
|
||||
* @param roomId - The unique identifier of the room to delete
|
||||
* @param withMeeting - Policy for handling rooms with active meetings
|
||||
* @param withRecordings - Policy for handling rooms with recordings
|
||||
* @returns Promise with deletion result including status code, success code, message and room (if updated instead of deleted)
|
||||
* @throws Error with specific error codes for conflict scenarios
|
||||
*/
|
||||
async bulkDeleteRooms(
|
||||
roomIds: string[],
|
||||
forceDelete: boolean
|
||||
): Promise<{ deleted: string[]; markedForDeletion: string[] }> {
|
||||
async deleteMeetRoom(
|
||||
roomId: string,
|
||||
withMeeting: MeetRoomDeletionPolicyWithMeeting,
|
||||
withRecordings: MeetRoomDeletionPolicyWithRecordings
|
||||
): Promise<{
|
||||
successCode: MeetRoomDeletionSuccessCode;
|
||||
message: string;
|
||||
room?: MeetRoom;
|
||||
}> {
|
||||
try {
|
||||
this.logger.info(`Starting bulk deletion of ${roomIds.length} rooms (forceDelete: ${forceDelete})`);
|
||||
|
||||
// Classify rooms into those to delete and those to mark for deletion
|
||||
const { toDelete, toMark } = await this.classifyRoomsForDeletion(roomIds, forceDelete);
|
||||
|
||||
// Process each group in parallel
|
||||
const [deletedRooms, markedRooms] = await Promise.all([
|
||||
this.batchDeleteRooms(toDelete),
|
||||
this.batchMarkRoomsForDeletion(toMark)
|
||||
]);
|
||||
|
||||
this.logger.info(
|
||||
`Bulk deletion completed: ${deletedRooms.length} deleted, ${markedRooms.length} marked for deletion`
|
||||
`Deleting room '${roomId}' with policies: withMeeting=${withMeeting}, withRecordings=${withRecordings}`
|
||||
);
|
||||
|
||||
return {
|
||||
deleted: deletedRooms,
|
||||
markedForDeletion: markedRooms
|
||||
};
|
||||
// Check if there's an active meeting in the room and/or if it has recordings associated
|
||||
const room = await this.getMeetRoom(roomId);
|
||||
const hasActiveMeeting = room.status === MeetRoomStatus.ACTIVE_MEETING;
|
||||
const hasRecordings = await this.recordingService.hasRoomRecordings(roomId);
|
||||
|
||||
this.logger.debug(
|
||||
`Room '${roomId}' status: hasActiveMeeting=${hasActiveMeeting}, hasRecordings=${hasRecordings}`
|
||||
);
|
||||
|
||||
const updatedRoom = await this.executeDeletionStrategy(
|
||||
roomId,
|
||||
hasActiveMeeting,
|
||||
hasRecordings,
|
||||
withMeeting,
|
||||
withRecordings
|
||||
);
|
||||
return this.getDeletionResponse(
|
||||
roomId,
|
||||
hasActiveMeeting,
|
||||
hasRecordings,
|
||||
withMeeting,
|
||||
withRecordings,
|
||||
updatedRoom
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error('Error deleting rooms:', error);
|
||||
this.logger.error(`Error deleting room '${roomId}': ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the deletion strategy for a room based on its state and the provided deletion policies.
|
||||
* - Validates the deletion policies (throws if not allowed).
|
||||
* - If no active meeting and no recordings, deletes the room directly.
|
||||
* - If there is an active meeting, sets the meeting end action (DELETE or CLOSE) and optionally ends the meeting.
|
||||
* - If there are recordings and policy is CLOSE, closes the room.
|
||||
* - If force delete is requested, deletes all recordings and the room.
|
||||
*/
|
||||
protected async executeDeletionStrategy(
|
||||
roomId: string,
|
||||
hasActiveMeeting: boolean,
|
||||
hasRecordings: boolean,
|
||||
withMeeting: MeetRoomDeletionPolicyWithMeeting,
|
||||
withRecordings: MeetRoomDeletionPolicyWithRecordings
|
||||
): Promise<MeetRoom | undefined> {
|
||||
// Validate policies first (fail-fast)
|
||||
this.validateDeletionPolicies(roomId, hasActiveMeeting, hasRecordings, withMeeting, withRecordings);
|
||||
|
||||
// No meeting, no recordings: simple deletion
|
||||
if (!hasActiveMeeting && !hasRecordings) {
|
||||
await this.storageService.deleteMeetRooms([roomId]);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const room = await this.getMeetRoom(roomId);
|
||||
|
||||
// Determine actions based on policies
|
||||
const shouldForceEndMeeting = hasActiveMeeting && withMeeting === MeetRoomDeletionPolicyWithMeeting.FORCE;
|
||||
const shouldCloseRoom = hasRecordings && withRecordings === MeetRoomDeletionPolicyWithRecordings.CLOSE;
|
||||
|
||||
if (hasActiveMeeting) {
|
||||
// Set meeting end action (DELETE or CLOSE) depending on recording policy
|
||||
room.meetingEndAction = shouldCloseRoom ? MeetingEndAction.CLOSE : MeetingEndAction.DELETE;
|
||||
await this.storageService.saveMeetRoom(room);
|
||||
|
||||
if (shouldForceEndMeeting) {
|
||||
// Force end meeting by deleting the LiveKit room
|
||||
await this.livekitService.deleteRoom(roomId);
|
||||
}
|
||||
|
||||
return room;
|
||||
}
|
||||
|
||||
if (shouldCloseRoom) {
|
||||
// Close room instead of deleting if recordings exist and policy is CLOSE
|
||||
room.status = MeetRoomStatus.CLOSED;
|
||||
await this.storageService.saveMeetRoom(room);
|
||||
return room;
|
||||
}
|
||||
|
||||
// Force delete: delete room and all recordings
|
||||
await Promise.all([
|
||||
this.recordingService.deleteAllRoomRecordings(roomId),
|
||||
this.storageService.deleteMeetRooms([roomId])
|
||||
]);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates deletion policies and throws appropriate errors for conflicts.
|
||||
*/
|
||||
protected validateDeletionPolicies(
|
||||
roomId: string,
|
||||
hasActiveMeeting: boolean,
|
||||
hasRecordings: boolean,
|
||||
withMeeting: MeetRoomDeletionPolicyWithMeeting,
|
||||
withRecordings: MeetRoomDeletionPolicyWithRecordings
|
||||
) {
|
||||
const baseMessage = `Room '${roomId}'`;
|
||||
|
||||
// Meeting policy validation
|
||||
if (hasActiveMeeting && withMeeting === MeetRoomDeletionPolicyWithMeeting.FAIL) {
|
||||
if (hasRecordings) {
|
||||
throw errorDeletingRoom(
|
||||
MeetRoomDeletionErrorCode.ROOM_WITH_RECORDINGS_HAS_ACTIVE_MEETING,
|
||||
`${baseMessage} with recordings cannot be deleted because it has an active meeting.`
|
||||
);
|
||||
} else {
|
||||
throw errorDeletingRoom(
|
||||
MeetRoomDeletionErrorCode.ROOM_HAS_ACTIVE_MEETING,
|
||||
`${baseMessage} cannot be deleted because it has an active meeting.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Recording policy validation
|
||||
if (hasRecordings && withRecordings === MeetRoomDeletionPolicyWithRecordings.FAIL) {
|
||||
if (hasActiveMeeting) {
|
||||
if (withMeeting === MeetRoomDeletionPolicyWithMeeting.WHEN_MEETING_ENDS) {
|
||||
throw errorDeletingRoom(
|
||||
MeetRoomDeletionErrorCode.ROOM_WITH_ACTIVE_MEETING_HAS_RECORDINGS_CANNOT_SCHEDULE_DELETION,
|
||||
`${baseMessage} with active meeting cannot be scheduled to be deleted because it has recordings.`
|
||||
);
|
||||
} else {
|
||||
throw errorDeletingRoom(
|
||||
MeetRoomDeletionErrorCode.ROOM_WITH_ACTIVE_MEETING_HAS_RECORDINGS,
|
||||
`${baseMessage} with active meeting cannot be deleted because it has recordings.`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw errorDeletingRoom(
|
||||
MeetRoomDeletionErrorCode.ROOM_HAS_RECORDINGS,
|
||||
`${baseMessage} cannot be deleted because it has recordings.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the appropriate response information based on room state and policies.
|
||||
*/
|
||||
private getDeletionResponse(
|
||||
roomId: string,
|
||||
hasActiveMeeting: boolean,
|
||||
hasRecordings: boolean,
|
||||
withMeeting: MeetRoomDeletionPolicyWithMeeting,
|
||||
withRecordings: MeetRoomDeletionPolicyWithRecordings,
|
||||
room?: MeetRoom
|
||||
): {
|
||||
successCode: MeetRoomDeletionSuccessCode;
|
||||
message: string;
|
||||
room?: MeetRoom;
|
||||
} {
|
||||
const baseMessage = `Room '${roomId}'`;
|
||||
|
||||
// No meeting, no recordings
|
||||
if (!hasActiveMeeting && !hasRecordings) {
|
||||
return {
|
||||
successCode: MeetRoomDeletionSuccessCode.ROOM_DELETED,
|
||||
message: `${baseMessage} deleted successfully`
|
||||
};
|
||||
}
|
||||
|
||||
// Has active meeting, no recordings
|
||||
if (hasActiveMeeting && !hasRecordings) {
|
||||
switch (withMeeting) {
|
||||
case MeetRoomDeletionPolicyWithMeeting.FORCE:
|
||||
return {
|
||||
successCode: MeetRoomDeletionSuccessCode.ROOM_WITH_ACTIVE_MEETING_DELETED,
|
||||
message: `${baseMessage} with active meeting deleted successfully`
|
||||
};
|
||||
case MeetRoomDeletionPolicyWithMeeting.WHEN_MEETING_ENDS:
|
||||
return {
|
||||
successCode: MeetRoomDeletionSuccessCode.ROOM_WITH_ACTIVE_MEETING_SCHEDULED_TO_BE_DELETED,
|
||||
message: `${baseMessage} with active meeting scheduled to be deleted when the meeting ends`,
|
||||
room
|
||||
};
|
||||
default:
|
||||
throw internalError(`Unexpected meeting deletion policy: ${withMeeting}`);
|
||||
}
|
||||
}
|
||||
|
||||
// No active meeting, has recordings
|
||||
if (!hasActiveMeeting && hasRecordings) {
|
||||
switch (withRecordings) {
|
||||
case MeetRoomDeletionPolicyWithRecordings.FORCE:
|
||||
return {
|
||||
successCode: MeetRoomDeletionSuccessCode.ROOM_AND_RECORDINGS_DELETED,
|
||||
message: `${baseMessage} and its recordings deleted successfully`
|
||||
};
|
||||
case MeetRoomDeletionPolicyWithRecordings.CLOSE:
|
||||
return {
|
||||
successCode: MeetRoomDeletionSuccessCode.ROOM_CLOSED,
|
||||
message: `${baseMessage} has been closed instead of deleted because it has recordings.`,
|
||||
room
|
||||
};
|
||||
default:
|
||||
throw internalError(`Unexpected recording deletion policy: ${withRecordings}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Has active meeting, has recordings
|
||||
switch (withMeeting) {
|
||||
case MeetRoomDeletionPolicyWithMeeting.FORCE: {
|
||||
switch (withRecordings) {
|
||||
case MeetRoomDeletionPolicyWithRecordings.FORCE:
|
||||
return {
|
||||
successCode: MeetRoomDeletionSuccessCode.ROOM_WITH_ACTIVE_MEETING_AND_RECORDINGS_DELETED,
|
||||
message: `${baseMessage} with active meeting and its recordings deleted successfully`
|
||||
};
|
||||
case MeetRoomDeletionPolicyWithRecordings.CLOSE:
|
||||
return {
|
||||
successCode: MeetRoomDeletionSuccessCode.ROOM_WITH_ACTIVE_MEETING_CLOSED,
|
||||
message: `${baseMessage} with active meeting has been closed instead of deleted because it has recordings.`,
|
||||
room
|
||||
};
|
||||
default:
|
||||
throw internalError(`Unexpected recording deletion policy: ${withRecordings}`);
|
||||
}
|
||||
}
|
||||
|
||||
case MeetRoomDeletionPolicyWithMeeting.WHEN_MEETING_ENDS: {
|
||||
switch (withRecordings) {
|
||||
case MeetRoomDeletionPolicyWithRecordings.FORCE:
|
||||
return {
|
||||
successCode:
|
||||
MeetRoomDeletionSuccessCode.ROOM_WITH_ACTIVE_MEETING_AND_RECORDINGS_SCHEDULED_TO_BE_DELETED,
|
||||
message: `${baseMessage} with active meeting and its recordings scheduled to be deleted when the meeting ends`,
|
||||
room
|
||||
};
|
||||
case MeetRoomDeletionPolicyWithRecordings.CLOSE:
|
||||
return {
|
||||
successCode: MeetRoomDeletionSuccessCode.ROOM_WITH_ACTIVE_MEETING_SCHEDULED_TO_BE_CLOSED,
|
||||
message: `${baseMessage} with active meeting scheduled to be closed when the meeting ends because it has recordings.`,
|
||||
room
|
||||
};
|
||||
default:
|
||||
throw internalError(`Unexpected recording deletion policy: ${withRecordings}`);
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
throw internalError(`Unexpected meeting deletion policy: ${withMeeting}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes multiple rooms in bulk using the deleteMeetRoom method, processing them in batches.
|
||||
*
|
||||
* @param roomIds - Array of room identifiers to be deleted
|
||||
* @param withMeeting - Policy for handling rooms with active meetings
|
||||
* @param withRecordings - Policy for handling rooms with recordings
|
||||
* @param batchSize - Number of rooms to process in each batch (default: 10)
|
||||
* @returns Promise with arrays of successful and failed deletions
|
||||
*/
|
||||
async bulkDeleteMeetRooms(
|
||||
roomIds: string[],
|
||||
withMeeting: MeetRoomDeletionPolicyWithMeeting,
|
||||
withRecordings: MeetRoomDeletionPolicyWithRecordings,
|
||||
batchSize = 10
|
||||
): Promise<{
|
||||
successful: {
|
||||
roomId: string;
|
||||
successCode: MeetRoomDeletionSuccessCode;
|
||||
message: string;
|
||||
room?: MeetRoom;
|
||||
}[];
|
||||
failed: {
|
||||
roomId: string;
|
||||
error: string;
|
||||
message: string;
|
||||
}[];
|
||||
}> {
|
||||
this.logger.info(
|
||||
`Starting bulk deletion of ${roomIds.length} rooms with policies: withMeeting=${withMeeting}, withRecordings=${withRecordings}`
|
||||
);
|
||||
|
||||
const successful: {
|
||||
roomId: string;
|
||||
successCode: MeetRoomDeletionSuccessCode;
|
||||
message: string;
|
||||
room?: MeetRoom;
|
||||
}[] = [];
|
||||
const failed: {
|
||||
roomId: string;
|
||||
error: string;
|
||||
message: string;
|
||||
}[] = [];
|
||||
|
||||
// Process rooms in batches
|
||||
for (let i = 0; i < roomIds.length; i += batchSize) {
|
||||
const batch = roomIds.slice(i, i + batchSize);
|
||||
const batchNumber = Math.floor(i / batchSize) + 1;
|
||||
const totalBatches = Math.ceil(roomIds.length / batchSize);
|
||||
|
||||
this.logger.debug(`Processing batch ${batchNumber}/${totalBatches} with ${batch.length} rooms`);
|
||||
|
||||
// Process all rooms in the current batch concurrently
|
||||
const batchResults = await Promise.all(
|
||||
batch.map(async (roomId) => {
|
||||
try {
|
||||
const result = await this.deleteMeetRoom(roomId, withMeeting, withRecordings);
|
||||
return {
|
||||
roomId,
|
||||
success: true,
|
||||
result
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
roomId,
|
||||
success: false,
|
||||
error
|
||||
};
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Process batch results
|
||||
batchResults.forEach((result) => {
|
||||
const { roomId, success, result: deletionResult, error } = result;
|
||||
|
||||
if (success) {
|
||||
successful.push({
|
||||
roomId,
|
||||
successCode: deletionResult!.successCode,
|
||||
message: deletionResult!.message,
|
||||
room: deletionResult!.room
|
||||
});
|
||||
} else {
|
||||
let meetError: OpenViduMeetError;
|
||||
|
||||
if (error instanceof OpenViduMeetError) {
|
||||
meetError = error;
|
||||
} else {
|
||||
meetError = internalError(`deleting room '${roomId}'`);
|
||||
}
|
||||
|
||||
failed.push({
|
||||
roomId,
|
||||
error: meetError.name,
|
||||
message: meetError.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.logger.debug(`Batch ${batchNumber} completed`);
|
||||
}
|
||||
|
||||
this.logger.info(
|
||||
`Bulk deletion completed: ${successful.length}/${roomIds.length} successful, ${failed.length}/${roomIds.length} failed`
|
||||
);
|
||||
return { successful, failed };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a secret against a room's moderator and speaker secrets and returns the corresponding role.
|
||||
*
|
||||
@ -352,156 +697,6 @@ export class RoomService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Classifies rooms into those that should be deleted immediately vs marked for deletion
|
||||
*/
|
||||
protected async classifyRoomsForDeletion(
|
||||
roomIds: string[],
|
||||
forceDelete: boolean
|
||||
): Promise<{ toDelete: string[]; toMark: string[] }> {
|
||||
this.logger.debug(`Classifying ${roomIds.length} rooms for deletion strategy`);
|
||||
|
||||
// Check all rooms in parallel
|
||||
const classificationResults = await Promise.allSettled(
|
||||
roomIds.map(async (roomId) => {
|
||||
try {
|
||||
const activeMeeting = await this.livekitService.roomExists(roomId);
|
||||
const shouldDelete = forceDelete || !activeMeeting;
|
||||
|
||||
return {
|
||||
roomId,
|
||||
action: shouldDelete ? 'delete' : 'mark'
|
||||
} as const;
|
||||
} catch (error) {
|
||||
this.logger.warn(`Failed to check participants for room ${roomId}: ${error}`);
|
||||
// Default to marking for deletion if we can't check participants
|
||||
return {
|
||||
roomId,
|
||||
action: 'mark'
|
||||
} as const;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Group results
|
||||
const toDelete: string[] = [];
|
||||
const toMark: string[] = [];
|
||||
|
||||
classificationResults.forEach((result, index) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
if (result.value.action === 'delete') {
|
||||
toDelete.push(result.value.roomId);
|
||||
} else {
|
||||
toMark.push(result.value.roomId);
|
||||
}
|
||||
} else {
|
||||
this.logger.warn(`Failed to classify room ${roomIds[index]}: ${result.reason}`);
|
||||
// Default to marking for deletion
|
||||
toMark.push(roomIds[index]);
|
||||
}
|
||||
});
|
||||
|
||||
this.logger.debug(`Classification complete: ${toDelete.length} to delete, ${toMark.length} to mark`);
|
||||
return { toDelete, toMark };
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs batch deletion of rooms that can be deleted immediately
|
||||
*/
|
||||
protected async batchDeleteRooms(roomIds: string[]): Promise<string[]> {
|
||||
if (roomIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
this.logger.info(`Batch deleting ${roomIds.length} rooms`);
|
||||
|
||||
try {
|
||||
// Check which rooms have an active LiveKit room (active meeting)
|
||||
const activeRoomChecks = await Promise.all(
|
||||
roomIds.map(async (roomId) => ({
|
||||
roomId,
|
||||
activeMeeting: await this.livekitService.roomExists(roomId)
|
||||
}))
|
||||
);
|
||||
|
||||
const withActiveMeeting = activeRoomChecks.filter((r) => r.activeMeeting).map((r) => r.roomId);
|
||||
const withoutActiveMeeting = activeRoomChecks.filter((r) => !r.activeMeeting).map((r) => r.roomId);
|
||||
|
||||
// Mark all rooms with active meetings for deletion (in batch)
|
||||
// This must be done before deleting the LiveKit rooms to ensure
|
||||
// the rooms are marked when 'room_finished' webhook is sent
|
||||
if (withActiveMeeting.length > 0) {
|
||||
await this.batchMarkRoomsForDeletion(withActiveMeeting);
|
||||
}
|
||||
|
||||
// Delete all LiveKit rooms for rooms with active meetings (in batch)
|
||||
const livekitDeletePromise =
|
||||
withActiveMeeting.length > 0
|
||||
? this.livekitService.batchDeleteRooms(withActiveMeeting)
|
||||
: Promise.resolve();
|
||||
|
||||
// Delete Meet rooms that do not have an active meeting (in batch)
|
||||
const meetRoomsDeletePromise =
|
||||
withoutActiveMeeting.length > 0
|
||||
? this.storageService.deleteMeetRooms(withoutActiveMeeting)
|
||||
: Promise.resolve();
|
||||
|
||||
await Promise.all([livekitDeletePromise, meetRoomsDeletePromise]);
|
||||
return roomIds;
|
||||
} catch (error) {
|
||||
this.logger.error(`Batch deletion failed for rooms: ${roomIds.join(', ')}`, error);
|
||||
throw internalError('Failed to delete rooms');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks multiple rooms for deletion in batch
|
||||
*/
|
||||
private async batchMarkRoomsForDeletion(roomIds: string[]): Promise<string[]> {
|
||||
if (roomIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
this.logger.info(`Batch marking ${roomIds.length} rooms for deletion`);
|
||||
|
||||
try {
|
||||
// Get all rooms in parallel
|
||||
const roomResults = await Promise.allSettled(
|
||||
roomIds.map((roomId) => this.storageService.getMeetRoom(roomId))
|
||||
);
|
||||
|
||||
// Prepare rooms for batch update
|
||||
const roomsToUpdate: { roomId: string; room: MeetRoom }[] = [];
|
||||
const successfulRoomIds: string[] = [];
|
||||
|
||||
roomResults.forEach((result, index) => {
|
||||
const roomId = roomIds[index];
|
||||
|
||||
if (result.status === 'fulfilled' && result.value) {
|
||||
const room = result.value;
|
||||
room.meetingEndAction = MeetingEndAction.DELETE;
|
||||
roomsToUpdate.push({ roomId, room });
|
||||
successfulRoomIds.push(roomId);
|
||||
} else {
|
||||
this.logger.warn(
|
||||
`Failed to get room ${roomId} for marking: ${result.status === 'rejected' ? result.reason : 'Room not found'}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Batch save all updated rooms
|
||||
if (roomsToUpdate.length > 0) {
|
||||
await Promise.allSettled(roomsToUpdate.map(({ room }) => this.storageService.saveMeetRoom(room)));
|
||||
}
|
||||
|
||||
this.logger.info(`Successfully marked ${successfulRoomIds.length} rooms for deletion`);
|
||||
return successfulRoomIds;
|
||||
} catch (error) {
|
||||
this.logger.error(`Batch marking failed for rooms: ${roomIds.join(', ')}`, error);
|
||||
throw internalError('Failed to mark rooms for deletion');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gracefully deletes expired rooms.
|
||||
*
|
||||
|
||||
@ -68,3 +68,23 @@ export type MeetRoomFilters = {
|
||||
roomName?: string;
|
||||
fields?: string;
|
||||
};
|
||||
|
||||
export const enum MeetRoomDeletionSuccessCode {
|
||||
ROOM_DELETED = 'room_deleted',
|
||||
ROOM_WITH_ACTIVE_MEETING_DELETED = 'room_with_active_meeting_deleted',
|
||||
ROOM_WITH_ACTIVE_MEETING_SCHEDULED_TO_BE_DELETED = 'room_with_active_meeting_scheduled_to_be_deleted',
|
||||
ROOM_AND_RECORDINGS_DELETED = 'room_and_recordings_deleted',
|
||||
ROOM_CLOSED = 'room_closed',
|
||||
ROOM_WITH_ACTIVE_MEETING_AND_RECORDINGS_DELETED = 'room_with_active_meeting_and_recordings_deleted',
|
||||
ROOM_WITH_ACTIVE_MEETING_CLOSED = 'room_with_active_meeting_closed',
|
||||
ROOM_WITH_ACTIVE_MEETING_AND_RECORDINGS_SCHEDULED_TO_BE_DELETED = 'room_with_active_meeting_and_recordings_scheduled_to_be_deleted',
|
||||
ROOM_WITH_ACTIVE_MEETING_SCHEDULED_TO_BE_CLOSED = 'room_with_active_meeting_scheduled_to_be_closed'
|
||||
}
|
||||
|
||||
export const enum MeetRoomDeletionErrorCode {
|
||||
ROOM_HAS_ACTIVE_MEETING = 'room_has_active_meeting',
|
||||
ROOM_HAS_RECORDINGS = 'room_has_recordings',
|
||||
ROOM_WITH_ACTIVE_MEETING_HAS_RECORDINGS = 'room_with_active_meeting_has_recordings',
|
||||
ROOM_WITH_ACTIVE_MEETING_HAS_RECORDINGS_CANNOT_SCHEDULE_DELETION = 'room_with_active_meeting_has_recordings_cannot_schedule_deletion',
|
||||
ROOM_WITH_RECORDINGS_HAS_ACTIVE_MEETING = 'room_with_recordings_has_active_meeting'
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user