std.concurrent.bus
request and response messaging between registered threads
A bus owns per-thread queues, dispatches messages to handlers, and completes futures when responses return.
1 Overview
A bus combines thread registration, blocking queues, handler loops, and future-backed responses. Use it when work must be addressed to a specific long-running thread rather than submitted to an anonymous executor task.
2 Walkthrough
2.1 Open a handler
bus:with-temp starts a temporary bus and registers the current thread. bus:open then starts a named handler loop and returns its registration details.
(require '[std.concurrent.bus :as bus])
(bus/bus:with-temp message-bus
(let [{:keys [id stopped]}
@(bus/bus:open message-bus
(fn [{:keys [value]}]
{:value (inc value)}))]
{:reply @(bus/bus:send message-bus id {:value 10})
:info (bus/info-bus message-bus)
:stop (bus/bus:close message-bus id)
:stopped stopped}))
2.2 Inspect and control workers
Use the registry functions to inspect IDs and queues. Prefer bus:close for cooperative shutdown; use bus:kill only when a handler thread must be interrupted.
(bus/bus:all-ids message-bus)
(bus/bus:get-count message-bus)
(bus/bus:send-all message-bus {:op :refresh})
(bus/bus:close-all message-bus)
3 Lifecycle
bus creates and starts a component. bus:create only constructs it. Component-aware code can use start-bus, stop-bus, started?-bus, and info-bus directly.
4 API
- +keys+
- +resource+
- ->Bus
- Bus
- bus
- bus:all-ids
- bus:all-threads
- bus:close
- bus:close-all
- bus:create
- bus:deregister
- bus:get-count
- bus:get-id
- bus:get-queue
- bus:get-thread
- bus:has-id?
- bus:kill
- bus:kill-all
- bus:open
- bus:register
- bus:reset-counters
- bus:send
- bus:send-all
- bus:wait
- bus:with-temp
- bus?
- handler-thunk
- info-bus
- main-loop
- main-thunk
- map->Bus
- run-handler
- start-bus
- started?-bus
- stop-bus
- string-bus
+resource+ ^
NONE
(def +resource+
(res/res:spec-add
{:type :hara/concurrent.bus
:instance {:create bus:create}}))
link
NONE
(impl/defimpl Bus [state results output counters]
:suffix "-bus"
:string string-bus
:protocols [std.protocol.component/IComponent
:include [-start -stop -started? -info]])
link
Bus ^
NONE
(impl/defimpl Bus [state results output counters]
:suffix "-bus"
:string string-bus
:protocols [std.protocol.component/IComponent
:include [-start -stop -started? -info]])
link
v 3.0
(defn bus:all-ids
([{:keys [state] :as bus}]
(let [{:keys [threads]} @state]
(set (keys threads)))))
link
(bus:with-temp bus (bus:all-ids bus)) => (contains #{string?})
v 3.0
(defn bus:all-threads
([{:keys [state] :as bus}]
(let [{:keys [threads]} @state]
(collection/map-vals :thread threads))))
link
(bus:with-temp bus (= (first (vals (bus:all-threads bus))) (Thread/currentThread))) => true
v 3.0
(defn bus:close
([bus id]
(bus:send bus id {:op :exit})))
link
(bus:with-temp bus (let [{:keys [stopped id]} @(bus:open bus (fn [m] (update m :value inc)))] (Thread/sleep 10) (bus:close bus id) [@stopped (bus:get-count bus)])) => (contains-in [{:exit :normal, :id string? :unprocessed empty? :start number? :end number?} 1])
v 3.0
(defn bus:close-all
([bus]
(collection/map-entries (fn [id]
[id (bus:close bus id)])
(bus:all-ids bus))))
link
(bus:with-temp bus (let [_ @(bus:open bus (fn [m] (update m :value inc))) _ @(bus:open bus (fn [m] (update m :value inc))) _ @(bus:open bus (fn [m] (update m :value inc))) _ @(bus:open bus (fn [m] (update m :value inc)))] (Thread/sleep 10) (bus:close-all bus) (Thread/sleep 100) (bus:get-count bus))) => 1
v 3.0
(defn bus:create
([] (bus:create nil))
([m]
(map->Bus (merge {:state (atom {})
:results (atom {})
:output (queue/queue)
:counters {:sent (atom 0)
:received (atom 0)}}
m))))
link
(bus:create) => bus?
v 3.0
(defn bus:deregister
([bus]
(bus:deregister bus (bus:get-id bus)))
([{:keys [state] :as bus} id]
(swap! state (fn [m]
(let [{:keys [thread]} (get-in m [:threads id])]
(-> m
(update :threads dissoc id)
(update :lookup dissoc thread)))))))
link
(bus:with-temp bus (bus:register bus "foo" (Thread.)) (bus:deregister bus "foo") (bus:has-id? bus "foo")) => false
v 3.0
(defn bus:get-count
([{:keys [state] :as bus}]
(-> state deref :threads count)))
link
(bus:with-temp bus (bus:get-count bus)) => 1
v 3.0
(defn bus:get-id
([bus]
(bus:get-id bus (thread/thread:current)))
([{:keys [state]} thread]
(let [{:keys [lookup]} @state]
(get lookup thread))))
link
(bus:with-temp bus (bus:get-id bus)) => string?
bus:get-queue ^
[bus] [{:keys [state], :as bus} obj]
gets the message queue associated with the thread
v 3.0
(defn bus:get-queue
([bus]
(bus:get-queue bus (thread/thread:current)))
([{:keys [state] :as bus} obj]
(let [{:keys [threads lookup]} @state]
(:queue (if (instance? Thread obj)
(get threads (get lookup obj))
(get threads obj))))))
link
(bus:with-temp bus (vec (bus:get-queue bus))) => []
v 3.0
(defn bus:get-thread
([{:keys [state]} id]
(let [{:keys [threads]} @state]
(:thread (get threads id)))))
link
(bus:with-temp bus (->> (bus:get-id bus) (bus:get-thread bus))) => (cc/thread:current)
v 3.0
(defn bus:has-id?
([{:keys [state]} id]
(let [{:keys [threads]} @state]
(boolean (get threads id)))))
link
(bus:with-temp bus (bus:has-id? bus (bus:get-id bus))) => true
v 3.0
(defn bus:kill
([bus id]
(let [thread (bus:get-thread bus id)]
(if (and thread (not= thread (thread/thread:current)))
(doto thread
(thread/thread:interrupt))))))
link
(bus:with-temp bus (let [{:keys [id stopped]} @(bus:open bus (fn [m] (update m :value inc)))] (Thread/sleep 10) (bus:kill bus id) [(deref stopped 1000 :timeout) (bus:get-count bus)])) => (contains-in [{:exit :interrupted :id string? :unprocessed empty? :start number? :end number?} 1])
v 3.0
(defn bus:kill-all
([bus]
(collection/map-entries (fn [id]
[id (bus:kill bus id)])
(bus:all-ids bus))))
link
(bus:with-temp bus (let [_ @(bus:open bus (fn [m] (update m :value inc))) _ @(bus:open bus (fn [m] (update m :value inc))) _ @(bus:open bus (fn [m] (update m :value inc))) _ @(bus:open bus (fn [m] (update m :value inc)))] (Thread/sleep 10) (bus:kill-all bus) (Thread/sleep 10) (bus:get-count bus))) => 1
bus:open ^
[bus handler] [bus handler {:keys [id], :as opts, :or {id (std.lib.foundation/sid)}}]
bus:opens a new handler loop given function
v 3.0
(defn bus:open
([bus handler]
(bus:open bus handler {}))
([bus handler {:keys [id] :as opts
:or {id (std.lib.foundation/sid)}}]
(if-not (component/started? bus) (std.lib.foundation/error "Bus not started" {:bus bus}))
(let [started (f/incomplete)
opts (assoc opts :id id :started started :stopped (f/incomplete))
_ (f/future
(run-handler bus handler opts))]
started)))
link
(bus:with-temp bus (let [{:keys [id]} (deref (bus:open bus (fn [m] (update m :value inc))) 1000 :timeout)] (Thread/sleep 100) (deref (bus:send bus id {:value 1}) 1000 :timeout))) => (contains {:value 2, :id string?}) (bus:with-temp bus (let [{:keys [id stopped]} (deref (bus:open bus (fn [m] (update m :value inc)) {:timeout 400}) 1000 :timeout)] [(deref stopped 1000 :timeout) (bus:get-count bus)])) => (contains-in [{:exit :timeout :id string? :start number? :end number?} 1])
bus:register ^
[bus] [bus id] [{:keys [state], :as bus} id thread]
registers a thread to the bus
v 3.0
(defn bus:register
([bus]
(bus:register bus (std.lib.foundation/sid)))
([bus id]
(bus:register bus id (thread/thread:current)))
([{:keys [state] :as bus} id thread]
(let [queue (queue/queue)]
(swap! state (fn [m]
(-> m
(assoc-in [:threads id]
{:id id :thread thread :queue queue})
(assoc-in [:lookup thread] id))))
queue)))
link
(bus:with-temp bus (bus:register bus "foo" (Thread.)) (bus:has-id? bus "foo")) => true
v 4.0
(defn bus:reset-counters
[bus]
(collection/map-vals #(atom/swap-return! % (fn [v] [v 0]))
(:counters bus)))
link
(bus:with-temp bus (bus:send bus (bus:get-id bus) {:op :test}) (bus:reset-counters bus) @(:received (:counters bus))) => 0
bus:send ^
[{:keys [results counters], :as bus} id msg]
sends a message to the given thread
v 3.0
(defn bus:send
([{:keys [results counters] :as bus} id msg]
(if-let [queue (bus:get-queue bus id)]
(let [msg-id (or (:id msg) (std.lib.foundation/sid))
return (f/incomplete)]
(swap! results assoc msg-id return)
(queue/put queue (assoc msg :id msg-id))
(swap! (:sent counters) inc)
return))))
link
(bus:with-temp bus (bus:send bus (bus:get-id bus) {:op :hello :message "world"}) (cc/take (bus:get-queue bus) 1000 :ms)) => (contains {:op :hello, :message "world", :id string?})
v 3.0
(defn bus:send-all
([bus msg]
(collection/map-entries (fn [id] [id (bus:send bus id msg)]) (bus:all-ids bus))))
link
(bus:with-temp bus (bus:send-all bus {:op :all}) (bus:wait bus {:timeout 1000})) => (contains {:op :all})
bus:wait ^
[bus] [bus {:keys [timeout timeunit], :or {timeunit :ms}}]
bus:waits on the message queue for message
v 3.0
(defn bus:wait
([bus]
(bus:wait bus {}))
([bus {:keys [timeout timeunit]
:or {timeunit :ms}}]
(queue/take (bus:get-queue bus) timeout timeunit)))
link
(bus:with-temp bus (bus:send bus (bus:get-id bus) {:op :hello :message "world"}) (bus:wait bus {:timeout 1000})) => (contains {:op :hello, :message "world", :id string?}) (bus:with-temp bus (bus:wait bus {:timeout 100}))
v 3.0
(defmacro bus:with-temp
([var & body]
`(let [~var (bus)
~'_ (bus:register ~var)]
(try ~@body
(finally (bus:deregister ~var)
(stop-bus ~var))))))
link
(bus:with-temp bus (bus? bus)) => true
handler-thunk ^
[{:keys [output], :as bus} handler {:keys [stopped on-timeout], session-id :id, :as opts}]
creates a thread loop for given message handler
v 3.0
(defn handler-thunk
([{:keys [output] :as bus} handler {:keys [stopped on-timeout] session-id :id :as opts}]
(fn []
(try
(loop []
(if-let [{:keys [id op] :as msg} (bus:wait bus opts)]
(cond (not= op :exit)
(let [[status data exception]
(try
[:success (handler msg) nil]
(catch InterruptedException e (throw e))
(catch Throwable t [:error nil t]))]
(queue/put output {:id id :status status :data data :exception exception})
(recur))
:else
(do (queue/put output {:id id :status :success :data {:exit true :stopped stopped}})
{:exit :normal :id session-id :unprocessed (bus:get-queue bus session-id)}))
(let [return (and on-timeout (on-timeout))]
(if-not (true? return)
{:exit :timeout :id session-id}
(recur)))))
(catch InterruptedException e
{:exit :interrupted :id session-id :unprocessed (bus:get-queue bus session-id)})))))
link
(bus:with-temp bus (let [output (q/queue) bus (assoc bus :output output) handler (fn [msg] (assoc msg :handled true)) thunk (handler-thunk bus handler {:stopped (f/incomplete)}) id (bus:get-id bus)] (f/future (bus:register bus id (Thread/currentThread)) (thunk)) (Thread/sleep 10) (bus:send bus id {:op :test}) (q/take output 1000 :ms))) => (contains {:status :success :data (contains {:handled true})})
info-bus ^
[{:keys [state results output counters], :as bus}]
returns info about the bus
v 3.0
(defn info-bus
([{:keys [state results output counters] :as bus}]
{:running (started?-bus bus)
:threads (count (bus:all-ids bus))
:waiting (count @results)
:queued (collection/map-vals (comp count :queue) (:threads @state))
:sent @(:sent counters)
:received @(:received counters)}))
link
(bus:with-temp bus (loop [i 0] (if (started?-bus bus) (info-bus bus) (if (< i 10) (do (Thread/sleep 20) (recur (inc i))) (info-bus bus))))) => (contains {:running true})
v 3.0
(defn main-loop
([{:keys [state] :as bus}]
(let [thunk (main-thunk bus)]
(swap! state assoc :main (thread/thread:current))
(thunk))))
link
(let [state (atom {})] (with-redefs [main-thunk (fn [_] (fn [] :done))] [(main-loop {:state state}) (= (Thread/currentThread) (:main @state))])) => [:done true]
main-thunk ^
[{:keys [results output counters], :as bus}]
creates main message return handler
v 3.0
(defn main-thunk
([{:keys [results output counters] :as bus}]
(fn []
(try
(loop [{:keys [id status data exception] :as result} (queue/take output)]
(let [return (get @results id)
_ (swap! results dissoc id)
_ (if (= :success status)
(f/future:force return data)
(f/future:force return :exception exception))
_ (swap! (:received counters) inc)]
(recur (queue/take output))))
(catch InterruptedException e)))))
link
(bus:with-temp bus (let [thunk (main-thunk bus) output (:output bus) results (:results bus) id "test-id" ret (f/incomplete) t (Thread. thunk)] (swap! results assoc id ret) (q/put output {:id id :status :success :data "data"}) (.start t) (try (deref ret 1000 :timeout) (finally (.interrupt t) (.join t 100))))) => "data"
NONE
(impl/defimpl Bus [state results output counters]
:suffix "-bus"
:string string-bus
:protocols [std.protocol.component/IComponent
:include [-start -stop -started? -info]])
link
run-handler ^
[bus handler] [bus handler {:keys [id started stopped on-start on-stop], :as opts}]
runs the handler in a thread loop
v 3.0
(defn run-handler
([bus handler]
(run-handler bus handler {}))
([bus handler {:keys [id started stopped on-start on-stop] :as opts}]
(try
(let [thunk (handler-thunk bus handler opts)
id (or id (std.lib.foundation/sid))
start (time/time-ns)
thread (thread/thread:current)
_ (bus:register bus id thread)
_ (f/future:force started {:id id :thread thread :stopped stopped})
_ (if on-start (on-start))
output (thunk)
_ (if on-stop (on-stop))
_ (bus:deregister bus id)
end (time/time-ns)
output (assoc output :start start :end end)
_ (f/future:force stopped output)]
output)
(catch Throwable t
(.printStackTrace t)))))
link
(bus:with-temp bus (let [started (f/incomplete) stopped (f/incomplete) _ (f/future (run-handler bus (fn [m] (assoc m :done true)) {:started started :stopped stopped})) {id :id} (deref started 1000 :timeout)] (bus:send bus id {:op :test}) (bus:send bus id {:op :exit}) (deref stopped 1000 :timeout))) => (contains {:exit :normal})
v 3.0
(defn start-bus
([bus]
(if-not (started?-bus bus)
(thread/thread {:handler #(main-loop bus)
:daemon true
:start true}))
bus))
link
(let [bus (start-bus (bus:create))] (loop [i 0] (if (started?-bus bus) true (if (< i 10) (do (Thread/sleep 20) (recur (inc i))) false)))) => true
v 3.0
(defn started?-bus
([{:keys [state] :as bus}]
(boolean (when-let [main ^Thread (:main @state)]
(.isAlive main)))))
link
(bus:with-temp bus (Thread/sleep 10) (started?-bus bus)) => true
v 3.0
(defn stop-bus
([{:keys [state] :as bus}]
(if (started?-bus bus)
(let [main ^Thread (:main @state)
_ (swap! state dissoc :main)]
(thread/thread:interrupt main)
(bus:kill-all bus)))
bus))
link
(let [bus (doto (bus:create) (start-bus))] (loop [i 0] (if (started?-bus bus) (do (stop-bus bus) (not (started?-bus bus))) (if (< i 10) (do (Thread/sleep 20) (recur (inc i))) false)))) => true