Skip to content
Merged
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 All @@ -34,6 +42,8 @@ public class FeatureManager {
private final FeatureManagementProperties featureManagementConfigurations;

private transient FeatureManagementConfigProperties properties;

private static final Duration DEFAULT_REQUEST_TIMEOUT = Duration.ofSeconds(100);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't think request is the right word since it's still relevant outside of requests. Maybe DEFAULT_BLOCK_TIMEOUT

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Updated to this in the variants PR


/**
* Can be called to check if a feature is enabled or disabled.
Expand All @@ -59,7 +69,7 @@ public class FeatureManager {
* @throws FilterNotFoundException file not found
*/
public Mono<Boolean> isEnabledAsync(String feature) {
return Mono.just(checkFeature(feature));
return checkFeature(feature, null);
Comment thread
mrm9084 marked this conversation as resolved.
}

/**
Expand All @@ -72,48 +82,93 @@ 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(DEFAULT_REQUEST_TIMEOUT);
}

/**
* 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);
}

private boolean checkFeature(String featureName) throws FilterNotFoundException {
/**
* Checks to see if the feature is enabled. If enabled it checks 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(DEFAULT_REQUEST_TIMEOUT);
}

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();
Comment thread
mrm9084 marked this conversation as resolved.

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,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;

/**
* A Filter for Feature Management that is attached to Features. The filter needs to have @Component set to be found by
* feature management. As a Contextual feature filter any context that is passed in to the feature request will be
* passed along to the filter(s).
*/
@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,27 @@
// 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. As a Contextual feature filter any context that is passed in to the feature request will be
* passed along to the filter(s).
*/
@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);
Comment thread
mrm9084 marked this conversation as resolved.

}
Loading