Naming your Daemons
- Tee Teoh
- 2nd May 2024
- 7 min of reading time
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:
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.
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.
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.
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.
#+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
Join Lorena in this years Advent of Code 2024. She'll be solving daily puzzles throughout the month of December.
Attila Sragli explores the BEAM VM's inner workings, comparing them to the JVM to highlight their importance.
Pawel Chrząszcz introduces MongooseIM 6.3.0 with Prometheus monitoring and CockroachDB support for greater scalability and flexibility.