Skip to content

Commit 848545a

Browse files
authored
feat: onConfigurationChange (#112)
* feat: onConfigurationChange from latest common * chore: bump version for minor release
1 parent e2ba029 commit 848545a

File tree

4 files changed

+100
-3
lines changed

4 files changed

+100
-3
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
```groovy
1111
dependencies {
12-
implementation 'cloud.eppo:eppo-server-sdk:5.0.0'
12+
implementation 'cloud.eppo:eppo-server-sdk:5.1.0'
1313
}
1414
```
1515

build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ java {
1111
}
1212

1313
group = 'cloud.eppo'
14-
version = '5.0.1-SNAPSHOT'
14+
version = '5.1.0'
1515
ext.isReleaseVersion = !version.endsWith("SNAPSHOT")
1616

1717
import org.apache.tools.ant.filters.ReplaceTokens
@@ -30,7 +30,7 @@ repositories {
3030
}
3131

3232
dependencies {
33-
api 'cloud.eppo:sdk-common-jvm:3.8.0'
33+
api 'cloud.eppo:sdk-common-jvm:3.9.0'
3434

3535
implementation 'com.github.zafarkhaja:java-semver:0.10.2'
3636
implementation 'com.fasterxml.jackson.core:jackson-databind:2.18.3'

src/main/java/cloud/eppo/EppoClient.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package cloud.eppo;
22

3+
import cloud.eppo.api.Configuration;
34
import cloud.eppo.api.IAssignmentCache;
45
import cloud.eppo.cache.ExpiringInMemoryAssignmentCache;
56
import cloud.eppo.cache.LRUInMemoryAssignmentCache;
67
import cloud.eppo.logging.AssignmentLogger;
78
import cloud.eppo.logging.BanditLogger;
89
import java.util.concurrent.TimeUnit;
10+
import java.util.function.Consumer;
911
import org.jetbrains.annotations.NotNull;
1012
import org.jetbrains.annotations.Nullable;
1113
import org.slf4j.Logger;
@@ -79,6 +81,7 @@ public static class Builder {
7981
private boolean forceReinitialize = DEFAULT_FORCE_REINITIALIZE;
8082
private long pollingIntervalMs = DEFAULT_POLLING_INTERVAL_MS;
8183
private String apiBaseUrl = null;
84+
@Nullable private Consumer<Configuration> configChangeCallback;
8285

8386
// Assignment and bandit caching on by default. To disable, call
8487
// `builder.assignmentCache(null).banditAssignmentCache(null);`
@@ -156,6 +159,14 @@ public Builder banditAssignmentCache(IAssignmentCache banditAssignmentCache) {
156159
return this;
157160
}
158161

162+
/**
163+
* Registers a callback for when a new configuration is applied to the `EppoClient` instance.
164+
*/
165+
public Builder onConfigurationChange(Consumer<Configuration> configChangeCallback) {
166+
this.configChangeCallback = configChangeCallback;
167+
return this;
168+
}
169+
159170
public EppoClient buildAndInit() {
160171
AppDetails appDetails = AppDetails.getInstance();
161172
String sdkName = appDetails.getName();
@@ -186,6 +197,10 @@ public EppoClient buildAndInit() {
186197
assignmentCache,
187198
banditAssignmentCache);
188199

200+
if (configChangeCallback != null) {
201+
instance.onConfigurationChange(configChangeCallback);
202+
}
203+
189204
// Fetch first configuration
190205
instance.loadConfiguration();
191206

src/test/java/cloud/eppo/EppoClientTest.java

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@
2525
import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
2626
import java.io.File;
2727
import java.lang.reflect.Field;
28+
import java.util.ArrayList;
29+
import java.util.List;
2830
import java.util.concurrent.CompletableFuture;
31+
import java.util.concurrent.ExecutionException;
2932
import java.util.stream.Stream;
3033
import org.apache.commons.io.FileUtils;
3134
import org.junit.jupiter.api.AfterAll;
@@ -51,6 +54,8 @@ public class EppoClientTest {
5154
private AssignmentLogger mockAssignmentLogger;
5255
private BanditLogger mockBanditLogger;
5356

57+
private static final byte[] EMPTY_CONFIG = "{\"flags\":{}}".getBytes();
58+
5459
@BeforeAll
5560
public static void initMockServer() {
5661
mockServer = new WireMockServer(TEST_PORT);
@@ -271,6 +276,47 @@ public void testGetConfiguration() {
271276
assertEquals(VariationType.NUMERIC, configuration.getFlagType("numeric_flag"));
272277
}
273278

279+
@Test
280+
public void testConfigurationChangeListener() throws ExecutionException, InterruptedException {
281+
List<Configuration> received = new ArrayList<>();
282+
283+
// Set up a changing response from the "server"
284+
EppoHttpClient mockHttpClient = mock(EppoHttpClient.class);
285+
286+
// Mock sync get to return empty
287+
when(mockHttpClient.get(anyString())).thenReturn(EMPTY_CONFIG);
288+
289+
// Mock async get to return empty
290+
when(mockHttpClient.get(anyString())).thenReturn(EMPTY_CONFIG);
291+
292+
setBaseClientHttpClientOverrideField(mockHttpClient);
293+
294+
EppoClient.Builder clientBuilder =
295+
EppoClient.builder(DUMMY_FLAG_API_KEY)
296+
.forceReinitialize(true)
297+
.onConfigurationChange(received::add)
298+
.isGracefulMode(false);
299+
300+
// Initialize and no exception should be thrown.
301+
EppoClient eppoClient = clientBuilder.buildAndInit();
302+
303+
verify(mockHttpClient, times(1)).get(anyString());
304+
assertEquals(1, received.size());
305+
306+
// Now, return the boolean flag config so that the config has changed.
307+
when(mockHttpClient.get(anyString())).thenReturn(BOOL_FLAG_CONFIG);
308+
309+
// Trigger a reload of the client
310+
eppoClient.loadConfiguration();
311+
312+
assertEquals(2, received.size());
313+
314+
// Reload the client again; the config hasn't changed, but Java doesn't check eTag (yet)
315+
eppoClient.loadConfiguration();
316+
317+
assertEquals(3, received.size());
318+
}
319+
274320
public static void mockHttpError() {
275321
// Create a mock instance of EppoHttpClient
276322
EppoHttpClient mockHttpClient = mock(EppoHttpClient.class);
@@ -353,4 +399,40 @@ public static void setBaseClientHttpClientOverrideField(EppoHttpClient httpClien
353399
throw new RuntimeException(e);
354400
}
355401
}
402+
403+
private static final byte[] BOOL_FLAG_CONFIG =
404+
("{\n"
405+
+ " \"createdAt\": \"2024-04-17T19:40:53.716Z\",\n"
406+
+ " \"format\": \"SERVER\",\n"
407+
+ " \"environment\": {\n"
408+
+ " \"name\": \"Test\"\n"
409+
+ " },\n"
410+
+ " \"flags\": {\n"
411+
+ " \"9a2025738dde19ff44cd30b9d2967000\": {\n"
412+
+ " \"key\": \"9a2025738dde19ff44cd30b9d2967000\",\n"
413+
+ " \"enabled\": true,\n"
414+
+ " \"variationType\": \"BOOLEAN\",\n"
415+
+ " \"variations\": {\n"
416+
+ " \"b24=\": {\n"
417+
+ " \"key\": \"b24=\",\n"
418+
+ " \"value\": \"dHJ1ZQ==\"\n"
419+
+ " }\n"
420+
+ " },\n"
421+
+ " \"allocations\": [\n"
422+
+ " {\n"
423+
+ " \"key\": \"b24=\",\n"
424+
+ " \"doLog\": true,\n"
425+
+ " \"splits\": [\n"
426+
+ " {\n"
427+
+ " \"variationKey\": \"b24=\",\n"
428+
+ " \"shards\": []\n"
429+
+ " }\n"
430+
+ " ]\n"
431+
+ " }\n"
432+
+ " ],\n"
433+
+ " \"totalShards\": 10000\n"
434+
+ " }\n"
435+
+ " }\n"
436+
+ "}")
437+
.getBytes();
356438
}

0 commit comments

Comments
 (0)