fix(room_io): disconnect locally before server delete to suppress spurious ERROR logs#6252
fix(room_io): disconnect locally before server delete to suppress spurious ERROR logs#6252tsushanth wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
π© Pre-existing dead code: _close_session_atask is never assigned
The field self._close_session_atask is initialized to None at line 84 and used as a guard at line 406 (and not self._close_session_atask), but it is never assigned anywhere else in the codebase. This means the guard is always True (never prevents the close). This appears to be a leftover from a previous refactor and is not related to this PR's changes.
(Refers to line 84)
Was this helpful? React with π or π to provide feedback.
|
Thanks for creating the PR! The exception handling issue from Devin looks valid, could you fix that? type checking is failing too. |
chenghao-mou
left a comment
There was a problem hiding this comment.
Happy to merge it once the exception handling comment and CI error are fixed
|
Fixed both items:
|
| # "publisher data channel '_reliable' closed unexpectedly" | ||
| # See https://github.com/livekit/agents/issues/6250 | ||
| try: | ||
| await self._room.disconnect() |
There was a problem hiding this comment.
maybe move this to job_ctx.delete_room?
|
The exception handling is already in place β |
Disconnect locally before issuing the API delete so the rust-sdk's connection-closed flag is set before the server closes the publisher data channels. Without this the channels are torn down from the remote side while the local session is still "connected", triggering spurious ERROR-level logs: "publisher data channel '_reliable' closed unexpectedly" Fixes livekit#6250
f18c338 to
128167c
Compare
| try: | ||
| await self._room.disconnect() | ||
| except Exception: | ||
| logger.exception( | ||
| "error disconnecting room before delete; proceeding with delete" | ||
| ) | ||
| try: | ||
| await self.api.room.delete_room( | ||
| api.DeleteRoomRequest(room=room_name or self._room.name) |
There was a problem hiding this comment.
π΄ Room deletion uses the room name read after disconnecting, which may be empty
The room name is read (self._room.name at livekit-agents/livekit/agents/job.py:647) after the local disconnect has already been issued, so if the SDK clears the name on disconnect the server-side delete request is sent with an empty room identifier.
Impact: Callers that invoke delete_room() without an explicit room_name (e.g. the end-call tool) silently fail to delete the room, leaving it open.
Mechanism: room name captured after disconnect instead of before
The new code at lines 639-644 calls await self._room.disconnect() before the delete-room API call at line 646-647. When room_name is None (the default), the fallback expression room_name or self._room.name reads self._room.name after the room has been disconnected.
In the LiveKit Python SDK, Room.name is backed by the Rust FFI handle's internal state. After disconnect(), this state is typically cleared, causing name to return an empty string. Since None or "" evaluates to "", the DeleteRoomRequest is issued with room="", which will either fail or be a no-op on the server.
Callers that hit this path:
livekit-agents/livekit/agents/beta/tools/end_call.py:127:await job_ctx.delete_room()β noroom_nameargument- Example code like
examples/voice_agents/error_callback.py:82:ctx.delete_room()
Callers that pass room_name explicitly (e.g. room_io.py:482, warm_transfer.py:252) are unaffected.
The fix is to capture the room name before disconnecting:
name = room_name or self._room.name
await self._room.disconnect()
...
await self.api.room.delete_room(api.DeleteRoomRequest(room=name))| try: | |
| await self._room.disconnect() | |
| except Exception: | |
| logger.exception( | |
| "error disconnecting room before delete; proceeding with delete" | |
| ) | |
| try: | |
| await self.api.room.delete_room( | |
| api.DeleteRoomRequest(room=room_name or self._room.name) | |
| try: | |
| await self._room.disconnect() | |
| except Exception: | |
| logger.exception( | |
| "error disconnecting room before delete; proceeding with delete" | |
| ) | |
| try: | |
| await self.api.room.delete_room( | |
| api.DeleteRoomRequest(room=_room_name) | |
| ) |
Was this helpful? React with π or π to provide feedback.
| def shutdown(self, reason: str = "") -> None: | ||
| self._on_shutdown(reason) |
There was a problem hiding this comment.
π Default shutdown reason changed from descriptive string to empty string
The default reason parameter for shutdown() was changed from "user requested" to "" at livekit-agents/livekit/agents/job.py:754. This reason string is passed through _on_shutdown and eventually to all registered shutdown callbacks (see livekit-agents/livekit/agents/ipc/job_proc_lazy_main.py:428). Any downstream code that logs or inspects the shutdown reason will now receive an empty string when shutdown() is called without arguments, which could make debugging harder. The _JobShutdownInfo dataclass at line 960 also stores this reason. If this is intentional (e.g. to distinguish programmatic shutdowns from user-initiated ones), it should be documented.
Was this helpful? React with π or π to provide feedback.
| # Disconnect locally before issuing the server-side delete so the | ||
| # rust-sdk's connection-closed flag is set before the server closes | ||
| # the publisher data channels. Without this the channels are torn | ||
| # down from the remote side while the local session is still | ||
| # "connected", which triggers spurious ERROR-level logs: | ||
| # "publisher data channel '_reliable' closed unexpectedly" | ||
| try: | ||
| await self._room.disconnect() | ||
| except Exception: | ||
| logger.exception( | ||
| "error disconnecting room before delete; proceeding with delete" | ||
| ) |
There was a problem hiding this comment.
π Early disconnect may race with in-flight operations on the room
The new disconnect-before-delete pattern at livekit-agents/livekit/agents/job.py:639-644 disconnects the local room session before issuing the server-side delete. While this prevents spurious error logs from the Rust SDK, it also means any other in-flight operations on the room (e.g. publishing tracks, sending data) will be interrupted. In the _on_agent_session_close path (livekit-agents/livekit/agents/voice/room_io/room_io.py:476-482), the session is already closing so this is likely fine. But in the end_call.py:127 path, the disconnect happens inside a fire-and-forget task, so it could race with other room operations if delete_room() is called while the agent is still actively using the room.
Was this helpful? React with π or π to provide feedback.
Fixes #6250
Root cause
When
delete_room_on_close=True,_on_agent_session_closecallsjob_ctx.delete_room()while the local RTC session is still in theconnected state. The server-side delete tears down the publisher data
channels from the remote end. The rust-sdk introduced a check in
rust-sdks#1137 that logs at ERROR level whenever a publisher data channel
closes while the local session has not yet set its own closed flag.
The result is three misleading ERROR lines on every clean teardown:
The call completes cleanly β these are false positives triggered by a race
between the server-initiated close and the local state.
Fix
Call
room.disconnect()first so the local RTC session marks itselfclosed before the API delete sends the server-side channel teardown.
The delete is chained immediately after in the same async task, so the
observable behaviour (room is deleted on session close) is unchanged.
The existing
aclosepath already awaits_delete_room_task, so thenew coroutine-backed task is drained correctly there too.
Testing
Reproduce with
delete_room_on_close=True(the default) andctx.shutdown()called a few seconds after room creation. Before thispatch the three ERROR lines appear on every run; after it they are gone.