Intro to GenServers
What is a GenServer?
According to the HexDocs for GenServers:
A GenServer is a process like any other Elixir process, and it can be used to keep state, execute code asynchronously and so on. The advantage of using a GenServer process is that it will have a standard set of interface functions.
What is a Process?
According to the HexDocs for Processes:
In Elixir, all code runs in processes. Processes are isolated from each other, run concurrent to one another and communicate via message passing. Processes are not only the basis for concurrenty in Elixir, but they also provide the means for building distributed and fault tolerant programs.
Processes are spawned, do some work, and then “die”. Let’s see that in action
spawn(fn -> IO.puts("Hello from the process") end)
The return from spawn/1 is a PID, or “process identifier”. This is a value we can use to communicate with a processes. For example, ask if it’s alive:
pid = spawn(fn -> IO.puts("What's my PID again?") end)
IO.inspect(pid, label: "The process' PID")
Process.alive?(pid)
What if we want the Process to stay alive?
If we want the process to stay alive, we need to give it a recurring (or recursive) “task”, like listening for messages
Consider the following module which spawns a process that listens for incoming messages:
defmodule LongLivedProcess do
def start_link do
spawn(fn -> loop() end)
end
defp loop do
receive do
{:print, message} ->
IO.puts("Printing message: #{message}")
_ ->
IO.puts("What?")
end
loop()
end
end
pid = LongLivedProcess.start_link()
send(pid, {:print, "A message to print"})
send(pid, :something_else)
Process.sleep(500)
Process.alive?(pid)
Can Processes be used to store state?
Yes! Processes can be used to store state.
All we need to do is pass an initial state into our loop function, and pass that argument around in our recursion.
Consider the following module which spawns a process that stores a list of things:
defmodule StateHoldingProcess do
def start_link do
spawn(fn -> loop([]) end)
end
defp loop(list) do
receive do
# Put an item at the head of the list
{:put, item} ->
updated_list = [item | list]
loop(updated_list)
# Pop an item off the head of a list and return it to the caller
{:pop, caller} ->
[item | updated_list] = list
send(caller, item)
loop(updated_list)
end
end
end
pid = StateHoldingProcess.start_link()
# Put two items in the list
send(pid, {:put, :first_item})
send(pid, {:put, :second_item})
# Retrieve the last item added to the list
# It is sent to self()'s mailbox so we need to "flush" it out
Process.alive?(pid)
send(pid, {:pop, self()})
IEx.Helpers.flush
This is cool, but is there a “better” way?
Yes, there is a “better” way! We can use a GenServer Behaviour, which removes a lot of the boilerplate around maintaining a process, exposes some helpful convenience functions, and allows developers to focus on actual functionality instead of managing a process.
A Behaviour is Elixir’s way of defining a set of functions that must be implemented, and checks that they are implemented.
The main functions to implement are, init/1, handle_call/3 and handle_cast/2
-
initis invoked when the server is started and handlesstart_link/3messages. This will block until the funciton returns. -
handle_call/3is invoked to handle synchronous (or blocking)callmessages to the GenServer. -
handle_cast/2is invoked to handle asynchronous (or non-blocking)castmessages to the GenServer.
Let’s pop over to VS Code to see some examples!
So what are GenServers actually good for?
GenServers are good any time you need to manage state or need concurrency and isolation.
For example:
- Making HTTP requests to external services: you don’t know how long the request is going to take and you don’t want to block your application while you wait for a response.
- Real-time “collaboration”: one of the most common intro-to-GenServer examples is an auction manager. You can store the current best bid, handle incoming bids, and count down the time left in the auciton all at once.
Next time:
- Supervision
- Name Registration
- Build an actual auction app