Skip to content

Commit d3750bf

Browse files
mrm9084rossgrambo
andauthored
Feature context and Async Filters (#43435)
* Object Context and AsyncFilters * Apply suggestions from code review Co-authored-by: Ross Grambo <rossgrambo@microsoft.com> * Review comments * Review comments * Review Comments --------- Co-authored-by: Ross Grambo <rossgrambo@microsoft.com>
1 parent a6d8ed1 commit d3750bf

File tree

10 files changed

+263
-63
lines changed

10 files changed

+263
-63
lines changed

sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java

Lines changed: 85 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,32 @@
22
// Licensed under the MIT License.
33
package com.azure.spring.cloud.feature.management;
44

5+
import static com.azure.spring.cloud.feature.management.implementation.FeatureManagementConstants.ALL_REQUIREMENT_TYPE;
6+
7+
import java.time.Duration;
8+
import java.util.ArrayList;
59
import java.util.HashSet;
6-
import java.util.Objects;
10+
import java.util.List;
711
import java.util.Set;
8-
import java.util.stream.Stream;
912

1013
import org.slf4j.Logger;
1114
import org.slf4j.LoggerFactory;
1215
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
1316
import org.springframework.context.ApplicationContext;
1417
import org.springframework.util.ReflectionUtils;
1518

19+
import com.azure.spring.cloud.feature.management.filters.ContextualFeatureFilter;
20+
import com.azure.spring.cloud.feature.management.filters.ContextualFeatureFilterAsync;
1621
import com.azure.spring.cloud.feature.management.filters.FeatureFilter;
22+
import com.azure.spring.cloud.feature.management.filters.FeatureFilterAsync;
1723
import com.azure.spring.cloud.feature.management.implementation.FeatureManagementConfigProperties;
1824
import com.azure.spring.cloud.feature.management.implementation.FeatureManagementProperties;
25+
import com.azure.spring.cloud.feature.management.models.Conditions;
1926
import com.azure.spring.cloud.feature.management.models.Feature;
2027
import com.azure.spring.cloud.feature.management.models.FeatureFilterEvaluationContext;
2128
import com.azure.spring.cloud.feature.management.models.FilterNotFoundException;
2229

30+
import reactor.core.publisher.Flux;
2331
import reactor.core.publisher.Mono;
2432

2533
/**
@@ -34,6 +42,8 @@ public class FeatureManager {
3442
private final FeatureManagementProperties featureManagementConfigurations;
3543

3644
private transient FeatureManagementConfigProperties properties;
45+
46+
private static final Duration DEFAULT_REQUEST_TIMEOUT = Duration.ofSeconds(100);
3747

3848
/**
3949
* Can be called to check if a feature is enabled or disabled.
@@ -59,7 +69,7 @@ public class FeatureManager {
5969
* @throws FilterNotFoundException file not found
6070
*/
6171
public Mono<Boolean> isEnabledAsync(String feature) {
62-
return Mono.just(checkFeature(feature));
72+
return checkFeature(feature, null);
6373
}
6474

6575
/**
@@ -72,48 +82,93 @@ public Mono<Boolean> isEnabledAsync(String feature) {
7282
* @throws FilterNotFoundException file not found
7383
*/
7484
public Boolean isEnabled(String feature) throws FilterNotFoundException {
75-
return checkFeature(feature);
85+
return checkFeature(feature, null).block(DEFAULT_REQUEST_TIMEOUT);
86+
}
87+
88+
/**
89+
* Checks to see if the feature is enabled. If enabled it check each filter, once a single filter returns true it
90+
* returns true. If no filter returns true, it returns false. If there are no filters, it returns true. If feature
91+
* isn't found it returns false.
92+
*
93+
* @param feature Feature being checked.
94+
* @param featureContext Local context
95+
* @return state of the feature
96+
* @throws FilterNotFoundException file not found
97+
*/
98+
public Mono<Boolean> isEnabledAsync(String feature, Object featureContext) {
99+
return checkFeature(feature, featureContext);
76100
}
77101

78-
private boolean checkFeature(String featureName) throws FilterNotFoundException {
102+
/**
103+
* Checks to see if the feature is enabled. If enabled it checks each filter, once a single filter returns true it
104+
* returns true. If no filter returns true, it returns false. If there are no filters, it returns true. If feature
105+
* isn't found it returns false.
106+
*
107+
* @param feature Feature being checked.
108+
* @param featureContext Local context
109+
* @return state of the feature
110+
* @throws FilterNotFoundException file not found
111+
*/
112+
public Boolean isEnabled(String feature, Object featureContext) throws FilterNotFoundException {
113+
return checkFeature(feature, featureContext).block(DEFAULT_REQUEST_TIMEOUT);
114+
}
115+
116+
private Mono<Boolean> checkFeature(String featureName, Object featureContext) throws FilterNotFoundException {
79117
Feature featureFlag = featureManagementConfigurations.getFeatureFlags().stream()
80118
.filter(feature -> feature.getId().equals(featureName)).findAny().orElse(null);
81119

82120
if (featureFlag == null) {
83-
return false;
121+
return Mono.just(false);
84122
}
85123

86-
Stream<FeatureFilterEvaluationContext> filters = featureFlag.getConditions().getClientFilters().stream()
87-
.filter(Objects::nonNull).filter(featureFilter -> featureFilter.getName() != null);
88-
89124
if (featureFlag.getConditions().getClientFilters().size() == 0) {
90-
return featureFlag.isEnabled();
91-
}
92-
93-
// All Filters must be true
94-
if (featureFlag.getConditions().getRequirementType().equals("All")) {
95-
return filters.allMatch(featureFilter -> isFeatureOn(featureFilter, featureName));
125+
return Mono.just(featureFlag.isEnabled());
96126
}
97127

98-
// Any Filter must be true
99-
return filters.anyMatch(featureFilter -> isFeatureOn(featureFilter, featureName));
128+
return checkFeatureFilters(featureFlag, featureContext);
100129
}
101130

102-
private boolean isFeatureOn(FeatureFilterEvaluationContext filter, String feature) {
103-
try {
104-
FeatureFilter featureFilter = (FeatureFilter) context.getBean(filter.getName());
105-
filter.setFeatureName(feature);
106-
107-
return featureFilter.evaluate(filter);
108-
} catch (NoSuchBeanDefinitionException e) {
109-
LOGGER.error("Was unable to find Filter {}. Does the class exist and set as an @Component?",
110-
filter.getName());
111-
if (properties.isFailFast()) {
112-
String message = "Fail fast is set and a Filter was unable to be found";
113-
ReflectionUtils.rethrowRuntimeException(new FilterNotFoundException(message, e, filter));
131+
private Mono<Boolean> checkFeatureFilters(Feature featureFlag, Object featureContext) {
132+
Conditions conditions = featureFlag.getConditions();
133+
List<FeatureFilterEvaluationContext> featureFilters = conditions.getClientFilters();
134+
135+
if (featureFilters.size() == 0) {
136+
return Mono.just(true);
137+
}
138+
139+
List<Mono<Boolean>> filterResults = new ArrayList<Mono<Boolean>>();
140+
for (FeatureFilterEvaluationContext featureFilter : featureFilters) {
141+
String filterName = featureFilter.getName();
142+
143+
try {
144+
Object filter = context.getBean(filterName);
145+
featureFilter.setFeatureName(featureFlag.getId());
146+
if (filter instanceof FeatureFilter) {
147+
filterResults.add(Mono.just(((FeatureFilter) filter).evaluate(featureFilter)));
148+
} else if (filter instanceof ContextualFeatureFilter) {
149+
filterResults
150+
.add(Mono.just(((ContextualFeatureFilter) filter).evaluate(featureFilter, featureContext)));
151+
} else if (filter instanceof FeatureFilterAsync) {
152+
filterResults.add(((FeatureFilterAsync) filter).evaluateAsync(featureFilter));
153+
} else if (filter instanceof ContextualFeatureFilterAsync) {
154+
filterResults
155+
.add(((ContextualFeatureFilterAsync) filter).evaluateAsync(featureFilter, featureContext));
156+
}
157+
} catch (NoSuchBeanDefinitionException e) {
158+
LOGGER.error("Was unable to find Filter {}. Does the class exist and set as an @Component?",
159+
filterName);
160+
if (properties.isFailFast()) {
161+
String message = "Fail fast is set and a Filter was unable to be found";
162+
ReflectionUtils.rethrowRuntimeException(new FilterNotFoundException(message, e, featureFilter));
163+
}
114164
}
115165
}
116-
return false;
166+
167+
if (ALL_REQUIREMENT_TYPE.equals(featureFlag.getConditions().getRequirementType())) {
168+
return Flux.merge(filterResults).reduce((a, b) -> a && b).single();
169+
}
170+
// Any Filter must be true
171+
return Flux.merge(filterResults).reduce((a, b) -> a || b).single();
117172
}
118173

119174
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
package com.azure.spring.cloud.feature.management.filters;
4+
5+
import com.azure.spring.cloud.feature.management.models.FeatureFilterEvaluationContext;
6+
7+
/**
8+
* A Filter for Feature Management that is attached to Features. The filter needs to have @Component set to be found by
9+
* feature management. As a Contextual feature filter any context that is passed in to the feature request will be
10+
* passed along to the filter(s).
11+
*/
12+
@FunctionalInterface
13+
public interface ContextualFeatureFilter {
14+
15+
/**
16+
* Evaluates if the filter is on or off. Returning true results in Feature evaluation ending and returning true.
17+
* Returning false results in the next Feature evaluation to continue.
18+
*
19+
* @param context The context for whether or not the filter is passed.
20+
* @param appContext The internal app context
21+
* @return True if the feature is enabled, false otherwise.
22+
*/
23+
boolean evaluate(FeatureFilterEvaluationContext context, Object appContext);
24+
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
package com.azure.spring.cloud.feature.management.filters;
4+
5+
import com.azure.spring.cloud.feature.management.models.FeatureFilterEvaluationContext;
6+
7+
import reactor.core.publisher.Mono;
8+
9+
/**
10+
* A Filter for Feature Management that is attached to Features. The filter needs to have @Component set to be found by
11+
* feature management. As a Contextual feature filter any context that is passed in to the feature request will be
12+
* passed along to the filter(s).
13+
*/
14+
@FunctionalInterface
15+
public interface ContextualFeatureFilterAsync {
16+
17+
/**
18+
* Evaluates if the filter is on or off. Returning true results in Feature evaluation ending and returning true.
19+
* Returning false results in the next Feature evaluation to continue.
20+
*
21+
* @param context The context for whether or not the filter is passed.
22+
* @param appContext The internal app context
23+
* @return true if the feature is enabled, false otherwise.
24+
*/
25+
Mono<Boolean> evaluateAsync(FeatureFilterEvaluationContext context, Object appContext);
26+
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
package com.azure.spring.cloud.feature.management.filters;
4+
5+
import com.azure.spring.cloud.feature.management.models.FeatureFilterEvaluationContext;
6+
7+
import reactor.core.publisher.Mono;
8+
9+
/**
10+
* A Filter for Feature Management that is attached to Features. The filter needs to have @Component set to be found by
11+
* feature management.
12+
*/
13+
@FunctionalInterface
14+
public interface FeatureFilterAsync {
15+
16+
/**
17+
* Evaluates if the filter is on or off. Returning true results in Feature evaluation ending and returning true.
18+
* Returning false results in the next Feature evaluation to continue.
19+
*
20+
* @param context The context for whether or not the filter is passed.
21+
* @return True if the feature is enabled, false otherwise.
22+
*/
23+
Mono<Boolean> evaluateAsync(FeatureFilterEvaluationContext context);
24+
25+
}

0 commit comments

Comments
 (0)