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)
      end
    
      @impl GenServer
      def init(:ok) do
        state = Shortener.new()
        {:ok, state}
      end
    
      def get(pid, hash) do
        GenServer.call(pid, {:get, hash})
      end
    
      def put(pid, hash, url) do
        GenServer.cast(pid, {:put, hash, url})
      end
    
      @impl GenServer
      def handle_call({:get, hash}, _timeout, state) do
        {:reply, Shortener.get_url(state, hash), state}
      end
    
      @impl GenServer
      def handle_cast({:put, hash, url}, state) do
        new_state = Shortener.create_short_url(state, hash, url)
        {:noreply, new_state}
      end
    end
  

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}
      end
    
      test "get and put work", %{pid: pid} do
        assert EX.Worker.get(pid, "x") === :undefined
    
        EX.Worker.put(pid, "x", "google.com")
    
        assert EX.Worker.get(pid, "x") === "google.com"
      end
    end
  

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, []}
      ]
    end
  

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)
      end
    end
  

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", "google.com")
    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