Skip to content

fix: disconnect() tears down locally after sending the Leave instead of waiting for the server echo - #1208

Draft
hiroshihorie wants to merge 2 commits into
mainfrom
hiroshi/complete-disconnect-on-socket-close
Draft

hiroshihorie wants to merge 2 commits into
mainfrom
hiroshi/complete-disconnect-on-socket-close

Conversation

@hiroshihorie

@hiroshihorie hiroshihorie commented Sep 15, 2026

Copy link
Copy Markdown
Member

Problem

engine.disconnect() sent the Leave and relied on the server's echo to run cleanup and emit EngineDisconnectedEvent, and Room.disconnect() waited up to 10 s for that event. Media nodes drop queued leave messages when they close the signal sink (pkg/routing/signal.go, signalMessageSink.Close, comment in place), so the echo can be lost. Every lost echo was a 10 s stall, a TimeoutException out of disconnect(), and a room left half torn down because Room.disconnect() never reached its own cleanup.

Found by the e2e-flutter suite (livekit/e2e-flutter#7): one agent per run against a local livekit-server 1.13.6, SignalClient did disconnect with no Leave received before it while the server logged sendLeave: true.

This design dates from the Dec 2023 engine rewrite (v1.5.5). #559 (v2.2.2, issue #553) made Room.disconnect() wait for the event, which turned a silently missing event into a visible stall. No other SDK waits on the server here: JS, Swift, Android and Rust all send the Leave and tear down locally.

Fix

Flutter now does the same.

  • engine.disconnect() sends the Leave, runs cleanUp() and emits EngineDisconnectedEvent in one go. No dependency on the echo.
  • A Leave that arrives after disconnect() started is the echo, or a server Leave racing ours, and is ignored.
  • Room.disconnect() awaits the engine teardown and then its own cleanup, no wait on the server and no timeout. The engine event handler skips its cleanup while Room.disconnect() is running, so teardown has one owner and a connect() right after disconnect() returns cannot race a straggling handler. That is the case behind [bug] Actions stop working after second connect attempt to same room flutter livekit client #553, now covered by a test.

The first commit on this branch was a narrower fallback that completed the disconnect on socket close while keeping the echo dependency. The second commit replaces it with the alignment above.

Tests

test/core/disconnect_event_test.dart, four added:

  • socket close without echo completes disconnect() with one clientInitiated event
  • echo followed by socket close emits exactly one event
  • disconnect() returns with engine and room torn down when the server sends nothing at all
  • connect again to the same room right after disconnect() returns

The last two fail on main. Full suite 428 passing.

Not yet run

The e2e-flutter suite against this branch. The fallback version of the branch turned the suite's one warning into zero and cut the run by 9 s. Xcode is not available on my machine at the moment, so the final version has not had its real-server run yet. Will report here when it has.

🤖 Generated with Claude Code

…echoing the leave

engine.disconnect() sends the Leave and relies on the server's echo to
run cleanup and emit EngineDisconnectedEvent. Media nodes drop queued
leave messages when they close the signal sink, so the echo can be
lost. The socket close was then ignored because _isClosed was set, and
Room.disconnect() waited out its 10 s timeout for an event that never
came, leaving the room half torn down.

The engine now remembers a disconnect that is waiting for its echo and,
on a socket close in that window, runs cleanup and emits the event
itself. A close that follows the echo is still ignored, so there is no
second event.
…of waiting for the echo

The engine used to send the Leave and rely on the server's echo to run
cleanup and emit EngineDisconnectedEvent, and Room.disconnect() waited
up to 10 s for that event. Media nodes drop queued leave messages when
they close the signal sink, so the echo can be lost, and every lost
echo was a 10 s stall, a TimeoutException, and a room left half torn
down. No other SDK waits on the server here.

disconnect() now sends the Leave, runs cleanUp() and emits the event in
one go, like JS, Swift, Android and Rust. A Leave that arrives after
disconnect() started is the echo and is ignored. Room.disconnect() owns
the teardown for the disconnect it started, so the engine event handler
skips its own cleanup in that case and a connect() right after
disconnect() returns cannot race it.

Replaces the earlier socket close fallback on this branch.
@hiroshihorie hiroshihorie changed the title fix: complete disconnect() when the server closes the socket without echoing the leave fix: disconnect() tears down locally after sending the Leave instead of waiting for the server echo Sep 16, 2026
@hiroshihorie

Copy link
Copy Markdown
Member Author

e2e-flutter suite (livekit/e2e-flutter#7) against this branch, local livekit-server 1.13.6, macOS, three full runs on 2026-09-16:

  • run 1: 8 pass, 10 skip, 0 warn, 0 fail
  • run 2: 8 pass, 10 skip, 0 warn, 1 fail in ParticipantMetadataTest, a race in the test itself (un-awaited waitUntil), fixed in e2e-flutter 920b3b2
  • run 3: 8 pass, 10 skip, 0 warn, 0 fail

ParticipantAttributesTest, the scenario whose agent hit the lost Leave echo on every run before, completed its teardown in about 0.5 s on all three runs with no warning. The 10 skips are camera scenarios, not runnable on this machine.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant