Skip to content

fix(core): single-flight Call.join to stop concurrent-join race - #1764

Open
PratimMallick wants to merge 2 commits into
developfrom
fix/join-single-flight
Open

fix(core): single-flight Call.join to stop concurrent-join race#1764
PratimMallick wants to merge 2 commits into
developfrom
fix/join-single-flight

Conversation

@PratimMallick

@PratimMallick PratimMallick commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Goal

closes AND-1379
Prevent overlapping Call.join() calls from creating multiple RtcSessions that share the same sessionId. The SFU keeps only the latest participant and evicts the others, which leaves zombie publishers that cannot publish A/V and often fail subsequent RPCs with PARTICIPANT_NOT_FOUND, triggering reconnect/rejoin loops.

This addresses a class of Android reconnect / join-storm failures observed when the app (or SDK paths such as lobby + CallActivity / auto-join) invoke join() concurrently on the same Call instance. The previous session.value != null guard is insufficient because the session is only set after the coordinator round-trip.

Implementation

  • Add single-flight coalescing on CallJoinCoordinator.join() via Mutex + CompletableDeferred: the first caller runs the join; concurrent callers await the same Result.
  • Keep the shared work on the caller's coroutine so ViewModel/UI cancellation still aborts the in-flight join.
  • On SFU connect failure during join, discardFailedSession() clears the active session and calls RtcSession.cleanup() so failed sessions do not keep issuing RPCs after eviction.
  • Unit tests cover concurrent join coalescing, once-only join setup (interceptor/analytics), fresh join after a completed flight, and cleanup of a session that cannot connect.

Testing

  • ./gradlew :stream-video-android-core:spotlessApply
  • ./gradlew :stream-video-android-core:testDebugUnitTest --tests 'io.getstream.video.android.core.call.components.CallJoinCoordinatorTest' — passed
  • Manual dogfood (before/after):
    • Without single-flight: 5 concurrent join() → 5× joinInternal within ~7ms, multiple RtcSessions, publisher ICE CONNECTED → DISCONNECTED / stuck CHECKING, unable to publish A/V
    • With single-flight: concurrent callers log Single-flight: join already in flight and share one session
Failure modes this mitigates
- Duplicate SFU joinRequests for the same session_id / unified_session_id
- Zombie RtcSessions after SFU participant eviction
- PARTICIPANT_NOT_FOUND on SetPublisher / UpdateMuteStates / IceTrickle / sendAnswer
- Publisher PC thrash (NEW→CLOSED for losers; survivor stuck CHECKING)
- Inability to publish audio/video after a “successful” join UI
- Cascading full-rejoin / reconnect loops driven by those RPC failures

☑️Contributor Checklist

General

  • I have signed the Stream CLA (required)
  • Assigned a person / code owner group (required)
  • Thread with the PR link started in a respective Slack channel (required internally)
  • PR targets the develop branch
  • PR is linked to the GitHub issue it resolves

Code & documentation

  • Changelog is updated with client-facing changes
  • New code is covered by unit tests
  • Comparison screenshots added for visual changes
  • Affected documentation updated (KDocs, docusaurus, tutorial)
  • Tutorial starter kit updated
  • Examples/guides starter kits updated (stream-video-examples)

☑️Reviewer Checklist

  • XML sample runs & works
  • Compose sample runs & works
  • Tutorial starter kit
  • Example starter kits work
  • UI Changes correct (before & after images)
  • Bugs validated (bugfixes)
  • New feature tested and works
  • Release notes and docs clearly describe changes
  • All code we touched has new or updated KDocs
  • Check the SDK Size Comparison table in the CI logs

🎉 GIF

N/A — core join orchestration fix, no UI changes.

Made with Cursor

Summary by CodeRabbit

  • Bug Fixes

    • Improved call joining when multiple join requests occur simultaneously.
    • Prevented failed connections from leaving behind incomplete sessions.
    • Ensured join failures and cancellations are consistently propagated.
  • Reliability

    • Completed joins now start fresh on subsequent requests.
    • Preserved call interceptor behavior during initialization.

Coalesce overlapping join() callers onto one in-flight attempt and clean
up sessions that fail to connect, preventing SFU-evicted zombie publishers.

Co-authored-by: Cursor <cursoragent@cursor.com>
@PratimMallick
PratimMallick requested a review from a team as a code owner August 10, 2026 11:18
@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

PR checklist ✅

All required conditions are satisfied:

  • Title length is OK (or ignored by label).
  • At least one pr: label exists.
  • Sections ### Goal, ### Implementation, and ### Testing are filled, or the PR is bot-authored.
  • An issue is linked (Linear ticket or GitHub issue), or the PR is bot-authored.

🎉 Great job! This PR is ready for review.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

CallJoinCoordinator now shares concurrent join work through a single in-flight result. Terminal or unrecoverable SFU failures clean up the session created by the failed join. Tests cover concurrency, interceptor retention, fresh joins, and cleanup.

Changes

Call join coordination

Layer / File(s) Summary
Single-flight join execution
stream-video-android-core/src/main/kotlin/.../CallJoinCoordinator.kt, stream-video-android-core/src/test/kotlin/.../CallJoinCoordinatorTest.kt
Concurrent callers share one join request, SFU connection, and session. Initialization runs once and preserves the first interceptor. Later joins start a new request.
Failed session cleanup
stream-video-android-core/src/main/kotlin/.../CallJoinCoordinator.kt, stream-video-android-core/src/test/kotlin/.../CallJoinCoordinatorTest.kt
Terminal and unrecoverable SFU failures discard and clean up the session created by the current join. Tests verify session flow clearing.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ConcurrentCallers
  participant CallJoinCoordinator
  participant executeJoin
  participant API
  participant SFUConnection
  ConcurrentCallers->>CallJoinCoordinator: call join
  CallJoinCoordinator->>executeJoin: execute one join
  executeJoin->>API: request join
  executeJoin->>SFUConnection: connect once
  CallJoinCoordinator-->>ConcurrentCallers: share join result
Loading
sequenceDiagram
  participant executeJoin
  participant SFUConnection
  participant CallJoinCoordinator
  participant Session
  executeJoin->>SFUConnection: report terminal failure
  executeJoin->>CallJoinCoordinator: discard failed session
  CallJoinCoordinator->>Session: clear and clean up session
  CallJoinCoordinator-->>executeJoin: return failure
Loading

Possibly related PRs

Suggested labels: pr:internal

Suggested reviewers: rahul-lohra

Poem

I’m a rabbit guarding joins tonight,
One shared hop keeps callers right.
Failed sessions leave no trace,
Fresh joins find their proper place.
Interceptors stay in line—
Thump, thump, concurrency works fine!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The description clearly states the goal, implementation, testing, and non-UI scope, with relevant checklist items recorded.
Title check ✅ Passed The title clearly and concisely describes the main change: preventing concurrent Call.join races with single-flight coordination.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/join-single-flight

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/components/CallJoinCoordinator.kt`:
- Line 390: Update executeJoin’s failed-join cleanup so it clears the active
session only when it is still the same localSession; preserve any replacement
installed by discardFailedSession during recovery. Add a recovery-failure test
that installs a replacement session before returning Failure and verifies the
replacement remains active.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5b98171e-ff06-4133-a27e-e543cf2d2d64

📥 Commits

Reviewing files that changed from the base of the PR and between b1ba57b and 6013e68.

📒 Files selected for processing (2)
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/components/CallJoinCoordinator.kt
  • stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/call/components/CallJoinCoordinatorTest.kt

@github-actions

Copy link
Copy Markdown
Contributor

SDK Size Comparison 📏

SDK Before After Difference Status
stream-video-android-core 12.29 MB 12.29 MB 0.00 MB 🟢
stream-video-android-ui-xml 5.70 MB 5.70 MB 0.00 MB 🟢
stream-video-android-ui-compose 6.19 MB 6.20 MB 0.02 MB 🟢

@PratimMallick PratimMallick added the pr:bug Fixes a bug label Aug 10, 2026
Remove the discardFailedSession ownership guard. Once join is returning
Failure (including after failed join-time recovery), clear the active
slot and cleanup both the join session and any reconnect replacement.

Co-authored-by: Cursor <cursoragent@cursor.com>
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
76.4% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

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

Labels

pr:bug Fixes a bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant