2021-02-12 16:01:59 +01:00
|
|
|
;; This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
|
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
;;
|
2022-09-20 23:23:22 +02:00
|
|
|
;; Copyright (c) KALEIDOS INC
|
2021-02-12 16:01:59 +01:00
|
|
|
|
|
|
|
(ns app.msgbus
|
|
|
|
"The msgbus abstraction implemented using redis as underlying backend."
|
|
|
|
(:require
|
2022-03-18 12:36:42 +01:00
|
|
|
[app.common.data :as d]
|
2021-09-29 16:39:25 +02:00
|
|
|
[app.common.logging :as l]
|
2024-10-29 09:08:25 +01:00
|
|
|
[app.common.schema :as sm]
|
2022-03-18 12:36:42 +01:00
|
|
|
[app.common.transit :as t]
|
2021-02-24 13:08:44 +01:00
|
|
|
[app.config :as cfg]
|
2023-02-20 12:44:35 +01:00
|
|
|
[app.redis :as rds]
|
2021-02-22 23:14:53 +01:00
|
|
|
[app.util.time :as dt]
|
2022-03-18 12:36:42 +01:00
|
|
|
[app.worker :as wrk]
|
2021-02-12 16:01:59 +01:00
|
|
|
[integrant.core :as ig]
|
2022-11-22 13:04:33 +01:00
|
|
|
[promesa.core :as p]
|
2023-02-20 12:44:35 +01:00
|
|
|
[promesa.exec :as px]
|
|
|
|
[promesa.exec.csp :as sp]))
|
2021-02-12 16:01:59 +01:00
|
|
|
|
2022-03-18 12:36:42 +01:00
|
|
|
(set! *warn-on-reflection* true)
|
2021-03-22 12:13:11 +01:00
|
|
|
(def ^:private prefix (cfg/get :tenant))
|
|
|
|
|
2024-10-29 09:08:25 +01:00
|
|
|
(defprotocol IMsgBus
|
|
|
|
(-sub [_ topics chan])
|
|
|
|
(-pub [_ topic message])
|
|
|
|
(-purge [_ chans]))
|
|
|
|
|
|
|
|
|
|
|
|
|
2021-03-22 12:13:11 +01:00
|
|
|
(defn- prefix-topic
|
|
|
|
[topic]
|
|
|
|
(str prefix "." topic))
|
|
|
|
|
2022-03-18 12:36:42 +01:00
|
|
|
(def ^:private xform-prefix-topic
|
|
|
|
(map (fn [obj] (update obj :topic prefix-topic))))
|
2021-03-07 20:30:41 +01:00
|
|
|
|
2024-10-29 09:08:25 +01:00
|
|
|
(declare ^:private redis-pub)
|
|
|
|
(declare ^:private redis-sub)
|
|
|
|
(declare ^:private redis-unsub)
|
|
|
|
(declare ^:private start-io-loop)
|
2022-09-05 19:00:20 +02:00
|
|
|
(declare ^:private subscribe-to-topics)
|
|
|
|
(declare ^:private unsubscribe-channels)
|
2021-02-12 16:01:59 +01:00
|
|
|
|
2024-10-29 09:08:25 +01:00
|
|
|
(defn msgbus?
|
|
|
|
[o]
|
|
|
|
(satisfies? IMsgBus o))
|
|
|
|
|
|
|
|
(sm/register!
|
|
|
|
{:type ::msgbus
|
|
|
|
:pred msgbus?})
|
|
|
|
|
|
|
|
(defmethod ig/expand-key ::msgbus
|
|
|
|
[k v]
|
|
|
|
{k (-> (d/without-nils v)
|
|
|
|
(assoc ::buffer-size 128)
|
|
|
|
(assoc ::timeout (dt/duration {:seconds 30})))})
|
2022-09-05 19:00:20 +02:00
|
|
|
|
2024-10-29 09:08:25 +01:00
|
|
|
(def ^:private schema:params
|
|
|
|
[:map ::rds/redis ::wrk/executor])
|
2023-02-20 12:44:35 +01:00
|
|
|
|
2024-10-29 09:08:25 +01:00
|
|
|
(defmethod ig/assert-key ::msgbus
|
|
|
|
[_ params]
|
|
|
|
(assert (sm/check schema:params params)))
|
2021-03-22 12:13:11 +01:00
|
|
|
|
2022-03-18 12:36:42 +01:00
|
|
|
(defmethod ig/init-key ::msgbus
|
2023-02-20 12:44:35 +01:00
|
|
|
[_ {:keys [::buffer-size ::wrk/executor ::timeout ::rds/redis] :as cfg}]
|
2022-08-30 14:26:54 +02:00
|
|
|
(l/info :hint "initialize msgbus" :buffer-size buffer-size)
|
2023-02-20 12:44:35 +01:00
|
|
|
(let [cmd-ch (sp/chan :buf buffer-size)
|
|
|
|
rcv-ch (sp/chan :buf (sp/dropping-buffer buffer-size))
|
|
|
|
pub-ch (sp/chan :buf (sp/dropping-buffer buffer-size)
|
|
|
|
:xf xform-prefix-topic)
|
2022-09-05 19:00:20 +02:00
|
|
|
state (agent {})
|
2023-02-20 12:44:35 +01:00
|
|
|
|
2024-10-29 09:08:25 +01:00
|
|
|
pconn (rds/connect redis :type :default :timeout timeout)
|
2023-02-20 12:44:35 +01:00
|
|
|
sconn (rds/connect redis :type :pubsub :timeout timeout)
|
2024-10-29 09:08:25 +01:00
|
|
|
|
|
|
|
_ (set-error-handler! state #(l/error :cause % :hint "unexpected error on agent" ::l/sync? true))
|
|
|
|
_ (set-error-mode! state :continue)
|
|
|
|
|
|
|
|
cfg (-> cfg
|
2023-02-20 12:44:35 +01:00
|
|
|
(assoc ::pconn pconn)
|
|
|
|
(assoc ::sconn sconn)
|
2022-03-18 12:36:42 +01:00
|
|
|
(assoc ::cmd-ch cmd-ch)
|
|
|
|
(assoc ::rcv-ch rcv-ch)
|
|
|
|
(assoc ::pub-ch pub-ch)
|
2024-10-29 09:08:25 +01:00
|
|
|
(assoc ::state state))
|
|
|
|
|
|
|
|
io-thr (start-io-loop cfg)]
|
|
|
|
|
|
|
|
(reify
|
|
|
|
java.lang.AutoCloseable
|
|
|
|
(close [_]
|
|
|
|
(px/interrupt! io-thr)
|
|
|
|
(sp/close! cmd-ch)
|
|
|
|
(sp/close! rcv-ch)
|
|
|
|
(sp/close! pub-ch)
|
|
|
|
(d/close! pconn)
|
|
|
|
(d/close! sconn))
|
|
|
|
|
|
|
|
IMsgBus
|
|
|
|
(-sub [_ topics chan]
|
|
|
|
(l/debug :hint "subscribe" :topics topics :chan (hash chan))
|
|
|
|
(send-via executor state subscribe-to-topics cfg topics chan))
|
2022-09-05 19:00:20 +02:00
|
|
|
|
2024-10-29 09:08:25 +01:00
|
|
|
(-pub [_ topic message]
|
|
|
|
(let [message (assoc message :topic topic)]
|
|
|
|
(sp/put! pub-ch {:topic topic :message message})))
|
2022-09-05 19:00:20 +02:00
|
|
|
|
2024-10-29 09:08:25 +01:00
|
|
|
(-purge [_ chans]
|
|
|
|
(l/debug :hint "purge" :chans (count chans))
|
|
|
|
(send-via executor state unsubscribe-channels cfg chans)))))
|
2023-02-20 12:44:35 +01:00
|
|
|
|
|
|
|
(defmethod ig/halt-key! ::msgbus
|
2024-10-29 09:08:25 +01:00
|
|
|
[_ instance]
|
|
|
|
(d/close! instance))
|
2022-09-05 19:00:20 +02:00
|
|
|
|
|
|
|
(defn sub!
|
2024-10-29 09:08:25 +01:00
|
|
|
[instance & {:keys [topic topics chan]}]
|
|
|
|
(assert (satisfies? IMsgBus instance) "expected valid msgbus instance")
|
2023-02-20 12:44:35 +01:00
|
|
|
(let [topics (into [] (map prefix-topic) (if topic [topic] topics))]
|
2024-10-29 09:08:25 +01:00
|
|
|
(-sub instance topics chan)
|
2023-02-20 12:44:35 +01:00
|
|
|
nil))
|
2022-03-18 12:36:42 +01:00
|
|
|
|
2022-09-05 19:00:20 +02:00
|
|
|
(defn pub!
|
2024-10-29 09:08:25 +01:00
|
|
|
[instance & {:keys [topic message]}]
|
|
|
|
(assert (satisfies? IMsgBus instance) "expected valid msgbus instance")
|
|
|
|
(-pub instance topic message))
|
2021-02-12 16:01:59 +01:00
|
|
|
|
2022-09-05 19:00:20 +02:00
|
|
|
(defn purge!
|
2024-10-29 09:08:25 +01:00
|
|
|
[instance chans]
|
|
|
|
(assert (satisfies? IMsgBus instance) "expected valid msgbus instance")
|
|
|
|
(assert (every? sp/chan? chans) "expected a seq of chans")
|
|
|
|
(-purge instance chans)
|
2023-02-20 12:44:35 +01:00
|
|
|
nil)
|
2021-03-22 12:13:11 +01:00
|
|
|
|
2022-03-18 12:36:42 +01:00
|
|
|
;; --- IMPL
|
2021-03-22 12:13:11 +01:00
|
|
|
|
2022-03-18 12:36:42 +01:00
|
|
|
(defn- conj-subscription
|
|
|
|
"A low level function that is responsible to create on-demand
|
|
|
|
subscriptions on redis. It reuses the same subscription if it is
|
2023-02-20 12:44:35 +01:00
|
|
|
already established."
|
2022-03-18 12:36:42 +01:00
|
|
|
[nsubs cfg topic chan]
|
|
|
|
(let [nsubs (if (nil? nsubs) #{chan} (conj nsubs chan))]
|
|
|
|
(when (= 1 (count nsubs))
|
2023-02-02 10:57:54 +01:00
|
|
|
(l/trace :hint "open subscription" :topic topic ::l/sync? true)
|
2024-10-29 09:08:25 +01:00
|
|
|
(redis-sub cfg topic))
|
2022-03-18 12:36:42 +01:00
|
|
|
nsubs))
|
|
|
|
|
|
|
|
(defn- disj-subscription
|
|
|
|
"A low level function responsible on removing subscriptions. The
|
:wrench: Fix typos in source code
Found via `codespell -q 3 -S *.po,./frontend/yarn.lock -L childs,clen,fpr,inflight,ody,ot,ro,te,trys,ue`
2022-10-02 14:00:19 -04:00
|
|
|
subscription is truly removed from redis once no single local
|
2023-02-20 12:44:35 +01:00
|
|
|
subscription is look for it."
|
2022-03-18 12:36:42 +01:00
|
|
|
[nsubs cfg topic chan]
|
|
|
|
(let [nsubs (disj nsubs chan)]
|
|
|
|
(when (empty? nsubs)
|
2023-02-02 10:57:54 +01:00
|
|
|
(l/trace :hint "close subscription" :topic topic ::l/sync? true)
|
2024-10-29 09:08:25 +01:00
|
|
|
(redis-unsub cfg topic))
|
2022-03-18 12:36:42 +01:00
|
|
|
nsubs))
|
|
|
|
|
|
|
|
(defn- subscribe-to-topics
|
2023-02-20 12:44:35 +01:00
|
|
|
"Function responsible to attach local subscription to the state."
|
|
|
|
[state cfg topics chan]
|
|
|
|
(let [state (update state :chans assoc chan topics)]
|
|
|
|
(reduce (fn [state topic]
|
|
|
|
(update-in state [:topics topic] conj-subscription cfg topic chan))
|
|
|
|
state
|
|
|
|
topics)))
|
|
|
|
|
|
|
|
(defn- unsubscribe-channel
|
:wrench: Fix typos in source code
Found via `codespell -q 3 -S *.po,./frontend/yarn.lock -L childs,clen,fpr,inflight,ody,ot,ro,te,trys,ue`
2022-10-02 14:00:19 -04:00
|
|
|
"Auxiliary function responsible on removing a single local
|
2022-03-18 12:36:42 +01:00
|
|
|
subscription from the state."
|
|
|
|
[state cfg chan]
|
|
|
|
(let [topics (get-in state [:chans chan])
|
|
|
|
state (update state :chans dissoc chan)]
|
|
|
|
(reduce (fn [state topic]
|
|
|
|
(update-in state [:topics topic] disj-subscription cfg topic chan))
|
|
|
|
state
|
|
|
|
topics)))
|
|
|
|
|
|
|
|
(defn- unsubscribe-channels
|
|
|
|
"Function responsible from detach from state a seq of channels,
|
|
|
|
useful when client disconnects or in-bulk unsubscribe
|
|
|
|
operations. Intended to be executed in agent."
|
2023-02-20 12:44:35 +01:00
|
|
|
[state cfg channels]
|
|
|
|
(reduce #(unsubscribe-channel %1 cfg %2) state channels))
|
2022-03-18 12:36:42 +01:00
|
|
|
|
|
|
|
(defn- create-listener
|
|
|
|
[rcv-ch]
|
2023-02-20 12:44:35 +01:00
|
|
|
(rds/pubsub-listener
|
2022-08-30 14:26:54 +02:00
|
|
|
:on-message (fn [_ topic message]
|
|
|
|
;; There are no back pressure, so we use a slidding
|
|
|
|
;; buffer for cases when the pubsub broker sends
|
|
|
|
;; more messages that we can process.
|
|
|
|
(let [val {:topic topic :message (t/decode message)}]
|
2023-02-20 12:44:35 +01:00
|
|
|
(when-not (sp/offer! rcv-ch val)
|
2022-08-30 14:26:54 +02:00
|
|
|
(l/warn :msg "dropping message on subscription loop"))))))
|
2022-03-18 12:36:42 +01:00
|
|
|
|
2024-10-29 09:08:25 +01:00
|
|
|
(defn- process-input
|
2023-02-20 12:44:35 +01:00
|
|
|
[{:keys [::state ::wrk/executor] :as cfg} topic message]
|
|
|
|
(let [chans (get-in @state [:topics topic])]
|
|
|
|
(when-let [closed (loop [chans (seq chans)
|
|
|
|
closed #{}]
|
|
|
|
(if-let [ch (first chans)]
|
|
|
|
(if (sp/put! ch message)
|
|
|
|
(recur (rest chans) closed)
|
|
|
|
(recur (rest chans) (conj closed ch)))
|
|
|
|
(seq closed)))]
|
|
|
|
(send-via executor state unsubscribe-channels cfg closed))))
|
|
|
|
|
|
|
|
|
2024-10-29 09:08:25 +01:00
|
|
|
(defn start-io-loop
|
2022-09-05 19:00:20 +02:00
|
|
|
[{:keys [::sconn ::rcv-ch ::pub-ch ::state ::wrk/executor] :as cfg}]
|
2024-10-29 09:08:25 +01:00
|
|
|
(rds/add-listener sconn (create-listener rcv-ch))
|
2023-02-20 12:44:35 +01:00
|
|
|
|
|
|
|
(px/thread
|
|
|
|
{:name "penpot/msgbus/io-loop"
|
|
|
|
:virtual true}
|
|
|
|
(try
|
2022-11-22 13:04:33 +01:00
|
|
|
(loop []
|
2023-02-20 12:44:35 +01:00
|
|
|
(let [timeout-ch (sp/timeout-chan 1000)
|
|
|
|
[val port] (sp/alts! [timeout-ch pub-ch rcv-ch])]
|
2022-11-22 13:04:33 +01:00
|
|
|
(cond
|
2023-02-20 12:44:35 +01:00
|
|
|
(identical? port timeout-ch)
|
|
|
|
(let [closed (->> (:chans @state)
|
|
|
|
(map key)
|
|
|
|
(filter sp/closed?))]
|
|
|
|
(when (seq closed)
|
|
|
|
(send-via executor state unsubscribe-channels cfg closed)
|
|
|
|
(l/debug :hint "proactively purge channels" :count (count closed)))
|
|
|
|
(recur))
|
|
|
|
|
2022-11-22 13:04:33 +01:00
|
|
|
(nil? val)
|
2023-02-20 12:44:35 +01:00
|
|
|
(throw (InterruptedException. "internally interrupted"))
|
|
|
|
|
|
|
|
(identical? port rcv-ch)
|
|
|
|
(let [{:keys [topic message]} val]
|
2024-10-29 09:08:25 +01:00
|
|
|
(process-input cfg topic message)
|
2022-11-22 13:04:33 +01:00
|
|
|
(recur))
|
|
|
|
|
2023-02-20 12:44:35 +01:00
|
|
|
(identical? port pub-ch)
|
|
|
|
(do
|
2024-10-29 09:08:25 +01:00
|
|
|
(redis-pub cfg val)
|
2023-02-20 12:44:35 +01:00
|
|
|
(recur)))))
|
|
|
|
|
|
|
|
(catch InterruptedException _
|
|
|
|
(l/trace :hint "io-loop thread interrumpted"))
|
|
|
|
|
|
|
|
(catch Throwable cause
|
|
|
|
(l/error :hint "unexpected exception on io-loop thread"
|
|
|
|
:cause cause))
|
|
|
|
(finally
|
|
|
|
(l/trace :hint "clearing io-loop state")
|
|
|
|
(when-let [chans (:chans @state)]
|
|
|
|
(run! sp/close! (keys chans)))
|
2022-03-18 12:36:42 +01:00
|
|
|
|
2023-02-20 12:44:35 +01:00
|
|
|
(l/debug :hint "io-loop thread terminated")))))
|
|
|
|
|
2024-10-29 09:08:25 +01:00
|
|
|
(defn- redis-pub
|
2022-03-18 12:36:42 +01:00
|
|
|
"Publish a message to the redis server. Asynchronous operation,
|
|
|
|
intended to be used in core.async go blocks."
|
|
|
|
[{:keys [::pconn] :as cfg} {:keys [topic message]}]
|
2023-02-20 12:44:35 +01:00
|
|
|
(try
|
2024-10-29 09:08:25 +01:00
|
|
|
(p/await! (rds/publish pconn topic (t/encode message)))
|
2023-02-20 12:44:35 +01:00
|
|
|
(catch InterruptedException cause
|
|
|
|
(throw cause))
|
|
|
|
(catch Throwable cause
|
|
|
|
(l/error :hint "unexpected error on publishing"
|
|
|
|
:message message
|
|
|
|
:cause cause))))
|
|
|
|
|
2024-10-29 09:08:25 +01:00
|
|
|
(defn- redis-sub
|
2022-03-18 12:36:42 +01:00
|
|
|
"Create redis subscription. Blocking operation, intended to be used
|
|
|
|
inside an agent."
|
|
|
|
[{:keys [::sconn] :as cfg} topic]
|
2023-02-20 12:44:35 +01:00
|
|
|
(try
|
2024-10-29 09:08:25 +01:00
|
|
|
(rds/subscribe sconn [topic])
|
2023-02-20 12:44:35 +01:00
|
|
|
(catch InterruptedException cause
|
|
|
|
(throw cause))
|
|
|
|
(catch Throwable cause
|
|
|
|
(l/trace :hint "exception on subscribing" :topic topic :cause cause))))
|
|
|
|
|
2024-10-29 09:08:25 +01:00
|
|
|
(defn- redis-unsub
|
2022-03-18 12:36:42 +01:00
|
|
|
"Removes redis subscription. Blocking operation, intended to be used
|
|
|
|
inside an agent."
|
|
|
|
[{:keys [::sconn] :as cfg} topic]
|
2023-02-20 12:44:35 +01:00
|
|
|
(try
|
2024-10-29 09:08:25 +01:00
|
|
|
(rds/unsubscribe sconn [topic])
|
2023-02-20 12:44:35 +01:00
|
|
|
(catch InterruptedException cause
|
|
|
|
(throw cause))
|
|
|
|
(catch Throwable cause
|
|
|
|
(l/trace :hint "exception on unsubscribing" :topic topic :cause cause))))
|
|
|
|
|