When a background SDK task fails, react in the client (#6166)

- For initialization issues or errors, we just print and report them.
- For panics (unrecoverable errors) we also crash the app.
This commit is contained in:
Jorge Martin Espinosa
2026-02-10 12:28:24 +01:00
committed by GitHub
parent ee0da6b15c
commit f6b8c189e1
4 changed files with 88 additions and 3 deletions

View File

@@ -10,11 +10,14 @@ package io.element.android.libraries.matrix.impl
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
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.mapper.toSessionData
import io.element.android.libraries.matrix.impl.paths.getSessionPaths
import io.element.android.libraries.matrix.impl.util.anonymizedTokens
import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.matrix.rustcomponents.sdk.ClientDelegate
import org.matrix.rustcomponents.sdk.ClientSessionDelegate
@@ -23,6 +26,7 @@ import timber.log.Timber
import uniffi.matrix_sdk_common.BackgroundTaskFailureReason
import java.lang.ref.WeakReference
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.time.Duration.Companion.milliseconds
private val loggerTag = LoggerTag("RustClientSessionDelegate")
@@ -36,6 +40,7 @@ private val loggerTag = LoggerTag("RustClientSessionDelegate")
class RustClientSessionDelegate(
private val sessionStore: SessionStore,
private val appCoroutineScope: CoroutineScope,
private val analyticsService: AnalyticsService,
coroutineDispatchers: CoroutineDispatchers,
) : ClientSessionDelegate, ClientDelegate {
// Used to ensure several calls to `didReceiveAuthError` don't trigger multiple logouts
@@ -122,8 +127,18 @@ class RustClientSessionDelegate(
}
override fun onBackgroundTaskErrorReport(taskName: String, error: BackgroundTaskFailureReason) {
// TODO actually implement the missing logic to report to sentry and crash the app
Timber.tag(loggerTag.value).e("onBackgroundTaskErrorReport(taskName=$taskName, error=$error)")
val backgroundTaskError = SdkBackgroundTaskError(taskName, error)
Timber.e(backgroundTaskError, "SDK background task failed")
analyticsService.trackError(backgroundTaskError)
if (error is BackgroundTaskFailureReason.Panic) {
appCoroutineScope.launch {
// The SDK failed in an unrecoverable way, so it will have indeterminate behaviour now.
// Crash the app instead after a small delay to send the error.
delay(500.milliseconds)
throw backgroundTaskError
}
}
}
override fun retrieveSessionFromKeychain(userId: String): Session {

View File

@@ -66,7 +66,12 @@ class RustMatrixClientFactory(
private val sqliteStoreBuilderProvider: SqliteStoreBuilderProvider,
private val workManagerScheduler: WorkManagerScheduler,
) {
private val sessionDelegate = RustClientSessionDelegate(sessionStore, appCoroutineScope, coroutineDispatchers)
private val sessionDelegate = RustClientSessionDelegate(
sessionStore = sessionStore,
appCoroutineScope = appCoroutineScope,
analyticsService = analyticsService,
coroutineDispatchers = coroutineDispatchers
)
suspend fun create(sessionData: SessionData): RustMatrixClient = withContext(coroutineDispatchers.io) {
val client = getBaseClientBuilder(

View File

@@ -0,0 +1,36 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.core
import uniffi.matrix_sdk_common.BackgroundTaskFailureReason
/**
* Error thrown when a background SDK task panics and can't recover.
* @param task The name of the task that failed.
* @param reason The cause of this error.
*/
class SdkBackgroundTaskError(
task: String,
reason: BackgroundTaskFailureReason,
) : Error() {
override val message: String = run {
val message = when (reason) {
is BackgroundTaskFailureReason.EarlyTermination -> "Early termination"
is BackgroundTaskFailureReason.Error -> "Error: ${reason.error}"
is BackgroundTaskFailureReason.Panic -> buildString {
append("Panic (unrecoverable): ")
reason.message?.let { append(it) }
reason.panicBacktrace?.let {
append("\n")
append(it)
}
}
}
"SDK background task '$task' failure: \n$message"
}
}

View File

@@ -9,16 +9,20 @@
package io.element.android.libraries.matrix.impl
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.impl.core.SdkBackgroundTaskError
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustSession
import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
import io.element.android.libraries.sessionstorage.test.aSessionData
import io.element.android.services.analytics.api.AnalyticsService
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.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Test
import uniffi.matrix_sdk_common.BackgroundTaskFailureReason
@OptIn(ExperimentalCoroutinesApi::class)
class RustClientSessionDelegateTest {
@@ -44,12 +48,37 @@ class RustClientSessionDelegateTest {
assertThat(result!!.accessToken).isEqualTo("at")
assertThat(result.refreshToken).isEqualTo("rt")
}
@Test
fun `onBackgroundTaskErrorReport reports the error to analytics if recoverable`() = runTest {
val analyticsService = FakeAnalyticsService(isEnabled = true)
val sut = aRustClientSessionDelegate(analyticsService = analyticsService)
sut.onBackgroundTaskErrorReport("Crasher", BackgroundTaskFailureReason.EarlyTermination)
sut.onBackgroundTaskErrorReport("Crasher", BackgroundTaskFailureReason.Error("BOOM"))
assertThat(analyticsService.trackedErrors).hasSize(2)
assertThat(analyticsService.trackedErrors[0].message).isEqualTo("SDK background task 'Crasher' failure: \nEarly termination")
assertThat(analyticsService.trackedErrors[1].message).isEqualTo("SDK background task 'Crasher' failure: \nError: BOOM")
}
@Test(expected = SdkBackgroundTaskError::class)
fun `onBackgroundTaskErrorReport reports the error to analytics and throws it if it's a panic`() = runTest {
val analyticsService = FakeAnalyticsService(isEnabled = true)
val sut = aRustClientSessionDelegate(analyticsService = analyticsService)
sut.onBackgroundTaskErrorReport("Crasher", BackgroundTaskFailureReason.Panic("BOOM", "Stacktrace"))
assertThat(analyticsService.trackedErrors).hasSize(1)
assertThat(analyticsService.trackedErrors[0].message)
.isEqualTo("SDK background task 'Crasher' failure: \nPanic (unrecoverable): BOOM\nStacktrace")
}
}
fun TestScope.aRustClientSessionDelegate(
sessionStore: SessionStore = InMemorySessionStore(),
analyticsService: AnalyticsService = FakeAnalyticsService(),
) = RustClientSessionDelegate(
sessionStore = sessionStore,
appCoroutineScope = this,
analyticsService = analyticsService,
coroutineDispatchers = testCoroutineDispatchers(),
)