29 February 2016

Microservices in Phoenix: Part 2

In the Part 1 of this series I talked about how to take a Phoenix application and break off bits into Microservices. I got as far along as extracting some code from a Phoenix controller into an OTP server. In this installment, we’ll take the next logical step and pull our OTP server out into a whole separate OTP application.

Before I get into the code, though, I thought it might be useful to step back and define the term Microservices and talk about what makes a good candidate for extracting.

You keep using that word

Here’s what I mean when I use the word: a bit of functionality that can be separately deployed and scaled independently of the main application. Notice what I’m not putting in this definition: anything about transport mechanisms, data formats, or protocol. I don’t care a bit for my purposes about HTTP, JSON, or XML. With this definition, OTP servers (and applications) fit the bill perfectly. They are separate processes with their own state and can be deployed and scaled separately. OTP applications in particular give me really powerful tools for managing all this.

In the case of our imaginary, oversimplified, auction application receiving bids and notifying connected clients of a new bid make a lot of sense as a candidate for extracting into a microservice. We can scale out our system by having a process (or processes) manage each item potentially. This is probably as granular as we want to get: having the management of which bid is winning gets complicated to distribute. It’s ideal to have a single process be in charge of deciding who the winner is.

Let’s shift gears back to our code. We have our OTP server, and now we’re ready to take the next step and move it into a separate application. I found doing this to be surprisingly simple. We start by creating a new application by running:

mix auction_server --sup

This creates a new elixir application (note that is not a Phoenix app). The code for this extracted OTP app is in this repo. The --sup flag tells mix to create our OTP application with a supervisor. I’m not going to cover supervisors in any great detail here, but suffice to say this is the entry point to our microservice and is responsible for starting and managing whatever processes we need to run. Here’s the supervisor that mix will generate for us:

