All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.getunleash.DefaultUnleash Maven / Gradle / Ivy

There is a newer version: 9.2.4
Show newest version
package io.getunleash;

import static io.getunleash.Variant.DISABLED_VARIANT;
import static java.util.Optional.ofNullable;

import io.getunleash.event.*;
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.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;

public class DefaultUnleash implements Unleash {
    private static final Logger LOGGER = LoggerFactory.getLogger(DefaultUnleash.class);

    private static ConcurrentHashMap initCounts = new ConcurrentHashMap<>();
    private static final List BUILTIN_STRATEGIES =
            Arrays.asList(
                    new DefaultStrategy(),
                    new ApplicationHostnameStrategy(),
                    new GradualRolloutRandomStrategy(),
                    new GradualRolloutSessionIdStrategy(),
                    new GradualRolloutUserIdStrategy(),
                    new RemoteAddressStrategy(),
                    new UserWithIdStrategy(),
                    new FlexibleRolloutStrategy());

    public static final UnknownStrategy UNKNOWN_STRATEGY = new UnknownStrategy();

    private final UnleashMetricService metricService;
    private final IFeatureRepository featureRepository;
    private final Map strategyMap;
    private final UnleashContextProvider contextProvider;
    private final EventDispatcher eventDispatcher;
    private final UnleashConfig config;

    private static IFeatureRepository defaultToggleRepository(UnleashConfig unleashConfig) {
        return new FeatureRepository(unleashConfig);
    }

    public DefaultUnleash(UnleashConfig unleashConfig, Strategy... strategies) {
        this(unleashConfig, defaultToggleRepository(unleashConfig), strategies);
    }

    public DefaultUnleash(
            UnleashConfig unleashConfig,
            IFeatureRepository featureRepository,
            Strategy... strategies) {
        this(
                unleashConfig,
                featureRepository,
                buildStrategyMap(strategies),
                unleashConfig.getContextProvider(),
                new EventDispatcher(unleashConfig),
                new UnleashMetricServiceImpl(unleashConfig, unleashConfig.getScheduledExecutor()),
                false);
    }

    // Visible for testing
    public DefaultUnleash(
            UnleashConfig unleashConfig,
            IFeatureRepository featureRepository,
            Map strategyMap,
            UnleashContextProvider contextProvider,
            EventDispatcher eventDispatcher,
            UnleashMetricService metricService) {
        this(
                unleashConfig,
                featureRepository,
                strategyMap,
                contextProvider,
                eventDispatcher,
                metricService,
                false);
    }

    public DefaultUnleash(
            UnleashConfig unleashConfig,
            IFeatureRepository featureRepository,
            Map strategyMap,
            UnleashContextProvider contextProvider,
            EventDispatcher eventDispatcher,
            UnleashMetricService metricService,
            boolean failOnMultipleInstantiations) {
        this.config = unleashConfig;
        this.featureRepository = featureRepository;
        this.strategyMap = strategyMap;
        this.contextProvider = contextProvider;
        this.eventDispatcher = eventDispatcher;
        this.metricService = metricService;
        metricService.register(strategyMap.keySet());
        initCounts.compute(
                config.getClientIdentifier(),
                (key, inits) -> {
                    if (inits != null) {
                        String error =
                                String.format(
                                        "You already have %d clients for AppName [%s] with instanceId: [%s] running. Please double check your code where you are instantiating the Unleash SDK",
                                        inits.sum(),
                                        unleashConfig.getAppName(),
                                        unleashConfig.getInstanceId());
                        if (failOnMultipleInstantiations) {
                            throw new RuntimeException(error);
                        } else {
                            LOGGER.error(error);
                        }
                    }
                    LongAdder a = inits == null ? new LongAdder() : inits;
                    a.increment();
                    return a;
                });
    }

    @Override
    public boolean isEnabled(final String toggleName, final boolean defaultSetting) {
        return isEnabled(toggleName, contextProvider.getContext(), defaultSetting);
    }

