All Downloads are FREE. Search and download functionalities are using the official Maven repository.
Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
io.featurehub.client.ClientFeatureRepository Maven / Gradle / Ivy
package io.featurehub.client;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import io.featurehub.sse.model.FeatureRolloutStrategy;
import io.featurehub.sse.model.SSEResultState;
import io.featurehub.strategies.matchers.MatcherRegistry;
import io.featurehub.strategies.percentage.PercentageMumurCalculator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
public class ClientFeatureRepository extends AbstractFeatureRepository
implements FeatureRepositoryContext {
private static final Logger log = LoggerFactory.getLogger(ClientFeatureRepository.class);
// feature-key, feature-state
private final Map features = new ConcurrentHashMap<>();
private final ExecutorService executor;
private final ObjectMapper mapper;
private boolean hasReceivedInitialState = false;
private final List analyticsCollectors = new ArrayList<>();
private Readyness readyness = Readyness.NotReady;
private final List readynessListeners = new ArrayList<>();
private final List featureValueInterceptors = new ArrayList<>();
private ObjectMapper jsonConfigObjectMapper;
private final ApplyFeature applyFeature;
private boolean serverEvaluation = false; // the client tells us, we pass it out to others
private final TypeReference> FEATURE_LIST_TYPEDEF =
new TypeReference>() {};
public ClientFeatureRepository(ExecutorService executor, ApplyFeature applyFeature) {
mapper = initializeMapper();
jsonConfigObjectMapper = mapper;
this.executor = executor;
this.applyFeature =
applyFeature == null
? new ApplyFeature(new PercentageMumurCalculator(), new MatcherRegistry())
: applyFeature;
}
public ClientFeatureRepository(int threadPoolSize) {
this(getExecutor(threadPoolSize), null);
}
public ClientFeatureRepository() {
this(1);
}
public ClientFeatureRepository(ExecutorService executor) {
this(executor == null ? getExecutor(1) : executor, null);
}
protected ObjectMapper initializeMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
mapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
return mapper;
}
protected static ExecutorService getExecutor(int threadPoolSize) {
return Executors.newFixedThreadPool(threadPoolSize);
}
public void setJsonConfigObjectMapper(ObjectMapper jsonConfigObjectMapper) {
this.jsonConfigObjectMapper = jsonConfigObjectMapper;
}
@Override
public boolean exists(String key) {
return features.containsKey(key);
}
@Override
public boolean isServerEvaluation() {
return serverEvaluation;
}
public Readyness getReadyness() {
return readyness;
}
@Override
public FeatureRepository addAnalyticCollector(AnalyticsCollector collector) {
analyticsCollectors.add(collector);
return this;
}
@Override
public FeatureRepository registerValueInterceptor(
boolean allowFeatureOverride, FeatureValueInterceptor interceptor) {
featureValueInterceptors.add(
new FeatureValueInterceptorHolder(allowFeatureOverride, interceptor));
return this;
}
@Override
public void notify(SSEResultState state, String data) {
log.trace("received state {} data {}", state, data);
if (state == null) {
log.warn("Unexpected null state");
} else {
try {
switch (state) {
case ACK:
case BYE:
break;
case DELETE_FEATURE:
deleteFeature(mapper.readValue(data, io.featurehub.sse.model.FeatureState.class));
break;
case FEATURE:
featureUpdate(mapper.readValue(data, io.featurehub.sse.model.FeatureState.class));
break;
case FEATURES:
List features =
mapper.readValue(data, FEATURE_LIST_TYPEDEF);
notify(features);
break;
case FAILURE:
readyness = Readyness.Failed;
broadcastReadyness();
break;
}
} catch (Exception e) {
log.error("Unable to process data `{}` for state `{}`", data, state, e);
}
}
}
@Override
public void notify(List states, boolean force) {
states.forEach(s -> featureUpdate(s, force));
if (!hasReceivedInitialState) {
checkForInvalidFeatures();
hasReceivedInitialState = true;
readyness = Readyness.Ready;
broadcastReadyness();
} else if (readyness != Readyness.Ready) {
readyness = Readyness.Ready;
broadcastReadyness();
}
}
@Override
public List getFeatureValueInterceptors() {
return featureValueInterceptors;
}
@Override
public Applied applyFeature(
List strategies, String key, String featureValueId, ClientContext cac) {
return applyFeature.applyFeature(strategies, key, featureValueId, cac);
}
@Override
public void execute(Runnable command) {
executor.execute(command);
}
@Override
public ObjectMapper getJsonObjectMapper() {
return jsonConfigObjectMapper;
}
@Override
public void setServerEvaluation(boolean val) {
this.serverEvaluation = val;
}
@Override
public void notReady() {
readyness = Readyness.NotReady;
broadcastReadyness();
}
@Override
public void close() {
log.info("featurehub repository closing");
features.clear();
readyness = Readyness.NotReady;
readynessListeners.stream().forEach(rl -> rl.notify(readyness));
executor.shutdownNow();
log.info("featurehub repository closed");
}
@Override
public void notify(List states) {
notify(states, false);
}
@Override
public FeatureRepository addReadynessListener(ReadynessListener rl) {
this.readynessListeners.add(rl);
if (!executor.isShutdown()) {
// let it know what the current state is
executor.execute(() -> rl.notify(readyness));
}
return this;
}
private void broadcastReadyness() {
if (!executor.isShutdown()) {
readynessListeners.forEach((rl) -> executor.execute(() -> rl.notify(readyness)));
}
}
private void deleteFeature(io.featurehub.sse.model.FeatureState readValue) {
readValue.setValue(null);
featureUpdate(readValue);
}
private void checkForInvalidFeatures() {
String invalidKeys =
features.values().stream()
.filter(v -> v.getKey() == null)
.map(FeatureState::getKey)
.collect(Collectors.joining(", "));
if (invalidKeys.length() > 0) {
log.error("FeatureHub error: application is requesting use of invalid keys: {}", invalidKeys);
}
}
@Override
public FeatureState getFeatureState(String key) {
return features.computeIfAbsent(
key,
key1 -> {
if (hasReceivedInitialState) {
log.error(
"FeatureHub error: application requesting use of invalid key after initialization: `{}`",
key1);
}
return new FeatureStateBase(null, this, key);
});
}
@Override
public List getAllFeatures() {
return new ArrayList<>(features.values());
}
@Override
public FeatureRepository logAnalyticsEvent(
String action, Map other, ClientContext ctx) {
// take a snapshot of the current state of the features
List featureStateAtCurrentTime =
features.values().stream()
.map(f -> ctx == null ? f : f.withContext(ctx))
.filter(FeatureState::isSet)
.map(f -> ((FeatureStateBase)f).analyticsCopy())
.collect(Collectors.toList());
executor.execute(
() ->
analyticsCollectors.forEach(
(c) -> c.logEvent(action, other, featureStateAtCurrentTime)));
return this;
}
private void featureUpdate(io.featurehub.sse.model.FeatureState featureState) {
featureUpdate(featureState, false);
}
private void featureUpdate(io.featurehub.sse.model.FeatureState featureState, boolean force) {
FeatureStateBase holder = features.get(featureState.getKey());
if (holder == null || holder.getKey() == null) {
holder = new FeatureStateBase(holder, this, featureState.getKey());
features.put(featureState.getKey(), holder);
} else if (!force && holder._featureState != null) {
if (holder._featureState.getVersion() > featureState.getVersion()
|| (holder._featureState.getVersion().equals(featureState.getVersion())
&& !FeatureStateUtils.changed(
holder._featureState.getValue(), featureState.getValue()))) {
// if the old version is newer, or they are the same version and the value hasn't changed.
// it can change with server side evaluation based on user data
return;
}
}
holder.setFeatureState(featureState);
}
}