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!
Top comments (0)