GenServer
GenServer
One of the benefits of using Elixir is its asynchronous nature by default. We can demonstrate this by making calls to an external api.
We are going to create a module in the lib directory of our application called api_handler.ex
. It will be a handler for external api calls.
We are going to spin off a process that will ask to make calls to an external api, which will allow us to handle api calls in a non-blocking way - so tha tour application is free to continue answering http requests. This feature allows elixir to be fast and non-blocking.
Because this pattern is so common in Erlang, the OTP library provides an interface called GenServer
.
In Elixir we will create a module that implements this behavior.
In our lib directory lets create a file called api_handler.ex
. Inside this file let’s define a module named MyApp.ApiHandler
.
defmodule MyApp.ApiHandler do
use GenServer
end
The GenServer behavior requires us to define functions for handling callbacks. The callbacks that we will be implementing are
- init/1
- handle_call/3
- handle_cast/2
- handle_info/2
- terminate/2
The init/1
is called when the server starts up and terminate/2
is called before the server shuts down.
defmodule MyApp.ApiHandler do
use GenServer
def init(args) do
{:ok, args}
end
def terminate(reason, state) do
:ok
end
end
A GenServer is a process that listens for messages. We can send 3 different types of messages to our GenServer process.
The most basic message is using native Erlang message passing. When the GenServer receives a message in this way the handle_info/2
callback is called.
defmodule MyApp.ApiHandler do
use GenServer
def handle_info(term, state) do
{:noreply, state}
end
end
We can send a message to the GenServer and wait for a response. In this case the handle_call/3
function will receive the message.
defmodule MyApp.ApiHandler do
use GenServer
def handle_call(request, from, state) do
{:reply, response, state}
end
end
Finally, we can send a message and not wait for a response. GenServer will call the handle_cast/2
callback to handle these messages.
defmodule MyApp.ApiHandler do
use GenServer
def handle_cast(request, state) do
{:noreply, state}
end
end
Notice in each of these functions we have this variable called state. We can keep state of the module using this variable. We are not going to keep track of any state in our module, however know that this functionality is available.
So let’s implement our module. We will focus on calls.
To call our GenServer we will use the GenServer.call/2
function.
Let’s add a function called get_character/1
to call our GenServer to retrieve a pokemon.
defmodule MyApp.ApiHandler do
use GenServer
def get_character(id_or_name) do
GenServer.call(__MODULE__, {:get_character, id_or_name})
end
end
Notice that when we call the GenServer we are sending a tuple. We can use pattern matching in our handle_call/3
callback to handle this message.
defmodule MyApp.ApiHandler do
use GenServer
def handle_call({:get_character, id_or_name}, _pid, state) do
{:ok, response} = get_character_request(id_or_name)
{:reply, response, state}
end
def handle_call(_term, _pid, state) do
{:reply, [], state}
end
end
So now we have completed our GenServer callback implementation. Let’s complete the functionality by implementing the get_poke_request/1
method.
But first let’s add the required dependencies to our mix.exs
file.
defp deps do
[
{:cowboy, "~> 2.6"},
{:plug, "~> 1.7"},
{:plug_cowboy, "~> 2.0"},
{:httpoison, "~> 1.4"},
{:poison, "~> 4.0"},
{:earmark, "~> 1.3", only: :dev},
{:ex_doc, "~> 0.19.1", only: :dev}
]
end
HTTPoison is an http client for elixir. Poison is a JSON library for elixir
Let’s fetch our dependencies by calling mix deps.get
. Now let’s implement our get_poke_request/1
method.
defmodule MyApp.ApiHandler do
use GenServer
defp get_character_request(id_or_name) do
url = api_route(id_or_name)
case HTTPoison.get(url, [], []) do
{:error, %HTTPoison.Error{reason: reason}} ->
{:error, reason}
{:ok, %HTTPoison.Response{body: body}} ->
{:ok, body}
end
end
defp api_route(id) do
"https://anapioficeandfire.com/api/characters/#{id}"
end
end
We’ll start the GenServer process with out supervisor we implemented previously.
The GenServer.start_link/3
function starts a process linked to the current process. Often times this is used in a Supervision tree.
Let’s add our ApiHandler
module to our supervision tree. Open up our myapp.ex
file. We will add our ApiHandler as a child in our supervision tree.
Add the following line to our list of children:
Our list of children should now look like this now:
def start(_type, _args) do
children = [
{MyApp.Router, []},
{MyApp.ApiHandler, []}
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
When our app is started it will start our ApiHandler as a child process.
Now we can call our get_character_request/1
function from our router.
defmodule MyApp.Router do
use Plug.Router
get("/:name") do
query_params = Plug.Conn.fetch_query_params(conn)
name = query_params.params["name"] || 1
character = MyApp.ApiHandler.get_character(name)
conn
|> put_resp_content_type("application/json")
|> send_resp(200, character)
end
end
Now we have a non-blocking GenServer that calls an external api in an efficient way.
The last thing we want to call out is that we want to register our GenServer with the Erlang name server.
We will do this with the start_link/2
function by calling GenServer.start_link/3
with the name we want to register the GenServer under. Below we are registering the GenServer with the name of the module.
defmodule MyApp.ApiHandler do
use GenServer
def start_link(_default) do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
end
The complete ApiHandler
module looks like this:
defmodule MyApp.ApiHandler do
use GenServer
def start_link(_default) do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init(args) do
{:ok, args}
end
def get_character(id_or_name) do
GenServer.call(__MODULE__, {:get_character, id_or_name})
end
def handle_call({:get_character, id_or_name}, _pid, state) do
{:ok, response} = get_character_request(id_or_name)
{:reply, response, state}
end
def handle_call(_term, _pid, state) do
{:reply, [], state}
end
defp get_character_request(id_or_name) do
url = api_route(id_or_name)
case HTTPoison.get(url, [], []) do
{:error, %HTTPoison.Error{reason: reason}} ->
{:error, reason}
{:ok, %HTTPoison.Response{body: body}} ->
{:ok, body}
end
end
defp api_route(id) do
"https://anapioficeandfire.com/api/characters/#{id}"
end
end
Next step:
Go on to Deployment.
Or:
Go back to Templating.