Skip to content

Recover initial join when connect safety-timeout leaves no reconnect#1741

Merged
PratimMallick merged 5 commits into
developfrom
fix/sfu-join-self-triggered-reconnect
Jul 13, 2026
Merged

Recover initial join when connect safety-timeout leaves no reconnect#1741
PratimMallick merged 5 commits into
developfrom
fix/sfu-join-self-triggered-reconnect

Conversation

@PratimMallick

@PratimMallick PratimMallick commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Goal

Follow-up to #1740 and AN-1294 That PR added a safety-timeout to RtcSession.connectInternal so the SFU connect wait can no longer suspend forever. But there is still a gap on the initial join path: when the safety-timeout fires, the socket is left in a non-terminal state that stateJob does not react to, so no Call.reconnect is ever launched. _join then calls didReconnectSucceed() and waits for a recovery loop that will never start — blocking indefinitely.

This PR closes that gap and makes the abandoned connect attempt safe to leave behind.

Implementation

  • SfuConnectionResult.Failure.reconnectTriggered — new flag that tells the join flow whether a recovery loop has already been launched by stateJob:
    • true for terminal disconnect states stateJob reacts to (a Call.reconnect is already in flight — the caller must not start another).
    • false for the connect safety-timeout, which leaves the socket in a state stateJob ignores (nothing will drive recovery on its own).
  • Tear down the abandoned socket on safety-timeout — the connect attempt is still in flight when the safety-timeout fires, so connectInternal now calls socketConnection.disconnect() before returning. This prevents a late Connected/JoinResponse from an abandoned attempt resurfacing through stateJob and resurrecting a dead session ("zombie socket").
  • Call._join self-triggers recovery — on a recoverable failure with reconnectTriggered == false, _join now launches a REJOIN itself before awaiting the outcome (a full REJOIN, not FAST, because the initial join never completed — there is no negotiated media path to resume, so FAST would just burn attempts on PARTICIPANT_NOT_FOUND). When reconnectTriggered == true it defers to the existing loop as before.
  • Rename SfuSocketState.Disconnected.canTriggersReconnect()triggersStateJobReconnect() for clarity (it describes which states stateJob acts on).
  • demo-app — align connectionTimeoutInMs with the SDK default (5_000L) instead of the hardcoded 12_000L.

Testing

  • ./gradlew :stream-video-android-core:testDebugUnitTest --tests "*.JoinRecoverableFailureTest" --tests "*.RtcSessionTest2" — passing
  • ./gradlew :stream-video-android-core:spotlessApply :demo-app:spotlessApply — clean

New/updated unit tests:

  • JoinRecoverableFailureTest
    • recoverable failure with reconnect already triggered awaits the existing loop — asserts _join does not start its own reconnect when reconnectTriggered == true.
    • recoverable failure with no reconnect triggered starts a REJOIN itself — asserts _join launches a REJOIN when reconnectTriggered == false.
  • RtcSessionTest2 — safety-timeout test now verifies the abandoned socket is disconnect()-ed.

Manual: reproduced a silent connect hang (OkHttp callTimeout/readTimeout set to 0 locally + Charles breakpoint on the SFU WS upgrade). Confirmed the safety-timeout fires, the socket is torn down, _join self-triggers a REJOIN, and the flow ends in a bounded failure instead of hanging.

☑️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 on SfuConnectionResult.Failure)
  • 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

No UI change — skipping.

Summary by CodeRabbit

  • Bug Fixes
    • Improved video connection recovery when the initial websocket join fails, including clearer handling of whether a reconnect is already in progress.
    • Tightened timeout handling so stalled connections are cleanly disconnected and can’t later re-activate an abandoned session.
    • Reduced the initial connection timeout for faster failure detection.
  • Tests
    • Updated recovery tests to cover both automatic reconnect and caller-initiated rejoin flows.
    • Added coverage for disconnect behavior when the safety timeout is reached.

… reconnect

