Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
23 changes: 23 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
labels: mergeable
---
_Eppo Internal_
[//]: # (Link to the issue or doc corresponding to this chunk of work)
🎟️ Fixes: #__issue__
📜 Design Doc (if applicable)

## Motivation and Context
[//]: # (Why is this change required? What problem does it solve?)

## Description
[//]: # (Describe your changes in detail)

## How has this been documented?
[//]: # (Please describe how you documented the developer impact of your changes; link to PRs or issues)

## How has this been tested?
[//]: # (Please describe in detail how you tested your changes)


[//]: # (OPTIONAL)
[//]: # (Add one or multiple labels: enhancement, refactoring, bugfix)
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.1-SNAPSHOT'
version = '5.0.0-SNAPSHOT'
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.6.0'
api 'cloud.eppo:sdk-common-jvm:3.8.0'

implementation 'com.github.zafarkhaja:java-semver:0.10.2'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.18.2'
Expand Down
54 changes: 23 additions & 31 deletions src/main/java/cloud/eppo/EppoClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
import cloud.eppo.cache.LRUInMemoryAssignmentCache;
import cloud.eppo.logging.AssignmentLogger;
import cloud.eppo.logging.BanditLogger;
import java.util.Timer;
import java.util.concurrent.TimeUnit;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -26,7 +26,6 @@ public class EppoClient extends BaseEppoClient {
private static final long DEFAULT_JITTER_INTERVAL_RATIO = 10;

private static EppoClient instance;
private static Timer pollTimer;

public static EppoClient getInstance() {
if (instance == null) {
Expand All @@ -36,7 +35,7 @@ public static EppoClient getInstance() {
}

private EppoClient(
String apiKey,
String sdkKey,
String sdkName,
String sdkVersion,
@Nullable String baseUrl,
Expand All @@ -46,7 +45,7 @@ private EppoClient(
@Nullable IAssignmentCache assignmentCache,
@Nullable IAssignmentCache banditAssignmentCache) {
super(
apiKey,
sdkKey,
sdkName,
sdkVersion,
null,
Expand All @@ -62,16 +61,18 @@ private EppoClient(
banditAssignmentCache);
}

/** Stops the client from polling Eppo for updated flag and bandit configurations */
public static void stopPolling() {
if (pollTimer != null) {
pollTimer.cancel();
}
/**
* Creates a new EppoClient Builder object with the specified SDK Key.
*
* @param sdkKey (see <a href="https://docs.geteppo.com/sdks/sdk-keys/">SDK Keys</a>)
*/
public static Builder builder(@NotNull String sdkKey) {
return new Builder(sdkKey);
}

/** Builder pattern to initialize the EppoClient singleton */
public static class Builder {
private String apiKey;
private final String sdkKey;
private AssignmentLogger assignmentLogger;
private BanditLogger banditLogger;
private boolean isGracefulMode = DEFAULT_IS_GRACEFUL_MODE;
Expand All @@ -85,10 +86,8 @@ public static class Builder {
private IAssignmentCache banditAssignmentCache =
new ExpiringInMemoryAssignmentCache(10, TimeUnit.MINUTES);

/** Sets the API Key--created within the eppo application--to use. This is required. */
public Builder apiKey(String apiKey) {
this.apiKey = apiKey;
return this;
private Builder(@NotNull String sdkKey) {
this.sdkKey = sdkKey;
}

/**
Expand Down Expand Up @@ -163,6 +162,8 @@ public EppoClient buildAndInit() {
String sdkVersion = appDetails.getVersion();

if (instance != null) {
// Stop any active polling.
instance.stopPolling();
if (forceReinitialize) {
log.warn(
"Eppo SDK is already initialized, reinitializing since forceReinitialize is true");
Expand All @@ -175,7 +176,7 @@ public EppoClient buildAndInit() {

instance =
new EppoClient(
apiKey,
sdkKey,
sdkName,
sdkVersion,
apiBaseUrl,
Expand All @@ -185,22 +186,13 @@ public EppoClient buildAndInit() {
assignmentCache,
banditAssignmentCache);

// Stop any active polling
stopPolling();

// Set up polling for experiment configurations
pollTimer = new Timer(true);
FetchConfigurationsTask fetchConfigurationsTask =
new FetchConfigurationsTask(
() -> instance.loadConfiguration(),
pollTimer,
pollingIntervalMs,
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();
// Fetch first configuration
instance.loadConfiguration();

// start polling, if enabled.
if (pollingIntervalMs > 0) {
instance.startPolling(pollingIntervalMs, pollingIntervalMs / DEFAULT_JITTER_INTERVAL_RATIO);
}

return instance;
}
Expand Down
32 changes: 22 additions & 10 deletions src/test/java/cloud/eppo/EppoClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@
import cloud.eppo.api.Attributes;
import cloud.eppo.api.BanditActions;
import cloud.eppo.api.BanditResult;
import cloud.eppo.api.Configuration;
import cloud.eppo.helpers.AssignmentTestCase;
import cloud.eppo.helpers.BanditTestCase;
import cloud.eppo.helpers.TestUtils;
import cloud.eppo.logging.Assignment;
import cloud.eppo.logging.AssignmentLogger;
import cloud.eppo.logging.BanditAssignment;
import cloud.eppo.logging.BanditLogger;
import cloud.eppo.ufc.dto.VariationType;
import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
Expand Down Expand Up @@ -91,7 +93,11 @@ private static String readConfig(String jsonToReturnFilePath) {
@AfterEach
public void cleanUp() {
TestUtils.setBaseClientHttpClientOverrideField(null);
EppoClient.stopPolling();
try {
EppoClient.getInstance().stopPolling();
} catch (IllegalStateException ex) {
// pass: Indicates that the singleton Eppo Client has not yet been initialized.
}
}

@AfterAll
Expand Down Expand Up @@ -197,7 +203,7 @@ public void testErrorGracefulModeOff() {
@Test
public void testReinitializeWithoutForcing() {
EppoClient firstInstance = initClient(DUMMY_FLAG_API_KEY);
EppoClient secondInstance = new EppoClient.Builder().apiKey(DUMMY_FLAG_API_KEY).buildAndInit();
EppoClient secondInstance = EppoClient.builder(DUMMY_FLAG_API_KEY).buildAndInit();

assertSame(firstInstance, secondInstance);
}
Expand All @@ -206,7 +212,7 @@ public void testReinitializeWithoutForcing() {
public void testReinitializeWitForcing() {
EppoClient firstInstance = initClient(DUMMY_FLAG_API_KEY);
EppoClient secondInstance =
new EppoClient.Builder().apiKey(DUMMY_FLAG_API_KEY).forceReinitialize(true).buildAndInit();
EppoClient.builder(DUMMY_FLAG_API_KEY).forceReinitialize(true).buildAndInit();

assertNotSame(firstInstance, secondInstance);
}
Expand All @@ -217,8 +223,7 @@ public void testPolling() {
EppoHttpClient httpClientSpy = spy(httpClient);
TestUtils.setBaseClientHttpClientOverrideField(httpClientSpy);

new EppoClient.Builder()
.apiKey(DUMMY_FLAG_API_KEY)
EppoClient.builder(DUMMY_FLAG_API_KEY)
.pollingIntervalMs(20)
.forceReinitialize(true)
.buildAndInit();
Expand All @@ -232,7 +237,7 @@ public void testPolling() {
// Now, the method should have been called twice
verify(httpClientSpy, times(2)).get(anyString());

EppoClient.stopPolling();
EppoClient.getInstance().stopPolling();
sleepUninterruptedly(25);

// No more calls since stopped
Expand All @@ -257,6 +262,15 @@ public void testClientMakesDefaultAssignmentsAfterFailingToInitialize() {
}
}

@Test
public void testGetConfiguration() {
EppoClient eppoClient = initClient(DUMMY_FLAG_API_KEY);
Configuration configuration = eppoClient.getConfiguration();
assertNotNull(configuration);
assertNotNull(configuration.getFlag("numeric_flag"));
assertEquals(VariationType.NUMERIC, configuration.getFlagType("numeric_flag"));
}

public static void mockHttpError() {
// Create a mock instance of EppoHttpClient
EppoHttpClient mockHttpClient = mock(EppoHttpClient.class);
Expand Down Expand Up @@ -285,8 +299,7 @@ private EppoClient initClient(String apiKey) {
mockAssignmentLogger = mock(AssignmentLogger.class);
mockBanditLogger = mock(BanditLogger.class);

return new EppoClient.Builder()
.apiKey(apiKey)
return EppoClient.builder(apiKey)
.apiBaseUrl(Constants.appendApiPathToHost(TEST_HOST))
.assignmentLogger(mockAssignmentLogger)
.banditLogger(mockBanditLogger)
Expand All @@ -299,8 +312,7 @@ private EppoClient initFailingGracefulClient(boolean isGracefulMode) {
mockAssignmentLogger = mock(AssignmentLogger.class);
mockBanditLogger = mock(BanditLogger.class);

return new EppoClient.Builder()
.apiKey(DUMMY_FLAG_API_KEY)
return EppoClient.builder(DUMMY_FLAG_API_KEY)
.apiBaseUrl("blag")
.assignmentLogger(mockAssignmentLogger)
.banditLogger(mockBanditLogger)
Expand Down