Skip to content

Commit

Permalink
fix: respect graceful mode in Android SDK (#133)
Browse files Browse the repository at this point in the history
* bump to latest common
* bump version
* respect graceful mode in android client
  • Loading branch information
typotter authored Nov 18, 2024
1 parent 5515526 commit bdb5f11
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 8 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ feature flagging and experimentation for Eppo customers. An API key is required

```groovy
dependencies {
implementation 'cloud.eppo:android-sdk:4.3.1'
implementation 'cloud.eppo:android-sdk:4.3.2'
}
dependencyResolutionManagement {
Expand Down
4 changes: 2 additions & 2 deletions eppo/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ plugins {
}

group = "cloud.eppo"
version = "4.3.2-SNAPSHOT"
version = "4.3.2"

android {
buildFeatures.buildConfig true
Expand Down Expand Up @@ -68,7 +68,7 @@ ext.versions = [
]

dependencies {
api 'cloud.eppo:sdk-common-jvm:3.5.0'
api 'cloud.eppo:sdk-common-jvm:3.5.3'

implementation 'org.slf4j:slf4j-api:2.0.16'

Expand Down
87 changes: 87 additions & 0 deletions eppo/src/androidTest/java/cloud/eppo/android/EppoClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
Expand Down Expand Up @@ -258,6 +259,92 @@ public void testErrorGracefulModeOff() {
"subject1", "experiment1", new Attributes(), mapper.readTree("{}")));
}

private static EppoHttpClient 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"));

return mockHttpClient;
}

@Test
public void testGracefulInitializationFailure() throws ExecutionException, InterruptedException {
// Set up bad HTTP response
EppoHttpClient http = mockHttpError();
setBaseClientHttpClientOverrideField(http);

EppoClient.Builder clientBuilder =
new EppoClient.Builder(DUMMY_API_KEY, ApplicationProvider.getApplicationContext())
.forceReinitialize(true)
.isGracefulMode(true);

// Initialize and no exception should be thrown.
clientBuilder.buildAndInitAsync().get();
}

@Test
public void testClientMakesDefaultAssignmentsAfterFailingToInitialize()
throws ExecutionException, InterruptedException {
// Set up bad HTTP response
setBaseClientHttpClientOverrideField(mockHttpError());

EppoClient.Builder clientBuilder =
new EppoClient.Builder(DUMMY_API_KEY, ApplicationProvider.getApplicationContext())
.forceReinitialize(true)
.isGracefulMode(true);

// Initialize and no exception should be thrown.
EppoClient eppoClient = clientBuilder.buildAndInitAsync().get();

assertEquals("default", eppoClient.getStringAssignment("experiment1", "subject1", "default"));
}

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

EppoClient.Builder clientBuilder =
new EppoClient.Builder(DUMMY_API_KEY, ApplicationProvider.getApplicationContext())
.forceReinitialize(true)
.isGracefulMode(false);

// Initialize, expect the exception and then verify that the client can still complete an
// assignment.
try {
clientBuilder.buildAndInitAsync().get();
fail("Expected exception");
} catch (RuntimeException | ExecutionException | InterruptedException e) {
// Expected
assertNotNull(e.getCause());

assertEquals(
"default",
EppoClient.getInstance().getStringAssignment("experiment1", "subject1", "default"));
}
}

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

EppoClient.Builder clientBuilder =
new EppoClient.Builder(DUMMY_API_KEY, ApplicationProvider.getApplicationContext())
.forceReinitialize(true)
.isGracefulMode(false);

// Initialize and expect an exception.
assertThrows(Exception.class, () -> clientBuilder.buildAndInitAsync().get());
}

private void runTestCases() {
try {
int testsRan = 0;
Expand Down
32 changes: 27 additions & 5 deletions eppo/src/main/java/cloud/eppo/android/EppoClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import cloud.eppo.logging.AssignmentLogger;
import cloud.eppo.ufc.dto.VariationType;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicInteger;

Expand Down Expand Up @@ -250,7 +251,7 @@ public CompletableFuture<EppoClient> buildAndInitAsync() {
} else if (failCount.incrementAndGet() == 2
|| instance.getInitialConfigFuture() == null) {
ret.completeExceptionally(
new RuntimeException(
new EppoInitializationException(
"Unable to initialize client; Configuration could not be loaded", ex));
}
return null;
Expand All @@ -266,7 +267,7 @@ public CompletableFuture<EppoClient> buildAndInitAsync() {
ret.complete(instance);
} else if (offlineMode || failCount.incrementAndGet() == 2) {
ret.completeExceptionally(
new RuntimeException(
new EppoInitializationException(
"Unable to initialize client; Configuration could not be loaded", ex));
} else {
Log.d(TAG, "Initial config was not used.");
Expand All @@ -275,16 +276,37 @@ public CompletableFuture<EppoClient> buildAndInitAsync() {
return null;
});
}
return ret;
return ret.exceptionally(
e -> {
Log.e(TAG, "Exception caught during initialization: " + e.getMessage(), e);
if (!isGracefulMode) {
throw new RuntimeException(e);
}
return instance;
});
}

/** Builds and initializes an `EppoClient`, immediately available to compute assignments. */
public EppoClient buildAndInit() {
try {
return buildAndInitAsync().get();
} catch (ExecutionException | InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException | InterruptedException | CompletionException e) {
// If the exception was an `EppoInitializationException`, we know for sure that
// `buildAndInitAsync` logged it (and wrapped it with a RuntimeException) which was then
// wrapped by `CompletableFuture` with a `CompletionException`.
if (e instanceof CompletionException) {
Throwable cause = e.getCause();
if (cause instanceof RuntimeException
&& cause.getCause() instanceof EppoInitializationException) {
return instance;
}
}
Log.e(TAG, "Exception caught during initialization: " + e.getMessage(), e);
if (!isGracefulMode) {
throw new RuntimeException(e);
}
}
return instance;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package cloud.eppo.android;

public class EppoInitializationException extends Exception {
public EppoInitializationException(String s, Throwable ex) {
super(s, ex);
}
}

0 comments on commit bdb5f11

Please sign in to comment.