Rollout is a popular Ruby gem that helps manage the sometimes tedious nature of feature flagging. It was originally architected with Redis as a dependency. While we do love Redis immensely, we felt now was not quite the time to add yet another piece of infrastructure to our application. Thankfully, a semi-recent update for Rollout removed the Redis dependency and allows the end user to use any key-value store. The only requirements are that an instance of the store needs to respond to the get, set, and del methods.

The Decision

Given that we are not ready to add another piece of infrastructure to our stack, we decided to create a wrapper for PostgreSQL’s Hstore feature. We created a new ActiveRecord model called FeatureFlag that has a singular attribute (migration and model below).

class CreateFeatureFlags < ActiveRecord::Migration
  def change
    create_table :feature_flags do |t|
      t.hstore :data
    end
  
    execute 'CREATE INDEX feature_flags_data ON feature_flags USING GIN(data)'
  end
end

class FeatureFlag < ActiveRecord::Base
end

So far, so good. We didn’t necessarily need an empty AR model and could have simply written some SQL statements inside of some PG connections, but most of our business logic resides in AR.

Implementing the Store

Now for the fun part! As stated above, an instance of our store needs to be able to handle the get, set, and del methods appropriately. Just from perusing the rollout codebase, the methods are defined as the following: - get takes a key argument and returns the value for the key - set takes two arguments (key, value) and creates or updates the key-value pair - del takes a key argument and deletes the key-value pair

Below is the code that handles these scenarios.

class RolloutPostgresStore
  def initialize(model, attribute)
    @model = model
    @attribute = attribute
  end

  def get(key)
    if flag = @model.where("#{@attribute} ? '#{key}'").first
      flag.send(@attribute)[key]
    end
  end

  def set(key, value)
    current = get(key)

    if current.nil?
      create_feature_flag(key, value)
    else
      update_flag(key, value)
    end
  end

  def del(key)
    if flag = @model.where("#{@attribute} ? '#{key}'").first
      flag.delete
    end
  end

  private
  def create_feature_flag(key, value)
    flag = @model.new
    flag.send("#{@attribute}=", { key => value })
    flag.save
  end

  def update_flag(key, value)
    flag = @model.where("#{@attribute} ? '#{key}'").first
    flag.send(@attribute)[key] = value
    flag.send("#{@attribute}_will_change!")
    flag.save
  end
end

It is relatively straightforward. We really utilize ActiveRecord a ton here but I think it’s an okay decision given our data structures above. Like any engineer that adheres to the values of testing, I also threw some tests in to cover the various scenarios for the RolloutStore. They are below.

ROLLOUT = Rollout.new(RolloutPostgresStore.new(FeatureFlag, 'data'))

describe RolloutPostgresStore, '.new' do
  it 'assigns the model instance variable' do
    store = RolloutPostgresStore.new(FeatureFlag, 'data')
    expect(store.instance_variable_get(:@model)).to be FeatureFlag
  end

  it 'assigns the attribute instance variable' do
    store = RolloutPostgresStore.new(FeatureFlag, 'data')
    expect(store.instance_variable_get(:@attribute)).to eql 'data'
  end
end

describe RolloutPostgresStore, '#get' do
  it 'returns nil for not having found a FeatureFlag with the given key' do
    store = RolloutPostgresStore.new(FeatureFlag, 'data')
    expect(store.get('test_key')).to be nil
  end

  it 'returns the value for a given key when a FeatureFlag is found' do
    ROLLOUT.activate_percentage(:search, 20)

    store = RolloutPostgresStore.new(FeatureFlag, 'data')
    expect(store.get('feature:search')).to eql '20||'
  end
end

describe RolloutPostgresStore, '#set' do
  it 'receives the create_feature_flag message for not having found a flag' do
    expect_any_instance_of(RolloutPostgresStore).to receive(:create_feature_flag).at_most(:once)

    RolloutPostgresStore.new(FeatureFlag, 'data').set('search', '20||')
  end

  it 'receives the update_flag message for having found a flag' do
    ROLLOUT.activate_percentage(:search, 20)

    expect_any_instance_of(RolloutPostgresStore).to receive(:update_flag).at_most(:once)

    RolloutPostgresStore.new(FeatureFlag, 'data').set('feature:search', '20||123')
  end
end

describe RolloutPostgresStore, '#del' do
  it 'returns the object deleted for finding the key to delete' do
    ROLLOUT.activate_percentage(:search, 30)

    # There is only one feature flag per the activation above
    feature_flag = FeatureFlag.first
    store = RolloutPostgresStore.new(FeatureFlag, 'data')
    expect(store.del('feature:search')).to eq feature_flag
  end

  it 'returns nil for not being able to find the key to delete' do
    expect(RolloutPostgresStore.new(FeatureFlag, 'data').del('feature:test')).to be nil
  end
end

Conclusion

We wanted to create something with as small a footprint as possible without having to add Redis to our stack for feature flagging. This little piece of software seems to fit exactly that. The code is available on Github and RubyGems.

Discuss

Discus on HN with us!