Skip to content

subscription_thread_dispatcher: guard against duplicate room events#195

Open
stephen-derosa wants to merge 11 commits into
livekit:mainfrom
stephen-derosa:fix/subscription_thread_dispatcher_cleanup
Open

subscription_thread_dispatcher: guard against duplicate room events#195
stephen-derosa wants to merge 11 commits into
livekit:mainfrom
stephen-derosa:fix/subscription_thread_dispatcher_cleanup

Conversation

@stephen-derosa

@stephen-derosa stephen-derosa commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Overview

  • Hardens SubscriptionThreadDispatcher and Room remote track callback handling against duplicate subscription events and unsafe callback replacement
  • Readers track the subscribed track SID and skip redundant restarts when the same publication is re-subscribed
  • Data track teardown marks in-flight subscriptions as cancelled so republish/unpublish does not leave stale readers or duplicate FFI subscriptions behind
  • setOn*FrameCallback is deprecated in favor of [[nodiscard]] trySetOn*FrameCallback, which returns false when a reader is already active; callers must clearOn*FrameCallback before replacing a running callback
  • Deprecated setOn* overloads remain as thin forwards for backwards compatibility
  • Room::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 event

Public API Changes

Added:

  • Room::trySetOnAudioFrameCallback, trySetOnVideoFrameCallback, trySetOnVideoFrameEventCallback — return bool
  • Matching SubscriptionThreadDispatcher::trySetOn* methods

Deprecated:

  • Room::setOnAudioFrameCallback, setOnVideoFrameCallback, setOnVideoFrameEventCallback
  • Matching SubscriptionThreadDispatcher::setOn* methods

Test-only:

  • RemoteDataTrack::testFfiHandleId() removed; tests use internal RemoteDataTrackTestAccess instead

README and Doxygen updated to document the deprecation and replacement pattern.

Testing

  • Unit tests cover duplicate-event SID guards, trySetOn* rejection while a reader is active, clear-then-replace flows, and data reader cancellation/republish behavior
  • Integration tests verify room lifecycle delegate callbacks fire exactly once and that auto-wired data callbacks follow unpublish/republish under the same track name
  • Existing audio, video metadata, and room callback tests updated to use trySetOn*

@stephen-derosa stephen-derosa self-assigned this Jul 6, 2026
Copilot AI review requested due to automatic review settings July 6, 2026 21:39
@stephen-derosa stephen-derosa marked this pull request as draft July 6, 2026 21:39

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

devin-ai-integration[bot]

This comment was marked as resolved.

@stephen-derosa stephen-derosa changed the title rm unneeded function, better cleanup subscription_thread_dispatcher: guard against duplicate room events Jul 7, 2026
@stephen-derosa stephen-derosa force-pushed the fix/subscription_thread_dispatcher_cleanup branch from 2f313fe to f1ff6e6 Compare July 7, 2026 18:17
…ks. Deprecate a setOn*Callback(), replace with trySetOn*Callback()
@stephen-derosa stephen-derosa requested a review from Copilot July 7, 2026 22:11
@stephen-derosa stephen-derosa marked this pull request as ready for review July 7, 2026 22:11

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

This comment was marked as resolved.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

Open in Devin Review

Comment thread src/subscription_thread_dispatcher.cpp

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.

Comment thread src/room.cpp
Comment on lines +469 to 473
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);
}
Comment thread src/room.cpp
Comment on lines +478 to 482
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);
}
Comment thread src/room.cpp
Comment on lines +487 to 491
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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread include/livekit/room.h
/// @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,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants