/alchemy

Perform experiments in production

Primary LanguageElixirMIT LicenseMIT

Alchemy

Hex.pm Hex.pm

Perform refactoring experiments in production. Based on Scientist


Installation

  1. Add alchemy to your list of dependencies in mix.exs:
def deps do
  [{:alchemy, "~> 0.0.1"}]
end
  1. Ensure that alchemy is started before your application:
def application do
  [applications: [:alchemy]]
end

Perform some experiments

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.

Publish results

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.

Multiple Candidates

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

Comparing results

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

Contributing

Pull Requests and Issues are greatly appreciated!