Masatoshi Nishiguchi
Posted on December 10, 2020
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"
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>}}
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
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>
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>}}
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)> _
iex --sname node2@localhost
iex(node2@localhost)> Node.connect(:node1@localhost)
true
We can confirm processes are shared in two iex shells (BEAM instances).
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
Posted on December 10, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.