Skip to content

Commit

Permalink
feat: Handle dependent feature toggles
Browse files Browse the repository at this point in the history
- A new feature added in beta to Unleash 5.6. With dependent feature toggle you
  can configure parent-child relationships. This change makes the SDK
  evaluate according to the client specifications defined for this.
  • Loading branch information
chriswk committed Oct 12, 2023
1 parent 54bf433 commit 86c0cb1
Show file tree
Hide file tree
Showing 7 changed files with 185 additions and 35 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
6 changes: 5 additions & 1 deletion src/main/java/io/getunleash/ActivationStrategy.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import io.getunleash.lang.Nullable;
import io.getunleash.variant.VariantDefinition;

import javax.annotation.Nonnull;
import java.util.*;

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

@Nonnull
public List<VariantDefinition> getVariants() {
return variants;

return variants;
}
}
96 changes: 73 additions & 23 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 @@ -168,40 +169,89 @@ 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);
}
}

//
// Checks a feature's dependencies (if it has any
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)
.getName());
}
return isEnabled(parent.getFeature(), context, fallbackAction);
} else {
return !isEnabled(parent.getFeature(), context, fallbackAction);
}
});
}
}

private void checkIfToggleMatchesNamePrefix(String toggleName) {
if (config.getNamePrefix() != null) {
if (!toggleName.startsWith(config.getNamePrefix())) {
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
19 changes: 15 additions & 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 Expand Up @@ -275,4 +272,18 @@ public void client_identifier_handles_api_key_being_null() {
assertThat(id)
.isEqualTo("f83eb743f4c8dc41294aafb96f454763e5a90b96db8b7040ddc505d636bdb243");
}

/* @Test
public child_yields_a_disabled_variant_if_the_parents_variant_does_not_match_the_required_one() {
FeatureFetcher fetcher = mock(FeatureFetcher.class);
HashMap<String, String> childParameters = new HashMap<>();
childParameters.put("rollout", "100");
childParameters.put("stickiness", "default");
childParameters.put("groupId", "groupId");
FeatureToggle child = new FeatureToggle("parent.non.matching.variant.child.enabled", true, Collections.singletonList(new ActivationStrategy("flexibleRollout", childParameters)), Collections.singletonList(new VariantDefinition("child.variant", 1, new Payload("string", "variantValue"), Collections.emptyList())), false, Collections.singletonList(new FeatureDependency("parent.with.variant", true, asList("nonmatching.variant"))))
FeatureToggle parent = new FeatureToggle("parent.with.variant", true, Arrays.asList)
FeatureCollection expectedResponse = new FeatureCollection();
expectedResponse.add
}*/
}

0 comments on commit 86c0cb1

Please sign in to comment.