Skip to content

Commit 95d0cda

Browse files
authored
fix: respect graceful mode during client initialization (#68)
* bump for patch release * respect graceful mode on initialization * test * update version string in tests
1 parent e1a5b57 commit 95d0cda

File tree

5 files changed

+143
-12
lines changed

5 files changed

+143
-12
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ or [JVM](https://github.com/Eppo-exp/java-server-sdk) SDKs.
1010

1111
```groovy
1212
dependencies {
13-
implementation 'cloud.eppo:sdk-common-jvm:3.5.0'
13+
implementation 'cloud.eppo:sdk-common-jvm:3.5.3'
1414
}
1515
```
1616

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ plugins {
66
}
77

88
group = 'cloud.eppo'
9-
version = '3.5.2-SNAPSHOT'
9+
version = '3.5.3'
1010
ext.isReleaseVersion = !version.endsWith("SNAPSHOT")
1111

1212
java {

src/main/java/cloud/eppo/BaseEppoClient.java

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,11 +110,32 @@ private EppoHttpClient buildHttpClient(
110110
}
111111

112112
protected void loadConfiguration() {
113-
requestor.fetchAndSaveFromRemote();
113+
try {
114+
requestor.fetchAndSaveFromRemote();
115+
} catch (Exception ex) {
116+
log.error("Encountered Exception while loading configuration", ex);
117+
if (!isGracefulMode) {
118+
throw ex;
119+
}
120+
}
114121
}
115122

116123
protected CompletableFuture<Void> loadConfigurationAsync() {
117-
return requestor.fetchAndSaveFromRemoteAsync();
124+
CompletableFuture<Void> future = new CompletableFuture<>();
125+
126+
requestor
127+
.fetchAndSaveFromRemoteAsync()
128+
.exceptionally(
129+
ex -> {
130+
log.error("Encountered Exception while loading configuration", ex);
131+
if (!isGracefulMode) {
132+
future.completeExceptionally(ex);
133+
}
134+
return null;
135+
})
136+
.thenAccept(future::complete);
137+
138+
return future;
118139
}
119140

120141
protected EppoValue getTypedAssignment(

src/test/java/cloud/eppo/BaseEppoClientTest.java

Lines changed: 103 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,17 @@
22

33
import static cloud.eppo.helpers.AssignmentTestCase.parseTestCaseFile;
44
import static cloud.eppo.helpers.AssignmentTestCase.runTestCase;
5+
import static cloud.eppo.helpers.TestUtils.mockHttpError;
56
import static cloud.eppo.helpers.TestUtils.mockHttpResponse;
67
import static cloud.eppo.helpers.TestUtils.setBaseClientHttpClientOverrideField;
8+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
79
import static org.junit.jupiter.api.Assertions.assertEquals;
810
import static org.junit.jupiter.api.Assertions.assertFalse;
911
import static org.junit.jupiter.api.Assertions.assertThrows;
1012
import static org.junit.jupiter.api.Assertions.assertTrue;
1113
import static org.mockito.Mockito.*;
1214

13-
import cloud.eppo.api.Attributes;
14-
import cloud.eppo.api.Configuration;
15-
import cloud.eppo.api.EppoValue;
16-
import cloud.eppo.api.IAssignmentCache;
15+
import cloud.eppo.api.*;
1716
import cloud.eppo.cache.LRUInMemoryAssignmentCache;
1817
import cloud.eppo.helpers.AssignmentTestCase;
1918
import cloud.eppo.logging.Assignment;
@@ -25,6 +24,7 @@
2524
import java.io.IOException;
2625
import java.util.*;
2726
import java.util.concurrent.CompletableFuture;
27+
import java.util.concurrent.CompletionException;
2828
import java.util.stream.Stream;
2929
import org.apache.commons.io.FileUtils;
3030
import org.junit.jupiter.api.BeforeEach;
@@ -66,7 +66,7 @@ private void initClientWithData(
6666
new BaseEppoClient(
6767
DUMMY_FLAG_API_KEY,
6868
isConfigObfuscated ? "android" : "java",
69-
"3.0.0",
69+
"100.1.0",
7070
TEST_HOST,
7171
mockAssignmentLogger,
7272
null,
@@ -86,7 +86,7 @@ private void initClient(boolean isGracefulMode, boolean isConfigObfuscated) {
8686
new BaseEppoClient(
8787
DUMMY_FLAG_API_KEY,
8888
isConfigObfuscated ? "android" : "java",
89-
"3.0.0",
89+
"100.1.0",
9090
TEST_HOST,
9191
mockAssignmentLogger,
9292
null,
@@ -102,14 +102,37 @@ private void initClient(boolean isGracefulMode, boolean isConfigObfuscated) {
102102
log.info("Test client initialized");
103103
}
104104

105+
private CompletableFuture<Void> initClientAsync(
106+
boolean isGracefulMode, boolean isConfigObfuscated) {
107+
mockAssignmentLogger = mock(AssignmentLogger.class);
108+
109+
eppoClient =
110+
new BaseEppoClient(
111+
DUMMY_FLAG_API_KEY,
112+
isConfigObfuscated ? "android" : "java",
113+
"100.1.0",
114+
TEST_HOST,
115+
mockAssignmentLogger,
116+
null,
117+
null,
118+
isGracefulMode,
119+
isConfigObfuscated,
120+
true,
121+
null,
122+
null,
123+
null);
124+
125+
return eppoClient.loadConfigurationAsync();
126+
}
127+
105128
private void initClientWithAssignmentCache(IAssignmentCache cache) {
106129
mockAssignmentLogger = mock(AssignmentLogger.class);
107130

108131
eppoClient =
109132
new BaseEppoClient(
110133
DUMMY_FLAG_API_KEY,
111134
"java",
112-
"3.0.0",
135+
"100.1.0",
113136
TEST_HOST,
114137
mockAssignmentLogger,
115138
null,
@@ -272,6 +295,78 @@ private CompletableFuture<Configuration> immediateConfigFuture(
272295
Configuration.builder(config.getBytes(), isObfuscated).build());
273296
}
274297

298+
@Test
299+
public void testGracefulInitializationFailure() {
300+
// Set up bad HTTP response
301+
mockHttpError();
302+
303+
// Initialize and no exception should be thrown.
304+
assertDoesNotThrow(() -> initClient(true, false));
305+
}
306+
307+
@Test
308+
public void testClientMakesDefaultAssignmentsAfterFailingToInitialize() {
309+
// Set up bad HTTP response
310+
mockHttpError();
311+
312+
// Initialize and no exception should be thrown.
313+
assertDoesNotThrow(() -> initClient(true, false));
314+
315+
assertEquals("default", eppoClient.getStringAssignment("experiment1", "subject1", "default"));
316+
}
317+
318+
@Test
319+
public void testClientMakesDefaultAssignmentsAfterFailingToInitializeNonGracefulMode() {
320+
// Set up bad HTTP response
321+
mockHttpError();
322+
323+
// Initialize and no exception should be thrown.
324+
try {
325+
initClient(false, false);
326+
} catch (RuntimeException e) {
327+
// Expected
328+
assertEquals("Intentional Error", e.getMessage());
329+
} finally {
330+
assertEquals("default", eppoClient.getStringAssignment("experiment1", "subject1", "default"));
331+
}
332+
}
333+
334+
@Test
335+
public void testNonGracefulInitializationFailure() {
336+
// Set up bad HTTP response
337+
mockHttpError();
338+
339+
// Initialize and assert exception thrown
340+
assertThrows(Exception.class, () -> initClient(false, false));
341+
}
342+
343+
@Test
344+
public void testGracefulAsyncInitializationFailure() {
345+
// Set up bad HTTP response
346+
mockHttpError();
347+
348+
// Initialize
349+
CompletableFuture<Void> init = initClientAsync(true, false);
350+
351+
// Wait for initialization; future should not complete exceptionally (equivalent of exception
352+
// being thrown).
353+
init.join();
354+
assertFalse(init.isCompletedExceptionally());
355+
}
356+
357+
@Test
358+
public void testNonGracefulAsyncInitializationFailure() {
359+
// Set up bad HTTP response
360+
mockHttpError();
361+
362+
// Initialize
363+
CompletableFuture<Void> init = initClientAsync(false, false);
364+
365+
// Exceptions thrown in CompletableFutures are wrapped in a CompletionException.
366+
assertThrows(CompletionException.class, init::join);
367+
assertTrue(init.isCompletedExceptionally());
368+
}
369+
275370
@Test
276371
public void testWithInitialConfiguration() {
277372
try {
@@ -341,7 +436,7 @@ public void testAssignmentEventCorrectlyCreated() {
341436
Map<String, String> expectedMeta = new HashMap<>();
342437
expectedMeta.put("obfuscated", "false");
343438
expectedMeta.put("sdkLanguage", "java");
344-
expectedMeta.put("sdkLibVersion", "3.0.0");
439+
expectedMeta.put("sdkLibVersion", "100.1.0");
345440

346441
assertEquals(expectedMeta, capturedAssignment.getMetaData());
347442
}

src/test/java/cloud/eppo/helpers/TestUtils.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,21 @@ public static void mockHttpResponse(String host, String responseBody) {
2727
setBaseClientHttpClientOverrideField(mockHttpClient);
2828
}
2929

30+
public static void mockHttpError() {
31+
// Create a mock instance of EppoHttpClient
32+
EppoHttpClient mockHttpClient = mock(EppoHttpClient.class);
33+
34+
// Mock sync get
35+
when(mockHttpClient.get(anyString())).thenThrow(new RuntimeException("Intentional Error"));
36+
37+
// Mock async get
38+
CompletableFuture<byte[]> mockAsyncResponse = new CompletableFuture<>();
39+
when(mockHttpClient.getAsync(anyString())).thenReturn(mockAsyncResponse);
40+
mockAsyncResponse.completeExceptionally(new RuntimeException("Intentional Error"));
41+
42+
setBaseClientHttpClientOverrideField(mockHttpClient);
43+
}
44+
3045
public static void setBaseClientHttpClientOverrideField(EppoHttpClient httpClient) {
3146
setBaseClientOverrideField("httpClientOverride", httpClient);
3247
}

0 commit comments

Comments
 (0)