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
DEBUGmessages.sliveris 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
connectit to another node (anerlangorslivernode) - you can
spawnprocesses - you can send (
!) messages to processes (local or remote) - you can
startthe 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. Apidcan be local to theslivernode, or remote (from anerlangor aslivernode) - a named process: this is the name (I prefer
symbolsorkeywordsthough any object should be good to be used as a name) under which the process is registered. Names are local to theslivernode - 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:
spawned a process that waits for a message (any message), and prints it to console- sent a message (
'ohai') to thepidof 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()).
trueAfter 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 pongNow 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 bytesNow 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
okHuzzah!
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
linkandspawn-linkinsliver. 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.