FunWithFlags
FunWithFlags, the Elixir feature flag library.
This library is still a work in progress and is not ready to use
This readme refers to the master
branch. For the latest version released on Hex, please check the readme published with the docs.
Goals
This library is meant to be an OTP application that provides an Elixir API to toggle and query feature flags and an (authenticated) web UI for administrators.
It should store feature flag information in Redis for persistence and syncronize different nodes, but it should also maintain a local cache in an ETS table for fast lookup. When flags are added or toggled, nodes should be notified (via Redis PubSub or polling) and update their local ETS representation.
Different kind of feature flags should be supported:
- simple (on, off);
- actors (on or off for specific structs or data);
- groups (or or off for structs or data that satisfy a condition).
The planned functionality of this library is heavily inspired by the flipper Ruby gem, although with a focus on:
- decreasing the load on Redis (feature flags are not toggled that often, there is no need to query Redis for each check);
- making it more reliable (it should keep working with the last cached values if Redis becomes unavailable, although nodes can be out of sync).
Just as Elixir and Phoenix are meant to scale better than Ruby on Rails with high levels of traffic and concurrency, FunWithFlags should aim to be more scalable than Flipper.
Status and Roadmap
Done
- Simple boolean flags: either ON or OFF.
- Flags are persisted in Redis and available on application restart.
- In-process ETS cache. On lookup, the library checks the cache first. If the ETS table doesn’t contain a flag, it falls back to Redis and copies the value into the cache. Subsequent lookups won’t hit Redis. The ETS table is empty when the application starts. Writes to the ETS table are managed by a GenServer and are serial, while any other process can read from it concurrently.
- Creating or toggling a flag will update both the ETS cache and Redis.
- Both the ETS cache and the Redis connection are in a supervision tree. The Redix adapter will try to reconnect to Redis if the connection is lost.
- If the connection to Redis is lost, the application will continue to work with the known values from the ETS cache. If an unknown flag is looked up when Redis is unavailable it will default to the disabled state.
- Several nodes can connect to the same Redis and share the flag settings. Each one will hit Redis the first time a flag is looked up, and then will populate its ETS cache.
- The ETS cache is enabled by default, but it can be disabled to only use Redis.
- The ETS cache supports a global TTL, expressed in seconds. It defaults to 900s (15 minutes). After expiration, flags are re-fetched from Redis. This allows multiple nodes to use the same redis, and slowly acquire and cache flags that have been changed by another node.
Next / Problems
When two or more nodes are using the same Redis, and one of them updates a flag that the others have already cached, or creates a flag that the others have already looked up (and cached as “disabled”), then the other nodes will not be notified of the changes.
Current mitigations:
- It’s possible to disable the cache
- the cache has a configurable TTL
Real solution, to be added:
- Use Redis PubSub to emit change notifications.
- Implement other “gates”: at least actors and groups.
- Add some optional randomness to the TTL, so that Redis doesn’t get hammered at constant intervals after a server restart.
Configuration
The library can be configured in host applications through Mix and the config.exs
file. This example shows the default values:
config :fun_with_flags, :cache,
enabled: true,
ttl: 900 # in seconds
# the Redis options will be forwarded to Redix
config :fun_with_flags, :redis,
host: 'localhost',
port: 6379,
database: 0
Usage
Still a work in progress, expect breaking changes.
FunWithFlags.enabled?(:cool_new_feature)
false
{:ok, true} = FunWithFlags.enable(:cool_new_feature)
FunWithFlags.enabled?(:cool_new_feature)
true
{:ok, false} = FunWithFlags.disable(:cool_new_feature)
FunWithFlags.enabled?(:cool_new_feature)
false
Installation
The package can be installed by adding fun_with_flags
to your list of dependencies in mix.exs
:
def deps do
[{:fun_with_flags, "~> 0.0.6"}]
end
Test
This library depends on Redis and you’ll need it installed on your system in order to run the tests. A test run will create a few keys in the Redis db number 5 and then remove them, but it’s safer to start Redis in a directory where there is no dump.rdb
file you care about to avoid issues.
Start Redis with:
$ redis-server
Then:
$ mix test.all
The test.all
task will run the default mix test
task with MIX_ENV=test
, and then will switch to a custom MIX_ENV=test_no_cache
environment where the ETS cache is disabled and re-run the integration tests.