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.