Skip to content

Commit 0b9ea06

Browse files
Implementation of teardown feature for bugfix
1 parent d5dda8c commit 0b9ea06

3 files changed

Lines changed: 140 additions & 93 deletions

File tree

include/livekit/room.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,7 @@ class LIVEKIT_API Room {
331331

332332
private:
333333
friend class RoomCallbackTest;
334+
friend struct RoomTestAccess;
334335

335336
mutable std::mutex lock_;
336337
ConnectionState connection_state_ = ConnectionState::Disconnected;
@@ -355,5 +356,10 @@ class LIVEKIT_API Room {
355356
int listener_id_{0};
356357

357358
void onEvent(const proto::FfiEvent& event);
359+
360+
// Tracks local teardown independently from connection_state_ to handle all teardown cases.
361+
bool teardown_started_{false};
362+
// Shared teardown path for explicit disconnect, server disconnect, EOS, and destruction.
363+
bool teardown(bool disconnect_ffi, DisconnectReason reason, bool notify_delegate);
358364
};
359365
} // namespace livekit

src/room.cpp

Lines changed: 55 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ bool Room::connect(const std::string& url, const std::string& token, const RoomO
102102
if (connection_state_ != ConnectionState::Disconnected) {
103103
throw std::runtime_error("already connected");
104104
}
105+
teardown_started_ = false;
105106
connection_state_ = ConnectionState::Reconnecting;
106107
}
107108

