Skip to content

Commit 0af30ec

Browse files
committed
✨(backend) notify participants only if the room exists
Improves sendData reliability by preventing execution when the room doesn’t exist. This change addresses errors in staging and production where waiting participants arrive before the room owner creates the room. In remote environments, the LiveKit Python SDK doesn’t return a clean Twirp error when the room is missing; instead of a proper "server unknown" response, it raises a ContentTypeError, as if the LiveKit server weren’t responding with a JSON payload, even though the code specifies otherwise. While the issue cannot be reproduced locally, this should help mitigate production errors. Part of a broader effort to enhance data transmission reliability. Importantly, a participant requesting entry to a room before the owner arrives should not be considered an exception.
1 parent 33e0174 commit 0af30ec

File tree

2 files changed

+76
-1
lines changed

2 files changed

+76
-1
lines changed

src/backend/core/services/lobby.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@
1212
from django.core.cache import cache
1313

1414
from asgiref.sync import async_to_sync
15-
from livekit.api import LiveKitAPI, SendDataRequest, TwirpError # pylint: disable=E0611
15+
from livekit.api import ( # pylint: disable=E0611
16+
ListRoomsRequest,
17+
LiveKitAPI,
18+
SendDataRequest,
19+
TwirpError,
20+
)
1621

1722
from core import models, utils
1823

@@ -343,7 +348,18 @@ async def notify_participants(self, room_id: UUID):
343348
}
344349

345350
lkapi = LiveKitAPI(**settings.LIVEKIT_CONFIGURATION)
351+
346352
try:
353+
room_response = await lkapi.room.list_rooms(
354+
ListRoomsRequest(
355+
names=[str(room_id)],
356+
)
357+
)
358+
359+
# Check if the room exists
360+
if not room_response.rooms:
361+
return
362+
347363
await lkapi.room.send_data(
348364
SendDataRequest(
349365
room=str(room_id),

src/backend/core/tests/services/test_lobby.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -776,6 +776,43 @@ def test_update_participant_status_success(mock_cache, lobby_service, participan
776776
lobby_service._get_cache_key.assert_called_once_with(room.id, participant_id)
777777

778778

779+
@mock.patch("core.services.lobby.LiveKitAPI")
780+
def test_notify_participants_success_no_room(mock_livekit_api, lobby_service):
781+
"""Test the notify_participants method when the LiveKit room doesn't exist yet."""
782+
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED)
783+
784+
# Set up the mock LiveKitAPI and its behavior
785+
mock_api_instance = mock.Mock()
786+
mock_api_instance.room = mock.Mock()
787+
mock_api_instance.room.send_data = mock.AsyncMock()
788+
789+
# Create a proper response object with an empty rooms list
790+
class MockResponse:
791+
"""LiveKit API response mock with empty rooms list."""
792+
793+
rooms = []
794+
795+
mock_api_instance.room.list_rooms = mock.AsyncMock(return_value=MockResponse())
796+
mock_api_instance.aclose = mock.AsyncMock()
797+
mock_livekit_api.return_value = mock_api_instance
798+
799+
# Act
800+
lobby_service.notify_participants(room.id)
801+
802+
# Assert
803+
# Verify the API was initialized with correct configuration
804+
mock_livekit_api.assert_called_once_with(**settings.LIVEKIT_CONFIGURATION)
805+
806+
# Verify that the service checked for existing rooms
807+
mock_api_instance.room.list_rooms.assert_called_once()
808+
809+
# Verify the send_data method was not called since no room exists
810+
mock_api_instance.room.send_data.assert_not_called()
811+
812+
# Verify the connection was properly closed
813+
mock_api_instance.aclose.assert_called_once()
814+
815+
779816
@mock.patch("core.services.lobby.LiveKitAPI")
780817
def test_notify_participants_success(mock_livekit_api, lobby_service):
781818
"""Test successful participant notification."""
@@ -784,6 +821,14 @@ def test_notify_participants_success(mock_livekit_api, lobby_service):
784821
mock_api_instance = mock.Mock()
785822
mock_api_instance.room = mock.Mock()
786823
mock_api_instance.room.send_data = mock.AsyncMock()
824+
825+
class MockResponse:
826+
"""LiveKit API response mock with non-empty rooms list."""
827+
828+
rooms = ["room-1"]
829+
830+
mock_api_instance.room.list_rooms = mock.AsyncMock(return_value=MockResponse())
831+
787832
mock_api_instance.aclose = mock.AsyncMock()
788833
mock_livekit_api.return_value = mock_api_instance
789834

@@ -793,6 +838,9 @@ def test_notify_participants_success(mock_livekit_api, lobby_service):
793838
# Verify the API was called correctly
794839
mock_livekit_api.assert_called_once_with(**settings.LIVEKIT_CONFIGURATION)
795840

841+
# Verify that the service checked for existing rooms
842+
mock_api_instance.room.list_rooms.assert_called_once()
843+
796844
# Verify the send_data method was called
797845
mock_api_instance.room.send_data.assert_called_once()
798846
send_data_request = mock_api_instance.room.send_data.call_args[0][0]
@@ -817,6 +865,14 @@ def test_notify_participants_error(mock_livekit_api, lobby_service):
817865
mock_api_instance.room.send_data = mock.AsyncMock(
818866
side_effect=TwirpError(msg="test error", code=123)
819867
)
868+
869+
class MockResponse:
870+
"""LiveKit API response mock with non-empty rooms list."""
871+
872+
rooms = ["room-1"]
873+
874+
mock_api_instance.room.list_rooms = mock.AsyncMock(return_value=MockResponse())
875+
820876
mock_api_instance.aclose = mock.AsyncMock()
821877
mock_livekit_api.return_value = mock_api_instance
822878

@@ -829,6 +885,9 @@ def test_notify_participants_error(mock_livekit_api, lobby_service):
829885
# Verify the API was called correctly
830886
mock_livekit_api.assert_called_once_with(**settings.LIVEKIT_CONFIGURATION)
831887

888+
# Verify that the service checked for existing rooms
889+
mock_api_instance.room.list_rooms.assert_called_once()
890+
832891
# Verify send_data was called
833892
mock_api_instance.room.send_data.assert_called_once()
834893

0 commit comments

Comments
 (0)