backend: Add race conditions tests for recording API to ensure correct handling of concurrent operations
This commit is contained in:
parent
7376f1dc04
commit
cc7e86c006
236
backend/tests/integration/api/recordings/race-conditions.test.ts
Normal file
236
backend/tests/integration/api/recordings/race-conditions.test.ts
Normal file
@ -0,0 +1,236 @@
|
||||
import { describe, it, expect, beforeAll, afterAll, afterEach, jest } from '@jest/globals';
|
||||
|
||||
import { eventController } from '../../../helpers/event-controller';
|
||||
import {
|
||||
startRecording,
|
||||
sleep,
|
||||
deleteAllRecordings,
|
||||
deleteAllRooms,
|
||||
startTestServer,
|
||||
stopRecording,
|
||||
stopAllRecordings,
|
||||
getRecordingMedia,
|
||||
deleteRecording,
|
||||
bulkDeleteRecordings
|
||||
} from '../../../helpers/request-helpers';
|
||||
|
||||
import {
|
||||
setupMultiRecordingsTestContext,
|
||||
setupMultiRoomTestContext,
|
||||
TestContext
|
||||
} from '../../../helpers/test-scenarios';
|
||||
import {
|
||||
expectValidStartRecordingResponse,
|
||||
expectValidStopRecordingResponse
|
||||
} from '../../../helpers/assertion-helpers';
|
||||
import { RecordingService, TaskSchedulerService } from '../../../../src/services';
|
||||
import { container } from '../../../../src/config/dependency-injector.config';
|
||||
|
||||
describe('Recording API Race Conditions Tests', () => {
|
||||
let context: TestContext | null = null;
|
||||
let recordingService: RecordingService;
|
||||
let taskSchedulerService: TaskSchedulerService;
|
||||
beforeAll(async () => {
|
||||
startTestServer();
|
||||
recordingService = container.get(RecordingService);
|
||||
taskSchedulerService = container.get(TaskSchedulerService);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
const moderatorCookie = context?.getRoomByIndex(0)?.moderatorCookie;
|
||||
|
||||
if (moderatorCookie) {
|
||||
await stopAllRecordings(moderatorCookie);
|
||||
}
|
||||
|
||||
eventController.reset();
|
||||
await Promise.all([deleteAllRecordings(), deleteAllRooms()]);
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterAll(async () => {});
|
||||
|
||||
it('should start recordings concurrently in two rooms and stop one before RECORDING_ACTIVE is received for the other', async () => {
|
||||
context = await setupMultiRoomTestContext(2, true);
|
||||
const roomDataA = context.getRoomByIndex(0);
|
||||
const roomDataB = context.getRoomByIndex(1);
|
||||
|
||||
eventController.initialize();
|
||||
eventController.pauseEventsForRoom(roomDataA!.room.roomId);
|
||||
|
||||
const recordingPromiseA = startRecording(roomDataA!.room.roomId, roomDataA!.moderatorCookie);
|
||||
|
||||
// Brief delay to ensure both recordings start in the right order
|
||||
await sleep('1s');
|
||||
|
||||
// Step 2: Start recording in roomB (this will complete quickly)
|
||||
const recordingResponseB = await startRecording(roomDataB!.room.roomId, roomDataB!.moderatorCookie);
|
||||
expectValidStartRecordingResponse(recordingResponseB, roomDataB!.room.roomId);
|
||||
const recordingIdB = recordingResponseB.body.recordingId;
|
||||
|
||||
// Step 3: Stop recording in roomB while roomA is still waiting for its event
|
||||
const stopResponseB = await stopRecording(recordingIdB, roomDataB!.moderatorCookie);
|
||||
expectValidStopRecordingResponse(stopResponseB, recordingIdB, roomDataB!.room.roomId);
|
||||
|
||||
eventController.releaseEventsForRoom(roomDataA!.room.roomId);
|
||||
|
||||
const recordingResponseA = (await Promise.race([
|
||||
recordingPromiseA,
|
||||
new Promise((_, reject) => setTimeout(() => reject(new Error('Recording A timed out')), 10000))
|
||||
])) as Response;
|
||||
|
||||
// If we get here, the recording in roomA completed despite roomB being stopped
|
||||
expect(recordingResponseA.status).toBe(201);
|
||||
});
|
||||
|
||||
it('should handle simultaneous recordings in different rooms correctly', async () => {
|
||||
context = await setupMultiRoomTestContext(5, true);
|
||||
|
||||
const roomDataList = Array.from({ length: 5 }, (_, index) => context!.getRoomByIndex(index)!);
|
||||
|
||||
const startResponses = await Promise.all(
|
||||
roomDataList.map((roomData) => startRecording(roomData.room.roomId, roomData.moderatorCookie))
|
||||
);
|
||||
|
||||
startResponses.forEach((response, index) => {
|
||||
expectValidStartRecordingResponse(response, roomDataList[index].room.roomId);
|
||||
});
|
||||
|
||||
const recordingIds = startResponses.map((res) => res.body.recordingId);
|
||||
|
||||
const stopResponses = await Promise.all(
|
||||
recordingIds.map((recordingId, index) => stopRecording(recordingId, roomDataList[index].moderatorCookie))
|
||||
);
|
||||
|
||||
stopResponses.forEach((response, index) => {
|
||||
expectValidStopRecordingResponse(response, recordingIds[index], roomDataList[index].room.roomId);
|
||||
});
|
||||
});
|
||||
|
||||
it('should stop multiple recordings in parallel', async () => {
|
||||
context = await setupMultiRoomTestContext(2, true);
|
||||
const roomDataA = context.getRoomByIndex(0);
|
||||
const roomDataB = context.getRoomByIndex(1);
|
||||
const responseA = await startRecording(roomDataA!.room.roomId, roomDataA!.moderatorCookie);
|
||||
const responseB = await startRecording(roomDataB!.room.roomId, roomDataB!.moderatorCookie);
|
||||
const recordingIdA = responseA.body.recordingId;
|
||||
const recordingIdB = responseB.body.recordingId;
|
||||
|
||||
const [stopResponseA, stopResponseB] = await Promise.all([
|
||||
stopRecording(recordingIdA, roomDataA!.moderatorCookie),
|
||||
stopRecording(recordingIdB, roomDataB!.moderatorCookie)
|
||||
]);
|
||||
expectValidStopRecordingResponse(stopResponseA, recordingIdA, roomDataA!.room.roomId);
|
||||
expectValidStopRecordingResponse(stopResponseB, recordingIdB, roomDataB!.room.roomId);
|
||||
});
|
||||
|
||||
it('should prevent multiple recording starts in the same room', async () => {
|
||||
context = await setupMultiRoomTestContext(1, true);
|
||||
const roomData = context.getRoomByIndex(0)!;
|
||||
|
||||
const [firstRecordingResponse, secondRecordingResponse] = await Promise.all([
|
||||
startRecording(roomData.room.roomId, roomData.moderatorCookie),
|
||||
startRecording(roomData.room.roomId, roomData.moderatorCookie)
|
||||
]);
|
||||
|
||||
console.log('First recording response:', firstRecordingResponse.body);
|
||||
console.log('Second recording response:', secondRecordingResponse.body);
|
||||
|
||||
// One of the recordings responses should be successful and the other should fail
|
||||
const oneShouldBeSuccessful = firstRecordingResponse.status === 201 || secondRecordingResponse.status === 201;
|
||||
const oneShouldBeFailed = firstRecordingResponse.status === 409 || secondRecordingResponse.status === 409;
|
||||
expect(oneShouldBeSuccessful).toBe(true);
|
||||
expect(oneShouldBeFailed).toBe(true);
|
||||
|
||||
if (firstRecordingResponse.status === 201) {
|
||||
expectValidStartRecordingResponse(firstRecordingResponse, roomData.room.roomId);
|
||||
} else {
|
||||
expectValidStartRecordingResponse(secondRecordingResponse, roomData.room.roomId);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle race condition between stopping recording and garbage collection', async () => {
|
||||
context = await setupMultiRoomTestContext(1, true);
|
||||
const roomData = context.getRoomByIndex(0)!;
|
||||
|
||||
const gcSpy = jest.spyOn(recordingService as any, 'performRecordingLocksGarbageCollection');
|
||||
|
||||
const startResponse = await startRecording(roomData.room.roomId, roomData.moderatorCookie);
|
||||
expectValidStartRecordingResponse(startResponse, roomData.room.roomId);
|
||||
const recordingId = startResponse.body.recordingId;
|
||||
|
||||
// Execute garbage collection while stopping the recording
|
||||
const stopPromise = stopRecording(recordingId, roomData.moderatorCookie);
|
||||
const gcPromise = recordingService['performRecordingLocksGarbageCollection']();
|
||||
|
||||
// Both operations should complete
|
||||
|
||||
await Promise.all([stopPromise, gcPromise]);
|
||||
|
||||
// Check that the recording was stopped successfully
|
||||
const stopResponse = await stopPromise;
|
||||
expectValidStopRecordingResponse(stopResponse, recordingId, roomData.room.roomId);
|
||||
|
||||
// Check that garbage collection was called
|
||||
expect(gcSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle race condition between streaming and deleting recording', async () => {
|
||||
const testContext = await setupMultiRecordingsTestContext(1, 1, 1, '4s');
|
||||
const roomData = testContext.getRoomByIndex(0)!;
|
||||
const recordingId = roomData.recordingId!;
|
||||
|
||||
// Start streaming and deleting the recording at the same time
|
||||
const streamPromise = getRecordingMedia(recordingId);
|
||||
const deletePromise = deleteRecording(recordingId);
|
||||
|
||||
// Both operations should complete but one should fail
|
||||
const [streamResponse, deleteResponse] = await Promise.allSettled([streamPromise, deletePromise]);
|
||||
|
||||
// One of the operations should be successful and the other should fail
|
||||
const streamSuccessful =
|
||||
streamResponse.status === 'fulfilled' &&
|
||||
(streamResponse.value.status === 200 || streamResponse.value.status === 206);
|
||||
|
||||
const deleteSuccessful = deleteResponse.status === 'fulfilled' && deleteResponse.value.status === 204;
|
||||
|
||||
expect(streamSuccessful || deleteSuccessful).toBe(true);
|
||||
|
||||
// If both operations are successful, it means that the logic has a problem
|
||||
expect(streamSuccessful && deleteSuccessful).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle race condition between bulk delete and recording start', async () => {
|
||||
context = await setupMultiRoomTestContext(3, true);
|
||||
|
||||
// Start recordings in the first two rooms
|
||||
const room1 = context.getRoomByIndex(0)!;
|
||||
const room2 = context.getRoomByIndex(1)!;
|
||||
const room3 = context.getRoomByIndex(2)!;
|
||||
|
||||
const start1 = await startRecording(room1.room.roomId, room1.moderatorCookie);
|
||||
const start2 = await startRecording(room2.room.roomId, room2.moderatorCookie);
|
||||
|
||||
const recordingId1 = start1.body.recordingId;
|
||||
const recordingId2 = start2.body.recordingId;
|
||||
|
||||
await stopRecording(recordingId1, room1.moderatorCookie);
|
||||
await stopRecording(recordingId2, room2.moderatorCookie);
|
||||
|
||||
// Bulk delete the recordings while starting a new one
|
||||
const bulkDeletePromise = bulkDeleteRecordings([recordingId1, recordingId2]);
|
||||
const startNewRecordingPromise = startRecording(room3.room.roomId, room3.moderatorCookie);
|
||||
|
||||
// Both operations should complete successfully
|
||||
const [bulkDeleteResult, newRecordingResult] = await Promise.all([bulkDeletePromise, startNewRecordingPromise]);
|
||||
|
||||
expect(bulkDeleteResult.status).toBe(204);
|
||||
expect(bulkDeleteResult.body).toEqual({});
|
||||
|
||||
// Check that the new recording started successfully
|
||||
expectValidStartRecordingResponse(newRecordingResult, room3.room.roomId);
|
||||
|
||||
const newStopResponse = await stopRecording(newRecordingResult.body.recordingId, room3.moderatorCookie);
|
||||
expectValidStopRecordingResponse(newStopResponse, newRecordingResult.body.recordingId, room3.room.roomId);
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user