-
Notifications
You must be signed in to change notification settings - Fork 288
Flipping Strategies
As introduced in the first chapter, the behavior of a feature can be enslaved with your custom implementation and rules. With ff4j, once the feature is enabled AND current authenticated user is granted, a test is performed to evaluate the value of FlippingStrategy.
The class is set up with a map of "initial parameters" and the init(..) method must be implemented. The getter of those parameters must also be implemented (for serialization purposes) and obviously the test is performed within evaluate(...) method.
The evaluate() method expects a FlippingExecutionContext which hold parameters as key/ value pairs and provides to you the feature name and a reference to the feature store.
There is a bunch of strategies provided out-of-the-box but to understand the concept we propose to create our own. In this sample we will toggle feature if, and only if the request is made during office time let's say 09:00 to 18:00.
There is no extra dependency required to implement strategy. The interface FlippingStrategy is in the ff4j-coremodule. Create the following class strategy class. Note that it inherit from AbstractFlipStrategy, it's not mandatory but provide a bunch of helpers.
public class OfficeHoursFlippingStrategy extends AbstractFlipStrategy {
/** Start Hour. */
private int start = 0;
/** Hend Hour. */
private int end = 0;
/** {@inheritDoc} */
@Override
public void init(String featureName, Map<String, String> initValue) {
super.init(featureName, initValue);
assertRequiredParameter("startDate");
assertRequiredParameter("endDate");
start = new Integer(initValue.get("startDate"));
end = new Integer(initValue.get("endDate"));
}
/** {@inheritDoc} */
@Override
public boolean evaluate(String fName, FeatureStore fStore, FlippingExecutionContext ctx) {
int currentHour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY);
return (currentHour >= start && currentHour < end);
}
}• Create a ff4j-strategy-1.xml with a feature reference our new strategy :
<?xml version="1.0" encoding="UTF-8" ?>
<features>
<feature uid="sayHello" enable="true" description="some desc">
<flipstrategy class="org.ff4j.sample.strategy.OfficeHoursFlippingStrategy" >
<param name="startDate">9</param>
<param name="endDate">18</param>
</flipstrategy>
</feature>
</features>• And the test to illustrate the behavior create the following unit test :
public class OfficeHoursFlippingStrategyTest {
// Initialization of target 'ff4j'
private final FF4j ff4j = new FF4j("ff4j-strategy-1.xml");
@Test
public void testCustomStrategy() throws Exception {
// Given
assertTrue(ff4j.exist("sayHello"));
FlippingStrategy fs = ff4j.getFeature("sayHello").getFlippingStrategy();
assertTrue(fs.getClass() == OfficeHoursFlippingStrategy.class);
assertEquals("9", fs.getInitParams().get("startDate"));
// When
int hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY);
boolean isNowOfficeTime = (hour > 9) && (hour < 18);
// Then
assertEquals(isNowOfficeTime, ff4j.check("sayHello"));
}
}This second sample will illustrate the FlippingExecutionContext behaviour. We create a strategy to enable a feature only for a subset of geographical regions.
• Create a strategy, initialized with the granted regions and expected user region's within execution context :
public class RegionFlippingStrategy extends AbstractFlipStrategy {
/** initial parameter. */
private static final String INIT_PARAMNAME_REGIONS = "grantedRegions";
/** current user attribute */
public static final String PARAMNAME_USER_REGION = "region";
/** Initial Granted Regions. */
private final Set<String> setOfGrantedRegions = new HashSet<String>();
/** {@inheritDoc} */
@Override
public void init(String featureName, Map<String, String> initValue) {
super.init(featureName, initValue);
assertRequiredParameter(INIT_PARAMNAME_REGIONS);
String[] arrayOfRegions = initValue.get(INIT_PARAMNAME_REGIONS).split(",");
setOfGrantedRegions.addAll(Arrays.asList(arrayOfRegions));
}
/** {@inheritDoc} */
@Override
public boolean evaluate(String fName, FeatureStore fStore, FlippingExecutionContext ctx) {
// true means required here
String userRegion = ctx.getString(PARAMNAME_USER_REGION, true);
return setOfGrantedRegions.contains(userRegion);
}
}• Create a xml file with a feature using the strategy :
<?xml version="1.0" encoding="UTF-8" ?>
<features>
<feature uid="notForEurop" enable="true" >
<flipstrategy class="org.ff4j.sample.strategy.RegionFlippingStrategy" >
<param name="grantedRegions">ASIA,AMER</param>
</flipstrategy>
</feature>
</features>• Create the unit test :
public class RegionFlippingStrategyTest {
// ff4j
private final FF4j ff4j = new FF4j("ff4j-strategy-2.xml");
// sample execution context
private final FlippingExecutionContext fex = new FlippingExecutionContext();
@Test
public void testRegionStrategy() throws Exception {
// Given assertTrue(ff4j.exist("notForEurop"));
FlippingStrategy fs = ff4j.getFeature("notForEurop").getFlippingStrategy();
assertTrue(fs.getClass() == RegionFlippingStrategy.class);
assertEquals("ASIA,AMER", fs.getInitParams().get("grantedRegions"));
// When
fex.addValue(RegionFlippingStrategy.PARAMNAME_USER_REGION, "AMER");
// Then
assertTrue(ff4j.check("notForEurop", fex));
// When
fex.addValue(RegionFlippingStrategy.PARAMNAME_USER_REGION, "EUROP");
// Then
assertFalse(ff4j.check("notForEurop", fex));
}
}Sometimes, even if a feature has a defined strategy, you would like to override it for a single invocation. The FF4J class provides another check() method which take a flipping strategy as second parameter. The strategy will overrides the existing one.
• Here is a sample unit test to illustrate the behavior :
public class OverridingStrategyTest {
// ff4j
private final FF4j ff4j = new FF4j("ff4j-strategy-1.xml");
@Test
public void testBehaviourOfOverriding() {
assertTrue(ff4j.exist("sayHello"));
// Behaviour of the strategy
FlippingStrategy fs = ff4j.getFeature("sayHello").getFlippingStrategy();
assertTrue(fs.getClass() == OfficeHoursFlippingStrategy.class);
assertEquals("9", fs.getInitParams().get("startDate"));
int hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY);
boolean isNowOfficeTime = (hour > 9) & (hour < 18);
assertEquals(isNowOfficeTime, ff4j.check("sayHello"));
// New Strategy : ReleaseDate with date in the past ==> Always true
FlippingStrategy newStrategy = new ReleaseDateFlipStrategy(new Date(System.currentTimeMillis() - 100000));
assertTrue(ff4j.checkOveridingStrategy("sayHello", newStrategy, null));
}
}definition, sample
definition, sample
The purpose of this strategy is to enable a feature for a limited list of clients. Each client must present
its 'hostname' in the context. If the hostname is in the white list, it's ok. The attribute to set up is clientHostName. The values are separated by a comma, there are no spaces between values.
• Here a sample XML file :
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration>
<features>
<feature uid="pingCluster" enable="true" description="limit client hosts">
<flipstrategy class="org.ff4j.strategy.ClientFilterStrategy" >
<param name="grantedClients">127.0.0.1,srvprd01,srvprd02</param>
</flipstrategy>
</feature>
</features>• And the related unit test :
public class ClientListStrategyTest {
// initialize ff4j
FF4j ff4j = new FF4j("ff4j-strategy-clientfilter.xml");
@Test
public void testClientFilter() {
// Given
assertTrue(ff4j.exist("pingCluster"));
assertTrue(ff4j.getFeature("pingCluster").isEnable());
// When no host provided, Then error
try {
assertFalse(ff4j.check("pingCluster"));
fail(); // error as parameter not present in execution context
} catch (IllegalArgumentException iae) {
assertTrue(iae.getMessage().contains(ClientFilterStrategy.CLIENT_HOSTNAME));
}
// When invalid host provided, Then unavailable
FlippingExecutionContext fex = new FlippingExecutionContext();
fex.addValue(ClientFilterStrategy.CLIENT_HOSTNAME, "invalid");
assertFalse(ff4j.check("pingCluster", fex));
// When correct hostname... OK
fex.addValue(ClientFilterStrategy.CLIENT_HOSTNAME, "srvprd01");
assertTrue(ff4j.check("pingCluster", fex));
}
}definition, sample
The purpose of this strategy is to enable a feature for a percentage of requests. It could be useful in Dark Launch zero downtime deployment pattern for instance. It expected a parameter weight but if not provided is set up to its default value 0.5
• Here a sample XML file :
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration>
<features>
<!-- Ponderation to 0 -->
<feature uid="pond_0" enable="true" description="some desc">
<flipstrategy class="org.ff4j.strategy.PonderationStrategy" >
<param name="weight" value="0" />
</flipstrategy>
</feature>
<!-- Ponderation to 1 -->
<feature uid="pond_1" enable="true" description="some desc" >
<flipstrategy class="org.ff4j.strategy.PonderationStrategy" >
<param name="weight" value="1" />
</flipstrategy>
</feature>
<feature uid="pond_06" enable="true" description="some desc">
<flipstrategy class="org.ff4j.strategy.PonderationStrategy" >
<param name="weight" value="0.6" />
</flipstrategy>
</feature>
<feature uid="pondDefault" enable="true" description="some desc">
<flipstrategy class="org.ff4j.strategy.PonderationStrategy" />
</feature>
</features>• And the related unit test :
public class PonderationFlippingStrategyTest {
// initialize ff4j
FF4j ff4j = new FF4j("ff4j-strategy-ponderation.xml");
@Test
public void testPonderation() {
// Given : weight = 0
assertTrue(ff4j.exist("pond_0"));
// Then => always false
assertFalse(ff4j.check("pond_0"));
// Given : weight = 100%
assertTrue(ff4j.exist("pond_1"));
// Then => Always true
assertTrue(ff4j.check("pond_1"));
// Given : weight = 60%
assertTrue(ff4j.exist("pond_06"));
// When : Try 1 million times
double success = 0.0;
for (int i = 0; i < 1000000; i++) {
if (ff4j.check("pond_06")) {
success++;
}
}
// Then, percentage ok with great precision
double resultPercent = success / 1000000;
assertTrue(resultPercent < (0.6 + 0.001));
assertTrue(resultPercent > (0.6 - 0.001));
}
}The purpose of this strategy is to enable a feature for a limited list of servers. The feature will be available only if the hostname of hosting server is in the white list. The attribute to set up is serverHostName but it'not required. If not provided ff4j will ask the JVM for the current hostname (through theInetAddress) The values are separated by a comma, there are no spaces between values.
• Here a sample XML file :
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration>
<features>
<feature uid="onlyOnPRODServers" enable="true" description="some desccheck hostname">
<flipstrategy class="org.ff4j.strategy.ServerFilterStrategy" >
<param name="grantedServers">srvprd01,srvprd02,srvprd03</param>
</flipstrategy>
</feature>
</features>• And the related unit test :
public class ServerListStrategyTest {
// initialize ff4j
FF4j ff4j = new FF4j("ff4j-strategy-serverfilter.xml");
@Test
public void testServerFilter() throws UnknownHostException {
// Given
assertTrue(ff4j.exist("onlyOnPRODServers"));
assertTrue(ff4j.getFeature("onlyOnPRODServers").isEnable());
// When invalid host provided, Then unavailable
FlippingExecutionContext fex = new FlippingExecutionContext();
fex.addValue(ServerFilterStrategy.SERVER_HOSTNAME, "invalid");
assertFalse(ff4j.check("onlyOnPRODServers", fex));
// When correct hostname... OK
fex.addValue(ServerFilterStrategy.SERVER_HOSTNAME, "srvprd01");
assertTrue(ff4j.check("onlyOnPRODServers", fex));
// When no host provided, Then try to identified by itself but not SECURE
System.out.println("Trying..." + InetAddress.getLocalHost().getHostName() + " against white list");
// my laptop hostname is not in the whitelist
assertFalse(ff4j.check("onlyOnPRODServers"));
}
}definition, sample
The purpose of this strategy is the made a feature available from a fixed date (like a releaseDate). Before the defined date, the feature is always false and after it's true. The format to set up the date is YYYY-MM-dd-HH:mm
• Here a sample XML file :
<?xml version="1.0" encoding="UTF-8" ?>
<features>
<feature uid="PAST" enable="true" description="Always true as in the past">
<flipstrategy class="org.ff4j.strategy.ReleaseDateFlipStrategy" >
<param name="releaseDate" value="2013-07-14-14:00" />
</flipstrategy>
</feature>
<feature uid="FUTURE" enable="true" description="Always false as in the (far) future">
<flipstrategy class="org.ff4j.strategy.ReleaseDateFlipStrategy" >
<param name="releaseDate" value="3013-07-14-14:00" />
</flipstrategy>
</feature>
</features>• And the related unit test :
public class ReleaseDateFlipStrategyTest {
// initialize ff4j
FF4j ff4j = new FF4j("ff4j-strategy-releasedate.xml");
@Test
public void testReleaseDateStrategy() throws ParseException {
// Given
assertTrue(ff4j.exist("PAST"));
Feature fPast = ff4j.getFeature("PAST");
ReleaseDateFlipStrategy rdsPast = (ReleaseDateFlipStrategy) fPast.getFlippingStrategy();
assertTrue(new Date().after(rdsPast.getReleaseDate()));
// Then
assertTrue(ff4j.check("PAST"));
// Given
assertTrue(ff4j.exist("FUTURE"));
Feature fFuture = ff4j.getFeature("FUTURE");
ReleaseDateFlipStrategy rdsFuture = (ReleaseDateFlipStrategy) fFuture.getFlippingStrategy();
Assert.assertTrue(new Date().before(rdsFuture.getReleaseDate()));
// Then
assertFalse(ff4j.check("FUTURE"));
}
}The idea behind this strategy is to evaluate a boolean expression by combining several feature with moore algebra. AND, OR, NOT and brackets are available. This way you can define features depending of status of other.
• Create a XML file, we want to check that feature 'D' is flipped if(A: AND B ) OR NOT(C) OR NOT(B). The expression is wrapped in a CDATA block. The charater for the operand AND is &, for OR it's| and for NOT it's!
<?xml version="1.0" encoding="UTF-8" ?>
<features>
<feature uid="A" enable="true" />
<feature uid="B" enable="false" />
<feature uid="C" enable="false" />
<feature uid="D" enable="true">
<flipstrategy class="org.ff4j.strategy.el.ExpressionFlipStrategy">
<param name="expression"><![CDATA[A & B | !C | !B]]></param>
</flipstrategy>
</feature>
<feature uid="E" enable="true">
<flipstrategy class="org.ff4j.strategy.el.ExpressionFlipStrategy">
<param name="expression"><![CDATA[A & B]]></param>
</flipstrategy>
</feature>
<feature uid="F" enable="true">
<flipstrategy class="org.ff4j.strategy.el.ExpressionFlipStrategy">
<param name="expression"><![CDATA[A | B]]></param>
</flipstrategy>
</feature>
</features>• The behaviour is detailed in the following unit test
public class ExpressionStrategyTest {
// ff4j
private final FF4j ff4j = new FF4j("ff4j-strategy-expression.xml");
@Test
public void testExpressions() {
// Given
assertTrue(ff4j.exist("A"));
assertTrue(ff4j.exist("B"));
assertTrue(ff4j.exist("C"));
ff4j.enable("D");
ff4j.enable("E");
ff4j.enable("F");
// When A=FALSE, B=TRUE, C=TRUE
assertFalse(ff4j.check("A"));
assertTrue(ff4j.check("B"));
assertTrue(ff4j.check("C"));
// THEN
//E = A AND B = FALSE AND TRUE = FALSE
assertFalse(ff4j.check("E"));
// F = A OR R = FALSE OR TRUE = TRUE assertTrue(ff4j.check("F"));
// D = (A AND B) OR NOT(B) OR NOT(C) = (false & true) or false or false
assertFalse(ff4j.check("D"));
// When enabling A
ff4j.enable("A");
// THEN
// E = A AND B = TRUE AND TRUE = TRUE
assertTrue(ff4j.check("E"));
// F = A AND B = TRUE OR TRUE = TRUE
assertTrue(ff4j.check("F"));
// D = (A AND B) OR NOT(B) OR NOT(C) = (true & true) or false or false
assertTrue(ff4j.check("D"));
}
}Home | Core | Advanced | Web | UI and Admin | Stores | Security | FlippingStrategies