When RtcSession.connectInternal hits its own safety-timeout, the socket is
left in a non-terminal state that stateJob ignores, so no Call.reconnect is
ever launched and _join's didReconnectSucceed() would block forever.

- Add SfuConnectionResult.Failure.reconnectTriggered so the join flow can tell
  whether a recovery loop has already been started by stateJob.
- On the safety-timeout path, tear down the abandoned (still-in-flight) socket
  so a late Connected/JoinResponse can't resurface and resurrect the session.
- In Call._join, trigger a REJOIN ourselves when a recoverable failure reports
  no reconnect was triggered; otherwise await the existing loop.
- Rename canTriggersReconnect() to triggersStateJobReconnect() for clarity.
- demo-app: align connectionTimeoutInMs with the SDK default (5s).

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

github-actions Bot commented Jul 8, 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.

@PratimMallick PratimMallick added the pr:improvement Enhances an existing feature or code label Jul 8, 2026
@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Reconnect handling in RtcSession.connectInternal and Call._join now distinguishes recoverable SFU failures that already triggered reconnect from those that haven't, disconnecting stuck sockets on safety timeout and explicitly starting a REJOIN when needed. Demo-app connection timeout reduced; tests updated accordingly.

Changes

Recoverable reconnect handling

Layer / File(s) Summary
SfuConnectionResult.Failure semantics and helper rename
stream-video-android-core/.../call/RtcSession.kt
Documentation for recoverable/reconnectTriggered clarified; canTriggersReconnect renamed to triggersStateJobReconnect.
connectInternal safety-timeout disconnect and reconnectTriggered computation
stream-video-android-core/.../call/RtcSession.kt
On safety timeout, socket is explicitly disconnected and reconnectTriggered=false is set; terminal disconnected states derive recoverable/reconnectTriggered from the renamed helper.
Call._join reconnect branching
stream-video-android-core/.../Call.kt
_join checks reconnectTriggered and starts a REJOIN reconnect if not already triggered, else awaits the existing reconnect outcome, with added logging.
Tests validating reconnect branching and socket disconnect
stream-video-android-core/src/test/.../JoinRecoverableFailureTest.kt, .../RtcSessionTest2.kt
New/updated tests cover both reconnectTriggered branches in _join and assert socket disconnect() on safety timeout.
Demo-app connection timeout reduction
demo-app/.../StreamVideoInitHelper.kt
connectionTimeoutInMs reduced from 12_000L to 5_000L.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Call
  participant RtcSession
  participant SfuSocket

  Call->>RtcSession: connectInternal()
  RtcSession->>SfuSocket: connect, await terminal state
  alt safety timeout, non-terminal
    RtcSession->>SfuSocket: disconnect()
    RtcSession-->>Call: Failure(recoverable=true, reconnectTriggered=false)
  else terminal disconnected
    RtcSession-->>Call: Failure(recoverable, reconnectTriggered via triggersStateJobReconnect)
  end
  Call->>Call: check reconnectTriggered
  alt reconnectTriggered=false
    Call->>Call: reconnect(WEBSOCKET_RECONNECT_STRATEGY_REJOIN)
  else reconnectTriggered=true
    Call->>Call: await existing reconnect outcome
  end
Loading

Possibly related PRs

Suggested labels: pr:improvement

Suggested reviewers: rahul-lohra, aleksandar-apostolov

Poem

A socket stuck, then cut with grace,
A REJOIN hop to save the race. 🐇
No more waiting lost in space,
Five seconds now, a tighter pace.
Hop, reconnect, and win the chase!

🚥 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.
Title check ✅ Passed The title clearly summarizes the main change: handling initial join recovery after the connect safety timeout.
Description check ✅ Passed The description covers goal, implementation, testing, checklists, and notes the lack of UI changes.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/sfu-join-self-triggered-reconnect

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.

🧹 Nitpick comments (1)
stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/rtc/RtcSessionTest2.kt (1)

361-368: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the safety-timeout reconnect contract too.

This test now covers socket teardown; add reconnectTriggered=false so it also locks the contract that makes _join self-trigger REJOIN for this path.

Proposed test assertion
             assertTrue("Expected recoverable=true for a safety timeout", result.recoverable)
+            assertFalse(
+                "Expected reconnectTriggered=false for a safety timeout",
+                result.reconnectTriggered,
+            )
             assertEquals(
                 AnalyticsCallAbortReason.REQUEST_TIMEOUT,
                 result.abortReason,
🤖 Prompt for 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.

In
`@stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/rtc/RtcSessionTest2.kt`
around lines 361 - 368, The safety-timeout test in RtcSessionTest2 currently
verifies recoverable and disconnect behavior but does not lock the reconnect
contract; update the same test around the `_join`/`join` flow to also assert
that `reconnectTriggered` is false for this path. Use the existing result object
in the safety-timeout scenario and add the missing assertion alongside the
current `recoverable`, `abortReason`, and `mockSocketConnection.disconnect()`
checks so the REJOIN self-trigger behavior is covered.
🤖 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.

Nitpick comments:
In
`@stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/rtc/RtcSessionTest2.kt`:
- Around line 361-368: The safety-timeout test in RtcSessionTest2 currently
verifies recoverable and disconnect behavior but does not lock the reconnect
contract; update the same test around the `_join`/`join` flow to also assert
that `reconnectTriggered` is false for this path. Use the existing result object
in the safety-timeout scenario and add the missing assertion alongside the
current `recoverable`, `abortReason`, and `mockSocketConnection.disconnect()`
checks so the REJOIN self-trigger behavior is covered.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 7e2b610c-b328-4b9b-9bd8-09a10690a7d2

📥 Commits

Reviewing files that changed from the base of the PR and between 374d8c1 and 22c384e.

📒 Files selected for processing (5)
  • demo-app/src/main/kotlin/io/getstream/video/android/util/StreamVideoInitHelper.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/RtcSession.kt
  • stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/rtc/JoinRecoverableFailureTest.kt
  • stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/rtc/RtcSessionTest2.kt

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

SDK Size Comparison 📏

SDK Before After Difference Status
stream-video-android-core 12.27 MB 12.27 MB 0.00 MB 🟢
stream-video-android-ui-xml 5.68 MB 5.68 MB 0.00 MB 🟢
stream-video-android-ui-compose 6.20 MB 6.20 MB 0.00 MB 🟢

@aleksandar-apostolov aleksandar-apostolov changed the title fix(core): recover initial join when connect safety-timeout leaves no reconnect Recover initial join when connect safety-timeout leaves no reconnect Jul 8, 2026
PratimMallick and others added 2 commits July 8, 2026 18:09
Address PR review: reconnect-orchestration state should not live on the
SfuConnectionResult.Failure DTO. Replace the boolean flag with a typed error
so callers of connectInternal branch on the failure kind.

- Add sealed SfuConnectException with Timeout (connect safety-timeout tore down
  a stuck socket; no recovery started) and Disconnected (terminal SFU state).
- connectInternal now returns these typed errors instead of Exception("msg").
- Drop SfuConnectionResult.Failure.hasReconnectStarted; _join self-triggers a
  REJOIN only when the error is SfuConnectException.Timeout, else awaits the
  loop stateJob already started.
- Update tests to construct/assert the typed errors.

Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: Refactor with SfuConnectFailureCause code

* fix: Add kdoc

* fix: Replace Singletone shared Call.testInstanceProvider.rtcSessionCreator with  Call..unitTestRtcSessionFactory
@rahul-lohra

Copy link
Copy Markdown
Contributor

All concerns resolved 👍

@rahul-lohra rahul-lohra 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.

lgtm

@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
48.2% Coverage on New Code (required ≥ 80%)
B Maintainability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

💡 Need a hand with PR review? Try Gitar by Sonar!

@PratimMallick PratimMallick merged commit 96bc4bc into develop Jul 13, 2026
16 of 17 checks passed
@PratimMallick PratimMallick deleted the fix/sfu-join-self-triggered-reconnect branch July 13, 2026 12:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr:improvement Enhances an existing feature or code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants