backend: enhance bulk room deletion logic to ensure proper handling of active meetings and update tests for consistency

This commit is contained in:
juancarmore 2025-06-20 15:00:05 +02:00
parent 3ef546488f
commit fd7260863f
3 changed files with 41 additions and 12 deletions

View File

@ -193,7 +193,8 @@ export class RoomService {
/** /**
* Deletes multiple rooms in bulk, with the option to force delete or gracefully handle rooms with active participants. * 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" * For rooms with participants, when `forceDelete` is false, the method performs a "graceful deletion"
* by marking the room as deleted without disrupting active sessions. * 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.
* *
* @param roomIds - Array of room identifiers to be deleted * @param roomIds - Array of room identifiers to be deleted
* @param forceDelete - If true, deletes rooms even if they have active participants. * @param forceDelete - If true, deletes rooms even if they have active participants.
@ -210,7 +211,6 @@ export class RoomService {
const { toDelete, toMark } = await this.classifyRoomsForDeletion(roomIds, forceDelete); const { toDelete, toMark } = await this.classifyRoomsForDeletion(roomIds, forceDelete);
// Process each group in parallel // Process each group in parallel
const [deletedRooms, markedRooms] = await Promise.all([ const [deletedRooms, markedRooms] = await Promise.all([
this.batchDeleteRooms(toDelete), this.batchDeleteRooms(toDelete),
this.batchMarkRoomsForDeletion(toMark) this.batchMarkRoomsForDeletion(toMark)
@ -368,8 +368,8 @@ export class RoomService {
const classificationResults = await Promise.allSettled( const classificationResults = await Promise.allSettled(
roomIds.map(async (roomId) => { roomIds.map(async (roomId) => {
try { try {
const hasParticipants = await this.livekitService.roomHasParticipants(roomId); const activeMeeting = await this.livekitService.roomExists(roomId);
const shouldDelete = forceDelete || !hasParticipants; const shouldDelete = forceDelete || !activeMeeting;
return { return {
roomId, roomId,
@ -419,11 +419,37 @@ export class RoomService {
this.logger.info(`Batch deleting ${roomIds.length} rooms`); this.logger.info(`Batch deleting ${roomIds.length} rooms`);
try { try {
await Promise.all([ // Check which rooms have an active LiveKit room (active meeting)
this.storageService.deleteMeetRooms(roomIds), const activeRoomChecks = await Promise.all(
this.livekitService.batchDeleteRooms(roomIds) 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; return roomIds;
} catch (error) { } catch (error) {
this.logger.error(`Batch deletion failed for rooms: ${roomIds.join(', ')}`, error); this.logger.error(`Batch deletion failed for rooms: ${roomIds.join(', ')}`, error);

View File

@ -6,6 +6,7 @@ import {
disconnectFakeParticipants, disconnectFakeParticipants,
getRoom, getRoom,
joinFakeParticipant, joinFakeParticipant,
sleep,
startTestServer startTestServer
} from '../../../helpers/request-helpers.js'; } from '../../../helpers/request-helpers.js';
@ -157,25 +158,26 @@ describe('Room API Tests', () => {
}); });
it('should delete rooms when force=true and participants exist', async () => { it('should delete rooms when force=true and participants exist', async () => {
// Create a test room // Create test rooms
const [room1, room2] = await Promise.all([ const [room1, room2] = await Promise.all([
createRoom({ roomIdPrefix: 'test-bulk-1' }), createRoom({ roomIdPrefix: 'test-bulk-1' }),
createRoom({ roomIdPrefix: 'test-bulk-2' }) createRoom({ roomIdPrefix: 'test-bulk-2' })
]); ]);
// Join a participant to the room // Join a participant to the rooms
await Promise.all([ await Promise.all([
joinFakeParticipant(room1.roomId, 'test-participant-1'), joinFakeParticipant(room1.roomId, 'test-participant-1'),
joinFakeParticipant(room2.roomId, 'test-participant-2') joinFakeParticipant(room2.roomId, 'test-participant-2')
]); ]);
// Attempt to delete the room with force=false // Attempt to delete the rooms with force=false
const response = await bulkDeleteRooms([room1.roomId, room2.roomId], true); const response = await bulkDeleteRooms([room1.roomId, room2.roomId], true);
expect(response.status).toBe(204); expect(response.status).toBe(204);
expect(response.body).toStrictEqual({}); expect(response.body).toStrictEqual({});
// Verify that the room is deleted // Verify that the rooms are deleted
await sleep('1s'); // Wait a bit for the meetings to be closed and the rooms deleted
const deletedRoom1 = await getRoom(room1.roomId); const deletedRoom1 = await getRoom(room1.roomId);
const deletedRoom2 = await getRoom(room2.roomId); const deletedRoom2 = await getRoom(room2.roomId);
expect(deletedRoom1.status).toBe(404); expect(deletedRoom1.status).toBe(404);

View File

@ -176,6 +176,7 @@ describe('Room API Tests', () => {
expect(response.status).toBe(204); expect(response.status).toBe(204);
// Try to retrieve the room again // Try to retrieve the room again
await sleep('1s'); // Wait a bit for the meeting to be closed and the room deleted
const responseAfterDelete = await getRoom(roomId); const responseAfterDelete = await getRoom(roomId);
expect(responseAfterDelete.status).toBe(404); expect(responseAfterDelete.status).toBe(404);
}); });