Naming your Daemons

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: 

  1. Organised processes: using a descriptive and meaningful name organises the processes in the system. It clarifies the purpose and responsibilities of the process.
  2. 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.
  3. 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

Keep reading

Advent of Code 2024

Advent of Code 2024

Join Lorena in this years Advent of Code 2024. She'll be solving daily puzzles throughout the month of December.

Optimising for Concurrency: Comparing and contrasting the BEAM and JVM virtual machines

Optimising for Concurrency: Comparing and contrasting the BEAM and JVM virtual machines

Attila Sragli explores the BEAM VM's inner workings, comparing them to the JVM to highlight their importance.

MongooseIM 6.3: Prometheus, CockroachDB and more

MongooseIM 6.3: Prometheus, CockroachDB and more

Pawel Chrząszcz introduces MongooseIM 6.3.0 with Prometheus monitoring and CockroachDB support for greater scalability and flexibility.