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
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
184
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
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
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
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
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
395 @Override
396 public Variant deprecatedGetVariant(String toggleName, UnleashContext context) {
397 return deprecatedGetVariant(toggleName, context, DISABLED_VARIANT);
398 }
399
400
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
424 metricService.count(toggleName, result.isEnabled());
425 }
426 dispatchVariantImpressionDataIfNeeded(
427 toggleName, variant.getName(), result.isEnabled(), context);
428 return variant;
429 }
430
431
438 @Override
439 public Variant deprecatedGetVariant(String toggleName) {
440 return deprecatedGetVariant(toggleName, contextProvider.getContext());
441 }
442
443
451 @Override
452 public Variant deprecatedGetVariant(String toggleName, Variant defaultValue) {
453 return deprecatedGetVariant(toggleName, contextProvider.getContext(), defaultValue);
454 }
455
456
461 @Deprecated
462 public Optional<FeatureToggle> getFeatureToggleDefinition(String toggleName) {
463 return ofNullable(featureRepository.getToggle(toggleName));
464 }
465
466
471 @Deprecated()
472 public List<String> getFeatureToggleNames() {
473 return featureRepository.getFeatureNames();
474 }
475
476
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) -> false, null);
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