mirror of
https://github.com/penpot/penpot.git
synced 2026-09-10 22:19:19 +00:00
* ✨ Add export job model, store and scheduler to exporter * ✨ Render wasm exports on pooled worker threads * ✨ Add export job REST API to exporter * ✨ Use export job API and allow cancelling wasm exports * 🔧 Show export jobs in the internal debug panel * 🔧 Pass flags and export job settings to the exporter container * 📚 Document the exporter job API and its redis layout
178 lines
6.4 KiB
Clojure
178 lines
6.4 KiB
Clojure
;; 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/.
|
|
;;
|
|
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
|
|
|
(ns app.redis
|
|
(:require
|
|
["ioredis" :as redis]
|
|
[app.common.data.macros :as dm]
|
|
[app.common.exceptions :as ex]
|
|
[app.common.logging :as l]
|
|
[app.common.transit :as t]
|
|
[app.config :as cf]
|
|
[promesa.core :as p]))
|
|
|
|
(l/set-level! :trace)
|
|
|
|
(def client (atom nil))
|
|
|
|
;; A connection in subscriber mode rejects every other command, so the
|
|
;; subscriptions need a connection of their own.
|
|
(def ^:private subscriber (atom nil))
|
|
|
|
(def ^:private subscriptions (atom {}))
|
|
|
|
(defn- create-client
|
|
[uri role]
|
|
(let [^js client (new redis/default uri)]
|
|
(.on client "connect"
|
|
(fn [] (l/info :hint "redis connection established" :uri uri :role role)))
|
|
(.on client "error"
|
|
(fn [cause] (l/error :hint "error on redis connection" :role role :cause cause)))
|
|
(.on client "close"
|
|
(fn [] (l/warn :hint "connection closed" :role role)))
|
|
(.on client "reconnect"
|
|
(fn [ms] (l/warn :hint "reconnecting to redis" :role role :ms ms)))
|
|
(.on client "end"
|
|
(fn [] (l/warn :hint "client ended, no more connections will be attempted" :role role)))
|
|
client))
|
|
|
|
(defn- dispatch-message
|
|
[topic payload]
|
|
(doseq [handler (get @subscriptions topic)]
|
|
(try
|
|
(handler payload)
|
|
(catch :default cause
|
|
(l/error :hint "error on redis subscription handler" :topic topic :cause cause)))))
|
|
|
|
(defn init
|
|
[]
|
|
(let [uri (cf/get :redis-uri)]
|
|
(swap! client (fn [prev]
|
|
(when prev (.disconnect ^js prev))
|
|
(create-client uri "commands")))
|
|
(swap! subscriber (fn [prev]
|
|
(when prev (.disconnect ^js prev))
|
|
(let [^js conn (create-client uri "subscriber")]
|
|
(.on conn "message" (fn [topic payload] (dispatch-message topic payload)))
|
|
;; Reinstate subscriptions after a reconnection.
|
|
(.on conn "connect"
|
|
(fn []
|
|
(doseq [topic (keys @subscriptions)]
|
|
(.subscribe conn topic))))
|
|
conn)))))
|
|
|
|
(defn stop
|
|
[]
|
|
(reset! subscriptions {})
|
|
(swap! subscriber (fn [conn]
|
|
(when conn (.quit ^js conn))
|
|
nil))
|
|
(swap! client (fn [client]
|
|
(when client (.quit ^js client))
|
|
nil)))
|
|
|
|
(def ^:private tenant (cf/get :tenant))
|
|
|
|
(defn ->tenant-key
|
|
"Namespaces `parts` under the tenant, the prefix the backend msgbus uses."
|
|
[& parts]
|
|
(dm/str tenant "." (apply str parts)))
|
|
|
|
(defn ->key
|
|
"Namespaces `parts` under the exporter, inside the tenant."
|
|
[& parts]
|
|
(dm/str "penpot.exporter." tenant "." (apply str parts)))
|
|
|
|
(defn pub!
|
|
"Publishes on `topic`, which must already be namespaced."
|
|
[topic payload]
|
|
(let [payload (if (map? payload) (t/encode-str payload) payload)]
|
|
(when-let [client @client]
|
|
(.publish ^js client topic payload))))
|
|
|
|
(defn sub!
|
|
"Subscribes `handler` (fn of the raw payload string) to `topic`, which must
|
|
already be namespaced. Returns a 0-arg fn that removes this handler."
|
|
[topic handler]
|
|
(swap! subscriptions update topic (fnil conj []) handler)
|
|
(when-let [conn @subscriber]
|
|
(.subscribe ^js conn topic))
|
|
(fn []
|
|
(swap! subscriptions update topic (fn [handlers] (vec (remove #(= % handler) handlers))))))
|
|
|
|
(defn- with-client
|
|
"Runs `f` against the command connection. Rejects when there is no connection
|
|
or the command fails: whether a failure is survivable depends on what the
|
|
caller was doing, and only the caller knows."
|
|
[f]
|
|
(if-let [client @client]
|
|
(p/do (f client))
|
|
(p/rejected (ex/error :type :internal
|
|
:code :redis-not-available
|
|
:hint "no redis connection"))))
|
|
|
|
(defn- with-client-lenient
|
|
"For reads, where an unreachable redis is reported as \"nothing there\"."
|
|
[f]
|
|
(->> (with-client f)
|
|
(p/merr (fn [cause]
|
|
(l/warn :hint "redis command failed" :cause cause)
|
|
(p/resolved nil)))))
|
|
|
|
(defn hset!
|
|
"Writes `data` (a map of string/keyword -> value) as a hash. Nil values are
|
|
dropped, since redis has no null."
|
|
[k data]
|
|
(let [obj (reduce-kv (fn [obj field value]
|
|
(if (some? value)
|
|
(doto obj (unchecked-set (name field) (str value)))
|
|
obj))
|
|
#js {}
|
|
data)]
|
|
(if (zero? (alength (js/Object.keys obj)))
|
|
(p/resolved nil)
|
|
(with-client (fn [^js client] (.hset client k obj))))))
|
|
|
|
(defn hgetall
|
|
"Returns the hash as a map of string keys, or nil when it does not exist."
|
|
[k]
|
|
(->> (with-client-lenient (fn [^js client] (.hgetall client k)))
|
|
(p/fmap (fn [result]
|
|
(when (and result (pos? (alength (js/Object.keys result))))
|
|
(persistent!
|
|
(reduce (fn [res field]
|
|
(assoc! res field (unchecked-get result field)))
|
|
(transient {})
|
|
(js/Object.keys result))))))))
|
|
|
|
(defn expire!
|
|
[k seconds]
|
|
(with-client (fn [^js client] (.expire client k seconds))))
|
|
|
|
(defn del!
|
|
[k]
|
|
(with-client (fn [^js client] (.del client k))))
|
|
|
|
(defn scan
|
|
"Every key matching `pattern`, walked in cursor batches so a large keyspace is
|
|
never blocked the way `KEYS` would block it.
|
|
|
|
Batches are accumulated in memory rather than consumed as a stream, which a
|
|
promise-returning fn cannot express. Fine for the job keyspace, but reading
|
|
redis wants a streaming or reactive interface before it is used for more."
|
|
[pattern]
|
|
(letfn [(step [cursor found]
|
|
(->> (with-client-lenient (fn [^js client] (.scan client cursor "MATCH" pattern "COUNT" 200)))
|
|
(p/mcat (fn [result]
|
|
(if (nil? result)
|
|
(p/resolved found)
|
|
(let [next-cursor (aget result 0)
|
|
found (into found (aget result 1))]
|
|
(if (= "0" next-cursor)
|
|
(p/resolved found)
|
|
(step next-cursor found))))))))]
|
|
(step "0" [])))
|