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

io.split.client.SplitClientImpl Maven / Gradle / Ivy

package io.split.client;

import io.split.client.api.Key;
import io.split.client.api.SplitResult;
import io.split.client.dtos.Event;
import io.split.client.events.EventsStorageProducer;
import io.split.client.impressions.Impression;
import io.split.client.impressions.ImpressionsManager;
import io.split.client.interceptors.FlagSetsFilter;
import io.split.engine.SDKReadinessGates;
import io.split.engine.evaluator.Evaluator;
import io.split.engine.evaluator.EvaluatorImp;
import io.split.engine.evaluator.Labels;
import io.split.grammar.Treatments;
import io.split.inputValidation.EventsValidator;
import io.split.inputValidation.KeyValidator;
import io.split.inputValidation.SplitNameValidator;
import io.split.inputValidation.TrafficTypeValidator;
import io.split.storages.SplitCacheConsumer;
import io.split.telemetry.domain.enums.MethodEnum;
import io.split.telemetry.storage.TelemetryConfigProducer;
import io.split.telemetry.storage.TelemetryEvaluationProducer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static com.google.common.base.Preconditions.checkNotNull;
import static io.split.inputValidation.FlagSetsValidator.cleanup;

/**
 * A basic implementation of SplitClient.
 *
 * @author adil
 */
public final class SplitClientImpl implements SplitClient {
    public static final SplitResult SPLIT_RESULT_CONTROL = new SplitResult(Treatments.CONTROL, null);
    private static final String CLIENT_DESTROY = "Client has already been destroyed - no calls possible";
    private static final String CATCHALL_EXCEPTION = "CatchAll Exception";
    private static final String MATCHING_KEY = "matchingKey";

    private static final Logger _log = LoggerFactory.getLogger(SplitClientImpl.class);

    private final SplitFactory _container;
    private final SplitCacheConsumer _splitCacheConsumer;
    private final ImpressionsManager _impressionManager;
    private final SplitClientConfig _config;
    private final EventsStorageProducer _eventsStorageProducer;
    private final SDKReadinessGates _gates;
    private final Evaluator _evaluator;
    private final TelemetryEvaluationProducer _telemetryEvaluationProducer;
    private final TelemetryConfigProducer _telemetryConfigProducer;
    private final FlagSetsFilter _flagSetsFilter;

    public SplitClientImpl(SplitFactory container,
                           SplitCacheConsumer splitCacheConsumer,
                           ImpressionsManager impressionManager,
                           EventsStorageProducer eventsStorageProducer,
                           SplitClientConfig config,
                           SDKReadinessGates gates,
                           Evaluator evaluator,
                           TelemetryEvaluationProducer telemetryEvaluationProducer,
                           TelemetryConfigProducer telemetryConfigProducer,
                           FlagSetsFilter flagSetsFilter) {
        _container = container;
        _splitCacheConsumer = checkNotNull(splitCacheConsumer);
        _impressionManager = checkNotNull(impressionManager);
        _eventsStorageProducer = eventsStorageProducer;
        _config = config;
        _gates = checkNotNull(gates);
        _evaluator = checkNotNull(evaluator);
        _telemetryEvaluationProducer = checkNotNull(telemetryEvaluationProducer);
        _telemetryConfigProducer = checkNotNull(telemetryConfigProducer);
        _flagSetsFilter = flagSetsFilter;
    }

    @Override
    public String getTreatment(String key, String featureFlagName) {
        return getTreatment(key, featureFlagName, Collections.emptyMap());
    }

    @Override
    public String getTreatment(String key, String featureFlagName, Map attributes) {
        return getTreatmentWithConfigInternal(key, null, featureFlagName, attributes, MethodEnum.TREATMENT).treatment();
    }

    @Override
    public String getTreatment(Key key, String featureFlagName, Map attributes) {
        return getTreatmentWithConfigInternal(key.matchingKey(), key.bucketingKey(), featureFlagName, attributes, MethodEnum.TREATMENT).treatment();
    }

