Flippant
Fast feature toggling for Elixir applications, backed by Redis.
Installation
If available in Hex, the package can be installed as:
Add flippant and redix to your list of dependencies in
mix.exs
:def deps do [{:flippant, "~> 0.1"}, {:redix, "~> 0.4"}] end ```
Ensure flippant and redix is started before your application:
def application do [applications: [:redix, :flippant]] end ```
Set an adapter within your
config.exs
:config :flippant, adapter: Flippant.Adapters.Redis, redis_opts: [url: System.get_env("REDIS_URL"), name: :flippant] ```
Usage
Flippant composes three constructs to determine whether a feature is enabled:
- Actors - An actor can be any value, but typically it is a
%User{}
or some other struct representing a user. - Groups - Groups are used to identify and qualify actors. For example, “everybody”, “nobody”, “admins”, “staff”, “testers” could all be groups names.
- Rules - Rules represent individual features which are evaluated against actors and groups. For example, “search”, “analytics”, “super-secret-feature” could all be rule names.
Let’s walk through setting up a few example groups and rules. You’ll want to establish groups at startup, as they aren’t likely to change (and defining functions from a web interface isn’t wise).
Groups
First, a group that nobody can belong to. This is useful for disabling a feature without deleting it:
Flippant.register("nobody", fn(_actor, _values) -> false end)
Now the opposite, a group that everybody can belong to:
Flippant.register("everybody", fn(_actor, _values) -> true end)
To be more exclusive and define staff only features we need a “staff” group:
Flippant.register("staff", fn
nil, _values -> false
actor, _values -> actor.staff?
end)
Lastly, we’ll roll out a feature out to a percentage of the actors:
Flippant.register("adopters", fn
_actor, [] -> false
%{id: id}, buckets -> rem(id, 10) in buckets
end)
With some core groups defined we can set up some rules now.
Rules
Rules are comprised of a name, a group, and an optional set of values. Starting with a simple example that builds on the groups we have already created, we’ll enable the “search” feature:
# Any staff can use the "search" feature
Flippant.enable("search", "staff")
# 30% of "adopters" can use the "search" feature as well
Flippant.enable("search", "adopters", [0, 1, 2])
Because rules are only built of binaries and simple data they can be defined or refined at runtime. In fact, this is a crucial part of feature toggling. With a web interface rules can be added, removed, or modified.
# Turn search off for adopters
Flippant.disable("search", "adopters")
# On second thought, enable it again for 10%
Flippant.enable("search", "adopters", [3])
With a set of groups and rules defined we can check whether a feature is enabled for a particular actor:
staff_user = %User{id: 1, staff?: true}
early_user = %User{id: 2, staff?: false}
later_user = %User{id: 3, staff?: false}
Flippant.enabled?("search", staff_user) #=> true, staff
Flippant.enabled?("search", early_user) #=> false, not an adopter
Flippant.enabled?("search", later_user) #=> true, is an adopter
If an actor qualifies for multiple groups and any of the rules evaluate to true that feature will be enabled for them. Think of the “nobody” and “everybody” groups that were defined earlier:
Flippant.enable("search", "everybody")
Flippant.enable("search", "nobody")
Flippant.enabled?("search", %User{}) #=> true
Breakdown
Evaluating rules requires a round trip to the database. Clearly, with a lot of rules it is inefficient to evaluate each one individually. There is a function to help with this exact scenario:
Flippant.enable("search", "staff")
Flippant.enable("delete", "everybody")
Flippant.enable("invite", "nobody")
Flippant.breakdown(%User{id: 1, staff?: true}) #=> %{
"search" => true,
"delete" => true,
"invite" => false
}
The breakdown is a simple map of binary keys to boolean values. This is extremely handy for single page applications where you can serialize the breakdown on boot or send it back from an endpoint as JSON.
Configuring Groups
Group definitions are stored in a process, which requires the Flippant
application to be started. That means they can’t be defined within a
configuration file and should instead be linked from Application.start/2
.
You can make Flippant.register/2
calls directly from the application
module, or put them into a separate module and start it as a temporary worker.
defmodule MyApp do
use Application
def start(_type, _args) do
import Supervisor.Spec, warn: false
children = [
worker(MyApp.Flippant, [], restart: :temporary)
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
end
Here you can see the worker was defined with restart: :temporary
. Now, within
the MyApp.Flippant
module:
defmodule MyApp.Flippant do
def start_link do
Flippant.register("nobody", &nobody?/2)
Flippant.register("everybody", &everybody?/2)
:ignore
end
def nobody?(_, _), do: false
def everybody?(_, _), do: true
end
Testing
To avoid touching Redis while testing you can use the Memory
adapter. Within
config/test.exs
override the :adapter
:
config :flippant, adapter: Flippant.Adapters.Memory
The memory adapter behaves identically but will clear out whenever the application is restarted.
Customizing Value Serialization
As seen above in [Usage][], it is possible to store a value along with a rule. Values can be any type of data structure, including lists, maps, or even modules. This adds a great deal of additional power to group evaluation, but it means that the values must be serialized in a high fidelity way.
By default, values are stored using Erlang’s binary term storage. This
works perfectly fine, but isn’t especially readable and isn’t compatible with
other languages. If you’d prefer to use JSON
or MessagePack
instead you can
provide a custom serializer and configure Flippant to use that instead. For
example, to use MessagePack via the Msgpax
libary:
defmodule MyApp.Serializer do
@behaviour Flippant.Serializer
def dump(value), do: Msgpax.pack!(value)
def load(value), do: Msgpax.unpack!(value)
end
Then, within config.exs
set the serializer:
config :flippant, serializer: MyApp.Serializer
License
MIT License, see LICENSE.txt for details.