subscription_thread_dispatcher: guard against duplicate room events#195
subscription_thread_dispatcher: guard against duplicate room events#195stephen-derosa wants to merge 11 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR refines SubscriptionThreadDispatcher’s reader-thread lifecycle handling to avoid redundant restarts and to ensure data reader threads reliably tear down (especially when a data-track subscription is still in flight), aligning with the SDK’s threading model by preventing teardown hangs.
Changes:
- Add cancellation signaling for active data readers and ensure cancellation is set before closing/joining during unpublish and teardown.
- Introduce a self-cleanup path (
eraseDataReaderIfCurrent) so data reader threads can safely remove stale slots when they exit. - Skip redundant audio/video/data reader restarts when the same track SID is already active.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| src/tests/unit/test_subscription_thread_dispatcher.cpp | Adds unit tests covering data reader cancellation semantics and self-erase behavior. |
| src/subscription_thread_dispatcher.cpp | Implements cancellation-before-close, self-erase cleanup, and redundant reader-start suppression. |
| include/livekit/subscription_thread_dispatcher.h | Adds per-reader state (track_sid, cancelled) and declares the new cleanup helper. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…plicate room events test
2f313fe to
f1ff6e6
Compare
| bool const result = trySetOnAudioFrameCallback(participant_identity, track_name, std::move(callback), opts); | ||
| if (!result) { | ||
| LK_LOG_ERROR("Room::setOnAudioFrameCallback: failed to set callback for participant={} track_name={}", | ||
| participant_identity, track_name); | ||
| } |
| bool const result = trySetOnVideoFrameCallback(participant_identity, track_name, std::move(callback), opts); | ||
| if (!result) { | ||
| LK_LOG_ERROR("Room::setOnVideoFrameCallback: failed to set callback for participant={} track_name={}", | ||
| participant_identity, track_name); | ||
| } |
| bool const result = trySetOnVideoFrameEventCallback(participant_identity, track_name, std::move(callback), opts); | ||
| if (!result) { | ||
| LK_LOG_ERROR("Room::setOnVideoFrameEventCallback: failed to set callback for participant={} track_name={}", | ||
| participant_identity, track_name); | ||
| } |
|
|
||
| private: | ||
| friend class Room; | ||
| #ifdef LIVEKIT_TEST_ACCESS |
There was a problem hiding this comment.
Not sure if better or worse, but I typically use GTest's FRIEND_TEST macro so you can just give a single test suite/case access to what it needs. May break down a bit between unit and integration test binaries if they have different suite names (could unify name). Basically decays down to a friend class <blah> anyways, just looks a bit cleaner: https://google.github.io/googletest/reference/testing.html#FRIEND_TEST
| /// @return @c true if the callback was registered; @c false if a reader is | ||
| /// already active for the key (call @ref clearOnAudioFrameCallback | ||
| /// first) or the room has no dispatcher. | ||
| [[nodiscard]] bool trySetOnAudioFrameCallback(const std::string& participant_identity, const std::string& track_name, |
There was a problem hiding this comment.
From what I can tell, these new "try" methods have the same inputs as the deprecated prior ones, just a new return type (and name).
This is actually a situation where I think maintaining the API and using exceptions is cleaner than deprecation/new API just to get the bool return type.
Thoughts on this? Could we not move the new implementations from room.cpp into the older versions and throw instead of returning false?
There was a problem hiding this comment.
isnt that still changing the public API though? maybe im wrong but i would think adding a throw to a function that previously didnt would require a major bump?
Overview
SubscriptionThreadDispatcherandRoomremote track callback handling against duplicate subscription events and unsafe callback replacementsetOn*FrameCallbackis deprecated in favor of[[nodiscard]] trySetOn*FrameCallback, which returnsfalsewhen a reader is already active; callers mustclearOn*FrameCallbackbefore replacing a running callbacksetOn*overloads remain as thin forwards for backwards compatibilityRoom::trySetOn*registers with the dispatcher and starts a reader immediately when the matching remote track is already subscribed; otherwise the reader starts on the next subscribe eventPublic API Changes
Added:
Room::trySetOnAudioFrameCallback,trySetOnVideoFrameCallback,trySetOnVideoFrameEventCallback— returnboolSubscriptionThreadDispatcher::trySetOn*methodsDeprecated:
Room::setOnAudioFrameCallback,setOnVideoFrameCallback,setOnVideoFrameEventCallbackSubscriptionThreadDispatcher::setOn*methodsTest-only:
RemoteDataTrack::testFfiHandleId()removed; tests use internalRemoteDataTrackTestAccessinsteadREADME and Doxygen updated to document the deprecation and replacement pattern.
Testing
trySetOn*rejection while a reader is active, clear-then-replace flows, and data reader cancellation/republish behaviortrySetOn*