Skip to content

Commit b3f027c

Browse files
Add exception handler
1 parent aa8d7a3 commit b3f027c

File tree

3 files changed

+120
-3
lines changed

3 files changed

+120
-3
lines changed

src/main/kotlin/com/statsig/sdk/StatsigServer.kt

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ private class StatsigServerImpl() :
195195
override var initialized = false
196196

197197
override fun setup(serverSecret: String, options: StatsigOptions) {
198+
Thread.setDefaultUncaughtExceptionHandler(MainThreadExceptionHandler(this, Thread.currentThread()))
198199
errorBoundary = ErrorBoundary(serverSecret, options, statsigMetadata)
199200
coroutineExceptionHandler = CoroutineExceptionHandler { _, ex ->
200201
// no-op - supervisor job should not throw when a child fails
@@ -209,6 +210,10 @@ private class StatsigServerImpl() :
209210

210211
override suspend fun initialize(serverSecret: String, options: StatsigOptions) {
211212
setup(serverSecret, options)
213+
initializeImpl(serverSecret, options)
214+
}
215+
216+
private suspend fun initializeImpl(serverSecret: String, options: StatsigOptions) {
212217
errorBoundary.capture(
213218
"initialize",
214219
{
@@ -512,7 +517,7 @@ private class StatsigServerImpl() :
512517
override fun initializeAsync(serverSecret: String, options: StatsigOptions): CompletableFuture<Void?> {
513518
setup(serverSecret, options)
514519
return statsigScope.future {
515-
initialize(serverSecret, options)
520+
initializeImpl(serverSecret, options)
516521
null
517522
}
518523
}
@@ -787,4 +792,15 @@ private class StatsigServerImpl() :
787792
diagnostics?.logDiagnostics(ContextType.INITIALIZE)
788793
diagnostics.diagnosticsContext = ContextType.CONFIG_SYNC
789794
}
795+
796+
class MainThreadExceptionHandler(val server: StatsigServer, val currentThread: Thread) : Thread.UncaughtExceptionHandler {
797+
override fun uncaughtException(t: Thread, e: Throwable) {
798+
if (!t.name.equals(currentThread.name)) {
799+
throw e
800+
}
801+
println("[Statsig]: Shutting down Statsig because of unhandled exception from your server")
802+
server.shutdown()
803+
throw e
804+
}
805+
}
790806
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package com.statsig.sdk
2+
3+
import com.google.gson.Gson
4+
import com.google.gson.GsonBuilder
5+
import com.google.gson.ToNumberPolicy
6+
import kotlinx.coroutines.CompletableDeferred
7+
import kotlinx.coroutines.runBlocking
8+
import okhttp3.mockwebserver.Dispatcher
9+
import okhttp3.mockwebserver.MockResponse
10+
import okhttp3.mockwebserver.MockWebServer
11+
import okhttp3.mockwebserver.RecordedRequest
12+
import org.junit.Before
13+
import org.junit.Test
14+
15+
class ExceptionHandlerTest {
16+
private lateinit var gson: Gson
17+
private lateinit var eventLogInputCompletable: CompletableDeferred<LogEventInput>
18+
private lateinit var evaluator: Evaluator
19+
private lateinit var driver: StatsigServer
20+
private lateinit var user: StatsigUser
21+
private lateinit var options: StatsigOptions
22+
23+
@Before
24+
fun setUp() {
25+
gson = GsonBuilder().setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE).create()
26+
user = StatsigUser("abc")
27+
eventLogInputCompletable = CompletableDeferred()
28+
29+
val mockGateResponse = APIFeatureGate("a_gate", true, "ruleID")
30+
val mockResponseBody = gson.toJson(mockGateResponse)
31+
32+
val downloadConfigSpecsResponse =
33+
StatsigE2ETest::class.java.getResource("/layer_exposure_download_config_specs.json")?.readText() ?: ""
34+
35+
val server = MockWebServer()
36+
server.apply {
37+
dispatcher = object : Dispatcher() {
38+
@Throws(InterruptedException::class)
39+
override fun dispatch(request: RecordedRequest): MockResponse {
40+
when (request.path) {
41+
"/v1/download_config_specs" -> {
42+
return MockResponse().setResponseCode(200).setBody(downloadConfigSpecsResponse)
43+
}
44+
"/v1/check_gate" -> {
45+
return MockResponse().setResponseCode(200).setBody(mockResponseBody)
46+
}
47+
"/v1/log_event" -> {
48+
val logBody = request.body.readUtf8()
49+
eventLogInputCompletable.complete(gson.fromJson(logBody, LogEventInput::class.java))
50+
return MockResponse().setResponseCode(200)
51+
}
52+
}
53+
return MockResponse().setResponseCode(404)
54+
}
55+
}
56+
}
57+
58+
options = StatsigOptions().apply {
59+
api = server.url("/v1").toString()
60+
disableDiagnostics = true
61+
}
62+
63+
driver = StatsigServer.create()
64+
}
65+
66+
@Test
67+
fun testDaemonThreadException() = runBlocking {
68+
driver.initialize("server-key", options)
69+
val daemon = Thread({
70+
throw Exception("Throwing from daemon thread")
71+
})
72+
daemon.isDaemon = true
73+
daemon.start()
74+
assert(driver.initialized)
75+
}
76+
77+
@Test
78+
fun testThrowingNonDaemon() = runBlocking {
79+
driver.initialize("server-key", options)
80+
val nonDaemon = Thread({
81+
throw Exception("Throwing from non-daemon thread")
82+
})
83+
nonDaemon.start()
84+
// For exception throw in other thread, we don't handle
85+
assert(driver.initialized)
86+
driver.shutdown()
87+
assert(!driver.initialized)
88+
}
89+
90+
@Test
91+
fun testThrowingOnSameThread() = runBlocking {
92+
val t = Thread {
93+
runBlocking {
94+
driver.initialize("server-key", options)
95+
}
96+
throw java.lang.Exception("throw exception")
97+
}
98+
t.start()
99+
assert(!driver.initialized)
100+
}
101+
}

src/test/java/com/statsig/sdk/StatsigOptionsTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class StatsigOptionsTest {
1919
"PRODUCTION" to "production",
2020
"STAGING" to "staging",
2121
"Development" to "development",
22-
"Custom Tier" to "custom tier"
22+
"Custom Tier" to "custom tier",
2323
)
2424

2525
for ((inputTier, expectedTier) in tierTestCases) {
@@ -33,7 +33,7 @@ class StatsigOptionsTest {
3333
val parameterTestCases = mapOf(
3434
"key1" to "value1",
3535
"key2" to "value2",
36-
"key1" to "updatedValue1"
36+
"key1" to "updatedValue1",
3737
)
3838

3939
for ((key, value) in parameterTestCases) {

0 commit comments

Comments
 (0)