An Example LFE/Clojure Multi-node System using OTP and Supervision Trees
This project is a port of Maxim Molchanov's example Erlang + Clojure interop project (via JInterface). Only minor changes were made to the Clojure code. The Erlang code was completely replaced with LFE.
This project demonstrates how one can:
- Create a Clojure project which communicates with LFE/Erlang nodes utilizing Clojang (an Erlang JInterface wrapper)
- Start a supervised Clojure node in LFE,
- Send messages to Clojure nodes from LFE
- Send messages to Clojure nodes from Clojure
- Send messages to LFE from Clojure
- Receive and respond to all messages in both Clojure and LFE
For LFE:
- Erlang
rebar3
For Clojure:
- Java
lein
To get started, compile the LFE source files for lfenode
and build the
Clojure uberjar for cljnode
:
$ make compile
We'll examine two ways of running our application nodes:
- As daemons (non-OTP release)
- From the LFE and Clojure REPLs
Once everything has compiled successfully, you can start an LFE REPL and then bring the app up:
TBD
During active development it's useful to be able to run your code in a REPL.
Below we show how to do that in the case of lfecljapp
, starting the two two
servers up from their respective REPLs for easy interaction.
Start the Clojure REPL and then start the Clojure node's server:
$ make clojure-repl
2017-02-12 18:59:34,080 [main] INFO clojang.agent.startup - Bringing up ...
2017-02-12 18:59:34,267 [main] INFO clojang.agent.startup - Registered nodes ...
nREPL server started on port 34904 on host 127.0.0.1 - nrepl://127.0.0.1:34904
REPL-y 0.3.7, nREPL 0.2.12
Clojure 1.8.0
OpenJDK 64-Bit Server VM 1.8.0_121-8u121-b13-0ubuntu1.16.04.2-b13
Docs: (doc function-name-here)
(find-doc "part-of-name-here")
Source: (source function-name-here)
Javadoc: (javadoc java-object-or-class-here)
Exit: Control+D or (exit) or (quit)
Results: Stored in vars *1, *2, *3, an exception in *e
cljnode.core=>
cljnode.core=> (def server-data (managed-server))
2017-02-13 17:42:23,386 [async-thread-macro-2] INFO cljnode.server - Starting ...
#'cljnode.core/server-data
Now let's talk to our Clojure server using the Clojure API we created (see
src/clj/cljnode/api.clj
):
cljnode.core=> (api/ping server-data)
2017-02-13 17:43:00,081 [async-thread-macro-1] INFO cljnode.server - Got :ping ...
:pong
cljnode.core=> (api/ping server-data)
2017-02-13 17:43:00,947 [async-thread-macro-1] INFO cljnode.server - Got :ping ...
:pong
cljnode.core=> (api/ping server-data)
2017-02-13 17:43:01,632 [async-thread-macro-1] INFO cljnode.server - Got :ping ...
:pong
cljnode.core=> (api/ping server-data)
2017-02-13 17:43:02,377 [async-thread-macro-1] INFO cljnode.server - Got :ping ...
:pong
cljnode.core=> (api/get-ping-count server-data)
2017-02-13 17:43:07,113 [async-thread-macro-1] INFO cljnode.server - Got :get-ping-count ...
4
$ make lfe-repl
Erlang/OTP 18 [erts-7.3] [source] [64-bit] [smp:4:4] [async-threads:10] ...
..-~.~_~---..
( \\ ) | A Lisp-2+ on the Erlang VM
|`-.._/_\\_.-': | Type (help) for usage info.
| g |_ \ |
| n | | | Docs: http://docs.lfe.io/
| a / / | Source: http://github.com/rvirding/lfe
\ l |_/ |
\ r / | LFE v1.3-dev (abort with ^G)
`-E___.-'
(lfenode@liberator)lfe>
(lfenode@liberator)lfe> (api:ping 'cljnode@liberator)
pong
(lfenode@liberator)lfe> (api:ping 'cljnode@liberator)
pong
(lfenode@liberator)lfe> (api:ping 'cljnode@liberator)
pong
(lfenode@liberator)lfe> (api:ping 'cljnode@liberator)
pong
(lfenode@liberator)lfe> (api:get-ping-count 'cljnode@liberator)
8
If you take a peek back over in the Clojure terminal, you should see log messages for each of those API calls:
2017-02-14 00:44:06,717 [async-thread-macro-1] INFO cljnode.server - Got :ping ...
2017-02-14 00:44:07,623 [async-thread-macro-1] INFO cljnode.server - Got :ping ...
2017-02-14 00:44:08,599 [async-thread-macro-1] INFO cljnode.server - Got :ping ...
2017-02-14 00:44:13,694 [async-thread-macro-1] INFO cljnode.server - Got :ping ...
2017-02-14 00:44:15,338 [async-thread-macro-1] INFO cljnode.server - Got :get-ping-count ...
You can do this either in Clojure:
cljnode.core=> (api/stop server-data)
:stopping
Or LFE:
(lfenode@liberator)lfe> (api:stop 'cljnode@liberator)
stopping
The end result will be the same:
2017-02-14 01:08:08,664 [async-thread-macro-1] WARN cljnode.server - Got :stop ...
2017-02-14 01:08:08,875 [async-dispatch-3] INFO cljnode.core - Server stopped ...
(defn run
[cmd-chan]
(log/info "Starting Clojure node with nodename ="
(System/getProperty "node.sname"))
(let [init-state 0]
(loop [png-count init-state]
(match (receive)
[:register caller]
(do
(log/infof "Got :register request from %s ..." caller)
(mbox/link (self) caller)
(! caller :linked)
(recur png-count))
[:ping caller]
(do
(log/infof "Got :ping request from %s ..." caller)
(! caller :pong)
(recur (inc png-count)))
[:get-ping-count caller]
(do
(log/infof "Got :get-ping-count request from %s ..." caller)
(! caller png-count)
(recur png-count))
[:stop caller]
(do
(log/warnf "Got :stop request from %s ..." caller)
(! caller :stopping)
:stopped)
[:shutdown caller]
(do
(log/warnf "Got :shutdown request from %s ..." caller)
(! caller :shutting-down)
(async/>! cmd-chan :shutdown))
[_ caller]
(do
(log/error "Bad message received: unknown command")
(! caller [:error :unknown-command])
(recur png-count))
[_]
(do
(log/error "Bad message received: improperly formatted")
(recur png-count))))))
(defn send-only
[server-data msg]
(async/>!! (get-in server-data [:bridge :channel]) msg)
:ok)
(defn send-and-receive
[server-data msg]
(send-only server-data msg)
(receive (get-in server-data [:bridge :mbox])))
(defn register
[server-data]
(send-only server-data :register))
(defn ping
[server-data]
(send-and-receive server-data :ping))
(defn get-ping-count
[server-data]
(send-and-receive server-data :get-ping-count))
(defn stop
[server-data]
(send-and-receive server-data :stop))
(defun send-only (node-name msg)
(! `#(default ,node-name) `#(,msg ,(self))))
(defun send-and-receive (node-name msg)
(send-only node-name msg)
(receive
(data data)))
(defun register (node-name)
(send-only node-name 'register))
(defun ping (node-name)
(send-and-receive node-name 'ping))
(defun get-ping-count (node-name)
(send-and-receive node-name 'get-ping-count))
(defun stop (node-name)
(send-and-receive node-name 'stop))
Here are some things I'd like to play with in this project:
- Setting up some example long-running computations in Clojure.
- Spawn multiple nodes and distribute computations across an LFE cluster.
- Use
lfecljapp
as a proxy to a Storm cluster, and explores ways in which it might be useful to interact with Storm from LFE.