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

Commit 077be645 authored by Abhishek Aggarwal's avatar Abhishek Aggarwal
Browse files

fix: retry deferred session refresh requests after in-flight failure

parent 07198260
Loading
Loading
Loading
Loading
+26 −1
Original line number Diff line number Diff line
@@ -77,7 +77,9 @@ class SessionStateHolder @Inject constructor(
                }

                is RefreshRequest.WaitForDifferentRequest -> {
                    awaitRefreshCompletion(request.completion)
                    if (awaitDifferentRefreshCompletionOrRetry(request.completion)) {
                        continue
                    }
                }

                is RefreshRequest.StartNew -> {
@@ -98,6 +100,29 @@ class SessionStateHolder @Inject constructor(
        }
    }

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

    private suspend fun acquireRefreshRequest(storesToReset: List<AuthStore>): RefreshRequest {
        return refreshMutex.withLock {
            val normalizedStoresToReset = storesToReset.toList()
+83 −6
Original line number Diff line number Diff line
@@ -400,7 +400,7 @@ class SessionStateHolderTest {
    }

    @Test
    fun `refreshSessions times out while waiting for a different in-flight refresh`() = runTest {
    fun `refreshSessions retries a different in-flight refresh after waiter timeout`() = runTest {
        val authRefreshRepository = BlockingAuthRefreshRepository(
            AuthRefreshOutcome(snapshot = AuthRefreshSnapshot(emptyList())),
        )
@@ -424,14 +424,70 @@ class SessionStateHolderTest {
        advanceTimeBy(SessionStateHolder.REFRESH_WAIT_TIMEOUT_MS + 1)
        runCurrent()

        val failure = secondRefresh.await().exceptionOrNull()
        assertThat(failure).isInstanceOf(SessionRefreshTimeoutException::class.java)
        val timeoutFailure = failure as SessionRefreshTimeoutException
        assertThat(timeoutFailure).hasMessageThat()
            .contains("Timed out waiting for in-flight auth refresh to complete")
        assertThat(secondRefresh.isCompleted).isFalse()

        authRefreshRepository.release.complete(Unit)
        firstRefresh.await()
        val secondResult = secondRefresh.await()

        assertThat(secondResult.exceptionOrNull()).isNull()
        assertThat(authRefreshRepository.fetchCount).isEqualTo(2)
    }

    @Test
    fun `refreshSessions retries a different in-flight refresh after first refresh fails`() = runTest {
        val successfulOutcome = AuthRefreshOutcome(
            snapshot = AuthRefreshSnapshot(
                entries = listOf(
                    AuthRefreshEntry(
                        store = AuthStore.OPEN_SOURCE,
                        result = AuthResult.Success(AuthSession.OpenSourceSession),
                    ),
                ),
            ),
        )
        val authRefreshRepository = BlockingSequentialAuthRefreshRepository(
            responses = ArrayDeque(
                listOf(
                    RefreshResponse.Failure(IOException("network down")),
                    RefreshResponse.Success(successfulOutcome),
                ),
            ),
        )
        val sessionStateHolder = SessionStateHolder(
            authRefreshRepository = authRefreshRepository,
            faultyTokenReporter = FaultyTokenReporter { },
        )

        val firstRefresh = async {
            runCatching {
                sessionStateHolder.refreshSessions(listOf(AuthStore.PLAY_STORE))
            }.exceptionOrNull()
        }
        authRefreshRepository.started.await()

        val secondRefresh = async {
            sessionStateHolder.refreshSessions(listOf(AuthStore.OPEN_SOURCE))
        }
        runCurrent()

        authRefreshRepository.release.complete(Unit)
        runCurrent()

        val firstFailure = firstRefresh.await()
        secondRefresh.await()

        assertThat(firstFailure).isInstanceOf(IOException::class.java)
        assertThat(authRefreshRepository.receivedStoresToResetHistory)
            .containsExactly(
                listOf(AuthStore.PLAY_STORE),
                listOf(AuthStore.OPEN_SOURCE),
            )
            .inOrder()
        assertThat(sessionStateHolder.authRefreshState.value)
            .isEqualTo(AuthRefreshState.Completed(successfulOutcome.snapshot))
        assertThat(sessionStateHolder.authRefreshSnapshot.value)
            .isEqualTo(successfulOutcome.snapshot)
    }

    private class FakeAuthRefreshRepository(
@@ -461,6 +517,27 @@ class SessionStateHolderTest {
        }
    }

    private class BlockingSequentialAuthRefreshRepository(
        private val responses: ArrayDeque<RefreshResponse>,
    ) : AuthRefreshRepository {
        val started = CompletableDeferred<Unit>()
        val release = CompletableDeferred<Unit>()
        val receivedStoresToResetHistory = mutableListOf<List<AuthStore>>()

        override suspend fun refreshSessions(storesToReset: List<AuthStore>): AuthRefreshOutcome {
            receivedStoresToResetHistory += storesToReset
            if (receivedStoresToResetHistory.size == 1) {
                started.complete(Unit)
                release.await()
            }

            return when (val response = responses.removeFirst()) {
                is RefreshResponse.Success -> response.authRefreshOutcome
                is RefreshResponse.Failure -> throw response.exception
            }
        }
    }

    private class ThrowingAuthRefreshRepository(
        private val exception: IOException,
    ) : AuthRefreshRepository {