Perform refactoring experiments in production. Based on Scientist
- Add alchemy to your list of dependencies in
mix.exs
:
def deps do
[{:alchemy, "~> 0.0.1"}]
end
- Ensure that alchemy is started before your application:
def application do
[applications: [:alchemy]]
end
Lets say that you have a controller that returns a list of users, and you want to change how that list of users is fetched from the database. Unit tests can help you but they can only account for examples that you've currently considered. Alchemy allows you to test your new code live in production.
defmodule MyApp.UserController do
import Alchemy.Experiment
def index(conn) do
users =
experiment("users-query")
|> control(&old_query/0)
|> candidate(&new_query/0)
|> run
render(conn, "index.json", users: users)
end
defp old_query do
# ...
end
defp new_query do
# ...
end
end
Both the control and the candidate are randomized and run concurrently. Once all of the behaviours have been observed the control is returned so that its easy to continue pipelining with other functions. All of the execution times for the duractions are also measured.
The experiment is now running but its not being published anywhere. To do that we'll need to create a new module with a publish function:
defmodule MyApp.ExperimentPublisher do
require Logger
alias Alchemy.Result
def publish(result=%Alchemy.Result{}) do
name = result.experiment.name
control = result.control
candidate = hd(result.observations)
mismatched = Result.mismatched?(result)
Logger.debug """
Test: #{name}
Match?: #{!Result.mismatched?(result)}
Control - value: #{control.value} | duration: #{control.duration}
Candidate - value: #{candidate.value} | duration: #{candidate.duration}
"""
end
end
And tell Alchemy where to send your results:
# config/config.exs
use Mix.Config
config :alchemy,
publish_module: MyApp.ExperimentPublisher
The publish function allows you to publish your results in whatever makes most sense for your application. You could persist them in ETS tables or stash them in Redis.
It's possible to create experiments with multiple candidates:
def some_query do
experiment("test multiple candidates")
|> control(&old_query/0)
|> candidate(&foo_query/0)
|> candidate(&bar_query/0)
|> candidate(&baz_query/0)
|> run
end
By default alchemy compares results with ==
. You can override this by supplying your own comparator:
def user_name do
experiment("test name is correct")
|> control(fn -> %{id: 1, name: "Alice"} end)
|> candidate(fn -> %{id: 2, name: "Alice"} end)
|> comparator(fn(control, candidate) -> control.name == candidate.name end)
|> run
end
Pull Requests and Issues are greatly appreciated!