Building Distributed Systems in Elixir: Part 6 — Named Processes
In the previous part of this series, we built a tiny supervisor from scratch. When a worker crashed, the supervisor started a replacement. That replacement had a new PID: old worker -> #PID<0.102.0> new worker -> #PID<0.105.0> This reveals an important limitation of sharing PIDs as a public interface. A PID identifies one running incarnation of a process. It is excellent for sending a reply,…
In this sixth part of the series on building distributed systems in Elixir, we explore named processes to provide discoverable addresses for workers. We examine three small examples using Process.register/2, Process.whereis/1, :global.register_name/2, and :global.whereis_name/1. The goal is to understand the lookup problem that registries solve before resorting to those abstractions.
Previously, we built a tiny supervisor in the previous part of this series. When a worker crashed, the supervisor started a replacement. The replacement had a new PID, which revealed an important limitation of using PIDs as a public interface. PIDs identify one running incarnation of a process and are excellent for sending replies, setting up monitors, or creating links. However, they are not stable addresses for a service that may stop and later be replaced.
To address this, we use named processes by registering a worker with a name using Process.register/2. For instance, we can associate the PID of a worker with a local name, :worker. This name is a registered local name on the current BEAM node. We can retrieve its current PID using Process.whereis/1. If the name is not registered on the node, Process.whereis/1 returns nil.
When sending messages to a worker, often, there is no need to perform a lookup yourself. The send/2 function accepts a local registered name, allowing the runtime to resolve the name and deliver the message to its current owner. This name-based approach eliminates the need for clients to carry PIDs around.
Our worker waits for a stop message and can be started with Worker.start(). We can register this PID with a local name using Process.register(pid, :worker). The name ":worker" is now a registered local name, and we can retrieve its current PID using Process.whereis(:worker). If the name is not registered on this node, Process.whereis/1 returns nil.
Sending to the name is straightforward. By using the send/2 function with the registered name, ":worker", the runtime resolves the name and delivers the message to its current owner. The caller does not need to carry a PID around, offering convenience and flexibility.
It is important to note that Process.register/2 is local to one node. If two disconnected nodes each run Process.register(pid, :worker), there is no conflict. Each node maintains its own local process registry. Therefore, Process.whereis(:worker) only answers the question of which local process on this node is registered as :worker. It does not discover a process on another BEAM node.
For cluster-wide lookup, a broader naming mechanism is required. Erlang provides the :global module for a namespace coordinated across connected nodes. In the second example, we register the worker with :global.register_name(:worker, pid) and look it up with :global.whereis_name(:worker). We then send a message to the PID returned by the lookup using send(:global.whereis_name(:worker), :stop). This example demonstrates the coordination of a single name across connected nodes in a connected Erlang cluster.
Written by urgent.news from Dev.to's reporting — not their text. Machine-written — may contain errors; check the original before relying on it.