    @Override
    public SplitResult getTreatmentWithConfig(String key, String featureFlagName) {
        return getTreatmentWithConfigInternal(key, null, featureFlagName, Collections.emptyMap(), MethodEnum.TREATMENT_WITH_CONFIG);
    }

    @Override
    public SplitResult getTreatmentWithConfig(String key, String featureFlagName, Map attributes) {
        return getTreatmentWithConfigInternal(key, null, featureFlagName, attributes, MethodEnum.TREATMENT_WITH_CONFIG);
    }

    @Override
    public SplitResult getTreatmentWithConfig(Key key, String featureFlagName, Map attributes) {
        return getTreatmentWithConfigInternal(key.matchingKey(), key.bucketingKey(), featureFlagName, attributes, MethodEnum.TREATMENT_WITH_CONFIG);
    }

    @Override
    public Map getTreatments(String key, List featureFlagNames) {
        return getTreatments(key, featureFlagNames, Collections.emptyMap());
    }

    @Override
    public Map getTreatments(String key, List featureFlagNames, Map attributes) {
        return getTreatmentsWithConfigInternal(key, null, featureFlagNames, attributes, MethodEnum.TREATMENTS)
                .entrySet().stream()
                .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().treatment()));
    }

    @Override
    public Map getTreatments(Key key, List featureFlagNames, Map attributes) {
        return getTreatmentsWithConfigInternal(key.matchingKey(), key.bucketingKey(), featureFlagNames, attributes, MethodEnum.TREATMENTS)
                .entrySet().stream()
                .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().treatment()));
    }

    @Override
    public Map getTreatmentsWithConfig(String key, List featureFlagNames) {
        return getTreatmentsWithConfigInternal(key, null, featureFlagNames, Collections.emptyMap(),
                MethodEnum.TREATMENTS_WITH_CONFIG);
    }

    @Override
    public Map getTreatmentsWithConfig(String key, List featureFlagNames, Map attributes) {
        return getTreatmentsWithConfigInternal(key, null, featureFlagNames, attributes, MethodEnum.TREATMENTS_WITH_CONFIG);
    }

    @Override
    public Map getTreatmentsWithConfig(Key key, List featureFlagNames, Map attributes) {
        return getTreatmentsWithConfigInternal(key.matchingKey(), key.bucketingKey(), featureFlagNames, attributes,
                MethodEnum.TREATMENTS_WITH_CONFIG);
    }

    @Override
    public Map getTreatmentsByFlagSet(String key, String flagSet) {
        return getTreatmentsBySetsWithConfigInternal(key, null, new ArrayList<>(Arrays.asList(flagSet)),
                null, MethodEnum.TREATMENTS_BY_FLAG_SET).entrySet().stream()
                .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().treatment()));
    }

    @Override
    public Map getTreatmentsByFlagSet(String key, String flagSet, Map attributes) {
        return getTreatmentsBySetsWithConfigInternal(key, null, new ArrayList<>(Arrays.asList(flagSet)),
                attributes, MethodEnum.TREATMENTS_BY_FLAG_SET).entrySet().stream()
                .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().treatment()));
    }

    @Override
    public Map getTreatmentsByFlagSet(Key key, String flagSet, Map attributes) {
        return getTreatmentsBySetsWithConfigInternal(key.matchingKey(), key.bucketingKey(), new ArrayList<>(Arrays.asList(flagSet)),
                attributes, MethodEnum.TREATMENTS_BY_FLAG_SET).entrySet().stream()
                .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().treatment()));
    }

    @Override
    public Map getTreatmentsByFlagSets(String key, List flagSets) {
        return getTreatmentsBySetsWithConfigInternal(key, null, flagSets,
                null, MethodEnum.TREATMENTS_BY_FLAG_SETS).entrySet().stream()
                .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().treatment()));
    }

    @Override
    public Map getTreatmentsByFlagSets(String key, List flagSets, Map attributes) {
        return getTreatmentsBySetsWithConfigInternal(key, null, flagSets,
                attributes, MethodEnum.TREATMENTS_BY_FLAG_SETS).entrySet().stream()
                .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().treatment()));
    }

    @Override
    public Map getTreatmentsByFlagSets(Key key, List flagSets, Map attributes) {
        return getTreatmentsBySetsWithConfigInternal(key.matchingKey(), key.bucketingKey(), flagSets,
                attributes, MethodEnum.TREATMENTS_BY_FLAG_SETS).entrySet().stream()
                .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().treatment()));
    }

    @Override
    public Map getTreatmentsWithConfigByFlagSet(String key, String flagSet) {
        return getTreatmentsBySetsWithConfigInternal(key, null, new ArrayList<>(Arrays.asList(flagSet)),
                null, MethodEnum.TREATMENTS_WITH_CONFIG_BY_FLAG_SET);
    }

    @Override
    public Map getTreatmentsWithConfigByFlagSet(String key, String flagSet, Map attributes) {
        return getTreatmentsBySetsWithConfigInternal(key, null, new ArrayList<>(Arrays.asList(flagSet)),
                attributes, MethodEnum.TREATMENTS_WITH_CONFIG_BY_FLAG_SET);
    }

    @Override
    public Map getTreatmentsWithConfigByFlagSet(Key key, String flagSet, Map attributes) {
        return getTreatmentsBySetsWithConfigInternal(key.matchingKey(), key.bucketingKey(), new ArrayList<>(Arrays.asList(flagSet)),
                attributes, MethodEnum.TREATMENTS_WITH_CONFIG_BY_FLAG_SET);
    }

    @Override
    public Map getTreatmentsWithConfigByFlagSets(String key, List flagSets) {
        return getTreatmentsBySetsWithConfigInternal(key, null, flagSets,
                null, MethodEnum.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS);
    }

    @Override
    public Map getTreatmentsWithConfigByFlagSets(String key, List flagSets, Map attributes) {
        return getTreatmentsBySetsWithConfigInternal(key, null, flagSets,
                attributes, MethodEnum.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS);
    }

    @Override
    public Map getTreatmentsWithConfigByFlagSets(Key key, List flagSets, Map attributes) {
        return getTreatmentsBySetsWithConfigInternal(key.matchingKey(), key.bucketingKey(), flagSets,
                attributes, MethodEnum.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS);
    }

    @Override
    public boolean track(String key, String trafficType, String eventType) {
        Event event = createEvent(key, trafficType, eventType);
        return track(event);
    }

    @Override
    public boolean track(String key, String trafficType, String eventType, double value) {
        Event event = createEvent(key, trafficType, eventType);
        event.value = value;

        return track(event);
    }

    @Override
    public boolean track(String key, String trafficType, String eventType, Map properties) {
        Event event = createEvent(key, trafficType, eventType);
        event.properties = new HashMap<>(properties);
        return track(event);
    }

    @Override
    public boolean track(String key, String trafficType, String eventType, double value, Map properties) {
        Event event = createEvent(key, trafficType, eventType);
        event.properties = new HashMap<>(properties);
        event.value = value;
        return track(event);
    }

    @Override
    public void blockUntilReady() throws TimeoutException, InterruptedException {
        long startTime = System.currentTimeMillis();
        if (_config.blockUntilReady() <= 0) {
            throw new IllegalArgumentException("setBlockUntilReadyTimeout must be positive but in config was: " + _config.blockUntilReady());
        }
        if (!_gates.waitUntilInternalReady(_config.blockUntilReady())) {
            throw new TimeoutException("SDK was not ready in " + _config.blockUntilReady() + " milliseconds");
        }
        _log.debug(String.format("Split SDK ready in %d ms", (System.currentTimeMillis() - startTime)));
    }

    @Override
    public void destroy() {
        _container.destroy();
    }

    private boolean track(Event event) {
        long initTime = System.currentTimeMillis();
        if (_container.isDestroyed()) {
            _log.error(CLIENT_DESTROY);
            return false;
        }

        // Traffic Type validations
        Optional trafficTypeResult = TrafficTypeValidator.isValid(event.trafficTypeName, _splitCacheConsumer, "track");
        if (!trafficTypeResult.isPresent()) {
            return false;
        }
        event.trafficTypeName = trafficTypeResult.get();

        // EventType validations
        if (!EventsValidator.typeIsValid(event.eventTypeId, "track")) {
            return false;
        }

        // Key Validations
        if (!KeyValidator.isValid(event.key, "key", _config.maxStringLength(), "track")) {
            return false;
        }

        // Properties validations
        EventsValidator.EventValidatorResult propertiesResult = EventsValidator.propertiesAreValid(event.properties);
        if (!propertiesResult.getSuccess()) {
            return false;
        }

        event.properties = propertiesResult.getValue();
        _telemetryEvaluationProducer.recordLatency(MethodEnum.TRACK, System.currentTimeMillis() - initTime);

        return _eventsStorageProducer.track(event, propertiesResult.getEventSize());
    }

    private SplitResult getTreatmentWithConfigInternal(String matchingKey, String bucketingKey, String featureFlag, Map attributes, MethodEnum methodEnum) {
        long initTime = System.currentTimeMillis();
        try {
            checkSDKReady(methodEnum, Arrays.asList(featureFlag));

            if (_container.isDestroyed()) {
                _log.error(CLIENT_DESTROY);
                return SPLIT_RESULT_CONTROL;
            }

            if (!KeyValidator.isValid(matchingKey, MATCHING_KEY, _config.maxStringLength(), methodEnum.getMethod())) {
                return SPLIT_RESULT_CONTROL;
            }

            if (!KeyValidator.bucketingKeyIsValid(bucketingKey, _config.maxStringLength(), methodEnum.getMethod())) {
                return SPLIT_RESULT_CONTROL;
            }

            Optional splitNameResult = SplitNameValidator.isValid(featureFlag, methodEnum.getMethod());
            if (!splitNameResult.isPresent()) {
                return SPLIT_RESULT_CONTROL;
            }
            featureFlag = splitNameResult.get();

            long start = System.currentTimeMillis();

            EvaluatorImp.TreatmentLabelAndChangeNumber result = _evaluator.evaluateFeature(matchingKey, bucketingKey, featureFlag, attributes);

            if (result.treatment.equals(Treatments.CONTROL) && result.label.equals(Labels.DEFINITION_NOT_FOUND) && _gates.isSDKReady()) {
                _log.warn(String.format(
                        "%s: you passed \"%s\" that does not exist in this environment, " +
                                "please double check what feature flags exist in the Split user interface.", methodEnum.getMethod(), featureFlag));
                return SPLIT_RESULT_CONTROL;
            }

            recordStats(
                    matchingKey,
                    bucketingKey,
                    featureFlag,
                    start,
                    result.treatment,
                    String.format("sdk.%s", methodEnum.getMethod()),
                    _config.labelsEnabled() ? result.label : null,
                    result.changeNumber,
                    attributes
            );
            _telemetryEvaluationProducer.recordLatency(methodEnum, System.currentTimeMillis() - initTime);
            return new SplitResult(result.treatment, result.configurations);
        } catch (Exception e) {
            try {
                _telemetryEvaluationProducer.recordException(methodEnum);
                _log.error(CATCHALL_EXCEPTION, e);
            } catch (Exception e1) {
                // ignore
            }
            return SPLIT_RESULT_CONTROL;
        }
    }

    private Map getTreatmentsWithConfigInternal(String matchingKey, String bucketingKey, List featureFlagNames,
                                                                     Map attributes, MethodEnum methodEnum) {
        long initTime = System.currentTimeMillis();
        if (featureFlagNames == null) {
            _log.error(String.format("%s: featureFlagNames must be a non-empty array", methodEnum.getMethod()));
            return new HashMap<>();
        }
        try {
            checkSDKReady(methodEnum, featureFlagNames);
            Map result = validateBeforeEvaluate(featureFlagNames, matchingKey, methodEnum, bucketingKey);
            if(result != null) {
                return result;
            }
            featureFlagNames = SplitNameValidator.areValid(featureFlagNames, methodEnum.getMethod());
            Map evaluatorResult = _evaluator.evaluateFeatures(matchingKey,
                    bucketingKey, featureFlagNames, attributes);
            return processEvaluatorResult(evaluatorResult, methodEnum, matchingKey, bucketingKey, attributes, initTime);
        } catch (Exception e) {
            try {
                _telemetryEvaluationProducer.recordException(methodEnum);
                _log.error(CATCHALL_EXCEPTION, e);
            } catch (Exception e1) {
                // ignore
            }
            return createMapControl(featureFlagNames);
        }
    }

    private Map getTreatmentsBySetsWithConfigInternal(String matchingKey, String bucketingKey,
                                                                           List sets, Map attributes, MethodEnum methodEnum) {

        long initTime = System.currentTimeMillis();
        if (sets == null || sets.isEmpty()) {
            _log.warn(String.format("%s: sets must be a non-empty array", methodEnum.getMethod()));
            return new HashMap<>();
        }
        Set cleanFlagSets = cleanup(sets);
        cleanFlagSets = filterSetsAreInConfig(cleanFlagSets, methodEnum);
        if (cleanFlagSets.isEmpty()) {
            return new HashMap<>();
        }
        List featureFlagNames = new ArrayList<>();
        try {
            checkSDKReady(methodEnum);
            Map result = validateBeforeEvaluateByFlagSets(matchingKey, methodEnum,bucketingKey);
            if(result != null) {
                return result;
            }
            Map evaluatorResult = _evaluator.evaluateFeaturesByFlagSets(matchingKey,
                    bucketingKey, new ArrayList<>(cleanFlagSets), attributes);
            return processEvaluatorResult(evaluatorResult, methodEnum, matchingKey, bucketingKey, attributes, initTime);
        } catch (Exception e) {
            try {
                _telemetryEvaluationProducer.recordException(methodEnum);
                _log.error(CATCHALL_EXCEPTION, e);
            } catch (Exception e1) {
                // ignore
            }
            return createMapControl(featureFlagNames);
        }
    }
    private Map processEvaluatorResult(Map evaluatorResult,
                                                            MethodEnum methodEnum, String matchingKey, String bucketingKey, Map attributes, long initTime){
        List impressions = new ArrayList<>();
        Map result = new HashMap<>();
        evaluatorResult.keySet().forEach(t -> {
            if (evaluatorResult.get(t).treatment.equals(Treatments.CONTROL) && evaluatorResult.get(t).label.
                    equals(Labels.DEFINITION_NOT_FOUND) && _gates.isSDKReady()) {
                _log.warn(String.format("%s: you passed \"%s\" that does not exist in this environment please double check " +
                        "what feature flags exist in the Split user interface.", methodEnum.getMethod(), t));
                result.put(t, SPLIT_RESULT_CONTROL);
            } else {
                result.put(t, new SplitResult(evaluatorResult.get(t).treatment, evaluatorResult.get(t).configurations));
                impressions.add(new Impression(matchingKey, bucketingKey, t, evaluatorResult.get(t).treatment, System.currentTimeMillis(),
                        evaluatorResult.get(t).label, evaluatorResult.get(t).changeNumber, attributes));
            }
        });
        _telemetryEvaluationProducer.recordLatency(methodEnum, System.currentTimeMillis() - initTime);
        if (impressions.size() > 0) {
            _impressionManager.track(impressions);
        }
        return result;
    }

    private Map validateBeforeEvaluateByFlagSets(String matchingKey, MethodEnum methodEnum,
                                                            String bucketingKey) {
        if (_container.isDestroyed()) {
            _log.error(CLIENT_DESTROY);
            return new HashMap<>();
        }
        if (!KeyValidator.isValid(matchingKey, MATCHING_KEY, _config.maxStringLength(), methodEnum.getMethod())) {
            return new HashMap<>();
        }
        if (!KeyValidator.bucketingKeyIsValid(bucketingKey, _config.maxStringLength(), methodEnum.getMethod())) {
            return new HashMap<>();
        }
        return null;
    }

    private Map validateBeforeEvaluate(List featureFlagNames, String matchingKey, MethodEnum methodEnum,
                                                            String bucketingKey) {
        if (_container.isDestroyed()) {
            _log.error(CLIENT_DESTROY);
            return createMapControl(featureFlagNames);
        }
        if (!KeyValidator.isValid(matchingKey, MATCHING_KEY, _config.maxStringLength(), methodEnum.getMethod())) {
            return createMapControl(featureFlagNames);
        }
        if (!KeyValidator.bucketingKeyIsValid(bucketingKey, _config.maxStringLength(), methodEnum.getMethod())) {
            return createMapControl(featureFlagNames);
        } else if (featureFlagNames.isEmpty()) {
            _log.error(String.format("%s: featureFlagNames must be a non-empty array", methodEnum.getMethod()));
            return new HashMap<>();
        }
        return null;
    }
    private Set filterSetsAreInConfig(Set sets, MethodEnum methodEnum) {
        Set setsToReturn = new HashSet<>();
        for (String set : sets) {
            if (!_flagSetsFilter.intersect(set)) {
                _log.warn(String.format("%s: you passed %s which is not part of the configured FlagSetsFilter, " +
                        "ignoring Flag Set.", methodEnum, set));
                continue;
            }
            setsToReturn.add(set);
        }
        return setsToReturn;
    }
    private void recordStats(String matchingKey, String bucketingKey, String featureFlagName, long start, String result,
                             String operation, String label, Long changeNumber, Map attributes) {
        try {
            _impressionManager.track(Stream.of(new Impression(matchingKey, bucketingKey, featureFlagName, result, System.currentTimeMillis(),
                    label, changeNumber, attributes)).collect(Collectors.toList()));
        } catch (Throwable t) {
            _log.error("Exception", t);
        }
    }

    private Event createEvent(String key, String trafficType, String eventType) {
        Event event = new Event();
        event.eventTypeId = eventType;
        event.trafficTypeName = trafficType;
        event.key = key;
        event.timestamp = System.currentTimeMillis();
        return event;
    }

    private void checkSDKReady(MethodEnum methodEnum, List featureFlagNames) {
        String toPrint =  featureFlagNames.size() == 1 ? featureFlagNames.get(0): String.join(",", featureFlagNames);
        if (!_gates.isSDKReady()) {
            _log.warn(String.format("%s: the SDK is not ready, results may be incorrect for feature flag %s. Make sure to wait for " +
                            "SDK readiness before using this method", methodEnum.getMethod(), toPrint));
            _telemetryConfigProducer.recordNonReadyUsage();
        }
    }

    private void checkSDKReady(MethodEnum methodEnum) {
        if (!_gates.isSDKReady()) {
            _log.warn(String.format("%s: the SDK is not ready, results may be incorrect. Make sure to wait for " +
                    "SDK readiness before using this method", methodEnum.getMethod()));
            _telemetryConfigProducer.recordNonReadyUsage();
        }
    }

    private Map createMapControl(List featureFlags) {
        Map result = new HashMap<>();
        featureFlags.forEach(s -> result.put(s, SPLIT_RESULT_CONTROL));
        return result;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy