Skip to content

Flipping Strategies

Cédrick Lunven edited this page Jul 1, 2016 · 6 revisions

Introduction

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.

Implement your own strategy

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

Overriding Strategy

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

BlackListStrategy

definition, sample

WhiteListStrategy

definition, sample

ClientFilterStrategy

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

DarkLaunchStrategy

definition, sample

PonderationStrategy

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

ServerFilterStrategy

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"));
  }
}

OfficeHourStrategy

definition, sample

ReleaseDateFlipStrategy

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"));
  }
}

ExpressionFlipStrategy

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"));
 }
}
You can’t perform that action at this time.