Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

```groovy
dependencies {
implementation 'cloud.eppo:eppo-server-sdk:5.0.0'
implementation 'cloud.eppo:eppo-server-sdk:5.1.0'
}
```

Expand Down
4 changes: 2 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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'
Expand Down
15 changes: 15 additions & 0 deletions src/main/java/cloud/eppo/EppoClient.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<Configuration> configChangeCallback;

// Assignment and bandit caching on by default. To disable, call
// `builder.assignmentCache(null).banditAssignmentCache(null);`
Expand Down Expand Up @@ -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<Configuration> configChangeCallback) {
this.configChangeCallback = configChangeCallback;
return this;
}

public EppoClient buildAndInit() {
AppDetails appDetails = AppDetails.getInstance();
String sdkName = appDetails.getName();
Expand Down Expand Up @@ -186,6 +197,10 @@ public EppoClient buildAndInit() {
assignmentCache,
banditAssignmentCache);

if (configChangeCallback != null) {
instance.onConfigurationChange(configChangeCallback);
}

// Fetch first configuration
instance.loadConfiguration();

Expand Down
82 changes: 82 additions & 0 deletions src/test/java/cloud/eppo/EppoClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -271,6 +276,47 @@ public void testGetConfiguration() {
assertEquals(VariationType.NUMERIC, configuration.getFlagType("numeric_flag"));
}

@Test
public void testConfigurationChangeListener() throws ExecutionException, InterruptedException {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ’ͺ

List<Configuration> 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);
Expand Down Expand Up @@ -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();
}