FeatureGuard is an open-source Ruby gem for feature-flagging that uses Redis as a key-value store. However, it can be difficult to find which keys currently stored in Redis are specifically associated with FeatureGuard. To solve for this, we decided to modify the FeatureGuard gem to store its keys in a hash, which could be especially useful if an app were to use hundreds or thousands of FeatureGuard keys.
Background on FeatureGuard
Sample FeatureGuard code:
1
2
3
4
5
6
7
FeatureGuard.enable :my_feature
FeatureGuard.enabled? :my_feature # returns true
FeatureGuard.disable :my_feature
FeatureGuard.enabled? :my_feature # returns false
FeatureGuard.toggle :my_feature
FeatureGuard.enabled? :my_feature # returns true
FeatureGuard can globally enable or disable a feature. For instance, if a feature is currently disabled, FeatureGuard.toggle will function the same as FeatureGuard.enable
, and as FeatureGuard.disable
if the feature is currently enabled. FeatureGuard.enabled?
is self-explanatory: it is used to query if my_feature
is currently enabled.
The fact that we can access FeatureGuard like this across an entire application indicates that FeatureGuard is a singleton, which not only allows FeatureGuard access across the entire application but also guarantees that FeatureGuard behavior will be the same throughout the application.
Sample code using ‘ramps’:
1
2
3
4
5
FeatureGuard.set_ramp :my_feature, 30.5 # 30.5%
FeatureGuard.bump_ramp :my_feature, 12 # 30.5 + 12 = 42.5%
FeatureGuard.bump_ramp :my_feature # 42.5 + 10 = 52.5%
FeatureGuard.ramp_val :my_feature # 52.5
FeatureGuard also has ramp values. set_ramp
, bump_ramp
,ramp_val
are getter and setter methods for the ramp value of my_feature
. We don’t actively use these in our main application, but these methods are most likely intended for use in A/B Testing.
How does Lumosity use FeatureGuard?
At Lumosity, we use FeatureGuard in a wide variety of contexts, including various initializers we have for services such as Mailfiend, in a number of research-related controllers and methods, and even in our purchasing system.
We mainly use FeatureGuard for hiding large portions of code that we don’t yet want active in production, and is particularly useful for hiding a feature that has multiple moving parts.
A simple example:
1
2
3
4
5
if FeatureGuard.enabled? :new_feature
do_something
else
do_nothing
end
When the feature is ready to go live, we can execute FeatureGuard.enable :new_feature
from a Rails console and BAM! - we are good to go!
Modifying FeatureGuard functionality
The next steps toward modifying FeatureGuard functionality are as follows:
- Figure out how this information is stored.
- Modify the gem to store values in a hash, rather than in a key-value store.
- Write and run a migration script for our existing FeatureGuard keys before FeatureGuard is upgraded.
FeatureGuard is a relatively small library. The file structure is as follows:
The meat of the gem can be found in guard.rb. From this file we learned that:
- FeatureGuard stores each flag under a
flag_key
, and each ramp under aramp_key
. - The
flag_key
andramp_key
have either ‘fgf_’ or ‘fgr_’ prepended to their name (which also happens to be the feature name). - FeatureGuard uses Redis’ set method to assign a key a value of 0 or 1, and Redis’ get to check an assigned value (as in
enabled?
). - Therefore,
FeatureGuard.enable :feature_name
is identical toredis.set('fgf_feature_name', 1)
What we need to do now is change Redis.set(key, value)
to Redis.hset(hash, key, value)
, and Redis.get(key, value)
to Redis.hget(hash, key, value)
. Also, since we’re now dealing with hashes, we earn the added bonus of being able to query all FeatureGuard flags and ramps via Redis.hkeys(hash)
. However, since it does not make much sense to ask for all flags and ramps via one method, we opted to implement both a FeatureGuard.all_flags
and FeatureGuard.all_ramps
method. We introduced all of these changes in this pull request.
Upgrading FeatureGuard
We asked our DevOps engineers to scan all of our Redis keys for anything beginning with the fgf_
or fgr_
prefix. We then ran the following script to restructure the keys into the same format used by our updates to FeatureGuard.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# After bumping the FeatureGuard version, we migrated old keys to the same format as in FeatureGuard 0.2.1, allowing us to safely upgrade.
fgf_keys = %w{ [...] }
fgr_ramps = %w{ [...] }
# See https://github.com/tdumitrescu/feature_guard/blob/master/lib/feature_guard.rb
flags_hkey = 'featureguard_flags'
ramps_hkey = 'featureguard_ramps'
fgf_keys.each do |key|
#do this to trim the prefix off of the string
redis.hset(flags_hkey, key[4..-1], redis.get(key))
end
fgr_ramps.each do |key|
redis.hset(ramps_hkey, key[4..-1], redis.get(key))
end
Note that we did not delete the old keys in this script. This was to ensure that if we needed to rollback to an earlier version of FeatureGuard, the original version of FeatureGuard would still function. Once we were sure that no issues had been introduced by the FeatureGuard upgrade, we ran a similar script a few weeks later to clean up the old keys.
Thanks to these changes we have added to FeatureGuard, we now have an easy way to retrieve all keys associated with FeatureGuard, thus enabling a dev to better track what features or ramps are in use by an application.
Our pull request was accepted and included in version 0.2.1 of FeatureGuard. To all of you reading this post, I hope the script proves useful should you need to upgrade from 0.1.2 to the latest version.
Happy code reading!