Skip to content

Commit d5a5f17

Browse files
Merge pull request #176 from statsig-io/fix-reinitialize
fix reinitializing statsig server
2 parents 811acaf + 327727a commit d5a5f17

19 files changed

+208
-131
lines changed

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ repositories {
2121

2222
configure<org.jlleitschuh.gradle.ktlint.KtlintExtension> {
2323
verbose.set(true)
24+
disabledRules.set(setOf("no-wildcard-imports"))
2425
}
2526

2627
dependencies {

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

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,14 @@ class Statsig {
1818
serverSecret: String,
1919
options: StatsigOptions,
2020
) {
21-
if (!::statsigServer.isInitialized) { // Quick check without synchronization
21+
if (!isInitialized()) { // Quick check without synchronization
2222
synchronized(this) {
23-
if (!::statsigServer.isInitialized
23+
if (!isInitialized()
2424
) { // Secondary check in case another thread already created the default server
25-
statsigServer = StatsigServer.create(serverSecret, options)
25+
statsigServer = StatsigServer.create()
2626
}
2727
}
28-
statsigServer.initialize()
28+
statsigServer.initialize(serverSecret, options)
2929
}
3030
}
3131

@@ -241,7 +241,7 @@ class Statsig {
241241
/**
242242
* Sets a value to be returned for the given dynamic config/experiment instead of the actual evaluated value.
243243
*
244-
* @param configName The name of the dynamic config or experiment to be overriden
244+
* @param configName The name of the dynamic config or experiment to be overridden
245245
* @param configValue The value that will be returned
246246
*/
247247
@JvmStatic
@@ -359,14 +359,14 @@ class Statsig {
359359
serverSecret: String,
360360
options: StatsigOptions = StatsigOptions(),
361361
): CompletableFuture<Void?> {
362-
if (!::statsigServer.isInitialized) { // Quick check without synchronization
362+
if (!isInitialized()) { // Quick check without synchronization
363363
synchronized(this) {
364-
if (!::statsigServer.isInitialized
364+
if (!isInitialized()
365365
) { // Secondary check in case another thread already created the default server
366-
statsigServer = StatsigServer.create(serverSecret, options)
366+
statsigServer = StatsigServer.create()
367367
}
368368
}
369-
return statsigServer.initializeAsync()
369+
return statsigServer.initializeAsync(serverSecret, options)
370370
}
371371
return CompletableFuture.completedFuture(null)
372372
}
@@ -631,12 +631,17 @@ class Statsig {
631631
runBlocking { statsigServer.shutdown() }
632632
}
633633

634+
@JvmStatic
635+
fun isInitialized(): Boolean {
636+
return ::statsigServer.isInitialized && statsigServer.initialized
637+
}
638+
634639
private fun checkInitialized(): Boolean {
635-
if (!::statsigServer.isInitialized) {
640+
val initialized = isInitialized()
641+
if (!initialized) {
636642
println("Call and wait for initialize to complete before calling SDK methods.")
637-
return false
638643
}
639-
return true
644+
return initialized
640645
}
641646
}
642647
}

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

Lines changed: 44 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,26 @@ package com.statsig.sdk
33
import com.google.gson.Gson
44
import com.google.gson.GsonBuilder
55
import com.google.gson.ToNumberPolicy
6-
import kotlinx.coroutines.CoroutineExceptionHandler
7-
import kotlinx.coroutines.CoroutineScope
8-
import kotlinx.coroutines.SupervisorJob
9-
import kotlinx.coroutines.cancel
10-
import kotlinx.coroutines.cancelAndJoin
6+
import kotlinx.coroutines.*
117
import kotlinx.coroutines.future.future
12-
import kotlinx.coroutines.runBlocking
138
import kotlinx.coroutines.sync.Mutex
149
import kotlinx.coroutines.sync.withLock
1510
import java.util.Collections.emptyMap
1611
import java.util.concurrent.CompletableFuture
1712

1813
sealed class StatsigServer {
19-
internal abstract val errorBoundary: ErrorBoundary
14+
internal abstract var errorBoundary: ErrorBoundary
15+
abstract var initialized: Boolean
2016

21-
@JvmSynthetic abstract suspend fun initialize()
17+
@JvmSynthetic abstract fun setup(
18+
serverSecret: String,
19+
options: StatsigOptions,
20+
)
21+
22+
@JvmSynthetic abstract suspend fun initialize(
23+
serverSecret: String,
24+
options: StatsigOptions,
25+
)
2226

2327
@JvmSynthetic abstract suspend fun checkGate(user: StatsigUser, gateName: String): Boolean
2428

@@ -105,7 +109,7 @@ sealed class StatsigServer {
105109
metadata: Map<String, String>? = null,
106110
)
107111

108-
abstract fun initializeAsync(): CompletableFuture<Void?>
112+
abstract fun initializeAsync(serverSecret: String, options: StatsigOptions): CompletableFuture<Void?>
109113

110114
abstract fun checkGateAsync(user: StatsigUser, gateName: String): CompletableFuture<Boolean>
111115
abstract fun checkGateWithExposureLoggingDisabledAsync(user: StatsigUser, gateName: String): CompletableFuture<Boolean>
@@ -169,42 +173,43 @@ sealed class StatsigServer {
169173

170174
@JvmStatic
171175
@JvmOverloads
172-
fun create(
173-
serverSecret: String,
174-
options: StatsigOptions = StatsigOptions(),
175-
): StatsigServer = StatsigServerImpl(serverSecret, options)
176+
fun create(): StatsigServer = StatsigServerImpl()
176177
}
177178
}
178179

179-
private class StatsigServerImpl(serverSecret: String, private val options: StatsigOptions) :
180+
private class StatsigServerImpl() :
180181
StatsigServer() {
181182

182-
init {
183-
if (serverSecret.isEmpty() || !serverSecret.startsWith("secret-")) {
184-
throw StatsigUninitializedException(
185-
"Statsig Server SDKs must be initialized with a secret key",
186-
)
187-
}
188-
}
189-
190183
private val gson = GsonBuilder().setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE).create()
191184

192-
override val errorBoundary = ErrorBoundary(serverSecret, options)
193-
private val coroutineExceptionHandler =
194-
CoroutineExceptionHandler { _, ex ->
185+
override lateinit var errorBoundary: ErrorBoundary
186+
private lateinit var coroutineExceptionHandler: CoroutineExceptionHandler
187+
private lateinit var statsigJob: CompletableJob
188+
private lateinit var statsigScope: CoroutineScope
189+
private lateinit var network: StatsigNetwork
190+
private lateinit var logger: StatsigLogger
191+
private lateinit var configEvaluator: Evaluator
192+
private lateinit var diagnostics: Diagnostics
193+
private var options: StatsigOptions = StatsigOptions()
194+
private val mutex = Mutex()
195+
private val statsigMetadata = StatsigMetadata.asMap()
196+
override var initialized = false
197+
198+
override fun setup(serverSecret: String, options: StatsigOptions) {
199+
errorBoundary = ErrorBoundary(serverSecret, options)
200+
coroutineExceptionHandler = CoroutineExceptionHandler { _, ex ->
195201
// no-op - supervisor job should not throw when a child fails
196202
errorBoundary.logException("coroutineExceptionHandler", ex)
197203
}
198-
private val statsigJob = SupervisorJob()
199-
private val statsigScope = CoroutineScope(statsigJob + coroutineExceptionHandler)
200-
private val mutex = Mutex()
201-
private val statsigMetadata = StatsigMetadata.asMap()
202-
private val network = StatsigNetwork(serverSecret, options, statsigMetadata, errorBoundary)
203-
private var logger: StatsigLogger = StatsigLogger(statsigScope, network, statsigMetadata)
204-
private lateinit var configEvaluator: Evaluator
205-
private lateinit var diagnostics: Diagnostics
204+
statsigJob = SupervisorJob()
205+
statsigScope = CoroutineScope(statsigJob + coroutineExceptionHandler)
206+
network = StatsigNetwork(serverSecret, options, statsigMetadata, errorBoundary)
207+
logger = StatsigLogger(statsigScope, network, statsigMetadata)
208+
this.options = options
209+
}
206210

207-
override suspend fun initialize() {
211+
override suspend fun initialize(serverSecret: String, options: StatsigOptions) {
212+
setup(serverSecret, options)
208213
errorBoundary.capture(
209214
"initialize",
210215
{
@@ -218,6 +223,7 @@ private class StatsigServerImpl(serverSecret: String, private val options: Stats
218223
configEvaluator =
219224
Evaluator(network, options, statsigScope, errorBoundary, diagnostics)
220225
configEvaluator.initialize()
226+
initialized = true
221227
endInitDiagnostics(isSDKInitialized())
222228
}
223229
},
@@ -472,6 +478,7 @@ private class StatsigServerImpl(serverSecret: String, private val options: Stats
472478
configEvaluator.shutdown()
473479
statsigJob.cancelAndJoin()
474480
statsigScope.cancel()
481+
initialized = false
475482
}
476483
}
477484

@@ -503,9 +510,10 @@ private class StatsigServerImpl(serverSecret: String, private val options: Stats
503510
}, { return@captureSync })
504511
}
505512

506-
override fun initializeAsync(): CompletableFuture<Void?> {
513+
override fun initializeAsync(serverSecret: String, options: StatsigOptions): CompletableFuture<Void?> {
514+
setup(serverSecret, options)
507515
return statsigScope.future {
508-
initialize()
516+
initialize(serverSecret, options)
509517
null
510518
}
511519
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,20 +90,20 @@ class ConcurrencyTest {
9090
}
9191
}
9292

93-
val options = StatsigOptions().apply {
93+
options = StatsigOptions().apply {
9494
api = server.url("/v1").toString()
9595
disableDiagnostics = true
9696

9797
// set sync interval to be short, so we are modifying and checking values at the same time
9898
rulesetsSyncIntervalMs = 10
9999
idListsSyncIntervalMs = 10
100100
}
101-
driver = StatsigServer.create("secret-testcase", options)
101+
driver = StatsigServer.create()
102102
}
103103

104104
@Test
105105
fun testCallingAPIsFromDifferentThreads() = runBlocking {
106-
driver.initialize()
106+
driver.initialize("secret-testcase", options)
107107
val threads = arrayListOf<Thread>()
108108
for (i in 1..20) {
109109
val t =

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

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,8 @@ class DataStoreTest {
9090

9191
@Test
9292
fun dataStoreIsLoaded() {
93-
driver = StatsigServer.create("secret-local", networkOptions)
94-
driver.initializeAsync().get()
93+
driver = StatsigServer.create()
94+
driver.initializeAsync("secret-local", networkOptions).get()
9595

9696
val res = driver.checkGateAsync(user, "gate_from_adapter_always_on").get()
9797
Assert.assertTrue(res)
@@ -105,8 +105,8 @@ class DataStoreTest {
105105
bootstrapValues = downloadConfigSpecsResponse,
106106
disableDiagnostics = true,
107107
)
108-
driver = StatsigServer.create("secret-local", networkOptions)
109-
driver.initializeAsync().get()
108+
driver = StatsigServer.create()
109+
driver.initializeAsync("secret-local", networkOptions).get()
110110
val dataStoreGateRes = driver.checkGateAsync(user, "gate_from_adapter_always_on").get()
111111
val bootstrapGateRes = driver.checkGateAsync(user, "always_on").get()
112112
driver.shutdown()
@@ -131,8 +131,8 @@ class DataStoreTest {
131131
bootstrapValues = downloadConfigSpecsResponse,
132132
disableDiagnostics = true,
133133
)
134-
driver = StatsigServer.create("secret-local", networkOptions)
135-
driver.initializeAsync().get()
134+
driver = StatsigServer.create()
135+
driver.initializeAsync("secret-local", networkOptions).get()
136136
val bootstrapGateRes = driver.checkGateAsync(user, "always_on").get()
137137
driver.shutdown()
138138

@@ -147,16 +147,16 @@ class DataStoreTest {
147147
val networkOptions = StatsigOptions(
148148
api = server.url("/v1").toString(),
149149
)
150-
driver = StatsigServer.create("secret-local", networkOptions)
151-
driver.initializeAsync().get()
150+
driver = StatsigServer.create()
151+
driver.initializeAsync("secret-local", networkOptions).get()
152152

153153
Assert.assertTrue(didCallDownloadConfig)
154154
}
155155

156156
@Test
157157
fun testNetworkNotCalledWhenAdapterIsPresent() {
158-
driver = StatsigServer.create("secret-local", networkOptions)
159-
driver.initializeAsync().get()
158+
driver = StatsigServer.create()
159+
driver.initializeAsync("secret-local", networkOptions).get()
160160

161161
Assert.assertFalse(didCallDownloadConfig)
162162
}
@@ -167,8 +167,8 @@ class DataStoreTest {
167167
api = server.url("/v1").toString(),
168168
bootstrapValues = downloadConfigSpecsResponse,
169169
)
170-
driver = StatsigServer.create("secret-local", networkOptions)
171-
driver.initializeAsync().get()
170+
driver = StatsigServer.create()
171+
driver.initializeAsync("secret-local", networkOptions).get()
172172

173173
Assert.assertFalse(didCallDownloadConfig)
174174
}
@@ -179,8 +179,8 @@ class DataStoreTest {
179179
api = server.url("/v1").toString(),
180180
bootstrapValues = "invalid bootstrap values",
181181
)
182-
driver = StatsigServer.create("secret-local", networkOptions)
183-
driver.initializeAsync().get()
182+
driver = StatsigServer.create()
183+
driver.initializeAsync("secret-local", networkOptions).get()
184184

185185
Assert.assertTrue(didCallDownloadConfig)
186186
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ class DiagnosticsTest {
4141
@Test
4242
fun testInitialize() = runBlocking {
4343
setupWebServer(downloadConfigSpecsResponse)
44-
driver.initializeAsync().get()
44+
driver.initializeAsync("secret-testcase", options).get()
4545
driver.shutdown()
4646
val events = TestUtil.captureEvents(eventLogInputCompletable)
4747
val diagnosticsEvent = events.find { it.eventName == "statsig::diagnostics" }
@@ -61,7 +61,7 @@ class DiagnosticsTest {
6161
fun testSamping() = runBlocking {
6262
val downloadConfigSpecsResponseWithSampling = StringBuilder(downloadConfigSpecsResponse).insert(downloadConfigSpecsResponse.length - 2, ",\n \"diagnostics\": {\"initialize\": \"0\"}").toString()
6363
setupWebServer(downloadConfigSpecsResponseWithSampling)
64-
driver.initializeAsync().get()
64+
driver.initializeAsync("secret-testcase", options).get()
6565
driver.shutdown()
6666
Assert.assertFalse(
6767
"should not have called log_event endpoint",
@@ -91,7 +91,7 @@ class DiagnosticsTest {
9191
options = StatsigOptions().apply {
9292
api = server.url("/v1").toString()
9393
}
94-
driver = StatsigServer.create("secret-testcase", options)
94+
driver = StatsigServer.create()
9595
}
9696
}
9797

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,8 @@ class EvaluationDetailsTest {
6969
disableDiagnostics = true
7070
}
7171

72-
driver = StatsigServer.create("secret-local", options)
73-
driver.initialize()
72+
driver = StatsigServer.create()
73+
driver.initialize("secret-local", options)
7474
}
7575

7676
@Test
@@ -160,8 +160,8 @@ class EvaluationDetailsTest {
160160
@Test
161161
fun bootstrapTest() = runBlocking {
162162
val options = StatsigOptions(bootstrapValues = configSpecsResponse, api = server.url("/v1").toString(), disableDiagnostics = true)
163-
val bootstrapServer = StatsigServer.create("secret-key", options)
164-
bootstrapServer.initialize()
163+
val bootstrapServer = StatsigServer.create()
164+
bootstrapServer.initialize("secret-key", options)
165165

166166
bootstrapServer.checkGate(user, "always_on_gate")
167167
bootstrapServer.getConfig(user, "test_config")

src/test/java/com/statsig/sdk/EvaluatorTest.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ public class EvaluatorTest {
2121

2222
@Test
2323
public void testIP3Country() throws NoSuchFieldException, IllegalAccessException, ExecutionException, InterruptedException {
24-
StatsigServer driver = StatsigServer.create("secret-local", new StatsigOptions());
25-
driver.initializeAsync().get();
24+
StatsigServer driver = StatsigServer.create();
25+
driver.initializeAsync("secret-local", new StatsigOptions()).get();
2626

2727
SpecStore specStore = TestUtilJava.getSpecStoreFromStatsigServer(driver);
2828
Evaluator eval = TestUtilJava.getEvaluatorFromStatsigServer(driver);
@@ -75,8 +75,8 @@ public CaseSensitiveTestCase (String name, Object one, String two, boolean ignor
7575

7676
@Test
7777
public void testCaseSensitivity() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, NoSuchFieldException, ExecutionException, InterruptedException {
78-
StatsigServer driver = StatsigServer.create("secret-local", new StatsigOptions());
79-
driver.initializeAsync().get();
78+
StatsigServer driver = StatsigServer.create();
79+
driver.initializeAsync("secret-local", new StatsigOptions()).get();
8080

8181
Evaluator eval = TestUtilJava.getEvaluatorFromStatsigServer(driver);
8282

0 commit comments

Comments
 (0)