A Clojure library that simulates an Erlang node.
NOTE: for the time being it seems that sliver only works with OTP 18 (haven't investigated OTP 19, and 20 definitely doesn't work).
Add [sliver "0.0.2-SNAPSHOT"]
if you're working with Leiningen. Alternatively, add
<dependency>
<groupId>sliver</groupId>
<artifactId>sliver</artifactId>
<version>0.0.2-SNAPSHOT</version>
</dependency>
if you're using Maven.
First create a node. Require the relevant namespaces you'll be using through this document before anything though:
(ns your-spiffy-new-node.core
(:require [sliver.node :as n]
[sliver.primitive :as p]
[co.paralleluniverse.pulsar.actors :as a]))
Now you can create the node. In the repl
:
your-spiffy-new-node.core> (def foo-node (n/node "[email protected]" "monster"))
2015-Nov-16 20:52:13 +0000 salad-fingers DEBUG [sliver.node] - foo :: 127.0.0.1
2015-Nov-16 20:52:13 +0000 salad-fingers DEBUG [sliver.node] - Registering #borges.type.Pid{:node foo@127.0.0.1, :pid 0, :serial 0, :creation 0} as _dead-processes-reaper
#'your-spiffy-new-node.core/foo-node
your-spiffy-new-node.core>
A few things should be noted here:
- You will get a bunch of
DEBUG
messages.sliver
is being heavily developed at the moment, I need all the help I can get - The order of these messages might vary from the example up here. That's because they happen asynchronously. Don't worry about it.
After creating a node, there's a few things you can do with it:
- you can
connect
it to another node (anerlang
orsliver
node) - you can
spawn
processes - you can send (
!
) messages to processes (local or remote) - you can
start
the node: the node will register withEPMD
(but won't start it if it's not running), and start listening for incoming connections
There's other things you can do, like link
or monitor
processes, but we
won't cover that here.
Spawning a new process will return its pid
. This is an erlang
pid, so it
can be used both in erlang
interop, as well as sending messages to other
sliver
processes.
After you've created the node, spawning a new process is done like so:
your-spiffy-new-node.core> (p/spawn foo-node #(println "I don't do much.")) ;; returns the pid
I don't do much.
#borges.type.Pid{:node foo@127.0.0.1, :pid 1, :serial 0, :creation 0}
your-spiffy-new-node.core>
The process spawned will execute the provided thunk (there's currently no
support for spawning functions that take arguments) until it finishes. Under
the hood, these are nothing but plain pulsar
processes.
Unless you're really curious, you shouldn't bother much with the underlying structure of pids.
As expected, you can spawn as many processes as you like (all credit goes to
the people who wrote Pulsar/Quasar
):
your-spiffy-new-node.core> (last (for [n (range 100000)] (p/spawn foo-node #(+ 1 1))))
#borges.type.Pid{:node foo@127.0.0.1, :pid 213433, :serial 1, :creation 0}
your-spiffy-new-node.core>
Granted, these processes did nothing much, but try starting 100k threads on a laptop and see how well it fares.
With !
(in the sliver.node-interface
namespace) you can send messages to:
- a
pid
: this is a process' identifier; a handle for when the process is not registered. Apid
can be local to thesliver
node, or remote (from anerlang
or asliver
node) - a named process: this is the name (I prefer
symbols
orkeywords
though any object should be good to be used as a name) under which the process is registered. Names are local to thesliver
node - name, node adress pairs, e.g.
[name "node@IP"]
: this is the equivalent toerlang
's{name, node@ip} ! message
. The message will be sent to the registered process in the specified remote node
Let's send a message to a local unregistered processes:
your-spiffy-new-node.core> (def pid1 (p/spawn foo-node #(a/receive m (prn m))))
#'your-spiffy-new-node.core/pid1
your-spiffy-new-node.core> (p/! foo-node pid1 'ohai)
nil
ohai
your-spiffy-new-node.core>
The code above:
spawn
ed a process that waits for a message (any message), and prints it to console- sent a message (
'ohai'
) to thepid
of that process
Let's try the same thing, but with registered processes:
your-spiffy-new-node.core> (def pid1 (p/spawn
foo-node
#(do
(p/register foo-node
'a-process
(p/self foo-node))
(a/receive m (prn m)))))
#'your-spiffy-new-node.core/pid1
2015-Nov-16 21:09:50 +0000 salad-fingers DEBUG [sliver.node] - Registering #borges.type.Pid{:node foo@127.0.0.1, :pid 213435, :serial 1, :creation 0} as a-process
your-spiffy-new-node.core> (p/! foo-node 'a-process 'ohai)
nil
ohai
your-spiffy-new-node.core>
The code above is identical to the first example with the exception that the
process registers itself via p/register
and that we send (!
) the message
to the process' name and not its pid
.
Neat.
However, so far you've not done anything we couldn't have done with plain
pulsar
actors. Let's talk about sending messages to remote processes.
sliver
can send messages to remote nodes. To demonstrate this, you could use
a second sliver
node, but that wouldn't be much fun. You'll use an erlang
node instead.
Start an erlang node:
~ erl -name [email protected]
Erlang/OTP 18 [erts-7.1] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]
Eshell V7.1 (abort with ^G)
([email protected])1>
Now connect sliver to it:
your-spiffy-new-node.core> (p/connect foo-node {:node-name "[email protected]"})
2015-Nov-16 21:28:26 +0000 salad-fingers DEBUG [sliver.handshake] - SEND NAME: foo@127.0.0.1
2015-Nov-16 21:28:26 +0000 salad-fingers DEBUG [sliver.handshake] - DECODED: :ok
2015-Nov-16 21:28:26 +0000 salad-fingers DEBUG [sliver.handshake] - DECODED: {:version 5, :flag 229372, :challenge 1347765539, :name bar@127.0.0.1}
2015-Nov-16 21:28:26 +0000 salad-fingers DEBUG [sliver.handshake] - DECODED: :ok
2015-Nov-16 21:28:26 +0000 salad-fingers DEBUG [sliver.node] - Registering #borges.type.Pid{:node foo@127.0.0.1, :pid 2, :serial 0, :creation 0} as bar-reader
2015-Nov-16 21:28:26 +0000 salad-fingers DEBUG [sliver.node] - foo: Reader for bar
2015-Nov-16 21:28:26 +0000 salad-fingers DEBUG [sliver.protocol] - Looping...
2015-Nov-16 21:28:26 +0000 salad-fingers DEBUG [sliver.node] - Registering #borges.type.Pid{:node foo@127.0.0.1, :pid 3, :serial 0, :creation 0} as bar-writer
2015-Nov-16 21:28:26 +0000 salad-fingers DEBUG [sliver.node] - foo: Writer for bar
#sliver.node.Node{:node-name "foo", :host "127.0.0.1", :cookie "monster", :handlers [#<handler$handle_messages sliver.handler$handle_messages@3646bfed>], :state #<Atom@5ee27c71: {:shutdown-notify #{_dead-processes-reaper bar-writer}}>, :pid-tracker #<Ref@23386cb9: {:creation 0, :serial 0, :pid 4}>, :ref-tracker #<Ref@4659de48: {:creation 0, :id [0 1 1]}>, :actor-tracker #<Ref@6dddba95: {#borges.type.Pid{:node [email protected], :pid 3, :serial 0, :creation 0} #<ActorRef ActorRef@5a56124c{PulsarActor@ac90383[owner: fiber-10000004]}>, #borges.type.Pid{:node [email protected], :pid 2, :serial 0, :creation 0} #<ActorRef ActorRef@48cb69e9{PulsarActor@3784b8bc[owner: fiber-10000003]}>, #borges.type.Pid{:node [email protected], :pid 0, :serial 0, :creation 0} #<ActorRef ActorRef@22966555{PulsarActor@3fd6c130[owner: fiber-10000001]}>}>, :reverse-actor-tracker #<Ref@7804c48c: {#<ActorRef ActorRef@5a56124c{PulsarActor@ac90383[owner: fiber-10000004]}> #borges.type.Pid{:node [email protected], :pid 3, :serial 0, :creation 0}, #<ActorRef ActorRef@48cb69e9{PulsarActor@3784b8bc[owner: fiber-10000003]}> #borges.type.Pid{:node [email protected], :pid 2, :serial 0, :creation 0}, #<ActorRef ActorRef@22966555{PulsarActor@3fd6c130[owner: fiber-10000001]}> #borges.type.Pid{:node [email protected], :pid 0, :serial 0, :creation 0}}>, :actor-registry #<Atom@2c25570e: {bar-writer #borges.type.Pid{:node [email protected], :pid 3, :serial 0, :creation 0}, bar-reader #borges.type.Pid{:node [email protected], :pid 2, :serial 0, :creation 0}, _dead-processes-reaper #borges.type.Pid{:node [email protected], :pid 0, :serial 0, :creation 0}}>}
your-spiffy-new-node.core>
Oh dear! Yes. Quite a few DEBUG
messages. I hope you can forgive me.
You've now connected the foo-node
sliver
node to an erlang
node
[email protected]
.
If you did nothing else, you should see messages in the repl
that look like:
2015-Nov-16 21:28:59 +0000 salad-fingers DEBUG [sliver.protocol] - :tock
2015-Nov-16 21:28:59 +0000 salad-fingers DEBUG [sliver.protocol] - Looping...
When two erlang
nodes are connected, if they don't exchange any data they
will ping each other every minute. In this case, [email protected]
, the erlang
node, is pinging your sliver
node which replies with a pong (in erlang
these are referred to as tick
and tock
). If a node does not reply a tick
with a tock
, its counterpart will assume the node is down and close the
socket.
Now that the nodes are connected, you can send messages between them. It'll be
easier if you send messages to a registered process first since this won't
require that the receiver send its own self
and waits for a reply.
The receiver will be an erlang
process. The easiest thing to do is to
register the shell in the erlang
node so that sliver
can send messages to
it:
(bar@127.0.0.1)1> register(shell, self()).
true
After that, send it a message from the sliver
node:
your-spiffy-new-node.core> (p/spawn foo-node #(p/! foo-node ['shell "[email protected]"] 'hai))
#borges.type.Pid{:node foo@127.0.0.1, :pid 4, :serial 0, :creation 0}
your-spiffy-new-node.core> 2015-Nov-16 21:47:01 +0000 salad-fingers DEBUG [sliver.node] - Sending reg msg to: shell on bar@127.0.0.1
2015-Nov-16 21:47:01 +0000 salad-fingers DEBUG [sliver.protocol] - SEND-REG-MESSAGE: Sent 54 bytes
your-spiffy-new-node.core>
Check that the message actually arrived:
(bar@127.0.0.1)2> flush().
Shell got hai
ok
(bar@127.0.0.1)3>
Great!
NOTE: The astute reader will have noticed that to send a message to a remote
registered process we need to spawn a sliver
process (a pulsar
actor won't
be enough). This is because the specifications of the erlang
protocol require
that the sending pid
is present in the message that goes in the wire (not
just the message you're trying to send, but what actually gets encoded and
travels across to the remote node). Hence, this must be done inside a sliver
process.
Now let's try sending a message to a remote pid
.
First let's spawn a registered sliver
process that receives the pid
of its
interlocutor. Once it receives it, it replies with a message. A sort of
ping/pong action:
your-spiffy-new-node.core> (def ping (p/spawn foo-node
#(do
(p/register foo-node 'pong (p/self foo-node))
(a/receive ['ping from]
(do (prn "Message received...replying")
(p/! foo-node from 'pong)
(prn "Finishing now."))))))
#'your-spiffy-new-node.core/ping
2015-Nov-16 23:55:28 +0000 salad-fingers DEBUG [sliver.node] - Registering #borges.type.Pid{:node foo@127.0.0.1, :pid 6, :serial 0, :creation 0} as pong
Now in the erlang
shell we send the sliver
process pong
a ping message
together with its pid
:
(bar@127.0.0.1)12> {pong, '[email protected]'} ! {ping, self()}.
{ping,<0.39.0>}
sliver
receives and delivers the message accordingly:
2015-Nov-16 23:55:36 +0000 salad-fingers DEBUG [sliver.handler] - HANDLER: #borges.type.Pid{:node bar@127.0.0.1, :pid 39, :serial 0, :creation 3} -> pong :: [ping #borges.type.Pid{:node bar@127.0.0.1, :pid 39, :serial 0, :creation 3}]
2015-Nov-16 23:55:36 +0000 salad-fingers DEBUG [sliver.protocol] - Looping...
"Message received...replying"
"Finishing now."
2015-Nov-16 23:55:36 +0000 salad-fingers DEBUG [sliver.protocol] - SEND-MESSAGE: Sent 50 bytes
Now we check back in the erlang
shell that the sliver
process has sent the
reply across:
(bar@127.0.0.1)13> flush().
Shell got pong
ok
Huzzah!
Underneath sliver
lies borges the
erlang
term encoder/decoder. To see which types can be de/encoded, please see
this test.
As you might've guessed, since sliver
uses pulsar
under the hood, many of
its features are integrated:
- links: you can
link
andspawn-link
insliver
. There's no support for remote linking though. Please see the tests for examples of how to do this. - monitors: you can
monitor
,demonitor
, andspawn-monitor
. Again, no support for remote monitoring just now. Please see the tests for examples of how to do this. - references: you can create references (like
erlang
) withmake-ref
. - finding names, pids, actors: you can find all the different personalities of a process via
whereis
,actor-for
,name-for
, andpid-for
Please send pull requests my way. You can run all tests with lein test
.
Copyright © 2015 Ulises Cerviño Beresi
Distributed under the MIT License.