Store Technologies

Cédrick Lunven edited this page May 1, 2017 · 12 revisions

InMemory

The implementation of this FeatureStore parses an XML document (file or stream) at startup and store the features as a ConcurrentHashMap in memory. It's the default implementation of FF4J. It does not required any external librairies and is available in the module ff4j-core. The XML schema of the file (or XSD) can be find there. http://ff4j.org/schema/ff4j.xsd

You can declared the schema in the XML file with the following schema :

<?xml version="1.0" encoding="UTF-8" ?>
<features xmlns="http://www.ff4j.org/schema/ff4j"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://www.ff4j.org/schema/ff4j http://ff4j.org/schema/ff4j.xsd">
  <!-- Here your declarations -- >
</features>

There are a lot of samples in this reference guide. Yet, let's summarize everything in a quite complete one

<?xml version="1.0" encoding="UTF-8" ?>
<features xmlns="http://www.ff4j.org/schema/ff4j"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://www.ff4j.org/schema/ff4j http://ff4j.org/schema/ff4j.xsd">

 <!-- Simplest -->
 <feature uid="A" enable="true" />

 <!-- Add description -->
 <feature uid="B" description="Expect to say good bye..." enable="false" />

 <!-- Security stuff -->
 <feature uid="C" enable="false">
  <security>
   <role name="USER"  />
   <role name="ADMIN" />
  </security>
 </feature>

 <!-- Some strategies and a group -->
 <feature-group name="strategies">
  
  <feature uid="S1" enable="true">
   <flipstrategy class="org.ff4j.strategy.el.ExpressionFlipStrategy"> 
    <param name="expression" value="A | B" />
   </flipstrategy>
  </feature>
  
  <feature uid="S2" enable="true">
   <flipstrategy class="org.ff4j.strategy.ReleaseDateFlipStrategy">
    <param name="releaseDate" value="2013-07-14-14:00" />
   </flipstrategy>
  </feature>

  <feature uid="S3" description="null" enable="true">
    <flipstrategy class="org.ff4j.strategy.PonderationStrategy">
     <param name="weight" value="0.5" />
    </flipstrategy>
  </feature>
  
  <feature uid="S4" description="z" enable="true">
    <flipstrategy class="org.ff4j.strategy.ClientFilterStrategy"> 
     <param name="grantedClients" value="c1,c2" />
    </flipstrategy>
  </feature>

  <feature uid="S5" description="null" enable="true"> 
   <flipstrategy class="org.ff4j.strategy.ServerFilterStrategy">
    <param name="grantedServers" value="s1,s2" />
   </flipstrategy>
  </feature>

 </feature-group>
</features>

The XML format can be generated whatever the FeatureStore configured. It should be use to export configuration from an environment and insert into another. As detailed further, this operation is available in the web console. If you would like to do it in your own code, here is the way to do it.

public class ImportExportXmlTest {

 @Test
 public void testImport() throws FileNotFoundException {
  // Given
  FF4j ff4j = new FF4j();
  
  // When
  FileInputStream fis = new FileInputStream(new File("src/test/resources/ff4j.xml")); 
  Map<String, Feature> mapsOfFeat = new FeatureXmlParser().parseConfigurationFile(fis); 
  for (Entry<String, Feature> feature : mapsOfFeat.entrySet()) {
    if (ff4j.exist(feature.getKey())) { 
      ff4j.getStore().update(feature.getValue());
    } else { 
      ff4j.getStore().create(feature.getValue());
    }
  }

  // Then
  assertEquals(2, ff4j.getFeatures().size());
}

 @Test
 public void testExport() throws IOException { 
  FF4j ff4j = new FF4j("ff4j.xml");
  InputStream in = ff4j.exportFeatures();
  // Write into console
  byte[] bbuf = new byte[4096]; int length = 0;
  while ((in != null) && (length != -1)) { 
   length = in.read(bbuf);
  }
  System.out.print(new String(bbuf));
 }
}

Jdbc

JdbcFeatureStore, JdbcPropertyStore, JdbcEventRepository of the module ff4j-core store informations in any RDBMS database through JDBC communications. The connectivity to DB rely on a simple Jdbc DataSource. To use this implementation, only your target driver is required, there is no extra dependency.

Data Model

-- Main Table to store Features
CREATE TABLE FF4J_FEATURES (
  "FEAT_UID"     	VARCHAR(100),
  "ENABLE"  		INTEGER NOT NULL,
  "DESCRIPTION" 	VARCHAR(1000),
  "STRATEGY"		VARCHAR(1000),
  "EXPRESSION"	    VARCHAR(255),
  "GROUPNAME"		VARCHAR(100),
  PRIMARY KEY("FEAT_UID")
);

