Skip to content

Commit 5b5224e

Browse files
authored
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.
1 parent b271e06 commit 5b5224e

File tree

4 files changed

+88
-3
lines changed

4 files changed

+88
-3
lines changed

libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustClientSessionDelegate.kt

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,14 @@ package io.element.android.libraries.matrix.impl
1010

1111
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
1212
import io.element.android.libraries.core.log.logger.LoggerTag
13+
import io.element.android.libraries.matrix.impl.core.SdkBackgroundTaskError
1314
import io.element.android.libraries.matrix.impl.mapper.toSessionData
1415
import io.element.android.libraries.matrix.impl.paths.getSessionPaths
1516
import io.element.android.libraries.matrix.impl.util.anonymizedTokens
1617
import io.element.android.libraries.sessionstorage.api.SessionStore
18+
import io.element.android.services.analytics.api.AnalyticsService
1719
import kotlinx.coroutines.CoroutineScope
20+
import kotlinx.coroutines.delay
1821
import kotlinx.coroutines.launch
1922
import org.matrix.rustcomponents.sdk.ClientDelegate
2023
import org.matrix.rustcomponents.sdk.ClientSessionDelegate
@@ -23,6 +26,7 @@ import timber.log.Timber
2326
import uniffi.matrix_sdk_common.BackgroundTaskFailureReason
2427
import java.lang.ref.WeakReference
2528
import java.util.concurrent.atomic.AtomicBoolean
29+
import kotlin.time.Duration.Companion.milliseconds
2630

2731
private val loggerTag = LoggerTag("RustClientSessionDelegate")
2832

