Skip to content

Commit 12be9ab

Browse files
committed
fix: add proper exception handling for background event source
1 parent 6bf69a3 commit 12be9ab

File tree

2 files changed

+59
-2
lines changed

2 files changed

+59
-2
lines changed

android-client-sdk/src/main/java/com/devcycle/sdk/android/api/DevCycleClient.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ class DevCycleClient private constructor(
4848
private val coroutineContext: CoroutineContext = Dispatchers.Default
4949
) {
5050
private var config: BucketedUserConfig? = null
51-
private var backgroundEventSource: BackgroundEventSource? = null
51+
private var backgroundEventSource: SafeBackgroundEventSource? = null
5252
private val defaultIntervalInMs: Long = 10000
5353
private val flushInMs: Long = options?.flushEventsIntervalMs ?: defaultIntervalInMs
5454

@@ -172,7 +172,8 @@ class DevCycleClient private constructor(
172172
.readTimeout(EVENT_SOURCE_RETRY_DELAY_MIN, TimeUnit.MINUTES)
173173
)
174174

175-
backgroundEventSource = BackgroundEventSource.Builder(handler, builder).build()
175+
val delegate = BackgroundEventSource.Builder(handler, builder).build()
176+
backgroundEventSource = SafeBackgroundEventSource(delegate)
176177
backgroundEventSource?.start()
177178
}
178179

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package com.devcycle.sdk.android.eventsource
2+
3+
import com.devcycle.sdk.android.util.DevCycleLogger
4+
import com.launchdarkly.eventsource.EventSource
5+
import com.launchdarkly.eventsource.ReadyState
6+
import com.launchdarkly.eventsource.background.BackgroundEventSource
7+
import java.util.concurrent.RejectedExecutionException
8+
9+
/**
10+
* A safe wrapper around BackgroundEventSource that handles RejectedExecutionException
11+
* during shutdown operations gracefully to prevent app crashes.
12+
*
13+
* This wrapper is necessary because when EventSource is closed, its internal executor
14+
* is shut down. If there are any pending tasks or race conditions during shutdown,
15+
* they would throw RejectedExecutionException which could crash the application.
16+
*/
17+
class SafeBackgroundEventSource(
18+
private val delegate: BackgroundEventSource
19+
) {
20+
/**
21+
* Access the underlying EventSource for state checks
22+
*/
23+
val eventSource: EventSource
24+
get() = delegate.eventSource
25+
26+
/**
27+
* Start the background event source
28+
*/
29+
fun start() {
30+
try {
31+
delegate.start()
32+
} catch (e: RejectedExecutionException) {
33+
DevCycleLogger.d("EventSource start rejected (executor may be shut down): ${e.message}")
34+
} catch (e: Exception) {
35+
DevCycleLogger.w("Unexpected error starting EventSource: ${e.message}")
36+
throw e
37+
}
38+
}
39+
40+
/**
41+
* Close the background event source safely, catching any RejectedExecutionException
42+
* that may occur during shutdown due to race conditions with internal executor threads.
43+
*/
44+
fun close() {
45+
try {
46+
delegate.close()
47+
} catch (e: RejectedExecutionException) {
48+
// This is expected during shutdown when tasks are submitted to an already-terminated executor
49+
DevCycleLogger.d("EventSource close rejected during shutdown (expected): ${e.message}")
50+
} catch (e: Exception) {
51+
// Log unexpected exceptions but don't propagate them to prevent crashes
52+
DevCycleLogger.w("Unexpected error closing EventSource: ${e.message}")
53+
}
54+
}
55+
}
56+

0 commit comments

Comments
 (0)