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:

  1. 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.

  2. 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 -

  1. 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.

  2. 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).

  3. 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.

  4. 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.

  5. 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 using name

  • 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 process

  • Lookup - Process.whereis/1 function is used to lookup PID of registered process

  • Unregister - 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 process

  • Lookup - :global.whereis_name/1 function is used to lookup PID of registered process

  • Unregister - :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 the Registry 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 process

  • Lookup - Registry.lookup/2 function is used to lookup PID of registered process

  • Unregister - 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 -

  1. Local name registration (atom) - This uses Process.register to register given name

  2. Global name registration ({:global, term}) - This uses :global module

  3. :via tuple ({:via, module, term}) - For this we will use Registry 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.

References

  1. https://hexdocs.pm/elixir/Process.html#register/2

  2. https://www.erlang.org/doc/man/global.html

  3. https://hexdocs.pm/elixir/1.12.3/Registry.html

  4. https://www.erlang.org/doc/man/gen_server#type-server_name

  5. https://www.manning.com/books/elixir-in-action

  6. https://hexdocs.pm/syn/readme.html

Did you find this article valuable?

Support Meraj Molla by becoming a sponsor. Any amount is appreciated!