example feature flag implementations

This commit is contained in:
Tom Hombergs
2021-10-20 07:09:53 +11:00
parent 7520c38d3e
commit e4d52aa182
11 changed files with 330 additions and 1 deletions

View File

@@ -21,10 +21,18 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<!-- TOGGLZ -->
<dependency>

View File

@@ -0,0 +1,9 @@
package io.reflectoring.featureflags.implementations;
public interface FeatureFlagService {
Boolean featureOne();
Integer featureTwo();
}

View File

@@ -0,0 +1,15 @@
package io.reflectoring.featureflags.implementations.code;
import io.reflectoring.featureflags.implementations.FeatureFlagService;
public class CodeBackedFeatureFlagService implements FeatureFlagService {
@Override
public Boolean featureOne() {
return true;
}
@Override
public Integer featureTwo() {
return 42;
}
}

View File

@@ -0,0 +1,52 @@
package io.reflectoring.featureflags.implementations.contextsensitive;
import io.reflectoring.featureflags.implementations.FeatureFlagService;
import io.reflectoring.featureflags.implementations.contextsensitive.Feature.RolloutStrategy;
import io.reflectoring.featureflags.web.UserSession;
import org.jetbrains.annotations.Nullable;
import org.springframework.jdbc.core.JdbcTemplate;
public class ContextSensitiveFeatureFlagService implements FeatureFlagService {
private final JdbcTemplate jdbcTemplate;
private final UserSession userSession;
public ContextSensitiveFeatureFlagService(JdbcTemplate jdbcTemplate, UserSession userSession) {
this.jdbcTemplate = jdbcTemplate;
this.userSession = userSession;
}
@Override
public Boolean featureOne() {
Feature feature = getFeatureFromDatabase();
if (feature == null) {
return Boolean.FALSE;
}
return feature.evaluateBoolean(userSession.getUsername());
}
@Override
public Integer featureTwo() {
Feature feature = getFeatureFromDatabase();
if (feature == null) {
return null;
}
return feature.evaluateInt(userSession.getUsername());
}
@Nullable
private Feature getFeatureFromDatabase() {
return jdbcTemplate.query("select targeting, value, defaultValue, percentage from features where feature_key='FEATURE_ONE'", resultSet -> {
if (!resultSet.next()) {
return null;
}
RolloutStrategy rolloutStrategy = Enum.valueOf(RolloutStrategy.class, resultSet.getString(1));
String value = resultSet.getString(2);
String defaultValue = resultSet.getString(3);
int percentage = resultSet.getInt(4);
return new Feature(rolloutStrategy, value, defaultValue, percentage);
});
}
}

View File

@@ -0,0 +1,102 @@
package io.reflectoring.featureflags.implementations.contextsensitive;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
public class Feature {
public enum RolloutStrategy {
GLOBAL,
PERCENTAGE;
}
private final RolloutStrategy rolloutStrategy;
private final int percentage;
private final String value;
private final String defaultValue;
public Feature(RolloutStrategy rolloutStrategy, String value, String defaultValue, int percentage) {
this.rolloutStrategy = rolloutStrategy;
this.percentage = percentage;
this.value = value;
this.defaultValue = defaultValue;
}
public boolean evaluateBoolean(String userId) {
switch (this.rolloutStrategy) {
case GLOBAL:
return this.getBooleanValue();
case PERCENTAGE:
if (percentageHashCode(userId) <= this.percentage) {
return this.getBooleanValue();
} else {
return this.getBooleanDefaultValue();
}
}
return this.getBooleanDefaultValue();
}
public Integer evaluateInt(String userId) {
switch (this.rolloutStrategy) {
case GLOBAL:
return this.getIntValue();
case PERCENTAGE:
if (percentageHashCode(userId) <= this.percentage) {
return this.getIntValue();
} else {
return this.getIntDefaultValue();
}
}
return this.getIntDefaultValue();
}
double percentageHashCode(String text) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] encodedhash = digest.digest(
text.getBytes(StandardCharsets.UTF_8));
double INTEGER_RANGE = 1L << 32;
return (((long) Arrays.hashCode(encodedhash) - Integer.MIN_VALUE) / INTEGER_RANGE) * 100;
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
}
}
public RolloutStrategy getTargeting() {
return rolloutStrategy;
}
public int getPercentage() {
return percentage;
}
public int getIntValue() {
return Integer.parseInt(this.value);
}
public int getIntDefaultValue() {
return Integer.parseInt(this.defaultValue);
}
public boolean getBooleanValue() {
return Boolean.parseBoolean(this.value);
}
public boolean getBooleanDefaultValue() {
return Boolean.parseBoolean(this.defaultValue);
}
public String getStringValue() {
return value;
}
public String getDefaultValue() {
return defaultValue;
}
}

View File

@@ -0,0 +1,32 @@
package io.reflectoring.featureflags.implementations.database;
import io.reflectoring.featureflags.implementations.FeatureFlagService;
import org.springframework.jdbc.core.JdbcTemplate;
public class DatabaseBackedFeatureFlagService implements FeatureFlagService {
private JdbcTemplate jdbcTemplate;
@Override
public Boolean featureOne() {
return jdbcTemplate.query("select value from features where feature_key='FEATURE_ONE'", resultSet -> {
if(!resultSet.next()){
return false;
}
boolean value = Boolean.parseBoolean(resultSet.getString(1));
return value ? Boolean.TRUE : Boolean.FALSE;
});
}
@Override
public Integer featureTwo() {
return jdbcTemplate.query("select value from features where feature_key='FEATURE_TWO'", resultSet -> {
if(!resultSet.next()){
return null;
}
return Integer.valueOf(resultSet.getString(1));
});
}
}

View File

@@ -0,0 +1,32 @@
package io.reflectoring.featureflags.implementations.launchdarkly;
import com.launchdarkly.sdk.LDUser;
import com.launchdarkly.sdk.server.LDClient;
import io.reflectoring.featureflags.implementations.FeatureFlagService;
import io.reflectoring.featureflags.web.UserSession;
public class LaunchDarklyFeatureFlagService implements FeatureFlagService {
private final LDClient launchdarklyClient;
private final UserSession userSession;
public LaunchDarklyFeatureFlagService(LDClient launchdarklyClient, UserSession userSession) {
this.launchdarklyClient = launchdarklyClient;
this.userSession = userSession;
}
@Override
public Boolean featureOne() {
return launchdarklyClient.boolVariation("feature-one", getLaunchdarklyUserFromSession(), false);
}
@Override
public Integer featureTwo() {
return launchdarklyClient.intVariation("feature-two", getLaunchdarklyUserFromSession(), 0);
}
private LDUser getLaunchdarklyUserFromSession() {
return new LDUser.Builder(userSession.getUsername())
.build();
}
}

View File

@@ -0,0 +1,31 @@
package io.reflectoring.featureflags.implementations.properties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties("features")
public class FeatureProperties {
private boolean featureOne;
private int featureTwo;
public FeatureProperties() {
}
public boolean getFeatureOne() {
return featureOne;
}
public void setFeatureOne(boolean featureOne) {
this.featureOne = featureOne;
}
public int getFeatureTwo() {
return featureTwo;
}
public void setFeatureTwo(int featureTwo) {
this.featureTwo = featureTwo;
}
}

View File

@@ -0,0 +1,25 @@
package io.reflectoring.featureflags.implementations.properties;
import io.reflectoring.featureflags.implementations.FeatureFlagService;
import org.springframework.stereotype.Component;
@Component
public class PropertiesBackedFeatureFlagService implements FeatureFlagService {
private final FeatureProperties featureProperties;
public PropertiesBackedFeatureFlagService(FeatureProperties featureProperties) {
this.featureProperties = featureProperties;
}
@Override
public Boolean featureOne() {
return featureProperties.getFeatureOne();
}
@Override
public Integer featureTwo() {
return featureProperties.getFeatureTwo();
}
}

View File

@@ -15,4 +15,8 @@ togglz:
enabled: true
secured: false
path: /togglz
use-management-port: false
use-management-port: false
features:
featureOne: true
featureTwo: 42

View File

@@ -0,0 +1,19 @@
package io.reflectoring.featureflags.implementations.contextsensitive;
import io.reflectoring.featureflags.implementations.contextsensitive.Feature.RolloutStrategy;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.offset;
public class FeatureTest {
@Test
void testHashCode(){
Feature feature = new Feature(RolloutStrategy.PERCENTAGE, "true", "false", 50);
assertThat(feature.percentageHashCode("1")).isCloseTo(27.74d, offset(0.01d));
assertThat(feature.percentageHashCode("2")).isCloseTo(81.12d, offset(0.01d));
assertThat(feature.percentageHashCode("3")).isCloseTo(21.69d, offset(0.01d));
}
}