├── .gitignore ├── .gitmodules ├── LICENSE ├── README.markdown ├── architecture.png ├── project.clj ├── src └── com │ └── keminglabs │ └── zmq_async │ └── core.clj ├── test └── com │ └── keminglabs │ └── zmq_async │ └── t_core.clj └── vendor ├── poms ├── linux64 │ └── pom.xml └── osx64 │ └── pom.xml └── release.sh /.gitignore: -------------------------------------------------------------------------------- 1 | target/ -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/jzmq"] 2 | path = vendor/jzmq 3 | url = https://github.com/zeromq/jzmq.git 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Kevin Lynagh 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * The name Kevin Lynagh may not be used to endorse or promote products 15 | derived from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL KEVIN LYNAGH BE LIABLE FOR ANY DIRECT, 21 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 22 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 24 | OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 25 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 26 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # ZeroMQ Async 2 | 3 | ZeroMQ is a message-oriented socket system that supports many communication styles (request/reply, publish/subscribe, fan-out, &c.) on top of many transport layers with bindings to many languages. 4 | However, ZeroMQ sockets are not thread safe---concurrent usage typically requires explicit locking or dedicated threads and queues. 5 | This library handles all of that for you, taking your ZeroMQ sockets and hiding them behind thread safe core.async channels. 6 | 7 | [Quick start](#quick-start) | [Caveats](#caveats) | [Architecture](#architecture) | [Thanks!](#thanks) | [Other Clojure ZMQ libs](#other-clojure-zeromq-libraries) 8 | 9 | ## Quick start 10 | 11 | Add to your `project.clj`: 12 | 13 | [com.keminglabs/zmq-async "0.1.0"] 14 | 15 | Your system should have ZeroMQ 3.2 installed: 16 | 17 | brew install zeromq 18 | 19 | or 20 | 21 | apt-get install libzmq3 22 | 23 | This library provides one function, `register-socket!`, which associates a ZeroMQ socket with core.async channel(s) `in` (into which strings or byte arrays are written) and/or `out` (whence byte arrays). 24 | Writing a Clojure collection of strings and/or byte arrays sends a multipart message; received multipart messages are put on core.async channels as a vector of byte arrays. 25 | 26 | The easiest way to get started is to have zmq-async create sockets and the backing message pumps automagically for you: 27 | 28 | ```clojure 29 | (require '[com.keminglabs.zmq-async.core :refer [register-socket!]] 30 | '[clojure.core.async :refer [>! ! s-in "pong")) 43 | (close! s-in)) 44 | 45 | (go (dotimes [_ n] 46 | (>! c-in "ping") 47 | (println (String. (! !! alts!!]] 4 | [clojure.core.match :refer [match]] 5 | [clojure.set :refer [subset?]] 6 | [clojure.edn :refer [read-string]] 7 | [clojure.set :refer [map-invert]]) 8 | (:import java.util.concurrent.LinkedBlockingQueue 9 | (org.zeromq ZMQ ZContext ZMQ$Socket ZMQ$Poller))) 10 | 11 | ;;Some terminology: 12 | ;; 13 | ;; sock: ZeroMQ socket object 14 | ;; addr: address of a sock (a string) 15 | ;; sock-id: randomly generated string ID created by the core.async thread when a new socket is requested 16 | ;; chan: core.async channel 17 | ;; pairing: map entry of {sock-id {:out chan :in chan}}. 18 | ;; 19 | ;; All in/out labels are written relative to this namespace. 20 | 21 | (def BLOCK 0) 22 | 23 | (defn send! 24 | [^ZMQ$Socket sock msg] 25 | (let [msg (if (coll? msg) msg [msg])] 26 | (loop [[head & tail] msg] 27 | ;;TODO: handle byte buffers. 28 | (let [res (.send sock head (if tail (bit-or ZMQ/NOBLOCK ZMQ/SNDMORE) ZMQ/NOBLOCK))] 29 | (cond 30 | (= false res) (println "WARNING: Message not sent on" sock) 31 | tail (recur tail)))))) 32 | 33 | (defn receive-all 34 | "Receive all data parts from the socket, returning a vector of byte arrays. 35 | If the socket does not contain a multipart message, returns a plain byte array." 36 | [^ZMQ$Socket sock] 37 | (loop [acc (transient [])] 38 | (let [new-acc (conj! acc (.recv sock))] 39 | (if (.hasReceiveMore sock) 40 | (recur new-acc) 41 | (let [res (persistent! new-acc)] 42 | (if (= 1 (count res)) 43 | (first res) res)))))) 44 | 45 | (defn poll 46 | "Blocking poll that returns a [val, socket] tuple. 47 | If multiple sockets are ready, one is chosen to be read from nondeterministically." 48 | [socks] 49 | ;;TODO: what's the perf cost of creating a new poller all the time? 50 | (let [n (count socks) 51 | poller (ZMQ$Poller. n)] 52 | (doseq [s socks] 53 | (.register poller s ZMQ$Poller/POLLIN)) 54 | (.poll poller) 55 | 56 | ;;Randomly take the first ready socket and its message, to match core.async's alts! behavior 57 | (->> (shuffle (range n)) 58 | (filter #(.pollin poller %)) 59 | first 60 | (.getSocket poller) 61 | ((juxt receive-all identity))))) 62 | 63 | 64 | (defn zmq-looper 65 | "Runnable fn with blocking loop on zmq sockets. 66 | Opens/closes zmq sockets according to messages received on `zmq-control-sock`. 67 | Relays messages from zmq sockets to `async-control-chan`." 68 | [queue zmq-control-sock async-control-chan] 69 | (fn [] 70 | ;;Socks is a map of string socket-ids to ZeroMQ socket objects (plus a single :control keyword key associated with the thread's control socket). 71 | (loop [socks {:control zmq-control-sock}] 72 | (let [[val sock] (poll (vals socks)) 73 | id (get (map-invert socks) sock) 74 | ;;Hack coercion so we can have a pattern match against message from control socket 75 | val (if (= :control id) (keyword (String. val)) val)] 76 | 77 | (assert (not (nil? id))) 78 | 79 | (match [id val] 80 | 81 | ;;A message indicating there's a message waiting for us to process on the queue. 82 | [:control :sentinel] 83 | (let [msg (.take queue)] 84 | (match [msg] 85 | 86 | [[:register sock-id new-sock]] 87 | (recur (assoc socks sock-id new-sock)) 88 | 89 | [[:close sock-id]] 90 | (do 91 | (.close (socks sock-id)) 92 | (recur (dissoc socks sock-id))) 93 | 94 | ;;Send a message out 95 | [[sock-id outgoing-message]] 96 | (do 97 | (send! (socks sock-id) outgoing-message) 98 | (recur socks)))) 99 | 100 | [:control :shutdown] 101 | (doseq [[_ sock] socks] 102 | (.close sock)) 103 | 104 | [:control msg] 105 | (throw (Exception. (str "bad ZMQ control message: " msg))) 106 | 107 | ;;It's an incoming message, send it to the async thread to convey to the application 108 | [incoming-sock-id msg] 109 | (do 110 | (>!! async-control-chan [incoming-sock-id msg]) 111 | (recur socks))))))) 112 | 113 | 114 | (defn sock-id-for-chan 115 | [c pairings] 116 | (first (for [[id {in :in out :out}] pairings 117 | :when (#{in out} c)] 118 | id))) 119 | 120 | (defn command-zmq-thread! 121 | "Helper used by the core.async thread to relay a command to the ZeroMQ thread. 122 | Puts message of interest on queue and then sends a sentinel value over zmq-control-sock so that ZeroMQ thread unblocks." 123 | [zmq-control-sock queue msg] 124 | (.put queue msg) 125 | (send! zmq-control-sock "sentinel")) 126 | 127 | (defn shutdown-pairing! 128 | "Close ZeroMQ socket with `id` and all associated channels." 129 | [[sock-id chanmap] zmq-control-sock queue] 130 | (command-zmq-thread! zmq-control-sock queue 131 | [:close sock-id]) 132 | (doseq [[_ c] chanmap] 133 | (when c (close! c)))) 134 | 135 | (defn async-looper 136 | "Runnable fn with blocking loop on channels. 137 | Controlled by messages sent over provided `async-control-chan`. 138 | Sends messages to complementary `zmq-looper` via provided `zmq-control-sock` (assumed to be connected)." 139 | [queue async-control-chan zmq-control-sock] 140 | (fn [] 141 | ;; Pairings is a map of string id to {:out chan :in chan} map, where existence of :out and :in depend on the type of ZeroMQ socket. 142 | (loop [pairings {:control {:in async-control-chan}}] 143 | (let [in-chans (remove nil? (map :in (vals pairings))) 144 | [val c] (alts!! in-chans) 145 | id (sock-id-for-chan c pairings)] 146 | 147 | (match [id val] 148 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 149 | ;;Control messages 150 | 151 | ;;Register a new socket. 152 | [:control [:register sock chanmap]] 153 | (let [sock-id (str (gensym "zmq-"))] 154 | (command-zmq-thread! zmq-control-sock queue [:register sock-id sock]) 155 | (recur (assoc pairings sock-id chanmap))) 156 | 157 | ;;Relay a message from ZeroMQ socket to core.async channel. 158 | [:control [sock-id msg]] 159 | (let [out (get-in pairings [sock-id :out])] 160 | (assert out) 161 | ;;We have a contract with library consumers that they cannot give us channels that can block, so this >!! won't tie up the async looper. 162 | (>!! out msg) 163 | (recur pairings)) 164 | 165 | ;;The control channel has been closed, close all ZMQ sockets and channels. 166 | [:control nil] 167 | (let [opened-pairings (dissoc pairings :control)] 168 | 169 | (doseq [p opened-pairings] 170 | (shutdown-pairing! p zmq-control-sock queue)) 171 | 172 | (send! zmq-control-sock "shutdown") 173 | ;;Don't recur... 174 | nil) 175 | 176 | [:control msg] (throw (Exception. (str "bad async control message: " msg))) 177 | 178 | 179 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 180 | ;;Non-control messages 181 | 182 | ;;The channel was closed, close the corresponding socket. 183 | [id nil] 184 | (do 185 | (shutdown-pairing! [id (pairings id)] zmq-control-sock queue) 186 | (recur (dissoc pairings id))) 187 | 188 | ;;Just convey the message to the ZeroMQ socket. 189 | [id msg] 190 | (do 191 | (command-zmq-thread! zmq-control-sock queue [id msg]) 192 | (recur pairings))))))) 193 | 194 | 195 | 196 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 197 | ;;Public API 198 | (defn create-context 199 | "Creates a zmq-async context map containing the following keys: 200 | 201 | zcontext jzmq ZContext object from which sockets are created 202 | shutdown no-arg fn that shuts down this context, closing all ZeroMQ sockets 203 | addr address of in-process ZeroMQ socket used to control ZeroMQ thread 204 | sock-server server end of zmq pair socket; must be bound via (.bind addr) method before starting the zmq thread 205 | sock-client client end of zmq pair socket; must be connected via (.connect addr) method before starting the async thread 206 | async-control-chan channel used to control async thread 207 | zmq-thread 208 | async-thread" 209 | 210 | ([] (create-context nil)) 211 | ([name] 212 | (let [addr (str "inproc://" (gensym "zmq-async-")) 213 | zcontext (ZContext.) 214 | sock-server (.createSocket zcontext ZMQ/PAIR) 215 | sock-client (.createSocket zcontext ZMQ/PAIR) 216 | 217 | ;;Shouldn't have to have a large queue; it's okay to block core.async thread puts since that'll give time for the ZeroMQ thread to catch up. 218 | queue (LinkedBlockingQueue. 8) 219 | 220 | async-control-chan (chan) 221 | 222 | zmq-thread (doto (Thread. (zmq-looper queue sock-server async-control-chan)) 223 | (.setName (str "ZeroMQ looper " "[" (or name addr) "]")) 224 | (.setDaemon true)) 225 | async-thread (doto (Thread. (async-looper queue async-control-chan sock-client)) 226 | (.setName (str "core.async looper" "[" (or name addr) "]")) 227 | (.setDaemon true))] 228 | 229 | {:zcontext zcontext 230 | :addr addr 231 | :sock-server sock-server 232 | :sock-client sock-client 233 | :queue queue 234 | :async-control-chan async-control-chan 235 | :zmq-thread zmq-thread 236 | :async-thread async-thread 237 | :shutdown #(close! async-control-chan)}))) 238 | 239 | (defn initialize! 240 | "Initializes a zmq-async context by binding/connecting both ends of the ZeroMQ control socket and starting both threads. 241 | Does nothing if zmq-thread is already started." 242 | [context] 243 | (let [{:keys [addr sock-server sock-client 244 | zmq-thread async-thread]} context] 245 | (when-not (.isAlive zmq-thread) 246 | (.bind sock-server addr) 247 | (.start zmq-thread) 248 | 249 | (.connect sock-client addr) 250 | (.start async-thread))) 251 | nil) 252 | 253 | (def ^:private automagic-context 254 | "Default context used by any calls to `register-socket!` that don't specify an explicit context." 255 | (create-context "zmq-async default context")) 256 | 257 | (defn register-socket! 258 | "Associate ZeroMQ `socket` with provided write-only `out` and read-only `in` ports. 259 | Accepts a map with the following keys: 260 | 261 | :context - The zmq-async context under which the ZeroMQ socket should be maintained; defaults to a global context if none is provided 262 | :in - Write-only core.async port on which you should place outgoing messages 263 | :out - Read-only core.async port on which zmq-async places incoming messages; this port should never block 264 | :socket - A ZeroMQ socket object that can be read from and/or written to (i.e., already bound/connected to at least one address) 265 | :socket-type - If a :socket is not provided, this socket-type will be created for you; must be one of :pair :dealer :router :pub :sub :req :rep :pull :push :xreq :xrep :xpub :xsub 266 | :configurator - If a :socket is not provided, this function will be used to configure a newly instantiated socket of :socket-type; you should bind/connect to at least one address within this function; see http://zeromq.github.io/jzmq/javadocs/ for the ZeroMQ socket configuration options 267 | 268 | " 269 | [{:keys [context in out socket socket-type configurator]}] 270 | 271 | (when (and (nil? socket) 272 | (or (nil? socket-type) (nil? configurator))) 273 | (throw (IllegalArgumentException. "Must provide an instantiated and bound/connected ZeroMQ socket or a socket-type and configurator fn."))) 274 | 275 | (when (and socket (or socket-type configurator)) 276 | (throw (IllegalArgumentException. "You can provide a ZeroMQ socket OR a socket-type and configurator, not both."))) 277 | 278 | (when (and (nil? out) (nil? in)) 279 | (throw (IllegalArgumentException. "You must provide at least one of :out and :in channels."))) 280 | 281 | (let [context (or context (doto automagic-context 282 | (initialize!))) 283 | ^ZMQ$Socket socket (or socket (doto (.createSocket (context :zcontext) 284 | (case socket-type 285 | :pair ZMQ/PAIR 286 | :pub ZMQ/PUB 287 | :sub ZMQ/SUB 288 | :req ZMQ/REQ 289 | :rep ZMQ/REP 290 | :xreq ZMQ/XREQ 291 | :xrep ZMQ/XREP 292 | :dealer ZMQ/DEALER 293 | :router ZMQ/ROUTER 294 | :xpub ZMQ/XPUB 295 | :xsub ZMQ/XSUB 296 | :pull ZMQ/PULL 297 | :push ZMQ/PUSH)) 298 | configurator))] 299 | 300 | (>!! (:async-control-chan context) 301 | [:register socket {:in in :out out}]))) 302 | 303 | 304 | (comment 305 | (require '[clojure.pprint :refer [pprint]] 306 | '[clojure.stacktrace :refer [e]] 307 | '[clojure.tools.namespace.repl :refer [refresh refresh-all]]) 308 | (clojure.tools.namespace.repl/refresh) 309 | 310 | ) -------------------------------------------------------------------------------- /test/com/keminglabs/zmq_async/t_core.clj: -------------------------------------------------------------------------------- 1 | (ns com.keminglabs.zmq-async.t-core 2 | (:require [com.keminglabs.zmq-async.core :refer :all] 3 | [clojure.core.async :refer [go close! >!! sock-A 20 | ;;Need to use this awkward seq stuff here to compare byte arrays by value 21 | (seq val) => (seq (.getBytes "A message")))))) 22 | 23 | (fact "ZMQ looper" 24 | (with-state-changes [(around :facts 25 | (let [{:keys [zmq-thread addr sock-server sock-client async-control-chan queue] 26 | :as context} (create-context) 27 | _ (do 28 | (.bind sock-server addr) 29 | (.start zmq-thread)) 30 | zcontrol (doto sock-client 31 | (.connect addr))] 32 | 33 | (try 34 | ?form 35 | (finally 36 | (send! zcontrol "shutdown") 37 | (.join zmq-thread 100) 38 | (assert (not (.isAlive zmq-thread))) 39 | 40 | ;;Close any hanging ZeroMQ sockets. 41 | (doseq [s (.getSockets (context :zcontext))] 42 | (.close s))))))] 43 | 44 | ;;TODO: rearchitect so that one concern can be tested at a time? 45 | ;;Then the zmq looper would need to use accessible mutable state instead of loop/recur... 46 | (fact "Opens sockets, conveys messages between sockets and async control channel" 47 | (let [test-addr "inproc://open-test" 48 | test-id "open-test" 49 | test-sock (doto (.createSocket (context :zcontext) ZMQ/PAIR) 50 | (.bind test-addr))] 51 | 52 | (command-zmq-thread! zcontrol queue 53 | [:register test-id test-sock]) 54 | 55 | (with-open [sock (.createSocket (context :zcontext) ZMQ/PAIR)] 56 | (.connect sock test-addr) 57 | 58 | ;;passes along received messages 59 | (let [test-msg "hihi"] 60 | (.send sock test-msg) 61 | 62 | (let [[id msg] ( test-id 64 | (seq msg) => (seq (.getBytes test-msg)))) 65 | 66 | ;;including multipart messages 67 | (let [test-msg ["yo" "what's" "up?"]] 68 | (.send sock "yo" ZMQ/SNDMORE) 69 | (.send sock "what's" ZMQ/SNDMORE) 70 | (.send sock "up?") 71 | 72 | (let [[id msg] ( test-id 74 | (map #(String. %) msg) => test-msg)) 75 | 76 | ;;sends messages when asked to 77 | (let [test-msg "heyo"] 78 | (command-zmq-thread! zcontrol queue 79 | [test-id test-msg]) 80 | (Thread/sleep 50) 81 | (.recvStr sock ZMQ/NOBLOCK) => test-msg)))))) 82 | 83 | (fact "core.async looper" 84 | (with-state-changes [(around :facts 85 | (let [context (create-context) 86 | {:keys [zmq-thread addr sock-server sock-client async-control-chan async-thread queue]} context 87 | acontrol async-control-chan 88 | zcontrol (doto sock-server 89 | (.bind addr))] 90 | 91 | (.connect sock-client addr) 92 | (.start async-thread) 93 | (try 94 | ?form 95 | (finally 96 | (close! acontrol) 97 | (.join async-thread 100) 98 | (assert (not (.isAlive async-thread))) 99 | 100 | ;;Close any hanging ZeroMQ sockets. 101 | (doseq [s (.getSockets (context :zcontext))] 102 | (.close s))))))] 103 | 104 | (fact "Tells ZMQ looper to shutdown when the async thread's control channel is closed" 105 | (close! acontrol) 106 | (Thread/sleep 50) 107 | (.recvStr zcontrol ZMQ/NOBLOCK) => "shutdown") 108 | 109 | (fact "Closes all open sockets when the async thread's control channel is closed" 110 | 111 | ;;register test socket 112 | (register-socket! {:context context :out (chan) :in (chan) 113 | :socket-type :req :configurator #(.connect % "ipc://test-addr")}) 114 | 115 | (Thread/sleep 50) 116 | (.recvStr zcontrol ZMQ/NOBLOCK) => "sentinel" 117 | 118 | (let [[cmd sock-id _] (.take queue)] 119 | cmd => :register 120 | ;;Okay, now to actually test what we care about... 121 | ;;close the control socket 122 | (close! acontrol) 123 | (Thread/sleep 50) 124 | 125 | ;;the ZMQ thread was told to close the socket we opened earlier 126 | (.recvStr zcontrol ZMQ/NOBLOCK) => "sentinel" 127 | (.take queue) => [:close sock-id] 128 | (.recvStr zcontrol ZMQ/NOBLOCK) => "shutdown")) 129 | 130 | (fact "Forwards messages recieved from ZeroMQ thread to appropriate core.async channel." 131 | (let [out (chan) in (chan)] 132 | 133 | ;;register test socket 134 | (register-socket! {:context context :out out :in in 135 | :socket-type :req :configurator #(.bind % "ipc://test-addr")}) 136 | 137 | (Thread/sleep 50) 138 | (.recvStr zcontrol ZMQ/NOBLOCK) => "sentinel" 139 | (let [[cmd sock-id _] (.take queue)] 140 | cmd => :register 141 | ;;Okay, now to actually test what we care about... 142 | (let [test-msg "hihi"] 143 | ;;pretend the zeromq thread got a message from the socket... 144 | (>!! acontrol [sock-id test-msg]) 145 | 146 | ;;and it should get forwarded the the recv port. 147 | ( test-msg)))) 148 | 149 | (fact "Forwards messages recieved from core.async 'out' channel to ZeroMQ thread." 150 | (let [out (chan) in (chan)] 151 | 152 | ;;register test socket 153 | (register-socket! {:context context :out out :in in 154 | :socket-type :req :configurator #(.bind % "ipc://test-addr")}) 155 | 156 | (Thread/sleep 50) 157 | (.recvStr zcontrol ZMQ/NOBLOCK) => "sentinel" 158 | (let [[cmd sock-id _] (.take queue)] 159 | cmd => :register 160 | 161 | ;;Okay, now to actually test what we care about... 162 | (let [test-msg "hihi"] 163 | (>!! in test-msg) 164 | (Thread/sleep 50) 165 | (.recvStr zcontrol ZMQ/NOBLOCK) => "sentinel" 166 | (.take queue) => [sock-id test-msg])))))) 167 | 168 | 169 | (fact "Integration" 170 | (with-state-changes [(around :facts 171 | (let [context (doto (create-context) 172 | (initialize!)) 173 | {:keys [async-thread zmq-thread]} context] 174 | 175 | (try 176 | ?form 177 | (finally 178 | ((:shutdown context)) 179 | (.join async-thread 100) 180 | (assert (not (.isAlive async-thread))) 181 | 182 | (.join zmq-thread 100) 183 | (assert (not (.isAlive zmq-thread))) 184 | 185 | ;;Close any hanging ZeroMQ sockets. 186 | (doseq [s (.getSockets (context :zcontext))] 187 | (.close s))))))] 188 | 189 | (fact "raw->wrapped" 190 | (let [addr "ipc://test-addr" test-msg "hihi" 191 | out (chan) in (chan)] 192 | 193 | (register-socket! {:context context :out out :in in 194 | :socket-type :pair :configurator #(.bind % addr)}) 195 | 196 | (.send (doto (.createSocket (context :zcontext) ZMQ/PAIR) 197 | (.connect addr)) 198 | test-msg) 199 | (String. ( test-msg)) 200 | 201 | (fact "raw->wrapped, no explicit context" 202 | (let [addr "ipc://test-addr" test-msg "hihi" 203 | out (chan) in (chan)] 204 | 205 | (register-socket! {:out out :in in 206 | :socket-type :pair :configurator #(.bind % addr)}) 207 | 208 | (.send (doto (.createSocket (context :zcontext) ZMQ/PAIR) 209 | (.connect addr)) 210 | test-msg) 211 | (String. ( test-msg)) 212 | 213 | (fact "wrapped->raw" 214 | (let [addr "inproc://test-addr" test-msg "hihi" 215 | out (chan) in (chan)] 216 | 217 | (register-socket! {:context context :out out :in in 218 | :socket-type :pair :configurator #(.bind % addr)}) 219 | 220 | (let [raw (doto (.createSocket (context :zcontext) ZMQ/PAIR) 221 | (.connect addr))] 222 | (>!! in test-msg) 223 | 224 | (Thread/sleep 50) ;;gross! 225 | (.recvStr raw ZMQ/NOBLOCK)) => test-msg)) 226 | 227 | 228 | (fact "wrapped pair -> wrapped pair" 229 | (let [addr "inproc://test-addr" test-msg "hihi" 230 | [s-out s-in c-out c-in] (repeatedly 4 chan)] 231 | 232 | (register-socket! {:context context :out s-out :in s-in 233 | :socket-type :pair :configurator #(.bind % addr)}) 234 | (register-socket! {:context context :out c-out :in c-in 235 | :socket-type :pair :configurator #(.connect % addr)}) 236 | 237 | (>!! c-in test-msg) 238 | (String. ( test-msg)) 239 | 240 | (fact "wrapped pair -> wrapped pair w/ multipart message" 241 | (let [addr "inproc://test-addr" test-msg ["hihi" "what's" "up?"] 242 | [s-out s-in c-out c-in] (repeatedly 4 chan)] 243 | 244 | (register-socket! {:context context :out s-out :in s-in 245 | :socket-type :pair :configurator #(.bind % addr)}) 246 | (register-socket! {:context context :out c-out :in c-in 247 | :socket-type :pair :configurator #(.connect % addr)}) 248 | 249 | (>!! c-in test-msg) 250 | (map #(String. %) ( test-msg)) 251 | 252 | 253 | (fact "wrapped req <-> wrapped rep, go/future" 254 | (let [addr "inproc://test-addr" 255 | [s-out s-in c-out c-in] (repeatedly 4 chan) 256 | n 5 257 | server (go 258 | (dotimes [_ n] 259 | (assert (= "ping" (String. (! s-in "pong")) 262 | :success) 263 | 264 | client (future 265 | (dotimes [_ n] 266 | (>!! c-in "ping") 267 | (assert (= "pong" (String. ( :success 277 | (close! c-out) 278 | (close! s-out) 279 | (close! server) 280 | ( :success)) 281 | 282 | (fact "wrapped req <-> wrapped rep, go/go" 283 | (let [addr "inproc://test-addr" 284 | [s-out s-in c-out c-in] (repeatedly 4 chan) 285 | n 5 286 | server (go 287 | (dotimes [_ n] 288 | (assert (= "ping" (String. (! s-in "pong")) 291 | :success) 292 | 293 | client (go 294 | (dotimes [_ n] 295 | (>! c-in "ping") 296 | (assert (= "pong" (String. ( :success 308 | (close! c-out) 309 | (close! s-out) 310 | (close! server) 311 | ( :success)))) 312 | 313 | (fact "register-socket! throws errors when given invalid optmaps" 314 | (register-socket! {}) => (throws IllegalArgumentException) 315 | (register-socket! {:out (chan) :in (chan)}) => (throws IllegalArgumentException) 316 | (register-socket! {:socket-type :req :configurator identity}) => (throws IllegalArgumentException) 317 | (register-socket! {:socket "grr" :out (chan) :in (chan) 318 | :socket-type :req :configurator identity}) => (throws IllegalArgumentException)) -------------------------------------------------------------------------------- /vendor/poms/linux64/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | com.keminglabs 4 | jzmq-linux64 5 | jar 6 | VERSION 7 | 8 | -------------------------------------------------------------------------------- /vendor/poms/osx64/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | com.keminglabs 4 | jzmq-osx64 5 | jar 6 | VERSION 7 | 8 | -------------------------------------------------------------------------------- /vendor/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | #This script builds jzmq and pushes the native JAR 6 | 7 | cd jzmq 8 | git reset HEAD --hard 9 | 10 | 11 | ############### 12 | #Build library 13 | 14 | # Note, homebrew's pkg-config has issues; run: 15 | # 16 | # eval `brew --config | grep HOMEBREW_PREFIX | sed 's/: /=/'` 17 | # sudo bash -c 'echo '$HOMEBREW_PREFIX/share/aclocal' >> `aclocal --print-ac-dir`/dirlist' 18 | # 19 | # to resolve. 20 | 21 | ./autogen.sh 22 | ./configure 23 | make 24 | 25 | 26 | ############### 27 | #Create jars 28 | 29 | VERSION=`git rev-parse HEAD | cut -c 1-7` 30 | 31 | mvn versions:set -DgenerateBackupPoms=false \ 32 | -DnewVersion=$VERSION 33 | 34 | ## It's not possible to change the group id using the maven versions plugin; use good ol' sed instead 35 | sed -i.bak "s/org.zeromq/com.keminglabs/" pom.xml 36 | 37 | mvn package 38 | 39 | JAR="jzmq-$VERSION.jar" 40 | if [ -f "target/$JAR" ]; then 41 | mvn install:install-file -Dfile="target/$JAR" \ 42 | -DgroupId=com.keminglabs \ 43 | -Dversion=$VERSION \ 44 | -Dpackaging=jar \ 45 | -DartifactId=jzmq 46 | echo "lein deploy clojars com.keminglabs/jzmq $VERSION 'vendor/jzmq/target/$JAR' 'vendor/jzmq/pom.xml'" 47 | fi 48 | 49 | OSX_JAR="jzmq-$VERSION-native-x86_64-Mac OS X.jar" 50 | if [ -f "target/$OSX_JAR" ]; then 51 | mvn install:install-file -Dfile="target/$OSX_JAR" \ 52 | -DgroupId=com.keminglabs \ 53 | -Dversion=$VERSION \ 54 | -Dpackaging=jar \ 55 | -DartifactId=jzmq-osx64 56 | mkdir -p osx64-pom 57 | cat ../poms/osx64/pom.xml | sed "s/VERSION/$VERSION/" > osx64-pom/pom.xml 58 | echo "lein deploy clojars com.keminglabs/jzmq-osx64 $VERSION 'vendor/jzmq/target/$OSX_JAR' 'vendor/jzmq/osx64-pom/pom.xml'" 59 | fi 60 | 61 | LINUX_JAR="jzmq-$VERSION-native-amd64-Linux.jar" 62 | if [ -f "target/$LINUX_JAR" ]; then 63 | mvn install:install-file -Dfile="target/$LINUX_JAR" \ 64 | -DgroupId=com.keminglabs \ 65 | -Dversion=$VERSION \ 66 | -Dpackaging=jar \ 67 | -DartifactId=jzmq-linux64 68 | mkdir -p linux64-pom 69 | cat ../poms/linux64/pom.xml | sed "s/VERSION/$VERSION/" > linux64-pom/pom.xml 70 | echo "lein deploy clojars com.keminglabs/jzmq-linux64 $VERSION 'vendor/jzmq/target/$LINUX_JAR' 'vendor/jzmq/linux64-pom/pom.xml'" 71 | fi 72 | 73 | 74 | echo "Success! Please push generated JARs to Clojars by copy/pasting 'lein deploy' strings printed above." 75 | 76 | --------------------------------------------------------------------------------