From d18c39fc1ae7d15daff1cc64383f577110fd67c5 Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Tue, 11 Mar 2025 13:16:53 -0600 Subject: [PATCH 1/3] feat: onConfigurationChange from latest common --- src/main/java/cloud/eppo/EppoClient.java | 15 ++++ src/test/java/cloud/eppo/EppoClientTest.java | 82 ++++++++++++++++++++ 2 files changed, 97 insertions(+) diff --git a/src/main/java/cloud/eppo/EppoClient.java b/src/main/java/cloud/eppo/EppoClient.java index 0dea696..8e351f4 100644 --- a/src/main/java/cloud/eppo/EppoClient.java +++ b/src/main/java/cloud/eppo/EppoClient.java @@ -1,11 +1,13 @@ package cloud.eppo; +import cloud.eppo.api.Configuration; import cloud.eppo.api.IAssignmentCache; import cloud.eppo.cache.ExpiringInMemoryAssignmentCache; import cloud.eppo.cache.LRUInMemoryAssignmentCache; import cloud.eppo.logging.AssignmentLogger; import cloud.eppo.logging.BanditLogger; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @@ -79,6 +81,7 @@ public static class Builder { private boolean forceReinitialize = DEFAULT_FORCE_REINITIALIZE; private long pollingIntervalMs = DEFAULT_POLLING_INTERVAL_MS; private String apiBaseUrl = null; + @Nullable private Consumer configChangeCallback; // Assignment and bandit caching on by default. To disable, call // `builder.assignmentCache(null).banditAssignmentCache(null);` @@ -156,6 +159,14 @@ public Builder banditAssignmentCache(IAssignmentCache banditAssignmentCache) { return this; } + /** + * Registers a callback for when a new configuration is applied to the `EppoClient` instance. + */ + public Builder onConfigurationChange(Consumer configChangeCallback) { + this.configChangeCallback = configChangeCallback; + return this; + } + public EppoClient buildAndInit() { AppDetails appDetails = AppDetails.getInstance(); String sdkName = appDetails.getName(); @@ -186,6 +197,10 @@ public EppoClient buildAndInit() { assignmentCache, banditAssignmentCache); + if (configChangeCallback != null) { + instance.onConfigurationChange(configChangeCallback); + } + // Fetch first configuration instance.loadConfiguration(); diff --git a/src/test/java/cloud/eppo/EppoClientTest.java b/src/test/java/cloud/eppo/EppoClientTest.java index e744d66..2e0ab67 100644 --- a/src/test/java/cloud/eppo/EppoClientTest.java +++ b/src/test/java/cloud/eppo/EppoClientTest.java @@ -25,7 +25,10 @@ import com.github.tomakehurst.wiremock.junit5.WireMockExtension; import java.io.File; import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; import java.util.stream.Stream; import org.apache.commons.io.FileUtils; import org.junit.jupiter.api.AfterAll; @@ -51,6 +54,8 @@ public class EppoClientTest { private AssignmentLogger mockAssignmentLogger; private BanditLogger mockBanditLogger; + private static final byte[] EMPTY_CONFIG = "{\"flags\":{}}".getBytes(); + @BeforeAll public static void initMockServer() { mockServer = new WireMockServer(TEST_PORT); @@ -271,6 +276,47 @@ public void testGetConfiguration() { assertEquals(VariationType.NUMERIC, configuration.getFlagType("numeric_flag")); } + @Test + public void testConfigurationChangeListener() throws ExecutionException, InterruptedException { + List received = new ArrayList<>(); + + // Set up a changing response from the "server" + EppoHttpClient mockHttpClient = mock(EppoHttpClient.class); + + // Mock sync get to return empty + when(mockHttpClient.get(anyString())).thenReturn(EMPTY_CONFIG); + + // Mock async get to return empty + when(mockHttpClient.get(anyString())).thenReturn(EMPTY_CONFIG); + + setBaseClientHttpClientOverrideField(mockHttpClient); + + EppoClient.Builder clientBuilder = + EppoClient.builder(DUMMY_FLAG_API_KEY) + .forceReinitialize(true) + .onConfigurationChange(received::add) + .isGracefulMode(false); + + // Initialize and no exception should be thrown. + EppoClient eppoClient = clientBuilder.buildAndInit(); + + verify(mockHttpClient, times(1)).get(anyString()); + assertEquals(1, received.size()); + + // Now, return the boolean flag config so that the config has changed. + when(mockHttpClient.get(anyString())).thenReturn(BOOL_FLAG_CONFIG); + + // Trigger a reload of the client + eppoClient.loadConfiguration(); + + assertEquals(2, received.size()); + + // Reload the client again; the config hasn't changed, but Java doesn't check eTag (yet) + eppoClient.loadConfiguration(); + + assertEquals(3, received.size()); + } + public static void mockHttpError() { // Create a mock instance of EppoHttpClient EppoHttpClient mockHttpClient = mock(EppoHttpClient.class); @@ -353,4 +399,40 @@ public static void setBaseClientHttpClientOverrideField(EppoHttpClient httpClien throw new RuntimeException(e); } } + + private static final byte[] BOOL_FLAG_CONFIG = + ("{\n" + + " \"createdAt\": \"2024-04-17T19:40:53.716Z\",\n" + + " \"format\": \"SERVER\",\n" + + " \"environment\": {\n" + + " \"name\": \"Test\"\n" + + " },\n" + + " \"flags\": {\n" + + " \"9a2025738dde19ff44cd30b9d2967000\": {\n" + + " \"key\": \"9a2025738dde19ff44cd30b9d2967000\",\n" + + " \"enabled\": true,\n" + + " \"variationType\": \"BOOLEAN\",\n" + + " \"variations\": {\n" + + " \"b24=\": {\n" + + " \"key\": \"b24=\",\n" + + " \"value\": \"dHJ1ZQ==\"\n" + + " }\n" + + " },\n" + + " \"allocations\": [\n" + + " {\n" + + " \"key\": \"b24=\",\n" + + " \"doLog\": true,\n" + + " \"splits\": [\n" + + " {\n" + + " \"variationKey\": \"b24=\",\n" + + " \"shards\": []\n" + + " }\n" + + " ]\n" + + " }\n" + + " ],\n" + + " \"totalShards\": 10000\n" + + " }\n" + + " }\n" + + "}") + .getBytes(); } From 45f2ec64163646762be02142ebc55a05f771b679 Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Tue, 11 Mar 2025 13:17:29 -0600 Subject: [PATCH 2/3] chore: bump version for minor release --- README.md | 2 +- build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7a68331..9dd90d6 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ ```groovy dependencies { - implementation 'cloud.eppo:eppo-server-sdk:5.0.0' + implementation 'cloud.eppo:eppo-server-sdk:5.1.0' } ``` diff --git a/build.gradle b/build.gradle index 5f50969..63587b5 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,7 @@ java { } group = 'cloud.eppo' -version = '5.0.1-SNAPSHOT' +version = '5.1.0' ext.isReleaseVersion = !version.endsWith("SNAPSHOT") import org.apache.tools.ant.filters.ReplaceTokens From 1fef24f922cb19f37b4ba37d457b8df174326e74 Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Tue, 11 Mar 2025 21:27:29 -0600 Subject: [PATCH 3/3] latest common --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 63587b5..5107785 100644 --- a/build.gradle +++ b/build.gradle @@ -30,7 +30,7 @@ repositories { } dependencies { - api 'cloud.eppo:sdk-common-jvm:3.8.0' + api 'cloud.eppo:sdk-common-jvm:3.9.0' implementation 'com.github.zafarkhaja:java-semver:0.10.2' implementation 'com.fasterxml.jackson.core:jackson-databind:2.18.2'