Skip to content

fix(rtc): avoid Room.sid hang on disconnect and deduplicate active speakers - #875

Open
NabiBukhsh-AI wants to merge 2 commits into
livekit:mainfrom
NabiBukhsh-AI:fix/651-room-sid-hang
Open

NabiBukhsh-AI wants to merge 2 commits into
livekit:mainfrom
NabiBukhsh-AI:fix/651-room-sid-hang

Conversation

@NabiBukhsh-AI

Copy link
Copy Markdown
Contributor

Description

This PR resolves two issues in livekit-rtc:

  1. Fixes Room.sid hangs indefinitely when room_sid_changed FFI event is not delivered #651: Room.sid hanging indefinitely when the room disconnects before receiving a SID, when connect fails, or when accessed on a disconnected room.
  2. Fixes Blank "active_speakers_changed" events are fired even when there are no changes #148: Blank or duplicate active_speakers_changed events being emitted when there are no changes to the active speakers list.

Root Cause Analysis & Key Changes

1. Issue #651 (Room.sid indefinite hang)

  • Problem: Room.sid awaited self._first_sid_future. If a room disconnected before receiving a SID (e.g. SIP ringback hangup before server sends SID), or if connect() threw a ConnectError, or if sid was queried on a disconnected room, _first_sid_future remained pending indefinitely, causing caller tasks to suspend forever.
  • Fix:
    • sid property: if connection_state == ConnectionState.CONN_DISCONNECTED and not currently connecting, immediately raises RuntimeError("cannot get sid of a disconnected room").
    • Added _fail_first_sid(exc): if _first_sid_future is pending, sets the exception (and marks it retrieved to prevent unretrieved future warnings if unawaited).
    • On room disconnect(), disconnected FFI event, or _listen_task termination (EOS/error): marks _first_sid_future failed with RuntimeError("room disconnected before receiving sid").
    • On connect() failure: marks _first_sid_future failed with the connect exception.
    • Added async def get_sid(self, timeout: float | None = None) -> str for callers to easily bound the wait time for a SID.
    • Defensively guard Room.__repr__ against unhandled exceptions on _first_sid_future.

2. Issue #148 (Blank and redundant active_speakers_changed events)

  • Problem: When audio level updates arrive from the SFU, _on_room_event emitted active_speakers_changed unconditionally, even when the active speaker list remained identical or empty.
  • Fix:
    • Tracked self._active_speakers: list[Participant] = [] and exposed property Room.active_speakers.
    • Only emit active_speakers_changed when the identities list differs from self._active_speakers.
    • On participant disconnect or room disconnect: cleanly update/reset self._active_speakers and emit an updated list.

3. Test Runner FFI Isolation

  • In livekit-rtc/tests/test_ffi_handle_lifecycle.py, test_live_handles_marked_consumed_and_no_assert_after_dispose is isolated in a subprocess to prevent FfiClient._dispose() from tearing down the C/Rust FFI runtime in the main pytest process.

Verification

  • uv run pytest tests/ livekit-rtc/tests/: 203 passed, 74 skipped, 0 failures.
  • uv run mypy livekit-protocol livekit-api livekit-rtc: Success (no issues found in 105 files).
  • uv run ruff check & uv run ruff format --check: Passed cleanly.

…peakers

- Fix Room.sid indefinitely hanging when room disconnects before receiving SID or when called on a disconnected room (fixes livekit#651)
- Add Room.get_sid(timeout=...) to allow bounding the wait for room SID
- Fail _first_sid_future cleanly with RuntimeError/ConnectError when disconnect or connect error occurs
- Safely retrieve participant identity in _retrieve_participant without requiring local_participant before connecting
- Safely read sid in Room.__repr__ without raising if _first_sid_future has an exception
- Deduplicate active_speakers_changed events to avoid emitting duplicate or blank events when active speakers have not changed (fixes livekit#148)
- Expose Room.active_speakers property to inspect currently active speakers
- Clear active_speakers and emit empty list when room disconnects or speaking participant disconnects
- Isolate FfiClient._dispose in subprocess in test_ffi_handle_lifecycle to avoid invalidating the global FFI client during test runs

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

Devin Review

Comment thread livekit-rtc/livekit/rtc/room.py Outdated
Comment on lines +646 to +650
except Exception as e:
self._fail_first_sid(e)
raise
finally:
self._is_connecting = False

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Cancelled connect strands SID waiters

When connect() is cancelled before its response, existing SID waiters remain blocked because asyncio.CancelledError bypasses the failure handler. The room's FFI subscription also stays open.

Learn more

connect() subscribes the room to FFI events before awaiting its response, and sid allows callers to wait while _is_connecting is true. Cancelling connect() during that await runs the inner queue cleanup, but asyncio.CancelledError does not enter except Exception. The SID future stays pending for callers already awaiting it. The outer room subscription also remains registered; isconnected is false without a room handle, so disconnect() cannot clean it up.

Example: Start connect() against an unresponsive server and start awaiting room.sid. Cancel the connect task with asyncio.wait_for; the SID task never finishes, and the room remains subscribed to FFI events.

Recommended fix: Handle cancellation separately in connect(), fail the pending SID future, and unsubscribe the room FFI queue when a connection does not complete. Preserve cancellation propagation and ensure cleanup applies to other early failures.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread livekit-rtc/livekit/rtc/room.py Outdated
- Catch BaseException in connect to properly fail pending SID future and unsubscribe FFI queue on cancellation
- Emit defensive list(speakers) copy to prevent listener mutation from corrupting cached speakers
- Add regression tests for connect cancellation and speaker list immutability

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant