Naming Your Processes

Although a PID is displayed as three numbers, it contains just two fields; the first number is the node ID and the next two numbers are the low and high bits of the process ID. When you run a process on your current node, its node ID will always be zero. However, when you export a PID to another node, the node ID is set to the number of the node on which the process lives.

That works well once a system is up and running and everything is knitted together. If you want to register a callback process on one node and an event-generating process on another, just give the callback PID to the generator.

But how can the callback find the generator in the first place? One way is for the generator to register its PID, giving it a name. The callback on the other node can look up the generator by name, using the PID that comes back to send messages to it.

Here’s an example. Let’s write a simple server that sends a notification about every 2 seconds. To receive the notification, a client has to register with the server. And we’ll arrange things so that clients on different nodes can register.

While we’re at it, we’ll do a little packaging so that to start the server you run Ticker.start, and to start the client you run Client.start. We’ll also add an API Ticker.register to register a client with the server.

Here’s the server code:

nodes/ticker.ex
 defmodule​ Ticker ​do
 
  @interval 2000 ​# 2 seconds
  @name ​:ticker
 
 def​ start ​do
  pid = spawn(__MODULE__, ​:generator​, [[]])
 :global​.register_name(@name, pid)
 end
 def​ register(client_pid) ​do
  send ​:global​.whereis_name(@name), { ​:register​, client_pid }
 end
 
 def​ generator(clients) ​do
 receive​ ​do
  { ​:register​, pid } ->
  IO.puts ​"​​registering ​​#{​inspect pid​}​​"
  generator([pid|clients])
 after
  @interval ->
  IO.puts ​"​​tick"
  Enum.each clients, ​fn​ client ->
  send client, { ​:tick​ }
 end
  generator(clients)
 end
 end
 end

We define a start function that spawns the server process. It then uses :global.register_name to register the PID of this server under the name :ticker.

Clients who want to register to receive ticks call the register function. This function sends a message to the Ticker server, asking it to add those clients to its list. Clients could have done this directly by sending the :register message to the server process. Instead, we give them an interface function that hides the registration details. This helps decouple the client from the server and gives us more flexibility to change things in the future.

Before we look at the actual tick process, let’s stop to consider the start and register functions. These are not part of the tick process—they are simply chunks of code in the Ticker module. This means they can be called directly wherever we have the module loaded—no message passing required. This is a common pattern; we have a module that is responsible both for spawning a process and for providing the external interface to that process.

Back to the code. The last function, generator, is the spawned process. It waits for two events. When it gets a tuple containing :register and a PID, it adds the PID to the list of clients and recurses. Alternatively, it may time out after 2 seconds, in which case it sends a {:tick} message to all registered clients.

(This code has no error handling and no means of terminating the process. I just wanted to illustrate passing PIDs and messages between nodes.) The client code is simple:

nodes/ticker.ex
 defmodule​ Client ​do
 
 def​ start ​do
  pid = spawn(__MODULE__, ​:receiver​, [])
  Ticker.register(pid)
 end
 
 def​ receiver ​do
 receive​ ​do
  { ​:tick​ } ->
  IO.puts ​"​​tock in client"
  receiver()
 end
 end
 end

It spawns a receiver to handle the incoming ticks, and passes the receiver’s PID to the server as an argument to the register function. Again, it’s worth noting that this function call is local—it runs on the same node as the client. However, inside the Ticker.register function, it locates the node containing the server and sends it a message. As our client’s PID is sent to the server, it becomes an external PID, pointing back to the client’s node.

The spawned client process simply loops, writing a cheery message to the console whenever it receives a tick message.

Let’s run it. We’ll start up our two nodes. We’ll call Ticker.start on node one. Then we’ll call Client.start on both node one and node two.

Window #1

 nodes % iex --sname one
 iex(one@light-boy)> c("ticker.ex")
 [Client,Ticker]
 iex(one@light-boy)> Node.connect :"two@light-boy"
 true
 iex(one@light-boy)> Ticker.start
 :yes
 tick
 tick
 iex(one@light-boy)> Client.start
 registering #PID<0.59.0>
 {:register,#PID<0.59.0>}
 tick
 tock in client
 tick
 tock in client
 tick
 tock in client
 tick
 tock in client
  : : :

Window #2

 nodes % iex --sname two
 iex(two@light-boy)> c("ticker.ex")
 [Client,Ticker]
 iex(two@light-boy)> Client.start
 {:register,#PID<0.53.0>}
 tock in client
 tock in client
 tock in client
  : : :

To stop this, you’ll need to exit IEx on both nodes.

When to Name Processes

When you name something, you are recording some global state. And as we all know, global state can be troublesome. What if two processes try to register the same name, for example?

The runtime has some tricks to help us. In particular, we can list the names our application will register in the app’s mix.exs file. (We’ll cover how when we look at packaging an application.) However, the general rule is to register your process names when your application starts.