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:
committed by
GitHub
parent
ee0da6b15c
commit
f6b8c189e1
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user