Room disconnects with `duplicateIdentity` after reconnecting from a single client: a timed-out signal connect leaves a live socket behind

Đang mở
#1,219 0 bình luận 1 reaction 0 người được giao Xem trên GitHub

Maintainer thường phản hồi trong vòng 2 ngày

Chưa có ai nhận issue này.

Đánh giá

Độ khó
4/5
Thời gian dự kiến
3-5 ngày
Mức phù hợp với người mới
30/100
Loại issue
Lỗi
Độ rõ ràng
Đặc tả rõ ràng
Mức độ hoạt động
Sôi nổi
Công nghệ
dart, flutter
Lĩnh vực
mobile-dev, networking

Hướng nghiên cứu

Read lib/src/core/signal_client.dart around SignalClient.connect() and the engine handler near engine.dart:~1603; trace what happens when the timed-out WebSocket connection completes late. The issue says the author already has a local fix and plans a PR, so check for that work before starting. Done means the late socket is closed, S2 remains connected, and no duplicateIdentity disconnect is emitted.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Mô tả

Describe the bug

While a room recovers from a network drop, it can end with RoomDisconnectedEvent(reason: DisconnectReason.duplicateIdentity), even though only one client uses that identity. The reconnect had already succeeded, and the connection was healthy when the event arrived.

duplicateIdentity is supposed to mean another participant joined with the same identity, so apps treat it as "you were replaced" and don't retry. Here, the other session is one this client opened during its own reconnect.

The server is behaving correctly. The client ends up with two sessions for one identity, the server kicks the older one, and the SDK applies that kick to the session it is actually using.

Root cause

SignalClient.connect() (lib/src/core/signal_client.dart, main @ 2.13.0):

var future = _wsConnector(rtcUri, options: WebSocketEventHandlers(
  onData: _onSocketData, onDispose: _onSocketDispose, onError: _onSocketError), ...);
future = future.timeout(connectOptions.timeouts.connection);
_ws = await future;
  1. Future.timeout does not cancel io.WebSocket.connect. On a slow link the handshake can finish after the timeout. The server then has a live session S1 for that attempt.
  2. That socket is never assigned to _ws, so cleanUp() can never close it. LiveKitWebSocketIO has already subscribed it to the shared _onSocketData, so it keeps delivering messages as if it were current.
  3. The engine retries, and the next join creates session S2, which is the one we keep. The server sees two sessions for one identity and sends Leave(DISCONNECT, DUPLICATE_IDENTITY) to S1, which is the orphaned socket.
  4. The leave comes through _onSocketData as an ordinary SignalLeaveEvent. The engine handler (engine.dart:~1603) runs signalClient.cleanUp() and disconnect(), which tears down S2 and emits RoomDisconnectedEvent(reason: duplicateIdentity). Signal is already disconnected at that point, so no leave is sent for S2 either. The server keeps S2 as a stale participant until it times out, and other participants go on seeing it in the room.
To reproduce

Deterministic on an Android emulator (reproduced 5+ times):

  1. Use a short connect timeout so the race is easy to hit: ConnectOptions(timeouts: Timeouts(connection: Duration(seconds: 3), ...)).
  2. Turn on verbose logs: hierarchicalLoggingEnabled = true; Logger('livekit').level = Level.ALL;.
  3. Emulator: Wi-Fi off, so traffic goes over the emulated mobile-data link. adb emu network delay only affects that link.
  4. Join a room.
  5. Airplane mode on for ~40 s, so the server drops the session.
  6. adb emu network delay 5000. Every packet now takes longer than the connect timeout.
  7. Airplane mode off.

About 30 s later the logs show:

Leave received, action: RECONNECT, reason: STATE_MISMATCH   // resume rejected, server already dropped us
EngineFullRestartingEvent                                    // full rejoin #1
Received JoinResponse                                        // S1's handshake finished...
Connect Error TimeoutException after 0:00:03                 // ...but the attempt had already timed out
EngineFullRestartingEvent                                    // full rejoin #2 -> S2
Leave received, action: DISCONNECT, reason: DUPLICATE_IDENTITY   // meant for S1, handled as S2's
RoomDisconnectedEvent(reason: duplicateIdentity)

Expected: the room stays connected on S2, S1's socket is closed, and no RoomDisconnectedEvent is emitted.
Actual: RoomDisconnectedEvent(reason: duplicateIdentity) on a single client.

We also see this in production on phones with poor mobile data: one device, one identity, and a duplicateIdentity disconnect right after a run of reconnect attempts.

Relation to #1211

#1211 adds a per-socket generation so callbacks from a replaced socket are ignored. Once the retry bumps the generation, that would also drop the late leave above. This issue is a different trigger (a timed-out attempt instead of a disposed one), and two gaps remain:

  • the late socket is never closed, so it stays open and holds a server session until the server closes it;
  • between the timeout and the next connect() (e.g. during retry backoff), the orphan's callbacks still carry the current generation and are processed.

I have a fix with tests for this locally and plan to open a PR.

Environment
  • livekit_client 2.7.0 in production, same code on main (2.13.0)
  • Android (emulator repro) and iOS (production)
  • LiveKit server (self-hosted)
Ngôn ngữ chính
Dart
Star
421
Fork
260
Merge trung bình
2 ngày 5 giờ
Pull request đã merge (30 ngày)
20

Chuẩn bị môi trường

Dự án này không cung cấp dev container, Dockerfile hay hướng dẫn đóng góp, nên bạn cần tự thiết lập môi trường: hãy bắt đầu từ README và xem hướng dẫn đóng góp lần đầu của chúng tôi để biết các bước chung.

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của livekit/client-sdk-flutter

Tất cả issue của livekit/client-sdk-flutter

Issue tương tự

Thêm issue về Dart

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.