Skip to content

Commit

Permalink
Moirai 3.x Redesign
Browse files Browse the repository at this point in the history
* Move config-reading abstract methods from each decider into a
  ConfigReader Interface
* Add featureGroup support to control what features use the same hashing
  for proportionOfUsers decider
* Move the provided config decider predicates to a single class as
  static factory methods that depend on a ConfigReader instead of separate
  abstract classes that must be implemented in a specific config module
  • Loading branch information
jrduncans committed Feb 11, 2021
1 parent e991122 commit f5a34d0
Show file tree
Hide file tree
Showing 24 changed files with 529 additions and 351 deletions.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,20 @@ import com.nike.moirai.ConfigFeatureFlagChecker;
import com.nike.moirai.FeatureCheckInput;
import com.nike.moirai.FeatureFlagChecker;
import com.nike.moirai.Suppliers;
import com.nike.moirai.config.ConfigDeciders;
import com.nike.moirai.resource.FileResourceLoaders;
import com.nike.moirai.resource.reload.ResourceReloader;
import com.nike.moirai.typesafeconfig.TypesafeConfigDecider;
import com.nike.moirai.typesafeconfig.TypesafeConfigReader;
import com.nike.moirai.typesafeconfig.TypesafeConfigLoader;
import com.typesafe.config.Config;

import java.io.File;
import java.util.function.Supplier;

import static com.nike.moirai.Suppliers.supplierAndThen;
import static com.nike.moirai.typesafeconfig.TypesafeConfigReader.TYPESAFE_CONFIG_READER;

public class Usage {
Supplier<String> fileSupplier = FileResourceLoaders.forFile(new File("/path/to/conf/file/moirai.conf"));
Supplier<Config> configSupplier = supplierAndThen(fileSupplier, TypesafeConfigReader.FROM_STRING);
Supplier<Config> configSupplier = Suppliers.supplierAndThen(fileSupplier, TypesafeConfigReader.FROM_STRING);

ResourceReloader<Config> resourceReloader = ResourceReloader.withDefaultSettings(
Suppliers.async(configSupplier),
Expand All @@ -46,7 +46,7 @@ public class Usage {

FeatureFlagChecker featureFlagChecker = ConfigFeatureFlagChecker.forReloadableResource(
resourceReloader,
TypesafeConfigDecider.ENABLED_USERS.or(TypesafeConfigDecider.PROPORTION_OF_USERS)
ConfigDeciders.enabledUsers(TYPESAFE_CONFIG_READER).or(ConfigDeciders.proportionOfUsers(TYPESAFE_CONFIG_READER))
);

public int getNumber(String userIdentity) {
Expand Down
3 changes: 2 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import sbt.Keys.javacOptions
import Dependencies._

lazy val commonSettings = Seq(
val commonSettings = Seq(
organization := "com.nike.moirai",
organizationName := "Nike",
organizationHomepage := Some(url("http://engineering.nike.com")),
Expand Down Expand Up @@ -85,6 +85,7 @@ lazy val `moirai-typesafeconfig` = project
libraryDependencies ++= Seq(
typesafeConfig,
scalaTest,
scalaCheck,
logback
)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public class FeatureCheckInput {
public enum DimensionKey {
USER_ID, DATE_TIME;

private static Set<String> KEYS = Arrays.stream(DimensionKey.values()).map(DimensionKey::name).collect(Collectors.toSet());
private static final Set<String> KEYS = Arrays.stream(DimensionKey.values()).map(DimensionKey::name).collect(Collectors.toSet());
}

private final Map<String, ?> dimensions;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.nike.moirai.config;

import com.nike.moirai.FeatureCheckInput;

import java.util.function.Predicate;

/**
* Support functions for creating a {@link java.util.function.Predicate} for {@link ConfigDecisionInput}.
*/
public class ConfigDeciderSupport {
/**
* Apply a predicate to the userId from a feature check input.
*
* @param featureCheckInput the input to check
* @param userIdCheck the predicate to apply
* @return false if there is no userId in the input, otherwise the result of the predicate applied to the userId
*/
public static boolean userIdCheck(FeatureCheckInput featureCheckInput, Predicate<String> userIdCheck) {
return featureCheckInput.getUserId().map(userIdCheck::test).orElse(false);
}

/**
* Apply a predicate to the dimension value from a feature check input.
*
* @param featureCheckInput the input to check
* @param dimensionKey the dimension to check
* @param dimensionCheck the predicate to apply
* @return false if there is no userId in the input, otherwise the result of the predicate applied to the userId
*/
public static boolean customDimensionCheck(FeatureCheckInput featureCheckInput, String dimensionKey, Predicate<Object> dimensionCheck) {
return featureCheckInput.getDimension(dimensionKey).map(dimensionCheck::test).orElse(false);
}

private ConfigDeciderSupport() {
// Prevent instantiation
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,51 @@

import com.nike.moirai.FeatureCheckInput;

import java.util.Collection;
import java.util.Optional;
import java.util.function.Predicate;

import static com.nike.moirai.config.ConfigDeciderSupport.userIdCheck;

/**
* Useful methods for implementing a Predicate&lt;ConfigDecisionInput&lt;T&gt;&gt;
* {@link Predicate} factories given a {@link ConfigReader}
*/
public class ConfigDeciders {
/**
* Apply a predicate to the userId from a feature check input.
*
* @param featureCheckInput the input to check
* @param userIdCheck the predicate to apply
* @return false if there is no userId in the input, otherwise the result of the predicate applied to the userId
*/
public static boolean userIdCheck(FeatureCheckInput featureCheckInput, Predicate<String> userIdCheck) {
return featureCheckInput.getUserId().map(userIdCheck::test).orElse(false);
public static <C> Predicate<ConfigDecisionInput<C>> featureEnabled(ConfigReader<C> configReader) {
return configDecisionInput ->
configReader.featureEnabled(configDecisionInput.getConfig(), configDecisionInput.getFeatureIdentifier()).orElse(false);
}

public static <C> Predicate<ConfigDecisionInput<C>> enabledUsers(ConfigReader<C> configReader) {
return new EnabledValuesConfigDecider<C, String>() {
@Override
protected Collection<String> enabledValues(C config, String featureIdentifier) {
return configReader.enabledUsers(config, featureIdentifier);
}

@Override
protected boolean checkValue(FeatureCheckInput featureCheckInput, Predicate<String> check) {
return userIdCheck(featureCheckInput, check);
}
};
}

public static <C> Predicate<ConfigDecisionInput<C>> proportionOfUsers(ConfigReader<C> configReader) {
return configDecisionInput ->
userIdCheck(configDecisionInput.getFeatureCheckInput(), userId ->
configReader.enabledProportion(configDecisionInput.getConfig(), configDecisionInput.getFeatureIdentifier()).map(enabledProportion ->
userHashEnabled(
userId,
configDecisionInput.getFeatureIdentifier(),
configReader.featureGroup(configDecisionInput.getConfig(), configDecisionInput.getFeatureIdentifier()),
enabledProportion)
).orElse(false)
);
}

/**
* Apply a predicate to the dimension value from a feature check input.
*
* @param featureCheckInput the input to check
* @param dimensionKey the dimension to check
* @param dimensionCheck the predicate to apply
* @return false if there is no userId in the input, otherwise the result of the predicate applied to the userId
*/
public static boolean customDimensionCheck(FeatureCheckInput featureCheckInput, String dimensionKey, Predicate<Object> dimensionCheck) {
return featureCheckInput.getDimension(dimensionKey).map(dimensionCheck::test).orElse(false);
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
private static boolean userHashEnabled(String userId, String featureIdentifier, Optional<String> featureGroup, double proportion) {
return (Math.abs((userId + featureGroup.orElse(featureIdentifier)).hashCode()) % 100) / 100.0 < proportion;
}

private ConfigDeciders() {
Expand Down
48 changes: 48 additions & 0 deletions moirai-core/src/main/java/com/nike/moirai/config/ConfigReader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.nike.moirai.config;

import java.util.Collection;
import java.util.Optional;

public interface ConfigReader<C> {
/**
* Provide the collection of users that should have the given feature enabled. Return an empty list if no configuration is provided for the feature.
*
* @param config the config source
* @param featureIdentifier the feature
* @return the collection of userIds that should be enabled for the feature
*/
Collection<String> enabledUsers(C config, String featureIdentifier);

/**
* Provide the boolean value on whether the feature should be enabled.
* Returning Optional.empty() is equivalent to false.
*
* @param config the config source
* @param featureIdentifier the feature
* @return true, false, or {@link Optional#empty()}
*/
Optional<Boolean> featureEnabled(C config, String featureIdentifier);

/**
* Provide the proportion of users that should be enabled for the given feature. The proportion should be a double from 0.0 to 1.0.
* 0.0 means no users will have the feature enabled, and 1.0 will mean that all users will have the feature enabled.
* Values below zero will be treated the same as 0.0 and values above 1.0 will be treated the same as 1.0.
* Returning Optional.empty() is equivalent to 0.0; both will return false for all users.
*
* @param config the config source
* @param featureIdentifier the feature
* @return some proportion between 0.0 and 1.0, or {@link Optional#empty()}
*/
Optional<Double> enabledProportion(C config, String featureIdentifier);

/**
* Provide the featureGroup that the feature should belong to. This will affect the {@link ConfigDeciders#proportionOfUsers(ConfigReader)} so that
* if two features with the same featureGroup have the same enabled proportion, the same users will be included in that proportion.
* Returning Optional.empty() will result in the feature being grouped by itself (the featureIdentifier will be used as the featureGroup).
*
* @param config the config source
* @param featureIdentifier the feature
* @return the identifier of the feature group or {@link Optional#empty()}
*/
Optional<String> featureGroup(C config, String featureIdentifier);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import java.util.function.Predicate;

import static com.nike.moirai.config.ConfigDeciders.customDimensionCheck;
import static com.nike.moirai.config.ConfigDeciderSupport.customDimensionCheck;

/**
* Returns true for a configured collection of values for some custom dimension.
Expand All @@ -15,6 +15,7 @@
* @param <V> the type of value for the dimension
*/
public abstract class EnabledCustomDimensionConfigDecider<C, V> extends EnabledValuesConfigDecider<C, V> {

private final Logger logger = LoggerFactory.getLogger(this.getClass());

@Override
Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

Loading

0 comments on commit f5a34d0

Please sign in to comment.