Skip to content

Commit 24d5664

Browse files
committed
rtc: shield the aborted-connect cleanup from a cancelled disconnect
gather() cancels its children when it is cancelled, so a caller who bounds disconnect() with a timeout, or abandons it during shutdown, cancelled the cleanup task that answers the FFI's wait for ReadyForRoomEvent. The wait then timed out and panicked, and the panic handler kills the process, which is the failure this path was added to prevent. The test fails without the shield.
1 parent 6e120fb commit 24d5664

2 files changed

Lines changed: 44 additions & 1 deletion

File tree

‎livekit-rtc/livekit/rtc/room.py‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -741,7 +741,14 @@ async def disconnect(
741741
if self._aborted_connect_tasks:
742742
# a cancelled connect may still be closing a room the FFI server opened.
743743
# wait for it so disconnect() leaves nothing behind.
744-
await asyncio.gather(*tuple(self._aborted_connect_tasks), return_exceptions=True)
744+
#
745+
# shielded, because gather() cancels its children when it is cancelled.
746+
# a caller who gives up on disconnect() would otherwise cancel the very
747+
# cleanup that answers the FFI's wait, leaving it to time out and panic,
748+
# which is the failure this path exists to prevent.
749+
await asyncio.shield(
750+
asyncio.gather(*tuple(self._aborted_connect_tasks), return_exceptions=True)
751+
)
745752

746753
if not self.isconnected():
747754
return

‎livekit-rtc/tests/test_connect_cancellation.py‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,39 @@ async def test_cancelled_connect_leaves_no_pending_work_when_the_server_errors(
116116
# there is no room to close, so nothing follows the connect
117117
assert [req.WhichOneof("message") for req in requests] == ["connect"]
118118
assert len(FfiClient.instance.queue._subscribers) == subscribers_before
119+
120+
121+
async def test_a_cancelled_disconnect_still_lets_the_cleanup_finish(
122+
monkeypatch: pytest.MonkeyPatch,
123+
) -> None:
124+
"""Giving up on disconnect() must not cancel the cleanup it is waiting on.
125+
126+
gather() cancels its children when it is cancelled, so a caller who bounds
127+
disconnect() with a timeout, or abandons it on shutdown, would cancel the
128+
task that answers the FFI's wait for ReadyForRoomEvent. The wait then times
129+
out, the FFI panics, and the panic handler kills the process: the exact
130+
failure the rest of this file is about, reintroduced one layer up.
131+
"""
132+
requests = _install_fake_ffi(monkeypatch)
133+
134+
room = rtc.Room()
135+
task = asyncio.create_task(room.connect("ws://localhost:7880", "token"))
136+
await wait_until(lambda: bool(requests), message="connect request never issued")
137+
138+
task.cancel()
139+
with pytest.raises(asyncio.CancelledError):
140+
await task
141+
142+
# the cleanup is now parked on the connect callback, which has not arrived
143+
closing = asyncio.create_task(room.disconnect())
144+
await asyncio.sleep(0)
145+
closing.cancel()
146+
with pytest.raises(asyncio.CancelledError):
147+
await closing
148+
149+
_deliver_connect_callback()
150+
await wait_until(
151+
lambda: any(r.WhichOneof("message") == "ready_for_room_event" for r in requests),
152+
message="the cancelled disconnect took the cleanup down with it",
153+
)
154+
assert requests[1].ready_for_room_event.room_handle == ROOM_HANDLE

0 commit comments

Comments
 (0)