Skip to content

Commit 90788da

Browse files
rm unneeded function, better cleanup
1 parent 3c57c66 commit 90788da

3 files changed

Lines changed: 148 additions & 22 deletions

File tree

include/livekit/subscription_thread_dispatcher.h

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
#pragma once
1818

19+
#include <atomic>
1920
#include <cstdint>
2021
#include <functional>
2122
#include <memory>
@@ -259,6 +260,9 @@ class LIVEKIT_API SubscriptionThreadDispatcher {
259260
std::shared_ptr<AudioStream> audio_stream;
260261
std::shared_ptr<VideoStream> video_stream;
261262
std::thread thread;
263+
/// SID of the subscribed track backing this reader, used to skip redundant
264+
/// reader restarts when the same publication is re-subscribed.
265+
std::string track_sid;
262266
};
263267

264268
/// Compound lookup key for a remote participant identity and data track name.
@@ -289,6 +293,9 @@ class LIVEKIT_API SubscriptionThreadDispatcher {
289293
/// Active read-side resources for one data track stream subscription.
290294
struct ActiveDataReader {
291295
std::shared_ptr<RemoteDataTrack> remote_track;
296+
/// Set true when this reader is being replaced or torn down so the reader
297+
/// thread can abort a subscription that is still in flight.
298+
std::atomic<bool> cancelled{false};
292299
std::mutex sub_mutex;
293300
std::shared_ptr<DataTrackStream> stream; // guarded by sub_mutex
294301
std::thread thread;
@@ -333,18 +340,21 @@ class LIVEKIT_API SubscriptionThreadDispatcher {
333340
const RegisteredVideoCallback& callback);
334341

335342
/// Extract and close the data reader for a given callback ID, returning its
336-
/// thread. Must be called with @ref lock_ held.
343+
/// thread. Marks the reader cancelled so a subscription still in flight is
344+
/// aborted. Must be called with @ref lock_ held.
337345
std::thread extractDataReaderThreadLocked(DataFrameCallbackId id);
338346

339-
/// Extract and close the data reader for a given (participant, track_name)
340-
/// key, returning its thread. Must be called with @ref lock_ held.
341-
std::thread extractDataReaderThreadLocked(const DataCallbackKey& key);
342-
343347
/// Start a data reader thread for the given callback ID, key, and track.
344348
/// Must be called with @ref lock_ held.
345349
std::thread startDataReaderLocked(DataFrameCallbackId id, const DataCallbackKey& key,
346350
const std::shared_ptr<RemoteDataTrack>& track, const DataFrameCallback& cb);
347351

352+
/// Remove @p reader from @ref active_data_readers_ if the slot for @p id
353+
/// still refers to it. Called by the reader thread itself when it exits
354+
/// after a failed or cancelled subscription so it does not leave a stale
355+
/// entry behind. Acquires @ref lock_.
356+
void eraseDataReaderIfCurrent(DataFrameCallbackId id, const std::shared_ptr<ActiveDataReader>& reader);
357+
348358
/// Protects callback registration maps and active reader state.
349359
mutable std::mutex lock_;
350360

src/subscription_thread_dispatcher.cpp

Lines changed: 68 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,9 @@ void SubscriptionThreadDispatcher::handleDataTrackUnpublished(const std::string&
259259
for (auto it = active_data_readers_.begin(); it != active_data_readers_.end();) {
260260
auto& reader = it->second;
261261
if (reader->remote_track && reader->remote_track->info().sid == sid) {
262+
// Mark cancelled before closing so a subscription still in flight aborts
263+
// even if the stream has not been created yet.
264+
reader->cancelled = true;
262265
{
263266
const std::scoped_lock<std::mutex> sub_guard(reader->sub_mutex);
264267
if (reader->stream) {
@@ -312,6 +315,10 @@ void SubscriptionThreadDispatcher::stopAll() {
312315
video_callbacks_.clear();
313316

314317
for (auto& [id, reader] : active_data_readers_) {
318+
// Mark cancelled before closing so a subscription still in flight aborts
319+
// even if the stream has not been created yet, avoiding a join that never
320+
// completes during teardown.
321+
reader->cancelled = true;
315322
{
316323
const std::scoped_lock<std::mutex> sub_guard(reader->sub_mutex);
317324
if (reader->stream) {
@@ -397,6 +404,16 @@ std::thread SubscriptionThreadDispatcher::startAudioReaderLocked(const CallbackK
397404
const AudioFrameCallback& cb,
398405
const AudioStream::Options& opts) {
399406
LK_LOG_DEBUG("Starting audio reader for participant={} track_name={}", key.participant_identity, key.track_name);
407+
408+
auto existing = active_readers_.find(key);
409+
if (existing != active_readers_.end() && existing->second.track_sid == track->sid()) {
410+
LK_LOG_DEBUG(
411+
"Skipping audio reader start for participant={} track_name={} because a "
412+
"reader for sid={} is already active",
413+
key.participant_identity, key.track_name, track->sid());
414+
return {};
415+
}
416+
400417
auto old_thread = extractReaderThreadLocked(key);
401418

402419
if (static_cast<int>(active_readers_.size()) >= kMaxActiveReaders) {
@@ -415,6 +432,7 @@ std::thread SubscriptionThreadDispatcher::startAudioReaderLocked(const CallbackK
415432

416433
ActiveReader reader;
417434
reader.audio_stream = stream;
435+
reader.track_sid = track->sid();
418436
const std::string participant_identity = key.participant_identity;
419437
const std::string track_name = key.track_name;
420438
// NOLINTBEGIN(bugprone-lambda-function-name,bugprone-exception-escape)
@@ -454,6 +472,16 @@ std::thread SubscriptionThreadDispatcher::startVideoReaderLocked(const CallbackK
454472
const std::shared_ptr<Track>& track,
455473
const RegisteredVideoCallback& callback) {
456474
LK_LOG_DEBUG("Starting video reader for participant={} track_name={}", key.participant_identity, key.track_name);
475+
476+
auto existing = active_readers_.find(key);
477+
if (existing != active_readers_.end() && existing->second.track_sid == track->sid()) {
478+
LK_LOG_DEBUG(
479+
"Skipping video reader start for participant={} track_name={} because a "
480+
"reader for sid={} is already active",
481+
key.participant_identity, key.track_name, track->sid());
482+
return {};
483+
}
484+
457485
auto old_thread = extractReaderThreadLocked(key);
458486

459487
if (static_cast<int>(active_readers_.size()) >= kMaxActiveReaders) {
@@ -472,6 +500,7 @@ std::thread SubscriptionThreadDispatcher::startVideoReaderLocked(const CallbackK
472500

473501
ActiveReader reader;
474502
reader.video_stream = stream;
503+
reader.track_sid = track->sid();
475504
auto legacy_cb = callback.legacy_callback;
476505
auto event_cb = callback.event_callback;
477506
const std::string participant_identity = key.participant_identity;
@@ -522,6 +551,9 @@ std::thread SubscriptionThreadDispatcher::extractDataReaderThreadLocked(DataFram
522551
}
523552
auto reader = std::move(it->second);
524553
active_data_readers_.erase(it);
554+
// Mark cancelled before closing so a subscription still in flight aborts even
555+
// if the stream has not been created yet.
556+
reader->cancelled = true;
525557
{
526558
const std::scoped_lock<std::mutex> guard(reader->sub_mutex);
527559
if (reader->stream) {
@@ -531,28 +563,36 @@ std::thread SubscriptionThreadDispatcher::extractDataReaderThreadLocked(DataFram
531563
return std::move(reader->thread);
532564
}
533565

534-
std::thread SubscriptionThreadDispatcher::extractDataReaderThreadLocked(const DataCallbackKey& key) {
535-
for (auto it = active_data_readers_.begin(); it != active_data_readers_.end(); ++it) {
536-
if (it->second && it->second->remote_track &&
537-
it->second->remote_track->publisherIdentity() == key.participant_identity &&
538-
it->second->remote_track->info().name == key.track_name) {
539-
auto reader = std::move(it->second);
540-
active_data_readers_.erase(it);
541-
{
542-
const std::scoped_lock<std::mutex> guard(reader->sub_mutex);
543-
if (reader->stream) {
544-
reader->stream->close();
545-
}
546-
}
547-
return std::move(reader->thread);
548-
}
566+
void SubscriptionThreadDispatcher::eraseDataReaderIfCurrent(DataFrameCallbackId id,
567+
const std::shared_ptr<ActiveDataReader>& reader) {
568+
const std::scoped_lock<std::mutex> lock(lock_);
569+
auto it = active_data_readers_.find(id);
570+
if (it == active_data_readers_.end() || it->second != reader) {
571+
// The slot was already extracted or replaced; the owner joins that thread.
572+
return;
549573
}
550-
return {};
574+
// Detach so the reader can be destroyed by its own thread's final shared_ptr
575+
// release without tripping std::thread's joinable-at-destruction check. The
576+
// thread is already returning when this runs, so nothing is left to join.
577+
if (reader->thread.joinable()) {
578+
reader->thread.detach();
579+
}
580+
active_data_readers_.erase(it);
551581
}
552582

553583
std::thread SubscriptionThreadDispatcher::startDataReaderLocked(DataFrameCallbackId id, const DataCallbackKey& key,
554584
const std::shared_ptr<RemoteDataTrack>& track,
555585
const DataFrameCallback& cb) {
586+
auto existing = active_data_readers_.find(id);
587+
if (existing != active_data_readers_.end() && existing->second->remote_track &&
588+
existing->second->remote_track->info().sid == track->info().sid) {
589+
LK_LOG_DEBUG(
590+
"Skipping data reader start for \"{}\" track=\"{}\" because a reader for "
591+
"sid={} is already active",
592+
key.participant_identity, key.track_name, track->info().sid);
593+
return {};
594+
}
595+
556596
auto old_thread = extractDataReaderThreadLocked(id);
557597

558598
const int total_active = static_cast<int>(active_readers_.size()) + static_cast<int>(active_data_readers_.size());
@@ -571,7 +611,7 @@ std::thread SubscriptionThreadDispatcher::startDataReaderLocked(DataFrameCallbac
571611
auto identity = key.participant_identity;
572612
auto track_name = key.track_name;
573613
// NOLINTBEGIN(bugprone-lambda-function-name)
574-
reader->thread = std::thread([reader, track, cb, identity, track_name]() {
614+
reader->thread = std::thread([this, id, reader, track, cb, identity, track_name]() {
575615
LK_LOG_INFO("Data reader thread: subscribing to \"{}\" track=\"{}\"", identity, track_name);
576616
std::shared_ptr<DataTrackStream> stream;
577617
auto subscribe_result = track->subscribe();
@@ -581,13 +621,21 @@ std::thread SubscriptionThreadDispatcher::startDataReaderLocked(DataFrameCallbac
581621
"Failed to subscribe to data track \"{}\" from \"{}\": code={} "
582622
"message={}",
583623
track_name, identity, static_cast<std::uint32_t>(error.code), error.message);
624+
eraseDataReaderIfCurrent(id, reader);
584625
return;
585626
}
586627
stream = subscribe_result.value();
587628
LK_LOG_INFO("Data reader thread: subscribed to \"{}\" track=\"{}\"", identity, track_name);
588629

589630
{
590631
const std::scoped_lock<std::mutex> guard(reader->sub_mutex);
632+
// A replacement or teardown may have cancelled this reader while the
633+
// subscribe was in flight. Close the fresh stream and bail so we do not
634+
// leave a second live subscription behind.
635+
if (reader->cancelled.load()) {
636+
stream->close();
637+
return;
638+
}
591639
reader->stream = stream;
592640
}
593641

@@ -606,6 +654,9 @@ std::thread SubscriptionThreadDispatcher::startDataReaderLocked(DataFrameCallbac
606654
"\"{}\": code={} message={}",
607655
track_name, identity, static_cast<std::uint32_t>(error->code), error->message);
608656
}
657+
// Clean our own slot if the stream ended on its own (server EOS) and no
658+
// extract/teardown already claimed it. A no-op when we were extracted.
659+
eraseDataReaderIfCurrent(id, reader);
609660
LK_LOG_INFO("Data reader thread exiting for \"{}\" track=\"{}\"", identity, track_name);
610661
});
611662
// NOLINTEND(bugprone-lambda-function-name)

src/tests/unit/test_subscription_thread_dispatcher.cpp

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
#include <livekit/livekit.h>
2121

2222
#include <atomic>
23+
#include <memory>
24+
#include <mutex>
2325
#include <thread>
2426
#include <unordered_map>
2527
#include <vector>
@@ -37,13 +39,25 @@ class SubscriptionThreadDispatcherTest : public ::testing::Test {
3739
using DataCallbackKey = SubscriptionThreadDispatcher::DataCallbackKey;
3840
using DataCallbackKeyHash = SubscriptionThreadDispatcher::DataCallbackKeyHash;
3941

42+
using ActiveDataReader = SubscriptionThreadDispatcher::ActiveDataReader;
43+
4044
static auto& audioCallbacks(SubscriptionThreadDispatcher& dispatcher) { return dispatcher.audio_callbacks_; }
4145
static auto& videoCallbacks(SubscriptionThreadDispatcher& dispatcher) { return dispatcher.video_callbacks_; }
4246
static auto& activeReaders(SubscriptionThreadDispatcher& dispatcher) { return dispatcher.active_readers_; }
4347
static auto& dataCallbacks(SubscriptionThreadDispatcher& dispatcher) { return dispatcher.data_callbacks_; }
4448
static auto& activeDataReaders(SubscriptionThreadDispatcher& dispatcher) { return dispatcher.active_data_readers_; }
4549
static auto& remoteDataTracks(SubscriptionThreadDispatcher& dispatcher) { return dispatcher.remote_data_tracks_; }
4650
static int maxActiveReaders() { return SubscriptionThreadDispatcher::kMaxActiveReaders; }
51+
52+
static std::thread extractDataReader(SubscriptionThreadDispatcher& dispatcher, DataFrameCallbackId id) {
53+
const std::scoped_lock<std::mutex> lock(dispatcher.lock_);
54+
return dispatcher.extractDataReaderThreadLocked(id);
55+
}
56+
57+
static void eraseDataReaderIfCurrent(SubscriptionThreadDispatcher& dispatcher, DataFrameCallbackId id,
58+
const std::shared_ptr<ActiveDataReader>& reader) {
59+
dispatcher.eraseDataReaderIfCurrent(id, reader);
60+
}
4761
};
4862

4963
// ============================================================================
@@ -478,6 +492,57 @@ TEST_F(SubscriptionThreadDispatcherTest, NoRemoteDataTracksInitially) {
478492
EXPECT_TRUE(remoteDataTracks(dispatcher).empty());
479493
}
480494

495+
// ============================================================================
496+
// Data reader replacement: cancellation and self-erase
497+
// ============================================================================
498+
499+
TEST_F(SubscriptionThreadDispatcherTest, ActiveDataReaderNotCancelledByDefault) {
500+
auto reader = std::make_shared<ActiveDataReader>();
501+
EXPECT_FALSE(reader->cancelled.load());
502+
}
503+
504+
TEST_F(SubscriptionThreadDispatcherTest, ExtractDataReaderMarksCancelledAndRemovesEntry) {
505+
SubscriptionThreadDispatcher dispatcher;
506+
auto reader = std::make_shared<ActiveDataReader>();
507+
activeDataReaders(dispatcher)[0] = reader;
508+
509+
auto extracted = extractDataReader(dispatcher, 0);
510+
511+
EXPECT_TRUE(reader->cancelled.load()) << "Extract must cancel so an in-flight subscribe aborts";
512+
EXPECT_FALSE(extracted.joinable()) << "No real thread was attached to the seeded reader";
513+
EXPECT_TRUE(activeDataReaders(dispatcher).empty());
514+
}
515+
516+
TEST_F(SubscriptionThreadDispatcherTest, ExtractMissingDataReaderIsNoOp) {
517+
SubscriptionThreadDispatcher dispatcher;
518+
auto extracted = extractDataReader(dispatcher, 42);
519+
EXPECT_FALSE(extracted.joinable());
520+
}
521+
522+
TEST_F(SubscriptionThreadDispatcherTest, EraseDataReaderIfCurrentRemovesMatchingEntry) {
523+
SubscriptionThreadDispatcher dispatcher;
524+
auto reader = std::make_shared<ActiveDataReader>();
525+
activeDataReaders(dispatcher)[0] = reader;
526+
527+
eraseDataReaderIfCurrent(dispatcher, 0, reader);
528+
529+
EXPECT_TRUE(activeDataReaders(dispatcher).empty());
530+
}
531+
532+
TEST_F(SubscriptionThreadDispatcherTest, EraseDataReaderIfCurrentLeavesReplacedEntry) {
533+
SubscriptionThreadDispatcher dispatcher;
534+
auto original = std::make_shared<ActiveDataReader>();
535+
auto replacement = std::make_shared<ActiveDataReader>();
536+
activeDataReaders(dispatcher)[0] = replacement;
537+
538+
// The original reader exited after being replaced; it must not evict the
539+
// newer reader that now owns the same callback id.
540+
eraseDataReaderIfCurrent(dispatcher, 0, original);
541+
542+
ASSERT_EQ(activeDataReaders(dispatcher).size(), 1u);
543+
EXPECT_EQ(activeDataReaders(dispatcher)[0], replacement);
544+
}
545+
481546
// ============================================================================
482547
// Data track destruction safety
483548
// ============================================================================

0 commit comments

Comments
 (0)