-
Notifications
You must be signed in to change notification settings - Fork 1
chore: v4 - android21 compatible mega PR #112
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 11 commits
3c95eea
933c21d
ac01489
2e55986
d3a792e
49b03ce
119bcb1
cd65a31
75df168
8852198
6fcfffb
6a4ca59
348ccd0
e0d9cd4
f6a9a7d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,84 +1,47 @@ | ||
package cloud.eppo; | ||
|
||
import cloud.eppo.api.Configuration; | ||
import cloud.eppo.api.EppoActionCallback; | ||
import cloud.eppo.callback.CallbackManager; | ||
import java.util.concurrent.CompletableFuture; | ||
import java.util.concurrent.ExecutionException; | ||
import java.util.function.Consumer; | ||
import org.jetbrains.annotations.NotNull; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
public class ConfigurationRequestor { | ||
private static final Logger log = LoggerFactory.getLogger(ConfigurationRequestor.class); | ||
|
||
private final EppoHttpClient client; | ||
private final IEppoHttpClient client; | ||
private final IConfigurationStore configurationStore; | ||
private final boolean expectObfuscatedConfig; | ||
private final boolean supportBandits; | ||
|
||
private CompletableFuture<Void> remoteFetchFuture = null; | ||
private CompletableFuture<Boolean> configurationFuture = null; | ||
private boolean initialConfigSet = false; | ||
|
||
private final CallbackManager<Configuration> configChangeManager = new CallbackManager<>(); | ||
private final CallbackManager<Configuration, Configuration.ConfigurationCallback> | ||
configChangeManager = | ||
new CallbackManager<>( | ||
// no lambdas before java8 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are they android 21 compatible through its "desugaring"? Would that be ok to support or, do we need stay away from that entirely? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The handy thing is that for the dev, if they're using a platform that supports lambdas, they can use a lambda when they register a callback (this is in |
||
new CallbackManager.Dispatcher<Configuration, Configuration.ConfigurationCallback>() { | ||
@Override | ||
public void dispatch( | ||
Configuration.ConfigurationCallback callback, Configuration data) { | ||
callback.accept(data); | ||
} | ||
}); | ||
|
||
public ConfigurationRequestor( | ||
@NotNull IConfigurationStore configurationStore, | ||
@NotNull EppoHttpClient client, | ||
boolean expectObfuscatedConfig, | ||
@NotNull IEppoHttpClient client, | ||
boolean supportBandits) { | ||
this.configurationStore = configurationStore; | ||
this.client = client; | ||
this.expectObfuscatedConfig = expectObfuscatedConfig; | ||
this.supportBandits = supportBandits; | ||
} | ||
|
||
// Synchronously set the initial configuration. | ||
public void setInitialConfiguration(@NotNull Configuration configuration) { | ||
if (initialConfigSet || this.configurationFuture != null) { | ||
throw new IllegalStateException("Initial configuration has already been set"); | ||
} | ||
|
||
initialConfigSet = saveConfigurationAndNotify(configuration).thenApply(v -> true).join(); | ||
} | ||
|
||
/** | ||
* Asynchronously sets the initial configuration. Resolves to `true` if the initial configuration | ||
* was used, false if not (due to being empty, a fetched config taking precedence, etc.) | ||
* Synchronously sets and activates the initial configuration. | ||
* | ||
* @param configuration The configuration to activate | ||
*/ | ||
public CompletableFuture<Boolean> setInitialConfiguration( | ||
@NotNull CompletableFuture<Configuration> configurationFuture) { | ||
if (initialConfigSet || this.configurationFuture != null) { | ||
throw new IllegalStateException("Configuration future has already been set"); | ||
} | ||
this.configurationFuture = | ||
configurationFuture | ||
.thenApply( | ||
(config) -> { | ||
synchronized (configurationStore) { | ||
if (config == null || config.isEmpty()) { | ||
log.debug("Initial configuration future returned empty/null"); | ||
return false; | ||
} else if (remoteFetchFuture != null | ||
&& remoteFetchFuture.isDone() | ||
&& !remoteFetchFuture.isCompletedExceptionally()) { | ||
// Don't clobber a successful fetch. | ||
log.debug("Fetch has completed; ignoring initial config load."); | ||
return false; | ||
} else { | ||
initialConfigSet = | ||
saveConfigurationAndNotify(config).thenApply((s) -> true).join(); | ||
return true; | ||
} | ||
} | ||
}) | ||
.exceptionally( | ||
(e) -> { | ||
log.error("Error setting initial config", e); | ||
return false; | ||
}); | ||
return this.configurationFuture; | ||
public void activateConfiguration(@NotNull Configuration configuration) { | ||
saveConfigurationAndNotify(configuration); | ||
} | ||
|
||
/** Loads configuration synchronously from the API server. */ | ||
|
@@ -90,70 +53,65 @@ void fetchAndSaveFromRemote() { | |
|
||
byte[] flagConfigurationJsonBytes = client.get(Constants.FLAG_CONFIG_ENDPOINT); | ||
Configuration.Builder configBuilder = | ||
Configuration.builder(flagConfigurationJsonBytes, expectObfuscatedConfig) | ||
.banditParametersFromConfig(lastConfig); | ||
Configuration.builder(flagConfigurationJsonBytes).banditParametersFromConfig(lastConfig); | ||
|
||
if (supportBandits && configBuilder.requiresUpdatedBanditModels()) { | ||
byte[] banditParametersJsonBytes = client.get(Constants.BANDIT_ENDPOINT); | ||
configBuilder.banditParameters(banditParametersJsonBytes); | ||
} | ||
|
||
saveConfigurationAndNotify(configBuilder.build()).join(); | ||
saveConfigurationAndNotify(configBuilder.build()); | ||
} | ||
|
||
/** Loads configuration asynchronously from the API server, off-thread. */ | ||
CompletableFuture<Void> fetchAndSaveFromRemoteAsync() { | ||
void fetchAndSaveFromRemoteAsync(EppoActionCallback<Configuration> callback) { | ||
log.debug("Fetching configuration from API server"); | ||
final Configuration lastConfig = configurationStore.getConfiguration(); | ||
|
||
if (remoteFetchFuture != null && !remoteFetchFuture.isDone()) { | ||
log.debug("Remote fetch is active. Cancelling and restarting"); | ||
remoteFetchFuture.cancel(true); | ||
remoteFetchFuture = null; | ||
} | ||
|
||
remoteFetchFuture = | ||
client | ||
.getAsync(Constants.FLAG_CONFIG_ENDPOINT) | ||
.thenCompose( | ||
flagConfigJsonBytes -> { | ||
synchronized (this) { | ||
Configuration.Builder configBuilder = | ||
Configuration.builder(flagConfigJsonBytes, expectObfuscatedConfig) | ||
.banditParametersFromConfig( | ||
lastConfig); // possibly reuse last bandit models loaded. | ||
|
||
if (supportBandits && configBuilder.requiresUpdatedBanditModels()) { | ||
byte[] banditParametersJsonBytes; | ||
try { | ||
banditParametersJsonBytes = | ||
client.getAsync(Constants.BANDIT_ENDPOINT).get(); | ||
} catch (InterruptedException | ExecutionException e) { | ||
log.error("Error fetching from remote: " + e.getMessage()); | ||
throw new RuntimeException(e); | ||
} | ||
if (banditParametersJsonBytes != null) { | ||
configBuilder.banditParameters(banditParametersJsonBytes); | ||
} | ||
} | ||
|
||
return saveConfigurationAndNotify(configBuilder.build()); | ||
} | ||
}); | ||
return remoteFetchFuture; | ||
} | ||
client.getAsync( | ||
Constants.FLAG_CONFIG_ENDPOINT, | ||
new IEppoHttpClient.EppoHttpCallback() { | ||
@Override | ||
public void onSuccess(byte[] flagConfigJsonBytes) { | ||
synchronized (this) { | ||
Configuration.Builder configBuilder = | ||
Configuration.builder(flagConfigJsonBytes) | ||
.banditParametersFromConfig( | ||
lastConfig); // possibly reuse last bandit models loaded. | ||
|
||
if (supportBandits && configBuilder.requiresUpdatedBanditModels()) { | ||
byte[] banditParametersJsonBytes; | ||
|
||
banditParametersJsonBytes = client.get(Constants.BANDIT_ENDPOINT); | ||
|
||
if (banditParametersJsonBytes != null) { | ||
configBuilder.banditParameters(banditParametersJsonBytes); | ||
} | ||
} | ||
|
||
Configuration config = configBuilder.build(); | ||
saveConfigurationAndNotify(config); | ||
callback.onSuccess(config); | ||
} | ||
} | ||
|
||
private CompletableFuture<Void> saveConfigurationAndNotify(Configuration configuration) { | ||
CompletableFuture<Void> saveFuture = configurationStore.saveConfiguration(configuration); | ||
return saveFuture.thenRun( | ||
() -> { | ||
synchronized (configChangeManager) { | ||
configChangeManager.notifyCallbacks(configuration); | ||
@Override | ||
public void onFailure(Throwable error) { | ||
log.error( | ||
"Failed to fetch configuration from API server: {}", error.getMessage(), error); | ||
callback.onFailure(error); | ||
} | ||
}); | ||
} | ||
|
||
public Runnable onConfigurationChange(Consumer<Configuration> callback) { | ||
private void saveConfigurationAndNotify(Configuration configuration) { | ||
configurationStore.saveConfiguration(configuration); | ||
synchronized (configChangeManager) { | ||
configChangeManager.notifyCallbacks(configuration); | ||
} | ||
} | ||
|
||
public Runnable onConfigurationChange(Configuration.ConfigurationCallback callback) { | ||
return configChangeManager.subscribe(callback); | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.