Use runBlocking for the token refresh logic (#6863)

* Use `runBlocking` for the token refresh logic

The `RustClientSessionDelegate` callbacks always run in a separate thread, so they don't block the main thread.

This ensures the token refresh is fully done (data saved/failed to) before the SDK continues sending the pending previously failed requests
This commit is contained in:
Jorge Martin Espinosa 2026-05-26 09:40:07 +02:00 committed by GitHub
parent 089bf0c690
commit d5fa633bba
3 changed files with 22 additions and 34 deletions

View file

@ -8,7 +8,7 @@
package io.element.android.libraries.matrix.impl package io.element.android.libraries.matrix.impl
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.matrix.impl.core.SdkBackgroundTaskError import io.element.android.libraries.matrix.impl.core.SdkBackgroundTaskError
import io.element.android.libraries.matrix.impl.mapper.toSessionData import io.element.android.libraries.matrix.impl.mapper.toSessionData
@ -19,6 +19,7 @@ import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.matrix.rustcomponents.sdk.ClientDelegate import org.matrix.rustcomponents.sdk.ClientDelegate
import org.matrix.rustcomponents.sdk.ClientSessionDelegate import org.matrix.rustcomponents.sdk.ClientSessionDelegate
import org.matrix.rustcomponents.sdk.Session import org.matrix.rustcomponents.sdk.Session
@ -41,14 +42,10 @@ class RustClientSessionDelegate(
private val sessionStore: SessionStore, private val sessionStore: SessionStore,
private val appCoroutineScope: CoroutineScope, private val appCoroutineScope: CoroutineScope,
private val analyticsService: AnalyticsService, private val analyticsService: AnalyticsService,
coroutineDispatchers: CoroutineDispatchers,
) : ClientSessionDelegate, ClientDelegate { ) : ClientSessionDelegate, ClientDelegate {
// Used to ensure several calls to `didReceiveAuthError` don't trigger multiple logouts // Used to ensure several calls to `didReceiveAuthError` don't trigger multiple logouts
private val isLoggingOut = AtomicBoolean(false) private val isLoggingOut = AtomicBoolean(false)
// To make sure only one coroutine affecting the token persistence can run at a time
private val updateTokensDispatcher = coroutineDispatchers.io.limitedParallelism(1)
// This Client needs to be set up as soon as possible so `didReceiveAuthError` can work properly. // This Client needs to be set up as soon as possible so `didReceiveAuthError` can work properly.
private var client: WeakReference<RustMatrixClient> = WeakReference(null) private var client: WeakReference<RustMatrixClient> = WeakReference(null)
@ -66,9 +63,10 @@ class RustClientSessionDelegate(
this.client.clear() this.client.clear()
} }
// This always runs on a background thread, so we *can* do blocking calls here, although we should avoid doing heavy work
override fun saveSessionInKeychain(session: Session) { override fun saveSessionInKeychain(session: Session) {
appCoroutineScope.launch(updateTokensDispatcher) { runCatchingExceptions {
val existingData = sessionStore.getSession(session.userId) ?: return@launch val existingData = runBlocking { sessionStore.getSession(session.userId) } ?: return
val (anonymizedAccessToken, anonymizedRefreshToken) = session.anonymizedTokens() val (anonymizedAccessToken, anonymizedRefreshToken) = session.anonymizedTokens()
Timber.tag(loggerTag.value).d( Timber.tag(loggerTag.value).d(
"Saving new session data with token: access token '$anonymizedAccessToken' and refresh token '$anonymizedRefreshToken'. " + "Saving new session data with token: access token '$anonymizedAccessToken' and refresh token '$anonymizedRefreshToken'. " +
@ -80,28 +78,27 @@ class RustClientSessionDelegate(
passphrase = existingData.passphrase, passphrase = existingData.passphrase,
sessionPaths = existingData.getSessionPaths(), sessionPaths = existingData.getSessionPaths(),
) )
sessionStore.updateData(newData) runBlocking { sessionStore.updateData(newData) }
Timber.tag(loggerTag.value).d("Saved new session data with access token: '$anonymizedAccessToken'.") Timber.tag(loggerTag.value).d("Saved new session data with access token: '$anonymizedAccessToken'.")
}.invokeOnCompletion { }.onFailure {
if (it != null) { Timber.tag(loggerTag.value).e(it, "Failed to save new session data.")
Timber.tag(loggerTag.value).e(it, "Failed to save new session data.")
}
} }
} }
// This always runs on a background thread, so we *can* do blocking calls here, although we should avoid doing heavy work
override fun didReceiveAuthError(isSoftLogout: Boolean) { override fun didReceiveAuthError(isSoftLogout: Boolean) {
Timber.tag(loggerTag.value).w("didReceiveAuthError(isSoftLogout=$isSoftLogout)") runCatchingExceptions {
if (isLoggingOut.getAndSet(true).not()) { Timber.tag(loggerTag.value).w("didReceiveAuthError(isSoftLogout=$isSoftLogout)")
Timber.tag(loggerTag.value).v("didReceiveAuthError -> do the cleanup") if (isLoggingOut.getAndSet(true).not()) {
// TODO handle isSoftLogout parameter. Timber.tag(loggerTag.value).v("didReceiveAuthError -> do the cleanup")
appCoroutineScope.launch(updateTokensDispatcher) { // TODO handle isSoftLogout parameter.
val currentClient = client.get() val currentClient = client.get()
if (currentClient == null) { if (currentClient == null) {
Timber.tag(loggerTag.value).w("didReceiveAuthError -> no client, exiting") Timber.tag(loggerTag.value).w("didReceiveAuthError -> no client, exiting")
isLoggingOut.set(false) isLoggingOut.set(false)
return@launch return
} }
val existingData = sessionStore.getSession(currentClient.sessionId.value) val existingData = runBlocking { sessionStore.getSession(currentClient.sessionId.value) }
val (anonymizedAccessToken, anonymizedRefreshToken) = existingData.anonymizedTokens() val (anonymizedAccessToken, anonymizedRefreshToken) = existingData.anonymizedTokens()
Timber.tag(loggerTag.value).d( Timber.tag(loggerTag.value).d(
"Removing session data with access token '$anonymizedAccessToken' " + "Removing session data with access token '$anonymizedAccessToken' " +
@ -110,19 +107,17 @@ class RustClientSessionDelegate(
if (existingData != null) { if (existingData != null) {
// Set isTokenValid to false // Set isTokenValid to false
val newData = existingData.copy(isTokenValid = false) val newData = existingData.copy(isTokenValid = false)
sessionStore.updateData(newData) runBlocking { sessionStore.updateData(newData) }
Timber.tag(loggerTag.value).d("Invalidated session data with access token: '$anonymizedAccessToken'.") Timber.tag(loggerTag.value).d("Invalidated session data with access token: '$anonymizedAccessToken'.")
} else { } else {
Timber.tag(loggerTag.value).d("No session data found.") Timber.tag(loggerTag.value).d("No session data found.")
} }
currentClient.logout(userInitiated = false, ignoreSdkError = true) appCoroutineScope.launch { currentClient.logout(userInitiated = false, ignoreSdkError = true) }
}.invokeOnCompletion { } else {
if (it != null) { Timber.tag(loggerTag.value).v("didReceiveAuthError -> already cleaning up")
Timber.tag(loggerTag.value).e(it, "Failed to remove session data.")
}
} }
} else { }.onFailure {
Timber.tag(loggerTag.value).v("didReceiveAuthError -> already cleaning up") Timber.tag(loggerTag.value).e(it, "Failed to remove session data.")
} }
} }

View file

@ -72,7 +72,6 @@ class RustMatrixClientFactory(
sessionStore = sessionStore, sessionStore = sessionStore,
appCoroutineScope = appCoroutineScope, appCoroutineScope = appCoroutineScope,
analyticsService = analyticsService, analyticsService = analyticsService,
coroutineDispatchers = coroutineDispatchers
) )
suspend fun create(sessionData: SessionData): RustMatrixClient = withContext(coroutineDispatchers.io) { suspend fun create(sessionData: SessionData): RustMatrixClient = withContext(coroutineDispatchers.io) {

View file

@ -16,15 +16,11 @@ import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
import io.element.android.libraries.sessionstorage.test.aSessionData import io.element.android.libraries.sessionstorage.test.aSessionData
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
import uniffi.matrix_sdk_common.BackgroundTaskFailureReason import uniffi.matrix_sdk_common.BackgroundTaskFailureReason
@OptIn(ExperimentalCoroutinesApi::class)
class RustClientSessionDelegateTest { class RustClientSessionDelegateTest {
@Test @Test
fun `saveSessionInKeychain should update the store`() = runTest { fun `saveSessionInKeychain should update the store`() = runTest {
@ -43,7 +39,6 @@ class RustClientSessionDelegateTest {
refreshToken = "rt", refreshToken = "rt",
) )
) )
runCurrent()
val result = sessionStore.getLatestSession() val result = sessionStore.getLatestSession()
assertThat(result!!.accessToken).isEqualTo("at") assertThat(result!!.accessToken).isEqualTo("at")
assertThat(result.refreshToken).isEqualTo("rt") assertThat(result.refreshToken).isEqualTo("rt")
@ -80,5 +75,4 @@ fun TestScope.aRustClientSessionDelegate(
sessionStore = sessionStore, sessionStore = sessionStore,
appCoroutineScope = this, appCoroutineScope = this,
analyticsService = analyticsService, analyticsService = analyticsService,
coroutineDispatchers = testCoroutineDispatchers(),
) )