    @Override
    public boolean isEnabled(
            final String toggleName, final BiPredicate fallbackAction) {
        return isEnabled(toggleName, contextProvider.getContext(), fallbackAction);
    }

    @Override
    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 (featureToggle.getStrategies().isEmpty()) {
            return new FeatureEvaluationResult(
                    true, VariantUtil.selectVariant(featureToggle, context, defaultVariant));
        } else {
            // Dependent toggles, no point in evaluating child strategies if our dependencies are
            // not satisfied
            if (isParentDependencySatisfied(featureToggle, context, fallbackAction)) {
                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
     *
     * @deprecated
     * @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
     */
    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 (featureToggle.getStrategies().isEmpty()) {
            return new FeatureEvaluationResult(
                    true,
                    VariantUtil.selectDeprecatedVariantHashingAlgo(
                            featureToggle, context, defaultVariant));
        } else {
            // Dependent toggles, no point in evaluating child strategies if our dependencies are
            // not satisfied
            if (isParentDependencySatisfied(featureToggle, context, fallbackAction)) {
                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;
                    }
                }
            }
            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());
            }
        }
    }

    @Override
    public Variant getVariant(String toggleName, UnleashContext context) {
        return getVariant(toggleName, context, DISABLED_VARIANT);
    }

    @Override
    public Variant getVariant(String toggleName, UnleashContext context, Variant defaultValue) {
        return getVariant(toggleName, context, defaultValue, false);
    }

    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;
    }

    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));
        }
    }

    @Override
    public Variant getVariant(String toggleName) {
        return getVariant(toggleName, contextProvider.getContext());
    }

    @Override
    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
     *
     * @deprecated
     * @param toggleName
     * @param context
     * @return
     */
    @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
     *
     * @deprecated
     * @param toggleName
     * @param context
     * @param defaultValue
     * @return
     */
    @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
     *
     * @deprecated
     * @param toggleName
     * @return
     */
    @Override
    public Variant deprecatedGetVariant(String toggleName) {
        return deprecatedGetVariant(toggleName, contextProvider.getContext());
    }

    /**
     * Uses the old, statistically broken Variant seed for finding the correct variant
     *
     * @deprecated
     * @param toggleName
     * @param defaultValue
     * @return
     */
    @Override
    public Variant deprecatedGetVariant(String toggleName, Variant defaultValue) {
        return deprecatedGetVariant(toggleName, contextProvider.getContext(), defaultValue);
    }

    /**
     * Use more().getFeatureToggleDefinition() instead
     *
     * @return the feature toggle
     */
    @Deprecated
    public Optional getFeatureToggleDefinition(String toggleName) {
        return ofNullable(featureRepository.getToggle(toggleName));
    }

    /**
     * Use more().getFeatureToggleNames() instead
     *
     * @return a list of known toggle names
     */
    @Deprecated()
    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<>();

        BUILTIN_STRATEGIES.forEach(strategy -> map.put(strategy.getName(), strategy));

        if (strategies != null) {
            for (Strategy strategy : strategies) {
                map.put(strategy.getName(), strategy);
            }
        }

        return map;
    }

    private Strategy getStrategy(String strategy) {
        return strategyMap.getOrDefault(strategy, config.getFallbackStrategy());
    }

    @Override
    public void shutdown() {
        config.getScheduledExecutor().shutdown();
    }

    @Override
    public MoreOperations more() {
        return new DefaultMore();
    }

    public class DefaultMore implements MoreOperations {

        @Override
        public List getFeatureToggleNames() {
            return featureRepository.getFeatureNames();
        }

        @Override
        public Optional getFeatureToggleDefinition(String toggleName) {
            return ofNullable(featureRepository.getToggle(toggleName));
        }

        @Override
        public List evaluateAllToggles() {
            return evaluateAllToggles(contextProvider.getContext());
        }

        @Override
        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());
                            })
                    .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);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy