Different Ways to Register GenServer Name in Elixir
The focus of this article is to demonstrate various ways of registering a GenServer in Elixir so that the server can be discovered and communicated with by sending messages. Elixir runs on Erlang BEAM virtual machine and BEAM nodes can run on a single machine as well as in distributed mode as a cluster. In this article, we will explore various ways to register a GenServer both locally and globally using working code examples.
What's a Node?
In the Erlang ecosystem, a node refers to a single instance of the Erlang runtime system running on a particular machine. Each Erlang node has its own set of processes, memory, and computational resources.
Here's a breakdown of the concepts:
Local Node: A local node is the Erlang runtime instance that is running on the current machine or system. It hosts a set of Erlang processes and manages local resources such as memory and CPU. When you start an Erlang application or shell on a machine, it initializes a local node.
Remote Node: A remote node is any other Erlang runtime instance running on a different machine or system. It also hosts its own set of Erlang processes and manages its resources independently. Remote nodes can communicate with each other over a network, enabling distributed computing in Erlang.
What's a Process Registry?
A process registry is a mechanism for associating a unique name with a specific process. It allows processes to be registered under a given name and then looked up later using that name. This abstraction simplifies process management and facilitates communication between processes, especially in distributed systems.
Here are the key aspects of a process registry -
Registration: Processes can register themselves or be registered by other processes using a unique name within a registry. When a process registers itself, it typically provides a name under which it will be known in the registry.
Lookup: Other processes can look up registered processes using their registered names. This allows processes to communicate with each other without needing to know each other's process identifiers (PIDs).
Scoping: Process registries can be local to a single node or global across multiple nodes in a distributed system. Local registries are confined to a single Erlang node, while global registries allow processes to be registered and looked up across all nodes in a distributed system.
Fault Tolerance: Process registries often include fault-tolerant features to handle scenarios such as process crashes or node failures. For example, some registries automatically remove entries for processes that have terminated unexpectedly.
Dynamic Nature: Process registries are dynamic, allowing processes to register and unregister themselves as needed. This flexibility is useful for managing the lifecycle of processes in a system.
Registering Processes
There are multiple ways to register a process locally in a node or globally in a cluster of nodes. Before moving on to discussing mechanisms specific to GenServer, we will start with simple process registration. We are primarily interested in 3 things -
Register a process using a given
name
Lookup a registered process's
PID
usingname
Unregister a process
Register Processes using Process
Module [1]
Elixir Process module provides functionality to register a process using given name
on a local node.
Register -
Process.register/2
function is used to register a processLookup -
Process.whereis/1
function is used to lookup PID of registered processUnregister -
Process.unregister/1
function is used to unregister a process
Using iex
session, we can register/lookup/unregister the shell
process under name :shell
as below -
Register Processes using global
Module [2]
Erlang's global
module provides a mechanism for global name registration, allowing processes to register and look up names across all nodes in a distributed Erlang system.
Register -
:global.register_name/2
function is used to register a processLookup -
:global.whereis_name/1
function is used to lookup PID of registered processUnregister -
:global.unregister_name/1
function is used to unregister a process
Using iex
session, we can register/lookup/unregister the shell
process under name :shell
as below -
Register processes using Registry
Module [3]
Registry
module provides a local, decentralized registry. While there are various ways this module can be used to register processes, in this article we will only show using :via
tuple as later we will use :via
tuple to register GenServer. :via
tuple takes the form -
{:via, registry, term}
:via
: This atom indicates that the registration and lookup should be done via a registry.registry
: This is the name of the registry module to use for registration and lookup. Commonly used registries include:global
,:gproc
, and custom registries implemented using theRegistry
module.term
: This is the key under which the GenServer process is registered in the registry. It can be any term (atom, string, tuple, etc.) that uniquely identifies the process within the scope of the registry.Register -
Registry.register/2
function is used to register a processLookup -
Registry.lookup/2
function is used to lookup PID of registered processUnregister -
Registry.unregister/2
function is used to unregister a process
Using iex
session, we can register/lookup/unregister the shell
process under name :shell
as below -
Note: we have to first start the Registry
process before we can register/lookup processes using this module.
Registering GenServer
A GenServer
can be registered using various mechanisms. The start_link/3
function for GenServer
takes a options
parameter which has the name
key. As per Erlang's GenServer official documentation [4], this name can have below format -
As we can see, we can register GenServer's name using -
Local name registration (
atom
) - This usesProcess.register
to register given nameGlobal name registration (
{:global, term}
) - This uses:global
module:via
tuple ({:via, module, term}
) - For this we will useRegistry
module
In this section we will start a ping_server_demo
project and implement a GenServer using various mechanisms.
We will start with mix project with the command and create a sample ping
server as below -
mix new --sup ping_server_demo
defmodule PingServer do
use GenServer
def start do
GenServer.start_link(__MODULE__, nil)
end
def ping(server) do
IO.inspect("Executing GenServer on node - #{node()}")
GenServer.call(server, :ping)
end
@impl GenServer
def init(_), do: {:ok, nil}
@impl GenServer
def handle_call(:ping, _, state), do: {:reply, :pong, state}
end
This is a rudimentary ping
server that does not register any name. So we will need to pass the PID
of the GenServer to various call/3
or cast/2
functions.
A sample run of the ping
server using iex -S mix
is as below -
Here, since we did not register the ping
server under any name, we had to pass server PID
to ping/1
function so that we can send messages to the server.
Register GenServer using Local Name
We can use any atom
to name a GenServer. In the below code, we are using the module
name itself to name the GenServer and later ping/1
function is using GenServer.call/2
to send message using the same name. This is how local name registration works.
defmodule PingServer do
use GenServer
def start do
GenServer.start_link(__MODULE__, nil, name: __MODULE__)
end
def ping do
IO.inspect("Executing Genserver on node - #{node()}")
GenServer.call(__MODULE__, :ping)
end
@impl GenServer
def init(_), do: {:ok, nil}
@impl GenServer
def handle_call(:ping, _, state), do: {:reply, :pong, state}
end
A sample run of the ping
server is as below -
Register GenServer using Global Name
We can use the tuple {:global, term}
to name a GenServer in global registry. In the below code, we are using the {:global, __MODULE__}
tuple to name the GenServer and later ping/1
function is using GenServer.call/2
to send message using the same name.
defmodule PingServer do
use GenServer
def start do
GenServer.start_link(__MODULE__, nil, name: {:global, __MODULE__})
end
def ping do
IO.inspect("Executing Genserver on node - #{node()}")
GenServer.call({:global, __MODULE__}, :ping)
end
@impl GenServer
def init(_), do: {:ok, nil}
@impl GenServer
def handle_call(:ping, _, state), do: {:reply, :pong, state}
end
We can easily verify that we can access this server from any node using the registered name by running two Erlang nodes and connecting the nodes in cluster mode as below -
As we can see, we started node1
and node2
in clustered mode and ping
server only in node1
. But from 2nd screenshot we can see we can still ping
the server and it can discover the server since the name is globally registered.
Register GenServer using Via Tuple
As mentioned earlier, to use the :via
tuple, we need to start Registry
process first. We will encapsulate some functionalities under a different module called PingServerRegistry
which looks as below (this code is slightly adapted from [5]) -
defmodule PingServerRegistry do
def start_link do
Registry.start_link(keys: :unique, name: __MODULE__)
end
def via_tuple(key) do
{:via, Registry, {__MODULE__, key}}
end
def child_spec(_) do
Supervisor.child_spec(
Registry,
id: __MODULE__,
start: {__MODULE__, :start_link, []}
)
end
end
and start the registry under application supervision tree -
defmodule PingServerDemo.Application do
# See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications
@moduledoc false
use Application
@impl true
def start(_type, _args) do
children = [
PingServerRegistry
]
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: PingServerDemo.Supervisor]
Supervisor.start_link(children, opts)
end
end
With this setup in place, we can update our ping
server code to use :via
tuple as shown below -
defmodule PingServer do
use GenServer
def start do
GenServer.start_link(__MODULE__, nil, name: via_tuple())
end
def ping do
IO.inspect("Executing Genserver on node - #{node()}")
GenServer.call(via_tuple(), :ping)
end
@impl GenServer
def init(_), do: {:ok, nil}
@impl GenServer
def handle_call(:ping, _, state), do: {:reply, :pong, state}
defp via_tuple, do: PingServerRegistry.via_tuple(__MODULE__)
end
We are using the via_tuple()
call to register GenServer and later ping/1
function is using GenServer.call/2
to send message using the same via_tuple()
.
A sample run of the ping
server is as below -
Complete source code for the various mechanisms described above are available here on my GitHub - github.com/imeraj/elixir_playground/tree/ma..
Conclusion
In this article, we demonstrated various ways to register Elixir processes/GenServers to discover and communicate with them properly. Some of this mechanisms are local to a particular node whereas some provide global name registration. Although, we can use the provided modules to register GenServer, sometimes they may have various limitations in terms of availability, consistency, and scalability. Interested readers are encouraged to look at 3rd party solutions too. One such hex package we used in the past is Syn [6], which overcomes some of the shortcomings of Erlang's global
registry and can automatically manage dynamic clusters and recover from net splits/partitions.
For more elaborate and in-depth future technical posts please follow me here or subscribe to my newsletter.