In this guide, we’ll explore how to use Registry along with a Dynamic Supervisor to create named GenServers.
Let's start by creating a simple GenServer
defmodule Server do
use GenServer
def start_link({init_arg, name}) do
GenServer.start_link(__MODULE__, init_arg, name: name)
end
@impl true
def init(arg) do
{:ok, arg}
end
def state(name) do
GenServer.call(name, :state)
end
@impl true
def handle_call(:state, _from, state) do
{:reply, state, state}
end
end
{:module, Server, <<70, 79, 82, 49, 0, 0, 19, ...>>, {:handle_call, 3}}
Now, we can start a new instance of the GenServer and attempt to retrieve its current state.
initial_value = 123
server_name = :server
Server.start_link({initial_value, server_name})
{:ok, #PID<0.160.0>}
Server.state(:server)
123
Note that the name cannot be a string:
Server.start_link({123, "server"})
Dynamic Supervisor
With our GenServer ready, the next step is to create a Dynamic Supervisor that will manage and start our GenServers.
defmodule DSupervisor do
use DynamicSupervisor
def start_link(init_arg) do
DynamicSupervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
end
def init(_init_arg) do
DynamicSupervisor.init(strategy: :one_for_one)
end
def start_server(initial_value, name) do
DynamicSupervisor.start_child(__MODULE__, {Server, {initial_value, name}})
end
end
{:module, DSupervisor, <<70, 79, 82, 49, 0, 0, 10, ...>>, {:start_server, 2}}
Now, we can start the Dynamic Supervisor and use it to launch another GenServer.
DSupervisor.start_link(1)
{:ok, #PID<0.170.0>}
DSupervisor.start_server(321, :test)
{:ok, #PID<0.171.0>}
Server.state(:server) |> IO.inspect()
Server.state(:test)
123
321
So, what’s the issue?
Naming a GenServer using atoms is generally not a problem. However, if your application dynamically creates GenServers with different names—such as when users start GenServers by creating a new match in a browser game or starting a new chat room—this approach can lead to problems.
Atoms are not garbage collected, and dynamically generating them as names for GenServers increases memory usage over time. Eventually, this can exhaust the atom table and cause the application to fail.
To solve this, we need a way to associate a custom name with a GenServer while storing its PID. Fortunately, Elixir provides a built-in solution for this: the Registry.
Registry.start_link(keys: :unique, name: Registry)
{:ok, #PID<0.172.0>}
Now we can use the Registry to register process names.
name = {:via, Registry, {Registry, "cool_name"}}
DSupervisor.start_server(42, name)
{:ok, #PID<0.174.0>}
Server.state(name)
42
The Registry allows us to fetch the PID of a process using the lookup function, which we will use in this guide to terminate a server.
[{pid, _value}] = Registry.lookup(Registry, "cool_name")
DynamicSupervisor.terminate_child(DSupervisor, pid)
:ok
Registry will monitor the GenServer and automatically remove the registration when the GenServer is terminated.
Server.state(name)
By using the Registry, we can safely register and manage names for GenServers with ease, ensuring that each process is uniquely identifiable and accessible. The Registry also helps by automatically cleaning up registrations when a GenServer is terminated, preventing memory leaks and improving the efficiency of your application.
With this approach, you can build scalable, maintainable systems where users can dynamically create and interact with multiple GenServers without worrying about the limitations of atom-based naming.
A few noteworthy details
When starting the Registry or the Dynamic Supervisor, the name we use sometimes looks like a module, but they are just atoms, they could be named differently, like :registry or :supervisor
Registry.start_link(keys: :unique, name: :registry)
name = {:via, Registry, {:registry, "another_name"}}
DSupervisor.start_server(42, name)
{:ok, #PID<0.5230.0>}
Interacting with the Registry could be encapsulated within the GenServer, making it easier to work with.
defp registry_name(name), do: {:via, Registry, {:registry, name}}
def start_link({init_arg, name}) do
GenServer.start_link(__MODULE__, init_arg, name: registry_name(name))
end
def state(name) do
GenServer.call(registry_name(name), :state)
end
Top comments (0)