diff --git a/pom.xml b/pom.xml index e3755ef01..8ae60f540 100644 --- a/pom.xml +++ b/pom.xml @@ -56,6 +56,12 @@ + + io.getunleash + yggdrasil-engine + 0.1.0-alpha.9 + x86_64-linux + com.google.code.gson gson diff --git a/src/main/java/io/getunleash/DefaultUnleash.java b/src/main/java/io/getunleash/DefaultUnleash.java index 9abcdb9ba..220ddabbe 100644 --- a/src/main/java/io/getunleash/DefaultUnleash.java +++ b/src/main/java/io/getunleash/DefaultUnleash.java @@ -3,22 +3,24 @@ import static io.getunleash.Variant.DISABLED_VARIANT; import static java.util.Optional.ofNullable; -import io.getunleash.event.*; +import io.getunleash.engine.*; +import io.getunleash.event.EventDispatcher; +import io.getunleash.event.IsEnabledImpressionEvent; +import io.getunleash.event.ToggleEvaluated; +import io.getunleash.event.VariantImpressionEvent; import io.getunleash.lang.Nullable; import io.getunleash.metric.UnleashMetricService; import io.getunleash.metric.UnleashMetricServiceImpl; import io.getunleash.repository.FeatureRepository; import io.getunleash.repository.IFeatureRepository; +import io.getunleash.repository.JsonFeatureParser; import io.getunleash.strategy.*; -import io.getunleash.util.ConstraintMerger; import io.getunleash.util.UnleashConfig; -import io.getunleash.variant.VariantUtil; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.LongAdder; import java.util.function.BiPredicate; import java.util.stream.Collectors; -import javax.annotation.Nonnull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -39,9 +41,9 @@ public class DefaultUnleash implements Unleash { public static final UnknownStrategy UNKNOWN_STRATEGY = new UnknownStrategy(); + private final UnleashEngine unleashEngine; private final UnleashMetricService metricService; private final IFeatureRepository featureRepository; - private final Map strategyMap; private final UnleashContextProvider contextProvider; private final EventDispatcher eventDispatcher; private final UnleashConfig config; @@ -64,7 +66,6 @@ public DefaultUnleash( buildStrategyMap(strategies), unleashConfig.getContextProvider(), new EventDispatcher(unleashConfig), - new UnleashMetricServiceImpl(unleashConfig, unleashConfig.getScheduledExecutor()), false); } @@ -74,15 +75,13 @@ public DefaultUnleash( IFeatureRepository featureRepository, Map strategyMap, UnleashContextProvider contextProvider, - EventDispatcher eventDispatcher, - UnleashMetricService metricService) { + EventDispatcher eventDispatcher) { this( unleashConfig, featureRepository, strategyMap, contextProvider, eventDispatcher, - metricService, false); } @@ -92,14 +91,33 @@ public DefaultUnleash( Map strategyMap, UnleashContextProvider contextProvider, EventDispatcher eventDispatcher, - UnleashMetricService metricService, boolean failOnMultipleInstantiations) { + + this.unleashEngine = + new UnleashEngine( + strategyMap.values().stream() + .map(YggdrasilAdapters::adapt) + .collect(Collectors.toList()), + Optional.ofNullable(unleashConfig.getFallbackStrategy()) + .map(YggdrasilAdapters::adapt) + .orElse(null)); + featureRepository.addConsumer( + featureCollection -> { + try { + this.unleashEngine.takeState( + JsonFeatureParser.toJsonString(featureCollection)); + } catch (YggdrasilInvalidInputException e) { + LOGGER.error("Unable to update features", e); + } + }); + this.config = unleashConfig; this.featureRepository = featureRepository; - this.strategyMap = strategyMap; this.contextProvider = contextProvider; this.eventDispatcher = eventDispatcher; - this.metricService = metricService; + this.metricService = + new UnleashMetricServiceImpl( + unleashConfig, unleashConfig.getScheduledExecutor(), this.unleashEngine); metricService.register(strategyMap.keySet()); initCounts.compute( config.getClientIdentifier(), @@ -139,204 +157,28 @@ public boolean isEnabled( String toggleName, UnleashContext context, BiPredicate fallbackAction) { - return isEnabled(toggleName, context, fallbackAction, false); - } - - public boolean isEnabled( - String toggleName, - UnleashContext context, - BiPredicate fallbackAction, - boolean isParent) { - FeatureEvaluationResult result = - getFeatureEvaluationResult(toggleName, context, fallbackAction, null); - if (!isParent) { - count(toggleName, result.isEnabled()); - } - eventDispatcher.dispatch(new ToggleEvaluated(toggleName, result.isEnabled())); - dispatchEnabledImpressionDataIfNeeded("isEnabled", toggleName, result.isEnabled(), context); - return result.isEnabled(); - } - - private void dispatchEnabledImpressionDataIfNeeded( - String eventType, String toggleName, boolean enabled, UnleashContext context) { - FeatureToggle toggle = featureRepository.getToggle(toggleName); - if (toggle != null && toggle.hasImpressionData()) { - eventDispatcher.dispatch(new IsEnabledImpressionEvent(toggleName, enabled, context)); - } - } - - private FeatureEvaluationResult getFeatureEvaluationResult( - String toggleName, - UnleashContext context, - BiPredicate fallbackAction, - @Nullable Variant defaultVariant) { - checkIfToggleMatchesNamePrefix(toggleName); - FeatureToggle featureToggle = featureRepository.getToggle(toggleName); - - UnleashContext enhancedContext = context.applyStaticFields(config); - if (featureToggle == null) { - return new FeatureEvaluationResult( - fallbackAction.test(toggleName, enhancedContext), defaultVariant); - } else if (!featureToggle.isEnabled()) { - return new FeatureEvaluationResult(false, defaultVariant); - } else if (isParentDependencySatisfied(featureToggle, context, fallbackAction)) { - // Dependent toggles, no point in evaluating child strategies if our dependencies are - // not satisfied - if (featureToggle.getStrategies().isEmpty()) { - return new FeatureEvaluationResult( - true, VariantUtil.selectVariant(featureToggle, context, defaultVariant)); - } - for (ActivationStrategy strategy : featureToggle.getStrategies()) { - Strategy configuredStrategy = getStrategy(strategy.getName()); - if (configuredStrategy == UNKNOWN_STRATEGY) { - LOGGER.warn( - "Unable to find matching strategy for toggle:{} strategy:{}", - toggleName, - strategy.getName()); - } - - FeatureEvaluationResult result = - configuredStrategy.getResult( - strategy.getParameters(), - enhancedContext, - ConstraintMerger.mergeConstraints(featureRepository, strategy), - strategy.getVariants()); - - if (result.isEnabled()) { - Variant variant = result.getVariant(); - // If strategy variant is null, look for a variant in the featureToggle - if (variant == null) { - variant = VariantUtil.selectVariant(featureToggle, context, defaultVariant); - } - result.setVariant(variant); - return result; - } - } - } - return new FeatureEvaluationResult(false, defaultVariant); - } - - /** - * Uses the old, statistically broken Variant seed for finding the correct variant - * - * @param toggleName Name of the toggle - * @param context The UnleashContext - * @param fallbackAction What to do if we fail to find the toggle - * @param defaultVariant If we can't resolve a variant, what are we returning - * @return A wrapper containing whether the feature was enabled as well which Variant was - * selected - * @deprecated - */ - private FeatureEvaluationResult deprecatedGetFeatureEvaluationResult( - String toggleName, - UnleashContext context, - BiPredicate fallbackAction, - @Nullable Variant defaultVariant) { - checkIfToggleMatchesNamePrefix(toggleName); - FeatureToggle featureToggle = featureRepository.getToggle(toggleName); UnleashContext enhancedContext = context.applyStaticFields(config); - if (featureToggle == null) { - return new FeatureEvaluationResult( - fallbackAction.test(toggleName, enhancedContext), defaultVariant); - } else if (!featureToggle.isEnabled()) { - return new FeatureEvaluationResult(false, defaultVariant); - } else if (isParentDependencySatisfied(featureToggle, context, fallbackAction)) { - if (featureToggle.getStrategies().isEmpty()) { - return new FeatureEvaluationResult( - true, - VariantUtil.selectDeprecatedVariantHashingAlgo( - featureToggle, context, defaultVariant)); - } - for (ActivationStrategy strategy : featureToggle.getStrategies()) { - Strategy configuredStrategy = getStrategy(strategy.getName()); - if (configuredStrategy == UNKNOWN_STRATEGY) { - LOGGER.warn( - "Unable to find matching strategy for toggle:{} strategy:{}", - toggleName, - strategy.getName()); - } - - FeatureEvaluationResult result = - configuredStrategy.getDeprecatedHashingAlgoResult( - strategy.getParameters(), - enhancedContext, - ConstraintMerger.mergeConstraints(featureRepository, strategy), - strategy.getVariants()); - - if (result.isEnabled()) { - Variant variant = result.getVariant(); - // If strategy variant is null, look for a variant in the featureToggle - if (variant == null) { - variant = - VariantUtil.selectDeprecatedVariantHashingAlgo( - featureToggle, context, defaultVariant); - } - result.setVariant(variant); - return result; - } + try { + Boolean enabled = + this.unleashEngine.isEnabled( + toggleName, YggdrasilAdapters.adapt(enhancedContext)); + if (enabled == null) { + enabled = fallbackAction.test(toggleName, enhancedContext); } - } - return new FeatureEvaluationResult(false, defaultVariant); - } - - private boolean isParentDependencySatisfied( - @Nonnull FeatureToggle featureToggle, - @Nonnull UnleashContext context, - BiPredicate fallbackAction) { - if (!featureToggle.hasDependencies()) { - return true; - } else { - return featureToggle.getDependencies().stream() - .allMatch( - parent -> { - FeatureToggle parentToggle = - featureRepository.getToggle(parent.getFeature()); - if (parentToggle == null) { - LOGGER.warn( - "Missing dependency [{}] for toggle: [{}]", - parent.getFeature(), - featureToggle.getName()); - return false; - } - if (!parentToggle.getDependencies().isEmpty()) { - LOGGER.warn( - "[{}] depends on feature [{}] which also depends on something. We don't currently support more than one level of dependency resolution", - featureToggle.getName(), - parent.getFeature()); - return false; - } - boolean parentSatisfied = - isEnabled( - parent.getFeature(), context, fallbackAction, true); - if (parentSatisfied) { - if (!parent.getVariants().isEmpty()) { - return parent.getVariants() - .contains( - getVariant( - parent.feature, - context, - DISABLED_VARIANT, - true) - .getName()); - } else { - return parent.isEnabled(); - } - } else { - return !parent.isEnabled(); - } - }); - } - } - private void checkIfToggleMatchesNamePrefix(String toggleName) { - if (config.getNamePrefix() != null) { - if (!toggleName.startsWith(config.getNamePrefix())) { - LOGGER.warn( - "Toggle [{}] doesnt start with configured name prefix of [{}] so it will always be disabled", - toggleName, - config.getNamePrefix()); + this.unleashEngine.countToggle(toggleName, enabled); + eventDispatcher.dispatch(new ToggleEvaluated(toggleName, enabled)); + if (this.unleashEngine.shouldEmitImpressionEvent(toggleName)) { + eventDispatcher.dispatch( + new IsEnabledImpressionEvent(toggleName, enabled, context)); } + return enabled; + } catch (YggdrasilInvalidInputException | YggdrasilError e) { + LOGGER.warn( + "A serious issue occurred when evaluating a feature toggle, defaulting to false", + e); + return false; } } @@ -347,30 +189,37 @@ public Variant getVariant(String toggleName, UnleashContext context) { @Override public Variant getVariant(String toggleName, UnleashContext context, Variant defaultValue) { - return getVariant(toggleName, context, defaultValue, false); - } + UnleashContext enhancedContext = context.applyStaticFields(config); - private Variant getVariant( - String toggleName, UnleashContext context, Variant defaultValue, boolean isParent) { - FeatureEvaluationResult result = - getFeatureEvaluationResult(toggleName, context, (n, c) -> false, defaultValue); - Variant variant = result.getVariant(); - if (!isParent) { - metricService.countVariant(toggleName, variant.getName()); - // Should count yes/no also when getting variant. - metricService.count(toggleName, result.isEnabled()); - } - dispatchVariantImpressionDataIfNeeded( - toggleName, variant.getName(), result.isEnabled(), context); - return variant; - } + try { + Context adaptedContext = YggdrasilAdapters.adapt(enhancedContext); + + Variant variant = + YggdrasilAdapters.adapt( + this.unleashEngine.getVariant(toggleName, adaptedContext), + defaultValue); - private void dispatchVariantImpressionDataIfNeeded( - String toggleName, String variantName, boolean enabled, UnleashContext context) { - FeatureToggle toggle = featureRepository.getToggle(toggleName); - if (toggle != null && toggle.hasImpressionData()) { - eventDispatcher.dispatch( - new VariantImpressionEvent(toggleName, enabled, context, variantName)); + Boolean enabled = this.unleashEngine.isEnabled(toggleName, adaptedContext); + + // TODO: Swap this for feature enabled + if (enabled == null) { + enabled = false; + } + + this.unleashEngine.countToggle(toggleName, enabled); + this.unleashEngine.countVariant(toggleName, variant.getName()); + eventDispatcher.dispatch(new ToggleEvaluated(toggleName, variant.isEnabled())); + if (unleashEngine.shouldEmitImpressionEvent(toggleName)) { + eventDispatcher.dispatch( + new VariantImpressionEvent( + toggleName, enabled, context, variant.getName())); + } + return variant; + } catch (YggdrasilInvalidInputException | YggdrasilError e) { + LOGGER.warn( + "A serious issue occurred when evaluating a variant, defaulting to the default value", + e); + return defaultValue; } } @@ -384,75 +233,6 @@ public Variant getVariant(String toggleName, Variant defaultValue) { return getVariant(toggleName, contextProvider.getContext(), defaultValue); } - /** - * Uses the old, statistically broken Variant seed for finding the correct variant - * - * @param toggleName - * @param context - * @return - * @deprecated - */ - @Override - public Variant deprecatedGetVariant(String toggleName, UnleashContext context) { - return deprecatedGetVariant(toggleName, context, DISABLED_VARIANT); - } - - /** - * Uses the old, statistically broken Variant seed for finding the correct variant - * - * @param toggleName - * @param context - * @param defaultValue - * @return - * @deprecated - */ - @Override - public Variant deprecatedGetVariant( - String toggleName, UnleashContext context, Variant defaultValue) { - return deprecatedGetVariant(toggleName, context, defaultValue, false); - } - - private Variant deprecatedGetVariant( - String toggleName, UnleashContext context, Variant defaultValue, boolean isParent) { - FeatureEvaluationResult result = - deprecatedGetFeatureEvaluationResult( - toggleName, context, (n, c) -> false, defaultValue); - Variant variant = result.getVariant(); - if (!isParent) { - metricService.countVariant(toggleName, variant.getName()); - // Should count yes/no also when getting variant. - metricService.count(toggleName, result.isEnabled()); - } - dispatchVariantImpressionDataIfNeeded( - toggleName, variant.getName(), result.isEnabled(), context); - return variant; - } - - /** - * Uses the old, statistically broken Variant seed for finding the correct variant - * - * @param toggleName - * @return - * @deprecated - */ - @Override - public Variant deprecatedGetVariant(String toggleName) { - return deprecatedGetVariant(toggleName, contextProvider.getContext()); - } - - /** - * Uses the old, statistically broken Variant seed for finding the correct variant - * - * @param toggleName - * @param defaultValue - * @return - * @deprecated - */ - @Override - public Variant deprecatedGetVariant(String toggleName, Variant defaultValue) { - return deprecatedGetVariant(toggleName, contextProvider.getContext(), defaultValue); - } - /** * Use more().getFeatureToggleDefinition() instead * @@ -473,12 +253,6 @@ public List getFeatureToggleNames() { return featureRepository.getFeatureNames(); } - /** Use more().count() instead */ - @Deprecated - public void count(final String toggleName, boolean enabled) { - metricService.count(toggleName, enabled); - } - private static Map buildStrategyMap(@Nullable Strategy[] strategies) { Map map = new HashMap<>(); @@ -493,10 +267,6 @@ private static Map buildStrategyMap(@Nullable Strategy[] strat return map; } - private Strategy getStrategy(String strategy) { - return strategyMap.getOrDefault(strategy, config.getFallbackStrategy()); - } - @Override public void shutdown() { config.getScheduledExecutor().shutdown(); @@ -529,24 +299,12 @@ public List evaluateAllToggles(UnleashContext context) { return getFeatureToggleNames().stream() .map( toggleName -> { - FeatureEvaluationResult result = - getFeatureEvaluationResult( - toggleName, context, (n, c) -> false, null); - - return new EvaluatedToggle( - toggleName, result.isEnabled(), result.getVariant()); + boolean enabled = + isEnabled(toggleName, context, (name, ctx) -> false); + Variant variant = getVariant(toggleName, context, DISABLED_VARIANT); + return new EvaluatedToggle(toggleName, enabled, variant); }) .collect(Collectors.toList()); } - - @Override - public void count(final String toggleName, boolean enabled) { - metricService.count(toggleName, enabled); - } - - @Override - public void countVariant(final String toggleName, String variantName) { - metricService.countVariant(toggleName, variantName); - } } } diff --git a/src/main/java/io/getunleash/FakeUnleash.java b/src/main/java/io/getunleash/FakeUnleash.java index 0e88d135a..64eb03e7f 100644 --- a/src/main/java/io/getunleash/FakeUnleash.java +++ b/src/main/java/io/getunleash/FakeUnleash.java @@ -107,17 +107,6 @@ public void disableAllExcept(String... excludedFeatures) { } } - @Override - public Variant deprecatedGetVariant(String toggleName, UnleashContext context) { - return null; - } - - @Override - public Variant deprecatedGetVariant( - String toggleName, UnleashContext context, Variant defaultValue) { - return null; - } - public void resetAll() { disableAll = false; enableAll = false; @@ -176,15 +165,5 @@ public List evaluateAllToggles(@Nullable UnleashContext context getVariant(toggleName))) .collect(Collectors.toList()); } - - @Override - public void count(String toggleName, boolean enabled) { - // Nothing to count - } - - @Override - public void countVariant(String toggleName, String variantName) { - // Nothing to count - } } } diff --git a/src/main/java/io/getunleash/MoreOperations.java b/src/main/java/io/getunleash/MoreOperations.java index ca37fdfd5..a362ac065 100644 --- a/src/main/java/io/getunleash/MoreOperations.java +++ b/src/main/java/io/getunleash/MoreOperations.java @@ -19,8 +19,4 @@ public interface MoreOperations { * @return */ List evaluateAllToggles(UnleashContext context); - - void count(String toggleName, boolean enabled); - - void countVariant(String toggleName, String variantName); } diff --git a/src/main/java/io/getunleash/Unleash.java b/src/main/java/io/getunleash/Unleash.java index 79fe6964e..6e5259799 100644 --- a/src/main/java/io/getunleash/Unleash.java +++ b/src/main/java/io/getunleash/Unleash.java @@ -43,19 +43,6 @@ default Variant getVariant(final String toggleName, final Variant defaultValue) return getVariant(toggleName, UnleashContext.builder().build(), defaultValue); } - Variant deprecatedGetVariant(final String toggleName, final UnleashContext context); - - Variant deprecatedGetVariant( - final String toggleName, final UnleashContext context, final Variant defaultValue); - - default Variant deprecatedGetVariant(final String toggleName) { - return deprecatedGetVariant(toggleName, UnleashContext.builder().build()); - } - - default Variant deprecatedGetVariant(final String toggleName, final Variant defaultValue) { - return deprecatedGetVariant(toggleName, UnleashContext.builder().build(), defaultValue); - } - /** * Use more().getFeatureToggleNames() instead * diff --git a/src/main/java/io/getunleash/YggdrasilAdapters.java b/src/main/java/io/getunleash/YggdrasilAdapters.java new file mode 100644 index 000000000..18753674b --- /dev/null +++ b/src/main/java/io/getunleash/YggdrasilAdapters.java @@ -0,0 +1,84 @@ +package io.getunleash; + +import io.getunleash.engine.Context; +import io.getunleash.engine.IStrategy; +import io.getunleash.engine.Payload; +import io.getunleash.engine.VariantDef; +import io.getunleash.lang.Nullable; +import io.getunleash.strategy.Strategy; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.Map; +import java.util.Optional; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +final class YggdrasilAdapters { + + private static final Logger LOGGER = LoggerFactory.getLogger(DefaultUnleash.class); + + @NotNull + public static IStrategy adapt(Strategy s) { + return new IStrategy() { + @Override + public String getName() { + return s.getName(); + } + + @Override + public boolean isEnabled(Map map, Context context) { + return s.isEnabled(map, adapt(context)); + } + }; + } + + public static UnleashContext adapt(Context context) { + ZonedDateTime currentTime = ZonedDateTime.now(); + if (context.getCurrentTime() != null) { + try { + currentTime = ZonedDateTime.parse(context.getCurrentTime()); + } catch (DateTimeParseException e) { + LOGGER.warn("Could not parse current time from context, falling back to system time: ", context.getCurrentTime()); + currentTime = ZonedDateTime.now(); + } + } + + return new UnleashContext( + context.getAppName(), + context.getEnvironment(), + context.getUserId(), + context.getSessionId(), + context.getRemoteAddress(), + currentTime, + context.getProperties()); + } + + public static Context adapt(UnleashContext context) { + Context mapped = new Context(); + mapped.setAppName(context.getAppName().orElse(null)); + mapped.setEnvironment(context.getEnvironment().orElse(null)); + mapped.setUserId(context.getUserId().orElse(null)); + mapped.setSessionId(context.getSessionId().orElse(null)); + mapped.setRemoteAddress(context.getRemoteAddress().orElse(null)); + mapped.setProperties(context.getProperties()); + mapped.setCurrentTime( + DateTimeFormatter.ISO_DATE_TIME.format( + context.getCurrentTime().orElse(ZonedDateTime.now()))); + return mapped; + } + + public static Variant adapt(VariantDef variant, Variant defaultValue) { + if (variant == null) { + return defaultValue; + } + return new Variant(variant.getName(), adapt(variant.getPayload()), variant.isEnabled()); + } + + public static @Nullable io.getunleash.variant.Payload adapt(@Nullable Payload payload) { + return Optional.ofNullable(payload) + .map(p -> new io.getunleash.variant.Payload(p.getType(), p.getValue())) + .orElse(new io.getunleash.variant.Payload("string", null)); + } +} diff --git a/src/main/java/io/getunleash/metric/ClientMetrics.java b/src/main/java/io/getunleash/metric/ClientMetrics.java index 82eac44e9..8d70070a8 100644 --- a/src/main/java/io/getunleash/metric/ClientMetrics.java +++ b/src/main/java/io/getunleash/metric/ClientMetrics.java @@ -1,5 +1,6 @@ package io.getunleash.metric; +import io.getunleash.engine.MetricsBucket; import io.getunleash.event.UnleashEvent; import io.getunleash.event.UnleashSubscriber; import io.getunleash.lang.Nullable; diff --git a/src/main/java/io/getunleash/metric/DefaultHttpMetricsSender.java b/src/main/java/io/getunleash/metric/DefaultHttpMetricsSender.java index a69b5c389..8c0abde25 100644 --- a/src/main/java/io/getunleash/metric/DefaultHttpMetricsSender.java +++ b/src/main/java/io/getunleash/metric/DefaultHttpMetricsSender.java @@ -5,12 +5,14 @@ import io.getunleash.event.EventDispatcher; import io.getunleash.util.AtomicLongSerializer; import io.getunleash.util.DateTimeSerializer; +import io.getunleash.util.InstantSerializer; import io.getunleash.util.UnleashConfig; import io.getunleash.util.UnleashURLs; import java.io.IOException; import java.io.OutputStreamWriter; import java.net.HttpURLConnection; import java.net.URL; +import java.time.Instant; import java.time.LocalDateTime; import java.util.concurrent.atomic.AtomicLong; @@ -33,6 +35,7 @@ public DefaultHttpMetricsSender(UnleashConfig unleashConfig) { this.gson = new GsonBuilder() .registerTypeAdapter(LocalDateTime.class, new DateTimeSerializer()) + .registerTypeAdapter(Instant.class, new InstantSerializer()) .registerTypeAdapter(AtomicLong.class, new AtomicLongSerializer()) .create(); } diff --git a/src/main/java/io/getunleash/metric/MetricsBucket.java b/src/main/java/io/getunleash/metric/MetricsBucket.java deleted file mode 100644 index be09773f6..000000000 --- a/src/main/java/io/getunleash/metric/MetricsBucket.java +++ /dev/null @@ -1,47 +0,0 @@ -package io.getunleash.metric; - -import io.getunleash.lang.Nullable; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; - -class MetricsBucket { - private final ConcurrentMap toggles; - private final LocalDateTime start; - @Nullable private volatile LocalDateTime stop; - - MetricsBucket() { - this.start = LocalDateTime.now(ZoneId.of("UTC")); - this.toggles = new ConcurrentHashMap<>(); - } - - void registerCount(String toggleName, boolean active) { - getOrCreate(toggleName).register(active); - } - - void registerCount(String toggleName, String variantName) { - getOrCreate(toggleName).register(variantName); - } - - private ToggleCount getOrCreate(String toggleName) { - return toggles.computeIfAbsent(toggleName, s -> new ToggleCount()); - } - - void end() { - this.stop = LocalDateTime.now(ZoneId.of("UTC")); - } - - public Map getToggles() { - return toggles; - } - - public LocalDateTime getStart() { - return start; - } - - public @Nullable LocalDateTime getStop() { - return stop; - } -} diff --git a/src/main/java/io/getunleash/metric/UnleashMetricService.java b/src/main/java/io/getunleash/metric/UnleashMetricService.java index 1865c3b82..d3602b7df 100644 --- a/src/main/java/io/getunleash/metric/UnleashMetricService.java +++ b/src/main/java/io/getunleash/metric/UnleashMetricService.java @@ -4,8 +4,4 @@ public interface UnleashMetricService { void register(Set strategies); - - void count(String toggleName, boolean active); - - void countVariant(String toggleName, String variantName); } diff --git a/src/main/java/io/getunleash/metric/UnleashMetricServiceImpl.java b/src/main/java/io/getunleash/metric/UnleashMetricServiceImpl.java index caf2c3b0e..bf735c3fb 100644 --- a/src/main/java/io/getunleash/metric/UnleashMetricServiceImpl.java +++ b/src/main/java/io/getunleash/metric/UnleashMetricServiceImpl.java @@ -1,5 +1,8 @@ package io.getunleash.metric; +import io.getunleash.engine.MetricsBucket; +import io.getunleash.engine.UnleashEngine; +import io.getunleash.engine.YggdrasilError; import io.getunleash.util.Throttler; import io.getunleash.util.UnleashConfig; import io.getunleash.util.UnleashScheduledExecutor; @@ -15,21 +18,25 @@ public class UnleashMetricServiceImpl implements UnleashMetricService { private final UnleashConfig unleashConfig; private final MetricSender metricSender; - // mutable - private volatile MetricsBucket currentMetricsBucket; + // synchronization is handled in the engine itself + private final UnleashEngine engine; private final Throttler throttler; public UnleashMetricServiceImpl( - UnleashConfig unleashConfig, UnleashScheduledExecutor executor) { - this(unleashConfig, unleashConfig.getMetricSenderFactory().apply(unleashConfig), executor); + UnleashConfig unleashConfig, UnleashScheduledExecutor executor, UnleashEngine engine) { + this( + unleashConfig, + unleashConfig.getMetricSenderFactory().apply(unleashConfig), + executor, + engine); } public UnleashMetricServiceImpl( UnleashConfig unleashConfig, MetricSender metricSender, - UnleashScheduledExecutor executor) { - this.currentMetricsBucket = new MetricsBucket(); + UnleashScheduledExecutor executor, + UnleashEngine engine) { this.started = LocalDateTime.now(ZoneId.of("UTC")); this.unleashConfig = unleashConfig; this.metricSender = metricSender; @@ -38,6 +45,7 @@ public UnleashMetricServiceImpl( (int) unleashConfig.getSendMetricsInterval(), 300, unleashConfig.getUnleashURLs().getClientMetricsURL()); + this.engine = engine; long metricsInterval = unleashConfig.getSendMetricsInterval(); executor.setInterval(sendMetrics(), metricsInterval, metricsInterval); @@ -50,29 +58,24 @@ public void register(Set strategies) { metricSender.registerClient(registration); } - @Override - public void count(String toggleName, boolean active) { - currentMetricsBucket.registerCount(toggleName, active); - } - - @Override - public void countVariant(String toggleName, String variantName) { - currentMetricsBucket.registerCount(toggleName, variantName); - } - private Runnable sendMetrics() { return () -> { if (throttler.performAction()) { - MetricsBucket metricsBucket = this.currentMetricsBucket; - this.currentMetricsBucket = new MetricsBucket(); - metricsBucket.end(); - ClientMetrics metrics = new ClientMetrics(unleashConfig, metricsBucket); - int statusCode = metricSender.sendMetrics(metrics); - if (statusCode >= 200 && statusCode < 400) { - throttler.decrementFailureCountAndResetSkips(); - } - if (statusCode >= 400) { - throttler.handleHttpErrorCodes(statusCode); + try { + MetricsBucket bucket = this.engine.getMetrics(); + + ClientMetrics metrics = new ClientMetrics(unleashConfig, bucket); + int statusCode = metricSender.sendMetrics(metrics); + if (statusCode >= 200 && statusCode < 400) { + throttler.decrementFailureCountAndResetSkips(); + } + if (statusCode >= 400) { + throttler.handleHttpErrorCodes(statusCode); + } + } catch (YggdrasilError e) { + LOGGER.error( + "Failed to retrieve metrics from the engine, this is a serious error, please report it", + e); } } else { throttler.skipped(); diff --git a/src/main/java/io/getunleash/repository/FeatureRepository.java b/src/main/java/io/getunleash/repository/FeatureRepository.java index dfeb8a61a..77bce48ae 100644 --- a/src/main/java/io/getunleash/repository/FeatureRepository.java +++ b/src/main/java/io/getunleash/repository/FeatureRepository.java @@ -10,6 +10,7 @@ import io.getunleash.util.UnleashConfig; import io.getunleash.util.UnleashScheduledExecutor; import java.util.Collections; +import java.util.LinkedList; import java.util.List; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -24,6 +25,8 @@ public class FeatureRepository implements IFeatureRepository { private final FeatureFetcher featureFetcher; private final EventDispatcher eventDispatcher; + private List> consumers = new LinkedList<>(); + private final Throttler throttler; private FeatureCollection featureCollection; @@ -116,6 +119,10 @@ private void initCollections(UnleashScheduledExecutor executor) { } } + public void addConsumer(Consumer consumer) { + this.consumers.add(consumer); + } + private Runnable updateFeatures(final Consumer handler) { return () -> { if (throttler.performAction()) { @@ -131,6 +138,14 @@ private Runnable updateFeatures(final Consumer handler) { ? segmentCollection : new SegmentCollection(Collections.emptyList())); + consumers.forEach( + consumer -> { + try { + consumer.accept(featureCollection); + } catch (Exception e) { + LOGGER.error("Error when calling consumer {}", consumer, e); + } + }); featureBackupHandler.write(featureCollection); } else if (response.getStatus() == ClientFeaturesResponse.Status.UNAVAILABLE) { if (!ready && unleashConfig.isSynchronousFetchOnInitialisation()) { diff --git a/src/main/java/io/getunleash/repository/IFeatureRepository.java b/src/main/java/io/getunleash/repository/IFeatureRepository.java index 635f612da..a15771713 100644 --- a/src/main/java/io/getunleash/repository/IFeatureRepository.java +++ b/src/main/java/io/getunleash/repository/IFeatureRepository.java @@ -2,8 +2,11 @@ import io.getunleash.Segment; import io.getunleash.lang.Nullable; +import java.util.function.Consumer; public interface IFeatureRepository extends ToggleRepository { @Nullable Segment getSegment(Integer id); + + void addConsumer(Consumer consumer); } diff --git a/src/main/java/io/getunleash/repository/JsonFeatureParser.java b/src/main/java/io/getunleash/repository/JsonFeatureParser.java index 4ff1efc66..5d1f83d44 100644 --- a/src/main/java/io/getunleash/repository/JsonFeatureParser.java +++ b/src/main/java/io/getunleash/repository/JsonFeatureParser.java @@ -4,7 +4,7 @@ import com.google.gson.GsonBuilder; import java.io.Reader; -final class JsonFeatureParser { +public final class JsonFeatureParser { private JsonFeatureParser() {} diff --git a/src/main/java/io/getunleash/util/InstantSerializer.java b/src/main/java/io/getunleash/util/InstantSerializer.java new file mode 100644 index 000000000..9491387cb --- /dev/null +++ b/src/main/java/io/getunleash/util/InstantSerializer.java @@ -0,0 +1,20 @@ +package io.getunleash.util; + +import com.google.gson.*; +import java.lang.reflect.Type; +import java.time.Instant; + +public class InstantSerializer implements JsonSerializer, JsonDeserializer { + + @Override + public JsonElement serialize( + Instant instant, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(instant.toString()); + } + + @Override + public Instant deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + return Instant.parse(json.getAsString()); + } +} diff --git a/src/test/java/io/getunleash/DefaultUnleashTest.java b/src/test/java/io/getunleash/DefaultUnleashTest.java index beebaa196..5344bf540 100644 --- a/src/test/java/io/getunleash/DefaultUnleashTest.java +++ b/src/test/java/io/getunleash/DefaultUnleashTest.java @@ -52,7 +52,6 @@ public void setup() { strategyMap.put("default", new DefaultStrategy()); contextProvider = mock(UnleashContextProvider.class); eventDispatcher = mock(EventDispatcher.class); - metricService = mock(UnleashMetricService.class); sut = new DefaultUnleash( @@ -60,8 +59,7 @@ public void setup() { featureRepository, strategyMap, contextProvider, - eventDispatcher, - metricService); + eventDispatcher); } @Test @@ -76,22 +74,6 @@ public void should_evaluate_all_toggle_with_context() { assertThat(t1.isEnabled()).isFalse(); } - @Test - public void should_count_and_not_throw_an_error() { - sut.more().count("toggle1", true); - sut.more().count("toggle1", false); - - verify(metricService).count("toggle1", true); - verify(metricService).count("toggle1", false); - } - - @Test - public void should_countVariant_and_not_throw_an_error() { - sut.more().countVariant("toggle1", "variant1"); - - verify(metricService).countVariant("toggle1", "variant1"); - } - @Test public void should_evaluate_missing_segment_as_false() { String toggleName = "F9.withMissingSegment"; @@ -104,9 +86,12 @@ public void should_evaluate_missing_segment_as_false() { asList(semverConstraint), asList(404), Collections.emptyList()); - when(featureRepository.getToggle(toggleName)) - .thenReturn(new FeatureToggle(toggleName, true, asList(withMissingSegment))); - when(featureRepository.getSegment(404)).thenReturn(Segment.DENY_SEGMENT); + new UnleashEngineStateHandler(sut) + .setState( + Collections.singletonList( + new FeatureToggle(toggleName, true, asList(withMissingSegment))), + Collections.singletonList(Segment.DENY_SEGMENT)); + when(contextProvider.getContext()) .thenReturn(UnleashContext.builder().addProperty("version", semVer).build()); assertThat(sut.isEnabled(toggleName)).isFalse(); @@ -123,18 +108,20 @@ public void should_evaluate_segment_collection_with_one_missing_segment_as_false asList(semverConstraint), asList(404, 1), Collections.emptyList()); - when(featureRepository.getToggle(toggleName)) - .thenReturn(new FeatureToggle(toggleName, true, asList(withMissingSegment))); - when(featureRepository.getSegment(1)) - .thenReturn( - new Segment( - 1, - "always true", - asList( - new Constraint( - "always_true", - Operator.NOT_IN, - Collections.EMPTY_LIST)))); + new UnleashEngineStateHandler(sut) + .setState( + Collections.singletonList( + new FeatureToggle(toggleName, true, asList(withMissingSegment))), + Collections.singletonList( + new Segment( + 1, + "always true", + asList( + new Constraint( + "always_true", + Operator.NOT_IN, + Collections.EMPTY_LIST))))); + when(contextProvider.getContext()) .thenReturn(UnleashContext.builder().addProperty("version", "1.2.2").build()); assertThat(sut.isEnabled(toggleName)).isFalse(); @@ -157,17 +144,16 @@ public void should_allow_fallback_strategy() { featureRepository, new HashMap<>(), contextProvider, - eventDispatcher, - metricService); + eventDispatcher); ActivationStrategy as = new ActivationStrategy("forFallback", new HashMap<>()); FeatureToggle toggle = new FeatureToggle("toggle1", true, Collections.singletonList(as)); - when(featureRepository.getToggle("toggle1")).thenReturn(toggle); + new UnleashEngineStateHandler(sut).setState(toggle); when(contextProvider.getContext()).thenReturn(UnleashContext.builder().build()); sut.isEnabled("toggle1"); - verify(fallback).isEnabled(any(), any(), anyList()); + verify(fallback).isEnabled(any(), any()); } @Test @@ -228,7 +214,7 @@ public void supports_failing_hard_on_multiple_instantiations() { assertThatThrownBy( () -> { Unleash unleash2 = - new DefaultUnleash(config, null, null, null, null, null, true); + new DefaultUnleash(config, null, null, null, null, true); }) .isInstanceOf(RuntimeException.class) .withFailMessage( diff --git a/src/test/java/io/getunleash/DependentFeatureToggleTest.java b/src/test/java/io/getunleash/DependentFeatureToggleTest.java deleted file mode 100644 index 194bf671f..000000000 --- a/src/test/java/io/getunleash/DependentFeatureToggleTest.java +++ /dev/null @@ -1,205 +0,0 @@ -package io.getunleash; - -import static java.util.Collections.emptyList; -import static java.util.Collections.singletonList; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.*; - -import io.getunleash.event.EventDispatcher; -import io.getunleash.event.IsEnabledImpressionEvent; -import io.getunleash.event.VariantImpressionEvent; -import io.getunleash.metric.UnleashMetricService; -import io.getunleash.repository.FeatureRepository; -import io.getunleash.strategy.DefaultStrategy; -import io.getunleash.strategy.Strategy; -import io.getunleash.util.UnleashConfig; -import io.getunleash.variant.VariantDefinition; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -public class DependentFeatureToggleTest { - private DefaultUnleash sut; - private FeatureRepository featureRepository; - private UnleashContextProvider contextProvider; - private EventDispatcher eventDispatcher; - private UnleashMetricService metricService; - - @BeforeEach - public void setup() { - UnleashConfig unleashConfig = - UnleashConfig.builder().unleashAPI("http://fakeAPI").appName("fakeApp").build(); - featureRepository = mock(FeatureRepository.class); - Map strategyMap = new HashMap<>(); - strategyMap.put("default", new DefaultStrategy()); - contextProvider = mock(UnleashContextProvider.class); - eventDispatcher = mock(EventDispatcher.class); - metricService = mock(UnleashMetricService.class); - - sut = - new DefaultUnleash( - unleashConfig, - featureRepository, - strategyMap, - contextProvider, - eventDispatcher, - metricService); - } - - @Test - public void should_not_increment_count_for_parent_toggle_when_checking_child_toggle() { - FeatureToggle child = - new FeatureToggle( - "child", - true, - singletonList(new ActivationStrategy("default", null)), - Collections.emptyList(), - false, - singletonList(new FeatureDependency("parent"))); - FeatureToggle parent = - new FeatureToggle( - "parent", - true, - singletonList(new ActivationStrategy("default", null)), - Collections.emptyList(), - false); - when(featureRepository.getToggle("child")).thenReturn(child); - when(featureRepository.getToggle("parent")).thenReturn(parent); - boolean enabled = sut.isEnabled("child", UnleashContext.builder().userId("7").build()); - assertThat(enabled).isTrue(); - verify(metricService).count("child", true); - verify(metricService, never()).count(eq("parent"), anyBoolean()); - } - - @Test - public void should_not_increment_count_for_parent_toggle_when_checking_parent_variants() { - FeatureToggle child = - new FeatureToggle( - "child", - true, - singletonList(new ActivationStrategy("default", null)), - singletonList(new VariantDefinition("childVariant", 1, null, null)), - false, - singletonList( - new FeatureDependency("parent", true, singletonList("first")))); - FeatureToggle parent = - new FeatureToggle( - "parent", - true, - singletonList(new ActivationStrategy("default", null)), - singletonList(new VariantDefinition("first", 1, null, null, null)), - false); - when(featureRepository.getToggle("child")).thenReturn(child); - when(featureRepository.getToggle("parent")).thenReturn(parent); - Variant variant = sut.getVariant("child", UnleashContext.builder().userId("7").build()); - assertThat(variant).isNotNull(); - verify(metricService).countVariant("child", "childVariant"); - verify(metricService, never()).countVariant("parent", "first"); - } - - @Test - public void should_trigger_impression_event_for_parent_toggle_when_checking_child_toggle() { - FeatureToggle child = - new FeatureToggle( - "child", - true, - singletonList(new ActivationStrategy("default", null)), - Collections.emptyList(), - false, - singletonList(new FeatureDependency("parent"))); - FeatureToggle parent = - new FeatureToggle( - "parent", - true, - singletonList(new ActivationStrategy("default", null)), - Collections.emptyList(), - true); - when(featureRepository.getToggle("child")).thenReturn(child); - when(featureRepository.getToggle("parent")).thenReturn(parent); - boolean enabled = sut.isEnabled("child", UnleashContext.builder().userId("7").build()); - assertThat(enabled).isTrue(); - verify(eventDispatcher).dispatch(any(IsEnabledImpressionEvent.class)); - } - - @Test - public void should_trigger_impression_event_for_parent_variant_when_checking_child_toggle() { - FeatureToggle child = - new FeatureToggle( - "child", - true, - singletonList(new ActivationStrategy("default", null)), - singletonList(new VariantDefinition("childVariant", 1, null, null)), - true, - singletonList( - new FeatureDependency("parent", true, singletonList("first")))); - FeatureToggle parent = - new FeatureToggle( - "parent", - true, - singletonList(new ActivationStrategy("default", null)), - singletonList(new VariantDefinition("first", 1, null, null, null)), - true); - when(featureRepository.getToggle("child")).thenReturn(child); - when(featureRepository.getToggle("parent")).thenReturn(parent); - Variant variant = sut.getVariant("child", UnleashContext.builder().userId("7").build()); - assertThat(variant).isNotNull(); - verify(eventDispatcher, times(2)).dispatch(any(VariantImpressionEvent.class)); - } - - @Test - public void - child_is_disabled_if_the_parent_is_disabled_even_if_the_childs_expected_variant_is_the_disabled_variant() { - Map parameters = new HashMap<>(); - parameters.put("rollout", "100"); - parameters.put("stickiness", "default"); - parameters.put("groupId", "groupId"); - String parentName = "parent.disabled"; - FeatureToggle parent = - new FeatureToggle( - parentName, - false, - singletonList(new ActivationStrategy("default", new HashMap<>()))); - String childName = "parent.disabled.child.expects.disabled.variant"; - FeatureToggle child = - new FeatureToggle( - childName, - true, - singletonList(new ActivationStrategy("flexibleRollout", parameters)), - emptyList(), - false, - singletonList( - new FeatureDependency( - parentName, null, singletonList("disabled")))); - when(featureRepository.getToggle(childName)).thenReturn(child); - when(featureRepository.getToggle(parentName)).thenReturn(parent); - assertThat(sut.isEnabled(childName, UnleashContext.builder().build())).isFalse(); - } - - @Test - public void childIsDisabledWhenChildDoesNotHaveStrategiesAndParentIsDisabled() { - FeatureToggle parent = - new FeatureToggle( - "parent", false, singletonList(new ActivationStrategy("default", null))); - FeatureDependency childDependsOnParent = new FeatureDependency("parant", true, emptyList()); - FeatureToggle child = - new FeatureToggle( - "child", - true, - emptyList(), - emptyList(), - true, - singletonList(childDependsOnParent)); - when(featureRepository.getToggle("child")).thenReturn(child); - when(featureRepository.getToggle("parent")).thenReturn(parent); - assertThat(sut.isEnabled("child", UnleashContext.builder().build())).isFalse(); - } - - @Test - public void shouldBeEnabledWhenMissingStrategies() { - FeatureToggle c = new FeatureToggle("c", true, emptyList()); - when(featureRepository.getToggle("c")).thenReturn(c); - assertThat(sut.isEnabled("c", UnleashContext.builder().build())).isTrue(); - } -} diff --git a/src/test/java/io/getunleash/FakeUnleashTest.java b/src/test/java/io/getunleash/FakeUnleashTest.java index daaaf09c2..bee2eceeb 100644 --- a/src/test/java/io/getunleash/FakeUnleashTest.java +++ b/src/test/java/io/getunleash/FakeUnleashTest.java @@ -168,18 +168,6 @@ public void should_get_disabled_variant_when_toggle_is_disabled() { assertThat(fakeUnleash.getVariant("t1").getName()).isEqualTo("disabled"); } - @Test - public void should_count_and_not_throw_an_error() { - FakeUnleash fakeUnleash = new FakeUnleash(); - fakeUnleash.more().count("anything", true); - } - - @Test - public void should_countVariant_and_not_throw_an_error() { - FakeUnleash fakeUnleash = new FakeUnleash(); - fakeUnleash.more().countVariant("toggleName", "variantName"); - } - @Test public void if_all_is_enabled_should_return_true_even_if_feature_does_not_exist_and_fallback_returns_false() { diff --git a/src/test/java/io/getunleash/UnleashTest.java b/src/test/java/io/getunleash/UnleashTest.java index f80c976bc..ea45f04b6 100644 --- a/src/test/java/io/getunleash/UnleashTest.java +++ b/src/test/java/io/getunleash/UnleashTest.java @@ -6,9 +6,10 @@ import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; +import io.getunleash.engine.MetricsBucket; import io.getunleash.event.EventDispatcher; -import io.getunleash.metric.UnleashMetricService; import io.getunleash.repository.*; +import io.getunleash.repository.UnleashEngineStateHandler; import io.getunleash.strategy.Strategy; import io.getunleash.strategy.UserWithIdStrategy; import io.getunleash.util.UnleashConfig; @@ -26,6 +27,8 @@ public class UnleashTest { private UnleashContextProvider contextProvider; private Unleash unleash; + private UnleashEngineStateHandler stateHandler; + @BeforeEach public void setup() { toggleRepository = mock(FeatureRepository.class); @@ -43,15 +46,13 @@ public void setup() { .build(); unleash = new DefaultUnleash(config, toggleRepository, new UserWithIdStrategy()); + stateHandler = new UnleashEngineStateHandler((DefaultUnleash) unleash); } @Test public void known_toogle_and_strategy_should_be_active() { - when(toggleRepository.getToggle("test")) - .thenReturn( - new FeatureToggle( - "test", true, asList(new ActivationStrategy("default", null)))); - + stateHandler.setState( + new FeatureToggle("test", true, asList(new ActivationStrategy("default", null)))); assertThat(unleash.isEnabled("test")).isTrue(); } @@ -113,10 +114,8 @@ void fallback_function_should_be_invoked_and_return_false() { @Test void fallback_function_should_not_be_called_when_toggle_is_defined() { - when(toggleRepository.getToggle("test")) - .thenReturn( - new FeatureToggle( - "test", true, asList(new ActivationStrategy("default", null)))); + stateHandler.setState( + new FeatureToggle("test", true, asList(new ActivationStrategy("default", null)))); BiPredicate fallbackAction = mock(BiPredicate.class); when(fallbackAction.test(eq("test"), any(UnleashContext.class))).thenReturn(false); @@ -139,14 +138,13 @@ public void should_register_custom_strategies() { .unleashAPI("http://localhost:4242/api/") .build(); unleash = new DefaultUnleash(config, toggleRepository, customStrategy); - when(toggleRepository.getToggle("test")) - .thenReturn( + new UnleashEngineStateHandler((DefaultUnleash) unleash) + .setState( new FeatureToggle( "test", true, asList(new ActivationStrategy("custom", null)))); - unleash.isEnabled("test"); - verify(customStrategy, times(1)).isEnabled(any(), any(UnleashContext.class), anyList()); + verify(customStrategy, times(1)).isEnabled(any(), any(UnleashContext.class)); } @Test @@ -157,7 +155,7 @@ public void should_support_multiple_strategies() { FeatureToggle featureToggle = new FeatureToggle("test", true, asList(strategy1, activeStrategy)); - when(toggleRepository.getToggle("test")).thenReturn(featureToggle); + stateHandler.setState(featureToggle); assertThat(unleash.isEnabled("test")).isTrue(); } @@ -189,7 +187,7 @@ public void shouldSupportMultipleRolloutStrategies() { FeatureToggle featureToggle = new FeatureToggle("test", true, asList(strategy1, strategy2)); - when(toggleRepository.getToggle("test")).thenReturn(featureToggle); + stateHandler.setState(featureToggle); assertThat(unleash.isEnabled("test", UnleashContext.builder().userId("1").build())) .isFalse(); @@ -212,7 +210,7 @@ public void should_support_context_provider() { ActivationStrategy strategy = new ActivationStrategy("userWithId", params); FeatureToggle featureToggle = new FeatureToggle("test", true, asList(strategy)); - when(toggleRepository.getToggle("test")).thenReturn(featureToggle); + stateHandler.setState(featureToggle); assertThat(unleash.isEnabled("test")).isTrue(); } @@ -227,7 +225,7 @@ public void should_support_context_as_part_of_is_enabled_call() { ActivationStrategy strategy = new ActivationStrategy("userWithId", params); FeatureToggle featureToggle = new FeatureToggle("test", true, asList(strategy)); - when(toggleRepository.getToggle("test")).thenReturn(featureToggle); + stateHandler.setState(featureToggle); assertThat(unleash.isEnabled("test", context)).isTrue(); } @@ -302,7 +300,6 @@ public void get_default_variant_when_disabled() { @Test public void getting_variant_when_disabled_should_increment_no_counter() { UnleashContext context = UnleashContext.builder().userId("1").build(); - UnleashMetricService metricService = mock(UnleashMetricService.class); UnleashConfig config = new UnleashConfig.Builder() .appName("test") @@ -317,21 +314,23 @@ public void getting_variant_when_disabled_should_increment_no_counter() { toggleRepository, Collections.emptyMap(), contextProvider, - new EventDispatcher(config), - metricService); + new EventDispatcher(config)); + UnleashEngineStateHandler localStateHandler = + new UnleashEngineStateHandler((DefaultUnleash) thisUnleash); // Set up a toggleName using UserWithIdStrategy Map params = new HashMap<>(); params.put("userIds", "123, 111, 121, 13"); ActivationStrategy strategy = new ActivationStrategy("userWithId", params); FeatureToggle featureToggle = new FeatureToggle("test", false, asList(strategy)); - when(toggleRepository.getToggle("test")).thenReturn(featureToggle); + localStateHandler.setState(featureToggle); final Variant result = thisUnleash.getVariant("test", context); assertThat(result).isNotNull(); - verify(metricService).count(anyString(), eq(false)); - verify(metricService).countVariant(anyString(), eq(result.getName())); + MetricsBucket bucket = localStateHandler.captureMetrics(); + assertThat(bucket.getToggles().get("test").getYes()).isEqualTo(0); + assertThat(bucket.getToggles().get("test").getNo()).isEqualTo(1); } @Test @@ -365,7 +364,7 @@ public void get_first_variant() { FeatureToggle featureToggle = new FeatureToggle("test", true, asList(strategy), getTestVariants()); - when(toggleRepository.getToggle("test")).thenReturn(featureToggle); + stateHandler.setState(featureToggle); final Variant result = unleash.getVariant("test", context); @@ -386,7 +385,7 @@ public void get_second_variant() { FeatureToggle featureToggle = new FeatureToggle("test", true, asList(strategy), getTestVariants()); - when(toggleRepository.getToggle("test")).thenReturn(featureToggle); + stateHandler.setState(featureToggle); final Variant result = unleash.getVariant("test", context); @@ -444,7 +443,7 @@ public void get_first_variant_with_context_provider() { FeatureToggle featureToggle = new FeatureToggle("test", true, asList(strategy), getTestVariants()); - when(toggleRepository.getToggle("test")).thenReturn(featureToggle); + stateHandler.setState(featureToggle); final Variant result = unleash.getVariant("test"); @@ -467,7 +466,7 @@ public void get_second_variant_with_context_provider() { FeatureToggle featureToggle = new FeatureToggle("test", true, asList(strategy), getTestVariants()); - when(toggleRepository.getToggle("test")).thenReturn(featureToggle); + stateHandler.setState(featureToggle); final Variant result = unleash.getVariant("test"); @@ -477,36 +476,6 @@ public void get_second_variant_with_context_provider() { assertThat(result.isEnabled()).isTrue(); } - @Test - public void get_second_variant_with_context_provider_and_deprecated_algorithm() { - - UnleashContext context = UnleashContext.builder().userId("5").build(); - when(contextProvider.getContext()).thenReturn(context); - - // Set up a toggleName using UserWithIdStrategy - Map params = new HashMap<>(); - params.put("userIds", "123, 5, 121"); - ActivationStrategy strategy = new ActivationStrategy("userWithId", params); - FeatureToggle featureToggle = - new FeatureToggle( - "test", true, asList(strategy), getTestVariantsForDeprecatedHash()); - - when(toggleRepository.getToggle("test")).thenReturn(featureToggle); - - final Variant result = unleash.deprecatedGetVariant("test"); - - assertThat(result).isNotNull(); - assertThat(result.getName()).isEqualTo("en"); - assertThat(result.getPayload().map(Payload::getValue).get()).isEqualTo("en"); - assertThat(result.isEnabled()).isTrue(); - - final Variant newHash = unleash.getVariant("test"); - assertThat(newHash).isNotNull(); - assertThat(newHash.getName()).isEqualTo("to"); - assertThat(newHash.getPayload().map(Payload::getValue).get()).isEqualTo("to"); - assertThat(newHash.isEnabled()).isTrue(); - } - @Test public void should_be_enabled_with_strategy_constraints() { List constraints = new ArrayList<>(); @@ -521,7 +490,7 @@ public void should_be_enabled_with_strategy_constraints() { FeatureToggle featureToggle = new FeatureToggle("test", true, asList(activeStrategy)); - when(toggleRepository.getToggle("test")).thenReturn(featureToggle); + stateHandler.setState(featureToggle); assertThat(unleash.isEnabled("test")).isTrue(); } @@ -550,24 +519,19 @@ public void should_handle_complex_segment_chains() { UnleashConfig config = UnleashConfig.builder() .appName("test") - .unleashAPI("http://http://unleash.org") + .unleashAPI("http://unleash.org") .backupFile( getClass().getResource("/unleash-repo-v2-advanced.json").getFile()) .build(); FeatureBackupHandlerFile backupHandler = new FeatureBackupHandlerFile(config); FeatureCollection featureCollection = backupHandler.read(); - when(toggleRepository.getToggle(anyString())) - .thenReturn(featureCollection.getToggle("Test.variants")); - when(toggleRepository.getSegment(0)).thenReturn(featureCollection.getSegment(0)); - when(toggleRepository.getSegment(1)).thenReturn(featureCollection.getSegment(1)); - when(toggleRepository.getSegment(2)).thenReturn(featureCollection.getSegment(2)); - when(toggleRepository.getSegment(3)).thenReturn(featureCollection.getSegment(3)); + stateHandler.setState(featureCollection); UnleashContext context = UnleashContext.builder() .addProperty("wins", "6") - .addProperty("dateLastWin", "2022-06-01T12:00:00") + .addProperty("dateLastWin", "2022-06-01T12:00:00.000Z") .addProperty("followers", "1500") .addProperty("single", "true") .addProperty("catOrDog", "cat") @@ -589,24 +553,20 @@ public void should_handle_complex_segment_chains_2() { FeatureBackupHandlerFile backupHandler = new FeatureBackupHandlerFile(config); FeatureCollection featureCollection = backupHandler.read(); - when(toggleRepository.getToggle(anyString())) - .thenReturn(featureCollection.getToggle("Test.variants")); - when(toggleRepository.getSegment(0)).thenReturn(featureCollection.getSegment(0)); - when(toggleRepository.getSegment(1)).thenReturn(featureCollection.getSegment(1)); - when(toggleRepository.getSegment(2)).thenReturn(featureCollection.getSegment(2)); - when(toggleRepository.getSegment(3)).thenReturn(featureCollection.getSegment(3)); + Unleash overrideUnleash = new DefaultUnleash(config); + new UnleashEngineStateHandler((DefaultUnleash) overrideUnleash).setState(featureCollection); UnleashContext context = UnleashContext.builder() .addProperty("wins", "4") - .addProperty("dateLastWin", "2022-06-01T12:00:00") + .addProperty("dateLastWin", "2022-06-01T12:00:00.000Z") .addProperty("followers", "900") .addProperty("single", "false") .addProperty("catOrDog", "dog") .build(); when(contextProvider.getContext()).thenReturn(context); - assertThat(unleash.isEnabled("Test.variants")).isFalse(); + assertThat(overrideUnleash.isEnabled("Test.variants")).isFalse(); } @Test diff --git a/src/test/java/io/getunleash/event/ImpressionDataSubscriberTest.java b/src/test/java/io/getunleash/event/ImpressionDataSubscriberTest.java index 48be6d8f8..1cb1a20f8 100644 --- a/src/test/java/io/getunleash/event/ImpressionDataSubscriberTest.java +++ b/src/test/java/io/getunleash/event/ImpressionDataSubscriberTest.java @@ -1,14 +1,12 @@ package io.getunleash.event; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; import io.getunleash.DefaultUnleash; import io.getunleash.FeatureToggle; import io.getunleash.SynchronousTestExecutor; import io.getunleash.Unleash; -import io.getunleash.repository.FeatureRepository; +import io.getunleash.repository.UnleashEngineStateHandler; import io.getunleash.util.UnleashConfig; import io.getunleash.variant.VariantDefinition; import java.util.ArrayList; @@ -20,10 +18,9 @@ public class ImpressionDataSubscriberTest { private ImpressionTestSubscriber testSubscriber = new ImpressionTestSubscriber(); - private FeatureRepository toggleRepository; - private UnleashConfig unleashConfig; + private UnleashEngineStateHandler stateHandler; private Unleash unleash; @BeforeEach @@ -32,22 +29,19 @@ void setup() { new UnleashConfig.Builder() .appName(SubscriberTest.class.getSimpleName()) .instanceId(SubscriberTest.class.getSimpleName()) - .synchronousFetchOnInitialisation(true) .unleashAPI("http://localhost:4242/api") .subscriber(testSubscriber) .scheduledExecutor(new SynchronousTestExecutor()) .build(); - toggleRepository = mock(FeatureRepository.class); - unleash = new DefaultUnleash(unleashConfig, toggleRepository); + unleash = new DefaultUnleash(unleashConfig); + stateHandler = new UnleashEngineStateHandler((DefaultUnleash) unleash); } @Test public void noEventsIfImpressionDataIsNotEnabled() { String featureWithoutImpressionDataEnabled = "feature.with.no.impressionData"; - when(toggleRepository.getToggle(featureWithoutImpressionDataEnabled)) - .thenReturn( - new FeatureToggle( - featureWithoutImpressionDataEnabled, true, new ArrayList<>())); + stateHandler.setState( + new FeatureToggle(featureWithoutImpressionDataEnabled, true, new ArrayList<>())); unleash.isEnabled(featureWithoutImpressionDataEnabled); assertThat(testSubscriber.isEnabledImpressions).isEqualTo(0); assertThat(testSubscriber.variantImpressions).isEqualTo(0); @@ -56,8 +50,8 @@ public void noEventsIfImpressionDataIsNotEnabled() { @Test public void isEnabledEventWhenImpressionDataIsEnabled() { String featureWithImpressionData = "feature.with.impressionData"; - when(toggleRepository.getToggle(featureWithImpressionData)) - .thenReturn( + new UnleashEngineStateHandler((DefaultUnleash) unleash) + .setState( new FeatureToggle( featureWithImpressionData, true, @@ -75,14 +69,9 @@ public void variantEventWhenVariantIsRequested() { VariantDefinition def = new VariantDefinition("blue", 1000, null, null); List variants = new ArrayList<>(); variants.add(def); - when(toggleRepository.getToggle(featureWithImpressionData)) - .thenReturn( - new FeatureToggle( - featureWithImpressionData, - true, - new ArrayList<>(), - variants, - true)); + stateHandler.setState( + new FeatureToggle( + featureWithImpressionData, true, new ArrayList<>(), variants, true)); unleash.getVariant(featureWithImpressionData); assertThat(testSubscriber.isEnabledImpressions).isEqualTo(0); assertThat(testSubscriber.variantImpressions).isEqualTo(1); diff --git a/src/test/java/io/getunleash/integration/ClientSpecificationTest.java b/src/test/java/io/getunleash/integration/ClientSpecificationTest.java index 7a3fc1255..d6cc50c21 100644 --- a/src/test/java/io/getunleash/integration/ClientSpecificationTest.java +++ b/src/test/java/io/getunleash/integration/ClientSpecificationTest.java @@ -17,6 +17,7 @@ import io.getunleash.Unleash; import io.getunleash.UnleashContext; import io.getunleash.Variant; +import io.getunleash.repository.UnleashEngineStateHandler; import io.getunleash.strategy.constraints.DateParser; import io.getunleash.util.UnleashConfig; import java.io.BufferedReader; @@ -119,7 +120,8 @@ private List createVariantTests(String fileName) private Unleash setupUnleash(TestDefinition testDefinition) throws URISyntaxException { mockUnleashAPI(testDefinition); - // Required because the client is available before it may have had the chance to talk with + // Required because the client is available before it may have had the chance to + // talk with // the API String backupFile = writeUnleashBackup(testDefinition); @@ -132,7 +134,10 @@ private Unleash setupUnleash(TestDefinition testDefinition) throws URISyntaxExce .backupFile(backupFile) .build(); - return new DefaultUnleash(config); + DefaultUnleash defaultUnleash = new DefaultUnleash(config); + new UnleashEngineStateHandler(defaultUnleash) + .setState(testDefinition.getState().toString()); + return defaultUnleash; } private void mockUnleashAPI(TestDefinition definition) { @@ -190,7 +195,8 @@ private String writeUnleashBackup(TestDefinition definition) { + definition.getName() + ".json"; - // TODO: we can probably drop this after introduction of `synchronousFetchOnInitialisation`. + // TODO: we can probably drop this after introduction of + // `synchronousFetchOnInitialisation`. try (FileWriter writer = new FileWriter(backupFile)) { writer.write(definition.getState().toString()); } catch (IOException e) { diff --git a/src/test/java/io/getunleash/metric/DefaultHttpMetricsSenderTest.java b/src/test/java/io/getunleash/metric/DefaultHttpMetricsSenderTest.java index f4250a99c..7f8be65e8 100644 --- a/src/test/java/io/getunleash/metric/DefaultHttpMetricsSenderTest.java +++ b/src/test/java/io/getunleash/metric/DefaultHttpMetricsSenderTest.java @@ -11,9 +11,11 @@ import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import com.github.tomakehurst.wiremock.junit5.WireMockExtension; +import io.getunleash.engine.MetricsBucket; import io.getunleash.util.UnleashConfig; import java.net.URI; import java.net.URISyntaxException; +import java.time.Instant; import java.time.LocalDateTime; import java.util.HashSet; import org.junit.jupiter.api.Test; @@ -60,7 +62,7 @@ public void should_send_client_metrics() throws URISyntaxException { UnleashConfig config = UnleashConfig.builder().appName("test-app").unleashAPI(uri).build(); DefaultHttpMetricsSender sender = new DefaultHttpMetricsSender(config); - MetricsBucket bucket = new MetricsBucket(); + MetricsBucket bucket = new MetricsBucket(Instant.now(), Instant.now(), null); ClientMetrics metrics = new ClientMetrics(config, bucket); sender.sendMetrics(metrics); @@ -82,7 +84,7 @@ public void should_handle_service_failure_when_sending_metrics() throws URISynta UnleashConfig config = UnleashConfig.builder().appName("test-app").unleashAPI(uri).build(); DefaultHttpMetricsSender sender = new DefaultHttpMetricsSender(config); - MetricsBucket bucket = new MetricsBucket(); + MetricsBucket bucket = new MetricsBucket(Instant.now(), Instant.now(), null); ClientMetrics metrics = new ClientMetrics(config, bucket); sender.sendMetrics(metrics); diff --git a/src/test/java/io/getunleash/metric/UnleashMetricServiceImplTest.java b/src/test/java/io/getunleash/metric/UnleashMetricServiceImplTest.java index ed5b631ad..ea501186e 100644 --- a/src/test/java/io/getunleash/metric/UnleashMetricServiceImplTest.java +++ b/src/test/java/io/getunleash/metric/UnleashMetricServiceImplTest.java @@ -3,6 +3,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.*; +import io.getunleash.engine.MetricsBucket; +import io.getunleash.engine.UnleashEngine; import io.getunleash.util.UnleashConfig; import io.getunleash.util.UnleashScheduledExecutor; import java.time.LocalDateTime; @@ -23,7 +25,8 @@ public void should_register_future_for_sending_interval_regualry() { .unleashAPI("http://unleash.com") .build(); UnleashScheduledExecutor executor = mock(UnleashScheduledExecutor.class); - UnleashMetricService unleashMetricService = new UnleashMetricServiceImpl(config, executor); + UnleashMetricService unleashMetricService = + new UnleashMetricServiceImpl(config, executor, null); verify(executor, times(1)).setInterval(any(Runnable.class), eq(interval), eq(interval)); } @@ -42,7 +45,7 @@ public void should_register_client() { DefaultHttpMetricsSender sender = mock(DefaultHttpMetricsSender.class); UnleashMetricService unleashMetricService = - new UnleashMetricServiceImpl(config, sender, executor); + new UnleashMetricServiceImpl(config, sender, executor, null); Set strategies = new HashSet<>(); strategies.add("default"); strategies.add("custom"); @@ -74,7 +77,7 @@ public void should_register_client_with_env() { DefaultHttpMetricsSender sender = mock(DefaultHttpMetricsSender.class); UnleashMetricService unleashMetricService = - new UnleashMetricServiceImpl(config, sender, executor); + new UnleashMetricServiceImpl(config, sender, executor, null); Set strategies = new HashSet<>(); strategies.add("default"); strategies.add("custom"); @@ -98,9 +101,10 @@ public void should_send_metrics() { UnleashScheduledExecutor executor = mock(UnleashScheduledExecutor.class); DefaultHttpMetricsSender sender = mock(DefaultHttpMetricsSender.class); + UnleashEngine engine = new UnleashEngine(); UnleashMetricService unleashMetricService = - new UnleashMetricServiceImpl(config, sender, executor); + new UnleashMetricServiceImpl(config, sender, executor, engine); ArgumentCaptor sendMetricsCallback = ArgumentCaptor.forClass(Runnable.class); verify(executor).setInterval(sendMetricsCallback.capture(), anyLong(), anyLong()); @@ -121,13 +125,14 @@ public void should_record_and_send_metrics() { UnleashScheduledExecutor executor = mock(UnleashScheduledExecutor.class); DefaultHttpMetricsSender sender = mock(DefaultHttpMetricsSender.class); + UnleashEngine engine = new UnleashEngine(); UnleashMetricService unleashMetricService = - new UnleashMetricServiceImpl(config, sender, executor); - unleashMetricService.count("someToggle", true); - unleashMetricService.count("someToggle", false); - unleashMetricService.count("someToggle", true); - unleashMetricService.count("otherToggle", true); + new UnleashMetricServiceImpl(config, sender, executor, engine); + engine.countToggle("someToggle", true); + engine.countToggle("someToggle", false); + engine.countToggle("someToggle", true); + engine.countToggle("otherToggle", true); // Call the sendMetricsCallback ArgumentCaptor sendMetricsCallback = ArgumentCaptor.forClass(Runnable.class); @@ -162,14 +167,15 @@ public void should_record_and_send_variant_metrics() { UnleashScheduledExecutor executor = mock(UnleashScheduledExecutor.class); DefaultHttpMetricsSender sender = mock(DefaultHttpMetricsSender.class); + UnleashEngine engine = new UnleashEngine(); UnleashMetricService unleashMetricService = - new UnleashMetricServiceImpl(config, sender, executor); - unleashMetricService.countVariant("someToggle", "v1"); - unleashMetricService.countVariant("someToggle", "v1"); - unleashMetricService.countVariant("someToggle", "v1"); - unleashMetricService.countVariant("someToggle", "v2"); - unleashMetricService.countVariant("someToggle", "disabled"); + new UnleashMetricServiceImpl(config, sender, executor, engine); + engine.countVariant("someToggle", "v1"); + engine.countVariant("someToggle", "v1"); + engine.countVariant("someToggle", "v1"); + engine.countVariant("someToggle", "v2"); + engine.countVariant("someToggle", "disabled"); // Call the sendMetricsCallback ArgumentCaptor sendMetricsCallback = ArgumentCaptor.forClass(Runnable.class); @@ -209,14 +215,15 @@ public void should_backoff_when_told_to_by_429_code() { UnleashScheduledExecutor executor = mock(UnleashScheduledExecutor.class); DefaultHttpMetricsSender sender = mock(DefaultHttpMetricsSender.class); + UnleashEngine engine = new UnleashEngine(); UnleashMetricServiceImpl unleashMetricService = - new UnleashMetricServiceImpl(config, sender, executor); - unleashMetricService.countVariant("someToggle", "v1"); - unleashMetricService.countVariant("someToggle", "v1"); - unleashMetricService.countVariant("someToggle", "v1"); - unleashMetricService.countVariant("someToggle", "v2"); - unleashMetricService.countVariant("someToggle", "disabled"); + new UnleashMetricServiceImpl(config, sender, executor, engine); + engine.countVariant("someToggle", "v1"); + engine.countVariant("someToggle", "v1"); + engine.countVariant("someToggle", "v1"); + engine.countVariant("someToggle", "v2"); + engine.countVariant("someToggle", "disabled"); // Call the sendMetricsCallback ArgumentCaptor sendMetricsCallback = ArgumentCaptor.forClass(Runnable.class); @@ -281,14 +288,15 @@ public void server_errors_should_also_incrementally_backoff() { UnleashScheduledExecutor executor = mock(UnleashScheduledExecutor.class); DefaultHttpMetricsSender sender = mock(DefaultHttpMetricsSender.class); + UnleashEngine engine = new UnleashEngine(); UnleashMetricServiceImpl unleashMetricService = - new UnleashMetricServiceImpl(config, sender, executor); - unleashMetricService.countVariant("someToggle", "v1"); - unleashMetricService.countVariant("someToggle", "v1"); - unleashMetricService.countVariant("someToggle", "v1"); - unleashMetricService.countVariant("someToggle", "v2"); - unleashMetricService.countVariant("someToggle", "disabled"); + new UnleashMetricServiceImpl(config, sender, executor, engine); + engine.countVariant("someToggle", "v1"); + engine.countVariant("someToggle", "v1"); + engine.countVariant("someToggle", "v1"); + engine.countVariant("someToggle", "v2"); + engine.countVariant("someToggle", "disabled"); // Call the sendMetricsCallback ArgumentCaptor sendMetricsCallback = ArgumentCaptor.forClass(Runnable.class); @@ -351,14 +359,15 @@ public void failure_to_authenticate_immediately_increases_interval_to_max() { UnleashScheduledExecutor executor = mock(UnleashScheduledExecutor.class); DefaultHttpMetricsSender sender = mock(DefaultHttpMetricsSender.class); + UnleashEngine engine = new UnleashEngine(); UnleashMetricServiceImpl unleashMetricService = - new UnleashMetricServiceImpl(config, sender, executor); - unleashMetricService.countVariant("someToggle", "v1"); - unleashMetricService.countVariant("someToggle", "v1"); - unleashMetricService.countVariant("someToggle", "v1"); - unleashMetricService.countVariant("someToggle", "v2"); - unleashMetricService.countVariant("someToggle", "disabled"); + new UnleashMetricServiceImpl(config, sender, executor, engine); + engine.countVariant("someToggle", "v1"); + engine.countVariant("someToggle", "v1"); + engine.countVariant("someToggle", "v1"); + engine.countVariant("someToggle", "v2"); + engine.countVariant("someToggle", "disabled"); // Call the sendMetricsCallback ArgumentCaptor sendMetricsCallback = ArgumentCaptor.forClass(Runnable.class); @@ -388,14 +397,15 @@ public void url_not_found_immediately_increases_interval_to_max() { UnleashScheduledExecutor executor = mock(UnleashScheduledExecutor.class); DefaultHttpMetricsSender sender = mock(DefaultHttpMetricsSender.class); + UnleashEngine engine = new UnleashEngine(); UnleashMetricServiceImpl unleashMetricService = - new UnleashMetricServiceImpl(config, sender, executor); - unleashMetricService.countVariant("someToggle", "v1"); - unleashMetricService.countVariant("someToggle", "v1"); - unleashMetricService.countVariant("someToggle", "v1"); - unleashMetricService.countVariant("someToggle", "v2"); - unleashMetricService.countVariant("someToggle", "disabled"); + new UnleashMetricServiceImpl(config, sender, executor, engine); + engine.countVariant("someToggle", "v1"); + engine.countVariant("someToggle", "v1"); + engine.countVariant("someToggle", "v1"); + engine.countVariant("someToggle", "v2"); + engine.countVariant("someToggle", "disabled"); // Call the sendMetricsCallback ArgumentCaptor sendMetricsCallback = ArgumentCaptor.forClass(Runnable.class); @@ -425,9 +435,10 @@ public void should_add_new_metrics_data_to_bucket() { UnleashScheduledExecutor executor = mock(UnleashScheduledExecutor.class); DefaultHttpMetricsSender sender = mock(DefaultHttpMetricsSender.class); + UnleashEngine engine = new UnleashEngine(); UnleashMetricService unleashMetricService = - new UnleashMetricServiceImpl(config, sender, executor); + new UnleashMetricServiceImpl(config, sender, executor, engine); ArgumentCaptor sendMetricsCallback = ArgumentCaptor.forClass(Runnable.class); verify(executor).setInterval(sendMetricsCallback.capture(), anyLong(), anyLong()); diff --git a/src/test/java/io/getunleash/repository/UnleashEngineStateHandler.java b/src/test/java/io/getunleash/repository/UnleashEngineStateHandler.java new file mode 100644 index 000000000..5b7ad6b95 --- /dev/null +++ b/src/test/java/io/getunleash/repository/UnleashEngineStateHandler.java @@ -0,0 +1,62 @@ +package io.getunleash.repository; + +import io.getunleash.DefaultUnleash; +import io.getunleash.FeatureToggle; +import io.getunleash.Segment; +import io.getunleash.engine.*; +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class UnleashEngineStateHandler { + private final UnleashEngine unleashEngine; + + public UnleashEngineStateHandler(DefaultUnleash unleash) { + // Access the private field + Field field = null; + try { + field = DefaultUnleash.class.getDeclaredField("unleashEngine"); + field.setAccessible(true); // Bypass the "private" access modifier + // Get value + unleashEngine = (UnleashEngine) field.get(unleash); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + public void setState(FeatureToggle... featureToggles) { + FeatureCollection madeUp = + new FeatureCollection( + new ToggleCollection(Arrays.asList(featureToggles)), + new SegmentCollection(Collections.emptyList())); + setState(madeUp); + } + + public void setState(List featureToggles, List segments) { + FeatureCollection madeUp = + new FeatureCollection( + new ToggleCollection(featureToggles), new SegmentCollection(segments)); + setState(madeUp); + } + + public void setState(FeatureCollection madeUp) { + setState(JsonFeatureParser.toJsonString(madeUp)); + } + + public MetricsBucket captureMetrics() { + try { + return this.unleashEngine.getMetrics(); + } catch (YggdrasilError e) { + throw new RuntimeException(e); + } + } + + public void setState(String raw) { + try { + this.unleashEngine.takeState(raw); + } catch (YggdrasilInvalidInputException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/test/resources/unleash-repo-v2-advanced.json b/src/test/resources/unleash-repo-v2-advanced.json index 1bd4cf751..4d1ba69d8 100644 --- a/src/test/resources/unleash-repo-v2-advanced.json +++ b/src/test/resources/unleash-repo-v2-advanced.json @@ -16,7 +16,7 @@ { "contextName": "dateLastWin", "operator": "DATE_AFTER", - "value": "2022-05-01T12:00:00", + "value": "2022-05-01T12:00:00.000Z", "inverted": false, "caseInsensitive": true }