-- Roles to store ACL, FK to main table
CREATE TABLE FF4J_ROLES (
  "FEAT_UID"     VARCHAR(100) REFERENCES FF4J_FEATURES("FEAT_UID"),
  "ROLE_NAME"    VARCHAR(100),
  PRIMARY KEY("FEAT_UID", "ROLE_NAME")
);

-- Feature Internal Custom Properties
CREATE TABLE FF4J_CUSTOM_PROPERTIES (
  "PROPERTY_ID"  VARCHAR(100) NOT NULL,
  "CLAZZ" 		 VARCHAR(255) NOT NULL,
  "CURRENTVALUE" VARCHAR(255),
  "FIXEDVALUES"	 VARCHAR(1000),
  "DESCRIPTION"	 VARCHAR(1000),
  "FEAT_UID"     VARCHAR(100) REFERENCES FF4J_FEATURES("FEAT_UID"),
  PRIMARY KEY("PROPERTY_ID", "FEAT_UID")
);

-- @PropertyStore (edit general properties)
CREATE TABLE FF4J_PROPERTIES (
  "PROPERTY_ID"  VARCHAR(100) NOT NULL,
  "CLAZZ" 		 VARCHAR(255) NOT NULL,
  "CURRENTVALUE" VARCHAR(255),
  "FIXEDVALUES"	 VARCHAR(1000),
  "DESCRIPTION"	 VARCHAR(1000),
  PRIMARY KEY("PROPERTY_ID")
);

-- @see JdbcEventRepository (audit event)
CREATE TABLE FF4J_AUDIT (
  "EVT_UUID" 	 VARCHAR(40)  NOT NULL,
  "EVT_TIME" 	 TIMESTAMP 	  NOT NULL,
  "EVT_TYPE" 	 VARCHAR(30)  NOT NULL,
  "EVT_NAME" 	 VARCHAR(30)  NOT NULL,
  "EVT_ACTION" 	 VARCHAR(30)  NOT NULL,
  "EVT_HOSTNAME" VARCHAR(100)  NOT NULL,
  "EVT_SOURCE" 	 VARCHAR(30)  NOT NULL,
  "EVT_DURATION" INTEGER,
  "EVT_USER" 	 VARCHAR(30),
  "EVT_VALUE" 	 VARCHAR(100),
  "EVT_KEYS" 	 VARCHAR(255),
  PRIMARY KEY("EVT_UUID", "EVT_TIME")
);

The sql file can be found HERE and will be up-to-date as used in all unit tests.

Sample Code

To initialize ff4j with those stores please use the following and adapt with your needs.

// Initialization of your DataSource
DataSource ds = ...

FF4j ff4j = new FF4j();
ff4j.setFeatureStore(new JdbcFeatureStore(ds));
ff4j.setPropertiesStore(new JdbcPropertyStore(ds));
ff4j.setEventRepository(new JdbcEventRepository(ds));

Spring JDBC

Rational

Spring, the well known framework, provides a lot of element to ease (and secure) the usage of JDBC : Transactions, Connection closes. FF4j provides an implementation of stores to work with spring-jdbc module.

Sample Code

// Initialization of your DataSource
DataSource ds = ...

// Init the framework full in memory
FF4j ff4j = new FF4j();

// Feature States in a RDBMS
FeatureStoreSpringJdbc featureStore= new FeatureStoreSpringJdbc();
featureStore.setDataSource(ds);
ff4j.setFeatureStore(featureStore);

// Properties in RDBMS
PropertyStoreSpringJdbc propertyStore= new PropertyStoreSpringJdbc();
jdbcStore.setDataSource(ds);
ff4j.setPropertiesStore(propertyStore);

// Audit in RDBMS
// So far the implementation with SpringJDBC is not there, leverage on default JDBC
EventRepository auditStore = new JdbcEventRepository(ds);
ff4j.setEventRepository(eventRepository);

ff4j.audit(true);

JCache

JSR107...

Redis

FeatureStoreRedis, PropertyStoreRedis, EventRepositoryRedis and FeatureCacheProviderRedis of the module ff4j-store-redis store informations in the (awesome) noSql Key-value database REDIS. The connectivity to Redis rely on the Jedis library. You can implement a direct connection (not threadsafe) or use pooled connections. It's possible to connect to a Redis 'Sentinel' to handle clustering.

