Skip to content

Commit

Permalink
feat: Handle dependent feature toggles (#218)
Browse files Browse the repository at this point in the history
* feat: Handle dependent feature toggles
  • Loading branch information
Christopher Kolstad authored Oct 12, 2023
1 parent def11ea commit f74e252
Show file tree
Hide file tree
Showing 8 changed files with 343 additions and 38 deletions.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<version.junit5>5.9.0</version.junit5>
<version.okhttp>4.10.0</version.okhttp>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<version.unleash.specification>4.3.0</version.unleash.specification>
<version.unleash.specification>4.5.1</version.unleash.specification>
<arguments />
<version.jackson>2.14.0</version.jackson>
</properties>
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/io/getunleash/ActivationStrategy.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import io.getunleash.lang.Nullable;
import io.getunleash.variant.VariantDefinition;
import java.util.*;
import javax.annotation.Nonnull;

public final class ActivationStrategy {
private final String name;
Expand Down Expand Up @@ -49,7 +50,9 @@ public List<Constraint> getConstraints() {
return constraints;
}

@Nonnull
public List<VariantDefinition> getVariants() {

return variants;
}
}
125 changes: 98 additions & 27 deletions src/main/java/io/getunleash/DefaultUnleash.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
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;

Expand Down Expand Up @@ -138,9 +139,19 @@ public boolean isEnabled(
String toggleName,
UnleashContext context,
BiPredicate<String, UnleashContext> fallbackAction) {
return isEnabled(toggleName, context, fallbackAction, false);
}

public boolean isEnabled(
String toggleName,
UnleashContext context,
BiPredicate<String, UnleashContext> fallbackAction,
boolean isParent) {
FeatureEvaluationResult result =
getFeatureEvaluationResult(toggleName, context, fallbackAction, null);
count(toggleName, result.isEnabled());
if (!isParent) {
count(toggleName, result.isEnabled());
}
eventDispatcher.dispatch(new ToggleEvaluated(toggleName, result.isEnabled()));
dispatchEnabledImpressionDataIfNeeded("isEnabled", toggleName, result.isEnabled(), context);
return result.isEnabled();
Expand Down Expand Up @@ -168,40 +179,93 @@ private FeatureEvaluationResult getFeatureEvaluationResult(
fallbackAction.test(toggleName, enhancedContext), defaultVariant);
} else if (!featureToggle.isEnabled()) {
return new FeatureEvaluationResult(false, defaultVariant);
} else if (featureToggle.getStrategies().size() == 0) {
} else if (featureToggle.getStrategies().isEmpty()) {
return new FeatureEvaluationResult(
true, VariantUtil.selectVariant(featureToggle, context, defaultVariant));
} else {
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());
}
// 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);
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;
}
result.setVariant(variant);
return result;
}
}
return new FeatureEvaluationResult(false, defaultVariant);
}
}

private boolean isParentDependencySatisfied(
@Nonnull FeatureToggle featureToggle,
@Nonnull UnleashContext context,
BiPredicate<String, UnleashContext> 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;
}
if (parent.isEnabled()) {
if (!parent.getVariants().isEmpty()) {
return parent.getVariants()
.contains(
getVariant(
parent.feature,
context,
DISABLED_VARIANT,
true)
.getName());
}
return isEnabled(
parent.getFeature(), context, fallbackAction, true);
} else {
return !isEnabled(
parent.getFeature(), context, fallbackAction, true);
}
});
}
}

