Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 1f109690 authored by Abhishek Aggarwal's avatar Abhishek Aggarwal
Browse files

fix(session): bound stale-refresh retry loop

parent 83d8bceb
Loading
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
@@ -35,3 +35,10 @@ class SessionRefreshExecutionException(
    message = cause.localizedMessage ?: "Auth refresh failed unexpectedly",
    cause = cause,
)

class SessionRefreshContentionException(
    cause: Throwable,
) : SessionRefreshException(
    message = "Gave up waiting for different in-flight auth refreshes after repeated failures",
    cause = cause,
)
+43 −16
Original line number Diff line number Diff line
@@ -49,6 +49,12 @@ class SessionStateHolder @Inject constructor(
    companion object {
        @VisibleForTesting
        internal const val REFRESH_WAIT_TIMEOUT_MS = 10_000L

        // Caps how many times a refreshSessions() call may loop back after observing a
        // different in-flight refresh fail. Without a bound, a steady stream of competing
        // refresh requests that all fail can keep one caller suspended indefinitely.
        @VisibleForTesting
        internal const val MAX_DIFFERENT_REFRESH_RETRIES = 3
    }

    private val refreshMutex = Mutex()
@@ -69,6 +75,8 @@ class SessionStateHolder @Inject constructor(
    override val authRefreshSnapshot: StateFlow<AuthRefreshSnapshot?> = _authRefreshSnapshot.asStateFlow()

    override suspend fun refreshSessions(storesToReset: List<AuthStore>) {
        var differentRefreshRetries = 0

        while (true) {
            when (val request = acquireRefreshRequest(storesToReset)) {
                is RefreshRequest.JoinExisting -> {
@@ -77,9 +85,7 @@ class SessionStateHolder @Inject constructor(
                }

                is RefreshRequest.WaitForDifferentRequest -> {
                    if (awaitDifferentRefreshCompletionOrRetry(request.completion)) {
                        continue
                    }
                    if (handleDifferentRequest(request, ++differentRefreshRetries)) continue
                }

                is RefreshRequest.StartNew -> {
@@ -90,6 +96,30 @@ class SessionStateHolder @Inject constructor(
        }
    }

    private suspend fun handleDifferentRequest(
        request: RefreshRequest.WaitForDifferentRequest,
        retryCount: Int
    ): Boolean {
        val outcome = awaitDifferentRefreshOutcome(request.completion)

        if (outcome is DifferentRefreshOutcome.Failed) {
            if (retryCount > MAX_DIFFERENT_REFRESH_RETRIES) {
                throw SessionRefreshContentionException(outcome.cause)
            }

            Timber.i(
                outcome.cause,
                "Different in-flight auth refresh failed (retry %d of %d)",
                retryCount,
                MAX_DIFFERENT_REFRESH_RETRIES
            )

            return true
        }

        return false
    }

    private suspend fun awaitRefreshCompletion(completion: CompletableDeferred<Unit>) {
        try {
            withTimeout(REFRESH_WAIT_TIMEOUT_MS) {
@@ -100,26 +130,18 @@ class SessionStateHolder @Inject constructor(
        }
    }

    private suspend fun awaitDifferentRefreshCompletionOrRetry(
    private suspend fun awaitDifferentRefreshOutcome(
        completion: CompletableDeferred<Unit>,
    ): Boolean {
    ): DifferentRefreshOutcome {
        return try {
            awaitRefreshCompletion(completion)
            false
            DifferentRefreshOutcome.Completed
        } catch (exception: CancellationException) {
            throw exception
        } catch (exception: IOException) {
            Timber.i(
                exception,
                "Different in-flight auth refresh finished unsuccessfully; retrying refresh acquisition",
            )
            true
            DifferentRefreshOutcome.Failed(exception)
        } catch (exception: SessionRefreshException) {
            Timber.i(
                exception,
                "Different in-flight auth refresh finished unsuccessfully; retrying refresh acquisition",
            )
            true
            DifferentRefreshOutcome.Failed(exception)
        }
    }

@@ -342,4 +364,9 @@ class SessionStateHolder @Inject constructor(
        data class JoinExisting(val completion: CompletableDeferred<Unit>) : RefreshRequest
        data class WaitForDifferentRequest(val completion: CompletableDeferred<Unit>) : RefreshRequest
    }

    private sealed interface DifferentRefreshOutcome {
        data object Completed : DifferentRefreshOutcome
        data class Failed(val cause: Throwable) : DifferentRefreshOutcome
    }
}
+1 −1
Original line number Diff line number Diff line
@@ -24,7 +24,7 @@ import foundation.e.apps.domain.model.install.Status

/*
 * Map raw application + contextual signals into a single button state.
 * Keep pure: no side effects; callers handle actions.
 * Keep pure: no side effects; callers handle acntions.
 */
fun mapAppToInstallState(input: InstallButtonStateInput): InstallButtonState {
    return when (val status = input.resolvedStatus) {