Elixir 1.6

Chapter 17 

Nodes—The Key to Distributing Services

Nodes

There’s nothing mysterious about a node. It is simply a running Erlang VM. Throughout this book we’ve been running our code on a node.

The Erlang VM, called Beam, is like its own little operating system running on top of your host operating system. It handles its own events, process scheduling, memory, naming services, and interprocess communication.

A node can connect to other nodes—in the same computer, across a LAN, or across the Internet—and provide many of the same services across these connections that it provides to the processes it hosts locally.

Naming Nodes

If we ask Elixir what the current node is called,
it’ll give us a made-up name:

​iex>​ Node.self
:nonode@nohost

# we can set the name
​$ ​​iex​​ ​​--name​​ ​​wibble@light-boy.local​
iex(wibble@light-boy.local)> Node.self
:"wibble@light-boy.local"

# the name returned is an atom
​$ ​​iex​​ ​​--sname​​ ​​wobble​
iex(wobble@light-boy)> Node.self
:"wobble@light-boy"

Notice the name is an atom.

Nodes

Let's get two nodes running:

# Window 1
$ iex --sname node_one
iex(node_one@light-boy)>
# Window 2
$ iex --sname node_two
iex(node_two@light-boy)> Node.list
[]
# notice the atom node name
iex(node_two@light-boy)> Node.connect :"node_one@light-boy"
true
iex(node_two@light-boy)> Node.list
[:"node_one@light-boy"]

We now have two nodes connected
using the connect command.

Nodes

iex(node_one@light-boy)> func = fn -> IO.inspect Node.self end
#Function<erl_eval.20.82930912>

# run using spawn
iex(node_one@light-boy)> spawn(func)
#PID<0.59.0>
node_one@light-boy

# specify node to spawn
iex(node_one@light-boy)> Node.spawn(:"node_one@light-boy", func)
#PID<0.57.0>
node_one@light-boy
# notice the 0 in the first field of the pid, that means local

iex(node_one@light-boy)> Node.spawn(:"node_two@light-boy", func)
#PID<7393.48.0>
node_two@light-boy
# notice the output from the other node appears here
# even though it was run on the other node

Nodes, Cookies, and Security

# you can specify cookie from the command line
​$ ​​iex​​ ​​--sname​​ ​​one​​ ​​--cookie​​ ​​chocolate-chip​
iex(one@light-boy)> Node.get_cookie
:"chocolate-chip”

Can you run arbitrary code on any connected node?

 

No. Every remote node uses a cookie to determine if the code should be executed.

When Erlang starts, it looks for an .erlang.cookie file in your home directory. If that file doesn’t exist, Erlang creates it and stores a random string in it.

Be careful when connecting nodes over a public network—the cookie is transmitted in plain text.

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. Local processes will have a node ID of zero.
 

 

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.

#PID<0.59.0>
#PID<7393.48.0>

Naming Your Processes

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

Naming Your Processes

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. 

generator is the spawned process and 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.
It may time out after 2 seconds, in which case it sends a {:tick} message to all registered clients.

 

Naming Your Processes

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. 

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

Naming Your Processes

# 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
 :    :   :
”

Naming Your Processes

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

 I/O, PIDs, and Node
 

# elixir IO.puts function
​def​ puts(device \\ group_leader(), item) ​do​
  erl_dev = map_dev(device)
  :io​.put_chars erl_dev, [to_iodata(item), ​?​\n]
​end​

Input and output in the Erlang VM are performed using I/O servers. These are simply Erlang processes that implement a low-level message interface.

In Elixir you identify an open file or device by the PID of its I/O server. And these PIDs behave just like all other.

The default device it uses is returned by the function :erlang.group_leader.
This will be the PID of an I/O server.

 

 I/O, PIDs, and Node
 

# Window 1
$ iex --sname one
iex(one@light-boy) >

Lets connect to node one from node two, and register the PID returned by group_leader under a global name (we use :two).

 

The default device it uses is returned by the function :erlang.group_leader.
This will be the PID of an I/O server.

# Window 2
$ iex --sname two
iex(two@light-boy) > 
  Node.connect(:"one@light-boy")
true
iex(two@light-boy) > 
  :global.register_name(:two, :erlang.group_leader)
:yes

 I/O, PIDs, and Node
 

# Window 1
iex(one@light-boy) > two = :global.whereis_name :two
#PID<7419.30.0>
iex(one@light-boy) > IO.puts(two, "Hello")
:ok
iex(one@light-boy) > IO.puts(two, "World!")
:ok

Now that we’ve registered the PID, we can access it from the other node. And once we’ve done that, we can pass it to IO.puts; the output appears in the other terminal window.

# Window 2
Hello
World
iex(two@light-boy) >

We’ve seen how we can create and interlink a number of Erlang virtual machines, potentially communicating across a network.

Thank you!

Programming Elixir 1.6 Chapter 17

By Dustin McCraw

Programming Elixir 1.6 Chapter 17

  • 1,080