| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Sorry, something went wrong.
There was a problem hiding this comment.
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:
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.
Sorry, something went wrong.
| /// @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?
Sorry, something went wrong.
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?
Sorry, something went wrong.
There was a problem hiding this comment.
Fair point, technically speaking those methods were never marked noexcept and anything within them could throw (STL container exceptions, runtime exceptions from FFI stuff, etc.) so I think it's the least of the evils?
Sorry, something went wrong.
There was a problem hiding this comment.
hm yeah im a little torn here. I dont want to just start throwing underneath applications, especially when our mantra has somewhat been team #nothrow 😅
but the point of other things can throw inside the function are valid 🤔
Sorry, something went wrong.
There was a problem hiding this comment.
Also torn, I don't love my suggestion, to me keeping the API clean/avoiding deprecations is the least of the evils. Maybe @xianshijing-lk can chime in
Sorry, something went wrong.
| /// @return @c true if the callback was registered; @c false if a reader is | ||
| /// already active for the key (call @ref clearOnVideoFrameCallback | ||
| /// first) or the room has no dispatcher. | ||
| [[nodiscard]] bool trySetOnVideoFrameCallback(const std::string& participant_identity, const std::string& track_name, |
There was a problem hiding this comment.
I guess I didn't understand why these callback events need to be in the room ?
couldn't they be in the remote participants?
And does this frame callbacks work on both local / remote participants ?
Sorry, something went wrong.
There was a problem hiding this comment.
the room has access to the room events which are needed to set callbacks to incoming track messages -- the remote participant doesnt really.
This callback only works for local participants, the intention is to provide a nice ergonomic way for the user to set callbacks for incoming messages.
Sorry, something went wrong.
| } else { | ||
| // The track is not subscribed yet. The callback is registered; the reader | ||
| // starts when the track is subscribed (see kTrackSubscribed in onEvent). | ||
| LK_LOG_DEBUG( |
There was a problem hiding this comment.
should you still return true here ?
Sorry, something went wrong.
There was a problem hiding this comment.
good question, yes since the user has successfully set the callback. It doesnt necessarily mean that a track has been published or that they will get messages on said track. If we were to only return true to a callback being set after a track has been published, then early joining participants that want incoming messages will effectively have to periodically set callbacks until success, or listen to room events -- both of which defeat the purpose of the ergonomic functionality of these setOn*Callback functions.
Sorry, something went wrong.
| // If we've already subscribed to the track, handle it immediately | ||
| auto track = findSubscribedRemoteTrack(participant_identity, track_name); | ||
| if (track) { | ||
| subscription_thread_dispatcher_->handleTrackSubscribed(participant_identity, track_name, track); |
There was a problem hiding this comment.
I think I forgot some technical details here, is this subscription_thread_dispatcher_ a manager that manages all the subscription thread ?
and does one callback has one corresponding subscription thread ? or one thread that handles all the callbacks ?
Sorry, something went wrong.
There was a problem hiding this comment.
I think I forgot some technical details here, is this subscription_thread_dispatcher_ a manager that manages all the subscription thread ?
it manages subscribing to tracks and providing incoming frames to the users callbacks
and does one callback has one corresponding subscription thread ? or one thread that handles all the callbacks ?
Each subscription gets its own thread
Sorry, something went wrong.
There was a problem hiding this comment.
@alan-george-lk
In this work, i realized subscription_thread_manager is marked public:
I missed this in the review for the LIVEKIT_API PR. any thoughts on making this private?
I ran a query on this and it doesnt look like:
Sorry, something went wrong.
There was a problem hiding this comment.
discussed in the standup need to confirm that:
Sorry, something went wrong.
There was a problem hiding this comment.
consider moving to src/ so that this isn't reachable
Sorry, something went wrong.
|
ensure that this also works for local publishing of video/audio |
Sorry, something went wrong.
…plicate room events test SubscriptionThreadDispatcher: proper replacing of audio/video callbacks. Deprecate a setOn*Callback(), replace with trySetOn*Callback() fix thread detaching
There was a problem hiding this comment.
Devin Review found 1 new potential issue.
4 flags not posted on this PR by your GitHub settings — view them in Devin Review. (Configure)
Sorry, something went wrong.
| if (it->second && isSelfThread(it->second->thread_id)) { | ||
| // The caller IS this reader, so it reached us from inside its own data frame | ||
| // callback. Joining would be a self-join, and unlike media readers a data | ||
| // reader cannot be detached: it re-enters the dispatcher after the callback | ||
| // returns. Leave the slot in place -- the reader exits on its own once its | ||
| // stream closes, and stopAll() reaps it. | ||
| LK_LOG_ERROR( | ||
| "Data reader for callback id={} tried to tear itself down from inside its " | ||
| "own data frame callback; leaving the reader in place. Removing a data " | ||
| "callback from within that callback is not supported", | ||
| id); | ||
| return {}; | ||
| } |
There was a problem hiding this comment.
🟡 Removing a data callback from inside itself never stops delivery
When the removal runs from inside the data callback it targets, the self-join guard (extractDataReaderThreadLocked) returns without cancelling the reader or closing its stream, so the reader keeps reading and keeps invoking the removed callback until the track is unpublished or the room is torn down. The unpublish path handles the same case by closing the stream.
Prompt for agentsIn extractDataReaderThreadLocked (src/subscription_thread_dispatcher.cpp), the self-thread branch returns early to avoid a self-join, but it does not stop the reader: it neither sets cancelled nor closes the reader's stream. As a result a removeOnDataFrameCallback call made from inside the target data callback leaves the reader looping on stream->read() and invoking its own captured copy of the callback indefinitely. The sibling case in handleDataTrackUnpublished handles this correctly by setting reader->cancelled = true and closing reader->stream under sub_mutex before leaving the slot in place. Consider mirroring that here: mark the reader cancelled and close its stream (guarded by sub_mutex; lock_ is already held so the lock_->sub_mutex order is preserved) before returning the empty thread, so the reader exits on its next read and stops delivering to the removed callback. Note the comment claiming 'the reader exits on its own once its stream closes' is currently inaccurate because nothing closes the stream in this branch.
Was this helpful? React with 👍 or 👎 to provide feedback.
Sorry, something went wrong.
| /// Registering again for a key that already has an active reader replaces the | ||
| /// callback in place: the previous reader's stream is closed and its thread | ||
| /// is joined before this call returns, and @ref Room then starts a fresh | ||
| /// reader bound to the new callback. When this call returns, the previous |
There was a problem hiding this comment.
at first i thought this was a bit verbose, but i actually think this is appropriate
Sorry, something went wrong.
| Back | FazBrowse Home | New Git URL |
Overview
Fixes silent callback-replacement failures and duplicate-subscription churn in SubscriptionThreadDispatcher. No public API changes — setOn*FrameCallback now does what it always documented.
The bug
Reader threads capture the frame callback by value. setOn*FrameCallback documented itself as "register or replace", but when a reader was already running, overwriting the registration map only replaced the dispatcher's copy — the running thread kept invoking the old callback indefinitely. Replacement was a silent no-op: no error, no warning, frames just kept going to the callback the caller thought they had discarded.
Changes
Behavioral notes (documented, not enforced)
Public API
No signature changes. include/livekit/room.h changes are documentation only.
Test-only: RemoteDataTrack::testFfiHandleId() removed in favor of RemoteDataTrackTestAccess; new RoomTestAccess exposes reader counts to integration tests.
Testing