DEV Community

loading...

[Elixir] GenServer Process management/registry

mnishiguchi profile image Masatoshi Nishiguchi ・4 min read

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

Discussion (0)

Forem Open with the Forem app