Skip to content

Commit

Permalink
feat: re-init / manual sync config specs (#394)
Browse files Browse the repository at this point in the history
Please see this pr for details:
statsig-io/private-java-server-sdk#392
  • Loading branch information
weihao-statsig authored Oct 18, 2024
1 parent 85922c9 commit f576480
Show file tree
Hide file tree
Showing 8 changed files with 166 additions and 1 deletion.
7 changes: 7 additions & 0 deletions src/main/kotlin/com/statsig/sdk/Evaluator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ internal class Evaluator(
specStore.shutdown()
}

suspend fun syncConfigSpecs(): FailureDetails? {
if (!isInitialized) {
return FailureDetails(FailureReason.EMPTY_SPEC)
}
return specStore.syncConfigSpecs()
}

private fun createEvaluationDetails(reason: EvaluationReason): EvaluationDetails {
if (reason == EvaluationReason.UNINITIALIZED) {
return EvaluationDetails(0, 0, reason)
Expand Down
5 changes: 5 additions & 0 deletions src/main/kotlin/com/statsig/sdk/InitializationDetails.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ data class InitializationDetails(
var failureDetails: FailureDetails? = null,
)

data class ConfigSyncDetails(
@SerializedName("details")
var details: InitializationDetails
)

data class FailureDetails(
@SerializedName("reason") var reason: FailureReason,
@SerializedName("exception") var exception: Exception? = null,
Expand Down
6 changes: 5 additions & 1 deletion src/main/kotlin/com/statsig/sdk/SpecStore.kt
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ internal class SpecStore(
if (!options.localMode) {
specUpdater.initialize()

var failureDetails = this.initializeSpecs()
val failureDetails = this.initializeSpecs()
this.initTime = if (specUpdater.lastUpdateTime == 0L) -1 else specUpdater.lastUpdateTime

this.syncIdListsFromNetwork(specUpdater.updateIDLists())
Expand All @@ -67,6 +67,10 @@ internal class SpecStore(
this.specUpdater.shutdown()
}

suspend fun syncConfigSpecs(): FailureDetails? {
return initializeSpecs()
}

fun setDownloadedConfigs(downloadedConfig: APIDownloadedConfigs, isFromBootstrap: Boolean = false): Boolean {
if (!downloadedConfig.hasUpdates) {
return false
Expand Down
11 changes: 11 additions & 0 deletions src/main/kotlin/com/statsig/sdk/Statsig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -784,6 +784,17 @@ class Statsig {
runBlocking { statsigServer.shutdown() }
}

/**
* Manually sync config specs if initialize is failed
*/
@JvmStatic
fun syncConfigSpecs(): CompletableFuture<ConfigSyncDetails> {
if (!checkInitialized()) {
return CompletableFuture.completedFuture(ConfigSyncDetails(InitializationDetails(0, false, false)))
}
return statsigServer.syncConfigSpecs()
}

@JvmStatic
fun isInitialized(): Boolean {
return ::statsigServer.isInitialized && statsigServer.initialized.get()
Expand Down
27 changes: 27 additions & 0 deletions src/main/kotlin/com/statsig/sdk/StatsigServer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ sealed class StatsigServer {
@JvmSynthetic
abstract suspend fun manuallyLogConfigExposure(user: StatsigUser, configName: String)

abstract fun syncConfigSpecs(): CompletableFuture<ConfigSyncDetails>

@JvmSynthetic
abstract suspend fun getExperiment(user: StatsigUser, experimentName: String): DynamicConfig

Expand Down Expand Up @@ -583,6 +585,31 @@ private class StatsigServerImpl() :
}
}

override fun syncConfigSpecs(): CompletableFuture<ConfigSyncDetails> {
return statsigScope.future {
errorBoundary.capture("syncConfigSpecs", {
val failureDetails = evaluator.syncConfigSpecs()
ConfigSyncDetails(
InitializationDetails(
System.currentTimeMillis() - setupStartTime,
isSDKReady = isSDKInitialized(),
configSpecReady = failureDetails == null,
failureDetails,
)
)
}, {
ConfigSyncDetails(
InitializationDetails(
System.currentTimeMillis() - setupStartTime,
isSDKReady = isSDKInitialized(),
configSpecReady = false,
FailureDetails(FailureReason.INTERNAL_ERROR),
)
)
})
}
}

override suspend fun getExperiment(user: StatsigUser, experimentName: String): DynamicConfig {
if (!isSDKInitialized()) {
return DynamicConfig.empty(experimentName)
Expand Down
9 changes: 9 additions & 0 deletions src/test/java/com/statsig/sdk/APIOverrideTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import okhttp3.mockwebserver.Dispatcher
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import okhttp3.mockwebserver.RecordedRequest
import org.junit.After
import org.junit.Before
import org.junit.Test

Expand Down Expand Up @@ -41,6 +42,7 @@ class APIOverrideTest {
}
}
}

server2.apply {
dispatcher = object : Dispatcher() {
@Throws(InterruptedException::class)
Expand Down Expand Up @@ -89,6 +91,13 @@ class APIOverrideTest {
server3Calls.clear()
}

@After
fun tearDown() {
server1.shutdown()
server2.shutdown()
server3.shutdown()
}

@Test
fun testAPIOverride() {
val options = StatsigOptions(
Expand Down
1 change: 1 addition & 0 deletions src/test/java/com/statsig/sdk/StatsigE2ETest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ class StatsigE2ETest {
@After
fun afterEach() {
server.shutdown()
driver.shutdown()
}

@Test
Expand Down
101 changes: 101 additions & 0 deletions src/test/java/com/statsig/sdk/StatsigReinitializeTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package com.statsig.sdk;

import okhttp3.mockwebserver.Dispatcher;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import org.jetbrains.annotations.NotNull;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;

public class StatsigReinitializeTest {

private static final int MAX_CNT = 2;

private MockWebServer server;
private StatsigOptions options;
private int requestCnt;
private String configSpecsResponse;

@Before
public void setUp() throws IOException {
configSpecsResponse = new String(Files.readAllBytes(Paths.get(Objects.requireNonNull(getClass().getResource("/download_config_specs.json")).getPath())));
server = new MockWebServer();
requestCnt = 0;
server.setDispatcher(new Dispatcher() {
@NotNull
@Override
public MockResponse dispatch(@NotNull RecordedRequest request) {
// Simulate failure on first request, success on subsequent requests
if (request.getPath().contains("v1/download_config_specs")) {
if (requestCnt < MAX_CNT) {
requestCnt++;
return new MockResponse().setResponseCode(500);
} else {
return new MockResponse().setResponseCode(200).setBody(configSpecsResponse);
}
}
return new MockResponse().setResponseCode(200);
}
});

server.start(1080);

options = new StatsigOptions();
options.setApi(server.url("/v1").toString());
options.setDisableDiagnostics(true);
}

@After
public void tearDown() throws IOException {
server.shutdown();
Statsig.shutdown();
}

@Test
public void testReinitialize() throws Exception {
CompletableFuture<InitializationDetails> detailsFuture = Statsig.initializeAsync("server-secret", options);
StatsigUser user = new StatsigUser("123");
user.setEmail("[email protected]");

InitializationDetails details = detailsFuture.get();
Assert.assertNotNull(details);
Assert.assertTrue(details.isSDKReady());
Assert.assertFalse(details.getConfigSpecReady()); // First init should fail due to 500 response
Assert.assertNotNull(details.getFailureDetails()); // Expect failure details for the first init
Assert.assertFalse(Statsig.checkGateSync(user, "always_on_gate"));
Assert.assertFalse(Statsig.checkGateSync(user, "on_for_statsig_email"));
Layer layer1 = Statsig.getLayerSync(user, "c_layer_with_holdout");
Assert.assertEquals(layer1.getString("holdout_layer_param", "should_return_default"), "should_return_default");


CompletableFuture<ConfigSyncDetails> details2Future = Statsig.syncConfigSpecs();
ConfigSyncDetails details2 = details2Future.get();
Assert.assertNotNull(details2);
Assert.assertTrue(details2.getDetails().isSDKReady());
Assert.assertFalse(details2.getDetails().getConfigSpecReady()); // Second try should also fail
Assert.assertFalse(Statsig.checkGateSync(user, "always_on_gate"));
Assert.assertFalse(Statsig.checkGateSync(user, "on_for_statsig_email"));
Layer layer2 = Statsig.getLayerSync(user, "c_layer_with_holdout");
Assert.assertEquals(layer2.getString("holdout_layer_param", "should_return_default"), "should_return_default");

CompletableFuture<ConfigSyncDetails> details3Future = Statsig.syncConfigSpecs();
ConfigSyncDetails details3 = details3Future.get();
Assert.assertNotNull(details3);
Assert.assertTrue(details3.getDetails().isSDKReady());
Assert.assertTrue(details3.getDetails().getConfigSpecReady()); // Third try should succeed with 200 response
Assert.assertNull(details3.getDetails().getFailureDetails()); // No failure details expected for successful init
Assert.assertTrue(Statsig.checkGateSync(user, "always_on_gate"));
Assert.assertTrue(Statsig.checkGateSync(user, "on_for_statsig_email"));
Layer layer3 = Statsig.getLayerSync(user, "c_layer_with_holdout");
Assert.assertEquals(layer3.getString("holdout_layer_param", "should_not_return_default"), "layer_default");
}
}

0 comments on commit f576480

Please sign in to comment.