GenServer refactor

Published October 20, 2018 by Toran Billups

In part 2 of the url shortener adventure we skip ahead to take advantage of server processes in Elixir.

Part 2: GenServer

Now that we have a working url shortener it's time to build the process phoenix will use when we start to implement the web interface at some point down the road. GenServer is the primitive we will use to do the heavy lifting for us. Start by adding a module called `EX.Worker`. This worker will listen for messages and shorten urls using the existing module `Shortener`.

    defmodule EX.Worker do
      use GenServer
      def start_link(args) do
        GenServer.start_link(__MODULE__, :ok, args)
      @impl GenServer
      def init(:ok) do
        state =
        {:ok, state}
      def get(pid, hash) do, {:get, hash})
      def put(pid, hash, url) do
        GenServer.cast(pid, {:put, hash, url})
      @impl GenServer
      def handle_call({:get, hash}, _timeout, state) do
        {:reply, Shortener.get_url(state, hash), state}
      @impl GenServer
      def handle_cast({:put, hash, url}, state) do
        new_state = Shortener.create_short_url(state, hash, url)
        {:noreply, new_state}

A lot to unpack here so let's start with the public api for the client code. The `get` and `put` methods are what other processes will use to interact with this GenServer. The functions `handle_call` and `handle_cast` are considered server callbacks.

Note: if organizing both client and server code together in the same module seems strange at first, know that you are not alone. I can still remember wrestling with this idea for some time before I fully understand how message passing worked with the GenServer primitive.

Warning: blatant hand-waving ahead

Without a further deep dive into processes and OTP you might accept this as encapsulation for now and assume the runtime has more to offer, with regard to concurrency, as a result of this implementation. If you want a legit resource that covers this topic in great detail be sure to checkout the Manning book Elixir in Action, Second Edition by Saša Jurić.

The `@impl` annotations you see above help the compiler warn you about incorrect server callback signatures for `GenServer`. If you remove the `_timeout` and `state` parameters from `handle_call` for example this warning will show up if you try to compile the application.

warning: got "@impl GenServer" for function handle_call/1 but this behaviour does not specify such callback. The known callbacks are:

  • GenServer.handle_call/3 (function)
  • GenServer.handle_cast/2 (function)
  • GenServer.handle_info/2 (function)
  • GenServer.init/1 (function)
  • GenServer.terminate/2 (function)

To see this module in action we next write a unit test to exercise the GenServer. You can run this from the command line using `mix test`.

    defmodule EX.WorkerTest do
      use ExUnit.Case, async: true
      setup do
        pid = start_supervised!(EX.Worker)
        %{pid: pid}
      test "get and put work", %{pid: pid} do
        assert EX.Worker.get(pid, "x") === :undefined
        EX.Worker.put(pid, "x", "")
        assert EX.Worker.get(pid, "x") === ""

Before we can spin up the application with IEx first update the file `mix.exs` to explicitly tell Elixir about our new `Application` (module) with the name `EX.Application`.

    def application do
        extra_applications: [:logger],
        mod: {EX.Application, []}

Next open the file `lib/example.ex` and update the module name to `EX.Application`. Also add the `use Application` statement and a `start` method that spins up the process we are using with the name `EX.Worker`

    defmodule EX.Application do
      use Application
      def start(_type, _args) do
        EX.Worker.start_link(name: EX.Worker)

To play around with this module in IEx run this command `iex -S mix run`

    pid = GenServer.whereis(EX.Worker)
    EX.Worker.get(pid, "x")
    EX.Worker.put(pid, "x", "")
    EX.Worker.get(pid, "x")

You can track my progress on github commit by commit. If you just want the code for this post checkout this commit.

Buy Me a Coffee

Twitter / Github / Email