diff --git a/Makefile b/Makefile index f5041c8..531cee6 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/README.md b/README.md index 351449e..3bcebc0 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ ```groovy dependencies { - implementation 'cloud.eppo:eppo-server-sdk:3.1.0' + implementation 'cloud.eppo:eppo-server-sdk:4.0.1' } ``` diff --git a/build.gradle b/build.gradle index 2042a74..5316f8e 100644 --- a/build.gradle +++ b/build.gradle @@ -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 diff --git a/src/main/java/cloud/eppo/EppoClient.java b/src/main/java/cloud/eppo/EppoClient.java index f0a3b26..e26cc25 100644 --- a/src/main/java/cloud/eppo/EppoClient.java +++ b/src/main/java/cloud/eppo/EppoClient.java @@ -198,6 +198,8 @@ public EppoClient buildAndInit() { pollingIntervalMs / DEFAULT_JITTER_INTERVAL_RATIO); // Kick off the first fetch + // Graceful mode is implicit here because `FetchConfigurationsTask` catches and logs errors + // without rethrowing. fetchConfigurationsTask.run(); return instance; diff --git a/src/test/java/cloud/eppo/EppoClientTest.java b/src/test/java/cloud/eppo/EppoClientTest.java index 582f7fa..924bd86 100644 --- a/src/test/java/cloud/eppo/EppoClientTest.java +++ b/src/test/java/cloud/eppo/EppoClientTest.java @@ -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; @@ -238,6 +239,39 @@ public void testPolling() { verify(httpClientSpy, times(2)).get(anyString()); } + // NOTE: Graceful mode during init is intrinsically true since the call is non-blocking and + // exceptions are caught without rethrowing in `FetchConfigurationsTask` + + @Test + public void testClientMakesDefaultAssignmentsAfterFailingToInitialize() { + // Set up bad HTTP response + mockHttpError(); + + // Initialize and no exception should be thrown. + try { + EppoClient eppoClient = initFailingGracefulClient(true); + Thread.sleep(25); // Sleep to allow the async config fetch call to happen (and fail) + assertEquals("default", eppoClient.getStringAssignment("experiment1", "subject1", "default")); + } catch (Exception e) { + fail("Unexpected exception: " + e); + } + } + + 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 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 { @@ -261,6 +295,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"); @@ -281,4 +329,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); + } + } }