@@ -212,7 +213,10 @@ bool Room::connect(const std::string& url, const std::string& token, const RoomO
212213

213214
bool Room::disconnect(DisconnectReason reason) {
214215
TRACE_EVENT0("livekit", "Room::disconnect");
216+
return teardown(true, reason, true);
217+
}
215218

219+
bool Room::teardown(bool disconnect_ffi, DisconnectReason reason, bool notify_delegate) {
216220
std::shared_ptr<FfiHandle> handle;
217221
RoomDelegate* delegate_snapshot = nullptr;
218222
std::shared_ptr<LocalParticipant> local_participant_to_cleanup;
@@ -224,58 +228,81 @@ bool Room::disconnect(DisconnectReason reason) {
224228

225229
{
226230
const std::scoped_lock<std::mutex> g(lock_);
227-
if (connection_state_ == ConnectionState::Disconnected) {
228-
// Already torn down (or never connected). Nothing to do.
231+
const bool has_room_state = connection_state_ != ConnectionState::Disconnected || listener_id_ != 0 ||
232+
room_handle_ || local_participant_ || !remote_participants_.empty();
233+
if (teardown_started_ || !has_room_state) {
229234
return false;
230235
}
231-
handle = room_handle_;
236+
teardown_started_ = true;
237+
handle = std::move(room_handle_);
232238
delegate_snapshot = delegate_;
233-
// Take ownership of everything under the lock so the kEos handler (which
234-
// also tries to move it out) loses any race here — only one teardown
235-
// path operates on this state.
236239
local_participant_to_cleanup = std::move(local_participant_);
237240
remote_participants_to_clear = std::move(remote_participants_);
238241
e2ee_manager_to_clear = std::move(e2ee_manager_);
239242
text_stream_readers_to_clear = std::move(text_stream_readers_);
240243
byte_stream_readers_to_clear = std::move(byte_stream_readers_);
241244
listener_to_remove = listener_id_;
242245
listener_id_ = 0;
243-
room_handle_.reset();
244-
// Flip state immediately so the in-flight Disconnected room-event we'll
245-
// get back doesn't double-fire onDisconnected. Mirrors Python's
246-
// Room.disconnect()
247246
connection_state_ = ConnectionState::Disconnected;
248247
}
249248

250-
// Drain in-flight RPC handlers BEFORE telling Rust to tear down the room.
251-
// Mirrors client-sdk-python's Room.disconnect() ordering
249+
bool teardown_ok = true;
252250
if (local_participant_to_cleanup) {
253-
local_participant_to_cleanup->shutdown();
251+
try {
252+
local_participant_to_cleanup->shutdown();
253+
} catch (const std::exception& e) {
254+
LK_LOG_ERROR("Room teardown: local participant shutdown failed: {}", e.what());
255+
teardown_ok = false;
256+
} catch (...) {
257+
LK_LOG_ERROR("Room teardown: local participant shutdown failed: unknown exception");
258+
teardown_ok = false;
259+
}
254260
}
255261

256-
// Tell the FFI to close the room and wait for the callback. If this fails
257-
// we still complete local-side teardown below
258-
bool ffi_ok = true;
259-
if (handle) {
262+
if (disconnect_ffi && handle && handle->valid()) {
260263
try {
261264
FfiClient::instance().disconnectAsync(handle->get(), reason).get();
262265
} catch (const std::exception& e) {
263-
LK_LOG_ERROR("Room::disconnect: FFI disconnect failed (continuing local teardown): {}", e.what());
264-
ffi_ok = false;
266+
LK_LOG_ERROR("Room teardown: FFI disconnect failed (continuing local teardown): {}", e.what());
267+
teardown_ok = false;
268+
} catch (...) {
269+
LK_LOG_ERROR("Room teardown: FFI disconnect failed (continuing local teardown): unknown exception");
270+
teardown_ok = false;
265271
}
266272
}
267273

268-
// Stop dispatcher so no track callbacks fire mid-teardown.
269274
if (subscription_thread_dispatcher_) {
270-
subscription_thread_dispatcher_->stopAll();
275+
try {
276+
subscription_thread_dispatcher_->stopAll();
277+
} catch (const std::exception& e) {
278+
LK_LOG_ERROR("Room teardown: subscription shutdown failed: {}", e.what());
279+
teardown_ok = false;
280+
} catch (...) {
281+
LK_LOG_ERROR("Room teardown: subscription shutdown failed: unknown exception");
282+
teardown_ok = false;
283+
}
271284
}
272285

273286
if (listener_to_remove != 0) {
274-
FfiClient::instance().removeListener(listener_to_remove);
287+
try {
288+
FfiClient::instance().removeListener(listener_to_remove);
289+
} catch (const std::exception& e) {
290+
LK_LOG_ERROR("Room teardown: listener removal failed: {}", e.what());
291+
teardown_ok = false;
292+
} catch (...) {
293+
LK_LOG_ERROR("Room teardown: listener removal failed: unknown exception");
294+
teardown_ok = false;
295+
}
275296
}
276297

277-
// Fire onDisconnected exactly once, with the reason the caller passed.
278-
if (delegate_snapshot) {
298+
local_participant_to_cleanup.reset();
299+
remote_participants_to_clear.clear();
300+
e2ee_manager_to_clear.reset();
301+
text_stream_readers_to_clear.clear();
302+
byte_stream_readers_to_clear.clear();
303+
handle.reset();
304+
305+
if (notify_delegate && delegate_snapshot) {
279306
DisconnectedEvent ev;
280307
ev.reason = reason;
281308
try {
@@ -287,9 +314,7 @@ bool Room::disconnect(DisconnectReason reason) {
287314
}
288315
}
289316

290-
// Moved-out state (local participant, remote participants, e2ee manager,
291-
// stream readers) destructs here, releasing FFI handles.
292-
return ffi_ok;
317+
return teardown_ok;
293318
}
294319

295320
RoomInfoData Room::roomInfo() const {
@@ -1168,22 +1193,8 @@ void Room::onEvent(const FfiEvent& event) {
11681193
break;
11691194
}
11701195
case proto::RoomEvent::kDisconnected: {
1171-
// If disconnect() was driven from our side, it already flipped state
1172-
// to Disconnected and fired the delegate; skip the duplicate here.
1173-
bool already_disconnected = false;
1174-
{
1175-
const std::scoped_lock<std::mutex> guard(lock_);
1176-
already_disconnected = (connection_state_ == ConnectionState::Disconnected);
1177-
connection_state_ = ConnectionState::Disconnected;
1178-
}
1179-
if (already_disconnected) {
1180-
break;
1181-
}
1182-
DisconnectedEvent ev;
1183-
ev.reason = toDisconnectReason(re.disconnected().reason());
1184-
if (delegate_snapshot) {
1185-
delegate_snapshot->onDisconnected(*this, ev);
1186-
}
1196+
const auto reason = toDisconnectReason(re.disconnected().reason());
1197+
(void)teardown(false, reason, true);
11871198
break;
11881199
}
11891200
case proto::RoomEvent::kReconnecting: {
@@ -1208,56 +1219,7 @@ void Room::onEvent(const FfiEvent& event) {
12081219
break;
12091220
}
12101221
case proto::RoomEvent::kEos: {
1211-
if (subscription_thread_dispatcher_) {
1212-
subscription_thread_dispatcher_->stopAll();
1213-
}
1214-
1215-
int listener_to_remove = 0;
1216-
1217-
// Move state out of lock scope before destroying to avoid holding lock
1218-
// during potentially long destructors
1219-
std::shared_ptr<LocalParticipant> old_local_participant;
1220-
std::unordered_map<std::string, std::shared_ptr<RemoteParticipant>> old_remote_participants;
1221-
std::shared_ptr<FfiHandle> old_room_handle;
1222-
std::shared_ptr<E2EEManager> old_e2ee_manager;
1223-
std::unordered_map<std::string, std::shared_ptr<TextStreamReader>> old_text_readers;
1224-
std::unordered_map<std::string, std::shared_ptr<ByteStreamReader>> old_byte_readers;
1225-
1226-
{
1227-
const std::scoped_lock<std::mutex> guard(lock_);
1228-
listener_to_remove = listener_id_;
1229-
listener_id_ = 0;
1230-
1231-
// Reset connection state
1232-
connection_state_ = ConnectionState::Disconnected;
1233-
1234-
// Move state out for cleanup outside lock
1235-
old_local_participant = std::move(local_participant_);
1236-
old_remote_participants = std::move(remote_participants_);
1237-
old_room_handle = std::move(room_handle_);
1238-
old_e2ee_manager = std::move(e2ee_manager_);
1239-
old_text_readers = std::move(text_stream_readers_);
1240-
old_byte_readers = std::move(byte_stream_readers_);
1241-
}
1242-
1243-
// Drain in-flight RPC invocations before destroying the local
1244-
// participant's FFI handle. Mirrors the ordering in disconnect();
1245-
// without this, a listener-thread RPC handler can race with handle
1246-
// disposal and send to a dead handle → INVALID_HANDLE → terminate.
1247-
if (old_local_participant) {
1248-
old_local_participant->shutdown();
1249-
}
1250-
1251-
// Remove listener outside lock
1252-
if (listener_to_remove != 0) {
1253-
FfiClient::instance().removeListener(listener_to_remove);
1254-
}
1255-
1256-
if (old_local_participant) {
1257-
old_local_participant->shutdown();
1258-
}
1259-
1260-
// old_* state is destroyed here when going out of scope
1222+
(void)teardown(false, DisconnectReason::Unknown, false);
12611223

12621224
const RoomEosEvent ev;
12631225
if (delegate_snapshot) {

src/tests/unit/test_room.cpp

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,44 @@
1717
#include <gtest/gtest.h>
1818
#include <livekit/livekit.h>
1919

20+
#include <atomic>
2021
#include <chrono>
2122
#include <future>
2223
#include <string>
2324

2425
#include "ffi.pb.h"
26+
#include "ffi_client.h"
2527
#include "room_proto_converter.h"
2628

29+
namespace livekit {
30+
31+
struct RoomTestAccess {
32+
static void installConnectedListener(Room& room, std::atomic<int>& callback_count) {
33+
const auto listener_id = FfiClient::instance().addListener([&room, &callback_count](const proto::FfiEvent& event) {
34+
callback_count.fetch_add(1, std::memory_order_relaxed);
35+
room.onEvent(event);
36+
});
37+
38+
const std::scoped_lock<std::mutex> guard(room.lock_);
39+
room.connection_state_ = ConnectionState::Connected;
40+
room.room_handle_ = std::make_shared<FfiHandle>();
41+
room.listener_id_ = listener_id;
42+
room.teardown_started_ = false;
43+
}
44+
45+
static bool hasRoomHandle(const Room& room) {
46+
const std::scoped_lock<std::mutex> guard(room.lock_);
47+
return static_cast<bool>(room.room_handle_);
48+
}
49+
50+
static int listenerId(const Room& room) {
51+
const std::scoped_lock<std::mutex> guard(room.lock_);
52+
return room.listener_id_;
53+
}
54+
};
55+
56+
} // namespace livekit
57+
2758
namespace livekit::test {
2859

2960
class RoomTest : public ::testing::Test {
@@ -33,6 +64,54 @@ class RoomTest : public ::testing::Test {
3364
void TearDown() override { livekit::shutdown(); }
3465
};
3566

67+
namespace {
68+
69+
class UnitDisconnectTrackingDelegate : public RoomDelegate {
70+
public:
71+
void onDisconnected(Room&, const DisconnectedEvent& event) override {
72+
++count;
73+
reason = event.reason;
74+
}
75+
76+
int count = 0;
77+
DisconnectReason reason = DisconnectReason::Unknown;
78+
};
79+
80+
void emitFfiEvent(const proto::FfiEvent& event) {
81+
std::string bytes;
82+
ASSERT_TRUE(event.SerializeToString(&bytes));
83+
ffiEventCallback(reinterpret_cast<const std::uint8_t*>(bytes.data()), bytes.size());
84+
}
85+
86+
} // namespace
87+
88+
TEST_F(RoomTest, ServerDisconnectTearsDownRoomAndRemovesListener) {
89+
Room room;
90+
UnitDisconnectTrackingDelegate delegate;
91+
std::atomic<int> listener_calls{0};
92+
room.setDelegate(&delegate);
93+
RoomTestAccess::installConnectedListener(room, listener_calls);
94+
95+
proto::FfiEvent event;
96+
auto* room_event = event.mutable_room_event();
97+
room_event->set_room_handle(0);
98+
room_event->mutable_disconnected()->set_reason(proto::ROOM_DELETED);
99+
100+
emitFfiEvent(event);
101+
102+
EXPECT_EQ(listener_calls.load(std::memory_order_relaxed), 1);
103+
EXPECT_EQ(room.connectionState(), ConnectionState::Disconnected);
104+
EXPECT_FALSE(RoomTestAccess::hasRoomHandle(room));
105+
EXPECT_EQ(RoomTestAccess::listenerId(room), 0);
106+
EXPECT_EQ(delegate.count, 1);
107+
EXPECT_EQ(delegate.reason, DisconnectReason::RoomDeleted);
108+
109+
emitFfiEvent(event);
110+
EXPECT_EQ(listener_calls.load(std::memory_order_relaxed), 1) << "server disconnect must unregister the Room listener";
111+
EXPECT_EQ(delegate.count, 1) << "server disconnect must notify the delegate exactly once";
112+
EXPECT_FALSE(room.disconnect()) << "disconnect after server teardown must be a no-op";
113+
}
114+
36115
TEST_F(RoomTest, ConnectWithoutInitialize) {
37116
// Test fixture initializes by default, do this to emulate lack of initialization
38117
livekit::shutdown();

0 commit comments

Comments
 (0)