[Elixir] GenServer Process management/registry
Tags: elixir, erlang, otp, genserver
Published: 2020-12-10 14:36:36

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](https://www.manning.com/books/elixir-in-action).
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.

```elixir
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"
```

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

```elixir
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>}}
```

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

```elixir
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
```

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

```elixir
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>
```

### 5. using global alias

- A cluster-wide lock is set so the processes can be shared across multiple nodes.

```elixir
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>}}
```

#### Starting a cluster

- Open two iex shells
- Turn BEAM instances (iex) into nodes
- Make a cluster connecting those notes

```elixir
iex --sname node1@localhost
iex(node2@localhost)> _
```

```
iex --sname node2@localhost
iex(node2@localhost)> Node.connect(:node1@localhost)
true
```

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

<img width="1286" alt="Screen Shot 2020-12-10 at 8 55 20 AM" src="https://user-images.githubusercontent.com/7563926/101781881-5d688a80-3ac6-11eb-86c2-ef130045e85f.png">

## 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

- [Elixir in Action by Saša Jurić - Chapter 12](https://www.manning.com/books/elixir-in-action)
- [Converting strings to atoms safely by TODAY I LEARNED](https://til.hashrocket.com/posts/gkwwfy9xvw-converting-strings-to-atoms-safely)
- [Current number of atoms in the atoms table by TODAY I LEARNED](https://til.hashrocket.com/posts/b9giaqz4lc-current-number-of-atoms-in-the-atoms-table)
- [How to start processes with dynamic names in Elixir by Thoughtbot](https://thoughtbot.com/blog/how-to-start-processes-with-dynamic-names-in-elixir)