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 Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ test-data:
mkdir -p ${tempDir}
git clone -b ${branchName} --depth 1 --single-branch ${githubRepoLink} ${gitDataDir}
cp -r ${gitDataDir}/ufc ${testDataDir}
rm ${testDataDir}/ufc/bandit-tests/*.dynamic-typing.json
rm -f ${testDataDir}/ufc/bandit-tests/*.dynamic-typing.json
rm -rf ${tempDir}

.PHONY: test
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 = '4.0.0-SNAPSHOT'
version = '4.0.1'
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.5.0'
api 'cloud.eppo:sdk-common-jvm:3.5.4'

implementation 'com.github.zafarkhaja:java-semver:0.10.2'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.1'
Expand Down
13 changes: 11 additions & 2 deletions src/main/java/cloud/eppo/EppoClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,17 @@ public EppoClient buildAndInit() {
pollingIntervalMs,
pollingIntervalMs / DEFAULT_JITTER_INTERVAL_RATIO);

// Kick off the first fetch
fetchConfigurationsTask.run();
fetchConfigurationsTask.scheduleNext();

// Kick off the first fetch, respecting graceful mode.
try {
instance.loadConfiguration();
} catch (RuntimeException e) {
log.error("Encountered Exception while loading configuration", e);
if (!isGracefulMode) {
throw e;
}
}

return instance;
}
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/cloud/eppo/FetchConfigurationsTask.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ public void run() {
} catch (Exception e) {
log.error("[Eppo SDK] Error fetching experiment configuration", e);
}
scheduleNext();
}

public void scheduleNext() {

long delay = this.intervalInMillis - (long) (Math.random() * this.jitterInMillis);
FetchConfigurationsTask nextTask =
Expand Down
92 changes: 92 additions & 0 deletions src/test/java/cloud/eppo/EppoClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
import java.io.File;
import java.lang.reflect.Field;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Stream;
import org.apache.commons.io.FileUtils;
import org.junit.jupiter.api.AfterAll;
Expand Down Expand Up @@ -238,6 +239,71 @@ public void testPolling() {
verify(httpClientSpy, times(2)).get(anyString());
}

@Test
public void testGracefulInitializationFailure() {
// Set up bad HTTP response
mockHttpError();

// Initialize and no exception should be thrown.
assertDoesNotThrow(() -> initFailingGracefulClient(true));
}

@Test
public void testClientMakesDefaultAssignmentsAfterFailingToInitialize() {
// Set up bad HTTP response
mockHttpError();

// Initialize and no exception should be thrown.
try {
EppoClient eppoClient = initFailingGracefulClient(true);
assertEquals("default", eppoClient.getStringAssignment("experiment1", "subject1", "default"));
} catch (Exception e) {
fail("Unexpected exception: " + e);
}
}

@Test
public void testClientMakesDefaultAssignmentsAfterFailingToInitializeNonGracefulMode() {
// Set up bad HTTP response
mockHttpError();

// Initialize and the exception should be thrown.
try {
initFailingGracefulClient(false);
} catch (RuntimeException e) {
// Expected
assertEquals("Intentional Error", e.getMessage());
} finally {
assertEquals(
"default",
EppoClient.getInstance().getStringAssignment("experiment1", "subject1", "default"));
}
}

@Test
public void testNonGracefulInitializationFailure() {
// Set up bad HTTP response
mockHttpError();

// Initialize and assert exception thrown
assertThrows(Exception.class, () -> initFailingGracefulClient(false));
}

public static void mockHttpError() {
// Create a mock instance of EppoHttpClient
EppoHttpClient mockHttpClient = mock(EppoHttpClient.class);

// Mock sync get
when(mockHttpClient.get(anyString())).thenThrow(new RuntimeException("Intentional Error"));

// Mock async get
CompletableFuture<byte[]> mockAsyncResponse = new CompletableFuture<>();
when(mockHttpClient.getAsync(anyString())).thenReturn(mockAsyncResponse);
mockAsyncResponse.completeExceptionally(new RuntimeException("Intentional Error"));

setBaseClientHttpClientOverrideField(mockHttpClient);
}

@SuppressWarnings("SameParameterValue")
private void sleepUninterruptedly(long sleepMs) {
try {
Expand All @@ -261,6 +327,20 @@ private EppoClient initClient(String apiKey) {
.buildAndInit();
}

private EppoClient initFailingGracefulClient(boolean isGracefulMode) {
mockAssignmentLogger = mock(AssignmentLogger.class);
mockBanditLogger = mock(BanditLogger.class);

return new EppoClient.Builder()
.apiKey(DUMMY_FLAG_API_KEY)
.host("blag")
.assignmentLogger(mockAssignmentLogger)
.banditLogger(mockBanditLogger)
.isGracefulMode(isGracefulMode)
.forceReinitialize(true) // Useful for tests
.buildAndInit();
}

private void uninitClient() {
try {
Field httpClientOverrideField = EppoClient.class.getDeclaredField("instance");
Expand All @@ -281,4 +361,16 @@ private void initBuggyClient() {
throw new RuntimeException(e);
}
}

public static void setBaseClientHttpClientOverrideField(EppoHttpClient httpClient) {
// Uses reflection to set a static override field used for tests (e.g., httpClientOverride)
try {
Field httpClientOverrideField = BaseEppoClient.class.getDeclaredField("httpClientOverride");
httpClientOverrideField.setAccessible(true);
httpClientOverrideField.set(null, httpClient);
httpClientOverrideField.setAccessible(false);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
Loading