private void checkIfToggleMatchesNamePrefix(String toggleName) {
if (config.getNamePrefix() != null) {
if (!toggleName.startsWith(config.getNamePrefix())) {
Expand All @@ -220,12 +284,19 @@ public Variant getVariant(String toggleName, UnleashContext context) {

@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();
metricService.countVariant(toggleName, variant.getName());
// Should count yes/no also when getting variant.
metricService.count(toggleName, result.isEnabled());
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;
Expand Down
51 changes: 51 additions & 0 deletions src/main/java/io/getunleash/FeatureDependency.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package io.getunleash;

import io.getunleash.lang.Nullable;
import java.util.Collections;
import java.util.List;
import javax.annotation.Nonnull;

public class FeatureDependency {
public String feature;
@Nullable public Boolean enabled;
@Nullable public List<String> variants;

public FeatureDependency(String feature) {
this.feature = feature;
}

public FeatureDependency(
String feature, @Nullable Boolean enabled, @Nullable List<String> variants) {
this.feature = feature;
this.enabled = enabled;
this.variants = variants;
}

public String getFeature() {
return feature;
}

public void setFeature(String feature) {
this.feature = feature;
}

public boolean isEnabled() {
return enabled == null || enabled; // Default value here should be true
}

public void setEnabled(@Nullable Boolean enabled) {
this.enabled = enabled;
}

@Nonnull
public List<String> getVariants() {
if (variants != null) {
return variants;
}
return Collections.emptyList();
}

public void setVariants(@Nullable List<String> variants) {
this.variants = variants;
}
}
44 changes: 38 additions & 6 deletions src/main/java/io/getunleash/FeatureToggle.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import io.getunleash.variant.VariantDefinition;
import java.util.Collections;
import java.util.List;
import javax.annotation.Nonnull;

public final class FeatureToggle {
private final String name;
Expand All @@ -14,16 +15,18 @@ public final class FeatureToggle {
@Nullable private final List<VariantDefinition> variants;
private final boolean impressionData;

@Nullable private final List<FeatureDependency> dependencies;

public FeatureToggle(String name, boolean enabled, List<ActivationStrategy> strategies) {
this(name, enabled, strategies, emptyList(), false);
this(name, enabled, strategies, emptyList(), false, emptyList());
}

public FeatureToggle(
String name,
boolean enabled,
List<ActivationStrategy> strategies,
List<VariantDefinition> variants) {
this(name, enabled, strategies, variants, false);
this(name, enabled, strategies, variants, false, emptyList());
}

public FeatureToggle(
Expand All @@ -32,11 +35,22 @@ public FeatureToggle(
List<ActivationStrategy> strategies,
@Nullable List<VariantDefinition> variants,
@Nullable Boolean impressionData) {
this(name, enabled, strategies, variants, impressionData, emptyList());
}

public FeatureToggle(
String name,
boolean enabled,
List<ActivationStrategy> strategies,
@Nullable List<VariantDefinition> variants,
@Nullable Boolean impressionData,
@Nullable List<FeatureDependency> dependencies) {
this.name = name;
this.enabled = enabled;
this.strategies = strategies;
this.variants = variants;
this.impressionData = impressionData != null ? impressionData : false;
this.dependencies = dependencies;
}

public String getName() {
Expand All @@ -47,10 +61,15 @@ public boolean isEnabled() {
return enabled;
}

@Nonnull
public List<ActivationStrategy> getStrategies() {
if (strategies == null) {
return Collections.emptyList();
}
return this.strategies;
}

@Nonnull
public List<VariantDefinition> getVariants() {
if (variants == null) {
return Collections.emptyList();
Expand All @@ -59,6 +78,19 @@ public List<VariantDefinition> getVariants() {
}
}

@Nonnull
public List<FeatureDependency> getDependencies() {
if (dependencies == null) {
return Collections.emptyList();
} else {
return dependencies;
}
}

public boolean hasDependencies() {
return dependencies != null && !dependencies.isEmpty();
}

@Nullable
public boolean hasImpressionData() {
return impressionData;
Expand All @@ -72,14 +104,14 @@ public String toString() {
+ '\''
+ ", enabled="
+ enabled
+ ", strategies='"
+ ", strategies="
+ strategies
+ '\''
+ ", variants='"
+ ", variants="
+ variants
+ ", impressionData="
+ impressionData
+ '\''
+ ", dependencies="
+ dependencies
+ '}';
}
}
2 changes: 2 additions & 0 deletions src/main/java/io/getunleash/variant/VariantUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
import io.getunleash.Variant;
import io.getunleash.lang.Nullable;
import io.getunleash.strategy.StrategyUtils;
import java.awt.*;
import java.util.*;
import java.util.List;
import java.util.function.Predicate;

public final class VariantUtil {
Expand Down
5 changes: 1 addition & 4 deletions src/test/java/io/getunleash/DefaultUnleashTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,7 @@
import io.getunleash.strategy.DefaultStrategy;
import io.getunleash.strategy.Strategy;
import io.getunleash.util.UnleashConfig;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.slf4j.LoggerFactory;
Expand Down
Loading

0 comments on commit f74e252

Please sign in to comment.