Keys are prefixed with FF4J_ to avoid any collision with existing data. There is no TTL for stores, keys remain forever.

  • Define the RedisConnection depending of the topology of your redis server :
//  Will use default value for REDIS (localhost/6379 @{@link `redis.clients.jedis.Protocol`})
new RedisConnection();
        
// enforce host and port
new RedisConnection("localhost", 6379);
        
// if password is enabled in redis
new RedisConnection("localhost", 6379, "requiredPassword");
        
// Defined your own pool with all capabilities of {@link redis.clients.jedis.JedisPool}
new RedisConnection(new JedisPool("localhost", 6379));
        
// Use the sentinel through specialized JedisPool {@link redis.clients.jedis.JedisSentinelPool}
new RedisConnection(new JedisSentinelPool("localhost", Util.set("master", "slave1")));
  • Initialize the store you will need (features, properties or event) :
// Initialization of FF4J
FF4j ff4j = new FF4j();
ff4j.setFeatureStore(new FeatureStoreRedis(redisConnection));
ff4j.setPropertiesStore(new PropertyStoreRedis(redisConnection));
ff4j.setEventRepository(new EventRepositoryRedis(redisConnection));

// Empty Store
ff4j.getFeatureStore().clear();
ff4j.getPropertiesStore().clear();

// Work a bit with CRUD
Feature f1 = new Feature("f1", true, "My firts feature", "Group1");
ff4j.getFeatureStore().create(f1);
        
PropertyString p1 = new PropertyString("p1", "v1");
ff4j.getPropertiesStore().createProperty(p1);
        
ff4j.check("f1");
  • To illustrate the structure in Redis, here are some redis-cli commands:
m127.0.0.1:6379> keys FF4J*
 1) "FF4J_PROPERTY_p1"
 2) "FF4J_FEATURE_f1"
 3) "FF4J_EVENT_-1467803617889-9ac83532-2480-4609-9a76-5793cdb21e1a"
 4) "FF4J_EVENT_-1467803617833-a856c1b9-cedc-4164-815b-55bf9dc3adef"
 5) "FF4J_EVENT_-1467803654139-03a9cb1e-b5b3-4d9f-91dd-0ca5b2cc5a01"
 6) "FF4J_EVENT_-1467803654144-cc591275-a98b-4efe-ab39-4bfd0839aa1e"
 7) "FF4J_EVENT_-1467803617829-4487077f-680a-44ba-b0e2-43e6685dc044"
 8) "FF4J_EVENT_-1467803654599-83af1ce2-05f6-4264-8b53-d1541e8e036c"

127.0.0.1:6379> get FF4J_FEATURE_f1
"{\"uid\":\"f1\",\"enable\":true,\"description\":\"My firts feature\",\"group\":\"Group1\",\"permissions\":[],\"flippingStrategy\":null,\"customProperties\":{}}"

127.0.0.1:6379> get FF4J_PROPERTY_p1
"{\"name\":\"p1\",\"description\":null,\"type\":\"org.ff4j.property.PropertyString\",\"value\":\"v1\",\"fixedValues\":null}"

127.0.0.1:6379> get FF4J_EVENT_-1467803654599-83af1ce2-05f6-4264-8b53-d1541e8e036c
"{\"id\": \"83af1ce2-05f6-4264-8b53-d1541e8e036c\", \"timestamp\":1467803654599, \"hostName\": \"mbp\", \"source\": \"JAVA_API\", \"name\": \"f1\", \"type\": \"feature\", \"action\": \"checkOn\", \"duration\":0}"

127.0.0.1:6379>

Because Redis is very fast it can also be used as cache for other stores slower. For instance, if you need to request features through http (client http store) it would be useful to use a cache to limit overheader. This is a simple way of doing it :

// Definition of slow stores (http, db, ...)
FeatureStore  fStore;
PropertyStore pStore;
        
FeatureCacheProviderRedis fcr = new FeatureCacheProviderRedis(redisConnection);
fcr.setTimeToLive(3600 * 2); // 2 hours
FF4jCacheProxy redisCacheProxy = new FF4jCacheProxy(fStore, pStore, fcr);
ff4j.setFeatureStore(redisCacheProxy);
ff4j.setPropertiesStore(redisCacheProxy);

@since 1.6.4 Due to the slowness of KEYS clause, a new dictionary key FF4J_FEATURE_MAP is provided to speed up things. To migrate to 1.6.4 you have to execute the following:

