Track-switching in a large Elixir web application

Large, continual changes made possible using a feature toggle

9elements
9elements
Feb 20, 2019 · 12 min read
Image for post
Image for post

The purpose of our feature toggle


A simple toggle router, and storage for the toggle

defmodule MyApplication.Switchboard
def use_new_logic? do
false
end
end
defmodule MyApplication.Switchboard
def use_new_logic? do
case Application.get_env(:my_application, :use_new_logic?) do
nil ->
value = get_value_from_redis() || false
Application.put_env(:my_application, :use_new_logic?, value)

value

value -> value
end
end

def use_new_logic!(value) do
put_value_in_redis(value)
Application.put_env(:my_application, :use_new_logic?, value)
end
end
value = get_value_from_redis()
Application.put_env(:my_application, :use_new_logic?, value)

Testing with a feature toggle


Passing the toggle value down in a request test

# In the test setup:
Process.put(:test_use_new_logic?, true)

# New switchboard code:
defmodule MyApplication.Switchboard
def use_new_logic? do
case Process.get(:test_use_new_logic?) do
nil ->
# old switchboard code here (persistence and cache)

test_value -> test_value
end
end

...
end
defmodule SomeTest do
use ExUnit.Case

@moduletag tag1: value1 # for all tests in the module

@tag tag2: value2 # only for this test
test "something" do
...
end
end
defmodule FeatureToggleTest do
defmacro __using__(_) do
setup tags do
Process.put(:test_use_new_logic?, tags.use_new_code?)
end
end
end
defmodule SomeRequestTest do
use MyApplication.ConnTest
use FeatureToggleTest

@tag use_new_code?: true
test "something" do
...
end
end

Passing the toggle value from a full-stack test

The SQL Sandbox already does this!

defmodule MyApplication.FeatureCase do
use ExUnit.CaseTemplate

using do
quote do
... # aliases and imports
end
end

setup tags do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(YourApp.Repo)

unless tags[:async] do
Ecto.Adapters.SQL.Sandbox.mode(YourApp.Repo, {:shared, self()})
end

metadata = Phoenix.Ecto.SQL.Sandbox.metadata_for(YourApp.Repo, self())
# Wallaby specific, but looks almost the same when using Hound
{:ok, session} = Wallaby.start_session(metadata: metadata)
{:ok, session: session}
end
end

Step 2: Mark tests to use old or new code

Step 3: Extract the metadata and pass the flag on to the toggle router

defmodule MyApp.Endpoint do
use Phoenix.Endpoint, otp_app: :my_app

if Application.get_env(:my_app, :sql_sandbox) do
plug Phoenix.Ecto.SQL.Sandbox
plug :extract_feature_toggle # <-- ours!
end

def extract_feature_toggle(conn, _) do
conn
|> get_req_header("user-agent")
|> List.first
|> Phoenix.Ecto.SQL.Sandbox.decode_metadata
|> case do
%{use_new_code?: flag} ->
Process.put(:test_use_new_logic?, flag)
_ ->
# No metadata was passed. Happens when hit by request test,
# not end-to-end test. Do nothing.
:ok
end

conn
end

...
end

Conclusions and final thoughts

With several years of experience in building and maintaining Elixir applications, we can help you build applications that can change as your business does. Get in touch!


9elements

We craft digital products and services.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch

Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore

Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store