-
chevron_right
Erlang Solutions: Naming your Daemons
news.movim.eu / PlanetJabber • 2 May, 2024 • 4 minutes
Within Unix systems, a daemon is a long-running background process which does not directly interact with users. Many similar processes exist within a BEAM application. At times it makes sense to name them, allowing sending messages without requiring the knowledge of their process identifier (aka PID). There are several benefits to naming processes, these include:
- Organised processes: using a descriptive and meaningful name organises the processes in the system. It clarifies the purpose and responsibilities of the process.
- Fault tolerance: when a process is restarted due to a fault it has to share its new PID to all callees. A registered name is a workaround to this. Once the restarted process is re-registered there is no additional action required and messages to the registered process resume uninterrupted.
- Pattern implementation: a Singleton, Coordinator, Mediator or Facade design pattern commonly has one registered process acting as the entry point for the pattern.
Naming your processes
Naturally, both Elixir and Erlang support this behaviour by registering the process. One downside with registering is requiring an atom. As a result, there is an unnecessary mapping between atoms and other data structures, typically between strings and atoms.
To get around this is it a common pattern to perform the registration as a two-step procedure and manage the associations manually, as shown in below:
#+begin_src Elixir
{:ok, pid} = GenServer.start_link(Worker, [], [])
register_name(pid, "router-thuringia-weimar")
pid = whereis_name("router-thuringia-weimar")
GenServer.call(pid, msg)
unregister_name("router-thuringia-weimar")
#+end_src
Figure 1
Notice the example uses a composite name: built up from equipment type, e.g. router, state, e.g. Thuringia, and city, e.g. Weimar. Indeed, this pattern is typically used to address composite names and in particular dynamic composite names. This avoids the issue of the lack of atoms garbage collection in the BEAM.
As a frequently observed pattern, both Elixir and Erlang offer a convenient method to accomplish this while ensuring a consistent process usage pattern. In typical Elixir and Erlang style, this is subtly suggested in the documentation through a concise, single-paragraph explanation.
In this write- up, we will demonstrate using built-in generic server options to achieve similar behaviour.
Alternative process registry
According to the documentation, we can register a GenServer into an alternative process registry using the
via
directive.
The registry must provide the following callbacks:
register_name/2
,
unregister_name/1
,
whereis_name/1
, and
send/2
.
As it happens there are two commonly available applications which satisfy these requirements:
gproc
and
Registry
.
gproc
is an external Erlang library written by Ulf Wiger, while
Registry
is a built-in Elixir library.
gproc
is an application in its own right, simplifying using it. It only needs to be started as part of your system, whereas
Registry
requires adding the
Registry GenServer
to your supervision tree.
We will be using
gproc
in the examples below to address the needs of both Erlang and Elixir applications.
To use
gproc
we have to add it to the project dependency.
Into Elixir’s
mix.exs
:
#+begin_src Elixir
defp deps do
[
{:gproc, git: "https://github.com/uwiger/gproc", tag: "0.9.1"}
]
end
#+end_src
Figure 2
Next, we change the arguments to
start_link
,
call
and
cast
to use the
gproc
alternative registry, as listed below:
#+begin_src Elixir :noweb yes :tangle worker.ex
defmodule Edproc.Worker do
use GenServer
def start_link(name) do
GenServer.start_link(__MODULE__, [], name: {:via, :gproc, {:n, :l, name}})
end
def call(name, msg) do
GenServer.call({:via, :gproc, {:n, :l, name}}, msg)
end
def cast(name, msg) do
GenServer.cast({:via, :gproc, {:n, :l, name}}, msg)
end
<<worker-gen-server-callbacks>>
end
#+end_src
Figure 3
As you can see the only change is using
{:via, :gproc, {:n, :l, name}}
as part of the GenServer name. No additional changes are necessary. Naturally,
the heavy lifting is
performed inside
gproc
.
The tuple
{:n, :l, name}
is specific for
gproc
and refers to setting up a “l:local n:name” registry. See the
gproc
for additional options.
Finally, let us take a look at some examples.
Example
In an Elixir shell:
#+begin_src Elixir
iex(1)> Edproc.Worker.start_link("router-thuringia-weimar")
{:ok, #PID<0.155.0>}
iex(2)> Edproc.Worker.call("router-thuringia-weimar", "hello world")
handle_call #PID<0.155.0> hello world
:ok
iex(4)> Edproc.Worker.start_link({:router, "thuringia", "weimar"})
{:ok, #PID<0.156.0>}
iex(5)> Edproc.Worker.call({:router, "thuringia", "weimar"}, "reset-counter")
handle_call #PID<0.156.0> reset-counter
:ok
#+end_src
Figure 4
As shown above, it is also possible to use a tuple as a name. Indeed, it is a common pattern to categorise processes with a tuple reference instead of constructing a delimited string.
Summary
The GenServer behaviour offers a convenient way to register a process with an alternative registry such as
gproc
. This registry permits the use of any BEAM term instead of the usual non-garbage collected atom name enhancing the ability to manage process identifiers dynamically. For Elixir applications, using the built-in
Registry
module might be a more straightforward and native choice, providing a simple yet powerful means of process registration directly integrated into the Elixir ecosystem.
Appendix
#+NAME: worker-gen-server-callbacks
#+BEGIN_SRC Elixir
@impl true
def init(_) do
{:ok, []}
end
@impl true
def handle_call(msg, _from, state) do
IO.puts("handle_call #{inspect(self())} #{msg}")
{:reply, :ok, state}
end
@impl true
def handle_cast(msg, state) do
IO.puts("handle_cast #{inspect(self())} #{msg}")
{:noreply, state}
end
#+END_SRC
Figure 5
The post Naming your Daemons appeared first on Erlang Solutions .