cloud.eppo.api.Configuration Maven / Gradle / Ivy
Show all versions of sdk-common-jvm Show documentation
package cloud.eppo.api;
import static cloud.eppo.Utils.getMD5Hex;
import cloud.eppo.ufc.dto.*;
import cloud.eppo.ufc.dto.adapters.EppoModule;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.io.*;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Encapsulates the Flag Configuration and Bandit parameters in an immutable object with a complete
* and coherent state.
*
* A Builder is used to prepare and then create am immutable data structure containing both flag
* and bandit configurations. An intermediate step is required in building the configuration to
* accommodate the as-needed loading of bandit parameters as a network call may not be needed if
* there are no bandits referenced by the flag configuration.
*
*
Usage: Building with just flag configuration (unobfuscated is default)
* Configuration config = new Configuration.Builder(flagConfigJsonString).build();
*
*
*
Building with bandits (known configuration)
* Configuration config = new Configuration.Builder(flagConfigJsonString).banditParameters(banditConfigJson).build();
*
*
*
Conditionally loading bandit models (with or without an existing bandit config JSON string).
*
* Configuration.Builder configBuilder = new Configuration.Builder(flagConfigJsonString).banditParameters(banditConfigJson);
* if (configBuilder.requiresBanditModels()) {
* // Load the bandit parameters encoded in a JSON string
* configBuilder.banditParameters(banditParameterJsonString);
* }
* Configuration config = configBuilder.build();
*
*
*
*
*
Hint: when loading new Flag configuration values, set the current bandit models in the builder
* then check `requiresBanditModels()`.
*/
public class Configuration {
private static final ObjectMapper mapper =
new ObjectMapper().registerModule(EppoModule.eppoModule());
private static final byte[] emptyFlagsBytes =
"{ \"flags\": {}, \"format\": \"SERVER\" }".getBytes();
private static final Logger log = LoggerFactory.getLogger(Configuration.class);
private final Map banditReferences;
private final Map flags;
private final Map bandits;
private final boolean isConfigObfuscated;
@SuppressWarnings("unused")
private final byte[] flagConfigJson;
private final byte[] banditParamsJson;
private Configuration(
Map flags,
Map banditReferences,
Map bandits,
boolean isConfigObfuscated,
byte[] flagConfigJson,
byte[] banditParamsJson) {
this.flags = flags;
this.banditReferences = banditReferences;
this.bandits = bandits;
this.isConfigObfuscated = isConfigObfuscated;
// Graft the `forServer` boolean into the flagConfigJson'
if (flagConfigJson != null && flagConfigJson.length != 0) {
try {
JsonNode jNode = mapper.readTree(flagConfigJson);
FlagConfigResponse.Format format =
isConfigObfuscated
? FlagConfigResponse.Format.CLIENT
: FlagConfigResponse.Format.SERVER;
((ObjectNode) jNode).put("format", format.toString());
flagConfigJson = mapper.writeValueAsBytes(jNode);
} catch (IOException e) {
log.error("Error adding `format` field to FlagConfigResponse JSON");
}
}
this.flagConfigJson = flagConfigJson;
this.banditParamsJson = banditParamsJson;
}
public static Configuration emptyConfig() {
return new Configuration(
Collections.emptyMap(),
Collections.emptyMap(),
Collections.emptyMap(),
false,
emptyFlagsBytes,
null);
}
public FlagConfig getFlag(String flagKey) {
String flagKeyForLookup = flagKey;
if (isConfigObfuscated()) {
flagKeyForLookup = getMD5Hex(flagKey);
}
if (flags == null) {
log.warn("Request for flag {} before flags have been loaded", flagKey);
return null;
} else if (flags.isEmpty()) {
log.warn("Request for flag {} with empty flags", flagKey);
}
return flags.get(flagKeyForLookup);
}
public String banditKeyForVariation(String flagKey, String variationValue) {
// Note: In practice this double loop should be quite quick as the number of bandits and bandit
// variations will be small. Should this ever change, we can optimize things.
for (Map.Entry banditEntry : banditReferences.entrySet()) {
BanditReference banditReference = banditEntry.getValue();
for (BanditFlagVariation banditFlagVariation : banditReference.getFlagVariations()) {
if (banditFlagVariation.getFlagKey().equals(flagKey)
&& banditFlagVariation.getVariationValue().equals(variationValue)) {
return banditEntry.getKey();
}
}
}
return null;
}
public BanditParameters getBanditParameters(String banditKey) {
return bandits.get(banditKey);
}
public boolean isConfigObfuscated() {
return isConfigObfuscated;
}
public byte[] serializeFlagConfigToBytes() {
return flagConfigJson;
}
public byte[] serializeBanditParamsToBytes() {
return banditParamsJson;
}
public boolean isEmpty() {
return flags == null || flags.isEmpty();
}
public static Builder builder(byte[] flagJson, boolean isConfigObfuscated) {
return new Builder(flagJson, isConfigObfuscated);
}
/**
* Builder to create the immutable config object.
*
* @see Configuration for usage.
*/
public static class Builder {
private final boolean isConfigObfuscated;
private final Map flags;
private final Map banditReferences;
private Map bandits = Collections.emptyMap();
private final byte[] flagJson;
private byte[] banditParamsJson;
private static FlagConfigResponse parseFlagResponse(byte[] flagJson) {
if (flagJson == null || flagJson.length == 0) {
log.warn("Null or empty configuration string. Call `Configuration.Empty()` instead");
return null;
}
FlagConfigResponse config;
try {
return mapper.readValue(flagJson, FlagConfigResponse.class);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public Builder(String flagJson, boolean isConfigObfuscated) {
this(flagJson.getBytes(), parseFlagResponse(flagJson.getBytes()), isConfigObfuscated);
}
public Builder(byte[] flagJson, boolean isConfigObfuscated) {
this(flagJson, parseFlagResponse(flagJson), isConfigObfuscated);
}
public Builder(byte[] flagJson, FlagConfigResponse flagConfigResponse) {
this(
flagJson,
flagConfigResponse,
flagConfigResponse.getFormat() == FlagConfigResponse.Format.CLIENT);
}
/** Use this constructor when the FlagConfigResponse has the `forServer` field populated. */
public Builder(byte[] flagJson) {
this(flagJson, parseFlagResponse(flagJson));
}
public Builder(
byte[] flagJson,
@Nullable FlagConfigResponse flagConfigResponse,
boolean isConfigObfuscated) {
this.isConfigObfuscated = isConfigObfuscated;
this.flagJson = flagJson;
if (flagConfigResponse == null
|| flagConfigResponse.getFlags() == null
|| flagConfigResponse.getFlags().isEmpty()) {
log.warn("'flags' map missing in flag definition JSON");
flags = Collections.emptyMap();
banditReferences = Collections.emptyMap();
} else {
flags = Collections.unmodifiableMap(flagConfigResponse.getFlags());
banditReferences = Collections.unmodifiableMap(flagConfigResponse.getBanditReferences());
log.debug("Loaded {} flag definitions from flag definition JSON", flags.size());
}
}
public boolean requiresUpdatedBanditModels() {
Set neededModelVersions = referencedBanditModelVersion();
return !loadedBanditModelVersions().containsAll(neededModelVersions);
}
public Set loadedBanditModelVersions() {
return bandits.values().stream()
.map(BanditParameters::getModelVersion)
.collect(Collectors.toSet());
}
public Set referencedBanditModelVersion() {
return banditReferences.values().stream()
.map(BanditReference::getModelVersion)
.collect(Collectors.toSet());
}
public Builder banditParametersFromConfig(Configuration currentConfig) {
if (currentConfig == null || currentConfig.bandits == null) {
bandits = Collections.emptyMap();
} else {
bandits = currentConfig.bandits;
banditParamsJson = currentConfig.banditParamsJson;
}
return this;
}
public Builder banditParameters(String banditParameterJson) {
return banditParameters(banditParameterJson.getBytes());
}
public Builder banditParameters(byte[] banditParameterJson) {
if (banditParameterJson == null || banditParameterJson.length == 0) {
log.debug("Bandit parameters are null or empty");
return this;
}
BanditParametersResponse config;
try {
config = mapper.readValue(banditParameterJson, BanditParametersResponse.class);
} catch (IOException e) {
log.error("Unable to parse bandit parameters JSON");
throw new RuntimeException(e);
}
if (config == null || config.getBandits() == null) {
log.warn("`bandits` map missing in bandit parameters JSON");
bandits = Collections.emptyMap();
} else {
bandits = Collections.unmodifiableMap(config.getBandits());
log.debug("Loaded {} bandit models from bandit parameters JSON", bandits.size());
}
return this;
}
public Configuration build() {
return new Configuration(
flags, banditReferences, bandits, isConfigObfuscated, flagJson, banditParamsJson);
}
}
}