@@ -36,6 +40,7 @@ private val loggerTag = LoggerTag("RustClientSessionDelegate")
3640
class RustClientSessionDelegate(
3741
private val sessionStore: SessionStore,
3842
private val appCoroutineScope: CoroutineScope,
43+
private val analyticsService: AnalyticsService,
3944
coroutineDispatchers: CoroutineDispatchers,
4045
) : ClientSessionDelegate, ClientDelegate {
4146
// Used to ensure several calls to `didReceiveAuthError` don't trigger multiple logouts
@@ -122,8 +127,18 @@ class RustClientSessionDelegate(
122127
}
123128

124129
override fun onBackgroundTaskErrorReport(taskName: String, error: BackgroundTaskFailureReason) {
125-
// TODO actually implement the missing logic to report to sentry and crash the app
126-
Timber.tag(loggerTag.value).e("onBackgroundTaskErrorReport(taskName=$taskName, error=$error)")
130+
val backgroundTaskError = SdkBackgroundTaskError(taskName, error)
131+
Timber.e(backgroundTaskError, "SDK background task failed")
132+
analyticsService.trackError(backgroundTaskError)
133+
134+
if (error is BackgroundTaskFailureReason.Panic) {
135+
appCoroutineScope.launch {
136+
// The SDK failed in an unrecoverable way, so it will have indeterminate behaviour now.
137+
// Crash the app instead after a small delay to send the error.
138+
delay(500.milliseconds)
139+
throw backgroundTaskError
140+
}
141+
}
127142
}
128143

129144
override fun retrieveSessionFromKeychain(userId: String): Session {

libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,12 @@ class RustMatrixClientFactory(
6666
private val sqliteStoreBuilderProvider: SqliteStoreBuilderProvider,
6767
private val workManagerScheduler: WorkManagerScheduler,
6868
) {
69-
private val sessionDelegate = RustClientSessionDelegate(sessionStore, appCoroutineScope, coroutineDispatchers)
69+
private val sessionDelegate = RustClientSessionDelegate(
70+
sessionStore = sessionStore,
71+
appCoroutineScope = appCoroutineScope,
72+
analyticsService = analyticsService,
73+
coroutineDispatchers = coroutineDispatchers
74+
)
7075

7176
suspend fun create(sessionData: SessionData): RustMatrixClient = withContext(coroutineDispatchers.io) {
7277
val client = getBaseClientBuilder(
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright (c) 2026 Element Creations Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
package io.element.android.libraries.matrix.impl.core
9+
10+
import uniffi.matrix_sdk_common.BackgroundTaskFailureReason
11+
12+
/**
13+
* Error thrown when a background SDK task panics and can't recover.
14+
* @param task The name of the task that failed.
15+
* @param reason The cause of this error.
16+
*/
17+
class SdkBackgroundTaskError(
18+
task: String,
19+
reason: BackgroundTaskFailureReason,
20+
) : Error() {
21+
override val message: String = run {
22+
val message = when (reason) {
23+
is BackgroundTaskFailureReason.EarlyTermination -> "Early termination"
24+
is BackgroundTaskFailureReason.Error -> "Error: ${reason.error}"
25+
is BackgroundTaskFailureReason.Panic -> buildString {
26+
append("Panic (unrecoverable): ")
27+
reason.message?.let { append(it) }
28+
reason.panicBacktrace?.let {
29+
append("\n")
30+
append(it)
31+
}
32+
}
33+
}
34+
"SDK background task '$task' failure: \n$message"
35+
}
36+
}

libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustClientSessionDelegateTest.kt

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,20 @@
99
package io.element.android.libraries.matrix.impl
1010

1111
import com.google.common.truth.Truth.assertThat
12+
import io.element.android.libraries.matrix.impl.core.SdkBackgroundTaskError
1213
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustSession
1314
import io.element.android.libraries.sessionstorage.api.SessionStore
1415
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
1516
import io.element.android.libraries.sessionstorage.test.aSessionData
17+
import io.element.android.services.analytics.api.AnalyticsService
18+
import io.element.android.services.analytics.test.FakeAnalyticsService
1619
import io.element.android.tests.testutils.testCoroutineDispatchers
1720
import kotlinx.coroutines.ExperimentalCoroutinesApi
1821
import kotlinx.coroutines.test.TestScope
1922
import kotlinx.coroutines.test.runCurrent
2023
import kotlinx.coroutines.test.runTest
2124
import org.junit.Test
25+
import uniffi.matrix_sdk_common.BackgroundTaskFailureReason
2226

2327
@OptIn(ExperimentalCoroutinesApi::class)
2428
class RustClientSessionDelegateTest {
@@ -44,12 +48,37 @@ class RustClientSessionDelegateTest {
4448
assertThat(result!!.accessToken).isEqualTo("at")
4549
assertThat(result.refreshToken).isEqualTo("rt")
4650
}
51+
52+
@Test
53+
fun `onBackgroundTaskErrorReport reports the error to analytics if recoverable`() = runTest {
54+
val analyticsService = FakeAnalyticsService(isEnabled = true)
55+
val sut = aRustClientSessionDelegate(analyticsService = analyticsService)
56+
sut.onBackgroundTaskErrorReport("Crasher", BackgroundTaskFailureReason.EarlyTermination)
57+
sut.onBackgroundTaskErrorReport("Crasher", BackgroundTaskFailureReason.Error("BOOM"))
58+
59+
assertThat(analyticsService.trackedErrors).hasSize(2)
60+
assertThat(analyticsService.trackedErrors[0].message).isEqualTo("SDK background task 'Crasher' failure: \nEarly termination")
61+
assertThat(analyticsService.trackedErrors[1].message).isEqualTo("SDK background task 'Crasher' failure: \nError: BOOM")
62+
}
63+
64+
@Test(expected = SdkBackgroundTaskError::class)
65+
fun `onBackgroundTaskErrorReport reports the error to analytics and throws it if it's a panic`() = runTest {
66+
val analyticsService = FakeAnalyticsService(isEnabled = true)
67+
val sut = aRustClientSessionDelegate(analyticsService = analyticsService)
68+
sut.onBackgroundTaskErrorReport("Crasher", BackgroundTaskFailureReason.Panic("BOOM", "Stacktrace"))
69+
70+
assertThat(analyticsService.trackedErrors).hasSize(1)
71+
assertThat(analyticsService.trackedErrors[0].message)
72+
.isEqualTo("SDK background task 'Crasher' failure: \nPanic (unrecoverable): BOOM\nStacktrace")
73+
}
4774
}
4875

4976
fun TestScope.aRustClientSessionDelegate(
5077
sessionStore: SessionStore = InMemorySessionStore(),
78+
analyticsService: AnalyticsService = FakeAnalyticsService(),
5179
) = RustClientSessionDelegate(
5280
sessionStore = sessionStore,
5381
appCoroutineScope = this,
82+
analyticsService = analyticsService,
5483
coroutineDispatchers = testCoroutineDispatchers(),
5584
)

0 commit comments

Comments
 (0)