DEV Community

Masatoshi Nishiguchi
Masatoshi Nishiguchi

Posted on

[Elixir] GenServer Process management/registry

While I was learning Elixir basics, I was confused about how to manage process registration and discovery.
I got tons of ideas from the book Elixir in Action by Saša Juric.
Here is my study note on it.

There are multiple ways to manage processes.

1. no registration; remember the pid

  • We need to remember the pid that is returned when a genserver process is started.
  • We can create as many processes as we want from the same module.
  • We need to be aware that a pid will be changed when a process is terminated and recreated.
defmodule MyApp.HelloServer do use GenServer def start_link(id) do GenServer.start_link(__MODULE__, id) end def hello(pid) do GenServer.call(pid, :hello) end @impl true def init(id) do {:ok, %{id: id}} end @impl true def handle_call(:hello, _from, state) do {:reply, "hello", state} end end iex> {:ok, pid} = MyApp.HelloServer.start_link(123) {:ok, #PID<0.123.0>} iex> MyApp.HelloServer.hello(pid) "Hello" 
Enter fullscreen mode Exit fullscreen mode

2. using module name as local alias

  • This is suitable when we need only one process from a module.
  • Generally, we just use the module name as a local alias.
defmodule MyApp.HelloServerLocalName do use GenServer def start_link(id) do GenServer.start_link(__MODULE__, id, name: __MODULE__) end def hello do GenServer.call(__MODULE__, :hello) end @impl true def init(id) do {:ok, %{id: id}} end @impl true def handle_call(:hello, _from, state) do {:reply, "hello", state} end end iex> MyApp.HelloServerLocalName.start_link(123) {:ok, #PID<0.205.0>} iex> MyApp.HelloServerLocalName.hello() "Hello" iex> MyApp.HelloServerLocalName.start_link(123) {:error, {:already_started, #PID<0.205.0>}} 
Enter fullscreen mode Exit fullscreen mode

3. using dynamic tuple as local alias (BAD)

When we want to register multiple processes, we may get tempted to generate an atom dynamically (I did); however it is not a good practice.
Erlang has a limit on the number of atoms we can create. Also atoms are not garbage-collected.

defmodule MyApp.HelloServerDynamicName do use GenServer def process_name(id) do String.to_atom("#{__MODULE__}_#{id}") end def start_link(id) do GenServer.start_link(__MODULE__, id, name: process_name(id)) end def hello(id) do GenServer.call(process_name(id), :hello) end @impl true def init(id) do {:ok, %{id: id}} end @impl true def handle_call(:hello, _from, state) do {:reply, "hello", state} end end iex> MyApp.HelloServerDynamicName.start_link(123) {:ok, #PID<0.164.0>} iex> MyApp.HelloServerDynamicName.hello(123) "Hello" iex> :erlang.system_info(:atom_limit) 1048576 iex> :erlang.system_info(:atom_count) 15849 iex> (1..99) |> Enum.each(fn x -> MyApp.HelloServerDynamicName.start_link(x) end) :ok iex> :erlang.system_info(:atom_count) 16115 
Enter fullscreen mode Exit fullscreen mode

4. using Registry and via tuple

  • We can use via_tuple in place of pid.
  • By using a composite key, many processes can be registered from the same module without creating extra atoms.
  • Obviously, a registry process needs to be started before the registration.
defmodule MyApp.ProcessRegistry do def via_tuple(key) when is_tuple(key) do {:via, Registry, {__MODULE__, key}} end def whereis_name(key) when is_tuple(key) do Registry.whereis_name({__MODULE__, key}) end def start_link() do Registry.start_link(keys: :unique, name: __MODULE__) end end defmodule MyApp.HelloServerViaTuple do use GenServer def via_tuple(id) do MyApp.ProcessRegistry.via_tuple({__MODULE__, id}) end def whereis(id) do case MyApp.ProcessRegistry.whereis_name({__MODULE__, id}) do :undefined -> nil pid -> pid end end def start_link(id) do GenServer.start_link(__MODULE__, id, name: via_tuple(id)) end def hello(id) do GenServer.call(via_tuple(id), :hello) end @impl true def init(id) do {:ok, %{id: id}} end @impl true def handle_call(:hello, _from, state) do {:reply, "hello", state} end end iex> MyApp.ProcessRegistry.start_link() {:ok, #PID<0.421.0>} iex> MyApp.HelloServerViaTuple.start_link(123) {:ok, #PID<0.164.0>} iex> MyApp.HelloServerViaTuple.hello(123) "Hello" iex> MyApp.HelloServerViaTuple.whereis(123) #PID<0.164.0> 
Enter fullscreen mode Exit fullscreen mode

5. using global alias

  • A cluster-wide lock is set so the processes can be shared across multiple nodes.
defmodule MyApp.HelloServerGlobalName do use GenServer def whereis(id) do case :global.whereis_name({__MODULE__, id}) do :undefined -> nil pid -> pid end end def register_process(pid, id) do case :global.register_name({__MODULE__, id}, pid) do :yes -> {:ok, pid} :no -> {:error, {:already_started, pid}} end end def start_link(id) do case whereis(id) do nil -> {:ok, pid} = GenServer.start_link(__MODULE__, id) register_process(pid, id) pid -> {:ok, pid} end end def hello(id) do GenServer.call(whereis(id), :hello) end @impl true def init(id) do {:ok, %{id: id}} end @impl true def handle_call(:hello, _from, state) do {:reply, "hello", state} end end iex> MyApp.HelloServerGlobalName.start_link(123) {:ok, #PID<0.205.0>} iex> MyApp.HelloServerGlobalName.hello(123) "Hello" iex> MyApp.HelloServerGlobalName.start_link(123) {:error, {:already_started, #PID<0.205.0>}} 
Enter fullscreen mode Exit fullscreen mode

Starting a cluster

  • Open two iex shells
  • Turn BEAM instances (iex) into nodes
  • Make a cluster connecting those notes
iex --sname node1@localhost iex(node2@localhost)> _ 
Enter fullscreen mode Exit fullscreen mode
iex --sname node2@localhost iex(node2@localhost)> Node.connect(:node1@localhost) true 
Enter fullscreen mode Exit fullscreen mode

We can confirm processes are shared in two iex shells (BEAM instances).

Screen Shot 2020-12-10 at 8 55 20 AM

Finally

With this much info, I feel confident about handling processes. I'll keep on updating the post as soon as I learn something new that is relevant.

Happy coding!

Links

Top comments (0)