defmodule AuctionServer do
  use Application

  # See https://elixir-lang.org/docs/stable/elixir/Application.html
  # for more information on OTP Applications
  def start(_type, _args) do
    import Supervisor.Spec, warn: false

    children = [
      # Define workers and child supervisors to be supervised
      # worker(AuctionServer.Worker, [arg1, arg2, arg3]),
    ]

    # See https://elixir-lang.org/docs/stable/elixir/Supervisor.html
    # for other strategies and supported options
    opts = [strategy: :one_for_one, name: AuctionServer.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

Note the empty list of children. That’s where we’ll put the processes we want to have supervised. Let’s look back at our main application. The main thing we want to move is AuctionServer. We can start by simply moving this file into our newly created application. However, I caused a slight problem for us when I named our new application AuctionServer. Let’s fix this by renaming our server process AuctionServer.BidServer. The result looks something like this:

defmodule AuctionServer.BidServer do
  use GenServer

  alias AuctionServer.Repo
  alias AuctionServer.Bid

  # Client API

  def start_link(opts \\ []) do
    {:ok, pid} = GenServer.start_link(__MODULE__, [], opts)
  end

  def new_bid(bid_params) do
    GenServer.call(:bid_server, {:new_bid, bid_params})
  end

  # Server implementation

  def init([]) do
    bids = Repo.all(Bid)
    {:ok, bids}
  end

  def handle_call({:new_bid, bid_params}, _from, bids) do
    changeset = Bid.changeset(%Bid{}, bid_params)
    case Repo.insert(changeset) do
      {:ok, bid} ->
        Auctioneer.Endpoint.broadcast! "bids:max", "change", Auctioneer.BidView.render("show.json", %{bid: bid})
        {:reply, {:ok, bid}, [bid | bids]}
      {:error, changeset} ->
        {:reply, {:error, changeset}, bids}
    end
  end

end

It’s pretty much exactly the same as before other than the rename. At the top of the file, notice those two aliases: they refer to Bid struct and the Repo that persists Bids to the database. We’ll want to move both of those into our new application as well. Doing so is as simple as copying the files and renaming Auctioneer to AuctionServer.

We’ll also need to add ecto and postgrex to our list of dependencies. We do this in mix.exs:

...
  defp deps do
    [
      {:ecto, "> 0.0.0"},
      {:postgrex, "> 0.0.0"}
    ]
  end
end

Now we’re ready to tell our supervisor how to start our processes. We’ll need to start both our Repo process to manage database connectivity and our BidServer. We do this by adding them to our list of workers:

...
    children = [
      # Define workers and child supervisors to be supervised
      # worker(AuctionServer.Worker, [arg1, arg2, arg3])
      worker(AuctionServer.Repo, []),
      worker(AuctionServer.BidServer, [[name: :bid_server]])
    ]

    # See https://elixir-lang.org/docs/stable/elixir/Supervisor.html
    # for other strategies and supported options
    opts = [strategy: :one_for_one, name: :auction_server_supervisor]
...

I’ve added a name for BidServer to make it a little easier to find in observer. I changed the name of the supervisor in opts for the same reason.

Next, we should be ready change our original Phoenix app, Auctioneer, to use our AuctionServer app. We do this by adding it to mix.exs, both as a dependency:

defp deps do
  [
    {:ex_machina, "~> 0.5"},
    {:plug_cors, "~> 0.7.3"},
    {:phoenix, "~> 1.0.2"},
    {:phoenix_ecto, "~> 1.1"},
    {:postgrex, ">= 0.0.0"},
    {:phoenix_html, "~> 2.1"},
    {:phoenix_live_reload, "~> 1.0", only: :dev},
    {:cowboy, "~> 1.0"},
    {:auction_server, path: "../auction_server", env: Mix.env}
  ]
end

And also to our list of applications:

def application do
  [mod: {Auctioneer, []},
   applications: [:ex_machina, :phoenix, :phoenix_html, :cowboy, :logger,
                  :phoenix_ecto, :postgrex, :auction_server]]
end

This will both bring in the code for our new application and ensure that its supervision tree is started. We’re ready to make the change in our controller next. In BidController we should just be able to change the call to AuctionServer to AuctionServer.BidServer.

def create(conn, %{"bid" => bid_params}) do
  case AuctionServer.BidServer.new_bid(bid_params) do
    {:ok, bid} ->
      conn
      |> put_status(:created)
      |> put_resp_header("location", bid_path(conn, :show, bid))
      |> render("show.json", bid: bid)
    {:error, changeset} ->
      conn
      |> put_status(:unprocessable_entity)
      |> render(Auctioneer.ChangesetView, "error.json", changeset: changeset)
  end
end

At this point, we should be good to go. If we launch our main phoenix app it should launch our AuctionServer application as well. We can see this in the observer if we bring up the applications tab:

observer_screenshot1

Sure enough, we see our auction_server in the list of applications and when we highlight it, we see only the processes for the auction_server OTP app, not for the main auctioneer phoenix app. Other than that, things should work as before.

Well, we’ve come to the end of our journey for now. There’s plenty more we could do from here: we still have some calls to Auctioneer from AuctionServer.BidServer. It would be nice to refactor those out to make our microservice more independent. It’s also worth pointing out that there is a handy feature in mix for creating projects that consist of multiple OTP apps: the umbrella project. Because I deliberately wanted to show what it looks like to pull an OTP app after the fact I chose not to use this feature, but it would have made some things even easier.

I hope you’ve enjoyed following along. We’re excited about Elixir and Phoenix, and if you’d like to learn more about both of these great technologies, we’ll be in San Francisco March 21-23 doing a three-day, hands-on class. We’d love to see you there!

Heads up! This article may make reference to the Gaslight team—that's still us! We go by Launch Scout now, this article was just written before we re-introduced ourselves. Find out more here.

Related Posts

Want to learn more about the work we do?

Explore our work

Ready to start your software journey with us?

Contact Us