Feature Flags with Rollout
Role-based authorization is a common requirement in modern web applications. In the Rails ecosystem, there are several great open source libraries that provide this (see devise’s roles, cancan, and rolify).
Feature flagging is a little bit different than roles. Features can be flipped on and off regardless of a users’ state. This means that if we want to test a feature for 50% of users, we shouldn’t need to move those users into a different role. We should just be able to declare that this feature is now flipped on for 50% of all users. The difference is subtle but important.
When BitLove approached this problem several years ago, there weren’t any good open source solutions for this. As a result we decided to roll our own and called it rollout. James Golick pioneered this project before I had joined the team, and it has since gained a large amount of popularity. It has even been ported into other languages (see proclaim, PHP rollout, and shoutout).
The premise of the rollout library is simple. Give it an object that implements a get/set interface (Redis, hash, etc.), and you can toggle features based on granularity such as user ID, group, percentage, and global.
The following illustrates how this works:
redis = Redis.new
rollout = Rollout.new(redis)
user = OpenStruct.new(id: 32, moderator?: true)
rollout.active?(:shiny_new_feature) # => false
rollout.active?(:shiny_new_feature, user) # => true
rollout.active?(:shiny_new_feature, user) # => false
rollout.define_group(:moderators) do |user|
rollout.active?(:shiny_new_feature_v2, user) # => true
rollout.active?(:shiny_new_feature_v3, 10) # => true
rollout.active?(:shiny_new_feature_v3, 11) # => false
rollout.active?(:shiny_new_feature_v3, 12) # => false
rollout.active?(:shiny_new_feature_v3, 13) # => true
rollout.active?(:shiny_new_feature_v4) # => true
rollout.active?(:shint_new_feature_v4, user) # => true
Rather than keep track of each user individually, rollout uses some clever encoding and math to make it quite optimal in both space and time efficiency. The only time specific user IDs are stored and queried are when you’re working with a granularity of user ID. This is important for
websites like FetLife that have millions of users.
The serialization of a feature is defined here:
This means that each feature only takes up one key of storage, for which it stores a string. Consult your storage engine’s documentation to figure out how much memory that might be. It should be very, very small.
We can clearly see how users and groups are calculated: they’re just read straight from storage. But how about percentage? Given a user ID, how are we calculating if it falls into a percentage?
Percentage-based feature distribution uses CRC32 with user IDs as function input. We know that hashing algorithms do not distribute perfectly (none of them do). However, as long as you’re dealing with an input space of at least a few thousand unique values, it tends to distribute well enough. The following implements the distribution check:
Zlib.crc32(user_id_for_percentage(user)) % 100 < @percentage
Rollout is a nifty library. It’s extremely simple (you can find the latest implementation in just one file), and yet it allows us to enforce complex cases of feature toggling. It’s also great for using in tandem with the circuit breaker
pattern. See the degrade library for an example of what this implementation could look like.