1 package io.getunleash;
2
3 import static io.getunleash.Variant.DISABLED_VARIANT;
4 import static java.util.Optional.ofNullable;
5
6 import io.getunleash.event.*;
7 import io.getunleash.lang.Nullable;
8 import io.getunleash.metric.UnleashMetricService;
9 import io.getunleash.metric.UnleashMetricServiceImpl;
10 import io.getunleash.repository.FeatureRepository;
11 import io.getunleash.repository.IFeatureRepository;
12 import io.getunleash.strategy.*;
13 import io.getunleash.util.ConstraintMerger;
14 import io.getunleash.util.UnleashConfig;
15 import io.getunleash.variant.VariantUtil;
16 import java.util.*;
17 import java.util.concurrent.ConcurrentHashMap;
18 import java.util.concurrent.atomic.LongAdder;
19 import java.util.function.BiPredicate;
20 import java.util.stream.Collectors;
21 import javax.annotation.Nonnull;
22 import org.slf4j.Logger;
23 import org.slf4j.LoggerFactory;
24
25 public class DefaultUnleash implements Unleash {
26     private static final Logger LOGGER = LoggerFactory.getLogger(DefaultUnleash.class);
27
28     private static ConcurrentHashMap<String, LongAdder> initCounts = new ConcurrentHashMap<>();
29     private static final List<Strategy> BUILTIN_STRATEGIES =
30             Arrays.asList(
31                     new DefaultStrategy(),
32                     new ApplicationHostnameStrategy(),
33                     new GradualRolloutRandomStrategy(),
34                     new GradualRolloutSessionIdStrategy(),
35                     new GradualRolloutUserIdStrategy(),
36                     new RemoteAddressStrategy(),
37                     new UserWithIdStrategy(),
38                     new FlexibleRolloutStrategy());
39
40     public static final UnknownStrategy UNKNOWN_STRATEGY = new UnknownStrategy();
41
42     private final UnleashMetricService metricService;
43     private final IFeatureRepository featureRepository;
44     private final Map<String, Strategy> strategyMap;
45     private final UnleashContextProvider contextProvider;
46     private final EventDispatcher eventDispatcher;
47     private final UnleashConfig config;
48
49     private static IFeatureRepository defaultToggleRepository(UnleashConfig unleashConfig) {
50         return new FeatureRepository(unleashConfig);
51     }
52
53     public DefaultUnleash(UnleashConfig unleashConfig, Strategy... strategies) {
54         this(unleashConfig, defaultToggleRepository(unleashConfig), strategies);
55     }
56
57     public DefaultUnleash(
58             UnleashConfig unleashConfig,
59             IFeatureRepository featureRepository,
60             Strategy... strategies) {
61         this(
62                 unleashConfig,
63                 featureRepository,
64                 buildStrategyMap(strategies),
65                 unleashConfig.getContextProvider(),
66                 new EventDispatcher(unleashConfig),
67                 new UnleashMetricServiceImpl(unleashConfig, unleashConfig.getScheduledExecutor()),
68                 false);
69     }
70
71     // Visible for testing
72     public DefaultUnleash(
73             UnleashConfig unleashConfig,
74             IFeatureRepository featureRepository,
75             Map<String, Strategy> strategyMap,
76             UnleashContextProvider contextProvider,
77             EventDispatcher eventDispatcher,
78             UnleashMetricService metricService) {
79         this(
80                 unleashConfig,
81                 featureRepository,
82                 strategyMap,
83                 contextProvider,
84                 eventDispatcher,
85                 metricService,
86                 false);
87     }
88
89     public DefaultUnleash(
90             UnleashConfig unleashConfig,
91             IFeatureRepository featureRepository,
92             Map<String, Strategy> strategyMap,
93             UnleashContextProvider contextProvider,
94             EventDispatcher eventDispatcher,
95             UnleashMetricService metricService,
96             boolean failOnMultipleInstantiations) {
97         this.config = unleashConfig;
98         this.featureRepository = featureRepository;
99         this.strategyMap = strategyMap;
100         this.contextProvider = contextProvider;
101         this.eventDispatcher = eventDispatcher;
102         this.metricService = metricService;
103         metricService.register(strategyMap.keySet());
104         initCounts.compute(
105                 config.getClientIdentifier(),
106                 (key, inits) -> {
107                     if (inits != null) {
108                         String error =
109                                 String.format(
110                                         "You already have %d clients for AppName [%s] with instanceId: [%s] running. Please double check your code where you are instantiating the Unleash SDK",
111                                         inits.sum(),
112                                         unleashConfig.getAppName(),
113                                         unleashConfig.getInstanceId());
114                         if (failOnMultipleInstantiations) {
115                             throw new RuntimeException(error);
116                         } else {
117                             LOGGER.error(error);
118                         }
119                     }
120                     LongAdder a = inits == null ? new LongAdder() : inits;
121                     a.increment();
122                     return a;
123                 });
124     }
125
126     @Override
127     public boolean isEnabled(final String toggleName, final boolean defaultSetting) {
128         return isEnabled(toggleName, contextProvider.getContext(), defaultSetting);
129     }
130
131     @Override
132     public boolean isEnabled(
133             final String toggleName, final BiPredicate<String, UnleashContext> fallbackAction) {
134         return isEnabled(toggleName, contextProvider.getContext(), fallbackAction);
135     }
136
137     @Override
138     public boolean isEnabled(
139             String toggleName,
140             UnleashContext context,
141             BiPredicate<String, UnleashContext> fallbackAction) {
142         return isEnabled(toggleName, context, fallbackAction, false);
143     }
144
145     public boolean isEnabled(
146             String toggleName,
147             UnleashContext context,
148             BiPredicate<String, UnleashContext> fallbackAction,
149             boolean isParent) {
150         FeatureEvaluationResult result =
151                 getFeatureEvaluationResult(toggleName, context, fallbackAction, null);
152         if (!isParent) {
153             count(toggleName, result.isEnabled());
154         }
155         eventDispatcher.dispatch(new ToggleEvaluated(toggleName, result.isEnabled()));
156         dispatchEnabledImpressionDataIfNeeded("isEnabled", toggleName, result.isEnabled(), context);
157         return result.isEnabled();
158     }
159
160     private void dispatchEnabledImpressionDataIfNeeded(
161             String eventType, String toggleName, boolean enabled, UnleashContext context) {
162         FeatureToggle toggle = featureRepository.getToggle(toggleName);
163         if (toggle != null && toggle.hasImpressionData()) {
164             eventDispatcher.dispatch(new IsEnabledImpressionEvent(toggleName, enabled, context));
165         }
166     }
167
168     private FeatureEvaluationResult getFeatureEvaluationResult(
169             String toggleName,
170             UnleashContext context,
171             BiPredicate<String, UnleashContext> fallbackAction,
172             @Nullable Variant defaultVariant) {
173         checkIfToggleMatchesNamePrefix(toggleName);
174         FeatureToggle featureToggle = featureRepository.getToggle(toggleName);
175
176         UnleashContext enhancedContext = context.applyStaticFields(config);
177         if (featureToggle == null) {
178             return new FeatureEvaluationResult(
179                     fallbackAction.test(toggleName, enhancedContext), defaultVariant);
180         } else if (!featureToggle.isEnabled()) {
181             return new FeatureEvaluationResult(false, defaultVariant);
182         } else if (isParentDependencySatisfied(featureToggle, context, fallbackAction)) {
183             // Dependent toggles, no point in evaluating child strategies if our dependencies are
184             // not satisfied
185             if (featureToggle.getStrategies().isEmpty()) {
186                 return new FeatureEvaluationResult(
187                         true, VariantUtil.selectVariant(featureToggle, context, defaultVariant));
188             }
189             for (ActivationStrategy strategy : featureToggle.getStrategies()) {
190                 Strategy configuredStrategy = getStrategy(strategy.getName());
191                 if (configuredStrategy == UNKNOWN_STRATEGY) {
192                     LOGGER.warn(
193                             "Unable to find matching strategy for toggle:{} strategy:{}",
194                             toggleName,
195                             strategy.getName());
196                 }
197
198                 FeatureEvaluationResult result =
199                         configuredStrategy.getResult(
200                                 strategy.getParameters(),
201                                 enhancedContext,
202                                 ConstraintMerger.mergeConstraints(featureRepository, strategy),
203                                 strategy.getVariants());
204
205                 if (result.isEnabled()) {
206                     Variant variant = result.getVariant();
207                     // If strategy variant is null, look for a variant in the featureToggle
208                     if (variant == null) {
209                         variant = VariantUtil.selectVariant(featureToggle, context, defaultVariant);
210                     }
211                     result.setVariant(variant);
212                     return result;
213                 }
214             }
215         }
216         return new FeatureEvaluationResult(false, defaultVariant);
217     }
218
219     /**
220      * Uses the old, statistically broken Variant seed for finding the correct variant
221      *
222      * @param toggleName Name of the toggle
223      * @param context The UnleashContext
224      * @param fallbackAction What to do if we fail to find the toggle
225      * @param defaultVariant If we can't resolve a variant, what are we returning
226      * @return A wrapper containing whether the feature was enabled as well which Variant was
227      *     selected
228      * @deprecated
229      */

230     private FeatureEvaluationResult deprecatedGetFeatureEvaluationResult(
231             String toggleName,
232             UnleashContext context,
233             BiPredicate<String, UnleashContext> fallbackAction,
234             @Nullable Variant defaultVariant) {
235         checkIfToggleMatchesNamePrefix(toggleName);
236         FeatureToggle featureToggle = featureRepository.getToggle(toggleName);
237
238         UnleashContext enhancedContext = context.applyStaticFields(config);
239         if (featureToggle == null) {
240             return new FeatureEvaluationResult(
241                     fallbackAction.test(toggleName, enhancedContext), defaultVariant);
242         } else if (!featureToggle.isEnabled()) {
243             return new FeatureEvaluationResult(false, defaultVariant);
244         } else if (isParentDependencySatisfied(featureToggle, context, fallbackAction)) {
245             if (featureToggle.getStrategies().isEmpty()) {
246                 return new FeatureEvaluationResult(
247                         true,
248                         VariantUtil.selectDeprecatedVariantHashingAlgo(
249                                 featureToggle, context, defaultVariant));
250             }
251             for (ActivationStrategy strategy : featureToggle.getStrategies()) {
252                 Strategy configuredStrategy = getStrategy(strategy.getName());
253                 if (configuredStrategy == UNKNOWN_STRATEGY) {
254                     LOGGER.warn(
255                             "Unable to find matching strategy for toggle:{} strategy:{}",
256                             toggleName,
257                             strategy.getName());
258                 }
259
260                 FeatureEvaluationResult result =
261                         configuredStrategy.getDeprecatedHashingAlgoResult(
262                                 strategy.getParameters(),
263                                 enhancedContext,
264                                 ConstraintMerger.mergeConstraints(featureRepository, strategy),
265                                 strategy.getVariants());
266
267                 if (result.isEnabled()) {
268                     Variant variant = result.getVariant();
269                     // If strategy variant is null, look for a variant in the featureToggle
270                     if (variant == null) {
271                         variant =
272                                 VariantUtil.selectDeprecatedVariantHashingAlgo(
273                                         featureToggle, context, defaultVariant);
274                     }
275                     result.setVariant(variant);
276                     return result;
277                 }
278             }
279         }
280         return new FeatureEvaluationResult(false, defaultVariant);
281     }
282
283     private boolean isParentDependencySatisfied(
284             @Nonnull FeatureToggle featureToggle,
285             @Nonnull UnleashContext context,
286             BiPredicate<String, UnleashContext> fallbackAction) {
287         if (!featureToggle.hasDependencies()) {
288             return true;
289         } else {
290             return featureToggle.getDependencies().stream()
291                     .allMatch(
292                             parent -> {
293                                 FeatureToggle parentToggle =
294                                         featureRepository.getToggle(parent.getFeature());
295                                 if (parentToggle == null) {
296                                     LOGGER.warn(
297                                             "Missing dependency [{}] for toggle: [{}]",
298                                             parent.getFeature(),
299                                             featureToggle.getName());
300                                     return false;
301                                 }
302                                 if (!parentToggle.getDependencies().isEmpty()) {
303                                     LOGGER.warn(
304                                             "[{}] depends on feature [{}] which also depends on something. We don't currently support more than one level of dependency resolution",
305                                             featureToggle.getName(),
306                                             parent.getFeature());
307                                     return false;
308                                 }
309                                 boolean parentSatisfied =
310                                         isEnabled(
311                                                 parent.getFeature(), context, fallbackAction, true);
312                                 if (parentSatisfied) {
313                                     if (!parent.getVariants().isEmpty()) {
314                                         return parent.getVariants()
315                                                 .contains(
316                                                         getVariant(
317                                                                         parent.feature,
318                                                                         context,
319                                                                         DISABLED_VARIANT,
320                                                                         true)
321                                                                 .getName());
322                                     } else {
323                                         return parent.isEnabled();
324                                     }
325                                 } else {
326                                     return !parent.isEnabled();
327                                 }
328                             });
329         }
330     }
331
332     private void checkIfToggleMatchesNamePrefix(String toggleName) {
333         if (config.getNamePrefix() != null) {
334             if (!toggleName.startsWith(config.getNamePrefix())) {
335                 LOGGER.warn(
336                         "Toggle [{}] doesnt start with configured name prefix of [{}] so it will always be disabled",
337                         toggleName,
338                         config.getNamePrefix());
339             }
340         }
341     }
342
343     @Override
344     public Variant getVariant(String toggleName, UnleashContext context) {
345         return getVariant(toggleName, context, DISABLED_VARIANT);
346     }
347
348     @Override
349     public Variant getVariant(String toggleName, UnleashContext context, Variant defaultValue) {
350         return getVariant(toggleName, context, defaultValue, false);
351     }
352
353     private Variant getVariant(
354             String toggleName, UnleashContext context, Variant defaultValue, boolean isParent) {
355         FeatureEvaluationResult result =
356                 getFeatureEvaluationResult(toggleName, context, (n, c) -> false, defaultValue);
357         Variant variant = result.getVariant();
358         if (!isParent) {
359             metricService.countVariant(toggleName, variant.getName());
360             // Should count yes/no also when getting variant.
361             metricService.count(toggleName, result.isEnabled());
362         }
363         dispatchVariantImpressionDataIfNeeded(
364                 toggleName, variant.getName(), result.isEnabled(), context);
365         return variant;
366     }
367
368     private void dispatchVariantImpressionDataIfNeeded(
369             String toggleName, String variantName, boolean enabled, UnleashContext context) {
370         FeatureToggle toggle = featureRepository.getToggle(toggleName);
371         if (toggle != null && toggle.hasImpressionData()) {
372             eventDispatcher.dispatch(
373                     new VariantImpressionEvent(toggleName, enabled, context, variantName));
374         }
375     }
376
377     @Override
378     public Variant getVariant(String toggleName) {
379         return getVariant(toggleName, contextProvider.getContext());
380     }
381
382     @Override
383     public Variant getVariant(String toggleName, Variant defaultValue) {
384         return getVariant(toggleName, contextProvider.getContext(), defaultValue);
385     }
386
387     /**
388      * Uses the old, statistically broken Variant seed for finding the correct variant
389      *
390      * @param toggleName
391      * @param context
392      * @return
393      * @deprecated
394      */

395     @Override
396     public Variant deprecatedGetVariant(String toggleName, UnleashContext context) {
397         return deprecatedGetVariant(toggleName, context, DISABLED_VARIANT);
398     }
399
400     /**
401      * Uses the old, statistically broken Variant seed for finding the correct variant
402      *
403      * @param toggleName
404      * @param context
405      * @param defaultValue
406      * @return
407      * @deprecated
408      */

409     @Override
410     public Variant deprecatedGetVariant(
411             String toggleName, UnleashContext context, Variant defaultValue) {
412         return deprecatedGetVariant(toggleName, context, defaultValue, false);
413     }
414
415     private Variant deprecatedGetVariant(
416             String toggleName, UnleashContext context, Variant defaultValue, boolean isParent) {
417         FeatureEvaluationResult result =
418                 deprecatedGetFeatureEvaluationResult(
419                         toggleName, context, (n, c) -> false, defaultValue);
420         Variant variant = result.getVariant();
421         if (!isParent) {
422             metricService.countVariant(toggleName, variant.getName());
423             // Should count yes/no also when getting variant.
424             metricService.count(toggleName, result.isEnabled());
425         }
426         dispatchVariantImpressionDataIfNeeded(
427                 toggleName, variant.getName(), result.isEnabled(), context);
428         return variant;
429     }
430
431     /**
432      * Uses the old, statistically broken Variant seed for finding the correct variant
433      *
434      * @param toggleName
435      * @return
436      * @deprecated
437      */

438     @Override
439     public Variant deprecatedGetVariant(String toggleName) {
440         return deprecatedGetVariant(toggleName, contextProvider.getContext());
441     }
442
443     /**
444      * Uses the old, statistically broken Variant seed for finding the correct variant
445      *
446      * @param toggleName
447      * @param defaultValue
448      * @return
449      * @deprecated
450      */

451     @Override
452     public Variant deprecatedGetVariant(String toggleName, Variant defaultValue) {
453         return deprecatedGetVariant(toggleName, contextProvider.getContext(), defaultValue);
454     }
455
456     /**
457      * Use more().getFeatureToggleDefinition() instead
458      *
459      * @return the feature toggle
460      */

461     @Deprecated
462     public Optional<FeatureToggle> getFeatureToggleDefinition(String toggleName) {
463         return ofNullable(featureRepository.getToggle(toggleName));
464     }
465
466     /**
467      * Use more().getFeatureToggleNames() instead
468      *
469      * @return a list of known toggle names
470      */

471     @Deprecated()
472     public List<String> getFeatureToggleNames() {
473         return featureRepository.getFeatureNames();
474     }
475
476     /** Use more().count() instead */
477     @Deprecated
478     public void count(final String toggleName, boolean enabled) {
479         metricService.count(toggleName, enabled);
480     }
481
482     private static Map<String, Strategy> buildStrategyMap(@Nullable Strategy[] strategies) {
483         Map<String, Strategy> map = new HashMap<>();
484
485         BUILTIN_STRATEGIES.forEach(strategy -> map.put(strategy.getName(), strategy));
486
487         if (strategies != null) {
488             for (Strategy strategy : strategies) {
489                 map.put(strategy.getName(), strategy);
490             }
491         }
492
493         return map;
494     }
495
496     private Strategy getStrategy(String strategy) {
497         return strategyMap.getOrDefault(strategy, config.getFallbackStrategy());
498     }
499
500     @Override
501     public void shutdown() {
502         config.getScheduledExecutor().shutdown();
503     }
504
505     @Override
506     public MoreOperations more() {
507         return new DefaultMore();
508     }
509
510     public class DefaultMore implements MoreOperations {
511
512         @Override
513         public List<String> getFeatureToggleNames() {
514             return featureRepository.getFeatureNames();
515         }
516
517         @Override
518         public Optional<FeatureToggle> getFeatureToggleDefinition(String toggleName) {
519             return ofNullable(featureRepository.getToggle(toggleName));
520         }
521
522         @Override
523         public List<EvaluatedToggle> evaluateAllToggles() {
524             return evaluateAllToggles(contextProvider.getContext());
525         }
526
527         @Override
528         public List<EvaluatedToggle> evaluateAllToggles(UnleashContext context) {
529             return getFeatureToggleNames().stream()
530                     .map(
531                             toggleName -> {
532                                 FeatureEvaluationResult result =
533                                         getFeatureEvaluationResult(
534                                                 toggleName, context, (n, c) -> falsenull);
535
536                                 return new EvaluatedToggle(
537                                         toggleName, result.isEnabled(), result.getVariant());
538                             })
539                     .collect(Collectors.toList());
540         }
541
542         @Override
543         public void count(final String toggleName, boolean enabled) {
544             metricService.count(toggleName, enabled);
545         }
546
547         @Override
548         public void countVariant(final String toggleName, String variantName) {
549             metricService.countVariant(toggleName, variantName);
550         }
551     }
552 }
553