-
Notifications
You must be signed in to change notification settings - Fork 58
Unify reconnect pipeline and harden network transition handling #1645
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
aleksandar-apostolov
merged 48 commits into
develop
from
fix/cleanup-old-rtc-session-on-migration
May 1, 2026
Merged
Changes from 25 commits
Commits
Show all changes
48 commits
Select commit
Hold shift + click to select a range
d3365f5
refactor(core): add retryable signaling decorator, retry policies, an…
PratimMallick 1d7596a
fix(core): phased migration to prevent old-session race condition
PratimMallick b9bf598
refactor(core): add unified reconnect loop with strategy escalation
PratimMallick 8def70d
refactor(core): simplify RtcSession reconnect delegation
PratimMallick 6bf2b55
test(core): update tests for unified reconnect architecture
PratimMallick 156c3de
fix(core): align reconnect guard with JS SDK — allow migrate while co…
PratimMallick d1f5b55
fix(core): prevent reconnect race and clean up migration/fast-reconnect
PratimMallick 58d2bd4
fix(core): reliable network transition handling without dropping calls
PratimMallick 27a3bbf
api dump and spotless changes
PratimMallick 20af1ce
Fix - catch (Exception) swallows CancellationException in reconnect loop
PratimMallick 1a09c75
Fix - session.value!! force-unwrap TOCTOU race in reconnectRejoin/rec…
PratimMallick 58b9d59
spotless apply
PratimMallick 65586f2
Merge branch 'develop' of github.com:GetStream/stream-video-android i…
PratimMallick cde0dd8
Typo fixed
PratimMallick 92f3ebe
Fix: Added catch (e: CancellationException) { throw e } before the ge…
PratimMallick d5a52a0
Adding back the sfuReconnectTimeoutMillis so that there's no breaking…
PratimMallick d026ed2
Fixed nitpicks by coderabbit
PratimMallick 5788b8d
Merge branch 'develop' of github.com:GetStream/stream-video-android i…
PratimMallick d3fa38c
import fixed
PratimMallick bd77b9e
Renamed the local variable to loopStartTime
PratimMallick bfa174e
Renamed the local variable to loopIteration
PratimMallick 60b26d8
Add comments
PratimMallick f9e7bc9
All precondition guards in reconnectFast, reconnectRejoin, reconnectM…
PratimMallick 580549f
refactor(core): replace exception-driven reconnect with sealed Reconn…
PratimMallick 01c22fb
refactor(core): introduce SfuConnectionResult and unify SFU connect p…
PratimMallick f33ab4e
refactor(core): fix sendLeaveEvent ordering, deprecate connect, use i…
PratimMallick 45c8a3f
fix(core): add ICE restart after fast reconnect and gate retries on n…
PratimMallick 0c9d26d
Api dump
PratimMallick 4c6f159
refactor(core): unify SFU reconnection under Call.reconnect() and fix…
PratimMallick e2e213a
Merge branch 'develop' of github.com:GetStream/stream-video-android i…
PratimMallick 1472e7b
test(core): fix ReconnectEscalationTest assertions after leave-on-fai…
PratimMallick bc5b391
test(core): fix test isolation and add internalConnect tests
PratimMallick feb4231
refactor(core): unify SFU error handling and add ICE health monitoring
PratimMallick a43055c
fix(core): defer ICE monitoring start until SFU socket is connected
PratimMallick 9661fc7
Merge branch 'develop' of github.com:GetStream/stream-video-android i…
PratimMallick 70e8b70
refactor(core): address PR review — rename APIs, unify DISCONNECT flo…
PratimMallick 1998f38
Merge branch 'develop' of github.com:GetStream/stream-video-android i…
PratimMallick 9d35abf
- Donot call reconnect on getting DisconnectedPermanently
PratimMallick a12bdd9
fix(core): centralize network check in reconnect loop, fail-fast on s…
PratimMallick 11e4306
fix: Correctly update currentStrategy after reconnectDeadlineMillis h…
rahul-lohra a250b33
- move the logic of incrementing nonFastReconnectAttempts inside the …
PratimMallick 24d05e6
spotlessApply
PratimMallick 411b971
Merge branch 'develop' of github.com:GetStream/stream-video-android i…
PratimMallick 7f8e79d
Fix unit test cases
PratimMallick 3c82c67
Make isClosed internal
PratimMallick 2a26a66
FIx unit test
PratimMallick d647d6d
add a 5s delay before asserting recording label reappearance
aleksandar-apostolov d4f6204
Merge remote-tracking branch 'origin/fix/cleanup-old-rtc-session-on-m…
aleksandar-apostolov File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
581 changes: 406 additions & 175 deletions
581
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
362 changes: 191 additions & 171 deletions
362
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/RtcSession.kt
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
152 changes: 152 additions & 0 deletions
152
...n/kotlin/io/getstream/video/android/core/call/utils/RetryableSignalingServiceDecorator.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,152 @@ | ||
| /* | ||
| * Copyright (c) 2014-2026 Stream.io Inc. All rights reserved. | ||
| * | ||
| * Licensed under the Stream License; | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * https://github.com/GetStream/stream-video-android/blob/main/LICENSE | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package io.getstream.video.android.core.call.utils | ||
|
|
||
| import io.getstream.video.android.core.api.SignalServerService | ||
| import io.getstream.video.android.core.retry.StreamRetryPolicy | ||
| import io.getstream.video.android.core.retry.StreamRetryProcessor | ||
| import stream.video.sfu.models.Error | ||
| import stream.video.sfu.models.ErrorCode | ||
| import stream.video.sfu.models.ICETrickle | ||
| import stream.video.sfu.signal.ICERestartRequest | ||
| import stream.video.sfu.signal.ICERestartResponse | ||
| import stream.video.sfu.signal.ICETrickleResponse | ||
| import stream.video.sfu.signal.SendAnswerRequest | ||
| import stream.video.sfu.signal.SendAnswerResponse | ||
| import stream.video.sfu.signal.SendStatsRequest | ||
| import stream.video.sfu.signal.SendStatsResponse | ||
| import stream.video.sfu.signal.SetPublisherRequest | ||
| import stream.video.sfu.signal.SetPublisherResponse | ||
| import stream.video.sfu.signal.StartNoiseCancellationRequest | ||
| import stream.video.sfu.signal.StartNoiseCancellationResponse | ||
| import stream.video.sfu.signal.StopNoiseCancellationRequest | ||
| import stream.video.sfu.signal.StopNoiseCancellationResponse | ||
| import stream.video.sfu.signal.UpdateMuteStatesRequest | ||
| import stream.video.sfu.signal.UpdateMuteStatesResponse | ||
| import stream.video.sfu.signal.UpdateSubscriptionsRequest | ||
| import stream.video.sfu.signal.UpdateSubscriptionsResponse | ||
|
|
||
| /** | ||
| * SFU response error wrapper that lets the retry loop inspect `should_retry` | ||
| * without knowing the concrete response type. | ||
| */ | ||
| internal class SfuRetryableException(val sfuError: Error) : Exception(sfuError.message) | ||
|
|
||
| /** | ||
| * Decorator that retries SFU API calls on transient failures and propagates | ||
| * errors to callbacks for reconnection handling. | ||
| * | ||
| * **Retry behaviour** (via [StreamRetryProcessor]): | ||
| * - Network errors (IOException, timeout) are retried automatically. | ||
| * - SFU responses with `should_retry = true` are retried (except SIGNAL_LOST, | ||
| * which needs a WebSocket reconnect, not an HTTP retry). | ||
| * - Retries stop on success, terminal error (`should_retry = false`), | ||
| * max attempts exhausted, or coroutine cancellation. | ||
| * | ||
| * **Error propagation**: | ||
| * - SFU response errors → [onTerminalError] (maps to fastReconnect / rejoin). | ||
| * - Network errors after all retries exhausted → [onNetworkFailure] (triggers | ||
| * reconnect since the SFU is unreachable via HTTP). | ||
| */ | ||
| internal class RetryableSignalingServiceDecorator( | ||
| private val decorated: SignalServerService, | ||
| private val onTerminalError: (Error) -> Unit = {}, | ||
| private val onNetworkFailure: (Throwable) -> Unit = {}, | ||
| private val retryProcessor: StreamRetryProcessor = StreamRetryProcessor("SfuRetry"), | ||
| private val policy: StreamRetryPolicy = StreamRetryPolicy.linear( | ||
| minRetries = 1, | ||
| maxRetries = 3, | ||
| backoffStepMillis = 250, | ||
| maxBackoffMillis = 2_000, | ||
| initialDelayMillis = 0, | ||
| ), | ||
| ) : SignalServerService { | ||
|
|
||
| override suspend fun setPublisher(setPublisherRequest: SetPublisherRequest): SetPublisherResponse = | ||
| retryCall({ it.error }) { decorated.setPublisher(setPublisherRequest) } | ||
|
|
||
| override suspend fun sendAnswer(sendAnswerRequest: SendAnswerRequest): SendAnswerResponse = | ||
| retryCall({ it.error }) { decorated.sendAnswer(sendAnswerRequest) } | ||
|
|
||
| override suspend fun iceTrickle(iceTrickle: ICETrickle): ICETrickleResponse = | ||
| retryCall({ it.error }) { decorated.iceTrickle(iceTrickle) } | ||
|
|
||
| override suspend fun updateSubscriptions( | ||
| updateSubscriptionsRequest: UpdateSubscriptionsRequest, | ||
| ): UpdateSubscriptionsResponse = | ||
| retryCall({ it.error }) { decorated.updateSubscriptions(updateSubscriptionsRequest) } | ||
|
|
||
| override suspend fun updateMuteStates(updateMuteStatesRequest: UpdateMuteStatesRequest): UpdateMuteStatesResponse = | ||
| retryCall({ it.error }) { decorated.updateMuteStates(updateMuteStatesRequest) } | ||
|
|
||
| override suspend fun iceRestart(iceRestartRequest: ICERestartRequest): ICERestartResponse = | ||
| retryCall({ it.error }) { decorated.iceRestart(iceRestartRequest) } | ||
|
|
||
| override suspend fun sendStats(sendStatsRequest: SendStatsRequest): SendStatsResponse = | ||
| retryCall({ it.error }) { decorated.sendStats(sendStatsRequest) } | ||
|
|
||
| override suspend fun startNoiseCancellation(startNoiseCancellationRequest: StartNoiseCancellationRequest): StartNoiseCancellationResponse = | ||
| retryCall({ it.error }) { decorated.startNoiseCancellation(startNoiseCancellationRequest) } | ||
|
|
||
| override suspend fun stopNoiseCancellation(stopNoiseCancellationRequest: StopNoiseCancellationRequest): StopNoiseCancellationResponse = | ||
| retryCall({ it.error }) { decorated.stopNoiseCancellation(stopNoiseCancellationRequest) } | ||
|
|
||
| /** | ||
| * Wraps an SFU API call with retry + error propagation. | ||
| * | ||
| * Network errors throw naturally and are retried by the processor. | ||
| * SFU-level `should_retry=true` errors are converted to [SfuRetryableException] | ||
| * to re-enter the retry loop (except SIGNAL_LOST which requires WS reconnect). | ||
| * | ||
| * After the final outcome: | ||
| * - SFU response with error → [onTerminalError] | ||
| * - Network failure after all retries → [onNetworkFailure], then rethrow | ||
| */ | ||
| private suspend inline fun <T> retryCall( | ||
| crossinline errorExtractor: (T) -> Error?, | ||
| crossinline block: suspend () -> T, | ||
| ): T { | ||
| var lastResponse: T? = null | ||
|
|
||
| val result = retryProcessor.retry(policy) { | ||
| val response = block() | ||
| lastResponse = response | ||
| val error = errorExtractor(response) | ||
| if (error != null && error.should_retry && !error.requiresReconnect()) { | ||
| throw SfuRetryableException(error) | ||
|
PratimMallick marked this conversation as resolved.
|
||
| } | ||
| response | ||
| } | ||
|
|
||
| val response = result.getOrElse { throwable -> | ||
| if (throwable is SfuRetryableException && lastResponse != null) { | ||
| @Suppress("UNCHECKED_CAST") | ||
| lastResponse as T | ||
| } else { | ||
| onNetworkFailure(throwable) | ||
| throw throwable | ||
| } | ||
| } | ||
|
|
||
| errorExtractor(response)?.let { onTerminalError(it) } | ||
|
|
||
| return response | ||
| } | ||
|
|
||
| private fun Error.requiresReconnect(): Boolean = | ||
| code == ErrorCode.ERROR_CODE_PARTICIPANT_SIGNAL_LOST | ||
| } | ||
120 changes: 0 additions & 120 deletions
120
.../kotlin/io/getstream/video/android/core/call/utils/SignalLostSignalingServiceDecorator.kt
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.