Migration for Redis key scan from 1.3 to 1.6.4

  1. First place the lua script in the same directory with the name: redis-migration.lua
  2. Place the redis-migration.sh shell script in the same directory.
  3. Execute the script with the parameters {host} {port} for the master of Redis instance/Sentinel. NOTE: This operation is idempotent, since underlying structure is SET and the script ignores self map from the list

redis-migration.lua

local keys = {};
local featureNames = "";
local feature = "FF4J_FEATURE_"
local property = "FF4J_PROPERTY_"
local cursor = "0"
local done = false;

local searchPattern = feature

if ARGV[1] == property then
	searchPattern = property
end

repeat
    local result = redis.call("SCAN", 0, "match", searchPattern .. "*", "count", 200000)
    cursor = result[1];
    keys = result[2];
    for i, key in ipairs(keys) do
    	local featureName = key:gsub(searchPattern, "");
		-- if this script is double run, ignore MAP!
		if featureName ~= "MAP" then
	        if i > 1 then 
				featureNames = featureNames .. ",";
			end
	 		featureNames = featureNames .. featureName;
	    end
	end

    if cursor == "0" then
        done = true;
    end
until done

return featureNames

MongoDB

Mongo v2 & v3

Neo4j

The graph database

EhCache

3.0

HttpClient

Using http client

CommonsConfiguration

The Commons Configuration software library provides a generic configuration interface which enables a Java application to read configuration data from a variety of sources. Commons Configuration provides typed access to single, and multi-valued configuration parameters as demonstrated by the following code.

Double double = config.getDouble("number");
Integer integer = config.getInteger("number");

FF4J proposes several use cases to work with commons configuration.

Use a FF4J PropertyStore as a source for Commons-configuration

You already has set up your application to use commons configurations but feel limited as the existing Configuration are limited to properties and xml files. Use the FF4jConfiguration to inject properties coming from other sources.

// sample object implementing the Configuration of commons-conf
FF4jConfiguration ff4jConf = new FF4jConfiguration(new InMemoryPropertyStore("ff4j-properties.xml"));

// Inject in existing with, for example a composite configuration
CompositeConfiguration config = new CompositeConfiguration();
config.setThrowExceptionOnMissing(true);
config.addConfiguration(ff4jConf);

Use Commons Configuration as input for FF4J property store

If properties are defined in a ff4j PropertyStore, they will be available in the web UI and CLI. AS a consequence you can perform CRUD operations through UI (caution some stores are read-only).

// Use the commons-conf syntax to init properties
Configuration conf = new PropertiesConfiguration("application.properties");

// Init property store
PropertyStore ff4jPropertyStore = new PropertyStoreCommonsConfig(conf);
ff4j.setPropertiesStore(ff4jPropertyStore );
// 

Archaius

Archaius is a framework proposed by Netflix to handle Properties Management Configuration. It leverages on commons-configurations (for best and worst). The integration with ff4j works in 2 ways.

Use the FF4J properties as a source for Archaius

Sample principle as before. Archaius leverage on commons configuration and provides new sources for properties. You can use the FF4JConfiguration and add FF4J properties to the dynamic configuration. Archiaus also introduce the PolledSourceConfiguration mechanism to polled target properties provider and update configuration accordingly. The ff4j-archaius module propose and implementation of PolledSourceConfiguration to work with FF4J. sample code

Use Archiaus for FF4J property store

Sample principle as before with Archaius, define a PropertyStore to work with a dynamic configuration, you will get all properties values in the UI. sample code

// Initialize configuration with the FF4J configuration implementation
FF4jConfiguration ff4jConfig = new FF4jConfiguration(new InMemoryPropertyStore("ff4j-properties.xml"));
// Define the property Store leveraging on archiaus
PropertyStore archaiusStore   = new PropertyStoreArchaius(ff4jConfig);
// Sample FF4J Store
PropertyStore ff4jStore = new InMemoryPropertyStore("ff4j-properties.xml");

// FF4Store as polling Source for Archiaus
PolledConfigurationSource ff4jSource = new FF4jPolledConfigurationSource(ff4jStore);

// Working Thread (polling from ff4j => Archaius)
AbstractPollingScheduler scheduler = new FixedDelayPollingScheduler(0, 1000, true);

// Define configuration with polling and source
DynamicConfiguration dynConfig = new DynamicConfiguration(ff4jSource,scheduler);

// You Can now use the 'dynConfig' in pure Archaius
ConfigurationManager.install(configuration);
  • You Can now use any 'DynamicConfiguration ' as input for FF4J as well
PropertyStore archaiusStore2 = new PropertyStoreArchaius(dynConfig);

You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.