Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature context and Async Filters #43435

Open
wants to merge 1 commit into
base: SpringCloudAzure6.0-Preview
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,32 @@
// Licensed under the MIT License.
package com.azure.spring.cloud.feature.management;

import static com.azure.spring.cloud.feature.management.implementation.FeatureManagementConstants.ALL_REQUIREMENT_TYPE;

import java.time.Duration;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Objects;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.util.ReflectionUtils;

import com.azure.spring.cloud.feature.management.filters.ContextualFeatureFilter;
import com.azure.spring.cloud.feature.management.filters.ContextualFeatureFilterAsync;
import com.azure.spring.cloud.feature.management.filters.FeatureFilter;
import com.azure.spring.cloud.feature.management.filters.FeatureFilterAsync;
import com.azure.spring.cloud.feature.management.implementation.FeatureManagementConfigProperties;
import com.azure.spring.cloud.feature.management.implementation.FeatureManagementProperties;
import com.azure.spring.cloud.feature.management.models.Conditions;
import com.azure.spring.cloud.feature.management.models.Feature;
import com.azure.spring.cloud.feature.management.models.FeatureFilterEvaluationContext;
import com.azure.spring.cloud.feature.management.models.FilterNotFoundException;

import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

/**
Expand Down Expand Up @@ -59,7 +67,7 @@ public class FeatureManager {
* @throws FilterNotFoundException file not found
*/
public Mono<Boolean> isEnabledAsync(String feature) {
return Mono.just(checkFeature(feature));
return checkFeature(feature, null);
}

/**
Expand All @@ -72,48 +80,94 @@ public Mono<Boolean> isEnabledAsync(String feature) {
* @throws FilterNotFoundException file not found
*/
public Boolean isEnabled(String feature) throws FilterNotFoundException {
return checkFeature(feature);
return checkFeature(feature, null).block(Duration.ofSeconds(100));
}

/**
* Checks to see if the feature is enabled. If enabled it check each filter, once a single filter returns true it
* returns true. If no filter returns true, it returns false. If there are no filters, it returns true. If feature
* isn't found it returns false.
*
* @param feature Feature being checked.
* @param featureContext Local context
* @return state of the feature
* @throws FilterNotFoundException file not found
*/
public Mono<Boolean> isEnabledAsync(String feature, Object featureContext) {
return checkFeature(feature, featureContext);
}

/**
* Checks to see if the feature is enabled. If enabled it check each filter, once a single filter returns true it
* returns true. If no filter returns true, it returns false. If there are no filters, it returns true. If feature
* isn't found it returns false.
*
* @param feature Feature being checked.
* @param featureContext Local context
* @return state of the feature
* @throws FilterNotFoundException file not found
*/
public Boolean isEnabled(String feature, Object featureContext) throws FilterNotFoundException {
return checkFeature(feature, featureContext).block(Duration.ofSeconds(100));
}

private boolean checkFeature(String featureName) throws FilterNotFoundException {
private Mono<Boolean> checkFeature(String featureName, Object featureContext) throws FilterNotFoundException {
Feature featureFlag = featureManagementConfigurations.getFeatureFlags().stream()
.filter(feature -> feature.getId().equals(featureName)).findAny().orElse(null);

if (featureFlag == null) {
return false;
return Mono.just(false);
}

Stream<FeatureFilterEvaluationContext> filters = featureFlag.getConditions().getClientFilters().stream()
.filter(Objects::nonNull).filter(featureFilter -> featureFilter.getName() != null);

if (featureFlag.getConditions().getClientFilters().size() == 0) {
return featureFlag.isEnabled();
}

// All Filters must be true
if (featureFlag.getConditions().getRequirementType().equals("All")) {
return filters.allMatch(featureFilter -> isFeatureOn(featureFilter, featureName));
return Mono.just(featureFlag.isEnabled());
}

// Any Filter must be true
return filters.anyMatch(featureFilter -> isFeatureOn(featureFilter, featureName));
return checkFeatureFilters(featureFlag, featureContext);
}

private boolean isFeatureOn(FeatureFilterEvaluationContext filter, String feature) {
try {
FeatureFilter featureFilter = (FeatureFilter) context.getBean(filter.getName());
filter.setFeatureName(feature);

return featureFilter.evaluate(filter);
} catch (NoSuchBeanDefinitionException e) {
LOGGER.error("Was unable to find Filter {}. Does the class exist and set as an @Component?",
filter.getName());
if (properties.isFailFast()) {
String message = "Fail fast is set and a Filter was unable to be found";
ReflectionUtils.rethrowRuntimeException(new FilterNotFoundException(message, e, filter));
private Mono<Boolean> checkFeatureFilters(Feature featureFlag, Object featureContext) {
Conditions conditions = featureFlag.getConditions();
List<FeatureFilterEvaluationContext> featureFilters = conditions.getClientFilters();

if (featureFilters.size() == 0) {
return Mono.just(true);
}

List<Mono<Boolean>> filterResults = new ArrayList<Mono<Boolean>>();
for (FeatureFilterEvaluationContext featureFilter : featureFilters) {
String filterName = featureFilter.getName();

try {

Object filter = context.getBean(filterName);
featureFilter.setFeatureName(featureFlag.getId());
if (filter instanceof FeatureFilter) {
filterResults.add(Mono.just(((FeatureFilter) filter).evaluate(featureFilter)));
} else if (filter instanceof ContextualFeatureFilter) {
filterResults
.add(Mono.just(((ContextualFeatureFilter) filter).evaluate(featureFilter, featureContext)));
} else if (filter instanceof FeatureFilterAsync) {
filterResults.add(((FeatureFilterAsync) filter).evaluateAsync(featureFilter));
} else if (filter instanceof ContextualFeatureFilterAsync) {
filterResults
.add(((ContextualFeatureFilterAsync) filter).evaluateAsync(featureFilter, featureContext));
}
} catch (NoSuchBeanDefinitionException e) {
LOGGER.error("Was unable to find Filter {}. Does the class exist and set as an @Component?",
filterName);
if (properties.isFailFast()) {
String message = "Fail fast is set and a Filter was unable to be found";
ReflectionUtils.rethrowRuntimeException(new FilterNotFoundException(message, e, featureFilter));
}
}
}
return false;

if (ALL_REQUIREMENT_TYPE.equals(featureFlag.getConditions().getRequirementType())) {
return Flux.merge(filterResults).reduce((a, b) -> a && b).single();
}
// Any Filter must be true
return Flux.merge(filterResults).reduce((a, b) -> a || b).single();
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.spring.cloud.feature.management.filters;

import com.azure.spring.cloud.feature.management.models.FeatureFilterEvaluationContext;

/**
* A Filter for Feature Management that is attached to Features. The filter needs to have @Component set to be found by
* feature management.
*/
@FunctionalInterface
public interface ContextualFeatureFilter {

/**
* Evaluates if the filter is on or off. Returning true results in Feature evaluation ending and returning true.
* Returning false results in the next Feature evaluation to continue.
*
* @param context The context for whether or not the filter is passed.
* @param appContext The internal app context
* @return True if the feature is enabled, false otherwise.
*/
boolean evaluate(FeatureFilterEvaluationContext context, Object appContext);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.spring.cloud.feature.management.filters;

import com.azure.spring.cloud.feature.management.models.FeatureFilterEvaluationContext;

import reactor.core.publisher.Mono;

/**
* A Filter for Feature Management that is attached to Features. The filter needs to have @Component set to be found by
* feature management.
*/
@FunctionalInterface
public interface ContextualFeatureFilterAsync {

/**
* Evaluates if the filter is on or off. Returning true results in Feature evaluation ending and returning true.
* Returning false results in the next Feature evaluation to continue.
*
* @param context The context for whether or not the filter is passed.
* @param appContext The internal app context
* @return true if the feature is enabled, false otherwise.
*/
Mono<Boolean> evaluateAsync(FeatureFilterEvaluationContext context, Object appContext);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.spring.cloud.feature.management.filters;

import com.azure.spring.cloud.feature.management.models.FeatureFilterEvaluationContext;

import reactor.core.publisher.Mono;

/**
* A Filter for Feature Management that is attached to Features. The filter needs to have @Component set to be found by
* feature management.
*/
@FunctionalInterface
public interface FeatureFilterAsync {

/**
* Evaluates if the filter is on or off. Returning true results in Feature evaluation ending and returning true.
* Returning false results in the next Feature evaluation to continue.
*
* @param context The context for whether or not the filter is passed.
* @return True if the feature is enabled, false otherwise.
*/
Mono<Boolean> evaluateAsync(FeatureFilterEvaluationContext context);
Copy link
Preview

Copilot AI Dec 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method evaluateAsync should handle exceptions properly to avoid unhandled exceptions during feature evaluation. Consider adding documentation or handling exceptions within the method.

Suggested change
Mono<Boolean> evaluateAsync(FeatureFilterEvaluationContext context);
Mono<Boolean> evaluateAsync(FeatureFilterEvaluationContext context) throws Exception;

Copilot is powered by AI, so mistakes are possible. Review output carefully before use.

Positive Feedback
Negative Feedback

Provide additional feedback

Please help us improve GitHub Copilot by sharing more details about this comment.

Please select one or more of the options

}
Loading
Loading