Merge pull request #3001 from penpot/niwinz-experiments-2

♻️ Refactor concurrency model (start using JDK19 virtual threads on RPC and WebSockets)
This commit is contained in:
Alejandro 2023-03-15 11:34:25 +01:00 committed by GitHub
commit afb09919ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
82 changed files with 2461 additions and 2500 deletions

View File

@ -2,6 +2,7 @@
{promesa.core/let clojure.core/let {promesa.core/let clojure.core/let
promesa.core/->> clojure.core/->> promesa.core/->> clojure.core/->>
promesa.core/-> clojure.core/-> promesa.core/-> clojure.core/->
promesa.exec.csp/go-loop clojure.core/loop
rumext.v2/defc clojure.core/defn rumext.v2/defc clojure.core/defn
rumext.v2/fnc clojure.core/fn rumext.v2/fnc clojure.core/fn
app.common.data/export clojure.core/def app.common.data/export clojure.core/def

View File

@ -1,4 +1,7 @@
{:deps {:mvn/repos
{"sonatype" {:url "https://oss.sonatype.org/content/repositories/snapshots/"}}
:deps
{penpot/common {:local/root "../common"} {penpot/common {:local/root "../common"}
org.clojure/clojure {:mvn/version "1.11.1"} org.clojure/clojure {:mvn/version "1.11.1"}
org.clojure/core.async {:mvn/version "1.6.673"} org.clojure/core.async {:mvn/version "1.6.673"}
@ -19,14 +22,16 @@
java-http-clj/java-http-clj {:mvn/version "0.4.3"} java-http-clj/java-http-clj {:mvn/version "0.4.3"}
funcool/yetti funcool/yetti
{:git/tag "v9.12" {:git/tag "v9.15"
:git/sha "51646d8" :git/sha "aa9b967"
:git/url "https://github.com/funcool/yetti.git" :git/url "https://github.com/funcool/yetti.git"
:exclusions [org.slf4j/slf4j-api]} :exclusions [org.slf4j/slf4j-api]}
com.github.seancorfield/next.jdbc {:mvn/version "1.3.847"} com.github.seancorfield/next.jdbc {:mvn/version "1.3.847"}
metosin/reitit-core {:mvn/version "0.5.18"} metosin/reitit-core {:mvn/version "0.5.18"}
org.postgresql/postgresql {:mvn/version "42.5.2"}
org.postgresql/postgresql {:mvn/version "42.6.0-SNAPSHOT"}
com.zaxxer/HikariCP {:mvn/version "5.0.1"} com.zaxxer/HikariCP {:mvn/version "5.0.1"}
io.whitfin/siphash {:mvn/version "2.0.0"} io.whitfin/siphash {:mvn/version "2.0.0"}
@ -34,7 +39,7 @@
buddy/buddy-hashers {:mvn/version "1.8.158"} buddy/buddy-hashers {:mvn/version "1.8.158"}
buddy/buddy-sign {:mvn/version "3.4.333"} buddy/buddy-sign {:mvn/version "3.4.333"}
com.github.ben-manes.caffeine/caffeine {:mvn/version "3.1.2"} com.github.ben-manes.caffeine/caffeine {:mvn/version "3.1.5"}
org.jsoup/jsoup {:mvn/version "1.15.3"} org.jsoup/jsoup {:mvn/version "1.15.3"}
org.im4java/im4java org.im4java/im4java

View File

@ -1,9 +1,14 @@
;; Example climit.edn file ;; Example climit.edn file
;; Required: concurrency ;; Required: permits
;; Optional: queue-size, ommited means Integer/MAX_VALUE ;; Optional: queue, ommited means Integer/MAX_VALUE
{:update-file {:concurrency 1 :queue-size 3} ;; Optional: timeout, ommited means no timeout
:auth {:concurrency 128} ;; Note: queue and timeout are excluding
:process-font {:concurrency 4 :queue-size 32} {:update-file-by-id {:permits 1 :queue 3}
:process-image {:concurrency 8 :queue-size 32} :update-file {:permits 20}
:push-audit-events
{:concurrency 1 :queue-size 3}} :derive-password {:permits 8}
:process-font {:permits 4 :queue 32}
:process-image {:permits 8 :queue 32}
:submit-audit-events-by-profile
{:permits 1 :queue 3}}

View File

@ -3,12 +3,12 @@
<Appenders> <Appenders>
<Console name="console" target="SYSTEM_OUT"> <Console name="console" target="SYSTEM_OUT">
<PatternLayout pattern="[%d{YYYY-MM-dd HH:mm:ss.SSS}] %level{length=1} %logger{36} - %msg%n" <PatternLayout pattern="[%d{YYYY-MM-dd HH:mm:ss.SSS}] %level{length=1} %logger{36} - %msg%n"
alwaysWriteExceptions="false" /> alwaysWriteExceptions="true" />
</Console> </Console>
<RollingFile name="main" fileName="logs/main.log" filePattern="logs/main-%i.log"> <RollingFile name="main" fileName="logs/main.log" filePattern="logs/main-%i.log">
<PatternLayout pattern="[%d{YYYY-MM-dd HH:mm:ss.SSS}] %level{length=1} %logger{36} - %msg%n" <PatternLayout pattern="[%d{YYYY-MM-dd HH:mm:ss.SSS}] %level{length=1} %logger{36} - %msg%n"
alwaysWriteExceptions="false" /> alwaysWriteExceptions="true" />
<Policies> <Policies>
<SizeBasedTriggeringPolicy size="50M"/> <SizeBasedTriggeringPolicy size="50M"/>
</Policies> </Policies>

View File

@ -3,8 +3,9 @@
{:default {:default
[[:default :window "200000/h"]] [[:default :window "200000/h"]]
#{:command/get-teams} ;; #{:command/get-teams}
[[:burst :bucket "5/1/5s"]] ;; [[:burst :bucket "5/5/5s"]]
#{:command/get-profile} ;; #{:command/get-profile}
[[:burst :bucket "60/60/1m"]]} ;; [[:burst :bucket "60/60/1m"]]
}

View File

@ -42,19 +42,40 @@ export PENPOT_ASSETS_STORAGE_BACKEND=assets-s3
export PENPOT_STORAGE_ASSETS_S3_ENDPOINT=http://minio:9000 export PENPOT_STORAGE_ASSETS_S3_ENDPOINT=http://minio:9000
export PENPOT_STORAGE_ASSETS_S3_BUCKET=penpot export PENPOT_STORAGE_ASSETS_S3_BUCKET=penpot
#-J-Djdk.virtualThreadScheduler.parallelism=16
export OPTIONS=" export OPTIONS="
-A:jmx-remote -A:dev \ -A:jmx-remote -A:dev \
-J-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \ -J-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \
-J-Djdk.attach.allowAttachSelf \ -J-Djdk.attach.allowAttachSelf \
-J-Dlog4j2.configurationFile=log4j2-devenv.xml \ -J-Dlog4j2.configurationFile=log4j2-devenv.xml \
-J-Xms50m \
-J-Xmx1024m \
-J-XX:+UseZGC \
-J-XX:-OmitStackTraceInFastThrow \ -J-XX:-OmitStackTraceInFastThrow \
-J-XX:+UnlockDiagnosticVMOptions \ -J-XX:+UnlockDiagnosticVMOptions \
-J-XX:+DebugNonSafepoints"; -J-XX:+DebugNonSafepoints \
-J-Djdk.tracePinnedThreads=full \
-J--enable-preview";
# Uncomment for use the ImageMagick v7.x # Setup HEAP
export OPTIONS="$OPTIONS -J-Xms50m -J-Xmx1024m"
# export OPTIONS="$OPTIONS -J-Xms1100m -J-Xmx1100m -J-XX:+AlwaysPreTouch"
# Increase virtual thread pool size
# export OPTIONS="$OPTIONS -J-Djdk.virtualThreadScheduler.parallelism=16"
# Disable C2 Compiler
# export OPTIONS="$OPTIONS -J-XX:TieredStopAtLevel=1"
# Disable all compilers
# export OPTIONS="$OPTIONS -J-Xint"
# Setup GC
export OPTIONS="$OPTIONS -J-XX:+UseG1GC"
# Setup GC
# export OPTIONS="$OPTIONS -J-XX:+UseZGC"
# Enable ImageMagick v7.x support
# export OPTIONS="-J-Dim4java.useV7=true $OPTIONS"; # export OPTIONS="-J-Dim4java.useV7=true $OPTIONS";
export OPTIONS_EVAL="nil" export OPTIONS_EVAL="nil"

View File

@ -6,15 +6,18 @@
(ns app.auth (ns app.auth
(:require (:require
[buddy.hashers :as hashers])) [buddy.hashers :as hashers]
[promesa.exec :as px]))
(def default-params
{:alg :argon2id
:memory (* 32768 2)
:iterations 5
:parallelism (px/get-available-processors)})
(defn derive-password (defn derive-password
[password] [password]
(hashers/derive password (hashers/derive password default-params))
{:alg :argon2id
:memory 16384
:iterations 20
:parallelism 2}))
(defn verify-password (defn verify-password
[attempt password] [attempt password]

View File

@ -17,7 +17,6 @@
[app.config :as cf] [app.config :as cf]
[app.db :as db] [app.db :as db]
[app.http.client :as http] [app.http.client :as http]
[app.http.middleware :as hmw]
[app.http.session :as session] [app.http.session :as session]
[app.loggers.audit :as audit] [app.loggers.audit :as audit]
[app.main :as-alias main] [app.main :as-alias main]
@ -25,14 +24,11 @@
[app.tokens :as tokens] [app.tokens :as tokens]
[app.util.json :as json] [app.util.json :as json]
[app.util.time :as dt] [app.util.time :as dt]
[app.worker :as wrk]
[clojure.set :as set] [clojure.set :as set]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[cuerdas.core :as str] [cuerdas.core :as str]
[integrant.core :as ig] [integrant.core :as ig]
[promesa.core :as p] [yetti.response :as-alias yrs]))
[promesa.exec :as px]
[yetti.response :as yrs]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS ;; HELPERS
@ -166,20 +162,22 @@
(defn- retrieve-github-email (defn- retrieve-github-email
[cfg tdata info] [cfg tdata info]
(or (some-> info :email p/resolved) (or (some-> info :email)
(->> (http/req! cfg (let [params {:uri "https://api.github.com/user/emails"
{:uri "https://api.github.com/user/emails" :headers {"Authorization" (dm/str (:type tdata) " " (:token tdata))}
:headers {"Authorization" (dm/str (:type tdata) " " (:token tdata))} :timeout 6000
:timeout 6000 :method :get}
:method :get})
(p/map (fn [{:keys [status body] :as response}] {:keys [status body]} (http/req! cfg params {:sync? true})]
(when-not (s/int-in-range? 200 300 status)
(ex/raise :type :internal (when-not (s/int-in-range? 200 300 status)
:code :unable-to-retrieve-github-emails (ex/raise :type :internal
:hint "unable to retrieve github emails" :code :unable-to-retrieve-github-emails
:http-status status :hint "unable to retrieve github emails"
:http-body body)) :http-status status
(->> response :body json/decode (filter :primary) first :email)))))) :http-body body))
(->> body json/decode (filter :primary) first :email))))
(defmethod ig/pre-init-spec ::providers/github [_] (defmethod ig/pre-init-spec ::providers/github [_]
(s/keys :req [::http/client])) (s/keys :req [::http/client]))
@ -290,80 +288,74 @@
:grant-type (:grant_type params) :grant-type (:grant_type params)
:redirect-uri (:redirect_uri params)) :redirect-uri (:redirect_uri params))
(->> (http/req! cfg req) (let [{:keys [status body]} (http/req! cfg req {:sync? true})]
(p/map (fn [{:keys [status body] :as res}] (l/trace :hint "access token response" :status status :body body)
(l/trace :hint "access token response" (if (= status 200)
:status status (let [data (json/decode body)]
:body body) {:token (get data :access_token)
(if (= status 200) :type (get data :token_type)})
(let [data (json/decode body)]
{:token (get data :access_token) (ex/raise :type :internal
:type (get data :token_type)}) :code :unable-to-retrieve-token
(ex/raise :type :internal :hint "unable to retrieve token"
:code :unable-to-retrieve-token :http-status status
:http-status status :http-body body)))))
:http-body body)))))))
(defn- retrieve-user-info (defn- retrieve-user-info
[{:keys [provider] :as cfg} tdata] [{:keys [provider] :as cfg} tdata]
(letfn [(retrieve [] (letfn [(get-email [info]
(l/trace :hint "request user info"
:uri (:user-uri provider)
:token (obfuscate-string (:token tdata))
:token-type (:type tdata))
(http/req! cfg
{:uri (:user-uri provider)
:headers {"Authorization" (str (:type tdata) " " (:token tdata))}
:timeout 6000
:method :get}))
(validate-response [response]
(l/trace :hint "user info response"
:status (:status response)
:body (:body response))
(when-not (s/int-in-range? 200 300 (:status response))
(ex/raise :type :internal
:code :unable-to-retrieve-user-info
:hint "unable to retrieve user info"
:http-status (:status response)
:http-body (:body response)))
response)
(get-email [info]
;; Allow providers hook into this for custom email ;; Allow providers hook into this for custom email
;; retrieval method. ;; retrieval method.
(if-let [get-email-fn (:get-email-fn provider)] (if-let [get-email-fn (:get-email-fn provider)]
(get-email-fn tdata info) (get-email-fn tdata info)
(let [attr-kw (cf/get :oidc-email-attr :email)] (let [attr-kw (cf/get :oidc-email-attr :email)]
(p/resolved (get info attr-kw))))) (get info attr-kw))))
(get-name [info] (get-name [info]
(let [attr-kw (cf/get :oidc-name-attr :name)] (let [attr-kw (cf/get :oidc-name-attr :name)]
(get info attr-kw))) (get info attr-kw)))
(process-response [response] (process-response [response]
(p/let [info (-> response :body json/decode) (let [info (-> response :body json/decode)
email (get-email info)] email (get-email info)]
{:backend (:name provider) {:backend (:name provider)
:email email :email email
:fullname (or (get-name info) email) :fullname (or (get-name info) email)
:props (->> (dissoc info :name :email) :props (->> (dissoc info :name :email)
(qualify-props provider))})) (qualify-props provider))}))]
(validate-info [info] (l/trace :hint "request user info"
(l/trace :hint "authentication info" :info info) :uri (:user-uri provider)
(when-not (s/valid? ::info info) :token (obfuscate-string (:token tdata))
(l/warn :hint "received incomplete profile info object (please set correct scopes)" :token-type (:type tdata))
:info (pr-str info))
(ex/raise :type :internal
:code :incomplete-user-info
:hint "inconmplete user info"
:info info))
info)]
(->> (retrieve) (let [request {:uri (:user-uri provider)
(p/fmap validate-response) :headers {"Authorization" (str (:type tdata) " " (:token tdata))}
(p/mcat process-response) :timeout 6000
(p/fmap validate-info)))) :method :get}
response (http/req! cfg request {:sync? true})]
(l/trace :hint "user info response"
:status (:status response)
:body (:body response))
(when-not (s/int-in-range? 200 300 (:status response))
(ex/raise :type :internal
:code :unable-to-retrieve-user-info
:hint "unable to retrieve user info"
:http-status (:status response)
:http-body (:body response)))
(let [info (process-response response)]
(l/trace :hint "authentication info" :info info)
(when-not (s/valid? ::info info)
(l/warn :hint "received incomplete profile info object (please set correct scopes)" :info info)
(ex/raise :type :internal
:code :incomplete-user-info
:hint "inconmplete user info"
:info info))
info))))
(s/def ::backend ::us/not-empty-string) (s/def ::backend ::us/not-empty-string)
(s/def ::email ::us/not-empty-string) (s/def ::email ::us/not-empty-string)
@ -377,61 +369,55 @@
(defn get-info (defn get-info
[{:keys [provider] :as cfg} {:keys [params] :as request}] [{:keys [provider] :as cfg} {:keys [params] :as request}]
(letfn [(validate-oidc [info] (when-let [error (get params :error)]
;; If the provider is OIDC, we can proceed to check (ex/raise :type :internal
;; roles if they are defined. :code :error-on-retrieving-code
(when (and (= "oidc" (:name provider)) :error-id error
(seq (:roles provider))) :error-desc (get params :error_description)))
(let [provider-roles (into #{} (:roles provider))
profile-roles (let [attr (cf/get :oidc-roles-attr :roles)
roles (get info attr)]
(cond
(string? roles) (into #{} (str/words roles))
(vector? roles) (into #{} roles)
:else #{}))]
;; check if profile has a configured set of roles (let [state (get params :state)
(when-not (set/subset? provider-roles profile-roles) code (get params :code)
(ex/raise :type :internal state (tokens/verify (::main/props cfg) {:token state :iss :oauth})
:code :unable-to-auth token (retrieve-access-token cfg code)
:hint "not enough permissions")))) info (retrieve-user-info cfg token)]
info)
(post-process [state info] ;; If the provider is OIDC, we can proceed to check
(cond-> info ;; roles if they are defined.
(some? (:invitation-token state)) (when (and (= "oidc" (:name provider))
(assoc :invitation-token (:invitation-token state)) (seq (:roles provider)))
(let [provider-roles (into #{} (:roles provider))
profile-roles (let [attr (cf/get :oidc-roles-attr :roles)
roles (get info attr)]
(cond
(string? roles) (into #{} (str/words roles))
(vector? roles) (into #{} roles)
:else #{}))]
;; If state token comes with props, merge them. The state token ;; check if profile has a configured set of roles
;; props can contain pm_ and utm_ prefixed query params. (when-not (set/subset? provider-roles profile-roles)
(map? (:props state)) (ex/raise :type :internal
(update :props merge (:props state))))] :code :unable-to-auth
:hint "not enough permissions"))))
(when-let [error (get params :error)] (cond-> info
(ex/raise :type :internal (some? (:invitation-token state))
:code :error-on-retrieving-code (assoc :invitation-token (:invitation-token state))
:error-id error
:error-desc (get params :error_description)))
(let [state (get params :state) ;; If state token comes with props, merge them. The state token
code (get params :code) ;; props can contain pm_ and utm_ prefixed query params.
state (tokens/verify (::main/props cfg) {:token state :iss :oauth})] (map? (:props state))
(-> (p/resolved code) (update :props merge (:props state)))))
(p/then #(retrieve-access-token cfg %))
(p/then #(retrieve-user-info cfg %))
(p/then' validate-oidc)
(p/then' (partial post-process state))))))
(defn- get-profile (defn- get-profile
[{:keys [::db/pool ::wrk/executor] :as cfg} info] [{:keys [::db/pool] :as cfg} info]
(px/with-dispatch executor (dm/with-open [conn (db/open pool)]
(with-open [conn (db/open pool)] (some->> (:email info)
(some->> (:email info) (profile/get-profile-by-email conn))))
(profile/get-profile-by-email conn)))))
(defn- redirect-response (defn- redirect-response
[uri] [uri]
(yrs/response :status 302 :headers {"location" (str uri)})) {::yrs/status 302
::yrs/headers {"location" (str uri)}})
(defn- generate-error-redirect (defn- generate-error-redirect
[_ error] [_ error]
@ -458,11 +444,11 @@
(ex/raise :type :restriction (ex/raise :type :restriction
:code :profile-blocked)) :code :profile-blocked))
(audit/submit! cfg {:type "command" (audit/submit! cfg {::audit/type "command"
:name "login-with-password" ::audit/name "login-with-oidc"
:profile-id (:id profile) ::audit/profile-id (:id profile)
:ip-addr (audit/parse-client-ip request) ::audit/ip-addr (audit/parse-client-ip request)
:props (audit/profile->props profile)}) ::audit/props (audit/profile->props profile)})
(->> (redirect-response uri) (->> (redirect-response uri)
(sxf request))) (sxf request)))
@ -478,6 +464,7 @@
uri (-> (u/uri (cf/get :public-uri)) uri (-> (u/uri (cf/get :public-uri))
(assoc :path "/#/auth/register/validate") (assoc :path "/#/auth/register/validate")
(assoc :query (u/map->query-string params)))] (assoc :query (u/map->query-string params)))]
(redirect-response uri)))) (redirect-response uri))))
(defn- auth-handler (defn- auth-handler
@ -489,27 +476,24 @@
:props props :props props
:exp (dt/in-future "4h")}) :exp (dt/in-future "4h")})
uri (build-auth-uri cfg state)] uri (build-auth-uri cfg state)]
(yrs/response 200 {:redirect-uri uri}))) {::yrs/status 200
::yrs/body {:redirect-uri uri}}))
(defn- callback-handler (defn- callback-handler
[cfg request] [cfg request]
(letfn [(process-request [] (try
(p/let [info (get-info cfg request) (let [info (get-info cfg request)
profile (get-profile cfg info)] profile (get-profile cfg info)]
(generate-redirect cfg request info profile))) (generate-redirect cfg request info profile))
(catch Throwable cause
(handle-error [cause] (l/error :hint "error on oauth process" :cause cause)
(l/error :hint "error on oauth process" :cause cause) (generate-error-redirect cfg cause))))
(generate-error-redirect cfg cause))]
(-> (process-request)
(p/catch handle-error))))
(def provider-lookup (def provider-lookup
{:compile {:compile
(fn [& _] (fn [& _]
(fn [handler] (fn [handler {:keys [::providers] :as cfg}]
(fn [{:keys [::providers] :as cfg} request] (fn [request]
(let [provider (some-> request :path-params :provider keyword)] (let [provider (some-> request :path-params :provider keyword)]
(if-let [provider (get providers provider)] (if-let [provider (get providers provider)]
(handler (assoc cfg :provider provider) request) (handler (assoc cfg :provider provider) request)
@ -553,18 +537,15 @@
[_] [_]
(s/keys :req [::session/manager (s/keys :req [::session/manager
::http/client ::http/client
::wrk/executor
::main/props ::main/props
::db/pool ::db/pool
::providers])) ::providers]))
(defmethod ig/init-key ::routes (defmethod ig/init-key ::routes
[_ {:keys [::wrk/executor] :as cfg}] [_ cfg]
(let [cfg (update cfg :provider d/without-nils)] (let [cfg (update cfg :provider d/without-nils)]
["" {:middleware [[session/authz cfg] ["" {:middleware [[session/authz cfg]
[hmw/with-dispatch executor] [provider-lookup cfg]]}
[hmw/with-config cfg]
[provider-lookup]]}
["/auth/oauth" ["/auth/oauth"
["/:provider" ["/:provider"
{:handler auth-handler {:handler auth-handler

View File

@ -37,6 +37,7 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- parse-address (defn- parse-address
^"[Ljakarta.mail.internet.InternetAddress;"
[v] [v]
(InternetAddress/parse ^String v)) (InternetAddress/parse ^String v))
@ -149,6 +150,7 @@
"mail.smtp.connectiontimeout" timeout})) "mail.smtp.connectiontimeout" timeout}))
(defn- create-smtp-session (defn- create-smtp-session
^Session
[cfg] [cfg]
(let [props (opts->props cfg)] (let [props (opts->props cfg)]
(Session/getInstance props))) (Session/getInstance props)))

View File

@ -19,19 +19,21 @@
[app.http.middleware :as mw] [app.http.middleware :as mw]
[app.http.session :as session] [app.http.session :as session]
[app.http.websocket :as-alias ws] [app.http.websocket :as-alias ws]
[app.main :as-alias main]
[app.metrics :as mtx] [app.metrics :as mtx]
[app.rpc :as-alias rpc] [app.rpc :as-alias rpc]
[app.rpc.doc :as-alias rpc.doc] [app.rpc.doc :as-alias rpc.doc]
[app.worker :as wrk] [app.worker :as wrk]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[integrant.core :as ig] [integrant.core :as ig]
[promesa.exec :as px]
[reitit.core :as r] [reitit.core :as r]
[reitit.middleware :as rr] [reitit.middleware :as rr]
[yetti.adapter :as yt] [yetti.adapter :as yt]
[yetti.request :as yrq] [yetti.request :as yrq]
[yetti.response :as yrs])) [yetti.response :as-alias yrs]))
(declare wrap-router) (declare router-handler)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HTTP SERVER ;; HTTP SERVER
@ -71,13 +73,16 @@
:http/host host :http/host host
:http/max-body-size (::max-body-size cfg) :http/max-body-size (::max-body-size cfg)
:http/max-multipart-body-size (::max-multipart-body-size cfg) :http/max-multipart-body-size (::max-multipart-body-size cfg)
:xnio/io-threads (::io-threads cfg) :xnio/io-threads (or (::io-threads cfg)
:xnio/dispatch (::wrk/executor cfg) (max 3 (px/get-available-processors)))
:xnio/worker-threads (or (::worker-threads cfg)
(max 6 (px/get-available-processors)))
:xnio/dispatch true
:ring/async true} :ring/async true}
handler (cond handler (cond
(some? router) (some? router)
(wrap-router router) (router-handler router)
(some? handler) (some? handler)
handler handler
@ -97,32 +102,35 @@
(defn- not-found-handler (defn- not-found-handler
[_ respond _] [_ respond _]
(respond (yrs/response 404))) (respond {::yrs/status 404}))
(defn- wrap-router (defn- router-handler
[router] [router]
(letfn [(handler [request respond raise] (letfn [(resolve-handler [request]
(if-let [match (r/match-by-path router (yrq/path request))] (if-let [match (r/match-by-path router (yrq/path request))]
(let [params (:path-params match) (let [params (:path-params match)
result (:result match) result (:result match)
handler (or (:handler result) not-found-handler) handler (or (:handler result) not-found-handler)
request (assoc request :path-params params)] request (assoc request :path-params params)]
(handler request respond raise)) (partial handler request))
(not-found-handler request respond raise))) (partial not-found-handler request)))
(on-error [cause request respond] (on-error [cause request]
(let [{:keys [body] :as response} (errors/handle cause request)] (let [{:keys [body] :as response} (errors/handle cause request)]
(respond (cond-> response
(cond-> response (map? body)
(map? body) (-> (update ::yrs/headers assoc "content-type" "application/transit+json")
(-> (update :headers assoc "content-type" "application/transit+json") (assoc ::yrs/body (t/encode-str body {:type :json-verbose}))))))]
(assoc :body (t/encode-str body {:type :json-verbose})))))))]
(fn [request respond _] (fn [request respond _]
(try (let [handler (resolve-handler request)
(handler request respond #(on-error % request respond)) exchange (yrq/exchange request)]
(catch Throwable cause (handler
(on-error cause request respond)))))) (fn [response]
(yt/dispatch! exchange (partial respond response)))
(fn [cause]
(let [response (on-error cause request)]
(yt/dispatch! exchange (partial respond response)))))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HTTP ROUTER ;; HTTP ROUTER
@ -130,11 +138,11 @@
(defmethod ig/pre-init-spec ::router [_] (defmethod ig/pre-init-spec ::router [_]
(s/keys :req [::session/manager (s/keys :req [::session/manager
::actoken/manager
::ws/routes ::ws/routes
::rpc/routes ::rpc/routes
::rpc.doc/routes ::rpc.doc/routes
::oidc/routes ::oidc/routes
::main/props
::assets/routes ::assets/routes
::debug/routes ::debug/routes
::db/pool ::db/pool
@ -151,7 +159,8 @@
[session/soft-auth cfg] [session/soft-auth cfg]
[actoken/soft-auth cfg] [actoken/soft-auth cfg]
[mw/errors errors/handle] [mw/errors errors/handle]
[mw/restrict-methods]]} [mw/restrict-methods]
[mw/with-dispatch :vthread]]}
(::mtx/routes cfg) (::mtx/routes cfg)
(::assets/routes cfg) (::assets/routes cfg)

View File

@ -7,26 +7,12 @@
(ns app.http.access-token (ns app.http.access-token
(:require (:require
[app.common.logging :as l] [app.common.logging :as l]
[app.common.spec :as us]
[app.config :as cf] [app.config :as cf]
[app.db :as db] [app.db :as db]
[app.main :as-alias main] [app.main :as-alias main]
[app.tokens :as tokens] [app.tokens :as tokens]
[app.worker :as-alias wrk]
[clojure.spec.alpha :as s]
[integrant.core :as ig]
[promesa.core :as p]
[promesa.exec :as px]
[yetti.request :as yrq])) [yetti.request :as yrq]))
(s/def ::manager
(s/keys :req [::db/pool ::wrk/executor ::main/props]))
(defmethod ig/pre-init-spec ::manager [_] ::manager)
(defmethod ig/init-key ::manager [_ cfg] cfg)
(defmethod ig/halt-key! ::manager [_ _])
(def header-re #"^Token\s+(.*)") (def header-re #"^Token\s+(.*)")
(defn- get-token (defn- get-token
@ -48,40 +34,32 @@
(db/decode-pgarray #{}))))) (db/decode-pgarray #{})))))
(defn- wrap-soft-auth (defn- wrap-soft-auth
[handler {:keys [::manager]}] "Soft Authentication, will be executed synchronously on the undertow
(us/assert! ::manager manager) worker thread."
[handler {:keys [::main/props]}]
(letfn [(handle-request [request]
(try
(let [token (get-token request)
claims (decode-token props token)]
(cond-> request
(map? claims)
(assoc ::id (:tid claims))))
(catch Throwable cause
(l/trace :hint "exception on decoding malformed token" :cause cause)
request)))]
(let [{:keys [::wrk/executor ::main/props]} manager]
(fn [request respond raise] (fn [request respond raise]
(let [token (get-token request)] (let [request (handle-request request)]
(->> (px/submit! executor (partial decode-token props token)) (handler request respond raise)))))
(p/fnly (fn [claims cause]
(when cause
(l/trace :hint "exception on decoding malformed token" :cause cause))
(let [request (cond-> request
(map? claims)
(assoc ::id (:tid claims)))]
(handler request respond raise)))))))))
(defn- wrap-authz (defn- wrap-authz
[handler {:keys [::manager]}] "Authorization middleware, will be executed synchronously on vthread."
(us/assert! ::manager manager) [handler {:keys [::db/pool]}]
(let [{:keys [::wrk/executor ::db/pool]} manager] (fn [request]
(fn [request respond raise] (let [perms (some->> (::id request) (get-token-perms pool))]
(if-let [token-id (::id request)] (handler (cond-> request
(->> (px/submit! executor (partial get-token-perms pool token-id)) (some? perms)
(p/fnly (fn [perms cause] (assoc ::perms perms))))))
(cond
(some? cause)
(raise cause)
(nil? perms)
(handler request respond raise)
:else
(let [request (assoc request ::perms perms)]
(handler request respond raise))))))
(handler request respond raise)))))
(def soft-auth (def soft-auth
{:name ::soft-auth {:name ::soft-auth

View File

@ -14,11 +14,9 @@
[app.db :as db] [app.db :as db]
[app.storage :as sto] [app.storage :as sto]
[app.util.time :as dt] [app.util.time :as dt]
[app.worker :as wrk]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[integrant.core :as ig] [integrant.core :as ig]
[promesa.core :as p] [yetti.response :as-alias yrs]))
[yetti.response :as yrs]))
(def ^:private cache-max-age (def ^:private cache-max-age
(dt/duration {:hours 24})) (dt/duration {:hours 24}))
@ -28,10 +26,9 @@
(defn get-id (defn get-id
[{:keys [path-params]}] [{:keys [path-params]}]
(if-let [id (some-> path-params :id d/parse-uuid)] (or (some-> path-params :id d/parse-uuid)
(p/resolved id) (ex/raise :type :not-found
(p/rejected (ex/error :type :not-found :hunt "object not found")))
:hunt "object not found"))))
(defn- get-file-media-object (defn- get-file-media-object
[pool id] [pool id]
@ -39,16 +36,12 @@
(defn- serve-object-from-s3 (defn- serve-object-from-s3
[{:keys [::sto/storage] :as cfg} obj] [{:keys [::sto/storage] :as cfg} obj]
(let [mdata (meta obj)] (let [{:keys [host port] :as url} (sto/get-object-url storage obj {:max-age signature-max-age})]
(->> (sto/get-object-url storage obj {:max-age signature-max-age}) {::yrs/status 307
(p/fmap (fn [{:keys [host port] :as url}] ::yrs/headers {"location" (str url)
(let [headers {"location" (str url) "x-host" (cond-> host port (str ":" port))
"x-host" (cond-> host port (str ":" port)) "x-mtype" (-> obj meta :content-type)
"x-mtype" (:content-type mdata) "cache-control" (str "max-age=" (inst-ms cache-max-age))}}))
"cache-control" (str "max-age=" (inst-ms cache-max-age))}]
(yrs/response
:status 307
:headers headers)))))))
(defn- serve-object-from-fs (defn- serve-object-from-fs
[{:keys [::path]} obj] [{:keys [::path]} obj]
@ -58,8 +51,8 @@
headers {"x-accel-redirect" (:path purl) headers {"x-accel-redirect" (:path purl)
"content-type" (:content-type mdata) "content-type" (:content-type mdata)
"cache-control" (str "max-age=" (inst-ms cache-max-age))}] "cache-control" (str "max-age=" (inst-ms cache-max-age))}]
(p/resolved {::yrs/status 204
(yrs/response :status 204 :headers headers)))) ::yrs/headers headers}))
(defn- serve-object (defn- serve-object
"Helper function that returns the appropriate response depending on "Helper function that returns the appropriate response depending on
@ -72,42 +65,34 @@
(defn objects-handler (defn objects-handler
"Handler that servers storage objects by id." "Handler that servers storage objects by id."
[{:keys [::sto/storage ::wrk/executor] :as cfg} request respond raise] [{:keys [::sto/storage] :as cfg} request]
(->> (get-id request) (let [id (get-id request)
(p/mcat executor (fn [id] (sto/get-object storage id))) obj (sto/get-object storage id)]
(p/mcat executor (fn [obj] (if obj
(if (some? obj) (serve-object cfg obj)
(serve-object cfg obj) {::yrs/status 404})))
(p/resolved (yrs/response 404)))))
(p/fnly executor (fn [result cause]
(if cause (raise cause) (respond result))))))
(defn- generic-handler (defn- generic-handler
"A generic handler helper/common code for file-media based handlers." "A generic handler helper/common code for file-media based handlers."
[{:keys [::sto/storage ::wrk/executor] :as cfg} request kf] [{:keys [::sto/storage] :as cfg} request kf]
(let [pool (::db/pool storage)] (let [pool (::db/pool storage)
(->> (get-id request) id (get-id request)
(p/fmap executor (fn [id] (get-file-media-object pool id))) mobj (get-file-media-object pool id)
(p/mcat executor (fn [mobj] (sto/get-object storage (kf mobj)))) sobj (sto/get-object storage (kf mobj))]
(p/mcat executor (fn [sobj] (if sobj
(if sobj (serve-object cfg sobj)
(serve-object cfg sobj) {::yrs/status 404})))
(p/resolved (yrs/response 404))))))))
(defn file-objects-handler (defn file-objects-handler
"Handler that serves storage objects by file media id." "Handler that serves storage objects by file media id."
[cfg request respond raise] [cfg request]
(->> (generic-handler cfg request :media-id) (generic-handler cfg request :media-id))
(p/fnly (fn [result cause]
(if cause (raise cause) (respond result))))))
(defn file-thumbnails-handler (defn file-thumbnails-handler
"Handler that serves storage objects by thumbnail-id and quick "Handler that serves storage objects by thumbnail-id and quick
fallback to file-media-id if no thumbnail is available." fallback to file-media-id if no thumbnail is available."
[cfg request respond raise] [cfg request]
(->> (generic-handler cfg request #(or (:thumbnail-id %) (:media-id %))) (generic-handler cfg request #(or (:thumbnail-id %) (:media-id %))))
(p/fnly (fn [result cause]
(if cause (raise cause) (respond result))))))
;; --- Initialization ;; --- Initialization
@ -115,7 +100,7 @@
(s/def ::routes vector?) (s/def ::routes vector?)
(defmethod ig/pre-init-spec ::routes [_] (defmethod ig/pre-init-spec ::routes [_]
(s/keys :req [::sto/storage ::wrk/executor ::path])) (s/keys :req [::sto/storage ::path]))
(defmethod ig/init-key ::routes (defmethod ig/init-key ::routes
[_ cfg] [_ cfg]

View File

@ -21,7 +21,7 @@
[jsonista.core :as j] [jsonista.core :as j]
[promesa.exec :as px] [promesa.exec :as px]
[yetti.request :as yrq] [yetti.request :as yrq]
[yetti.response :as yrs])) [yetti.response :as-alias yrs]))
(declare parse-json) (declare parse-json)
(declare handle-request) (declare handle-request)
@ -39,7 +39,7 @@
(letfn [(handler [request respond _] (letfn [(handler [request respond _]
(let [data (-> request yrq/body slurp)] (let [data (-> request yrq/body slurp)]
(px/run! executor #(handle-request cfg data))) (px/run! executor #(handle-request cfg data)))
(respond (yrs/response 200)))] (respond {::yrs/status 200}))]
["/sns" {:handler handler ["/sns" {:handler handler
:allowed-methods #{:post}}])) :allowed-methods #{:post}}]))

View File

@ -40,12 +40,25 @@
(catch Throwable cause (catch Throwable cause
(p/rejected cause)))))) (p/rejected cause))))))
(defn- resolve-client
[params]
(cond
(instance? HttpClient params)
params
(map? params)
(resolve-client (::client params))
:else
(throw (UnsupportedOperationException. "invalid arguments"))))
(defn req! (defn req!
"A convencience toplevel function for gradual migration to a new API "A convencience toplevel function for gradual migration to a new API
convention." convention."
([{:keys [::client]} request] ([cfg-or-client request]
(us/assert! ::client client) (let [client (resolve-client cfg-or-client)]
(send! client request {})) (send! client request {})))
([{:keys [::client]} request options] ([cfg-or-client request options]
(us/assert! ::client client) (let [client (resolve-client cfg-or-client)]
(send! client request options))) (send! client request options))))

View File

@ -13,7 +13,6 @@
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.config :as cf] [app.config :as cf]
[app.db :as db] [app.db :as db]
[app.http.middleware :as mw]
[app.http.session :as session] [app.http.session :as session]
[app.rpc.commands.binfile :as binf] [app.rpc.commands.binfile :as binf]
[app.rpc.commands.files-create :refer [create-file]] [app.rpc.commands.files-create :refer [create-file]]
@ -21,7 +20,6 @@
[app.util.blob :as blob] [app.util.blob :as blob]
[app.util.template :as tmpl] [app.util.template :as tmpl]
[app.util.time :as dt] [app.util.time :as dt]
[app.worker :as wrk]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[cuerdas.core :as str] [cuerdas.core :as str]
[datoteka.io :as io] [datoteka.io :as io]
@ -48,13 +46,17 @@
(defn prepare-response (defn prepare-response
[body] [body]
(let [headers {"content-type" "application/transit+json"}] (let [headers {"content-type" "application/transit+json"}]
(yrs/response :status 200 :body body :headers headers))) {::yrs/status 200
::yrs/body body
::yrs/headers headers}))
(defn prepare-download-response (defn prepare-download-response
[body filename] [body filename]
(let [headers {"content-disposition" (str "attachment; filename=" filename) (let [headers {"content-disposition" (str "attachment; filename=" filename)
"content-type" "application/octet-stream"}] "content-type" "application/octet-stream"}]
(yrs/response :status 200 :body body :headers headers))) {::yrs/status 200
::yrs/body body
::yrs/headers headers}))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; INDEX ;; INDEX
@ -65,10 +67,10 @@
(when-not (authorized? pool request) (when-not (authorized? pool request)
(ex/raise :type :authentication (ex/raise :type :authentication
:code :only-admins-allowed)) :code :only-admins-allowed))
(yrs/response :status 200 {::yrs/status 200
:headers {"content-type" "text/html"} ::yrs/headers {"content-type" "text/html"}
:body (-> (io/resource "app/templates/debug.tmpl") ::yrs/body (-> (io/resource "app/templates/debug.tmpl")
(tmpl/render {})))) (tmpl/render {}))})
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; FILE CHANGES ;; FILE CHANGES
@ -115,7 +117,8 @@
:project-id project-id :project-id project-id
:profile-id profile-id :profile-id profile-id
:data data}) :data data})
(yrs/response 201 "OK CREATED")) {::yrs/status 201
::yrs/body "OK CREATED"})
:else :else
(prepare-response (blob/decode data)))))) (prepare-response (blob/decode data))))))
@ -143,7 +146,8 @@
(db/update! pool :file (db/update! pool :file
{:data (blob/encode data)} {:data (blob/encode data)}
{:id file-id}) {:id file-id})
(yrs/response 200 "OK UPDATED")) {::yrs/status 200
::yrs/body "OK UPDATED"})
(do (do
(create-file pool {:id file-id (create-file pool {:id file-id
@ -151,9 +155,11 @@
:project-id project-id :project-id project-id
:profile-id profile-id :profile-id profile-id
:data data}) :data data})
(yrs/response 201 "OK CREATED")))) {::yrs/status 201
::yrs/body "OK CREATED"})))
(yrs/response 500 "ERROR")))) {::yrs/status 500
::yrs/body "ERROR"})))
(defn file-data-handler (defn file-data-handler
[cfg request] [cfg request]
@ -241,11 +247,12 @@
(let [result (if (= 1 (:version report)) (let [result (if (= 1 (:version report))
(render-template-v1 report) (render-template-v1 report)
(render-template-v2 report))] (render-template-v2 report))]
(yrs/response :status 200 {::yrs/status 200
:body result ::yrs/body result
:headers {"content-type" "text/html; charset=utf-8" ::yrs/headers {"content-type" "text/html; charset=utf-8"
"x-robots-tag" "noindex"})) "x-robots-tag" "noindex"}})
(yrs/response 404 "not found")))) {::yrs/status 404
::yrs/body "not found"})))
(def sql:error-reports (def sql:error-reports
"SELECT id, created_at, "SELECT id, created_at,
@ -261,11 +268,11 @@
:code :only-admins-allowed)) :code :only-admins-allowed))
(let [items (->> (db/exec! pool [sql:error-reports]) (let [items (->> (db/exec! pool [sql:error-reports])
(map #(update % :created-at dt/format-instant :rfc1123)))] (map #(update % :created-at dt/format-instant :rfc1123)))]
(yrs/response :status 200 {::yrs/status 200
:body (-> (io/resource "app/templates/error-list.tmpl") ::yrs/body (-> (io/resource "app/templates/error-list.tmpl")
(tmpl/render {:items items})) (tmpl/render {:items items}))
:headers {"content-type" "text/html; charset=utf-8" ::yrs/headers {"content-type" "text/html; charset=utf-8"
"x-robots-tag" "noindex"}))) "x-robots-tag" "noindex"}}))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; EXPORT/IMPORT ;; EXPORT/IMPORT
@ -301,16 +308,15 @@
::binf/profile-id profile-id ::binf/profile-id profile-id
::binf/project-id project-id)) ::binf/project-id project-id))
(yrs/response {::yrs/status 200
:status 200 ::yrs/headers {"content-type" "text/plain"}
:headers {"content-type" "text/plain"} ::yrs/body "OK CLONED"})
:body "OK CLONED"))
{::yrs/status 200
::yrs/body (io/input-stream path)
::yrs/headers {"content-type" "application/octet-stream"
"content-disposition" (str "attachmen; filename=" (first file-ids) ".penpot")}}))))
(yrs/response
:status 200
:headers {"content-type" "application/octet-stream"
"content-disposition" (str "attachmen; filename=" (first file-ids) ".penpot")}
:body (io/input-stream path))))))
(defn import-handler (defn import-handler
@ -340,10 +346,9 @@
::binf/profile-id profile-id ::binf/profile-id profile-id
::binf/project-id project-id)) ::binf/project-id project-id))
(yrs/response {::yrs/status 200
:status 200 ::yrs/headers {"content-type" "text/plain"}
:headers {"content-type" "text/plain"} ::yrs/body "OK"}))
:body "OK")))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; OTHER SMALL VIEWS/HANDLERS ;; OTHER SMALL VIEWS/HANDLERS
@ -354,11 +359,13 @@
[{:keys [::db/pool]} _] [{:keys [::db/pool]} _]
(try (try
(db/exec-one! pool ["select count(*) as count from server_prop;"]) (db/exec-one! pool ["select count(*) as count from server_prop;"])
(yrs/response 200 "OK") {::yrs/status 200
::yrs/body "OK"}
(catch Throwable cause (catch Throwable cause
(l/warn :hint "unable to execute query on health handler" (l/warn :hint "unable to execute query on health handler"
:cause cause) :cause cause)
(yrs/response 503 "KO")))) {::yrs/status 503
::yrs/body "KO"})))
(defn changelog-handler (defn changelog-handler
[_ _] [_ _]
@ -367,10 +374,11 @@
(md->html [text] (md->html [text]
(md/md-to-html-string text :replacement-transformers (into [transform-emoji] mdt/transformer-vector)))] (md/md-to-html-string text :replacement-transformers (into [transform-emoji] mdt/transformer-vector)))]
(if-let [clog (io/resource "changelog.md")] (if-let [clog (io/resource "changelog.md")]
(yrs/response :status 200 {::yrs/status 200
:headers {"content-type" "text/html; charset=utf-8"} ::yrs/headers {"content-type" "text/html; charset=utf-8"}
:body (-> clog slurp md->html)) ::yrs/body (-> clog slurp md->html)}
(yrs/response :status 404 :body "NOT FOUND")))) {::yrs/status 404
::yrs/body "NOT FOUND"})))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; INIT ;; INIT
@ -380,33 +388,26 @@
{:compile {:compile
(fn [& _] (fn [& _]
(fn [handler pool] (fn [handler pool]
(fn [request respond raise] (fn [request]
(if (authorized? pool request) (if (authorized? pool request)
(handler request respond raise) (handler request)
(raise (ex/error :type :authentication (ex/raise :type :authentication
:code :only-admins-allowed))))))}) :code :only-admins-allowed)))))})
(defmethod ig/pre-init-spec ::routes [_] (defmethod ig/pre-init-spec ::routes [_]
(s/keys :req [::db/pool (s/keys :req [::db/pool ::session/manager]))
::wrk/executor
::session/manager]))
(defmethod ig/init-key ::routes (defmethod ig/init-key ::routes
[_ {:keys [::db/pool ::wrk/executor] :as cfg}] [_ {:keys [::db/pool] :as cfg}]
[["/readyz" {:middleware [[mw/with-dispatch executor] [["/readyz" {:handler (partial health-handler cfg)}]
[mw/with-config cfg]]
:handler health-handler}]
["/dbg" {:middleware [[session/authz cfg] ["/dbg" {:middleware [[session/authz cfg]
[with-authorization pool] [with-authorization pool]]}
[mw/with-dispatch executor] ["" {:handler (partial index-handler cfg)}]
[mw/with-config cfg]]} ["/health" {:handler (partial health-handler cfg)}]
["" {:handler index-handler}] ["/changelog" {:handler (partial changelog-handler cfg)}]
["/health" {:handler health-handler}] ["/error/:id" {:handler (partial error-handler cfg)}]
["/changelog" {:handler changelog-handler}] ["/error" {:handler (partial error-list-handler cfg)}]
;; ["/error-by-id/:id" {:handler error-handler}] ["/file/export" {:handler (partial export-handler cfg)}]
["/error/:id" {:handler error-handler}] ["/file/import" {:handler (partial import-handler cfg)}]
["/error" {:handler error-list-handler}] ["/file/data" {:handler (partial file-data-handler cfg)}]
["/file/export" {:handler export-handler}] ["/file/changes" {:handler (partial file-changes-handler cfg)}]]])
["/file/import" {:handler import-handler}]
["/file/data" {:handler file-data-handler}]
["/file/changes" {:handler file-changes-handler}]]])

View File

@ -46,20 +46,30 @@
(defmethod handle-exception :authentication (defmethod handle-exception :authentication
[err _] [err _]
(yrs/response 401 (ex-data err))) {::yrs/status 401
::yrs/body (ex-data err)})
(defmethod handle-exception :authorization (defmethod handle-exception :authorization
[err _] [err _]
(yrs/response 403 (ex-data err))) {::yrs/status 403
::yrs/body (ex-data err)})
(defmethod handle-exception :restriction (defmethod handle-exception :restriction
[err _] [err _]
(yrs/response 400 (ex-data err))) {::yrs/status 400
::yrs/body (ex-data err)})
(defmethod handle-exception :rate-limit (defmethod handle-exception :rate-limit
[err _] [err _]
(let [headers (-> err ex-data ::http/headers)] (let [headers (-> err ex-data ::http/headers)]
(yrs/response :status 429 :body "" :headers headers))) {::yrs/status 429
::yrs/headers headers}))
(defmethod handle-exception :concurrency-limit
[err _]
(let [headers (-> err ex-data ::http/headers)]
{::yrs/status 429
::yrs/headers headers}))
(defmethod handle-exception :validation (defmethod handle-exception :validation
[err _] [err _]
@ -67,16 +77,16 @@
(cond (cond
(= code :spec-validation) (= code :spec-validation)
(let [explain (ex/explain data)] (let [explain (ex/explain data)]
(yrs/response :status 400 {::yrs/status 400
:body (-> data ::yrs/body (-> data
(dissoc ::s/problems ::s/value) (dissoc ::s/problems ::s/value)
(cond-> explain (assoc :explain explain))))) (cond-> explain (assoc :explain explain)))})
(= code :request-body-too-large) (= code :request-body-too-large)
(yrs/response :status 413 :body data) {::yrs/status 413 ::yrs/body data}
:else :else
(yrs/response :status 400 :body data)))) {::yrs/status 400 ::yrs/body data})))
(defmethod handle-exception :assertion (defmethod handle-exception :assertion
[error request] [error request]
@ -84,31 +94,27 @@
explain (ex/explain edata)] explain (ex/explain edata)]
(binding [l/*context* (request->context request)] (binding [l/*context* (request->context request)]
(l/error :hint "Assertion error" :message (ex-message error) :cause error) (l/error :hint "Assertion error" :message (ex-message error) :cause error)
(yrs/response :status 500 {::yrs/status 500
:body {:type :server-error ::yrs/body {:type :server-error
:code :assertion :code :assertion
:data (-> edata :data (-> edata
(dissoc ::s/problems ::s/value ::s/spec) (dissoc ::s/problems ::s/value ::s/spec)
(cond-> explain (assoc :explain explain)))})))) (cond-> explain (assoc :explain explain)))}})))
(defmethod handle-exception :not-found (defmethod handle-exception :not-found
[err _] [err _]
(yrs/response 404 (ex-data err))) {::yrs/status 404
::yrs/body (ex-data err)})
(defmethod handle-exception :internal (defmethod handle-exception :internal
[error request] [error request]
(let [{:keys [code] :as edata} (ex-data error)] (binding [l/*context* (request->context request)]
(cond (l/error :hint "Internal error" :message (ex-message error) :cause error)
(= :concurrency-limit-reached code) {::yrs/status 500
(yrs/response 429) ::yrs/body {:type :server-error
:code :unhandloed
:else :hint (ex-message error)
(binding [l/*context* (request->context request)] :data (ex-data error)}}))
(l/error :hint "Internal error" :message (ex-message error) :cause error)
(yrs/response 500 {:type :server-error
:code :unhandled
:hint (ex-message error)
:data edata})))))
(defmethod handle-exception org.postgresql.util.PSQLException (defmethod handle-exception org.postgresql.util.PSQLException
[error request] [error request]
@ -117,20 +123,23 @@
(l/error :hint "PSQL error" :message (ex-message error) :cause error) (l/error :hint "PSQL error" :message (ex-message error) :cause error)
(cond (cond
(= state "57014") (= state "57014")
(yrs/response 504 {:type :server-error {::yrs/status 504
:code :statement-timeout ::yrs/body {:type :server-error
:hint (ex-message error)}) :code :statement-timeout
:hint (ex-message error)}}
(= state "25P03") (= state "25P03")
(yrs/response 504 {:type :server-error {::yrs/status 504
:code :idle-in-transaction-timeout ::yrs/body {:type :server-error
:hint (ex-message error)}) :code :idle-in-transaction-timeout
:hint (ex-message error)}}
:else :else
(yrs/response 500 {:type :server-error {::yrs/status 500
:code :unexpected ::yrs/body {:type :server-error
:hint (ex-message error) :code :unexpected
:state state}))))) :hint (ex-message error)
:state state}}))))
(defmethod handle-exception :default (defmethod handle-exception :default
[error request] [error request]
@ -140,9 +149,10 @@
(nil? edata) (nil? edata)
(binding [l/*context* (request->context request)] (binding [l/*context* (request->context request)]
(l/error :hint "Unexpected error" :message (ex-message error) :cause error) (l/error :hint "Unexpected error" :message (ex-message error) :cause error)
(yrs/response 500 {:type :server-error {::yrs/status 500
:code :unexpected ::yrs/body {:type :server-error
:hint (ex-message error)})) :code :unexpected
:hint (ex-message error)}})
;; This is a special case for the idle-in-transaction error; ;; This is a special case for the idle-in-transaction error;
;; when it happens, the connection is automatically closed and ;; when it happens, the connection is automatically closed and
@ -156,10 +166,11 @@
:else :else
(binding [l/*context* (request->context request)] (binding [l/*context* (request->context request)]
(l/error :hint "Unhandled error" :message (ex-message error) :cause error) (l/error :hint "Unhandled error" :message (ex-message error) :cause error)
(yrs/response 500 {:type :server-error {::yrs/status 500
:code :unhandled ::yrs/body {:type :server-error
:hint (ex-message error) :code :unhandled
:data edata}))))) :hint (ex-message error)
:data edata}}))))
(defn handle (defn handle
[cause request] [cause request]

View File

@ -14,6 +14,7 @@
[cuerdas.core :as str] [cuerdas.core :as str]
[promesa.core :as p] [promesa.core :as p]
[promesa.exec :as px] [promesa.exec :as px]
[promesa.util :as pu]
[yetti.adapter :as yt] [yetti.adapter :as yt]
[yetti.middleware :as ymw] [yetti.middleware :as ymw]
[yetti.request :as yrq] [yetti.request :as yrq]
@ -22,7 +23,10 @@
com.fasterxml.jackson.core.JsonParseException com.fasterxml.jackson.core.JsonParseException
com.fasterxml.jackson.core.io.JsonEOFException com.fasterxml.jackson.core.io.JsonEOFException
io.undertow.server.RequestTooBigException io.undertow.server.RequestTooBigException
java.io.OutputStream)) java.io.OutputStream
java.io.InputStream))
(set! *warn-on-reflection* true)
(def server-timing (def server-timing
{:name ::server-timing {:name ::server-timing
@ -44,14 +48,14 @@
(let [header (yrq/get-header request "content-type")] (let [header (yrq/get-header request "content-type")]
(cond (cond
(str/starts-with? header "application/transit+json") (str/starts-with? header "application/transit+json")
(with-open [is (yrq/body request)] (with-open [^InputStream is (yrq/body request)]
(let [params (t/read! (t/reader is))] (let [params (t/read! (t/reader is))]
(-> request (-> request
(assoc :body-params params) (assoc :body-params params)
(update :params merge params)))) (update :params merge params))))
(str/starts-with? header "application/json") (str/starts-with? header "application/json")
(with-open [is (yrq/body request)] (with-open [^InputStream is (yrq/body request)]
(let [params (json/decode is json-mapper)] (let [params (json/decode is json-mapper)]
(-> request (-> request
(assoc :body-params params) (assoc :body-params params)
@ -62,6 +66,11 @@
(handle-error [raise cause] (handle-error [raise cause]
(cond (cond
(instance? RuntimeException cause)
(if-let [cause (ex-cause cause)]
(handle-error raise cause)
(raise cause))
(instance? RequestTooBigException cause) (instance? RequestTooBigException cause)
(raise (ex/error :type :validation (raise (ex/error :type :validation
:code :request-body-too-large :code :request-body-too-large
@ -78,12 +87,12 @@
(raise cause)))] (raise cause)))]
(fn [request respond raise] (fn [request respond raise]
(let [request (ex/try! (process-request request))] (if (= (yrq/method request) :post)
(if (ex/exception? request) (let [request (ex/try! (process-request request))]
(if (ex/runtime-exception? request) (if (ex/exception? request)
(handle-error raise (or (ex-cause request) request)) (handle-error raise request)
(handle-error raise request)) (handler request respond raise)))
(handler request respond raise)))))) (handler request respond raise)))))
(def parse-request (def parse-request
{:name ::parse-request {:name ::parse-request
@ -94,12 +103,7 @@
needed because transit-java calls flush very aggresivelly on each needed because transit-java calls flush very aggresivelly on each
object write." object write."
[^java.io.OutputStream os ^long chunk-size] [^java.io.OutputStream os ^long chunk-size]
(proxy [java.io.BufferedOutputStream] [os (int chunk-size)] (yetti.util.BufferedOutputStream. os (int chunk-size)))
;; Explicitly do not forward flush
(flush [])
(close []
(proxy-super flush)
(proxy-super close))))
(def ^:const buffer-size (:xnio/buffer-size yt/defaults)) (def ^:const buffer-size (:xnio/buffer-size yt/defaults))
@ -109,13 +113,10 @@
(reify yrs/StreamableResponseBody (reify yrs/StreamableResponseBody
(-write-body-to-stream [_ _ output-stream] (-write-body-to-stream [_ _ output-stream]
(try (try
(with-open [bos (buffered-output-stream output-stream buffer-size)] (with-open [^OutputStream bos (buffered-output-stream output-stream buffer-size)]
(let [tw (t/writer bos opts)] (let [tw (t/writer bos opts)]
(t/write! tw data))) (t/write! tw data)))
(catch java.io.IOException _)
(catch java.io.IOException _cause
;; Do nothing, EOF means client closes connection abruptly
nil)
(catch Throwable cause (catch Throwable cause
(l/warn :hint "unexpected error on encoding response" (l/warn :hint "unexpected error on encoding response"
:cause cause)) :cause cause))
@ -126,13 +127,10 @@
(reify yrs/StreamableResponseBody (reify yrs/StreamableResponseBody
(-write-body-to-stream [_ _ output-stream] (-write-body-to-stream [_ _ output-stream]
(try (try
(with-open [^OutputStream bos (buffered-output-stream output-stream buffer-size)]
(with-open [bos (buffered-output-stream output-stream buffer-size)]
(json/write! bos data json-mapper)) (json/write! bos data json-mapper))
(catch java.io.IOException _cause (catch java.io.IOException _)
;; Do nothing, EOF means client closes connection abruptly
nil)
(catch Throwable cause (catch Throwable cause
(l/warn :hint "unexpected error on encoding response" (l/warn :hint "unexpected error on encoding response"
:cause cause)) :cause cause))
@ -140,15 +138,15 @@
(.close ^OutputStream output-stream)))))) (.close ^OutputStream output-stream))))))
(format-response-with-json [response _] (format-response-with-json [response _]
(let [body (yrs/body response)] (let [body (::yrs/body response)]
(if (or (boolean? body) (coll? body)) (if (or (boolean? body) (coll? body))
(-> response (-> response
(update :headers assoc "content-type" "application/json") (update ::yrs/headers assoc "content-type" "application/json")
(assoc :body (json-streamable-body body))) (assoc ::yrs/body (json-streamable-body body)))
response))) response)))
(format-response-with-transit [response request] (format-response-with-transit [response request]
(let [body (yrs/body response)] (let [body (::yrs/body response)]
(if (or (boolean? body) (coll? body)) (if (or (boolean? body) (coll? body))
(let [qs (yrq/query request) (let [qs (yrq/query request)
opts (if (or (contains? cf/flags :transit-readable-response) opts (if (or (contains? cf/flags :transit-readable-response)
@ -156,8 +154,8 @@
{:type :json-verbose} {:type :json-verbose}
{:type :json})] {:type :json})]
(-> response (-> response
(update :headers assoc "content-type" "application/transit+json") (update ::yrs/headers assoc "content-type" "application/transit+json")
(assoc :body (transit-streamable-body body opts)))) (assoc ::yrs/body (transit-streamable-body body opts))))
response))) response)))
(format-response [response request] (format-response [response request]
@ -181,8 +179,7 @@
(fn [request respond raise] (fn [request respond raise]
(handler request (handler request
(fn [response] (fn [response]
(let [response (process-response response request)] (respond (process-response response request)))
(respond response)))
raise)))) raise))))
(def format-response (def format-response
@ -191,74 +188,59 @@
(defn wrap-errors (defn wrap-errors
[handler on-error] [handler on-error]
(fn [request respond _] (fn [request respond raise]
(handler request respond (fn [cause] (handler request respond (fn [cause]
(-> cause (on-error request) respond))))) (try
(respond (on-error cause request))
(catch Throwable cause
(raise cause)))))))
(def errors (def errors
{:name ::errors {:name ::errors
:compile (constantly wrap-errors)}) :compile (constantly wrap-errors)})
(defn- with-cors-headers
[headers origin]
(-> headers
(assoc "access-control-allow-origin" origin)
(assoc "access-control-allow-methods" "GET,POST,DELETE,OPTIONS,PUT,HEAD,PATCH")
(assoc "access-control-allow-credentials" "true")
(assoc "access-control-expose-headers" "x-requested-with, content-type, cookie")
(assoc "access-control-allow-headers" "x-frontend-version, content-type, accept, x-requested-width")))
(defn wrap-cors (defn wrap-cors
[handler] [handler]
(if-not (contains? cf/flags :cors) (fn [request]
handler (let [response (if (= (yrq/method request) :options)
(letfn [(add-headers [headers request] {::yrs/status 200}
(let [origin (yrq/get-header request "origin")] (handler request))
(-> headers origin (yrq/get-header request "origin")]
(assoc "access-control-allow-origin" origin) (update response ::yrs/headers with-cors-headers origin))))
(assoc "access-control-allow-methods" "GET,POST,DELETE,OPTIONS,PUT,HEAD,PATCH")
(assoc "access-control-allow-credentials" "true")
(assoc "access-control-expose-headers" "x-requested-with, content-type, cookie")
(assoc "access-control-allow-headers" "x-frontend-version, content-type, accept, x-requested-width"))))
(update-response [response request]
(update response :headers add-headers request))]
(fn [request respond raise]
(if (= (yrq/method request) :options)
(-> (yrs/response 200)
(update-response request)
(respond))
(handler request
(fn [response]
(respond (update-response response request)))
raise))))))
(def cors (def cors
{:name ::cors {:name ::cors
:compile (constantly wrap-cors)}) :compile (fn [& _]
(when (contains? cf/flags :cors)
(defn compile-restrict-methods wrap-cors))})
[data _]
(when-let [allowed (:allowed-methods data)]
(fn [handler]
(fn [request respond raise]
(let [method (yrq/method request)]
(if (contains? allowed method)
(handler request respond raise)
(respond (yrs/response 405))))))))
(def restrict-methods (def restrict-methods
{:name ::restrict-methods {:name ::restrict-methods
:compile compile-restrict-methods}) :compile
(fn [data _]
(when-let [allowed (:allowed-methods data)]
(fn [handler]
(fn [request respond raise]
(let [method (yrq/method request)]
(if (contains? allowed method)
(handler request respond raise)
(respond {::yrs/status 405})))))))})
(def with-dispatch (def with-dispatch
{:name ::with-dispatch {:name ::with-dispatch
:compile :compile
(fn [& _] (fn [& _]
(fn [handler executor] (fn [handler executor]
(fn [request respond raise] (let [executor (px/resolve-executor executor)]
(-> (px/submit! executor #(handler request)) (fn [request respond raise]
(p/bind p/wrap) (->> (px/submit! executor (partial handler request))
(p/then respond) (p/fnly (pu/handler respond raise)))))))})
(p/catch raise)))))})
(def with-config
{:name ::with-config
:compile
(fn [& _]
(fn [handler config]
(fn
([request] (handler config request))
([request respond raise] (handler config request respond raise)))))})

View File

@ -8,7 +8,6 @@
(:refer-clojure :exclude [read]) (:refer-clojure :exclude [read])
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.logging :as l] [app.common.logging :as l]
[app.common.spec :as us] [app.common.spec :as us]
[app.config :as cf] [app.config :as cf]
@ -18,12 +17,9 @@
[app.main :as-alias main] [app.main :as-alias main]
[app.tokens :as tokens] [app.tokens :as tokens]
[app.util.time :as dt] [app.util.time :as dt]
[app.worker :as wrk]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[cuerdas.core :as str] [cuerdas.core :as str]
[integrant.core :as ig] [integrant.core :as ig]
[promesa.core :as p]
[promesa.exec :as px]
[yetti.request :as yrq])) [yetti.request :as yrq]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -76,69 +72,56 @@
:id key}) :id key})
(defn- database-manager (defn- database-manager
[{:keys [::db/pool ::wrk/executor ::main/props]}] [pool]
^{::wrk/executor executor
::db/pool pool
::main/props props}
(reify ISessionManager (reify ISessionManager
(read [_ token] (read [_ token]
(px/with-dispatch executor (db/exec-one! pool (sql/select :http-session {:id token})))
(db/exec-one! pool (sql/select :http-session {:id token}))))
(write! [_ key params] (write! [_ key params]
(px/with-dispatch executor (let [params (prepare-session-params key params)]
(let [params (prepare-session-params key params)] (db/insert! pool :http-session params)
(db/insert! pool :http-session params) params))
params)))
(update! [_ params] (update! [_ params]
(let [updated-at (dt/now)] (let [updated-at (dt/now)]
(px/with-dispatch executor (db/update! pool :http-session
(db/update! pool :http-session {:updated-at updated-at}
{:updated-at updated-at} {:id (:id params)})
{:id (:id params)}) (assoc params :updated-at updated-at)))
(assoc params :updated-at updated-at))))
(delete! [_ token] (delete! [_ token]
(px/with-dispatch executor (db/delete! pool :http-session {:id token})
(db/delete! pool :http-session {:id token}) nil)))
nil))))
(defn inmemory-manager (defn inmemory-manager
[{:keys [::db/pool ::wrk/executor ::main/props]}] []
(let [cache (atom {})] (let [cache (atom {})]
^{::main/props props
::wrk/executor executor
::db/pool pool}
(reify ISessionManager (reify ISessionManager
(read [_ token] (read [_ token]
(p/do (get @cache token))) (get @cache token))
(write! [_ key params] (write! [_ key params]
(p/do (let [params (prepare-session-params key params)]
(let [params (prepare-session-params key params)] (swap! cache assoc key params)
(swap! cache assoc key params) params))
params)))
(update! [_ params] (update! [_ params]
(p/do (let [updated-at (dt/now)]
(let [updated-at (dt/now)] (swap! cache update (:id params) assoc :updated-at updated-at)
(swap! cache update (:id params) assoc :updated-at updated-at) (assoc params :updated-at updated-at)))
(assoc params :updated-at updated-at))))
(delete! [_ token] (delete! [_ token]
(p/do (swap! cache dissoc token)
(swap! cache dissoc token) nil))))
nil)))))
(defmethod ig/pre-init-spec ::manager [_] (defmethod ig/pre-init-spec ::manager [_]
(s/keys :req [::db/pool ::wrk/executor ::main/props])) (s/keys :req [::db/pool]))
(defmethod ig/init-key ::manager (defmethod ig/init-key ::manager
[_ {:keys [::db/pool] :as cfg}] [_ {:keys [::db/pool]}]
(if (db/read-only? pool) (if (db/read-only? pool)
(inmemory-manager cfg) (inmemory-manager)
(database-manager cfg))) (database-manager pool)))
(defmethod ig/halt-key! ::manager (defmethod ig/halt-key! ::manager
[_ _]) [_ _])
@ -154,40 +137,35 @@
(declare ^:private gen-token) (declare ^:private gen-token)
(defn create-fn (defn create-fn
[{:keys [::manager]} profile-id] [{:keys [::manager ::main/props]} profile-id]
(us/assert! ::manager manager) (us/assert! ::manager manager)
(us/assert! ::us/uuid profile-id) (us/assert! ::us/uuid profile-id)
(let [props (-> manager meta ::main/props)] (fn [request response]
(fn [request response] (let [uagent (yrq/get-header request "user-agent")
(let [uagent (yrq/get-header request "user-agent") params {:profile-id profile-id
params {:profile-id profile-id :user-agent uagent
:user-agent uagent :created-at (dt/now)}
:created-at (dt/now)} token (gen-token props params)
token (gen-token props params)] session (write! manager token params)]
(l/trace :hint "create" :profile-id (str profile-id))
(-> response
(assign-auth-token-cookie session)
(assign-authenticated-cookie session)))))
(->> (write! manager token params)
(p/fmap (fn [session]
(l/trace :hint "create" :profile-id (str profile-id))
(-> response
(assign-auth-token-cookie session)
(assign-authenticated-cookie session)))))))))
(defn delete-fn (defn delete-fn
[{:keys [::manager]}] [{:keys [::manager]}]
(us/assert! ::manager manager) (us/assert! ::manager manager)
(letfn [(delete [{:keys [profile-id] :as request}] (fn [request response]
(let [cname (cf/get :auth-token-cookie-name default-auth-token-cookie-name) (let [cname (cf/get :auth-token-cookie-name default-auth-token-cookie-name)
cookie (yrq/get-cookie request cname)] cookie (yrq/get-cookie request cname)]
(l/trace :hint "delete" :profile-id profile-id) (l/trace :hint "delete" :profile-id (:profile-id request))
(some->> (:value cookie) (delete! manager))))] (some->> (:value cookie) (delete! manager))
(fn [request response] (-> response
(p/do (assoc :status 204)
(delete request) (assoc :body nil)
(-> response (clear-auth-token-cookie)
(assoc :status 204) (clear-authenticated-cookie)))))
(assoc :body nil)
(clear-auth-token-cookie)
(clear-authenticated-cookie))))))
(defn- gen-token (defn- gen-token
[props {:keys [profile-id created-at]}] [props {:keys [profile-id created-at]}]
@ -216,58 +194,39 @@
(let [elapsed (dt/diff updated-at (dt/now))] (let [elapsed (dt/diff updated-at (dt/now))]
(neg? (compare default-renewal-max-age elapsed))))) (neg? (compare default-renewal-max-age elapsed)))))
(defn- wrap-reneval
[respond manager session]
(fn [response]
(p/let [session (update! manager session)]
(-> response
(assign-auth-token-cookie session)
(assign-authenticated-cookie session)
(respond)))))
(defn- wrap-soft-auth (defn- wrap-soft-auth
[handler {:keys [::manager]}] [handler {:keys [::manager ::main/props]}]
(us/assert! ::manager manager) (us/assert! ::manager manager)
(letfn [(handle-request [request]
(try
(let [token (get-token request)
claims (decode-token props token)]
(cond-> request
(map? claims)
(-> (assoc ::token-claims claims)
(assoc ::token token))))
(catch Throwable cause
(l/trace :hint "exception on decoding malformed token" :cause cause)
request)))]
(let [{:keys [::wrk/executor ::main/props]} (meta manager)]
(fn [request respond raise] (fn [request respond raise]
(let [token (ex/try! (get-token request))] (let [request (handle-request request)]
(if (ex/exception? token) (handler request respond raise)))))
(raise token)
(->> (px/submit! executor (partial decode-token props token))
(p/fnly (fn [claims cause]
(when cause
(l/trace :hint "exception on decoding malformed token" :cause cause))
(let [request (cond-> request
(map? claims)
(-> (assoc ::token-claims claims)
(assoc ::token token)))]
(handler request respond raise))))))))))
(defn- wrap-authz (defn- wrap-authz
[handler {:keys [::manager]}] [handler {:keys [::manager]}]
(us/assert! ::manager manager) (us/assert! ::manager manager)
(fn [request respond raise] (fn [request]
(if-let [token (::token request)] (let [session (get-session manager (::token request))
(->> (get-session manager token) request (cond-> request
(p/fnly (fn [session cause] (some? session)
(cond (assoc ::profile-id (:profile-id session)
(some? cause) ::id (:id session)))]
(raise cause)
(nil? session) (cond-> (handler request)
(handler request respond raise) (renew-session? session)
(-> (assign-auth-token-cookie session)
:else (assign-authenticated-cookie session))))))
(let [request (-> request
(assoc ::profile-id (:profile-id session))
(assoc ::id (:id session)))
respond (cond-> respond
(renew-session? session)
(wrap-reneval manager session))]
(handler request respond raise))))))
(handler request respond raise))))
(def soft-auth (def soft-auth
{:name ::soft-auth {:name ::soft-auth

View File

@ -17,9 +17,9 @@
[app.msgbus :as mbus] [app.msgbus :as mbus]
[app.util.time :as dt] [app.util.time :as dt]
[app.util.websocket :as ws] [app.util.websocket :as ws]
[clojure.core.async :as a]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[integrant.core :as ig] [integrant.core :as ig]
[promesa.exec.csp :as sp]
[yetti.websocket :as yws])) [yetti.websocket :as yws]))
(def recv-labels (def recv-labels
@ -34,70 +34,38 @@
(def state (atom {})) (def state (atom {}))
(defn- on-connect
[{:keys [::mtx/metrics]} wsp]
(let [created-at (dt/now)]
(swap! state assoc (::ws/id @wsp) wsp)
(mtx/run! metrics
:id :websocket-active-connections
:inc 1)
(fn []
(swap! state dissoc (::ws/id @wsp))
(mtx/run! metrics :id :websocket-active-connections :dec 1)
(mtx/run! metrics
:id :websocket-session-timing
:val (/ (inst-ms (dt/diff created-at (dt/now))) 1000.0)))))
(defn- on-rcv-message
[{:keys [::mtx/metrics]} _ message]
(mtx/run! metrics
:id :websocket-messages-total
:labels recv-labels
:inc 1)
message)
(defn- on-snd-message
[{:keys [::mtx/metrics]} _ message]
(mtx/run! metrics
:id :websocket-messages-total
:labels send-labels
:inc 1)
message)
;; REPL HELPERS ;; REPL HELPERS
(defn repl-get-connections-for-file (defn repl-get-connections-for-file
[file-id] [file-id]
(->> (vals @state) (->> (vals @state)
(filter #(= file-id (-> % deref ::file-subscription :file-id))) (filter #(= file-id (-> % deref ::file-subscription :file-id)))
(map deref)
(map ::ws/id))) (map ::ws/id)))
(defn repl-get-connections-for-team (defn repl-get-connections-for-team
[team-id] [team-id]
(->> (vals @state) (->> (vals @state)
(filter #(= team-id (-> % deref ::team-subscription :team-id))) (filter #(= team-id (-> % deref ::team-subscription :team-id)))
(map deref)
(map ::ws/id))) (map ::ws/id)))
(defn repl-close-connection (defn repl-close-connection
[id] [id]
(when-let [wsp (get @state id)] (when-let [{:keys [::ws/close-ch] :as wsp} (get @state id)]
(a/>!! (::ws/close-ch @wsp) [8899 "closed from server"]) (sp/put! close-ch [8899 "closed from server"])
(a/close! (::ws/close-ch @wsp)))) (sp/close! close-ch)))
(defn repl-get-connection-info (defn repl-get-connection-info
[id] [id]
(when-let [wsp (get @state id)] (when-let [wsp (get @state id)]
{:id id {:id id
:created-at (::created-at @wsp) :created-at (::created-at wsp)
:profile-id (::profile-id @wsp) :profile-id (::profile-id wsp)
:session-id (::session-id @wsp) :session-id (::session-id wsp)
:user-agent (::ws/user-agent @wsp) :user-agent (::ws/user-agent wsp)
:ip-addr (::ws/remote-addr @wsp) :ip-addr (::ws/remote-addr wsp)
:last-activity-at (::ws/last-activity-at @wsp) :last-activity-at (::ws/last-activity-at wsp)
:subscribed-file (-> wsp deref ::file-subscription :file-id) :subscribed-file (-> wsp ::file-subscription :file-id)
:subscribed-team (-> wsp deref ::team-subscription :team-id)})) :subscribed-team (-> wsp ::team-subscription :team-id)}))
(defn repl-print-connection-info (defn repl-print-connection-info
[id] [id]
@ -117,223 +85,215 @@
(fn [_ _ message] (fn [_ _ message]
(:type message))) (:type message)))
(defmethod handle-message :connect (defmethod handle-message :open
[cfg wsp _] [{:keys [::mbus/msgbus]} {:keys [::ws/id ::ws/output-ch ::ws/state ::profile-id ::session-id] :as wsp} _]
(l/trace :fn "handle-message" :event "open" :conn-id id)
(let [ch (sp/chan :buf (sp/dropping-buffer 16)
:xf (remove #(= (:session-id %) session-id)))]
(let [msgbus (::mbus/msgbus cfg) ;; Subscribe to the profile channel and forward all messages to websocket output
conn-id (::ws/id @wsp) ;; channel (send them to the client).
profile-id (::profile-id @wsp) (swap! state assoc ::profile-subscription {:channel ch})
session-id (::session-id @wsp)
output-ch (::ws/output-ch @wsp)
xform (remove #(= (:session-id %) session-id)) ;; Forward the subscription messages directly to the websocket output channel
channel (a/chan (a/dropping-buffer 16) xform)] (sp/pipe ch output-ch false)
(l/trace :fn "handle-message" :event "connect" :conn-id conn-id) ;; Subscribe to the profile topic on msgbus/redis
(mbus/sub! msgbus :topic profile-id :chan ch)))
;; Subscribe to the profile channel and forward all messages to (defmethod handle-message :close
;; websocket output channel (send them to the client). [{:keys [::mbus/msgbus]} {:keys [::ws/id ::ws/state ::profile-id ::session-id]} _]
(swap! wsp assoc ::profile-subscription channel) (l/trace :fn "handle-message" :event "close" :conn-id id)
(a/pipe channel output-ch false) (let [psub (::profile-subscription @state)
(mbus/sub! msgbus :topic profile-id :chan channel))) fsub (::file-subscription @state)
tsub (::team-subscription @state)
msg {:type :disconnect
:subs-id profile-id
:profile-id profile-id
:session-id session-id}]
(defmethod handle-message :disconnect ;; Close profile subscription if exists
[cfg wsp _] (when-let [ch (:channel psub)]
(let [msgbus (::mbus/msgbus cfg) (sp/close! ch)
conn-id (::ws/id @wsp) (mbus/purge! msgbus [ch]))
profile-id (::profile-id @wsp)
session-id (::session-id @wsp)
profile-ch (::profile-subscription @wsp)
fsub (::file-subscription @wsp)
tsub (::team-subscription @wsp)
message {:type :disconnect ;; Close team subscription if exists
:subs-id profile-id (when-let [ch (:channel tsub)]
:profile-id profile-id (sp/close! ch)
:session-id session-id}] (mbus/purge! msgbus [ch]))
(l/trace :fn "handle-message"
:event :disconnect
:conn-id conn-id)
(a/go
;; Close the main profile subscription
(a/close! profile-ch)
(a/<! (mbus/purge! msgbus [profile-ch]))
;; Close tram subscription if exists
(when-let [channel (:channel tsub)]
(a/close! channel)
(a/<! (mbus/purge! msgbus channel)))
;; Close file subscription if exists
(when-let [{:keys [topic channel]} fsub] (when-let [{:keys [topic channel]} fsub]
(a/close! channel) (sp/close! channel)
(a/<! (mbus/purge! msgbus channel)) (mbus/purge! msgbus [channel])
(a/<! (mbus/pub! msgbus :topic topic :message message)))))) (mbus/pub! msgbus :topic topic :message msg))))
(defmethod handle-message :subscribe-team (defmethod handle-message :subscribe-team
[cfg wsp {:keys [team-id] :as params}] [{:keys [::mbus/msgbus]} {:keys [::ws/id ::ws/state ::ws/output-ch ::session-id]} {:keys [team-id] :as params}]
(let [msgbus (::mbus/msgbus cfg) (l/trace :fn "handle-message" :event "subscribe-team" :team-id team-id :conn-id id)
conn-id (::ws/id @wsp) (let [prev-subs (get @state ::team-subscription)
session-id (::session-id @wsp) channel (sp/chan :buf (sp/dropping-buffer 64)
output-ch (::ws/output-ch @wsp) :xf (comp
prev-subs (get @wsp ::team-subscription) (remove #(= (:session-id %) session-id))
xform (comp (map #(assoc % :subs-id team-id))))]
(remove #(= (:session-id %) session-id))
(map #(assoc % :subs-id team-id)))
channel (a/chan (a/dropping-buffer 64) xform)] (sp/pipe channel output-ch false)
(mbus/sub! msgbus :topic team-id :chan channel)
(l/trace :fn "handle-message" (let [subs {:team-id team-id :channel channel :topic team-id}]
:event :subscribe-team (swap! state assoc ::team-subscription subs))
:team-id team-id
:conn-id conn-id)
(a/pipe channel output-ch false) ;; Close previous subscription if exists
(when-let [ch (:channel prev-subs)]
(sp/close! ch)
(mbus/purge! msgbus [ch]))))
(let [state {:team-id team-id :channel channel :topic team-id}]
(swap! wsp assoc ::team-subscription state))
(a/go
;; Close previous subscription if exists
(when-let [channel (:channel prev-subs)]
(a/close! channel)
(a/<! (mbus/purge! msgbus channel))))
(a/go
(a/<! (mbus/sub! msgbus :topic team-id :chan channel)))))
(defmethod handle-message :subscribe-file (defmethod handle-message :subscribe-file
[cfg wsp {:keys [file-id] :as params}] [{:keys [::mbus/msgbus]} {:keys [::ws/id ::ws/state ::ws/output-ch ::session-id ::profile-id]} {:keys [file-id] :as params}]
(let [msgbus (::mbus/msgbus cfg) (l/trace :fn "handle-message" :event "subscribe-file" :file-id file-id :conn-id id)
conn-id (::ws/id @wsp) (let [psub (::file-subscription @state)
profile-id (::profile-id @wsp) fch (sp/chan :buf (sp/dropping-buffer 64)
session-id (::session-id @wsp) :xf (comp (remove #(= (:session-id %) session-id))
output-ch (::ws/output-ch @wsp) (map #(assoc % :subs-id file-id))))]
prev-subs (::file-subscription @wsp)
xform (comp (remove #(= (:session-id %) session-id))
(map #(assoc % :subs-id file-id)))
channel (a/chan (a/dropping-buffer 64) xform)]
(l/trace :fn "handle-message" (let [subs {:file-id file-id :channel fch :topic file-id}]
:event :subscribe-file (swap! state assoc ::file-subscription subs))
:file-id file-id
:conn-id conn-id)
(let [state {:file-id file-id :channel channel :topic file-id}] ;; Close previous subscription if exists
(swap! wsp assoc ::file-subscription state)) (when-let [ch (:channel psub)]
(sp/close! ch)
(mbus/purge! msgbus [ch]))
(a/go (sp/go-loop []
;; Close previous subscription if exists (when-let [{:keys [type] :as message} (sp/take! fch)]
(when-let [channel (:channel prev-subs)] (sp/put! output-ch message)
(a/close! channel) (when (or (= :join-file type)
(a/<! (mbus/purge! msgbus channel)))) (= :leave-file type)
(= :disconnect type))
;; Message forwarding (let [message {:type :presence
(a/go :file-id file-id
(loop [] :session-id session-id
(when-let [{:keys [type] :as message} (a/<! channel)]
(when (or (= :join-file type)
(= :leave-file type)
(= :disconnect type))
(let [message {:type :presence
:file-id file-id
:session-id session-id
:profile-id profile-id}] :profile-id profile-id}]
(a/<! (mbus/pub! msgbus :topic file-id :message message)))) (mbus/pub! msgbus
(a/>! output-ch message) :topic file-id
(recur)))) :message message)))
(recur)))
(a/go ;; Subscribe to file topic
;; Subscribe to file topic (mbus/sub! msgbus :topic file-id :chan fch)
(a/<! (mbus/sub! msgbus :topic file-id :chan channel))
;; Notifify the rest of participants of the new connection. ;; Notifify the rest of participants of the new connection.
(let [message {:type :join-file (let [message {:type :join-file
:file-id file-id :file-id file-id
:subs-id file-id :subs-id file-id
:session-id session-id :session-id session-id
:profile-id profile-id}] :profile-id profile-id}]
(a/<! (mbus/pub! msgbus :topic file-id :message message)))))) (mbus/pub! msgbus :topic file-id :message message))))
(defmethod handle-message :unsubscribe-file (defmethod handle-message :unsubscribe-file
[cfg wsp {:keys [file-id] :as params}] [{:keys [::mbus/msgbus]} {:keys [::ws/id ::ws/state ::session-id ::profile-id]} {:keys [file-id] :as params}]
(let [msgbus (::mbus/msgbus cfg) (l/trace :fn "handle-message" :event "unsubscribe-file" :file-id file-id :conn-id id)
conn-id (::ws/id @wsp)
session-id (::session-id @wsp)
profile-id (::profile-id @wsp)
subs (::file-subscription @wsp)
message {:type :leave-file (let [subs (::file-subscription @state)
:file-id file-id message {:type :leave-file
:session-id session-id :file-id file-id
:profile-id profile-id}] :session-id session-id
:profile-id profile-id}]
(l/trace :fn "handle-message" (when (= (:file-id subs) file-id)
:event :unsubscribe-file (mbus/pub! msgbus :topic file-id :message message)
:file-id file-id (let [ch (:channel subs)]
:conn-id conn-id) (sp/close! ch)
(mbus/purge! msgbus [ch])))))
(a/go
(when (= (:file-id subs) file-id)
(let [channel (:channel subs)]
(a/close! channel)
(a/<! (mbus/purge! msgbus channel))
(a/<! (mbus/pub! msgbus :topic file-id :message message)))))))
(defmethod handle-message :keepalive (defmethod handle-message :keepalive
[_ _ _] [_ _ _]
(l/trace :fn "handle-message" :event :keepalive) (l/trace :fn "handle-message" :event :keepalive))
(a/go :nothing))
(defmethod handle-message :broadcast
[{:keys [::mbus/msgbus]} {:keys [::ws/id ::session-id ::profile-id]} message]
(l/trace :fn "handle-message" :event "broadcast" :conn-id id)
(let [message (-> message
(assoc :subs-id profile-id)
(assoc :profile-id profile-id)
(assoc :session-id session-id))]
(mbus/pub! msgbus :topic profile-id :message message)))
(defmethod handle-message :pointer-update (defmethod handle-message :pointer-update
[cfg wsp {:keys [file-id] :as message}] [{:keys [::mbus/msgbus]} {:keys [::ws/state ::session-id ::profile-id]} {:keys [file-id] :as message}]
(let [msgbus (::mbus/msgbus cfg) (when (::file-subscription @state)
profile-id (::profile-id @wsp) (let [message (-> message
session-id (::session-id @wsp) (assoc :subs-id file-id)
subs (::file-subscription @wsp) (assoc :profile-id profile-id)
message (-> message (assoc :session-id session-id))]
(assoc :subs-id file-id) (mbus/pub! msgbus :topic file-id :message message))))
(assoc :profile-id profile-id)
(assoc :session-id session-id))]
(a/go
;; Only allow receive pointer updates when active subscription
(when subs
(a/<! (mbus/pub! msgbus :topic file-id :message message))))))
(defmethod handle-message :default (defmethod handle-message :default
[_ wsp message] [_ {:keys [::ws/id]} message]
(let [conn-id (::ws/id @wsp)] (l/warn :hint "received unexpected message"
(l/warn :hint "received unexpected message" :message message
:message message :conn-id id))
:conn-id conn-id)
(a/go :none)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HTTP HANDLER ;; HTTP HANDLER
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- on-connect
[{:keys [::mtx/metrics]} {:keys [::ws/id] :as wsp}]
(let [created-at (dt/now)]
(l/trace :fn "on-connect" :conn-id id)
(swap! state assoc id wsp)
(mtx/run! metrics
:id :websocket-active-connections
:inc 1)
(assoc wsp ::ws/on-disconnect
(fn []
(l/trace :fn "on-disconnect" :conn-id id)
(swap! state dissoc id)
(mtx/run! metrics :id :websocket-active-connections :dec 1)
(mtx/run! metrics
:id :websocket-session-timing
:val (/ (inst-ms (dt/diff created-at (dt/now))) 1000.0))))))
(defn- on-rcv-message
[{:keys [::mtx/metrics ::profile-id ::session-id]} message]
(mtx/run! metrics
:id :websocket-messages-total
:labels recv-labels
:inc 1)
(assoc message :profile-id profile-id :session-id session-id))
(defn- on-snd-message
[{:keys [::mtx/metrics]} message]
(mtx/run! metrics
:id :websocket-messages-total
:labels send-labels
:inc 1)
message)
(s/def ::session-id ::us/uuid) (s/def ::session-id ::us/uuid)
(s/def ::handler-params (s/def ::handler-params
(s/keys :req-un [::session-id])) (s/keys :req-un [::session-id]))
(defn- http-handler (defn- http-handler
[cfg {:keys [params ::session/profile-id] :as request} respond raise] [cfg {:keys [params ::session/profile-id] :as request}]
(let [{:keys [session-id]} (us/conform ::handler-params params)] (let [{:keys [session-id]} (us/conform ::handler-params params)]
(cond (cond
(not profile-id) (not profile-id)
(raise (ex/error :type :authentication (ex/raise :type :authentication
:hint "Authentication required.")) :hint "Authentication required.")
(not (yws/upgrade-request? request)) (not (yws/upgrade-request? request))
(raise (ex/error :type :validation (ex/raise :type :validation
:code :websocket-request-expected :code :websocket-request-expected
:hint "this endpoint only accepts websocket connections")) :hint "this endpoint only accepts websocket connections")
:else :else
(do (do
(l/trace :hint "websocket request" :profile-id profile-id :session-id session-id) (l/trace :hint "websocket request" :profile-id profile-id :session-id session-id)
(->> (ws/handler (->> (ws/handler
::ws/on-rcv-message (partial on-rcv-message cfg) ::ws/on-rcv-message (partial on-rcv-message cfg)
::ws/on-snd-message (partial on-snd-message cfg) ::ws/on-snd-message (partial on-snd-message cfg)
@ -341,8 +301,7 @@
::ws/handler (partial handle-message cfg) ::ws/handler (partial handle-message cfg)
::profile-id profile-id ::profile-id profile-id
::session-id session-id) ::session-id session-id)
(yws/upgrade request) (yws/upgrade request))))))
(respond))))))
(defmethod ig/pre-init-spec ::routes [_] (defmethod ig/pre-init-spec ::routes [_]
(s/keys :req [::mbus/msgbus (s/keys :req [::mbus/msgbus

View File

@ -16,13 +16,15 @@
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.config :as cf] [app.config :as cf]
[app.db :as db] [app.db :as db]
[app.http.client :as http] [app.http :as-alias http]
[app.http.client :as http.client]
[app.loggers.audit.tasks :as-alias tasks] [app.loggers.audit.tasks :as-alias tasks]
[app.loggers.webhooks :as-alias webhooks] [app.loggers.webhooks :as-alias webhooks]
[app.main :as-alias main] [app.main :as-alias main]
[app.rpc :as-alias rpc] [app.rpc :as-alias rpc]
[app.tokens :as tokens] [app.tokens :as tokens]
[app.util.retry :as rtry] [app.util.retry :as rtry]
[app.util.services :as-alias sv]
[app.util.time :as dt] [app.util.time :as dt]
[app.worker :as wrk] [app.worker :as wrk]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
@ -92,6 +94,15 @@
;; --- SPECS ;; --- SPECS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; COLLECTOR
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Defines a service that collects the audit/activity log using
;; internal database. Later this audit log can be transferred to
;; an external storage and data cleared.
(s/def ::profile-id ::us/uuid) (s/def ::profile-id ::us/uuid)
(s/def ::name ::us/string) (s/def ::name ::us/string)
(s/def ::type ::us/string) (s/def ::type ::us/string)
@ -104,20 +115,13 @@
(s/or :fn fn? :str string? :kw keyword?)) (s/or :fn fn? :str string? :kw keyword?))
(s/def ::event (s/def ::event
(s/keys :req-un [::type ::name ::profile-id] (s/keys :req [::type ::name ::profile-id]
:opt-un [::ip-addr ::props] :opt [::ip-addr
:opt [::webhooks/event? ::props
::webhooks/event?
::webhooks/batch-timeout ::webhooks/batch-timeout
::webhooks/batch-key])) ::webhooks/batch-key]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; COLLECTOR
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Defines a service that collects the audit/activity log using
;; internal database. Later this audit log can be transferred to
;; an external storage and data cleared.
(s/def ::collector (s/def ::collector
(s/keys :req [::wrk/executor ::db/pool])) (s/keys :req [::wrk/executor ::db/pool]))
@ -133,15 +137,58 @@
:else :else
cfg)) cfg))
(defn prepare-event
[cfg mdata params result]
(let [resultm (meta result)
request (::http/request params)
profile-id (or (::profile-id resultm)
(:profile-id result)
(::rpc/profile-id params)
uuid/zero)
props (-> (or (::replace-props resultm)
(-> params
(merge (::props resultm))
(dissoc :profile-id)
(dissoc :type)))
(clean-props))]
{::type (or (::type resultm)
(::rpc/type cfg))
::name (or (::name resultm)
(::sv/name mdata))
::profile-id profile-id
::ip-addr (some-> request parse-client-ip)
::props props
;; NOTE: for batch-key lookup we need the params as-is
;; because the rpc api does not need to know the
;; audit/webhook specific object layout.
::rpc/params (dissoc params ::http/request)
::webhooks/batch-key
(or (::webhooks/batch-key mdata)
(::webhooks/batch-key resultm))
::webhooks/batch-timeout
(or (::webhooks/batch-timeout mdata)
(::webhooks/batch-timeout resultm))
::webhooks/event?
(or (::webhooks/event? mdata)
(::webhooks/event? resultm)
false)}))
(defn- handle-event! (defn- handle-event!
[conn-or-pool event] [conn-or-pool event]
(us/verify! ::event event) (us/verify! ::event event)
(let [params {:id (uuid/next) (let [params {:id (uuid/next)
:name (:name event) :name (::name event)
:type (:type event) :type (::type event)
:profile-id (:profile-id event) :profile-id (::profile-id event)
:ip-addr (:ip-addr event) :ip-addr (::ip-addr event)
:props (:props event)}] :props (::props event)}]
(when (contains? cf/flags :audit-log) (when (contains? cf/flags :audit-log)
;; NOTE: this operation may cause primary key conflicts on inserts ;; NOTE: this operation may cause primary key conflicts on inserts
@ -207,7 +254,7 @@
(s/def ::tasks/uri ::us/string) (s/def ::tasks/uri ::us/string)
(defmethod ig/pre-init-spec ::tasks/archive-task [_] (defmethod ig/pre-init-spec ::tasks/archive-task [_]
(s/keys :req [::db/pool ::main/props ::http/client])) (s/keys :req [::db/pool ::main/props ::http.client/client]))
(defmethod ig/init-key ::tasks/archive (defmethod ig/init-key ::tasks/archive
[_ cfg] [_ cfg]
@ -231,7 +278,7 @@
(if n (if n
(do (do
(px/sleep 100) (px/sleep 100)
(recur (+ total n))) (recur (+ total ^long n)))
(when (pos? total) (when (pos? total)
(l/debug :hint "events archived" :total total))))))))) (l/debug :hint "events archived" :total total)))))))))
@ -281,7 +328,7 @@
:method :post :method :post
:headers headers :headers headers
:body body} :body body}
resp (http/req! cfg params {:sync? true})] resp (http.client/req! cfg params {:sync? true})]
(if (= (:status resp) 204) (if (= (:status resp) 204)
true true
(do (do

View File

@ -62,6 +62,11 @@
(dissoc ::s/problems ::s/value ::s/spec :hint) (dissoc ::s/problems ::s/value ::s/spec :hint)
(pp/pprint-str :width 200))}))) (pp/pprint-str :width 200))})))
(defn error-record?
[{:keys [::l/level ::l/cause]}]
(and (= :error level)
(ex/exception? cause)))
(defn- handle-event (defn- handle-event
[{:keys [::db/pool]} {:keys [::l/id] :as record}] [{:keys [::db/pool]} {:keys [::l/id] :as record}]
(try (try
@ -74,20 +79,16 @@
(catch Throwable cause (catch Throwable cause
(l/warn :hint "unexpected exception on database error logger" :cause cause)))) (l/warn :hint "unexpected exception on database error logger" :cause cause))))
(defn error-record?
[{:keys [::l/level ::l/cause]}]
(and (= :error level)
(ex/exception? cause)))
(defmethod ig/pre-init-spec ::reporter [_] (defmethod ig/pre-init-spec ::reporter [_]
(s/keys :req [::db/pool])) (s/keys :req [::db/pool]))
(defmethod ig/init-key ::reporter (defmethod ig/init-key ::reporter
[_ cfg] [_ cfg]
(let [input (sp/chan (sp/sliding-buffer 32) (filter error-record?))] (let [input (sp/chan :buf (sp/sliding-buffer 32)
:xf (filter error-record?))]
(add-watch l/log-record ::reporter #(sp/put! input %4)) (add-watch l/log-record ::reporter #(sp/put! input %4))
(px/thread
{:name "penpot/database-reporter" :virtual true} (px/thread {:name "penpot/database-reporter" :virtual true}
(l/info :hint "initializing database error persistence") (l/info :hint "initializing database error persistence")
(try (try
(loop [] (loop []

View File

@ -77,7 +77,8 @@
{:name "penpot/mattermost-reporter" {:name "penpot/mattermost-reporter"
:virtual true} :virtual true}
(l/info :hint "initializing error reporter" :uri uri) (l/info :hint "initializing error reporter" :uri uri)
(let [input (sp/chan (sp/sliding-buffer 128) (filter ldb/error-record?))] (let [input (sp/chan :buf (sp/sliding-buffer 128)
:xf (filter ldb/error-record?))]
(add-watch l/log-record ::reporter #(sp/put! input %4)) (add-watch l/log-record ::reporter #(sp/put! input %4))
(try (try
(loop [] (loop []

View File

@ -14,7 +14,6 @@
[app.db :as-alias db] [app.db :as-alias db]
[app.email :as-alias email] [app.email :as-alias email]
[app.http :as-alias http] [app.http :as-alias http]
[app.http.access-token :as-alias actoken]
[app.http.assets :as-alias http.assets] [app.http.assets :as-alias http.assets]
[app.http.awsns :as http.awsns] [app.http.awsns :as http.awsns]
[app.http.client :as-alias http.client] [app.http.client :as-alias http.client]
@ -37,7 +36,8 @@
[app.util.time :as dt] [app.util.time :as dt]
[app.worker :as-alias wrk] [app.worker :as-alias wrk]
[cuerdas.core :as str] [cuerdas.core :as str]
[integrant.core :as ig]) [integrant.core :as ig]
[promesa.exec :as px])
(:gen-class)) (:gen-class))
(def default-metrics (def default-metrics
@ -102,15 +102,15 @@
::mdef/labels ["name"] ::mdef/labels ["name"]
::mdef/type :summary} ::mdef/type :summary}
:rpc-climit-queue-size :rpc-climit-queue
{::mdef/name "penpot_rpc_climit_queue_size" {::mdef/name "penpot_rpc_climit_queue"
::mdef/help "Current number of queued submissions on the CLIMIT." ::mdef/help "Current number of queued submissions."
::mdef/labels ["name"] ::mdef/labels ["name"]
::mdef/type :gauge} ::mdef/type :gauge}
:rpc-climit-concurrency :rpc-climit-permits
{::mdef/name "penpot_rpc_climit_concurrency" {::mdef/name "penpot_rpc_climit_permits"
::mdef/help "Current number of used concurrency capacity on the CLIMIT" ::mdef/help "Current number of available permits"
::mdef/labels ["name"] ::mdef/labels ["name"]
::mdef/type :gauge} ::mdef/type :gauge}
@ -174,10 +174,8 @@
;; Default thread pool for IO operations ;; Default thread pool for IO operations
::wrk/executor ::wrk/executor
{::wrk/parallelism (cf/get :default-executor-parallelism 100)} {::wrk/parallelism (cf/get :default-executor-parallelism
(+ 3 (* (px/get-available-processors) 3)))}
::wrk/scheduled-executor
{::wrk/parallelism (cf/get :scheduled-executor-parallelism 20)}
::wrk/monitor ::wrk/monitor
{::mtx/metrics (ig/ref ::mtx/metrics) {::mtx/metrics (ig/ref ::mtx/metrics)
@ -194,17 +192,16 @@
{::mtx/metrics (ig/ref ::mtx/metrics)} {::mtx/metrics (ig/ref ::mtx/metrics)}
::rds/redis ::rds/redis
{::rds/uri (cf/get :redis-uri) {::rds/uri (cf/get :redis-uri)
::mtx/metrics (ig/ref ::mtx/metrics)} ::mtx/metrics (ig/ref ::mtx/metrics)
::wrk/executor (ig/ref ::wrk/executor)}
::mbus/msgbus ::mbus/msgbus
{:backend (cf/get :msgbus-backend :redis) {::wrk/executor (ig/ref ::wrk/executor)
:executor (ig/ref ::wrk/executor) ::rds/redis (ig/ref ::rds/redis)}
:redis (ig/ref ::rds/redis)}
:app.storage.tmp/cleaner :app.storage.tmp/cleaner
{::wrk/executor (ig/ref ::wrk/executor) {::wrk/executor (ig/ref ::wrk/executor)}
::wrk/scheduled-executor (ig/ref ::wrk/scheduled-executor)}
::sto/gc-deleted-task ::sto/gc-deleted-task
{::db/pool (ig/ref ::db/pool) {::db/pool (ig/ref ::db/pool)
@ -217,14 +214,7 @@
{::wrk/executor (ig/ref ::wrk/executor)} {::wrk/executor (ig/ref ::wrk/executor)}
::session/manager ::session/manager
{::db/pool (ig/ref ::db/pool) {::db/pool (ig/ref ::db/pool)}
::wrk/executor (ig/ref ::wrk/executor)
::props (ig/ref :app.setup/props)}
::actoken/manager
{::db/pool (ig/ref ::db/pool)
::wrk/executor (ig/ref ::wrk/executor)
::props (ig/ref :app.setup/props)}
::session.tasks/gc ::session.tasks/gc
{::db/pool (ig/ref ::db/pool)} {::db/pool (ig/ref ::db/pool)}
@ -239,8 +229,7 @@
{::http/port (cf/get :http-server-port) {::http/port (cf/get :http-server-port)
::http/host (cf/get :http-server-host) ::http/host (cf/get :http-server-host)
::http/router (ig/ref ::http/router) ::http/router (ig/ref ::http/router)
::http/metrics (ig/ref ::mtx/metrics) ::wrk/executor (ig/ref ::wrk/executor)
::http/executor (ig/ref ::wrk/executor)
::http/io-threads (cf/get :http-server-io-threads) ::http/io-threads (cf/get :http-server-io-threads)
::http/max-body-size (cf/get :http-server-max-body-size) ::http/max-body-size (cf/get :http-server-max-body-size)
::http/max-multipart-body-size (cf/get :http-server-max-multipart-body-size)} ::http/max-multipart-body-size (cf/get :http-server-max-multipart-body-size)}
@ -275,7 +264,6 @@
{::http.client/client (ig/ref ::http.client/client) {::http.client/client (ig/ref ::http.client/client)
::db/pool (ig/ref ::db/pool) ::db/pool (ig/ref ::db/pool)
::props (ig/ref :app.setup/props) ::props (ig/ref :app.setup/props)
::wrk/executor (ig/ref ::wrk/executor)
::oidc/providers {:google (ig/ref ::oidc.providers/google) ::oidc/providers {:google (ig/ref ::oidc.providers/google)
:github (ig/ref ::oidc.providers/github) :github (ig/ref ::oidc.providers/github)
:gitlab (ig/ref ::oidc.providers/gitlab) :gitlab (ig/ref ::oidc.providers/gitlab)
@ -284,8 +272,6 @@
:app.http/router :app.http/router
{::session/manager (ig/ref ::session/manager) {::session/manager (ig/ref ::session/manager)
::actoken/manager (ig/ref ::actoken/manager)
::wrk/executor (ig/ref ::wrk/executor)
::db/pool (ig/ref ::db/pool) ::db/pool (ig/ref ::db/pool)
::rpc/routes (ig/ref ::rpc/routes) ::rpc/routes (ig/ref ::rpc/routes)
::rpc.doc/routes (ig/ref ::rpc.doc/routes) ::rpc.doc/routes (ig/ref ::rpc.doc/routes)
@ -302,10 +288,10 @@
::wrk/executor (ig/ref ::wrk/executor) ::wrk/executor (ig/ref ::wrk/executor)
::session/manager (ig/ref ::session/manager)} ::session/manager (ig/ref ::session/manager)}
:app.http.websocket/routes ::http.ws/routes
{::db/pool (ig/ref ::db/pool) {::db/pool (ig/ref ::db/pool)
::mtx/metrics (ig/ref ::mtx/metrics) ::mtx/metrics (ig/ref ::mtx/metrics)
::mbus/msgbus (ig/ref :app.msgbus/msgbus) ::mbus/msgbus (ig/ref ::mbus/msgbus)
::session/manager (ig/ref ::session/manager)} ::session/manager (ig/ref ::session/manager)}
:app.http.assets/routes :app.http.assets/routes
@ -320,8 +306,7 @@
::wrk/executor (ig/ref ::wrk/executor)} ::wrk/executor (ig/ref ::wrk/executor)}
:app.rpc/rlimit :app.rpc/rlimit
{::wrk/executor (ig/ref ::wrk/executor) {::wrk/executor (ig/ref ::wrk/executor)}
::wrk/scheduled-executor (ig/ref ::wrk/scheduled-executor)}
:app.rpc/methods :app.rpc/methods
{::http.client/client (ig/ref ::http.client/client) {::http.client/client (ig/ref ::http.client/client)
@ -351,7 +336,6 @@
::db/pool (ig/ref ::db/pool) ::db/pool (ig/ref ::db/pool)
::wrk/executor (ig/ref ::wrk/executor) ::wrk/executor (ig/ref ::wrk/executor)
::session/manager (ig/ref ::session/manager) ::session/manager (ig/ref ::session/manager)
::actoken/manager (ig/ref ::actoken/manager)
::props (ig/ref :app.setup/props)} ::props (ig/ref :app.setup/props)}
::wrk/registry ::wrk/registry
@ -467,8 +451,7 @@
(def worker-config (def worker-config
{::wrk/cron {::wrk/cron
{::wrk/scheduled-executor (ig/ref ::wrk/scheduled-executor) {::wrk/registry (ig/ref ::wrk/registry)
::wrk/registry (ig/ref ::wrk/registry)
::db/pool (ig/ref ::db/pool) ::db/pool (ig/ref ::db/pool)
::wrk/entries ::wrk/entries
[{:cron #app/cron "0 0 * * * ?" ;; hourly [{:cron #app/cron "0 0 * * * ?" ;; hourly

View File

@ -16,6 +16,7 @@
[app.storage :as-alias sto] [app.storage :as-alias sto]
[app.storage.tmp :as tmp] [app.storage.tmp :as tmp]
[app.util.svg :as svg] [app.util.svg :as svg]
[app.util.time :as dt]
[buddy.core.bytes :as bb] [buddy.core.bytes :as bb]
[buddy.core.codecs :as bc] [buddy.core.codecs :as bc]
[clojure.java.shell :as sh] [clojure.java.shell :as sh]
@ -168,7 +169,7 @@
(ex/raise :type :validation (ex/raise :type :validation
:code :invalid-svg-file :code :invalid-svg-file
:hint "uploaded svg does not provides dimensions")) :hint "uploaded svg does not provides dimensions"))
(merge input info)) (merge input info {:ts (dt/now)}))
(let [instance (Info. (str path)) (let [instance (Info. (str path))
mtype' (.getProperty instance "Mime type")] mtype' (.getProperty instance "Mime type")]
@ -183,7 +184,8 @@
;; any frame. ;; any frame.
(assoc input (assoc input
:width (.getPageWidth instance) :width (.getPageWidth instance)
:height (.getPageHeight instance)))))) :height (.getPageHeight instance)
:ts (dt/now))))))
(defmethod process-error org.im4java.core.InfoException (defmethod process-error org.im4java.core.InfoException
[error] [error]

View File

@ -8,20 +8,18 @@
"The msgbus abstraction implemented using redis as underlying backend." "The msgbus abstraction implemented using redis as underlying backend."
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.logging :as l] [app.common.logging :as l]
[app.common.spec :as us] [app.common.spec :as us]
[app.common.transit :as t] [app.common.transit :as t]
[app.config :as cfg] [app.config :as cfg]
[app.redis :as redis] [app.redis :as rds]
[app.util.async :as aa]
[app.util.time :as dt] [app.util.time :as dt]
[app.worker :as wrk] [app.worker :as wrk]
[clojure.core.async :as a]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[integrant.core :as ig] [integrant.core :as ig]
[promesa.core :as p] [promesa.core :as p]
[promesa.exec :as px])) [promesa.exec :as px]
[promesa.exec.csp :as sp]))
(set! *warn-on-reflection* true) (set! *warn-on-reflection* true)
@ -34,132 +32,116 @@
(def ^:private xform-prefix-topic (def ^:private xform-prefix-topic
(map (fn [obj] (update obj :topic prefix-topic)))) (map (fn [obj] (update obj :topic prefix-topic))))
(declare ^:private redis-connect) (declare ^:private redis-pub!)
(declare ^:private redis-disconnect) (declare ^:private redis-sub!)
(declare ^:private redis-pub) (declare ^:private redis-unsub!)
(declare ^:private redis-sub)
(declare ^:private redis-unsub)
(declare ^:private start-io-loop!) (declare ^:private start-io-loop!)
(declare ^:private subscribe-to-topics) (declare ^:private subscribe-to-topics)
(declare ^:private unsubscribe-channels) (declare ^:private unsubscribe-channels)
(defmethod ig/prep-key ::msgbus (s/def ::cmd-ch sp/chan?)
[_ cfg] (s/def ::rcv-ch sp/chan?)
(merge {:buffer-size 128 (s/def ::pub-ch sp/chan?)
:timeout (dt/duration {:seconds 30})}
(d/without-nils cfg)))
(s/def ::cmd-ch ::aa/channel)
(s/def ::rcv-ch ::aa/channel)
(s/def ::pub-ch ::aa/channel)
(s/def ::state ::us/agent) (s/def ::state ::us/agent)
(s/def ::pconn ::redis/connection-holder) (s/def ::pconn ::rds/connection-holder)
(s/def ::sconn ::redis/connection-holder) (s/def ::sconn ::rds/connection-holder)
(s/def ::msgbus (s/def ::msgbus
(s/keys :req [::cmd-ch ::rcv-ch ::pub-ch ::state ::pconn ::sconn ::wrk/executor])) (s/keys :req [::cmd-ch ::rcv-ch ::pub-ch ::state ::pconn ::sconn ::wrk/executor]))
(s/def ::buffer-size ::us/integer)
(defmethod ig/pre-init-spec ::msgbus [_] (defmethod ig/pre-init-spec ::msgbus [_]
(s/keys :req-un [::buffer-size ::redis/timeout ::redis/redis ::wrk/executor])) (s/keys :req [::rds/redis ::wrk/executor]))
(defmethod ig/prep-key ::msgbus
[_ cfg]
(-> cfg
(assoc ::buffer-size 128)
(assoc ::timeout (dt/duration {:seconds 30}))))
(defmethod ig/init-key ::msgbus (defmethod ig/init-key ::msgbus
[_ {:keys [buffer-size executor] :as cfg}] [_ {:keys [::buffer-size ::wrk/executor ::timeout ::rds/redis] :as cfg}]
(l/info :hint "initialize msgbus" :buffer-size buffer-size) (l/info :hint "initialize msgbus" :buffer-size buffer-size)
(let [cmd-ch (a/chan buffer-size) (let [cmd-ch (sp/chan :buf buffer-size)
rcv-ch (a/chan (a/dropping-buffer buffer-size)) rcv-ch (sp/chan :buf (sp/dropping-buffer buffer-size))
pub-ch (a/chan (a/dropping-buffer buffer-size) xform-prefix-topic) pub-ch (sp/chan :buf (sp/dropping-buffer buffer-size)
:xf xform-prefix-topic)
state (agent {}) state (agent {})
msgbus (-> (redis-connect cfg)
pconn (rds/connect redis :timeout timeout)
sconn (rds/connect redis :type :pubsub :timeout timeout)
msgbus (-> cfg
(assoc ::pconn pconn)
(assoc ::sconn sconn)
(assoc ::cmd-ch cmd-ch) (assoc ::cmd-ch cmd-ch)
(assoc ::rcv-ch rcv-ch) (assoc ::rcv-ch rcv-ch)
(assoc ::pub-ch pub-ch) (assoc ::pub-ch pub-ch)
(assoc ::state state) (assoc ::state state)
(assoc ::wrk/executor executor))] (assoc ::wrk/executor executor))]
(us/verify! ::msgbus msgbus)
(set-error-handler! state #(l/error :cause % :hint "unexpected error on agent" ::l/sync? true)) (set-error-handler! state #(l/error :cause % :hint "unexpected error on agent" ::l/sync? true))
(set-error-mode! state :continue) (set-error-mode! state :continue)
(start-io-loop! msgbus)
msgbus)) (assoc msgbus ::io-thr (start-io-loop! msgbus))))
(defn sub!
[{:keys [::state ::wrk/executor] :as cfg} & {:keys [topic topics chan]}]
(let [done-ch (a/chan)
topics (into [] (map prefix-topic) (if topic [topic] topics))]
(l/debug :hint "subscribe" :topics topics)
(send-via executor state subscribe-to-topics cfg topics chan done-ch)
done-ch))
(defn pub!
[{::keys [pub-ch]} & {:as params}]
(a/go
(a/>! pub-ch params)))
(defn purge!
[{:keys [::state ::wrk/executor] :as msgbus} chans]
(l/trace :hint "purge" :chans (count chans))
(let [done-ch (a/chan)]
(send-via executor state unsubscribe-channels msgbus chans done-ch)
done-ch))
(defmethod ig/halt-key! ::msgbus (defmethod ig/halt-key! ::msgbus
[_ msgbus] [_ msgbus]
(redis-disconnect msgbus) (px/interrupt! (::io-thr msgbus))
(a/close! (::cmd-ch msgbus)) (sp/close! (::cmd-ch msgbus))
(a/close! (::rcv-ch msgbus)) (sp/close! (::rcv-ch msgbus))
(a/close! (::pub-ch msgbus))) (sp/close! (::pub-ch msgbus))
(d/close! (::pconn msgbus))
(d/close! (::sconn msgbus)))
(defn sub!
[{:keys [::state ::wrk/executor] :as cfg} & {:keys [topic topics chan]}]
(let [topics (into [] (map prefix-topic) (if topic [topic] topics))]
(l/debug :hint "subscribe" :topics topics :chan (hash chan))
(send-via executor state subscribe-to-topics cfg topics chan)
nil))
(defn pub!
[{::keys [pub-ch]} & {:as params}]
(sp/put! pub-ch params))
(defn purge!
[{:keys [::state ::wrk/executor] :as msgbus} chans]
(l/debug :hint "purge" :chans (count chans))
(send-via executor state unsubscribe-channels msgbus chans)
nil)
;; --- IMPL ;; --- IMPL
(defn- redis-connect
[{:keys [timeout redis] :as cfg}]
(let [pconn (redis/connect redis :timeout timeout)
sconn (redis/connect redis :type :pubsub :timeout timeout)]
{::pconn pconn
::sconn sconn}))
(defn- redis-disconnect
[{:keys [::pconn ::sconn] :as cfg}]
(d/close! pconn)
(d/close! sconn))
(defn- conj-subscription (defn- conj-subscription
"A low level function that is responsible to create on-demand "A low level function that is responsible to create on-demand
subscriptions on redis. It reuses the same subscription if it is subscriptions on redis. It reuses the same subscription if it is
already established. Intended to be executed in agent." already established."
[nsubs cfg topic chan] [nsubs cfg topic chan]
(let [nsubs (if (nil? nsubs) #{chan} (conj nsubs chan))] (let [nsubs (if (nil? nsubs) #{chan} (conj nsubs chan))]
(when (= 1 (count nsubs)) (when (= 1 (count nsubs))
(l/trace :hint "open subscription" :topic topic ::l/sync? true) (l/trace :hint "open subscription" :topic topic ::l/sync? true)
(redis-sub cfg topic)) (redis-sub! cfg topic))
nsubs)) nsubs))
(defn- disj-subscription (defn- disj-subscription
"A low level function responsible on removing subscriptions. The "A low level function responsible on removing subscriptions. The
subscription is truly removed from redis once no single local subscription is truly removed from redis once no single local
subscription is look for it. Intended to be executed in agent." subscription is look for it."
[nsubs cfg topic chan] [nsubs cfg topic chan]
(let [nsubs (disj nsubs chan)] (let [nsubs (disj nsubs chan)]
(when (empty? nsubs) (when (empty? nsubs)
(l/trace :hint "close subscription" :topic topic ::l/sync? true) (l/trace :hint "close subscription" :topic topic ::l/sync? true)
(redis-unsub cfg topic)) (redis-unsub! cfg topic))
nsubs)) nsubs))
(defn- subscribe-to-topics (defn- subscribe-to-topics
"Function responsible to attach local subscription to the "Function responsible to attach local subscription to the state."
state. Intended to be used in agent." [state cfg topics chan]
[state cfg topics chan done-ch] (let [state (update state :chans assoc chan topics)]
(aa/with-closing done-ch (reduce (fn [state topic]
(let [state (update state :chans assoc chan topics)] (update-in state [:topics topic] conj-subscription cfg topic chan))
(reduce (fn [state topic] state
(update-in state [:topics topic] conj-subscription cfg topic chan)) topics)))
state
topics))))
(defn- unsubscribe-single-channel (defn- unsubscribe-channel
"Auxiliary function responsible on removing a single local "Auxiliary function responsible on removing a single local
subscription from the state." subscription from the state."
[state cfg chan] [state cfg chan]
@ -174,87 +156,113 @@
"Function responsible from detach from state a seq of channels, "Function responsible from detach from state a seq of channels,
useful when client disconnects or in-bulk unsubscribe useful when client disconnects or in-bulk unsubscribe
operations. Intended to be executed in agent." operations. Intended to be executed in agent."
[state cfg channels done-ch] [state cfg channels]
(aa/with-closing done-ch (reduce #(unsubscribe-channel %1 cfg %2) state channels))
(reduce #(unsubscribe-single-channel %1 cfg %2) state channels)))
(defn- create-listener (defn- create-listener
[rcv-ch] [rcv-ch]
(redis/pubsub-listener (rds/pubsub-listener
:on-message (fn [_ topic message] :on-message (fn [_ topic message]
;; There are no back pressure, so we use a slidding ;; There are no back pressure, so we use a slidding
;; buffer for cases when the pubsub broker sends ;; buffer for cases when the pubsub broker sends
;; more messages that we can process. ;; more messages that we can process.
(let [val {:topic topic :message (t/decode message)}] (let [val {:topic topic :message (t/decode message)}]
(when-not (a/offer! rcv-ch val) (when-not (sp/offer! rcv-ch val)
(l/warn :msg "dropping message on subscription loop")))))) (l/warn :msg "dropping message on subscription loop"))))))
(defn- process-input!
[{: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))))
(defn start-io-loop! (defn start-io-loop!
[{:keys [::sconn ::rcv-ch ::pub-ch ::state ::wrk/executor] :as cfg}] [{:keys [::sconn ::rcv-ch ::pub-ch ::state ::wrk/executor] :as cfg}]
(redis/add-listener! sconn (create-listener rcv-ch)) (rds/add-listener! sconn (create-listener rcv-ch))
(letfn [(send-to-topic [topic message]
(a/go-loop [chans (seq (get-in @state [:topics topic]))
closed #{}]
(if-let [ch (first chans)]
(if (a/>! ch message)
(recur (rest chans) closed)
(recur (rest chans) (conj closed ch)))
(seq closed))))
(process-incoming [{:keys [topic message]}] (px/thread
(a/go {:name "penpot/msgbus/io-loop"
(when-let [closed (a/<! (send-to-topic topic message))] :virtual true}
(send-via executor state unsubscribe-channels cfg closed nil)))) (try
]
(px/thread
{:name "penpot/msgbus-io-loop"}
(loop [] (loop []
(let [[val port] (a/alts!! [pub-ch rcv-ch])] (let [timeout-ch (sp/timeout-chan 1000)
[val port] (sp/alts! [timeout-ch pub-ch rcv-ch])]
(cond (cond
(nil? val) (identical? port timeout-ch)
(do (let [closed (->> (:chans @state)
(l/trace :hint "stopping io-loop, nil received") (map key)
(send-via executor state (fn [state] (filter sp/closed?))]
(->> (vals state) (when (seq closed)
(mapcat identity) (send-via executor state unsubscribe-channels cfg closed)
(filter some?) (l/debug :hint "proactively purge channels" :count (count closed)))
(run! a/close!))
nil)))
(= port rcv-ch)
(do
(a/<!! (process-incoming val))
(recur)) (recur))
(= port pub-ch) (nil? val)
(let [result (a/<!! (redis-pub cfg val))] (throw (InterruptedException. "internally interrupted"))
(when (ex/exception? result)
(l/error :hint "unexpected error on publishing"
:message val
:cause result))
(recur))))))))
(defn- redis-pub (identical? port rcv-ch)
(let [{:keys [topic message]} val]
(process-input! cfg topic message)
(recur))
(identical? port pub-ch)
(do
(redis-pub! cfg val)
(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)))
(l/debug :hint "io-loop thread terminated")))))
(defn- redis-pub!
"Publish a message to the redis server. Asynchronous operation, "Publish a message to the redis server. Asynchronous operation,
intended to be used in core.async go blocks." intended to be used in core.async go blocks."
[{:keys [::pconn] :as cfg} {:keys [topic message]}] [{:keys [::pconn] :as cfg} {:keys [topic message]}]
(let [message (t/encode message) (try
res (a/chan 1)] (p/await! (rds/publish! pconn topic (t/encode message)))
(-> (redis/publish! pconn topic message) (catch InterruptedException cause
(p/finally (fn [_ cause] (throw cause))
(when (and cause (redis/open? pconn)) (catch Throwable cause
(a/offer! res cause)) (l/error :hint "unexpected error on publishing"
(a/close! res)))) :message message
res)) :cause cause))))
(defn redis-sub (defn- redis-sub!
"Create redis subscription. Blocking operation, intended to be used "Create redis subscription. Blocking operation, intended to be used
inside an agent." inside an agent."
[{:keys [::sconn] :as cfg} topic] [{:keys [::sconn] :as cfg} topic]
(redis/subscribe! sconn topic)) (try
(rds/subscribe! sconn topic)
(catch InterruptedException cause
(throw cause))
(catch Throwable cause
(l/trace :hint "exception on subscribing" :topic topic :cause cause))))
(defn redis-unsub (defn- redis-unsub!
"Removes redis subscription. Blocking operation, intended to be used "Removes redis subscription. Blocking operation, intended to be used
inside an agent." inside an agent."
[{:keys [::sconn] :as cfg} topic] [{:keys [::sconn] :as cfg} topic]
(redis/unsubscribe! sconn topic)) (try
(rds/unsubscribe! sconn topic)
(catch InterruptedException cause
(throw cause))
(catch Throwable cause
(l/trace :hint "exception on unsubscribing" :topic topic :cause cause))))

View File

@ -8,17 +8,21 @@
"The msgbus abstraction implemented using redis as underlying backend." "The msgbus abstraction implemented using redis as underlying backend."
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.logging :as l] [app.common.logging :as l]
[app.common.spec :as us] [app.common.spec :as us]
[app.metrics :as mtx] [app.metrics :as mtx]
[app.redis.script :as-alias rscript] [app.redis.script :as-alias rscript]
[app.util.cache :as cache]
[app.util.time :as dt] [app.util.time :as dt]
[app.worker :as-alias wrk]
[clojure.core :as c] [clojure.core :as c]
[clojure.java.io :as io] [clojure.java.io :as io]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[cuerdas.core :as str] [cuerdas.core :as str]
[integrant.core :as ig] [integrant.core :as ig]
[promesa.core :as p]) [promesa.core :as p]
[promesa.exec :as px])
(:import (:import
clojure.lang.IDeref clojure.lang.IDeref
clojure.lang.MapEntry clojure.lang.MapEntry
@ -87,7 +91,7 @@
(s/def ::connect? ::us/boolean) (s/def ::connect? ::us/boolean)
(s/def ::io-threads ::us/integer) (s/def ::io-threads ::us/integer)
(s/def ::worker-threads ::us/integer) (s/def ::worker-threads ::us/integer)
(s/def ::cache #(instance? clojure.lang.Atom %)) (s/def ::cache some?)
(s/def ::redis (s/def ::redis
(s/keys :req [::resources (s/keys :req [::resources
@ -99,11 +103,11 @@
(defmethod ig/prep-key ::redis (defmethod ig/prep-key ::redis
[_ cfg] [_ cfg]
(let [runtime (Runtime/getRuntime) (let [cpus (px/get-available-processors)
cpus (.availableProcessors ^Runtime runtime)] threads (max 1 (int (* cpus 0.2)))]
(merge {::timeout (dt/duration "10s") (merge {::timeout (dt/duration "10s")
::io-threads (max 3 cpus) ::io-threads (max 3 threads)
::worker-threads (max 3 cpus)} ::worker-threads (max 3 threads)}
(d/without-nils cfg)))) (d/without-nils cfg))))
(defmethod ig/pre-init-spec ::redis [_] (defmethod ig/pre-init-spec ::redis [_]
@ -129,6 +133,15 @@
(def string-codec (def string-codec
(RedisCodec/of StringCodec/UTF8 StringCodec/UTF8)) (RedisCodec/of StringCodec/UTF8 StringCodec/UTF8))
(defn- create-cache
[{:keys [::wrk/executor] :as cfg}]
(letfn [(on-remove [key val cause]
(l/trace :hint "evict connection (cache)" :key key :reason cause)
(some-> val d/close!))]
(cache/create :executor executor
:on-remove on-remove
:keepalive "5m")))
(defn- initialize-resources (defn- initialize-resources
"Initialize redis connection resources" "Initialize redis connection resources"
[{:keys [::uri ::io-threads ::worker-threads ::connect?] :as cfg}] [{:keys [::uri ::io-threads ::worker-threads ::connect?] :as cfg}]
@ -145,19 +158,21 @@
(timer ^Timer timer) (timer ^Timer timer)
(build)) (build))
redis-uri (RedisURI/create ^String uri)] redis-uri (RedisURI/create ^String uri)
cfg (-> cfg
(assoc ::resources resources)
(assoc ::timer timer)
(assoc ::redis-uri redis-uri))]
(-> cfg (assoc cfg ::cache (create-cache cfg))))
(assoc ::resources resources)
(assoc ::timer timer)
(assoc ::cache (atom {}))
(assoc ::redis-uri redis-uri))))
(defn- shutdown-resources (defn- shutdown-resources
[{:keys [::resources ::cache ::timer]}] [{:keys [::resources ::cache ::timer]}]
(run! d/close! (vals @cache)) (cache/invalidate-all! cache)
(when resources (when resources
(.shutdown ^ClientResources resources)) (.shutdown ^ClientResources resources))
(when timer (when timer
(.stop ^Timer timer))) (.stop ^Timer timer)))
@ -173,6 +188,7 @@
:default (.connect ^RedisClient client ^RedisCodec codec) :default (.connect ^RedisClient client ^RedisCodec codec)
:pubsub (.connectPubSub ^RedisClient client ^RedisCodec codec))] :pubsub (.connectPubSub ^RedisClient client ^RedisCodec codec))]
(l/trc :hint "connect" :hid (hash client))
(.setTimeout ^StatefulConnection conn ^Duration timeout) (.setTimeout ^StatefulConnection conn ^Duration timeout)
(reify (reify
IDeref IDeref
@ -180,8 +196,9 @@
AutoCloseable AutoCloseable
(close [_] (close [_]
(.close ^StatefulConnection conn) (ex/ignoring (.close ^StatefulConnection conn))
(.shutdown ^RedisClient client))))) (ex/ignoring (.shutdown ^RedisClient client))
(l/trc :hint "disconnect" :hid (hash client))))))
(defn connect (defn connect
[state & {:as opts}] [state & {:as opts}]
@ -194,15 +211,10 @@
(defn get-or-connect (defn get-or-connect
[{:keys [::cache] :as state} key options] [{:keys [::cache] :as state} key options]
(us/assert! ::redis state) (us/assert! ::redis state)
(-> state (let [connection (cache/get cache key (fn [_] (connect* state options)))]
(assoc ::connection (-> state
(or (get @cache key) (dissoc ::cache)
(-> (swap! cache (fn [cache] (assoc ::connection connection))))
(when-let [prev (get cache key)]
(d/close! prev))
(assoc cache key (connect* state options))))
(get key))))
(dissoc ::cache)))
(defn add-listener! (defn add-listener!
[{:keys [::connection] :as conn} listener] [{:keys [::connection] :as conn} listener]
@ -344,7 +356,7 @@
(do (do
(l/error :hint "no script found" :name sname :cause cause) (l/error :hint "no script found" :name sname :cause cause)
(->> (load-script) (->> (load-script)
(p/mapcat eval-script))) (p/mcat eval-script)))
(if-let [on-error (::rscript/on-error script)] (if-let [on-error (::rscript/on-error script)]
(on-error cause) (on-error cause)
(p/rejected cause)))) (p/rejected cause))))
@ -375,15 +387,16 @@
(load-script [] (load-script []
(l/trace :hint "load script" :name sname) (l/trace :hint "load script" :name sname)
(->> (.scriptLoad ^RedisScriptingAsyncCommands cmd (->> (.scriptLoad ^RedisScriptingAsyncCommands cmd
^String (read-script)) ^String (read-script))
(p/map (fn [sha] (p/fmap (fn [sha]
(swap! scripts-cache assoc sname sha) (swap! scripts-cache assoc sname sha)
sha))))] sha))))]
(if-let [sha (get @scripts-cache sname)] (p/await!
(eval-script sha) (if-let [sha (get @scripts-cache sname)]
(->> (load-script) (eval-script sha)
(p/mapcat eval-script)))))) (->> (load-script)
(p/mapcat eval-script)))))))
(defn timeout-exception? (defn timeout-exception?
[cause] [cause]

View File

@ -11,7 +11,6 @@
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.logging :as l] [app.common.logging :as l]
[app.common.spec :as us] [app.common.spec :as us]
[app.common.uuid :as uuid]
[app.config :as cf] [app.config :as cf]
[app.db :as db] [app.db :as db]
[app.http :as-alias http] [app.http :as-alias http]
@ -19,7 +18,6 @@
[app.http.client :as-alias http.client] [app.http.client :as-alias http.client]
[app.http.session :as session] [app.http.session :as session]
[app.loggers.audit :as audit] [app.loggers.audit :as audit]
[app.loggers.webhooks :as-alias webhooks]
[app.main :as-alias main] [app.main :as-alias main]
[app.metrics :as mtx] [app.metrics :as mtx]
[app.msgbus :as-alias mbus] [app.msgbus :as-alias mbus]
@ -35,7 +33,6 @@
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[integrant.core :as ig] [integrant.core :as ig]
[promesa.core :as p] [promesa.core :as p]
[promesa.exec :as px]
[yetti.request :as yrq] [yetti.request :as yrq]
[yetti.response :as yrs])) [yetti.response :as yrs]))
@ -47,12 +44,10 @@
(defn- handle-response-transformation (defn- handle-response-transformation
[response request mdata] [response request mdata]
(let [transform-fn (reduce (fn [res-fn transform-fn] (reduce (fn [response transform-fn]
(fn [request response] (transform-fn request response))
(p/then (res-fn request response) #(transform-fn request %)))) response
(constantly response) (::response-transform-fns mdata)))
(::response-transform-fns mdata))]
(transform-fn request response)))
(defn- handle-before-comple-hook (defn- handle-before-comple-hook
[response mdata] [response mdata]
@ -63,18 +58,18 @@
(defn- handle-response (defn- handle-response
[request result] [request result]
(if (fn? result) (if (fn? result)
(p/wrap (result request)) (result request)
(let [mdata (meta result)] (let [mdata (meta result)]
(p/-> (yrs/response {:status (::http/status mdata 200) (-> {::yrs/status (::http/status mdata 200)
:headers (::http/headers mdata {}) ::yrs/headers (::http/headers mdata {})
:body (rph/unwrap result)}) ::yrs/body (rph/unwrap result)}
(handle-response-transformation request mdata) (handle-response-transformation request mdata)
(handle-before-comple-hook mdata))))) (handle-before-comple-hook mdata)))))
(defn- rpc-query-handler (defn- rpc-query-handler
"Ring handler that dispatches query requests and convert between "Ring handler that dispatches query requests and convert between
internal async flow into ring async flow." internal async flow into ring async flow."
[methods {:keys [params path-params] :as request} respond raise] [methods {:keys [params path-params] :as request}]
(let [type (keyword (:type path-params)) (let [type (keyword (:type path-params))
profile-id (or (::session/profile-id request) profile-id (or (::session/profile-id request)
(::actoken/profile-id request)) (::actoken/profile-id request))
@ -87,19 +82,14 @@
(assoc :profile-id profile-id) (assoc :profile-id profile-id)
(assoc ::profile-id profile-id)) (assoc ::profile-id profile-id))
(dissoc data :profile-id ::profile-id)) (dissoc data :profile-id ::profile-id))
method (get methods type default-handler)] method (get methods type default-handler)
response (method data)]
(->> (method data) (handle-response request response)))
(p/mcat (partial handle-response request))
(p/fnly (fn [response cause]
(if cause
(raise cause)
(respond response)))))))
(defn- rpc-mutation-handler (defn- rpc-mutation-handler
"Ring handler that dispatches mutation requests and convert between "Ring handler that dispatches mutation requests and convert between
internal async flow into ring async flow." internal async flow into ring async flow."
[methods {:keys [params path-params] :as request} respond raise] [methods {:keys [params path-params] :as request}]
(let [type (keyword (:type path-params)) (let [type (keyword (:type path-params))
profile-id (or (::session/profile-id request) profile-id (or (::session/profile-id request)
(::actoken/profile-id request)) (::actoken/profile-id request))
@ -111,24 +101,18 @@
(assoc :profile-id profile-id) (assoc :profile-id profile-id)
(assoc ::profile-id profile-id)) (assoc ::profile-id profile-id))
(dissoc data :profile-id)) (dissoc data :profile-id))
method (get methods type default-handler)] method (get methods type default-handler)
response (method data)]
(->> (method data) (handle-response request response)))
(p/mcat (partial handle-response request))
(p/fnly (fn [response cause]
(if cause
(raise cause)
(respond response)))))))
(defn- rpc-command-handler (defn- rpc-command-handler
"Ring handler that dispatches cmd requests and convert between "Ring handler that dispatches cmd requests and convert between
internal async flow into ring async flow." internal async flow into ring async flow."
[methods {:keys [params path-params] :as request} respond raise] [methods {:keys [params path-params] :as request}]
(let [type (keyword (:type path-params)) (let [type (keyword (:type path-params))
etag (yrq/get-header request "if-none-match") etag (yrq/get-header request "if-none-match")
profile-id (or (::session/profile-id request) profile-id (or (::session/profile-id request)
(::actoken/profile-id request)) (::actoken/profile-id request))
data (-> params data (-> params
(assoc ::request-at (dt/now)) (assoc ::request-at (dt/now))
(assoc ::session/id (::session/id request)) (assoc ::session/id (::session/id request))
@ -140,12 +124,8 @@
method (get methods type default-handler)] method (get methods type default-handler)]
(binding [cond/*enabled* true] (binding [cond/*enabled* true]
(->> (method data) (let [response (method data)]
(p/mcat (partial handle-response request)) (handle-response request response)))))
(p/fnly (fn [response cause]
(if cause
(raise cause)
(respond response))))))))
(defn- wrap-metrics (defn- wrap-metrics
"Wrap service method with metrics measurement." "Wrap service method with metrics measurement."
@ -153,23 +133,22 @@
(let [labels (into-array String [(::sv/name mdata)])] (let [labels (into-array String [(::sv/name mdata)])]
(fn [cfg params] (fn [cfg params]
(let [tp (dt/tpoint)] (let [tp (dt/tpoint)]
(->> (f cfg params) (try
(p/fnly (fn [_ _] (f cfg params)
(mtx/run! metrics (finally
:id metrics-id (mtx/run! metrics
:val (inst-ms (tp)) :id metrics-id
:labels labels)))))))) :val (inst-ms (tp))
:labels labels)))))))
(defn- wrap-authentication (defn- wrap-authentication
[_ f mdata] [_ f mdata]
(fn [cfg params] (fn [cfg params]
(let [profile-id (::profile-id params)] (let [profile-id (::profile-id params)]
(if (and (::auth mdata true) (not (uuid? profile-id))) (if (and (::auth mdata true) (not (uuid? profile-id)))
(p/rejected (ex/raise :type :authentication
(ex/error :type :authentication :code :authentication-required
:code :authentication-required :hint "authentication required for this endpoint")
:hint "authentication required for this endpoint"))
(f cfg params))))) (f cfg params)))))
(defn- wrap-access-token (defn- wrap-access-token
@ -182,98 +161,34 @@
(let [perms (::actoken/perms request #{})] (let [perms (::actoken/perms request #{})]
(if (contains? perms name) (if (contains? perms name)
(f cfg params) (f cfg params)
(p/rejected (ex/raise :type :authorization
(ex/error :type :authorization :code :operation-not-allowed
:code :operation-not-allowed :allowed perms)))
:allowed perms))))
(f cfg params)))) (f cfg params))))
f)) f))
(defn- wrap-dispatch
"Wraps service method into async flow, with the ability to dispatching
it to a preconfigured executor service."
[{:keys [::wrk/executor] :as cfg} f mdata]
(with-meta
(fn [cfg params]
(->> (px/submit! executor (px/wrap-bindings #(f cfg params)))
(p/mapcat p/wrap)
(p/map rph/wrap)))
mdata))
(defn- wrap-audit (defn- wrap-audit
[cfg f mdata] [_ f mdata]
(if (or (contains? cf/flags :webhooks) (if (or (contains? cf/flags :webhooks)
(contains? cf/flags :audit-log)) (contains? cf/flags :audit-log))
(letfn [(handle-audit [params result] (if-not (::audit/skip mdata)
(let [resultm (meta result) (fn [cfg params]
request (::http/request params) (let [result (f cfg params)]
(->> (audit/prepare-event cfg mdata params result)
profile-id (or (::audit/profile-id resultm) (audit/submit! cfg))
(:profile-id result) result))
(if (= (::type cfg) "command") f)
(::profile-id params)
(:profile-id params))
uuid/zero)
props (-> (or (::audit/replace-props resultm)
(-> params
(merge (::audit/props resultm))
(dissoc :profile-id)
(dissoc :type)))
(audit/clean-props))
event {:type (or (::audit/type resultm)
(::type cfg))
:name (or (::audit/name resultm)
(::sv/name mdata))
:profile-id profile-id
:ip-addr (some-> request audit/parse-client-ip)
:props props
;; NOTE: for batch-key lookup we need the params as-is
;; because the rpc api does not need to know the
;; audit/webhook specific object layout.
::params (dissoc params ::http/request)
::webhooks/batch-key
(or (::webhooks/batch-key mdata)
(::webhooks/batch-key resultm))
::webhooks/batch-timeout
(or (::webhooks/batch-timeout mdata)
(::webhooks/batch-timeout resultm))
::webhooks/event?
(or (::webhooks/event? mdata)
(::webhooks/event? resultm)
false)}]
(audit/submit! cfg event)))
(handle-request [cfg params]
(->> (f cfg params)
(p/fnly (fn [result cause]
(when-not cause
(handle-audit params result))))))]
(if-not (::audit/skip mdata)
(with-meta handle-request mdata)
f))
f)) f))
(defn- wrap-spec-conform (defn- wrap-spec-conform
[_ f mdata] [_ f mdata]
(let [spec (or (::sv/spec mdata) (s/spec any?))] (let [spec (or (::sv/spec mdata) (s/spec any?))]
(fn [cfg params] (fn [cfg params]
(let [params (ex/try! (us/conform spec params))] (f cfg (us/conform spec params)))))
(if (ex/exception? params)
(p/rejected params)
(f cfg params))))))
(defn- wrap-all (defn- wrap-all
[cfg f mdata] [cfg f mdata]
(as-> f $ (as-> f $
(wrap-dispatch cfg $ mdata)
(wrap-metrics cfg $ mdata) (wrap-metrics cfg $ mdata)
(cond/wrap cfg $ mdata) (cond/wrap cfg $ mdata)
(retry/wrap-retry cfg $ mdata) (retry/wrap-retry cfg $ mdata)
@ -288,13 +203,11 @@
[cfg f mdata] [cfg f mdata]
(l/debug :hint "register method" :name (::sv/name mdata)) (l/debug :hint "register method" :name (::sv/name mdata))
(let [f (wrap-all cfg f mdata)] (let [f (wrap-all cfg f mdata)]
(with-meta #(f cfg %) mdata))) (partial f cfg)))
(defn- process-method (defn- process-method
[cfg vfn] [cfg [vfn mdata]]
(let [mdata (meta vfn)] [(keyword (::sv/name mdata)) [mdata (wrap cfg vfn mdata)]])
[(keyword (::sv/name mdata))
(wrap cfg vfn mdata)]))
(defn- resolve-query-methods (defn- resolve-query-methods
[cfg] [cfg]
@ -371,13 +284,13 @@
:commands (resolve-command-methods cfg)})) :commands (resolve-command-methods cfg)}))
(s/def ::mutations (s/def ::mutations
(s/map-of keyword? fn?)) (s/map-of keyword? (s/tuple map? fn?)))
(s/def ::queries (s/def ::queries
(s/map-of keyword? fn?)) (s/map-of keyword? (s/tuple map? fn?)))
(s/def ::commands (s/def ::commands
(s/map-of keyword? fn?)) (s/map-of keyword? (s/tuple map? fn?)))
(s/def ::methods (s/def ::methods
(s/keys :req-un [::mutations (s/keys :req-un [::mutations
@ -391,15 +304,18 @@
::db/pool ::db/pool
::main/props ::main/props
::wrk/executor ::wrk/executor
::session/manager ::session/manager]))
::actoken/manager]))
(defmethod ig/init-key ::routes (defmethod ig/init-key ::routes
[_ {:keys [::methods] :as cfg}] [_ {:keys [::methods] :as cfg}]
[["/rpc" {:middleware [[session/authz cfg] (let [methods (-> methods
[actoken/authz cfg]]} (update :commands update-vals peek)
["/command/:type" {:handler (partial rpc-command-handler (:commands methods))}] (update :queries update-vals peek)
["/query/:type" {:handler (partial rpc-query-handler (:queries methods))}] (update :mutations update-vals peek))]
["/mutation/:type" {:handler (partial rpc-mutation-handler (:mutations methods)) [["/rpc" {:middleware [[session/authz cfg]
:allowed-methods #{:post}}]]]) [actoken/authz cfg]]}
["/command/:type" {:handler (partial rpc-command-handler (:commands methods))}]
["/query/:type" {:handler (partial rpc-query-handler (:queries methods))}]
["/mutation/:type" {:handler (partial rpc-mutation-handler (:mutations methods))
:allowed-methods #{:post}}]]]))

View File

@ -6,14 +6,16 @@
(ns app.rpc.climit (ns app.rpc.climit
"Concurrencly limiter for RPC." "Concurrencly limiter for RPC."
(:refer-clojure :exclude [run!])
(:require (:require
[app.common.data :as d]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.logging :as l] [app.common.logging :as l]
[app.common.spec :as us] [app.common.spec :as us]
[app.config :as cf] [app.config :as cf]
[app.metrics :as mtx] [app.metrics :as mtx]
[app.rpc :as-alias rpc] [app.rpc :as-alias rpc]
[app.rpc.climit.config :as-alias config]
[app.util.cache :as cache]
[app.util.services :as-alias sv] [app.util.services :as-alias sv]
[app.util.time :as dt] [app.util.time :as dt]
[app.worker :as-alias wrk] [app.worker :as-alias wrk]
@ -23,184 +25,200 @@
[integrant.core :as ig] [integrant.core :as ig]
[promesa.core :as p] [promesa.core :as p]
[promesa.exec :as px] [promesa.exec :as px]
[promesa.exec.bulkhead :as pxb]) [promesa.exec.bulkhead :as pbh])
(:import (:import
com.github.benmanes.caffeine.cache.Cache clojure.lang.ExceptionInfo))
com.github.benmanes.caffeine.cache.CacheLoader
com.github.benmanes.caffeine.cache.Caffeine
com.github.benmanes.caffeine.cache.RemovalListener))
(defn- capacity-exception? (set! *warn-on-reflection* true)
[o]
(and (ex/error? o)
(let [data (ex-data o)]
(and (= :bulkhead-error (:type data))
(= :capacity-limit-reached (:code data))))))
(defn invoke! (defn- create-bulkhead-cache
[limiter f] [{:keys [::wrk/executor]} config]
(->> (px/submit! limiter f) (letfn [(load-fn [key]
(p/hcat (fn [result cause] (let [config (get config (nth key 0))]
(cond (l/trace :hint "insert into cache" :key key)
(capacity-exception? cause) (pbh/create :permits (or (:permits config) (:concurrency config))
(p/rejected :queue (or (:queue config) (:queue-size config))
(ex/error :type :internal :timeout (:timeout config)
:code :concurrency-limit-reached :executor executor
:queue (-> limiter meta ::bkey name) :type (:type config :semaphore))))
:cause cause))
(some? cause) (on-remove [_ _ cause]
(p/rejected cause) (l/trace :hint "evict from cache" :key key :reason (str cause)))]
:else (cache/create :executor :same-thread
(p/resolved result)))))) :on-remove on-remove
:keepalive "5m"
:load-fn load-fn)))
(defn- create-limiter (s/def ::config/permits ::us/integer)
[{:keys [::wrk/executor ::mtx/metrics ::bkey ::skey concurrency queue-size]}] (s/def ::config/queue ::us/integer)
(let [labels (into-array String [(name bkey)]) (s/def ::config/timeout ::us/integer)
on-queue (fn [instance]
(l/trace :hint "enqueued"
:key (name bkey)
:skey (str skey)
:queue-size (get instance ::pxb/current-queue-size)
:concurrency (get instance ::pxb/current-concurrency))
(mtx/run! metrics
:id :rpc-climit-queue-size
:val (get instance ::pxb/current-queue-size)
:labels labels)
(mtx/run! metrics
:id :rpc-climit-concurrency
:val (get instance ::pxb/current-concurrency)
:labels labels))
on-run (fn [instance task]
(let [elapsed (- (inst-ms (dt/now))
(inst-ms task))]
(l/trace :hint "execute"
:key (name bkey)
:skey (str skey)
:elapsed (str elapsed "ms"))
(mtx/run! metrics
:id :rpc-climit-timing
:val elapsed
:labels labels)
(mtx/run! metrics
:id :rpc-climit-queue-size
:val (get instance ::pxb/current-queue-size)
:labels labels)
(mtx/run! metrics
:id :rpc-climit-concurrency
:val (get instance ::pxb/current-concurrency)
:labels labels)))
options {:executor executor
:concurrency concurrency
:queue-size (or queue-size Integer/MAX_VALUE)
:on-queue on-queue
:on-run on-run}]
(-> (pxb/create options)
(vary-meta assoc ::bkey bkey ::skey skey))))
(defn- create-cache
[{:keys [::wrk/executor] :as params} config]
(let [listener (reify RemovalListener
(onRemoval [_ key _val cause]
(l/trace :hint "cache: remove" :key key :reason (str cause))))
loader (reify CacheLoader
(load [_ key]
(let [[bkey skey] key]
(when-let [config (get config bkey)]
(-> (merge params config)
(assoc ::bkey bkey)
(assoc ::skey skey)
(create-limiter))))))]
(.. (Caffeine/newBuilder)
(weakValues)
(executor executor)
(removalListener listener)
(build loader))))
(defprotocol IConcurrencyManager)
(s/def ::concurrency ::us/integer)
(s/def ::queue-size ::us/integer)
(s/def ::config (s/def ::config
(s/map-of keyword? (s/map-of keyword?
(s/keys :req-un [::concurrency] (s/keys :opt-un [::config/permits
:opt-un [::queue-size]))) ::config/queue
::config/timeout])))
(defmethod ig/prep-key ::rpc/climit (defmethod ig/prep-key ::rpc/climit
[_ cfg] [_ cfg]
(merge {::path (cf/get :rpc-climit-config)} (assoc cfg ::path (cf/get :rpc-climit-config)))
(d/without-nils cfg)))
(s/def ::path ::fs/path) (s/def ::path ::fs/path)
(defmethod ig/pre-init-spec ::rpc/climit [_] (defmethod ig/pre-init-spec ::rpc/climit [_]
(s/keys :req [::wrk/executor ::mtx/metrics ::path])) (s/keys :req [::wrk/executor ::mtx/metrics ::path]))
(defmethod ig/init-key ::rpc/climit (defmethod ig/init-key ::rpc/climit
[_ {:keys [::path] :as params}] [_ {:keys [::path ::mtx/metrics ::wrk/executor] :as cfg}]
(when (contains? cf/flags :rpc-climit) (when (contains? cf/flags :rpc-climit)
(if-let [config (some->> path slurp edn/read-string)] (when-let [params (some->> path slurp edn/read-string)]
(do (l/info :hint "initializing concurrency limit" :config (str path))
(l/info :hint "initializing concurrency limit" :config (str path)) (us/verify! ::config params)
(us/verify! ::config config) {::cache (create-bulkhead-cache cfg params)
::config params
(let [cache (create-cache params config)] ::wrk/executor executor
^{::cache cache} ::mtx/metrics metrics})))
(reify
IConcurrencyManager
clojure.lang.IDeref
(deref [_] config)
clojure.lang.ILookup
(valAt [_ key]
(let [key (if (vector? key) key [key])]
(.get ^Cache cache key))))))
(l/warn :hint "unable to load configuration" :config (str path)))))
(s/def ::cache cache/cache?)
(s/def ::instance
(s/keys :req [::cache ::config ::wrk/executor]))
(s/def ::rpc/climit (s/def ::rpc/climit
(s/nilable #(satisfies? IConcurrencyManager %))) (s/nilable ::instance))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; PUBLIC API ;; PUBLIC API
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn invoke!
[cache metrics id key f]
(let [limiter (cache/get cache [id key])
tpoint (dt/tpoint)
labels (into-array String [(name id)])
wrapped
(fn []
(let [elapsed (tpoint)
stats (pbh/get-stats limiter)]
(l/trace :hint "executed"
:id (name id)
:key key
:fnh (hash f)
:permits (:permits stats)
:queue (:queue stats)
:max-permits (:max-permits stats)
:max-queue (:max-queue stats)
:elapsed (dt/format-duration elapsed))
(mtx/run! metrics
:id :rpc-climit-timing
:val (inst-ms elapsed)
:labels labels)
(try
(f)
(finally
(let [elapsed (tpoint)]
(l/trace :hint "finished"
:id (name id)
:key key
:fnh (hash f)
:permits (:permits stats)
:queue (:queue stats)
:max-permits (:max-permits stats)
:max-queue (:max-queue stats)
:elapsed (dt/format-duration elapsed)))))))
measure!
(fn [stats]
(mtx/run! metrics
:id :rpc-climit-queue
:val (:queue stats)
:labels labels)
(mtx/run! metrics
:id :rpc-climit-permits
:val (:permits stats)
:labels labels))]
(try
(let [stats (pbh/get-stats limiter)]
(measure! stats)
(l/trace :hint "enqueued"
:id (name id)
:key key
:fnh (hash f)
:permits (:permits stats)
:queue (:queue stats)
:max-permits (:max-permits stats)
:max-queue (:max-queue stats))
(pbh/invoke! limiter wrapped))
(catch ExceptionInfo cause
(let [{:keys [type code]} (ex-data cause)]
(if (= :bulkhead-error type)
(ex/raise :type :concurrency-limit
:code code
:hint "concurrency limit reached")
(throw cause))))
(finally
(measure! (pbh/get-stats limiter))))))
(defn run!
[{:keys [::id ::cache ::mtx/metrics]} f]
(if (and cache id)
(invoke! cache metrics id nil f)
(f)))
(defn submit!
[{:keys [::id ::cache ::wrk/executor ::mtx/metrics]} f]
(let [f (partial px/submit! executor f)]
(if (and cache id)
(p/await! (invoke! cache metrics id nil f))
(p/await! (f)))))
(defn configure
([{:keys [::rpc/climit]} id]
(us/assert! ::rpc/climit climit)
(assoc climit ::id id))
([{:keys [::rpc/climit]} id executor]
(us/assert! ::rpc/climit climit)
(-> climit
(assoc ::id id)
(assoc ::wrk/executor executor))))
(defmacro with-dispatch!
"Dispatch blocking operation to a separated thread protected with the
specified concurrency limiter. If climit is not active, the function
will be scheduled to execute without concurrency monitoring."
[instance & body]
(if (vector? instance)
`(-> (app.rpc.climit/configure ~@instance)
(app.rpc.climit/run! (^:once fn* [] ~@body)))
`(run! ~instance (^:once fn* [] ~@body))))
(defmacro with-dispatch (defmacro with-dispatch
[lim & body] "Dispatch blocking operation to a separated thread protected with
`(if ~lim the specified semaphore.
(invoke! ~lim (^:once fn [] (p/wrap (do ~@body)))) DEPRECATED"
(p/wrap (do ~@body)))) [& params]
`(with-dispatch! ~@params))
(def noop-fn (constantly nil))
(defn wrap (defn wrap
[{:keys [::rpc/climit]} f {:keys [::queue ::key-fn] :as mdata}] [{:keys [::rpc/climit ::mtx/metrics]} f {:keys [::id ::key-fn] :or {key-fn noop-fn} :as mdata}]
(if (and (some? climit) (if (and (some? climit) (some? id))
(some? queue)) (if-let [config (get-in climit [::config id])]
(if-let [config (get @climit queue)] (let [cache (::cache climit)]
(do
(l/debug :hint "wrap: instrumenting method" (l/debug :hint "wrap: instrumenting method"
:limit-name (name queue) :limit (name id)
:service-name (::sv/name mdata) :service-name (::sv/name mdata)
:queue-size (or (:queue-size config) Integer/MAX_VALUE) :timeout (:timeout config)
:concurrency (:concurrency config) :permits (:permits config)
:queue (:queue config)
:keyed? (some? key-fn)) :keyed? (some? key-fn))
(if (some? key-fn) (fn [cfg params]
(fn [cfg params] (invoke! cache metrics id (key-fn params) (partial f cfg params))))
(let [key [queue (key-fn params)]
lim (get climit key)]
(invoke! lim (partial f cfg params))))
(let [lim (get climit queue)]
(fn [cfg params]
(invoke! lim (partial f cfg params))))))
(do (do
(l/warn :hint "wrap: no config found" (l/warn :hint "no config found for specified queue" :id id)
:queue (name queue)
:service (::sv/name mdata))
f)) f))
f)) f))

View File

@ -21,10 +21,7 @@
[app.rpc.helpers :as rph] [app.rpc.helpers :as rph]
[app.util.services :as sv] [app.util.services :as sv]
[app.util.time :as dt] [app.util.time :as dt]
[app.worker :as wrk] [clojure.spec.alpha :as s]))
[clojure.spec.alpha :as s]
[promesa.core :as p]
[promesa.exec :as px]))
(defn- event->row [event] (defn- event->row [event]
[(uuid/next) [(uuid/next)
@ -71,17 +68,22 @@
:req-un [::events])) :req-un [::events]))
(sv/defmethod ::push-audit-events (sv/defmethod ::push-audit-events
{::climit/queue :push-audit-events {::climit/id :submit-audit-events-by-profile
::climit/key-fn ::rpc/profile-id ::climit/key-fn ::rpc/profile-id
::audit/skip true ::audit/skip true
::doc/added "1.17"} ::doc/added "1.17"}
[{:keys [::db/pool ::wrk/executor] :as cfg} params] [{:keys [::db/pool] :as cfg} params]
(if (or (db/read-only? pool) (if (or (db/read-only? pool)
(not (contains? cf/flags :audit-log))) (not (contains? cf/flags :audit-log)))
(do (do
(l/warn :hint "audit: http handler disabled or db is read-only") (l/warn :hint "audit: http handler disabled or db is read-only")
(rph/wrap nil)) (rph/wrap nil))
(->> (px/submit! executor #(handle-events cfg params)) (do
(p/fmap (constantly nil))))) (try
(handle-events cfg params)
(catch Throwable cause
(l/error :hint "unexpected error on persisting audit events from frontend"
:cause cause)))
(rph/wrap nil))))

View File

@ -6,9 +6,9 @@
(ns app.rpc.commands.auth (ns app.rpc.commands.auth
(:require (:require
[app.auth :as auth]
[app.common.data :as d] [app.common.data :as d]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.spec :as us] [app.common.spec :as us]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.config :as cf] [app.config :as cf]
@ -18,7 +18,6 @@
[app.loggers.audit :as audit] [app.loggers.audit :as audit]
[app.main :as-alias main] [app.main :as-alias main]
[app.rpc :as-alias rpc] [app.rpc :as-alias rpc]
[app.rpc.climit :as climit]
[app.rpc.commands.profile :as profile] [app.rpc.commands.profile :as profile]
[app.rpc.commands.teams :as teams] [app.rpc.commands.teams :as teams]
[app.rpc.doc :as-alias doc] [app.rpc.doc :as-alias doc]
@ -63,14 +62,20 @@
:code :login-disabled :code :login-disabled
:hint "login is disabled in this instance")) :hint "login is disabled in this instance"))
(letfn [(check-password [profile password] (letfn [(check-password [conn profile password]
(when (= (:password profile) "!") (when (= (:password profile) "!")
(ex/raise :type :validation (ex/raise :type :validation
:code :account-without-password :code :account-without-password
:hint "the current account does not have password")) :hint "the current account does not have password"))
(:valid (auth/verify-password password (:password profile)))) (let [result (profile/verify-password cfg password (:password profile))]
(when (:update result)
(l/trace :hint "updating profile password" :id (:id profile) :email (:email profile))
(profile/update-profile-password! (assoc cfg ::db/conn conn)
(assoc profile :password password)))
(:valid result)))
(validate-profile [profile]
(validate-profile [conn profile]
(when-not profile (when-not profile
(ex/raise :type :validation (ex/raise :type :validation
:code :wrong-credentials)) :code :wrong-credentials))
@ -80,7 +85,7 @@
(when (:is-blocked profile) (when (:is-blocked profile)
(ex/raise :type :restriction (ex/raise :type :restriction
:code :profile-blocked)) :code :profile-blocked))
(when-not (check-password profile password) (when-not (check-password conn profile password)
(ex/raise :type :validation (ex/raise :type :validation
:code :wrong-credentials)) :code :wrong-credentials))
(when-let [deleted-at (:deleted-at profile)] (when-let [deleted-at (:deleted-at profile)]
@ -92,8 +97,7 @@
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
(let [profile (->> (profile/get-profile-by-email conn email) (let [profile (->> (profile/get-profile-by-email conn email)
(validate-profile) (validate-profile conn)
(profile/decode-row)
(profile/strip-private-attrs)) (profile/strip-private-attrs))
invitation (when-let [token (:invitation-token params)] invitation (when-let [token (:invitation-token params)]
@ -118,7 +122,6 @@
(sv/defmethod ::login-with-password (sv/defmethod ::login-with-password
"Performs authentication using penpot password." "Performs authentication using penpot password."
{::rpc/auth false {::rpc/auth false
::climit/queue :auth
::doc/added "1.15"} ::doc/added "1.15"}
[cfg params] [cfg params]
(login-with-password cfg params)) (login-with-password cfg params))
@ -144,7 +147,7 @@
(:profile-id tdata))) (:profile-id tdata)))
(update-password [conn profile-id] (update-password [conn profile-id]
(let [pwd (auth/derive-password password)] (let [pwd (profile/derive-password cfg password)]
(db/update! conn :profile {:password pwd} {:id profile-id})))] (db/update! conn :profile {:password pwd} {:id profile-id})))]
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
@ -158,7 +161,6 @@
(sv/defmethod ::recover-profile (sv/defmethod ::recover-profile
{::rpc/auth false {::rpc/auth false
::climit/queue :auth
::doc/added "1.15"} ::doc/added "1.15"}
[cfg params] [cfg params]
(recover-profile cfg params)) (recover-profile cfg params))
@ -264,9 +266,7 @@
:nudge {:big 10 :small 1}}) :nudge {:big 10 :small 1}})
(db/tjson)) (db/tjson))
password (if-let [password (:password params)] password (or (:password params) "!")
(auth/derive-password password)
"!")
locale (:locale params) locale (:locale params)
locale (when (and (string? locale) (not (str/blank? locale))) locale (when (and (string? locale) (not (str/blank? locale)))
@ -344,8 +344,11 @@
profile (if-let [profile-id (:profile-id claims)] profile (if-let [profile-id (:profile-id claims)]
(profile/get-profile conn profile-id) (profile/get-profile conn profile-id)
(->> (create-profile! conn (assoc params :is-active is-active)) (let [params (-> params
(create-profile-rels! conn))) (assoc :is-active is-active)
(update :password #(profile/derive-password cfg %)))]
(->> (create-profile! conn params)
(create-profile-rels! conn))))
invitation (when-let [token (:invitation-token params)] invitation (when-let [token (:invitation-token params)]
(tokens/verify (::main/props cfg) {:token token :iss :team-invitation}))] (tokens/verify (::main/props cfg) {:token token :iss :team-invitation}))]
@ -356,9 +359,9 @@
(when-let [id (:profile-id claims)] (when-let [id (:profile-id claims)]
(db/update! conn :profile {:modified-at (dt/now)} {:id id}) (db/update! conn :profile {:modified-at (dt/now)} {:id id})
(audit/submit! cfg (audit/submit! cfg
{:type "fact" {::audit/type "fact"
:name "register-profile-retry" ::audit/name "register-profile-retry"
:profile-id id})) ::audit/profile-id id}))
(cond (cond
;; If invitation token comes in params, this is because the ;; If invitation token comes in params, this is because the
@ -406,7 +409,6 @@
(sv/defmethod ::register-profile (sv/defmethod ::register-profile
{::rpc/auth false {::rpc/auth false
::climit/queue :auth
::doc/added "1.15"} ::doc/added "1.15"}
[{:keys [::db/pool] :as cfg} params] [{:keys [::db/pool] :as cfg} params]
(db/with-atomic [conn pool] (db/with-atomic [conn pool]

View File

@ -354,7 +354,6 @@
(with-open [^AutoCloseable conn (db/open pool)] (with-open [^AutoCloseable conn (db/open pool)]
(db/exec! conn [sql:file-library-rels (db/create-array conn "uuid" ids)]))) (db/exec! conn [sql:file-library-rels (db/create-array conn "uuid" ids)])))
(defn- create-or-update-file (defn- create-or-update-file
[conn params] [conn params]
(let [sql (str "INSERT INTO file (id, project_id, name, revn, is_shared, data, created_at, modified_at) " (let [sql (str "INSERT INTO file (id, project_id, name, revn, is_shared, data, created_at, modified_at) "
@ -527,13 +526,13 @@
(write-obj! output sids) (write-obj! output sids)
(doseq [id sids] (doseq [id sids]
(let [{:keys [size] :as obj} @(sto/get-object storage id)] (let [{:keys [size] :as obj} (sto/get-object storage id)]
(l/debug :hint "write sobject" :id id ::l/sync? true) (l/debug :hint "write sobject" :id id ::l/sync? true)
(doto output (doto output
(write-uuid! id) (write-uuid! id)
(write-obj! (meta obj))) (write-obj! (meta obj)))
(with-open [^InputStream stream @(sto/get-object-data storage obj)] (with-open [^InputStream stream (sto/get-object-data storage obj)]
(let [written (write-stream! output stream size)] (let [written (write-stream! output stream size)]
(when (not= written size) (when (not= written size)
(ex/raise :type :validation (ex/raise :type :validation
@ -719,7 +718,7 @@
(assoc ::sto/touched-at (dt/now)) (assoc ::sto/touched-at (dt/now))
(assoc :bucket "file-media-object")) (assoc :bucket "file-media-object"))
sobject @(sto/put-object! storage params)] sobject (sto/put-object! storage params)]
(l/debug :hint "persisted storage object" :id id :new-id (:id sobject) ::l/sync? true) (l/debug :hint "persisted storage object" :id id :new-id (:id sobject) ::l/sync? true)
(vswap! *state* update :index assoc id (:id sobject))))) (vswap! *state* update :index assoc id (:id sobject)))))
@ -910,7 +909,9 @@
(export! output-stream))))] (export! output-stream))))]
(fn [_] (fn [_]
(yrs/response 200 body {"content-type" "application/octet-stream"})))) {::yrs/status 200
::yrs/body body
::yrs/headers {"content-type" "application/octet-stream"}})))
(s/def ::file ::media/upload) (s/def ::file ::media/upload)
(s/def ::import-binfile (s/def ::import-binfile

View File

@ -101,7 +101,7 @@
(sv/defmethod ::get-comment-threads (sv/defmethod ::get-comment-threads
{::doc/added "1.15"} {::doc/added "1.15"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id share-id] :as params}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id share-id] :as params}]
(with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(files/check-comment-permissions! conn profile-id file-id share-id) (files/check-comment-permissions! conn profile-id file-id share-id)
(get-comment-threads conn profile-id file-id))) (get-comment-threads conn profile-id file-id)))
@ -144,7 +144,7 @@
(sv/defmethod ::get-unread-comment-threads (sv/defmethod ::get-unread-comment-threads
{::doc/added "1.15"} {::doc/added "1.15"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}]
(with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(teams/check-read-permissions! conn profile-id team-id) (teams/check-read-permissions! conn profile-id team-id)
(get-unread-comment-threads conn profile-id team-id))) (get-unread-comment-threads conn profile-id team-id)))
@ -191,7 +191,7 @@
(sv/defmethod ::get-comment-thread (sv/defmethod ::get-comment-thread
{::doc/added "1.15"} {::doc/added "1.15"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id id share-id] :as params}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id id share-id] :as params}]
(with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(files/check-comment-permissions! conn profile-id file-id share-id) (files/check-comment-permissions! conn profile-id file-id share-id)
(let [sql (str "with threads as (" sql:comment-threads ")" (let [sql (str "with threads as (" sql:comment-threads ")"
"select * from threads where id = ?")] "select * from threads where id = ?")]
@ -211,7 +211,7 @@
(sv/defmethod ::get-comments (sv/defmethod ::get-comments
{::doc/added "1.15"} {::doc/added "1.15"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id thread-id share-id] :as params}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id thread-id share-id] :as params}]
(with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(let [{:keys [file-id] :as thread} (get-comment-thread conn thread-id)] (let [{:keys [file-id] :as thread} (get-comment-thread conn thread-id)]
(files/check-comment-permissions! conn profile-id file-id share-id) (files/check-comment-permissions! conn profile-id file-id share-id)
(get-comments conn thread-id)))) (get-comments conn thread-id))))
@ -263,7 +263,7 @@
{::doc/added "1.15" {::doc/added "1.15"
::doc/changes ["1.15" "Imported from queries and renamed."]} ::doc/changes ["1.15" "Imported from queries and renamed."]}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id share-id]}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id share-id]}]
(with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(files/check-comment-permissions! conn profile-id file-id share-id) (files/check-comment-permissions! conn profile-id file-id share-id)
(get-file-comments-users conn file-id profile-id))) (get-file-comments-users conn file-id profile-id)))

View File

@ -13,6 +13,7 @@
[app.loggers.audit :as audit] [app.loggers.audit :as audit]
[app.rpc :as-alias rpc] [app.rpc :as-alias rpc]
[app.rpc.commands.auth :as auth] [app.rpc.commands.auth :as auth]
[app.rpc.commands.profile :as profile]
[app.rpc.doc :as-alias doc] [app.rpc.doc :as-alias doc]
[app.util.services :as sv] [app.util.services :as sv]
[app.util.time :as dt] [app.util.time :as dt]
@ -48,7 +49,7 @@
:fullname fullname :fullname fullname
:is-active true :is-active true
:deleted-at (dt/in-future cf/deletion-delay) :deleted-at (dt/in-future cf/deletion-delay)
:password password :password (profile/derive-password cfg password)
:props {}}] :props {}}]
(db/with-atomic [conn pool] (db/with-atomic [conn pool]

View File

@ -277,7 +277,7 @@
::cond/get-object #(get-minimal-file %1 (:id %2)) ::cond/get-object #(get-minimal-file %1 (:id %2))
::cond/key-fn get-file-etag} ::cond/key-fn get-file-etag}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id features]}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id features]}]
(with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(let [perms (get-permissions conn profile-id id)] (let [perms (get-permissions conn profile-id id)]
(check-read-permissions! perms) (check-read-permissions! perms)
(let [file (-> (get-file conn id features) (let [file (-> (get-file conn id features)
@ -305,7 +305,7 @@
{::doc/added "1.17" {::doc/added "1.17"
::rpc/:auth false} ::rpc/:auth false}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id fragment-id share-id] }] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id fragment-id share-id] }]
(with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(let [perms (get-permissions conn profile-id file-id share-id)] (let [perms (get-permissions conn profile-id file-id share-id)]
(check-read-permissions! perms) (check-read-permissions! perms)
(-> (get-file-fragment conn file-id fragment-id) (-> (get-file-fragment conn file-id fragment-id)
@ -341,7 +341,7 @@
::cond/reuse-key? true ::cond/reuse-key? true
::cond/key-fn get-file-etag} ::cond/key-fn get-file-etag}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
(with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(check-read-permissions! conn profile-id file-id) (check-read-permissions! conn profile-id file-id)
(get-object-thumbnails conn file-id))) (get-object-thumbnails conn file-id)))
@ -372,7 +372,7 @@
"Get all files for the specified project." "Get all files for the specified project."
{::doc/added "1.17"} {::doc/added "1.17"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id]}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id]}]
(with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(projects/check-read-permissions! conn profile-id project-id) (projects/check-read-permissions! conn profile-id project-id)
(get-project-files conn project-id))) (get-project-files conn project-id)))
@ -391,7 +391,7 @@
"Checks if the file has libraries. Returns a boolean" "Checks if the file has libraries. Returns a boolean"
{::doc/added "1.15.1"} {::doc/added "1.15.1"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id]}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id]}]
(with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(check-read-permissions! pool profile-id file-id) (check-read-permissions! pool profile-id file-id)
(get-has-file-libraries conn file-id))) (get-has-file-libraries conn file-id)))
@ -458,7 +458,7 @@
Mainly used for rendering purposes." Mainly used for rendering purposes."
{::doc/added "1.17"} {::doc/added "1.17"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
(with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(check-read-permissions! conn profile-id file-id) (check-read-permissions! conn profile-id file-id)
(get-page conn params))) (get-page conn params)))
@ -511,7 +511,7 @@
"Get all file (libraries) for the specified team." "Get all file (libraries) for the specified team."
{::doc/added "1.17"} {::doc/added "1.17"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
(with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(teams/check-read-permissions! conn profile-id team-id) (teams/check-read-permissions! conn profile-id team-id)
(get-team-shared-files conn team-id))) (get-team-shared-files conn team-id)))
@ -565,7 +565,7 @@
"Get libraries used by the specified file." "Get libraries used by the specified file."
{::doc/added "1.17"} {::doc/added "1.17"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id features]}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id features]}]
(with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(check-read-permissions! conn profile-id file-id) (check-read-permissions! conn profile-id file-id)
(get-file-libraries conn file-id features))) (get-file-libraries conn file-id features)))
@ -591,7 +591,7 @@
"Returns all the file references that use specified file (library) id." "Returns all the file references that use specified file (library) id."
{::doc/added "1.17"} {::doc/added "1.17"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
(with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(check-read-permissions! conn profile-id file-id) (check-read-permissions! conn profile-id file-id)
(get-library-file-references conn file-id))) (get-library-file-references conn file-id)))
@ -628,7 +628,7 @@
(sv/defmethod ::get-team-recent-files (sv/defmethod ::get-team-recent-files
{::doc/added "1.17"} {::doc/added "1.17"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
(with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(teams/check-read-permissions! conn profile-id team-id) (teams/check-read-permissions! conn profile-id team-id)
(get-team-recent-files conn team-id))) (get-team-recent-files conn team-id)))
@ -662,7 +662,7 @@
(sv/defmethod ::get-file-thumbnail (sv/defmethod ::get-file-thumbnail
{::doc/added "1.17"} {::doc/added "1.17"}
[{:keys [::db/pool]} {:keys [::rpc/profile-id file-id revn]}] [{:keys [::db/pool]} {:keys [::rpc/profile-id file-id revn]}]
(with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(check-read-permissions! conn profile-id file-id) (check-read-permissions! conn profile-id file-id)
(-> (get-file-thumbnail conn file-id revn) (-> (get-file-thumbnail conn file-id revn)
(rph/with-http-cache long-cache-duration)))) (rph/with-http-cache long-cache-duration))))
@ -758,7 +758,7 @@
mainly for render thumbnails on dashboard." mainly for render thumbnails on dashboard."
{::doc/added "1.17"} {::doc/added "1.17"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id features] :as props}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id features] :as props}]
(with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(check-read-permissions! conn profile-id file-id) (check-read-permissions! conn profile-id file-id)
;; NOTE: we force here the "storage/pointer-map" feature, because ;; NOTE: we force here the "storage/pointer-map" feature, because
;; it used internally only and is independent if user supports it ;; it used internally only and is independent if user supports it

View File

@ -101,7 +101,7 @@
(defn- wrap-with-pointer-map-context (defn- wrap-with-pointer-map-context
[f] [f]
(fn [{:keys [conn] :as cfg} {:keys [id] :as file}] (fn [{:keys [::db/conn] :as cfg} {:keys [id] :as file}]
(binding [pmap/*tracked* (atom {}) (binding [pmap/*tracked* (atom {})
pmap/*load-fn* (partial files/load-pointer conn id) pmap/*load-fn* (partial files/load-pointer conn id)
ffeat/*wrap-with-pointer-map-fn* pmap/wrap] ffeat/*wrap-with-pointer-map-fn* pmap/wrap]
@ -126,7 +126,7 @@
;; database. ;; database.
(sv/defmethod ::update-file (sv/defmethod ::update-file
{::climit/queue :update-file {::climit/id :update-file-by-id
::climit/key-fn :id ::climit/key-fn :id
::webhooks/event? true ::webhooks/event? true
::webhooks/batch-timeout (dt/duration "2m") ::webhooks/batch-timeout (dt/duration "2m")
@ -136,8 +136,7 @@
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
(files/check-edition-permissions! conn profile-id id) (files/check-edition-permissions! conn profile-id id)
(db/xact-lock! conn id) (db/xact-lock! conn id)
(let [cfg (assoc cfg ::db/conn conn)
(let [cfg (assoc cfg :conn conn)
params (assoc params :profile-id profile-id) params (assoc params :profile-id profile-id)
tpoint (dt/tpoint)] tpoint (dt/tpoint)]
(-> (update-file cfg params) (-> (update-file cfg params)
@ -145,7 +144,7 @@
(l/trace :hint "update-file" :time (dt/format-duration elapsed)))))))) (l/trace :hint "update-file" :time (dt/format-duration elapsed))))))))
(defn update-file (defn update-file
[{:keys [conn ::mtx/metrics] :as cfg} {:keys [profile-id id changes changes-with-metadata] :as params}] [{:keys [::db/conn ::mtx/metrics] :as cfg} {:keys [profile-id id changes changes-with-metadata] :as params}]
(let [file (get-file conn id) (let [file (get-file conn id)
features (->> (concat (:features file) features (->> (concat (:features file)
(:features params)) (:features params))
@ -197,24 +196,34 @@
:project-id (:project-id file) :project-id (:project-id file)
:team-id (:team-id file)})))))) :team-id (:team-id file)}))))))
(defn- update-file-data
[file changes]
(-> file
(update :revn inc)
(update :data (fn [data]
(cond-> data
:always
(-> (blob/decode)
(assoc :id (:id file))
(pmg/migrate-data))
(and (contains? ffeat/*current* "components/v2")
(not (contains? ffeat/*previous* "components/v2")))
(ctf/migrate-to-components-v2)
:always
(-> (cp/process-changes changes)
(blob/encode)))))))
(defn- update-file* (defn- update-file*
[{:keys [conn] :as cfg} {:keys [profile-id file changes session-id ::created-at] :as params}] [{:keys [::db/conn] :as cfg} {:keys [profile-id file changes session-id ::created-at] :as params}]
(let [file (-> file (let [;; Process the file data in the CLIMIT context; scheduling it
(update :revn inc) ;; to be executed on a separated executor for avoid to do the
(update :data (fn [data] ;; CPU intensive operation on vthread.
(cond-> data file (-> (climit/configure cfg :update-file)
:always (climit/submit! (partial update-file-data file changes)))]
(-> (blob/decode)
(assoc :id (:id file))
(pmg/migrate-data))
(and (contains? ffeat/*current* "components/v2")
(not (contains? ffeat/*previous* "components/v2")))
(ctf/migrate-to-components-v2)
:always
(-> (cp/process-changes changes)
(blob/encode))))))]
(db/insert! conn :file-change (db/insert! conn :file-change
{:id (uuid/next) {:id (uuid/next)
:session-id session-id :session-id session-id
@ -273,11 +282,10 @@
(vec))) (vec)))
(defn- send-notifications! (defn- send-notifications!
[{:keys [conn] :as cfg} {:keys [file changes session-id] :as params}] [{:keys [::db/conn] :as cfg} {:keys [file changes session-id] :as params}]
(let [lchanges (filter library-change? changes) (let [lchanges (filter library-change? changes)
msgbus (::mbus/msgbus cfg)] msgbus (::mbus/msgbus cfg)]
;; Asynchronously publish message to the msgbus
(mbus/pub! msgbus (mbus/pub! msgbus
:topic (:id file) :topic (:id file)
:message {:type :file-change :message {:type :file-change
@ -290,7 +298,6 @@
(when (and (:is-shared file) (seq lchanges)) (when (and (:is-shared file) (seq lchanges))
(let [team-id (or (:team-id file) (let [team-id (or (:team-id file)
(files/get-team-id conn (:project-id file)))] (files/get-team-id conn (:project-id file)))]
;; Asynchronously publish message to the msgbus
(mbus/pub! msgbus (mbus/pub! msgbus
:topic team-id :topic team-id
:message {:type :library-change :message {:type :library-change

View File

@ -6,7 +6,7 @@
(ns app.rpc.commands.fonts (ns app.rpc.commands.fonts
(:require (:require
[app.common.data :as d] [app.common.data.macros :as dm]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.spec :as us] [app.common.spec :as us]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
@ -15,7 +15,7 @@
[app.loggers.webhooks :as-alias webhooks] [app.loggers.webhooks :as-alias webhooks]
[app.media :as media] [app.media :as media]
[app.rpc :as-alias rpc] [app.rpc :as-alias rpc]
[app.rpc.climit :as-alias climit] [app.rpc.climit :as climit]
[app.rpc.commands.files :as files] [app.rpc.commands.files :as files]
[app.rpc.commands.projects :as projects] [app.rpc.commands.projects :as projects]
[app.rpc.commands.teams :as teams] [app.rpc.commands.teams :as teams]
@ -25,10 +25,7 @@
[app.storage :as sto] [app.storage :as sto]
[app.util.services :as sv] [app.util.services :as sv]
[app.util.time :as dt] [app.util.time :as dt]
[app.worker :as-alias wrk] [clojure.spec.alpha :as s]))
[clojure.spec.alpha :as s]
[promesa.core :as p]
[promesa.exec :as px]))
(def valid-weight #{100 200 300 400 500 600 700 800 900 950}) (def valid-weight #{100 200 300 400 500 600 700 800 900 950})
(def valid-style #{"normal" "italic"}) (def valid-style #{"normal" "italic"})
@ -59,7 +56,7 @@
(sv/defmethod ::get-font-variants (sv/defmethod ::get-font-variants
{::doc/added "1.18"} {::doc/added "1.18"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id file-id project-id] :as params}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id file-id project-id] :as params}]
(with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(cond (cond
(uuid? team-id) (uuid? team-id)
(do (do
@ -107,50 +104,45 @@
(create-font-variant cfg (assoc params :profile-id profile-id)))) (create-font-variant cfg (assoc params :profile-id profile-id))))
(defn create-font-variant (defn create-font-variant
[{:keys [::sto/storage ::db/pool ::wrk/executor ::rpc/climit]} {:keys [data] :as params}] [{:keys [::sto/storage ::db/pool] :as cfg} {:keys [data] :as params}]
(letfn [(generate-fonts [data] (letfn [(generate-missing! [data]
(climit/with-dispatch (:process-font climit) (let [data (media/run {:cmd :generate-fonts :input data})]
(media/run {:cmd :generate-fonts :input data}))) (when (and (not (contains? data "font/otf"))
(not (contains? data "font/ttf"))
(not (contains? data "font/woff"))
(not (contains? data "font/woff2")))
(ex/raise :type :validation
:code :invalid-font-upload
:hint "invalid font upload, unable to generate missing font assets"))
data))
;; Function responsible of calculating cryptographyc hash of (prepare-font [data mtype]
;; the provided data.
(calculate-hash [data]
(px/with-dispatch executor
(sto/calculate-hash data)))
(validate-data [data]
(when (and (not (contains? data "font/otf"))
(not (contains? data "font/ttf"))
(not (contains? data "font/woff"))
(not (contains? data "font/woff2")))
(ex/raise :type :validation
:code :invalid-font-upload))
data)
(persist-font-object [data mtype]
(when-let [resource (get data mtype)] (when-let [resource (get data mtype)]
(p/let [hash (calculate-hash resource) (let [hash (sto/calculate-hash resource)
content (-> (sto/content resource) content (-> (sto/content resource)
(sto/wrap-with-hash hash))] (sto/wrap-with-hash hash))]
(sto/put-object! storage {::sto/content content {::sto/content content
::sto/touched-at (dt/now) ::sto/touched-at (dt/now)
::sto/deduplicate? true ::sto/deduplicate? true
:content-type mtype :content-type mtype
:bucket "team-font-variant"})))) :bucket "team-font-variant"})))
(persist-fonts [data] (persist-fonts-files! [data]
(p/let [otf (persist-font-object data "font/otf") (let [otf-params (prepare-font data "font/otf")
ttf (persist-font-object data "font/ttf") ttf-params (prepare-font data "font/ttf")
woff1 (persist-font-object data "font/woff") wf1-params (prepare-font data "font/woff")
woff2 (persist-font-object data "font/woff2")] wf2-params (prepare-font data "font/woff2")]
(cond-> {}
(some? otf-params)
(assoc :otf (sto/put-object! storage otf-params))
(some? ttf-params)
(assoc :ttf (sto/put-object! storage ttf-params))
(some? wf1-params)
(assoc :woff1 (sto/put-object! storage wf1-params))
(some? wf2-params)
(assoc :woff2 (sto/put-object! storage wf2-params)))))
(d/without-nils (insert-font-variant! [{:keys [woff1 woff2 otf ttf]}]
{:otf otf
:ttf ttf
:woff1 woff1
:woff2 woff2})))
(insert-into-db [{:keys [woff1 woff2 otf ttf]}]
(db/insert! pool :team-font-variant (db/insert! pool :team-font-variant
{:id (uuid/next) {:id (uuid/next)
:team-id (:team-id params) :team-id (:team-id params)
@ -164,13 +156,11 @@
:ttf-file-id (:id ttf)})) :ttf-file-id (:id ttf)}))
] ]
(->> (generate-fonts data) (let [data (-> (climit/configure cfg :process-font)
(p/fmap validate-data) (climit/submit! (partial generate-missing! data)))
(p/mcat executor persist-fonts) assets (persist-fonts-files! data)
(p/fmap executor insert-into-db) result (insert-font-variant! assets)]
(p/fmap (fn [result] (vary-meta result assoc ::audit/replace-props (update params :data (comp vec keys))))))
(let [params (update params :data (comp vec keys))]
(rph/with-meta result {::audit/replace-props params})))))))
;; --- UPDATE FONT FAMILY ;; --- UPDATE FONT FAMILY

View File

@ -22,13 +22,9 @@
[app.storage :as sto] [app.storage :as sto]
[app.storage.tmp :as tmp] [app.storage.tmp :as tmp]
[app.util.services :as sv] [app.util.services :as sv]
[app.util.time :as dt]
[app.worker :as-alias wrk]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[cuerdas.core :as str] [cuerdas.core :as str]
[datoteka.io :as io] [datoteka.io :as io]))
[promesa.core :as p]
[promesa.exec :as px]))
(def default-max-file-size (def default-max-file-size
(* 1024 1024 10)) ; 10 MiB (* 1024 1024 10)) ; 10 MiB
@ -110,71 +106,62 @@
;; witch holds the reference to storage object (it some kind of ;; witch holds the reference to storage object (it some kind of
;; inverse, soft referential integrity). ;; inverse, soft referential integrity).
(defn- process-main-image
[info]
(let [hash (sto/calculate-hash (:path info))
data (-> (sto/content (:path info))
(sto/wrap-with-hash hash))]
{::sto/content data
::sto/deduplicate? true
::sto/touched-at (:ts info)
:content-type (:mtype info)
:bucket "file-media-object"}))
(defn- process-thumb-image
[info]
(let [thumb (-> thumbnail-options
(assoc :cmd :generic-thumbnail)
(assoc :input info)
(media/run))
hash (sto/calculate-hash (:data thumb))
data (-> (sto/content (:data thumb) (:size thumb))
(sto/wrap-with-hash hash))]
{::sto/content data
::sto/deduplicate? true
::sto/touched-at (:ts info)
:content-type (:mtype thumb)
:bucket "file-media-object"}))
(defn- process-image
[content]
(let [info (media/run {:cmd :info :input content})]
(cond-> info
(and (not (svg-image? info))
(big-enough-for-thumbnail? info))
(assoc ::thumb (process-thumb-image info))
:always
(assoc ::image (process-main-image info)))))
(defn create-file-media-object (defn create-file-media-object
[{:keys [::sto/storage ::db/pool climit ::wrk/executor]} [{:keys [::sto/storage ::db/pool] :as cfg}
{:keys [id file-id is-local name content]}] {:keys [id file-id is-local name content]}]
(letfn [;; Function responsible to retrieve the file information, as
;; it is synchronous operation it should be wrapped into
;; with-dispatch macro.
(get-info [content]
(climit/with-dispatch (:process-image climit)
(media/run {:cmd :info :input content})))
;; Function responsible of calculating cryptographyc hash of (let [result (-> (climit/configure cfg :process-image)
;; the provided data. (climit/submit! (partial process-image content)))
(calculate-hash [data]
(px/with-dispatch executor
(sto/calculate-hash data)))
;; Function responsible of generating thumnail. As it is synchronous image (sto/put-object! storage (::image result))
;; opetation, it should be wrapped into with-dispatch macro thumb (when-let [params (::thumb result)]
(generate-thumbnail [info] (sto/put-object! storage params))]
(climit/with-dispatch (:process-image climit)
(media/run (assoc thumbnail-options
:cmd :generic-thumbnail
:input info))))
(create-thumbnail [info] (db/exec-one! pool [sql:create-file-media-object
(when (and (not (svg-image? info)) (or id (uuid/next))
(big-enough-for-thumbnail? info)) file-id is-local name
(p/let [thumb (generate-thumbnail info) (:id image)
hash (calculate-hash (:data thumb)) (:id thumb)
content (-> (sto/content (:data thumb) (:size thumb)) (:width result)
(sto/wrap-with-hash hash))] (:height result)
(sto/put-object! storage (:mtype result)])))
{::sto/content content
::sto/deduplicate? true
::sto/touched-at (dt/now)
:content-type (:mtype thumb)
:bucket "file-media-object"}))))
(create-image [info]
(p/let [data (:path info)
hash (calculate-hash data)
content (-> (sto/content data)
(sto/wrap-with-hash hash))]
(sto/put-object! storage
{::sto/content content
::sto/deduplicate? true
::sto/touched-at (dt/now)
:content-type (:mtype info)
:bucket "file-media-object"})))
(insert-into-database [info image thumb]
(px/with-dispatch executor
(db/exec-one! pool [sql:create-file-media-object
(or id (uuid/next))
file-id is-local name
(:id image)
(:id thumb)
(:width info)
(:height info)
(:mtype info)])))]
(p/let [info (get-info content)
thumb (create-thumbnail info)
image (create-image info)]
(insert-into-database info image thumb))))
;; --- Create File Media Object (from URL) ;; --- Create File Media Object (from URL)
@ -192,9 +179,9 @@
(files/check-edition-permissions! pool profile-id file-id) (files/check-edition-permissions! pool profile-id file-id)
(create-file-media-object-from-url cfg params))) (create-file-media-object-from-url cfg params)))
(defn- create-file-media-object-from-url (defn- download-image
[cfg {:keys [url name] :as params}] [{:keys [::http/client]} uri]
(letfn [(parse-and-validate-size [headers] (letfn [(parse-and-validate [{:keys [headers] :as response}]
(let [size (some-> (get headers "content-length") d/parse-integer) (let [size (some-> (get headers "content-length") d/parse-integer)
mtype (get headers "content-type") mtype (get headers "content-type")
format (cm/mtype->format mtype) format (cm/mtype->format mtype)
@ -217,32 +204,34 @@
:code :media-type-not-allowed :code :media-type-not-allowed
:hint "seems like the url points to an invalid media object")) :hint "seems like the url points to an invalid media object"))
{:size size {:size size :mtype mtype :format format}))]
:mtype mtype
:format format}))
(download-media [uri] (let [{:keys [body] :as response} (http/req! client
(-> (http/req! cfg {:method :get :uri uri} {:response-type :input-stream}) {:method :get :uri uri}
(p/then process-response))) {:response-type :input-stream :sync? true})
{:keys [size mtype]} (parse-and-validate response)
(process-response [{:keys [body headers] :as response}] path (tmp/tempfile :prefix "penpot.media.download.")
(let [{:keys [size mtype]} (parse-and-validate-size headers) written (io/write-to-file! body path :size size)]
path (tmp/tempfile :prefix "penpot.media.download.")
written (io/write-to-file! body path :size size)]
(when (not= written size) (when (not= written size)
(ex/raise :type :internal (ex/raise :type :internal
:code :mismatch-write-size :code :mismatch-write-size
:hint "unexpected state: unable to write to file")) :hint "unexpected state: unable to write to file"))
{:filename "tempfile" {:filename "tempfile"
:size size :size size
:path path :path path
:mtype mtype}))] :mtype mtype})))
(p/let [content (download-media url)]
(->> (merge params {:content content :name (or name (:filename content))}) (defn- create-file-media-object-from-url
(create-file-media-object cfg))))) [cfg {:keys [url name] :as params}]
(let [content (download-image cfg url)
params (-> params
(assoc :content content)
(assoc :name (or name (:filename content))))]
(create-file-media-object cfg params)))
;; --- Clone File Media object (Upload and create from url) ;; --- Clone File Media object (Upload and create from url)

View File

@ -26,17 +26,16 @@
[app.tokens :as tokens] [app.tokens :as tokens]
[app.util.services :as sv] [app.util.services :as sv]
[app.util.time :as dt] [app.util.time :as dt]
[app.worker :as-alias wrk]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[cuerdas.core :as str] [cuerdas.core :as str]))
[promesa.core :as p]
[promesa.exec :as px]))
(declare check-profile-existence!)
(declare decode-row) (declare decode-row)
(declare derive-password)
(declare filter-props)
(declare get-profile) (declare get-profile)
(declare strip-private-attrs) (declare strip-private-attrs)
(declare filter-props) (declare verify-password)
(declare check-profile-existence!)
;; --- QUERY: Get profile (own) ;; --- QUERY: Get profile (own)
@ -50,6 +49,7 @@
;; We need to return the anonymous profile object in two cases, when ;; We need to return the anonymous profile object in two cases, when
;; no profile-id is in session, and when db call raises not found. In all other ;; no profile-id is in session, and when db call raises not found. In all other
;; cases we need to reraise the exception. ;; cases we need to reraise the exception.
(try (try
(-> (get-profile pool profile-id) (-> (get-profile pool profile-id)
(strip-private-attrs) (strip-private-attrs)
@ -120,10 +120,10 @@
:req-un [::password ::old-password])) :req-un [::password ::old-password]))
(sv/defmethod ::update-profile-password (sv/defmethod ::update-profile-password
{::climit/queue :auth}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id password] :as params}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id password] :as params}]
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
(let [profile (validate-password! conn (assoc params :profile-id profile-id)) (let [cfg (assoc cfg ::db/conn conn)
profile (validate-password! cfg (assoc params :profile-id profile-id))
session-id (::session/id params)] session-id (::session/id params)]
(when (= (str/lower (:email profile)) (when (= (str/lower (:email profile))
@ -132,29 +132,30 @@
:code :email-as-password :code :email-as-password
:hint "you can't use your email as password")) :hint "you can't use your email as password"))
(update-profile-password! conn (assoc profile :password password)) (update-profile-password! cfg (assoc profile :password password))
(invalidate-profile-session! conn profile-id session-id) (invalidate-profile-session! cfg profile-id session-id)
nil))) nil)))
(defn- invalidate-profile-session! (defn- invalidate-profile-session!
"Removes all sessions except the current one." "Removes all sessions except the current one."
[conn profile-id session-id] [{:keys [::db/conn]} profile-id session-id]
(let [sql "delete from http_session where profile_id = ? and id != ?"] (let [sql "delete from http_session where profile_id = ? and id != ?"]
(:next.jdbc/update-count (db/exec-one! conn [sql profile-id session-id])))) (:next.jdbc/update-count (db/exec-one! conn [sql profile-id session-id]))))
(defn- validate-password! (defn- validate-password!
[conn {:keys [profile-id old-password] :as params}] [{:keys [::db/conn] :as cfg} {:keys [profile-id old-password] :as params}]
(let [profile (db/get-by-id conn :profile profile-id ::db/for-update? true)] (let [profile (db/get-by-id conn :profile profile-id ::db/for-update? true)]
(when-not (:valid (auth/verify-password old-password (:password profile))) (when-not (:valid (verify-password cfg old-password (:password profile)))
(ex/raise :type :validation (ex/raise :type :validation
:code :old-password-not-match)) :code :old-password-not-match))
profile)) profile))
(defn update-profile-password! (defn update-profile-password!
[conn {:keys [id password] :as profile}] [{:keys [::db/conn] :as cfg} {:keys [id password] :as profile}]
(db/update! conn :profile (let [password (derive-password cfg password)]
{:password (auth/derive-password password)} (db/update! conn :profile
{:id id})) {:password password}
{:id id})))
;; --- MUTATION: Update Photo ;; --- MUTATION: Update Photo
@ -173,61 +174,49 @@
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)] (let [cfg (update cfg ::sto/storage media/configure-assets-storage)]
(update-profile-photo cfg (assoc params :profile-id profile-id)))) (update-profile-photo cfg (assoc params :profile-id profile-id))))
;; TODO: reimplement it without p/let
(defn update-profile-photo (defn update-profile-photo
[{:keys [::db/pool ::sto/storage ::wrk/executor] :as cfg} {:keys [profile-id file] :as params}] [{:keys [::db/pool ::sto/storage] :as cfg} {:keys [profile-id file] :as params}]
(letfn [(on-uploaded [photo] (let [photo (upload-photo cfg params)
(let [profile (db/get-by-id pool :profile profile-id ::db/for-update? true)] profile (db/get-by-id pool :profile profile-id ::db/for-update? true)]
;; Schedule deletion of old photo ;; Schedule deletion of old photo
(when-let [id (:photo-id profile)] (when-let [id (:photo-id profile)]
(sto/touch-object! storage id)) (sto/touch-object! storage id))
;; Save new photo ;; Save new photo
(db/update! pool :profile (db/update! pool :profile
{:photo-id (:id photo)} {:photo-id (:id photo)}
{:id profile-id}) {:id profile-id})
(-> (rph/wrap) (-> (rph/wrap)
(rph/with-meta {::audit/replace-props (rph/with-meta {::audit/replace-props
{:file-name (:filename file) {:file-name (:filename file)
:file-size (:size file) :file-size (:size file)
:file-path (str (:path file)) :file-path (str (:path file))
:file-mtype (:mtype file)}}))))] :file-mtype (:mtype file)}}))))
(->> (upload-photo cfg params)
(p/fmap executor on-uploaded)))) (defn- generate-thumbnail!
[file]
(let [input (media/run {:cmd :info :input file})
thumb (media/run {:cmd :profile-thumbnail
:format :jpeg
:quality 85
:width 256
:height 256
:input input})
hash (sto/calculate-hash (:data thumb))
content (-> (sto/content (:data thumb) (:size thumb))
(sto/wrap-with-hash hash))]
{::sto/content content
::sto/deduplicate? true
:bucket "profile"
:content-type (:mtype thumb)}))
(defn upload-photo (defn upload-photo
[{:keys [::sto/storage ::wrk/executor climit] :as cfg} {:keys [file]}] [{:keys [::sto/storage] :as cfg} {:keys [file]}]
(letfn [(get-info [content] (let [params (-> (climit/configure cfg :process-image)
(climit/with-dispatch (:process-image climit) (climit/submit! (partial generate-thumbnail! file)))]
(media/run {:cmd :info :input content}))) (sto/put-object! storage params)))
(generate-thumbnail [info]
(climit/with-dispatch (:process-image climit)
(media/run {:cmd :profile-thumbnail
:format :jpeg
:quality 85
:width 256
:height 256
:input info})))
;; Function responsible of calculating cryptographyc hash of
;; the provided data.
(calculate-hash [data]
(px/with-dispatch executor
(sto/calculate-hash data)))]
(p/let [info (get-info file)
thumb (generate-thumbnail info)
hash (calculate-hash (:data thumb))
content (-> (sto/content (:data thumb) (:size thumb))
(sto/wrap-with-hash hash))]
(sto/put-object! storage {::sto/content content
::sto/deduplicate? true
:bucket "profile"
:content-type (:mtype thumb)}))))
;; --- MUTATION: Request Email Change ;; --- MUTATION: Request Email Change
@ -417,6 +406,17 @@
[props] [props]
(into {} (filter (fn [[k _]] (simple-ident? k))) props)) (into {} (filter (fn [[k _]] (simple-ident? k))) props))
(defn derive-password
[cfg password]
(when password
(-> (climit/configure cfg :derive-password)
(climit/submit! (partial auth/derive-password password)))))
(defn verify-password
[cfg password password-data]
(-> (climit/configure cfg :derive-password)
(climit/submit! (partial auth/verify-password password password-data))))
(defn decode-row (defn decode-row
[{:keys [props] :as row}] [{:keys [props] :as row}]
(cond-> row (cond-> row

View File

@ -6,6 +6,7 @@
(ns app.rpc.commands.projects (ns app.rpc.commands.projects
(:require (:require
[app.common.data.macros :as dm]
[app.common.spec :as us] [app.common.spec :as us]
[app.db :as db] [app.db :as db]
[app.loggers.audit :as-alias audit] [app.loggers.audit :as-alias audit]
@ -79,7 +80,7 @@
(sv/defmethod ::get-projects (sv/defmethod ::get-projects
{::doc/added "1.18"} {::doc/added "1.18"}
[{:keys [::db/pool]} {:keys [::rpc/profile-id team-id]}] [{:keys [::db/pool]} {:keys [::rpc/profile-id team-id]}]
(with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(teams/check-read-permissions! conn profile-id team-id) (teams/check-read-permissions! conn profile-id team-id)
(get-projects conn profile-id team-id))) (get-projects conn profile-id team-id)))
@ -114,7 +115,7 @@
(sv/defmethod ::get-all-projects (sv/defmethod ::get-all-projects
{::doc/added "1.18"} {::doc/added "1.18"}
[{:keys [::db/pool]} {:keys [::rpc/profile-id]}] [{:keys [::db/pool]} {:keys [::rpc/profile-id]}]
(with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(get-all-projects conn profile-id))) (get-all-projects conn profile-id)))
(def sql:all-projects (def sql:all-projects
@ -157,7 +158,7 @@
(sv/defmethod ::get-project (sv/defmethod ::get-project
{::doc/added "1.18"} {::doc/added "1.18"}
[{:keys [::db/pool]} {:keys [::rpc/profile-id id]}] [{:keys [::db/pool]} {:keys [::rpc/profile-id id]}]
(with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(let [project (db/get-by-id conn :project id)] (let [project (db/get-by-id conn :project id)]
(check-read-permissions! conn profile-id id) (check-read-permissions! conn profile-id id)
project))) project)))

View File

@ -7,6 +7,7 @@
(ns app.rpc.commands.teams (ns app.rpc.commands.teams
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.logging :as l] [app.common.logging :as l]
[app.common.spec :as us] [app.common.spec :as us]
@ -27,11 +28,8 @@
[app.tokens :as tokens] [app.tokens :as tokens]
[app.util.services :as sv] [app.util.services :as sv]
[app.util.time :as dt] [app.util.time :as dt]
[app.worker :as-alias wrk]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[cuerdas.core :as str] [cuerdas.core :as str]))
[promesa.core :as p]
[promesa.exec :as px]))
;; --- Helpers & Specs ;; --- Helpers & Specs
@ -78,13 +76,15 @@
(declare retrieve-teams) (declare retrieve-teams)
(def counter (volatile! 0))
(s/def ::get-teams (s/def ::get-teams
(s/keys :req [::rpc/profile-id])) (s/keys :req [::rpc/profile-id]))
(sv/defmethod ::get-teams (sv/defmethod ::get-teams
{::doc/added "1.17"} {::doc/added "1.17"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
(with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(retrieve-teams conn profile-id))) (retrieve-teams conn profile-id)))
(def sql:teams (def sql:teams
@ -129,7 +129,7 @@
(sv/defmethod ::get-team (sv/defmethod ::get-team
{::doc/added "1.17"} {::doc/added "1.17"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id]}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id]}]
(with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(retrieve-team conn profile-id id))) (retrieve-team conn profile-id id)))
(defn retrieve-team (defn retrieve-team
@ -170,7 +170,7 @@
(sv/defmethod ::get-team-members (sv/defmethod ::get-team-members
{::doc/added "1.17"} {::doc/added "1.17"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
(with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(check-read-permissions! conn profile-id team-id) (check-read-permissions! conn profile-id team-id)
(retrieve-team-members conn team-id))) (retrieve-team-members conn team-id)))
@ -188,7 +188,7 @@
(sv/defmethod ::get-team-users (sv/defmethod ::get-team-users
{::doc/added "1.17"} {::doc/added "1.17"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id file-id]}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id file-id]}]
(with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(if team-id (if team-id
(do (do
(check-read-permissions! conn profile-id team-id) (check-read-permissions! conn profile-id team-id)
@ -246,7 +246,7 @@
(sv/defmethod ::get-team-stats (sv/defmethod ::get-team-stats
{::doc/added "1.17"} {::doc/added "1.17"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
(with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(check-read-permissions! conn profile-id team-id) (check-read-permissions! conn profile-id team-id)
(retrieve-team-stats conn team-id))) (retrieve-team-stats conn team-id)))
@ -277,7 +277,7 @@
(sv/defmethod ::get-team-invitations (sv/defmethod ::get-team-invitations
{::doc/added "1.17"} {::doc/added "1.17"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
(with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(check-read-permissions! conn profile-id team-id) (check-read-permissions! conn profile-id team-id)
(get-team-invitations conn team-id))) (get-team-invitations conn team-id)))
@ -588,10 +588,9 @@
(update-team-photo cfg (assoc params :profile-id profile-id)))) (update-team-photo cfg (assoc params :profile-id profile-id))))
(defn update-team-photo (defn update-team-photo
[{:keys [::db/pool ::sto/storage ::wrk/executor] :as cfg} {:keys [profile-id team-id] :as params}] [{:keys [::db/pool ::sto/storage] :as cfg} {:keys [profile-id team-id] :as params}]
(p/let [team (px/with-dispatch executor (let [team (retrieve-team pool profile-id team-id)
(retrieve-team pool profile-id team-id)) photo (profile/upload-photo cfg params)]
photo (profile/upload-photo cfg params)]
;; Mark object as touched for make it ellegible for tentative ;; Mark object as touched for make it ellegible for tentative
;; garbage collection. ;; garbage collection.
@ -694,13 +693,13 @@
(l/info :hint "invitation token" :token itoken)) (l/info :hint "invitation token" :token itoken))
(audit/submit! cfg (audit/submit! cfg
{:type "action" {::audit/type "action"
:name (if updated? ::audit/name (if updated?
"update-team-invitation" "update-team-invitation"
"create-team-invitation") "create-team-invitation")
:profile-id (:id profile) ::audit/profile-id (:id profile)
:props (-> (dissoc tprops :profile-id) ::audit/props (-> (dissoc tprops :profile-id)
(d/without-nils))}) (d/without-nils))})
(eml/send! {::eml/conn conn (eml/send! {::eml/conn conn
::eml/factory eml/invite-to-team ::eml/factory eml/invite-to-team
@ -802,13 +801,13 @@
::quotes/incr (count emails)})) ::quotes/incr (count emails)}))
(audit/submit! cfg (audit/submit! cfg
{:type "command" {::audit/type "command"
:name "create-team-invitations" ::audit/name "create-team-invitations"
:profile-id profile-id ::audit/profile-id profile-id
:props {:emails emails ::audit/props {:emails emails
:role role :role role
:profile-id profile-id :profile-id profile-id
:invitations (count emails)}}) :invitations (count emails)}})
(vary-meta team assoc ::audit/props {:invitations (count emails)})))) (vary-meta team assoc ::audit/props {:invitations (count emails)}))))

View File

@ -6,6 +6,7 @@
(ns app.rpc.commands.viewer (ns app.rpc.commands.viewer
(:require (:require
[app.common.data.macros :as dm]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.db :as db] [app.db :as db]
[app.rpc :as-alias rpc] [app.rpc :as-alias rpc]
@ -85,5 +86,5 @@
::cond/reuse-key? true ::cond/reuse-key? true
::doc/added "1.17"} ::doc/added "1.17"}
[{:keys [::db/pool]} {:keys [::rpc/profile-id] :as params}] [{:keys [::db/pool]} {:keys [::rpc/profile-id] :as params}]
(with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(get-view-only-bundle conn (assoc params :profile-id profile-id)))) (get-view-only-bundle conn (assoc params :profile-id profile-id))))

View File

@ -6,6 +6,7 @@
(ns app.rpc.commands.webhooks (ns app.rpc.commands.webhooks
(:require (:require
[app.common.data.macros :as dm]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.spec :as us] [app.common.spec :as us]
[app.common.uri :as u] [app.common.uri :as u]
@ -18,10 +19,8 @@
[app.rpc.doc :as-alias doc] [app.rpc.doc :as-alias doc]
[app.util.services :as sv] [app.util.services :as sv]
[app.util.time :as dt] [app.util.time :as dt]
[app.worker :as-alias wrk]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[cuerdas.core :as str] [cuerdas.core :as str]))
[promesa.core :as p]))
(defn decode-row (defn decode-row
[{:keys [uri] :as row}] [{:keys [uri] :as row}]
@ -48,30 +47,26 @@
(defn- validate-webhook! (defn- validate-webhook!
[cfg whook params] [cfg whook params]
(letfn [(handle-exception [exception] (when (not= (:uri whook) (:uri params))
(if-let [hint (webhooks/interpret-exception exception)] (try
(ex/raise :type :validation (let [response (http/req! cfg
:code :webhook-validation {:method :head
:hint hint) :uri (str (:uri params))
(ex/raise :type :internal :timeout (dt/duration "3s")}
:code :webhook-validation {:sync? true})]
:cause exception))) (when-let [hint (webhooks/interpret-response response)]
(ex/raise :type :validation
:code :webhook-validation
:hint hint)))
(handle-response [response] (catch Throwable cause
(when-let [hint (webhooks/interpret-response response)] (if-let [hint (webhooks/interpret-exception cause)]
(ex/raise :type :validation (ex/raise :type :validation
:code :webhook-validation :code :webhook-validation
:hint hint)))] :hint hint)
(ex/raise :type :internal
(if (not= (:uri whook) (:uri params)) :code :webhook-validation
(->> (http/req! cfg {:method :head :cause cause))))))
:uri (str (:uri params))
:timeout (dt/duration "3s")})
(p/hmap (fn [response exception]
(if exception
(handle-exception exception)
(handle-response response)))))
(p/resolved nil))))
(defn- validate-quotes! (defn- validate-quotes!
[{:keys [::db/pool]} {:keys [team-id]}] [{:keys [::db/pool]} {:keys [team-id]}]
@ -106,22 +101,22 @@
(sv/defmethod ::create-webhook (sv/defmethod ::create-webhook
{::doc/added "1.17"} {::doc/added "1.17"}
[{:keys [::db/pool ::wrk/executor] :as cfg} {:keys [::rpc/profile-id team-id] :as params}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}]
(check-edition-permissions! pool profile-id team-id) (check-edition-permissions! pool profile-id team-id)
(validate-quotes! cfg params) (validate-quotes! cfg params)
(->> (validate-webhook! cfg nil params) (validate-webhook! cfg nil params)
(p/fmap executor (fn [_] (insert-webhook! cfg params))))) (insert-webhook! cfg params))
(s/def ::update-webhook (s/def ::update-webhook
(s/keys :req-un [::id ::uri ::mtype ::is-active])) (s/keys :req-un [::id ::uri ::mtype ::is-active]))
(sv/defmethod ::update-webhook (sv/defmethod ::update-webhook
{::doc/added "1.17"} {::doc/added "1.17"}
[{:keys [::db/pool ::wrk/executor] :as cfg} {:keys [::rpc/profile-id id] :as params}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
(let [whook (-> (db/get pool :webhook {:id id}) (decode-row))] (let [whook (-> (db/get pool :webhook {:id id}) (decode-row))]
(check-edition-permissions! pool profile-id (:team-id whook)) (check-edition-permissions! pool profile-id (:team-id whook))
(->> (validate-webhook! cfg whook params) (validate-webhook! cfg whook params)
(p/fmap executor (fn [_] (update-webhook! cfg whook params)))))) (update-webhook! cfg whook params)))
(s/def ::delete-webhook (s/def ::delete-webhook
(s/keys :req [::rpc/profile-id] (s/keys :req [::rpc/profile-id]
@ -149,7 +144,7 @@
(sv/defmethod ::get-webhooks (sv/defmethod ::get-webhooks
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
(with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(check-read-permissions! conn profile-id team-id) (check-read-permissions! conn profile-id team-id)
(->> (db/exec! conn [sql:get-webhooks team-id]) (->> (db/exec! conn [sql:get-webhooks team-id])
(mapv decode-row)))) (mapv decode-row))))

View File

@ -27,8 +27,6 @@
[app.common.logging :as l] [app.common.logging :as l]
[app.rpc.helpers :as rph] [app.rpc.helpers :as rph]
[app.util.services :as-alias sv] [app.util.services :as-alias sv]
[promesa.core :as p]
[promesa.exec :as px]
[yetti.response :as yrs])) [yetti.response :as yrs]))
(def (def
@ -38,30 +36,24 @@
(defn- fmt-key (defn- fmt-key
[s] [s]
(when s (str "W/\"" s "\""))
(str "W/\"" s "\"")))
(defn wrap (defn wrap
[{:keys [executor]} f {:keys [::get-object ::key-fn ::reuse-key?] :as mdata}] [_ f {:keys [::get-object ::key-fn ::reuse-key?] :as mdata}]
(if (and (ifn? get-object) (ifn? key-fn)) (if (and (ifn? get-object) (ifn? key-fn))
(do (do
(l/debug :hint "instrumenting method" :service (::sv/name mdata)) (l/debug :hint "instrumenting method" :service (::sv/name mdata))
(fn [cfg {:keys [::key] :as params}] (fn [cfg {:keys [::key] :as params}]
(if *enabled* (if *enabled*
(->> (if (or key reuse-key?) (let [key' (when (or key reuse-key?)
(->> (px/submit! executor (partial get-object cfg params)) (some-> (get-object cfg params) key-fn fmt-key))]
(p/map key-fn) (if (and (some? key)
(p/map fmt-key)) (= key key'))
(p/resolved nil)) (fn [_] {::yrs/status 304})
(p/mapcat (fn [key'] (let [result (f cfg params)
(if (and (some? key) etag (or (and reuse-key? key')
(= key key')) (some-> result meta ::key fmt-key)
(p/resolved (fn [_] (yrs/response 304))) (some-> result key-fn fmt-key))]
(->> (f cfg params) (rph/with-header result "etag" etag))))
(p/map (fn [result]
(->> (or (and reuse-key? key')
(-> result meta ::key fmt-key)
(-> result key-fn fmt-key))
(rph/with-header result "etag")))))))))
(f cfg params)))) (f cfg params))))
f)) f))

View File

@ -30,32 +30,34 @@
(defn- prepare-context (defn- prepare-context
[methods] [methods]
(letfn [(gen-doc [type [name f]] (letfn [(gen-doc [type [{:keys [::sv/name] :as mdata} _f]]
(let [mdata (meta f)] {:type (d/name type)
{:type (d/name type) :name (d/name name)
:name (d/name name) :module (-> (:ns mdata) (str/split ".") last)
:module (-> (:ns mdata) (str/split ".") last) :auth (:auth mdata true)
:auth (:auth mdata true) :webhook (::webhooks/event? mdata false)
:webhook (::webhooks/event? mdata false) :docs (::sv/docstring mdata)
:docs (::sv/docstring mdata) :deprecated (::deprecated mdata)
:deprecated (::deprecated mdata) :added (::added mdata)
:added (::added mdata) :changes (some->> (::changes mdata) (partition-all 2) (map vec))
:changes (some->> (::changes mdata) (partition-all 2) (map vec)) :spec (get-spec-str (::sv/spec mdata))})]
:spec (get-spec-str (::sv/spec mdata))}))]
{:version (:main cf/version) {:version (:main cf/version)
:command-methods :command-methods
(->> (:commands methods) (->> (:commands methods)
(map val)
(map (partial gen-doc :command)) (map (partial gen-doc :command))
(sort-by (juxt :module :name))) (sort-by (juxt :module :name)))
:query-methods :query-methods
(->> (:queries methods) (->> (:queries methods)
(map val)
(map (partial gen-doc :query)) (map (partial gen-doc :query))
(sort-by (juxt :module :name))) (sort-by (juxt :module :name)))
:mutation-methods :mutation-methods
(->> (:mutations methods) (->> (:mutations methods)
(map val)
(map (partial gen-doc :query)) (map (partial gen-doc :query))
(sort-by (juxt :module :name)))})) (sort-by (juxt :module :name)))}))
@ -64,11 +66,11 @@
(if (contains? cf/flags :backend-api-doc) (if (contains? cf/flags :backend-api-doc)
(let [context (prepare-context methods)] (let [context (prepare-context methods)]
(fn [_ respond _] (fn [_ respond _]
(respond (yrs/response 200 (-> (io/resource "app/templates/api-doc.tmpl") (respond {::yrs/status 200
(tmpl/render context)))))) ::yrs/body (-> (io/resource "app/templates/api-doc.tmpl")
(tmpl/render context))})))
(fn [_ respond _] (fn [_ respond _]
(respond (yrs/response 404))))) (respond {::yrs/status 404}))))
(s/def ::routes vector?) (s/def ::routes vector?)

View File

@ -10,7 +10,8 @@
(:require (:require
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.http :as-alias http] [app.http :as-alias http]
[app.rpc :as-alias rpc])) [app.rpc :as-alias rpc]
[yetti.response :as-alias yrs]))
;; A utilty wrapper object for wrap service responses that does not ;; A utilty wrapper object for wrap service responses that does not
;; implements the IObj interface that make possible attach metadata to ;; implements the IObj interface that make possible attach metadata to
@ -35,7 +36,9 @@
o o
(MetadataWrapper. o {}))) (MetadataWrapper. o {})))
([o m] ([o m]
(MetadataWrapper. o m))) (if (instance? clojure.lang.IObj o)
(vary-meta o merge m)
(MetadataWrapper. o m))))
(defn wrapped? (defn wrapped?
[o] [o]
@ -74,4 +77,4 @@
(fn [_ response] (fn [_ response]
(let [exp (if (integer? max-age) max-age (inst-ms max-age)) (let [exp (if (integer? max-age) max-age (inst-ms max-age))
val (dm/fmt "max-age=%" (int (/ exp 1000.0)))] val (dm/fmt "max-age=%" (int (/ exp 1000.0)))]
(update response :headers assoc "cache-control" val))))) (update response ::yrs/headers assoc "cache-control" val)))))

View File

@ -14,7 +14,6 @@
[app.http.session :as session] [app.http.session :as session]
[app.loggers.audit :as audit] [app.loggers.audit :as audit]
[app.media :as media] [app.media :as media]
[app.rpc.climit :as-alias climit]
[app.rpc.commands.profile :as profile] [app.rpc.commands.profile :as profile]
[app.rpc.doc :as-alias doc] [app.rpc.doc :as-alias doc]
[app.rpc.helpers :as rph] [app.rpc.helpers :as rph]
@ -78,20 +77,20 @@
(s/keys :req-un [::profile-id ::password ::old-password])) (s/keys :req-un [::profile-id ::password ::old-password]))
(sv/defmethod ::update-profile-password (sv/defmethod ::update-profile-password
{::climit/queue :auth {::doc/added "1.0"
::doc/added "1.0"
::doc/deprecated "1.18"} ::doc/deprecated "1.18"}
[{:keys [::db/pool] :as cfg} {:keys [password] :as params}] [{:keys [::db/pool] :as cfg} {:keys [password] :as params}]
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
(let [profile (#'profile/validate-password! conn params) (let [cfg (assoc cfg ::db/conn conn)
profile (#'profile/validate-password! cfg params)
session-id (::session/id params)] session-id (::session/id params)]
(when (= (str/lower (:email profile)) (when (= (str/lower (:email profile))
(str/lower (:password params))) (str/lower (:password params)))
(ex/raise :type :validation (ex/raise :type :validation
:code :email-as-password :code :email-as-password
:hint "you can't use your email as password")) :hint "you can't use your email as password"))
(profile/update-profile-password! conn (assoc profile :password password)) (profile/update-profile-password! cfg (assoc profile :password password))
(#'profile/invalidate-profile-session! conn (:id profile) session-id) (#'profile/invalidate-profile-session! cfg (:id profile) session-id)
nil))) nil)))

View File

@ -10,8 +10,7 @@
(:require (:require
[app.common.logging :as l] [app.common.logging :as l]
[app.util.retry :refer [conflict-exception?]] [app.util.retry :refer [conflict-exception?]]
[app.util.services :as sv] [app.util.services :as sv]))
[promesa.core :as p]))
(defn conflict-db-insert? (defn conflict-db-insert?
"Check if exception matches a insertion conflict on postgresql." "Check if exception matches a insertion conflict on postgresql."
@ -28,18 +27,16 @@
(if-let [max-retries (::max-retries mdata)] (if-let [max-retries (::max-retries mdata)]
(fn [cfg params] (fn [cfg params]
(letfn [(run [retry] ((fn run [retry]
(->> (f cfg params) (try
(p/merr (partial handle-error retry)))) (f cfg params)
(catch Throwable cause
(handle-error [retry cause] (if (matches cause)
(if (matches cause) (let [current-retry (inc retry)]
(let [current-retry (inc retry)] (l/trace :hint "running retry algorithm" :retry current-retry)
(l/trace :hint "running retry algorithm" :retry current-retry) (if (<= current-retry max-retries)
(if (<= current-retry max-retries) (run current-retry)
(run current-retry) (throw cause)))
(throw cause))) (throw cause))))) 1))
(throw cause)))]
(run 1)))
f)) f))

View File

@ -55,6 +55,7 @@
[app.redis :as rds] [app.redis :as rds]
[app.redis.script :as-alias rscript] [app.redis.script :as-alias rscript]
[app.rpc :as-alias rpc] [app.rpc :as-alias rpc]
[app.rpc.helpers :as rph]
[app.rpc.rlimit.result :as-alias lresult] [app.rpc.rlimit.result :as-alias lresult]
[app.util.services :as-alias sv] [app.util.services :as-alias sv]
[app.util.time :as dt] [app.util.time :as dt]
@ -64,7 +65,6 @@
[cuerdas.core :as str] [cuerdas.core :as str]
[datoteka.fs :as fs] [datoteka.fs :as fs]
[integrant.core :as ig] [integrant.core :as ig]
[promesa.core :as p]
[promesa.exec :as px])) [promesa.exec :as px]))
(def ^:private default-timeout (def ^:private default-timeout
@ -82,7 +82,7 @@
{::rscript/name ::window-rate-limit {::rscript/name ::window-rate-limit
::rscript/path "app/rpc/rlimit/window.lua"}) ::rscript/path "app/rpc/rlimit/window.lua"})
(def enabled? (def enabled
"Allows on runtime completely disable rate limiting." "Allows on runtime completely disable rate limiting."
(atom true)) (atom true))
@ -119,116 +119,97 @@
(defmethod parse-limit :bucket (defmethod parse-limit :bucket
[[name strategy opts :as vlimit]] [[name strategy opts :as vlimit]]
(us/assert! ::limit-tuple vlimit) (us/assert! ::limit-tuple vlimit)
(merge (if-let [[_ capacity rate interval] (re-find bucket-opts-re opts)]
{::name name (let [interval (dt/duration interval)
::strategy strategy} rate (parse-long rate)
(if-let [[_ capacity rate interval] (re-find bucket-opts-re opts)] capacity (parse-long capacity)]
(let [interval (dt/duration interval) {::name name
rate (parse-long rate) ::strategy strategy
capacity (parse-long capacity)] ::capacity capacity
{::capacity capacity ::rate rate
::rate rate ::interval interval
::interval interval ::opts opts
::opts opts ::params [(dt/->seconds interval) rate capacity]
::params [(dt/->seconds interval) rate capacity] ::key (str "ratelimit.bucket." (d/name name))})
::key (str "ratelimit.bucket." (d/name name))}) (ex/raise :type :validation
(ex/raise :type :validation :code :invalid-bucket-limit-opts
:code :invalid-bucket-limit-opts :hint (str/ffmt "looks like '%' does not have a valid format" opts))))
:hint (str/ffmt "looks like '%' does not have a valid format" opts)))))
(defmethod process-limit :bucket (defmethod process-limit :bucket
[redis user-id now {:keys [::key ::params ::service ::capacity ::interval ::rate] :as limit}] [redis user-id now {:keys [::key ::params ::service ::capacity ::interval ::rate] :as limit}]
(let [script (-> bucket-rate-limit-script (let [script (-> bucket-rate-limit-script
(assoc ::rscript/keys [(str key "." service "." user-id)]) (assoc ::rscript/keys [(str key "." service "." user-id)])
(assoc ::rscript/vals (conj params (dt/->seconds now))))] (assoc ::rscript/vals (conj params (dt/->seconds now))))
(->> (rds/eval! redis script) result (rds/eval! redis script)
(p/fmap (fn [result] allowed? (boolean (nth result 0))
(let [allowed? (boolean (nth result 0)) remaining (nth result 1)
remaining (nth result 1) reset (* (/ (inst-ms interval) rate)
reset (* (/ (inst-ms interval) rate) (- capacity remaining))]
(- capacity remaining))] (l/trace :hint "limit processed"
(l/trace :hint "limit processed" :service service
:service service :limit (name (::name limit))
:limit (name (::name limit)) :strategy (name (::strategy limit))
:strategy (name (::strategy limit)) :opts (::opts limit)
:opts (::opts limit) :allowed allowed?
:allowed? allowed? :remaining remaining)
:remaining remaining) (-> limit
(-> limit (assoc ::lresult/allowed allowed?)
(assoc ::lresult/allowed? allowed?) (assoc ::lresult/reset (dt/plus now reset))
(assoc ::lresult/reset (dt/plus now reset)) (assoc ::lresult/remaining remaining))))
(assoc ::lresult/remaining remaining))))))))
(defmethod process-limit :window (defmethod process-limit :window
[redis user-id now {:keys [::nreq ::unit ::key ::service] :as limit}] [redis user-id now {:keys [::nreq ::unit ::key ::service] :as limit}]
(let [ts (dt/truncate now unit) (let [ts (dt/truncate now unit)
ttl (dt/diff now (dt/plus ts {unit 1})) ttl (dt/diff now (dt/plus ts {unit 1}))
script (-> window-rate-limit-script script (-> window-rate-limit-script
(assoc ::rscript/keys [(str key "." service "." user-id "." (dt/format-instant ts))]) (assoc ::rscript/keys [(str key "." service "." user-id "." (dt/format-instant ts))])
(assoc ::rscript/vals [nreq (dt/->seconds ttl)]))] (assoc ::rscript/vals [nreq (dt/->seconds ttl)]))
(->> (rds/eval! redis script) result (rds/eval! redis script)
(p/fmap (fn [result] allowed? (boolean (nth result 0))
(let [allowed? (boolean (nth result 0)) remaining (nth result 1)]
remaining (nth result 1)] (l/trace :hint "limit processed"
(l/trace :hint "limit processed" :service service
:service service :limit (name (::name limit))
:limit (name (::name limit)) :strategy (name (::strategy limit))
:strategy (name (::strategy limit)) :opts (::opts limit)
:opts (::opts limit) :allowed allowed?
:allowed? allowed? :remaining remaining)
:remaining remaining) (-> limit
(-> limit (assoc ::lresult/allowed allowed?)
(assoc ::lresult/allowed? allowed?) (assoc ::lresult/remaining remaining)
(assoc ::lresult/remaining remaining) (assoc ::lresult/reset (dt/plus ts {unit 1})))))
(assoc ::lresult/reset (dt/plus ts {unit 1})))))))))
(defn- process-limits! (defn- process-limits!
[redis user-id limits now] [redis user-id limits now]
(->> (p/all (map (partial process-limit redis user-id now) limits)) (let [results (into [] (map (partial process-limit redis user-id now)) limits)
(p/fmap (fn [results] remaining (->> results
(let [remaining (->> results (d/index-by ::name ::lresult/remaining)
(d/index-by ::name ::lresult/remaining) (uri/map->query-string))
(uri/map->query-string)) reset (->> results
reset (->> results (d/index-by ::name (comp dt/->seconds ::lresult/reset))
(d/index-by ::name (comp dt/->seconds ::lresult/reset)) (uri/map->query-string))
(uri/map->query-string))
rejected (->> results
(filter (complement ::lresult/allowed?))
(first))]
(when rejected rejected (d/seek (complement ::lresult/allowed) results)]
(l/warn :hint "rejected rate limit"
:user-id (str user-id)
:limit-service (-> rejected ::service name)
:limit-name (-> rejected ::name name)
:limit-strategy (-> rejected ::strategy name)))
{:enabled? true (when rejected
:allowed? (not (some? rejected)) (l/warn :hint "rejected rate limit"
:headers {"x-rate-limit-remaining" remaining :user-id (str user-id)
"x-rate-limit-reset" reset}}))))) :limit-service (-> rejected ::service name)
:limit-name (-> rejected ::name name)
:limit-strategy (-> rejected ::strategy name)))
(defn- handle-response {::enabled true
[f cfg params result] ::allowed (not (some? rejected))
(if (:enabled? result) ::remaingin remaining
(let [headers (:headers result)] ::reset reset
(if (:allowed? result) ::headers {"x-rate-limit-remaining" remaining
(->> (f cfg params) "x-rate-limit-reset" reset}}))
(p/fmap (fn [response]
(vary-meta response update ::http/headers merge headers))))
(p/rejected
(ex/error :type :rate-limit
:code :request-blocked
:hint "rate limit reached"
::http/headers headers))))
(f cfg params)))
(defn- get-limits (defn- get-limits
[state skey sname] [state skey sname]
(some->> (or (get-in @state [::limits skey]) (when-let [limits (or (get-in @state [::limits skey])
(get-in @state [::limits :default])) (get-in @state [::limits :default]))]
(map #(assoc % ::service sname)) (into [] (map #(assoc % ::service sname)) limits)))
(seq)))
(defn- get-uid (defn- get-uid
[{:keys [::http/request] :as params}] [{:keys [::http/request] :as params}]
@ -236,6 +217,31 @@
(some-> request parse-client-ip) (some-> request parse-client-ip)
uuid/zero)) uuid/zero))
(defn process-request!
[{:keys [::rpc/rlimit ::rds/redis ::skey ::sname] :as cfg} params]
(when-let [limits (get-limits rlimit skey sname)]
(let [redis (rds/get-or-connect redis ::rpc/rlimit default-options)
uid (get-uid params)
;; FIXME: why not clasic try/catch?
result (ex/try! (process-limits! redis uid limits (dt/now)))]
(l/trc :hint "process-limits"
:service sname
:remaining (::remaingin result)
:reset (::reset result))
(cond
(ex/exception? result)
(do
(l/error :hint "error on processing rate-limit" :cause result)
{::enabled false})
(contains? cf/flags :soft-rpc-rlimit)
{::enabled false}
:else
result))))
(defn wrap (defn wrap
[{:keys [::rpc/rlimit ::rds/redis] :as cfg} f mdata] [{:keys [::rpc/rlimit ::rds/redis] :as cfg} f mdata]
(us/assert! ::rpc/rlimit rlimit) (us/assert! ::rpc/rlimit rlimit)
@ -243,36 +249,25 @@
(if rlimit (if rlimit
(let [skey (keyword (::rpc/type cfg) (->> mdata ::sv/spec name)) (let [skey (keyword (::rpc/type cfg) (->> mdata ::sv/spec name))
sname (str (::rpc/type cfg) "." (->> mdata ::sv/spec name))] sname (str (::rpc/type cfg) "." (->> mdata ::sv/spec name))
cfg (-> cfg
(assoc ::skey skey)
(assoc ::sname sname))]
(fn [cfg params] (fn [hcfg params]
(if @enabled? (if @enabled
(try (let [result (process-request! cfg params)]
(let [uid (get-uid params) (if (::enabled result)
rsp (when-let [limits (get-limits rlimit skey sname)] (if (::allowed result)
(let [redis (rds/get-or-connect redis ::rpc/rlimit default-options) (-> (f hcfg params)
rsp (->> (process-limits! redis uid limits (dt/now)) (rph/wrap)
(p/merr (fn [cause] (vary-meta update ::http/headers merge (::headers result)))
;; If we have an error on processing the rate-limit we just skip (ex/raise :type :rate-limit
;; it for do not cause service interruption because of redis :code :request-blocked
;; downtime or similar situation. :hint "rate limit reached"
(l/error :hint "error on processing rate-limit" :cause cause) ::http/headers (::headers result)))
(p/resolved {:enabled? false}))))] (f hcfg params)))
(f hcfg params))))
;; If soft rate are enabled, we process the rate-limit but return unprotected
;; response.
(if (contains? cf/flags :soft-rpc-rlimit)
{:enabled? false}
rsp)))]
(->> (p/promise rsp)
(p/fmap #(or % {:enabled? false}))
(p/mcat #(handle-response f cfg params %))))
(catch Throwable cause
(p/rejected cause)))
(f cfg params))))
f)) f))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -352,7 +347,7 @@
::limits limits})))) ::limits limits}))))
(defn- refresh-config (defn- refresh-config
[{:keys [::state ::path ::wrk/executor ::wrk/scheduled-executor] :as cfg}] [{:keys [::state ::path ::wrk/executor] :as cfg}]
(letfn [(update-config [{:keys [::updated-at] :as state}] (letfn [(update-config [{:keys [::updated-at] :as state}]
(let [updated-at' (fs/last-modified-time path)] (let [updated-at' (fs/last-modified-time path)]
(merge state (merge state
@ -367,8 +362,7 @@
state))))) state)))))
(schedule-next [state] (schedule-next [state]
(px/schedule! scheduled-executor (px/schedule! (inst-ms (::refresh state))
(inst-ms (::refresh state))
(partial refresh-config cfg)) (partial refresh-config cfg))
state)] state)]
@ -391,8 +385,7 @@
(and (fs/exists? path) (fs/regular-file? path) path))) (and (fs/exists? path) (fs/regular-file? path) path)))
(defmethod ig/pre-init-spec :app.rpc/rlimit [_] (defmethod ig/pre-init-spec :app.rpc/rlimit [_]
(s/keys :req [::wrk/executor (s/keys :req [::wrk/executor]))
::wrk/scheduled-executor]))
(defmethod ig/init-key ::rpc/rlimit (defmethod ig/init-key ::rpc/rlimit
[_ {:keys [::wrk/executor] :as cfg}] [_ {:keys [::wrk/executor] :as cfg}]

View File

@ -22,8 +22,7 @@
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[datoteka.fs :as fs] [datoteka.fs :as fs]
[integrant.core :as ig] [integrant.core :as ig]
[promesa.core :as p] [promesa.core :as p]))
[promesa.exec :as px]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Storage Module State ;; Storage Module State
@ -79,42 +78,40 @@
(update :metadata db/decode-transit-pgobject)))) (update :metadata db/decode-transit-pgobject))))
(defn- create-database-object (defn- create-database-object
[{:keys [::backend ::wrk/executor ::db/pool-or-conn]} {:keys [::content ::expired-at ::touched-at] :as params}] [{:keys [::backend ::db/pool-or-conn]} {:keys [::content ::expired-at ::touched-at] :as params}]
(px/with-dispatch executor (let [id (uuid/random)
(let [id (uuid/random) mdata (cond-> (get-metadata params)
(satisfies? impl/IContentHash content)
(assoc :hash (impl/get-hash content)))
mdata (cond-> (get-metadata params) ;; NOTE: for now we don't reuse the deleted objects, but in
(satisfies? impl/IContentHash content) ;; futute we can consider reusing deleted objects if we
(assoc :hash (impl/get-hash content))) ;; found a duplicated one and is marked for deletion but
;; still not deleted.
result (when (and (::deduplicate? params)
(:hash mdata)
(:bucket mdata))
(get-database-object-by-hash pool-or-conn backend (:bucket mdata) (:hash mdata)))
;; NOTE: for now we don't reuse the deleted objects, but in result (or result
;; futute we can consider reusing deleted objects if we (-> (db/insert! pool-or-conn :storage-object
;; found a duplicated one and is marked for deletion but {:id id
;; still not deleted. :size (impl/get-size content)
result (when (and (::deduplicate? params) :backend (name backend)
(:hash mdata) :metadata (db/tjson mdata)
(:bucket mdata)) :deleted-at expired-at
(get-database-object-by-hash pool-or-conn backend (:bucket mdata) (:hash mdata))) :touched-at touched-at})
(update :metadata db/decode-transit-pgobject)
(update :metadata assoc ::created? true)))]
result (or result (impl/storage-object
(-> (db/insert! pool-or-conn :storage-object (:id result)
{:id id (:size result)
:size (impl/get-size content) (:created-at result)
:backend (name backend) (:deleted-at result)
:metadata (db/tjson mdata) (:touched-at result)
:deleted-at expired-at backend
:touched-at touched-at}) (:metadata result))))
(update :metadata db/decode-transit-pgobject)
(update :metadata assoc ::created? true)))]
(impl/storage-object
(:id result)
(:size result)
(:created-at result)
(:deleted-at result)
(:touched-at result)
backend
(:metadata result)))))
(def ^:private sql:retrieve-storage-object (def ^:private sql:retrieve-storage-object
"select * from storage_object where id = ? and (deleted_at is null or deleted_at > now())") "select * from storage_object where id = ? and (deleted_at is null or deleted_at > now())")
@ -153,45 +150,41 @@
(dm/export impl/object?) (dm/export impl/object?)
(defn get-object (defn get-object
[{:keys [::db/pool-or-conn ::wrk/executor] :as storage} id] [{:keys [::db/pool-or-conn] :as storage} id]
(us/assert! ::storage storage) (us/assert! ::storage storage)
(px/with-dispatch executor (retrieve-database-object pool-or-conn id))
(retrieve-database-object pool-or-conn id)))
(defn put-object! (defn put-object!
"Creates a new object with the provided content." "Creates a new object with the provided content."
[{:keys [::backend] :as storage} {:keys [::content] :as params}] [{:keys [::backend] :as storage} {:keys [::content] :as params}]
(us/assert! ::storage-with-backend storage) (us/assert! ::storage-with-backend storage)
(us/assert! ::impl/content content) (us/assert! ::impl/content content)
(->> (create-database-object storage params) (let [object (create-database-object storage params)]
(p/mcat (fn [object] (if (::created? (meta object))
(if (::created? (meta object)) ;; Store the data finally on the underlying storage subsystem.
;; Store the data finally on the underlying storage subsystem. (-> (impl/resolve-backend storage backend)
(-> (impl/resolve-backend storage backend) (impl/put-object object content))
(impl/put-object object content)) object)))
(p/resolved object))))))
(defn touch-object! (defn touch-object!
"Mark object as touched." "Mark object as touched."
[{:keys [::db/pool-or-conn ::wrk/executor] :as storage} object-or-id] [{:keys [::db/pool-or-conn] :as storage} object-or-id]
(us/assert! ::storage storage) (us/assert! ::storage storage)
(px/with-dispatch executor (let [id (if (impl/object? object-or-id) (:id object-or-id) object-or-id)
(let [id (if (impl/object? object-or-id) (:id object-or-id) object-or-id) rs (db/update! pool-or-conn :storage-object
rs (db/update! pool-or-conn :storage-object {:touched-at (dt/now)}
{:touched-at (dt/now)} {:id id}
{:id id} {::db/return-keys? false})]
{::db/return-keys? false})] (pos? (db/get-update-count rs))))
(pos? (db/get-update-count rs)))))
(defn get-object-data (defn get-object-data
"Return an input stream instance of the object content." "Return an input stream instance of the object content."
[storage object] [storage object]
(us/assert! ::storage storage) (us/assert! ::storage storage)
(if (or (nil? (:expired-at object)) (when (or (nil? (:expired-at object))
(dt/is-after? (:expired-at object) (dt/now))) (dt/is-after? (:expired-at object) (dt/now)))
(-> (impl/resolve-backend storage (:backend object)) (-> (impl/resolve-backend storage (:backend object))
(impl/get-object-data object)) (impl/get-object-data object))))
(p/resolved nil)))
(defn get-object-bytes (defn get-object-bytes
"Returns a byte array of object content." "Returns a byte array of object content."
@ -208,11 +201,10 @@
(get-object-url storage object nil)) (get-object-url storage object nil))
([storage object options] ([storage object options]
(us/assert! ::storage storage) (us/assert! ::storage storage)
(if (or (nil? (:expired-at object)) (when (or (nil? (:expired-at object))
(dt/is-after? (:expired-at object) (dt/now))) (dt/is-after? (:expired-at object) (dt/now)))
(-> (impl/resolve-backend storage (:backend object)) (-> (impl/resolve-backend storage (:backend object))
(impl/get-object-url object options)) (impl/get-object-url object options)))))
(p/resolved nil))))
(defn get-object-path (defn get-object-path
"Get the Path to the object. Only works with `:fs` type of "Get the Path to the object. Only works with `:fs` type of
@ -220,24 +212,20 @@
[storage object] [storage object]
(us/assert! ::storage storage) (us/assert! ::storage storage)
(let [backend (impl/resolve-backend storage (:backend object))] (let [backend (impl/resolve-backend storage (:backend object))]
(if (not= :fs (::type backend)) (when (and (= :fs (::type backend))
(p/resolved nil) (or (nil? (:expired-at object))
(if (or (nil? (:expired-at object)) (dt/is-after? (:expired-at object) (dt/now))))
(dt/is-after? (:expired-at object) (dt/now))) (-> (impl/get-object-url backend object nil) file-url->path))))
(->> (impl/get-object-url backend object nil)
(p/fmap file-url->path))
(p/resolved nil)))))
(defn del-object! (defn del-object!
[{:keys [::db/pool-or-conn ::wrk/executor] :as storage} object-or-id] [{:keys [::db/pool-or-conn] :as storage} object-or-id]
(us/assert! ::storage storage) (us/assert! ::storage storage)
(px/with-dispatch executor (let [id (if (impl/object? object-or-id) (:id object-or-id) object-or-id)
(let [id (if (impl/object? object-or-id) (:id object-or-id) object-or-id) res (db/update! pool-or-conn :storage-object
res (db/update! pool-or-conn :storage-object {:deleted-at (dt/now)}
{:deleted-at (dt/now)} {:id id}
{:id id} {::db/return-keys? false})]
{::db/return-keys? false})] (pos? (db/get-update-count res))))
(pos? (db/get-update-count res)))))
(dm/export impl/resolve-backend) (dm/export impl/resolve-backend)
(dm/export impl/calculate-hash) (dm/export impl/calculate-hash)
@ -281,7 +269,7 @@
(doseq [id ids] (doseq [id ids]
(l/debug :hint "gc-deleted: permanently delete storage object" :backend backend-id :id id)) (l/debug :hint "gc-deleted: permanently delete storage object" :backend backend-id :id id))
@(impl/del-objects-in-bulk backend ids)))] (impl/del-objects-in-bulk backend ids)))]
(fn [params] (fn [params]
(let [min-age (or (:min-age params) min-age)] (let [min-age (or (:min-age params) min-age)]
@ -422,8 +410,8 @@
(ex/raise :type :internal (ex/raise :type :internal
:code :unexpected-unknown-reference :code :unexpected-unknown-reference
:hint (dm/fmt "unknown reference %" bucket)))] :hint (dm/fmt "unknown reference %" bucket)))]
(recur (+ to-freeze f) (recur (+ to-freeze (long f))
(+ to-delete d) (+ to-delete (long d))
(rest groups))) (rest groups)))
(do (do
(l/info :hint "gc-touched: task finished" :to-freeze to-freeze :to-delete to-delete) (l/info :hint "gc-touched: task finished" :to-freeze to-freeze :to-delete to-delete)

View File

@ -6,22 +6,18 @@
(ns app.storage.fs (ns app.storage.fs
(:require (:require
[app.common.data.macros :as dm]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.spec :as us] [app.common.spec :as us]
[app.common.uri :as u] [app.common.uri :as u]
[app.storage :as-alias sto] [app.storage :as-alias sto]
[app.storage.impl :as impl] [app.storage.impl :as impl]
[app.worker :as-alias wrk]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[cuerdas.core :as str] [cuerdas.core :as str]
[datoteka.fs :as fs] [datoteka.fs :as fs]
[datoteka.io :as io] [datoteka.io :as io]
[integrant.core :as ig] [integrant.core :as ig])
[promesa.core :as p]
[promesa.exec :as px])
(:import (:import
java.io.InputStream
java.io.OutputStream
java.nio.file.Path java.nio.file.Path
java.nio.file.Files)) java.nio.file.Files))
@ -48,74 +44,66 @@
(s/keys :req [::directory (s/keys :req [::directory
::uri] ::uri]
:opt [::sto/type :opt [::sto/type
::sto/id ::sto/id]))
::wrk/executor]))
;; --- API IMPL ;; --- API IMPL
(defmethod impl/put-object :fs (defmethod impl/put-object :fs
[{:keys [::wrk/executor] :as backend} {:keys [id] :as object} content] [backend {:keys [id] :as object} content]
(us/assert! ::backend backend) (us/assert! ::backend backend)
(px/with-dispatch executor (let [base (fs/path (::directory backend))
(let [base (fs/path (::directory backend)) path (fs/path (impl/id->path id))
path (fs/path (impl/id->path id)) full (fs/normalize (fs/join base path))]
full (fs/normalize (fs/join base path))]
(when-not (fs/exists? (fs/parent full))
(fs/create-dir (fs/parent full)))
(with-open [^InputStream src (io/input-stream content)
^OutputStream dst (io/output-stream full)]
(io/copy! src dst))
object))) (when-not (fs/exists? (fs/parent full))
(fs/create-dir (fs/parent full)))
(dm/with-open [src (io/input-stream content)
dst (io/output-stream full)]
(io/copy! src dst))
object))
(defmethod impl/get-object-data :fs (defmethod impl/get-object-data :fs
[{:keys [::wrk/executor] :as backend} {:keys [id] :as object}] [backend {:keys [id] :as object}]
(us/assert! ::backend backend) (us/assert! ::backend backend)
(px/with-dispatch executor (let [^Path base (fs/path (::directory backend))
(let [^Path base (fs/path (::directory backend)) ^Path path (fs/path (impl/id->path id))
^Path path (fs/path (impl/id->path id)) ^Path full (fs/normalize (fs/join base path))]
^Path full (fs/normalize (fs/join base path))] (when-not (fs/exists? full)
(when-not (fs/exists? full) (ex/raise :type :internal
(ex/raise :type :internal :code :filesystem-object-does-not-exists
:code :filesystem-object-does-not-exists :path (str full)))
:path (str full))) (io/input-stream full)))
(io/input-stream full))))
(defmethod impl/get-object-bytes :fs (defmethod impl/get-object-bytes :fs
[backend object] [backend object]
(->> (impl/get-object-data backend object) (dm/with-open [input (impl/get-object-data backend object)]
(p/fmap (fn [input] (io/read-as-bytes input)))
(try
(io/read-as-bytes input)
(finally
(io/close! input)))))))
(defmethod impl/get-object-url :fs (defmethod impl/get-object-url :fs
[{:keys [::uri] :as backend} {:keys [id] :as object} _] [{:keys [::uri] :as backend} {:keys [id] :as object} _]
(us/assert! ::backend backend) (us/assert! ::backend backend)
(p/resolved (update uri :path
(update uri :path (fn [existing]
(fn [existing] (if (str/ends-with? existing "/")
(if (str/ends-with? existing "/") (str existing (impl/id->path id))
(str existing (impl/id->path id)) (str existing "/" (impl/id->path id))))))
(str existing "/" (impl/id->path id)))))))
(defmethod impl/del-object :fs (defmethod impl/del-object :fs
[{:keys [::wrk/executor] :as backend} {:keys [id] :as object}] [backend {:keys [id] :as object}]
(us/assert! ::backend backend) (us/assert! ::backend backend)
(px/with-dispatch executor (let [base (fs/path (::directory backend))
(let [base (fs/path (::directory backend)) path (fs/path (impl/id->path id))
path (fs/path (impl/id->path id)) path (fs/join base path)]
path (fs/join base path)] (Files/deleteIfExists ^Path path)))
(Files/deleteIfExists ^Path path))))
(defmethod impl/del-objects-in-bulk :fs (defmethod impl/del-objects-in-bulk :fs
[{:keys [::wrk/executor] :as backend} ids] [backend ids]
(us/assert! ::backend backend) (us/assert! ::backend backend)
(px/with-dispatch executor (let [base (fs/path (::directory backend))]
(let [base (fs/path (::directory backend))] (doseq [id ids]
(doseq [id ids] (let [path (fs/path (impl/id->path id))
(let [path (fs/path (impl/id->path id)) path (fs/join base path)]
path (fs/join base path)] (Files/deleteIfExists ^Path path)))))
(Files/deleteIfExists ^Path path))))))

View File

@ -153,8 +153,8 @@
(content (.toPath ^java.io.File data) size) (content (.toPath ^java.io.File data) size)
(instance? String data) (instance? String data)
(let [data (.getBytes data "UTF-8")] (let [data (.getBytes ^String data "UTF-8")]
(bytes->content data (alength data))) (bytes->content data (alength ^bytes data)))
(bytes? data) (bytes? data)
(bytes->content data (or size (alength ^bytes data))) (bytes->content data (or size (alength ^bytes data)))
@ -195,7 +195,7 @@
(defn calculate-hash (defn calculate-hash
[resource] [resource]
(let [result (with-open [input (io/input-stream resource)] (let [result (dm/with-open [input (io/input-stream resource)]
(-> (bh/blake2b-256 input) (-> (bh/blake2b-256 input)
(bc/bytes->hex)))] (bc/bytes->hex)))]
(str "blake2b:" result))) (str "blake2b:" result)))

View File

@ -45,6 +45,7 @@
software.amazon.awssdk.http.nio.netty.SdkEventLoopGroup software.amazon.awssdk.http.nio.netty.SdkEventLoopGroup
software.amazon.awssdk.regions.Region software.amazon.awssdk.regions.Region
software.amazon.awssdk.services.s3.S3AsyncClient software.amazon.awssdk.services.s3.S3AsyncClient
software.amazon.awssdk.services.s3.S3AsyncClientBuilder
software.amazon.awssdk.services.s3.S3Configuration software.amazon.awssdk.services.s3.S3Configuration
software.amazon.awssdk.services.s3.model.Delete software.amazon.awssdk.services.s3.model.Delete
software.amazon.awssdk.services.s3.model.DeleteObjectRequest software.amazon.awssdk.services.s3.model.DeleteObjectRequest
@ -121,7 +122,7 @@
(defmethod impl/put-object :s3 (defmethod impl/put-object :s3
[backend object content] [backend object content]
(us/assert! ::backend backend) (us/assert! ::backend backend)
(put-object backend object content)) (p/await! (put-object backend object content)))
(defmethod impl/get-object-data :s3 (defmethod impl/get-object-data :s3
[backend object] [backend object]
@ -135,12 +136,13 @@
:cause cause))] :cause cause))]
(-> (get-object-data backend object) (-> (get-object-data backend object)
(p/catch no-such-key? handle-not-found)))) (p/catch no-such-key? handle-not-found)
(p/await!))))
(defmethod impl/get-object-bytes :s3 (defmethod impl/get-object-bytes :s3
[backend object] [backend object]
(us/assert! ::backend backend) (us/assert! ::backend backend)
(get-object-bytes backend object)) (p/await! (get-object-bytes backend object)))
(defmethod impl/get-object-url :s3 (defmethod impl/get-object-url :s3
[backend object options] [backend object options]
@ -150,12 +152,12 @@
(defmethod impl/del-object :s3 (defmethod impl/del-object :s3
[backend object] [backend object]
(us/assert! ::backend backend) (us/assert! ::backend backend)
(del-object backend object)) (p/await! (del-object backend object)))
(defmethod impl/del-objects-in-bulk :s3 (defmethod impl/del-objects-in-bulk :s3
[backend ids] [backend ids]
(us/assert! ::backend backend) (us/assert! ::backend backend)
(del-object-in-bulk backend ids)) (p/await! (del-object-in-bulk backend ids)))
;; --- HELPERS ;; --- HELPERS
@ -187,13 +189,17 @@
(.writeTimeout default-timeout) (.writeTimeout default-timeout)
(.build)) (.build))
client (-> (S3AsyncClient/builder) client (let [builder (S3AsyncClient/builder)
(.serviceConfiguration ^S3Configuration sconfig) builder (.serviceConfiguration ^S3AsyncClientBuilder builder ^S3Configuration sconfig)
(.asyncConfiguration ^ClientAsyncConfiguration aconfig) builder (.asyncConfiguration ^S3AsyncClientBuilder builder ^ClientAsyncConfiguration aconfig)
(.httpClient ^NettyNioAsyncHttpClient hclient) builder (.httpClient ^S3AsyncClientBuilder builder ^NettyNioAsyncHttpClient hclient)
(.region (lookup-region region)) builder (.region ^S3AsyncClientBuilder builder (lookup-region region))
(cond-> (some? endpoint) (.endpointOverride (URI. endpoint))) builder (cond-> ^S3AsyncClientBuilder builder
(.build))] (some? endpoint)
(.endpointOverride (URI. endpoint)))]
(.build ^S3AsyncClientBuilder builder))
]
(reify (reify
clojure.lang.IDeref clojure.lang.IDeref
@ -288,6 +294,7 @@
^AsyncRequestBody rbody) ^AsyncRequestBody rbody)
(p/fmap (constantly object))))) (p/fmap (constantly object)))))
;; FIXME: research how to avoid reflection on close method
(defn- path->stream (defn- path->stream
[path] [path]
(proxy [FilterInputStream] [(io/input-stream path)] (proxy [FilterInputStream] [(io/input-stream path)]
@ -347,8 +354,7 @@
(getObjectRequest ^GetObjectRequest gor) (getObjectRequest ^GetObjectRequest gor)
(build)) (build))
pgor (.presignGetObject ^S3Presigner presigner ^GetObjectPresignRequest gopr)] pgor (.presignGetObject ^S3Presigner presigner ^GetObjectPresignRequest gopr)]
(p/resolved (u/uri (str (.url ^PresignedGetObjectRequest pgor)))))
(u/uri (str (.url ^PresignedGetObjectRequest pgor))))))
(defn- del-object (defn- del-object
[{:keys [::bucket ::client ::prefix]} {:keys [id] :as obj}] [{:keys [::bucket ::client ::prefix]} {:keys [id] :as obj}]

View File

@ -10,57 +10,59 @@
the operating system cleaning task should be responsible of the operating system cleaning task should be responsible of
permanently delete these files (look at systemd-tempfiles)." permanently delete these files (look at systemd-tempfiles)."
(:require (:require
[app.common.data :as d]
[app.common.logging :as l] [app.common.logging :as l]
[app.storage :as-alias sto]
[app.util.time :as dt] [app.util.time :as dt]
[app.worker :as wrk] [app.worker :as wrk]
[clojure.core.async :as a]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[datoteka.fs :as fs] [datoteka.fs :as fs]
[integrant.core :as ig] [integrant.core :as ig]
[promesa.exec :as px])) [promesa.exec :as px]
[promesa.exec.csp :as sp]))
(declare remove-temp-file) (declare ^:private remove-temp-file)
(defonce queue (a/chan 128)) (declare ^:private io-loop)
(defonce queue (sp/chan :buf 128))
(defmethod ig/pre-init-spec ::cleaner [_] (defmethod ig/pre-init-spec ::cleaner [_]
(s/keys :req [::sto/min-age ::wrk/scheduled-executor])) (s/keys :req [::wrk/executor]))
(defmethod ig/prep-key ::cleaner (defmethod ig/prep-key ::cleaner
[_ cfg] [_ cfg]
(merge {::sto/min-age (dt/duration "30m")} (assoc cfg ::min-age (dt/duration "30m")))
(d/without-nils cfg)))
(defmethod ig/init-key ::cleaner (defmethod ig/init-key ::cleaner
[_ {:keys [::sto/min-age ::wrk/scheduled-executor] :as cfg}] [_ cfg]
(px/thread (px/fn->thread (partial io-loop cfg)
{:name "penpot/storage-tmp-cleaner"} {:name "penpot/storage/tmp-cleaner" :virtual true}))
(try
(l/info :hint "started tmp file cleaner")
(loop []
(when-let [path (a/<!! queue)]
(l/trace :hint "schedule tempfile deletion" :path path
:expires-at (dt/plus (dt/now) min-age))
(px/schedule! scheduled-executor
(inst-ms min-age)
(partial remove-temp-file path))
(recur)))
(catch InterruptedException _
(l/debug :hint "interrupted"))
(finally
(l/info :hint "terminated tmp file cleaner")))))
(defmethod ig/halt-key! ::cleaner (defmethod ig/halt-key! ::cleaner
[_ thread] [_ thread]
(px/interrupt! thread)) (px/interrupt! thread))
(defn- io-loop
[{:keys [::min-age] :as cfg}]
(l/info :hint "started tmp file cleaner")
(try
(loop []
(when-let [path (sp/take! queue)]
(l/debug :hint "schedule tempfile deletion" :path path
:expires-at (dt/plus (dt/now) min-age))
(px/schedule! (inst-ms min-age) (partial remove-temp-file cfg path))
(recur)))
(catch InterruptedException _
(l/trace :hint "cleaner interrupted"))
(finally
(l/info :hint "cleaner terminated"))))
(defn- remove-temp-file (defn- remove-temp-file
"Permanently delete tempfile" "Permanently delete tempfile"
[path] [{:keys [::wrk/executor path]}]
(l/trace :hint "permanently delete tempfile" :path path)
(when (fs/exists? path) (when (fs/exists? path)
(fs/delete path))) (px/run! executor
(fn []
(l/debug :hint "permanently delete tempfile" :path path)
(fs/delete path)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; API ;; API
@ -72,7 +74,7 @@
:or {prefix "penpot." :or {prefix "penpot."
suffix ".tmp"}}] suffix ".tmp"}}]
(let [candidate (fs/tempfile :suffix suffix :prefix prefix)] (let [candidate (fs/tempfile :suffix suffix :prefix prefix)]
(a/offer! queue candidate) (sp/offer! queue candidate)
candidate)) candidate))
(defn create-tempfile (defn create-tempfile
@ -80,5 +82,5 @@
:or {prefix "penpot." :or {prefix "penpot."
suffix ".tmp"}}] suffix ".tmp"}}]
(let [path (fs/create-tempfile :suffix suffix :prefix prefix)] (let [path (fs/create-tempfile :suffix suffix :prefix prefix)]
(a/offer! queue path) (sp/offer! queue path)
path)) path))

View File

@ -85,7 +85,7 @@
;; Mark as deleted the storage object related with the ;; Mark as deleted the storage object related with the
;; photo-id field. ;; photo-id field.
(some->> photo-id (sto/touch-object! storage) deref) (some->> photo-id (sto/touch-object! storage))
;; And finally, permanently delete the profile. ;; And finally, permanently delete the profile.
(db/delete! conn :profile {:id id}) (db/delete! conn :profile {:id id})
@ -117,7 +117,7 @@
;; Mark as deleted the storage object related with the ;; Mark as deleted the storage object related with the
;; photo-id field. ;; photo-id field.
(some->> photo-id (sto/touch-object! storage) deref) (some->> photo-id (sto/touch-object! storage))
;; And finally, permanently delete the team. ;; And finally, permanently delete the team.
(db/delete! conn :team {:id id}) (db/delete! conn :team {:id id})
@ -184,10 +184,10 @@
(l/debug :hint "permanently delete font variant" :id (str id)) (l/debug :hint "permanently delete font variant" :id (str id))
;; Mark as deleted the all related storage objects ;; Mark as deleted the all related storage objects
(some->> (:woff1-file-id font) (sto/touch-object! storage) deref) (some->> (:woff1-file-id font) (sto/touch-object! storage))
(some->> (:woff2-file-id font) (sto/touch-object! storage) deref) (some->> (:woff2-file-id font) (sto/touch-object! storage))
(some->> (:otf-file-id font) (sto/touch-object! storage) deref) (some->> (:otf-file-id font) (sto/touch-object! storage))
(some->> (:ttf-file-id font) (sto/touch-object! storage) deref) (some->> (:ttf-file-id font) (sto/touch-object! storage))
;; And finally, permanently delete the team font variant ;; And finally, permanently delete the team font variant
(db/delete! conn :team-font-variant {:id id}) (db/delete! conn :team-font-variant {:id id})

View File

@ -0,0 +1,69 @@
;; 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 INC
(ns app.util.cache
"In-memory cache backed by Caffeine"
(:refer-clojure :exclude [get])
(:require
[app.util.time :as dt]
[promesa.core :as p]
[promesa.exec :as px])
(:import
com.github.benmanes.caffeine.cache.AsyncCache
com.github.benmanes.caffeine.cache.AsyncLoadingCache
com.github.benmanes.caffeine.cache.CacheLoader
com.github.benmanes.caffeine.cache.Caffeine
com.github.benmanes.caffeine.cache.RemovalListener
java.time.Duration
java.util.concurrent.Executor
java.util.function.Function))
(set! *warn-on-reflection* true)
(defn create-listener
[f]
(reify RemovalListener
(onRemoval [_ key val cause]
(when val
(f key val cause)))))
(defn create-loader
[f]
(reify CacheLoader
(load [_ key]
(f key))))
(defn create
[& {:keys [executor on-remove load-fn keepalive]}]
(as-> (Caffeine/newBuilder) builder
(if on-remove (.removalListener builder (create-listener on-remove)) builder)
(if executor (.executor builder ^Executor (px/resolve-executor executor)) builder)
(if keepalive (.expireAfterAccess builder ^Duration (dt/duration keepalive)) builder)
(if load-fn
(.buildAsync builder ^CacheLoader (create-loader load-fn))
(.buildAsync builder))))
(defn invalidate-all!
[^AsyncCache cache]
(.invalidateAll (.synchronous cache)))
(defn get
([cache key]
(assert (instance? AsyncLoadingCache cache) "should be AsyncLoadingCache instance")
(p/await! (.get ^AsyncLoadingCache cache ^Object key)))
([cache key not-found-fn]
(assert (instance? AsyncCache cache) "should be AsyncCache instance")
(p/await! (.get ^AsyncCache cache
^Object key
^Function (reify
Function
(apply [_ key]
(not-found-fn key)))))))
(defn cache?
[o]
(or (instance? AsyncCache o)
(instance? AsyncLoadingCache o)))

View File

@ -45,9 +45,9 @@
(map second) (map second)
(filter #(::spec (meta %))) (filter #(::spec (meta %)))
(map (fn [fvar] (map (fn [fvar]
(with-meta (deref fvar) [(deref fvar)
(-> (meta fvar) (-> (meta fvar)
(assoc :ns (-> ns ns-name str))))))))))) (assoc :ns (-> ns ns-name str)))])))))))
(defn scan-ns (defn scan-ns
[& nsyms] [& nsyms]

View File

@ -6,27 +6,30 @@
(ns app.util.svg (ns app.util.svg
(:require (:require
[app.common.data.macros :as dm]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.logging :as l] [app.common.logging :as l]
[clojure.xml :as xml] [clojure.xml :as xml]
[cuerdas.core :as str]) [cuerdas.core :as str])
(:import (:import
javax.xml.XMLConstants javax.xml.XMLConstants
java.io.InputStream
javax.xml.parsers.SAXParserFactory javax.xml.parsers.SAXParserFactory
clojure.lang.XMLHandler
org.apache.commons.io.IOUtils)) org.apache.commons.io.IOUtils))
(defn- secure-parser-factory (defn- secure-parser-factory
[s ch] [^InputStream input ^XMLHandler handler]
(.. (doto (SAXParserFactory/newInstance) (.. (doto (SAXParserFactory/newInstance)
(.setFeature XMLConstants/FEATURE_SECURE_PROCESSING true) (.setFeature XMLConstants/FEATURE_SECURE_PROCESSING true)
(.setFeature "http://apache.org/xml/features/disallow-doctype-decl" true)) (.setFeature "http://apache.org/xml/features/disallow-doctype-decl" true))
(newSAXParser) (newSAXParser)
(parse s ch))) (parse input handler)))
(defn parse (defn parse
[data] [^String data]
(try (try
(with-open [istream (IOUtils/toInputStream data "UTF-8")] (dm/with-open [istream (IOUtils/toInputStream data "UTF-8")]
(xml/parse istream secure-parser-factory)) (xml/parse istream secure-parser-factory))
(catch Exception e (catch Exception e
(l/warn :hint "error on processing svg" (l/warn :hint "error on processing svg"

View File

@ -19,6 +19,7 @@
java.time.ZonedDateTime java.time.ZonedDateTime
java.time.format.DateTimeFormatter java.time.format.DateTimeFormatter
java.time.temporal.ChronoUnit java.time.temporal.ChronoUnit
java.time.temporal.Temporal
java.time.temporal.TemporalAmount java.time.temporal.TemporalAmount
java.time.temporal.TemporalUnit java.time.temporal.TemporalUnit
java.util.Date java.util.Date
@ -160,11 +161,29 @@
(defn plus (defn plus
[d ta] [d ta]
(.plus d ^TemporalAmount (duration ta))) (let [^TemporalAmount ta (duration ta)]
(cond
(instance? Duration d)
(.plus ^Duration d ta)
(instance? Temporal d)
(.plus ^Temporal d ta)
:else
(throw (UnsupportedOperationException. "unsupported type")))))
(defn minus (defn minus
[d ta] [d ta]
(.minus d ^TemporalAmount (duration ta))) (let [^TemporalAmount ta (duration ta)]
(cond
(instance? Duration d)
(.minus ^Duration d ta)
(instance? Temporal d)
(.minus ^Temporal d ta)
:else
(throw (UnsupportedOperationException. "unsupported type")))))
(defn now (defn now
[] []

View File

@ -5,7 +5,7 @@
;; Copyright (c) KALEIDOS INC ;; Copyright (c) KALEIDOS INC
(ns app.util.websocket (ns app.util.websocket
"A general protocol implementation on top of websockets." "A general protocol implementation on top of websockets using vthreads."
(:require (:require
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.logging :as l] [app.common.logging :as l]
@ -13,22 +13,42 @@
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.loggers.audit :refer [parse-client-ip]] [app.loggers.audit :refer [parse-client-ip]]
[app.util.time :as dt] [app.util.time :as dt]
[clojure.core.async :as a] [promesa.exec :as px]
[promesa.exec.csp :as sp]
[yetti.request :as yr] [yetti.request :as yr]
[yetti.util :as yu] [yetti.util :as yu]
[yetti.websocket :as yws]) [yetti.websocket :as yws])
(:import (:import
java.nio.ByteBuffer)) java.nio.ByteBuffer))
(declare decode-beat)
(declare encode-beat)
(declare start-io-loop)
(declare ws-ping!)
(declare ws-send!)
(declare filter-options)
(def noop (constantly nil)) (def noop (constantly nil))
(def identity-3 (fn [_ _ o] o)) (def identity-3 (fn [_ _ o] o))
(def max-missed-heartbeats 3)
(def heartbeat-interval 5000)
(defn- encode-beat
[n]
(doto (ByteBuffer/allocate 8)
(.putLong n)
(.rewind)))
(defn- decode-beat
[^ByteBuffer buffer]
(when (= 8 (.capacity buffer))
(.rewind buffer)
(.getLong buffer)))
(defn- wrap-handler
[handler]
(fn [wsp message]
(try
(handler wsp message)
(catch Throwable cause
(if (ex/error? cause)
{:type :error :error (ex-data cause)}
{:type :error :error {:message (ex-message cause)}})))))
(declare start-io-loop!)
(defn handler (defn handler
"A WebSocket upgrade handler factory. Returns a handler that can be "A WebSocket upgrade handler factory. Returns a handler that can be
@ -46,12 +66,11 @@
::on-connect ::on-connect
::input-buff-size ::input-buff-size
::output-buff-size ::output-buff-size
::handler
::idle-timeout] ::idle-timeout]
:or {input-buff-size 64 :or {input-buff-size 64
output-buff-size 64 output-buff-size 64
idle-timeout 60000 idle-timeout 60000
on-connect noop on-connect identity
on-snd-message identity-3 on-snd-message identity-3
on-rcv-message identity-3} on-rcv-message identity-3}
:as options}] :as options}]
@ -61,91 +80,65 @@
(assert (fn? on-connect) "'on-connect' should be a function") (assert (fn? on-connect) "'on-connect' should be a function")
(fn [{:keys [::yws/channel] :as request}] (fn [{:keys [::yws/channel] :as request}]
(let [input-ch (a/chan input-buff-size) (let [input-ch (sp/chan :buf input-buff-size)
output-ch (a/chan output-buff-size) output-ch (sp/chan :buf output-buff-size)
hbeat-ch (a/chan (a/sliding-buffer 6)) hbeat-ch (sp/chan :buf (sp/sliding-buffer 6))
close-ch (a/chan) close-ch (sp/chan)
stop-ch (a/chan)
ip-addr (parse-client-ip request) ip-addr (parse-client-ip request)
uagent (yr/get-header request "user-agent") uagent (yr/get-header request "user-agent")
id (uuid/next) id (uuid/next)
state (atom {})
beats (atom #{})
options (-> (filter-options options) options (-> options
(merge {::id id (update ::handler wrap-handler)
::created-at (dt/now) (assoc ::id id)
::input-ch input-ch (assoc ::state state)
::heartbeat-ch hbeat-ch (assoc ::beats beats)
::output-ch output-ch (assoc ::created-at (dt/now))
::close-ch close-ch (assoc ::input-ch input-ch)
::stop-ch stop-ch (assoc ::heartbeat-ch hbeat-ch)
::channel channel (assoc ::output-ch output-ch)
::remote-addr ip-addr (assoc ::close-ch close-ch)
::user-agent uagent}) (assoc ::channel channel)
(atom)) (assoc ::remote-addr ip-addr)
(assoc ::user-agent uagent)
;; call the on-connect hook and memoize the on-terminate instance (on-connect))
on-terminate (on-connect options)
on-ws-open on-ws-open
(fn [channel] (fn [channel]
(l/trace :fn "on-ws-open" :conn-id id) (l/trace :fn "on-ws-open" :conn-id id)
(yws/idle-timeout! channel (dt/duration idle-timeout))) (let [timeout (dt/duration idle-timeout)
name (str "penpot/websocket/io-loop/" id)]
(yws/idle-timeout! channel timeout)
(px/fn->thread (partial start-io-loop! options)
{:name name :virtual true})))
on-ws-terminate on-ws-terminate
(fn [_ code reason] (fn [_ code reason]
(l/trace :fn "on-ws-terminate" :conn-id id :code code :reason reason) (l/trace :fn "on-ws-terminate"
(a/close! close-ch)) :conn-id id
:code code
:reason reason)
(sp/close! close-ch))
on-ws-error on-ws-error
(fn [_ error] (fn [_ cause]
(when-not (or (instance? java.nio.channels.ClosedChannelException error) (sp/close! close-ch cause))
(instance? java.net.SocketException error)
(instance? java.io.IOException error))
(l/error :fn "on-ws-error" :conn-id id
:hint (ex-message error)
:cause error))
(on-ws-terminate nil 8801 "close after error"))
on-ws-message on-ws-message
(fn [_ message] (fn [_ message]
(try (sp/offer! input-ch message)
(let [message (on-rcv-message options message) (swap! state assoc ::last-activity-at (dt/now)))
message (t/decode-str message)]
(a/offer! input-ch message)
(swap! options assoc ::last-activity-at (dt/now)))
(catch Throwable e
(l/warn :hint "error on decoding incoming message from websocket"
:wsmsg (pr-str message)
:cause e)
(a/>! close-ch [8802 "decode error"])
(a/close! close-ch))))
on-ws-pong on-ws-pong
(fn [_ buffers] (fn [_ buffers]
(a/>!! hbeat-ch (yu/copy-many buffers)))] ;; (l/trace :fn "on-ws-pong" :buffers (pr-str buffers))
(sp/put! hbeat-ch (yu/copy-many buffers)))]
;; Wait a close signal (yws/on-close! channel (fn [_]
(a/go (sp/close! close-ch)))
(let [[code reason] (a/<! close-ch)]
(a/close! stop-ch)
(a/close! hbeat-ch)
(a/close! output-ch)
(a/close! input-ch)
(when (and code reason)
(l/trace :hint "close channel condition" :code code :reason reason)
(yws/close! channel code reason))
(when (fn? on-terminate)
(on-terminate))
(l/trace :hint "connection terminated")))
;; React on messages received from the client
(a/go
(a/<! (start-io-loop options handler on-snd-message on-ws-terminate))
(l/trace :hint "io loop terminated"))
{:on-open on-ws-open {:on-open on-ws-open
:on-error on-ws-error :on-error on-ws-error
@ -153,118 +146,81 @@
:on-text on-ws-message :on-text on-ws-message
:on-pong on-ws-pong}))) :on-pong on-ws-pong})))
(defn- ws-send! (defn- handle-ping!
[channel s] [{:keys [::id ::beats ::channel] :as wsp} beat-id]
(let [ch (a/chan 1)] (l/trace :hint "ping" :beat beat-id :conn-id id)
(yws/ping! channel (encode-beat beat-id))
(let [issued (swap! beats conj (long beat-id))]
(not (>= (count issued) max-missed-heartbeats))))
(defn- start-io-loop!
[{:keys [::id ::close-ch ::input-ch ::output-ch ::heartbeat-ch ::channel ::handler ::beats ::on-rcv-message ::on-snd-message] :as wsp}]
(px/thread
{:name (str "penpot/websocket/io-loop/" id)
:virtual true}
(try (try
(yws/send! channel s (fn [e] (handler wsp {:type :open})
(when e (a/offer! ch e)) (loop [i 0]
(a/close! ch))) (let [ping-ch (sp/timeout-chan heartbeat-interval)
[msg p] (sp/alts! [close-ch input-ch output-ch heartbeat-ch ping-ch])]
(when (yws/connected? channel)
(cond
(identical? p ping-ch)
(if (handle-ping! wsp i)
(recur (inc i))
(yws/close! channel 8802 "missing to many pings"))
(or (identical? p close-ch) (nil? msg))
(do :nothing)
(identical? p heartbeat-ch)
(let [beat (decode-beat msg)]
;; (l/trace :hint "pong" :beat beat :conn-id id)
(swap! beats disj beat)
(recur i))
(identical? p input-ch)
(let [message (t/decode-str msg)
message (on-rcv-message message)
{:keys [request-id] :as response} (handler wsp message)]
(when (map? response)
(sp/put! output-ch
(cond-> response
(some? request-id)
(assoc :request-id request-id))))
(recur i))
(identical? p output-ch)
(let [message (on-snd-message msg)
message (t/encode-str message {:type :json-verbose})]
;; (l/trace :hint "writing message to output" :message msg)
(yws/send! channel message)
(recur i))))))
(catch java.nio.channels.ClosedChannelException _)
(catch java.net.SocketException _)
(catch java.io.IOException _)
(catch InterruptedException _
(l/debug :hint "websocket thread interrumpted" :conn-id id))
(catch Throwable cause (catch Throwable cause
(a/offer! ch cause) (l/error :hint "unhandled exception on websocket thread"
(a/close! ch))) :conn-id id
ch)) :cause cause))
(defn- ws-ping! (finally
[channel s] (handler wsp {:type :close})
(let [ch (a/chan 1)]
(try
(yws/ping! channel s (fn [e]
(when e (a/offer! ch e))
(a/close! ch)))
(catch Throwable cause
(a/offer! ch cause)
(a/close! ch)))
ch))
(defn- encode-beat (when (yws/connected? channel)
[n] ;; NOTE: we need to ignore all exceptions here because
(doto (ByteBuffer/allocate 8) ;; there can be a race condition that first returns that
(.putLong n) ;; channel is connected but on closing, will raise that
(.rewind))) ;; channel is already closed.
(ex/ignoring
(yws/close! channel 8899 "terminated")))
(defn- decode-beat (when-let [on-disconnect (::on-disconnect wsp)]
[^ByteBuffer buffer] (on-disconnect))
(when (= 8 (.capacity buffer))
(.rewind buffer)
(.getLong buffer)))
(defn- wrap-handler (l/trace :hint "websocket thread terminated" :conn-id id)))))
[handler]
(fn [wsp message]
(locking wsp
(handler wsp message))))
(def max-missed-heartbeats 3)
(def heartbeat-interval 5000)
(defn- start-io-loop
[wsp handler on-snd-message on-ws-terminate]
(let [input-ch (::input-ch @wsp)
output-ch (::output-ch @wsp)
stop-ch (::stop-ch @wsp)
hbeat-pong-ch (::heartbeat-ch @wsp)
channel (::channel @wsp)
conn-id (::id @wsp)
handler (wrap-handler handler)
beats (atom #{})
choices [stop-ch
input-ch
output-ch
hbeat-pong-ch]]
;; Start IO loop
(a/go
(a/<! (handler wsp {:type :connect}))
(a/<! (a/go-loop [i 0]
(let [hbeat-ping-ch (a/timeout heartbeat-interval)
[v p] (a/alts! (conj choices hbeat-ping-ch))]
(cond
(not (yws/connected? channel))
(on-ws-terminate nil 8800 "channel disconnected")
(= p hbeat-ping-ch)
(do
(l/trace :hint "ping" :beat i :conn-id conn-id)
(a/<! (ws-ping! channel (encode-beat i)))
(let [issued (swap! beats conj (long i))]
(if (>= (count issued) max-missed-heartbeats)
(on-ws-terminate nil 8802 "heartbeat: timeout")
(recur (inc i)))))
(= p hbeat-pong-ch)
(let [beat (decode-beat v)]
(l/trace :hint "pong" :beat beat :conn-id conn-id)
(swap! beats disj beat)
(recur i))
(= p input-ch)
(let [result (a/<! (handler wsp v))]
;; (l/trace :hint "message received" :message v)
(cond
(ex/error? result)
(a/>! output-ch {:type :error :error (ex-data result)})
(ex/exception? result)
(a/>! output-ch {:type :error :error {:message (ex-message result)}})
(map? result)
(a/>! output-ch (cond-> result (:request-id v) (assoc :request-id (:request-id v)))))
(recur i))
(= p output-ch)
(let [v (on-snd-message wsp v)]
;; (l/trace :hint "writing message to output" :message v)
(a/<! (ws-send! channel (t/encode-str v)))
(recur i))))))
(a/<! (handler wsp {:type :disconnect})))))
(defn- filter-options
"Remove from options all namespace qualified keys that matches the
current namespace."
[options]
(into {}
(remove (fn [[key]]
(= (namespace key) "app.util.websocket")))
options))

View File

@ -22,17 +22,16 @@
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[cuerdas.core :as str] [cuerdas.core :as str]
[integrant.core :as ig] [integrant.core :as ig]
[promesa.core :as p]
[promesa.exec :as px]) [promesa.exec :as px])
(:import (:import
java.util.concurrent.ExecutorService java.util.concurrent.ExecutorService
java.util.concurrent.ForkJoinPool java.util.concurrent.ForkJoinPool
java.util.concurrent.Future java.util.concurrent.Future))
java.util.concurrent.ScheduledExecutorService))
(set! *warn-on-reflection* true) (set! *warn-on-reflection* true)
(s/def ::executor #(instance? ExecutorService %)) (s/def ::executor #(instance? ExecutorService %))
(s/def ::scheduled-executor #(instance? ScheduledExecutorService %))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Executor ;; Executor
@ -47,29 +46,23 @@
[skey {:keys [::parallelism]}] [skey {:keys [::parallelism]}]
(let [prefix (if (vector? skey) (-> skey first name) "default") (let [prefix (if (vector? skey) (-> skey first name) "default")
tname (str "penpot/" prefix "/%s") tname (str "penpot/" prefix "/%s")
factory (px/forkjoin-thread-factory :name tname)] ttype (cf/get :worker-executor-type :fjoin)]
(px/forkjoin-executor (case ttype
:factory factory :fjoin
:parallelism parallelism (let [factory (px/forkjoin-thread-factory :name tname)]
:async? true))) (px/forkjoin-executor {:factory factory
:core-size (px/get-available-processors)
:parallelism parallelism
:async true}))
:cached
(let [factory (px/thread-factory :name tname)]
(px/cached-executor :factory factory)))))
(defmethod ig/halt-key! ::executor (defmethod ig/halt-key! ::executor
[_ instance] [_ instance]
(px/shutdown! instance)) (px/shutdown! instance))
(defmethod ig/pre-init-spec ::scheduled-executor [_]
(s/keys :req [::parallelism]))
(defmethod ig/init-key ::scheduled-executor
[_ {:keys [::parallelism]}]
(px/scheduled-executor
:parallelism parallelism
:factory (px/thread-factory :name "penpot/scheduled-executor/%s")))
(defmethod ig/halt-key! ::scheduled-executor
[_ instance]
(px/shutdown! instance))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; TASKS REGISTRY ;; TASKS REGISTRY
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -147,7 +140,7 @@
steals))] steals))]
(px/thread (px/thread
{:name "penpot/executors-monitor"} {:name "penpot/executors-monitor" :virtual true}
(l/info :hint "monitor: started" :name name) (l/info :hint "monitor: started" :name name)
(try (try
(loop [steals 0] (loop [steals 0]
@ -220,53 +213,48 @@
:queued res))) :queued res)))
(run-batch! [rconn] (run-batch! [rconn]
(db/with-atomic [conn pool] (try
(when-let [tasks (get-tasks conn)] (db/with-atomic [conn pool]
(->> (group-by :queue tasks) (if-let [tasks (get-tasks conn)]
(run! (partial push-tasks! conn rconn))) (->> (group-by :queue tasks)
true)))] (run! (partial push-tasks! conn rconn)))
(px/sleep (::wait-duration cfg))))
(catch InterruptedException cause
(throw cause))
(catch Exception cause
(cond
(rds/exception? cause)
(do
(l/warn :hint "dispatcher: redis exception (will retry in an instant)" :cause cause)
(px/sleep (::rds/timeout rconn)))
(db/sql-exception? cause)
(do
(l/warn :hint "dispatcher: database exception (will retry in an instant)" :cause cause)
(px/sleep (::rds/timeout rconn)))
:else
(do
(l/error :hint "dispatcher: unhandled exception (will retry in an instant)" :cause cause)
(px/sleep (::rds/timeout rconn)))))))
(dispatcher []
(l/info :hint "dispatcher: started")
(try
(dm/with-open [rconn (rds/connect redis)]
(loop []
(run-batch! rconn)
(recur)))
(catch InterruptedException _
(l/trace :hint "dispatcher: interrupted"))
(catch Throwable cause
(l/error :hint "dispatcher: unexpected exception" :cause cause))
(finally
(l/info :hint "dispatcher: terminated"))))]
(if (db/read-only? pool) (if (db/read-only? pool)
(l/warn :hint "dispatcher: not started (db is read-only)") (l/warn :hint "dispatcher: not started (db is read-only)")
(px/thread (px/fn->thread dispatcher :name "penpot/worker/dispatcher" :virtual true))))
{:name "penpot/worker-dispatcher"}
(l/info :hint "dispatcher: started")
(try
(dm/with-open [rconn (rds/connect redis)]
(loop []
(when (px/interrupted?)
(throw (InterruptedException. "interrumpted")))
(try
(when-not (run-batch! rconn)
(px/sleep (::wait-duration cfg)))
(catch InterruptedException cause
(throw cause))
(catch Exception cause
(cond
(rds/exception? cause)
(do
(l/warn :hint "dispatcher: redis exception (will retry in an instant)" :cause cause)
(px/sleep (::rds/timeout rconn)))
(db/sql-exception? cause)
(do
(l/warn :hint "dispatcher: database exception (will retry in an instant)" :cause cause)
(px/sleep (::rds/timeout rconn)))
:else
(do
(l/error :hint "dispatcher: unhandled exception (will retry in an instant)" :cause cause)
(px/sleep (::rds/timeout rconn))))))
(recur)))
(catch InterruptedException _
(l/debug :hint "dispatcher: interrupted"))
(catch Throwable cause
(l/error :hint "dispatcher: unexpected exception" :cause cause))
(finally
(l/info :hint "dispatcher: terminated")))))))
(defmethod ig/halt-key! ::dispatcher (defmethod ig/halt-key! ::dispatcher
[_ thread] [_ thread]
@ -311,7 +299,7 @@
(defn- start-worker! (defn- start-worker!
[{:keys [::rds/redis ::worker-id ::queue] :as cfg}] [{:keys [::rds/redis ::worker-id ::queue] :as cfg}]
(px/thread (px/thread
{:name (format "penpot/worker/%s" worker-id)} {:name (format "penpot/worker/runner:%s" worker-id)}
(l/info :hint "worker: started" :worker-id worker-id :queue queue) (l/info :hint "worker: started" :worker-id worker-id :queue queue)
(try (try
(dm/with-open [rconn (rds/connect redis)] (dm/with-open [rconn (rds/connect redis)]
@ -462,7 +450,8 @@
(case status (case status
:retry (handle-task-retry result) :retry (handle-task-retry result)
:failed (handle-task-failure result) :failed (handle-task-failure result)
:completed (handle-task-completion result)))) :completed (handle-task-completion result)
nil)))
(run-task-loop [task-id] (run-task-loop [task-id]
(loop [result (run-task task-id)] (loop [result (run-task task-id)]
@ -531,7 +520,7 @@
(s/def ::entries (s/coll-of (s/nilable ::cron-task))) (s/def ::entries (s/coll-of (s/nilable ::cron-task)))
(defmethod ig/pre-init-spec ::cron [_] (defmethod ig/pre-init-spec ::cron [_]
(s/keys :req [::scheduled-executor ::db/pool ::entries ::registry])) (s/keys :req [::db/pool ::entries ::registry]))
(defmethod ig/init-key ::cron (defmethod ig/init-key ::cron
[_ {:keys [::entries ::registry ::db/pool] :as cfg}] [_ {:keys [::entries ::registry ::db/pool] :as cfg}]
@ -598,22 +587,23 @@
(defn- execute-cron-task (defn- execute-cron-task
[{:keys [::db/pool] :as cfg} {:keys [id] :as task}] [{:keys [::db/pool] :as cfg} {:keys [id] :as task}]
(try (px/thread
(db/with-atomic [conn pool] {:name (str "penpot/cront-task/" id)}
(when (db/exec-one! conn [sql:lock-cron-task (d/name id)]) (try
(l/trace :hint "cron: execute task" :task-id id) (db/with-atomic [conn pool]
((:fn task) task))) (when (db/exec-one! conn [sql:lock-cron-task (d/name id)])
(catch InterruptedException _ (l/trace :hint "cron: execute task" :task-id id)
(px/interrupt! (px/current-thread)) ((:fn task) task)))
(l/debug :hint "cron: task interrupted" :task-id id)) (catch InterruptedException _
(catch Throwable cause (l/debug :hint "cron: task interrupted" :task-id id))
(l/error :hint "cron: unhandled exception on running task" (catch Throwable cause
::l/context (get-error-context cause task) (l/error :hint "cron: unhandled exception on running task"
:task-id id ::l/context (get-error-context cause task)
:cause cause)) :task-id id
(finally :cause cause))
(when-not (px/interrupted? :current) (finally
(schedule-cron-task cfg task))))) (when-not (px/interrupted? :current)
(schedule-cron-task cfg task))))))
(defn- ms-until-valid (defn- ms-until-valid
[cron] [cron]
@ -622,16 +612,11 @@
next (dt/next-valid-instant-from cron now)] next (dt/next-valid-instant-from cron now)]
(inst-ms (dt/diff now next)))) (inst-ms (dt/diff now next))))
(def ^:private
xf-without-done
(remove #(.isDone ^Future %)))
(defn- schedule-cron-task (defn- schedule-cron-task
[{:keys [::scheduled-executor ::running] :as cfg} {:keys [cron] :as task}] [{:keys [::running] :as cfg} {:keys [cron] :as task}]
(let [ft (px/schedule! scheduled-executor (let [ft (px/schedule! (ms-until-valid cron)
(ms-until-valid cron)
(partial execute-cron-task cfg task))] (partial execute-cron-task cfg task))]
(swap! running #(into #{ft} xf-without-done %)))) (swap! running #(into #{ft} (filter p/pending?) %))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View File

@ -8,6 +8,7 @@
(:require (:require
[app.auth] [app.auth]
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.flags :as flags] [app.common.flags :as flags]
[app.common.pages :as cp] [app.common.pages :as cp]
@ -65,6 +66,47 @@
:enable-smtp :enable-smtp
:enable-quotes]) :enable-quotes])
(def test-init-sql
["alter table project_profile_rel set unlogged;\n"
"alter table file_profile_rel set unlogged;\n"
"alter table presence set unlogged;\n"
"alter table presence set unlogged;\n"
"alter table http_session set unlogged;\n"
"alter table team_profile_rel set unlogged;\n"
"alter table team_project_profile_rel set unlogged;\n"
"alter table comment_thread_status set unlogged;\n"
"alter table comment set unlogged;\n"
"alter table comment_thread set unlogged;\n"
"alter table profile_complaint_report set unlogged;\n"
"alter table file_change set unlogged;\n"
"alter table team_font_variant set unlogged;\n"
"alter table share_link set unlogged;\n"
"alter table usage_quote set unlogged;\n"
"alter table access_token set unlogged;\n"
"alter table profile set unlogged;\n"
"alter table file_library_rel set unlogged;\n"
"alter table file_thumbnail set unlogged;\n"
"alter table file_object_thumbnail set unlogged;\n"
"alter table file_media_object set unlogged;\n"
"alter table file_data_fragment set unlogged;\n"
"alter table file set unlogged;\n"
"alter table project set unlogged;\n"
"alter table team_invitation set unlogged;\n"
"alter table webhook_delivery set unlogged;\n"
"alter table webhook set unlogged;\n"
"alter table team set unlogged;\n"
;; For some reason, modifying the task realted tables is very very
;; slow (5s); so we just don't alter them
;; "alter table task set unlogged;\n"
;; "alter table task_default set unlogged;\n"
;; "alter table task_completed set unlogged;\n"
"alter table audit_log_default set unlogged ;\n"
"alter table storage_object set unlogged;\n"
"alter table server_error_report set unlogged;\n"
"alter table server_prop set unlogged;\n"
"alter table global_complaint_report set unlogged;\n"
])
(defn state-init (defn state-init
[next] [next]
(with-redefs [app.config/flags (flags/parse flags/default default-flags) (with-redefs [app.config/flags (flags/parse flags/default default-flags)
@ -97,9 +139,7 @@
:app.http.oauth/handler :app.http.oauth/handler
:app.notifications/handler :app.notifications/handler
:app.loggers.mattermost/reporter :app.loggers.mattermost/reporter
:app.loggers.loki/reporter
:app.loggers.database/reporter :app.loggers.database/reporter
:app.loggers.zmq/receiver
:app.worker/cron :app.worker/cron
:app.worker/worker)) :app.worker/worker))
_ (ig/load-namespaces system) _ (ig/load-namespaces system)
@ -108,6 +148,9 @@
(try (try
(binding [*system* system (binding [*system* system
*pool* (:app.db/pool system)] *pool* (:app.db/pool system)]
(db/with-atomic [conn *pool*]
(doseq [sql test-init-sql]
(db/exec! conn [sql])))
(next)) (next))
(finally (finally
(ig/halt! system)))))) (ig/halt! system))))))
@ -120,11 +163,15 @@
" AND table_name != 'migrations';")] " AND table_name != 'migrations';")]
(db/with-atomic [conn *pool*] (db/with-atomic [conn *pool*]
(let [result (->> (db/exec! conn [sql]) (let [result (->> (db/exec! conn [sql])
(map :table-name))] (map :table-name)
(db/exec! conn [(str "TRUNCATE " (remove #(= "task" %)))
(apply str (interpose ", " result)) sql (str "TRUNCATE "
" CASCADE;")])))) (apply str (interpose ", " result))
(next)) " CASCADE;")]
(doseq [table result]
(db/exec! conn [(str "delete from " table ";")]))))
(next)))
(defn clean-storage (defn clean-storage
[next] [next]
@ -162,7 +209,7 @@
:password "123123" :password "123123"
:is-demo false} :is-demo false}
params)] params)]
(with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(->> params (->> params
(cmd.auth/create-profile! conn) (cmd.auth/create-profile! conn)
(cmd.auth/create-profile-rels! conn)))))) (cmd.auth/create-profile-rels! conn))))))
@ -172,7 +219,7 @@
([pool i {:keys [profile-id team-id] :as params}] ([pool i {:keys [profile-id team-id] :as params}]
(us/assert uuid? profile-id) (us/assert uuid? profile-id)
(us/assert uuid? team-id) (us/assert uuid? team-id)
(with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(->> (merge {:id (mk-uuid "project" i) (->> (merge {:id (mk-uuid "project" i)
:name (str "project" i)} :name (str "project" i)}
params) params)
@ -184,7 +231,7 @@
([pool i {:keys [profile-id project-id] :as params}] ([pool i {:keys [profile-id project-id] :as params}]
(us/assert uuid? profile-id) (us/assert uuid? profile-id)
(us/assert uuid? project-id) (us/assert uuid? project-id)
(with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(files.create/create-file conn (files.create/create-file conn
(merge {:id (mk-uuid "file" i) (merge {:id (mk-uuid "file" i)
:name (str "file" i) :name (str "file" i)
@ -200,7 +247,7 @@
([i params] (create-team* *pool* i params)) ([i params] (create-team* *pool* i params))
([pool i {:keys [profile-id] :as params}] ([pool i {:keys [profile-id] :as params}]
(us/assert uuid? profile-id) (us/assert uuid? profile-id)
(with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(let [id (mk-uuid "team" i)] (let [id (mk-uuid "team" i)]
(teams/create-team conn {:id id (teams/create-team conn {:id id
:profile-id profile-id :profile-id profile-id
@ -211,7 +258,7 @@
([pool {:keys [name width height mtype file-id is-local media-id] ([pool {:keys [name width height mtype file-id is-local media-id]
:or {name "sample" width 100 height 100 mtype "image/svg+xml" is-local true}}] :or {name "sample" width 100 height 100 mtype "image/svg+xml" is-local true}}]
(with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(db/insert! conn :file-media-object (db/insert! conn :file-media-object
{:id (uuid/next) {:id (uuid/next)
:file-id file-id :file-id file-id
@ -225,12 +272,12 @@
(defn link-file-to-library* (defn link-file-to-library*
([params] (link-file-to-library* *pool* params)) ([params] (link-file-to-library* *pool* params))
([pool {:keys [file-id library-id] :as params}] ([pool {:keys [file-id library-id] :as params}]
(with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(#'files/link-file-to-library conn {:file-id file-id :library-id library-id})))) (#'files/link-file-to-library conn {:file-id file-id :library-id library-id}))))
(defn create-complaint-for (defn create-complaint-for
[pool {:keys [id created-at type]}] [pool {:keys [id created-at type]}]
(with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(db/insert! conn :profile-complaint-report (db/insert! conn :profile-complaint-report
{:profile-id id {:profile-id id
:created-at (or created-at (dt/now)) :created-at (or created-at (dt/now))
@ -239,7 +286,7 @@
(defn create-global-complaint-for (defn create-global-complaint-for
[pool {:keys [email type created-at]}] [pool {:keys [email type created-at]}]
(with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(db/insert! conn :global-complaint-report (db/insert! conn :global-complaint-report
{:email email {:email email
:type (name type) :type (name type)
@ -249,7 +296,7 @@
(defn create-team-role* (defn create-team-role*
([params] (create-team-role* *pool* params)) ([params] (create-team-role* *pool* params))
([pool {:keys [team-id profile-id role] :or {role :owner}}] ([pool {:keys [team-id profile-id role] :or {role :owner}}]
(with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(#'teams/create-team-role conn {:team-id team-id (#'teams/create-team-role conn {:team-id team-id
:profile-id profile-id :profile-id profile-id
:role role})))) :role role}))))
@ -257,7 +304,7 @@
(defn create-project-role* (defn create-project-role*
([params] (create-project-role* *pool* params)) ([params] (create-project-role* *pool* params))
([pool {:keys [project-id profile-id role] :or {role :owner}}] ([pool {:keys [project-id profile-id role] :or {role :owner}}]
(with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(#'teams/create-project-role conn {:project-id project-id (#'teams/create-project-role conn {:project-id project-id
:profile-id profile-id :profile-id profile-id
:role role})))) :role role}))))
@ -265,7 +312,7 @@
(defn create-file-role* (defn create-file-role*
([params] (create-file-role* *pool* params)) ([params] (create-file-role* *pool* params))
([pool {:keys [file-id profile-id role] :or {role :owner}}] ([pool {:keys [file-id profile-id role] :or {role :owner}}]
(with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(files.create/create-file-role! conn {:file-id file-id (files.create/create-file-role! conn {:file-id file-id
:profile-id profile-id :profile-id profile-id
:role role})))) :role role}))))
@ -274,10 +321,10 @@
([params] (update-file* *pool* params)) ([params] (update-file* *pool* params))
([pool {:keys [file-id changes session-id profile-id revn] ([pool {:keys [file-id changes session-id profile-id revn]
:or {session-id (uuid/next) revn 0}}] :or {session-id (uuid/next) revn 0}}]
(with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(let [features #{"components/v2"} (let [features #{"components/v2"}
cfg (-> (select-keys *system* [::mbus/msgbus ::mtx/metrics]) cfg (-> (select-keys *system* [::mbus/msgbus ::mtx/metrics])
(assoc :conn conn))] (assoc ::db/conn conn))]
(files.update/update-file cfg (files.update/update-file cfg
{:id file-id {:id file-id
:revn revn :revn revn
@ -310,7 +357,7 @@
(defmacro try-on! (defmacro try-on!
[expr] [expr]
`(try `(try
(let [result# (deref ~expr) (let [result# ~expr
result# (cond-> result# (rph/wrapped? result#) deref)] result# (cond-> result# (rph/wrapped? result#) deref)]
{:error nil {:error nil
:result result#}) :result result#})
@ -320,7 +367,7 @@
(defn command! (defn command!
[{:keys [::type] :as data}] [{:keys [::type] :as data}]
(let [method-fn (get-in *system* [:app.rpc/methods :commands type])] (let [[mdata method-fn] (get-in *system* [:app.rpc/methods :commands type])]
(when-not method-fn (when-not method-fn
(ex/raise :type :assertion (ex/raise :type :assertion
:code :rpc-method-not-found :code :rpc-method-not-found
@ -333,7 +380,7 @@
(defn mutation! (defn mutation!
[{:keys [::type profile-id] :as data}] [{:keys [::type profile-id] :as data}]
(let [method-fn (get-in *system* [:app.rpc/methods :mutations type])] (let [[mdata method-fn] (get-in *system* [:app.rpc/methods :mutations type])]
(try-on! (method-fn (-> data (try-on! (method-fn (-> data
(dissoc ::type) (dissoc ::type)
(assoc ::rpc/profile-id profile-id) (assoc ::rpc/profile-id profile-id)
@ -341,7 +388,7 @@
(defn query! (defn query!
[{:keys [::type profile-id] :as data}] [{:keys [::type profile-id] :as data}]
(let [method-fn (get-in *system* [:app.rpc/methods :queries type])] (let [[mdata method-fn] (get-in *system* [:app.rpc/methods :queries type])]
(try-on! (method-fn (-> data (try-on! (method-fn (-> data
(dissoc ::type) (dissoc ::type)
(assoc ::rpc/profile-id profile-id) (assoc ::rpc/profile-id profile-id)

View File

@ -40,6 +40,6 @@
{:keys [error result]} (th/command! (assoc params ::cond/key etag))] {:keys [error result]} (th/command! (assoc params ::cond/key etag))]
(t/is (nil? error)) (t/is (nil? error))
(t/is (fn? result)) (t/is (fn? result))
(t/is (= 304 (-> (result nil) :status)))) (t/is (= 304 (-> (result nil) :yetti.response/status))))
)))) ))))

View File

@ -215,10 +215,10 @@
(t/is (= 1 (count rows)))) (t/is (= 1 (count rows))))
;; The underlying storage objects are still available. ;; The underlying storage objects are still available.
(t/is (some? @(sto/get-object storage (:media-id fmo2)))) (t/is (some? (sto/get-object storage (:media-id fmo2))))
(t/is (some? @(sto/get-object storage (:thumbnail-id fmo2)))) (t/is (some? (sto/get-object storage (:thumbnail-id fmo2))))
(t/is (some? @(sto/get-object storage (:media-id fmo1)))) (t/is (some? (sto/get-object storage (:media-id fmo1))))
(t/is (some? @(sto/get-object storage (:thumbnail-id fmo1)))) (t/is (some? (sto/get-object storage (:thumbnail-id fmo1))))
;; proceed to remove usage of the file ;; proceed to remove usage of the file
(update-file {:file-id (:id file) (update-file {:file-id (:id file)
@ -246,10 +246,10 @@
;; Finally, check that some of the objects that are marked as ;; Finally, check that some of the objects that are marked as
;; deleted we are unable to retrieve them using standard storage ;; deleted we are unable to retrieve them using standard storage
;; public api. ;; public api.
(t/is (nil? @(sto/get-object storage (:media-id fmo2)))) (t/is (nil? (sto/get-object storage (:media-id fmo2))))
(t/is (nil? @(sto/get-object storage (:thumbnail-id fmo2)))) (t/is (nil? (sto/get-object storage (:thumbnail-id fmo2))))
(t/is (nil? @(sto/get-object storage (:media-id fmo1)))) (t/is (nil? (sto/get-object storage (:media-id fmo1))))
(t/is (nil? @(sto/get-object storage (:thumbnail-id fmo1)))) (t/is (nil? (sto/get-object storage (:thumbnail-id fmo1))))
))) )))
(t/deftest permissions-checks-creating-file (t/deftest permissions-checks-creating-file

View File

@ -26,9 +26,9 @@
(let [storage (-> (:app.storage/storage th/*system*) (let [storage (-> (:app.storage/storage th/*system*)
(configure-storage-backend)) (configure-storage-backend))
sobject @(sto/put-object! storage {::sto/content (sto/content "content") sobject (sto/put-object! storage {::sto/content (sto/content "content")
:content-type "text/plain" :content-type "text/plain"
:other "data"}) :other "data"})
profile (th/create-profile* 1 {:is-active true}) profile (th/create-profile* 1 {:is-active true})
project (th/create-project* 1 {:team-id (:default-team-id profile) project (th/create-project* 1 {:team-id (:default-team-id profile)
:profile-id (:id profile)}) :profile-id (:id profile)})
@ -98,9 +98,9 @@
(t/deftest duplicate-file-with-deleted-relations (t/deftest duplicate-file-with-deleted-relations
(let [storage (-> (:app.storage/storage th/*system*) (let [storage (-> (:app.storage/storage th/*system*)
(configure-storage-backend)) (configure-storage-backend))
sobject @(sto/put-object! storage {::sto/content (sto/content "content") sobject (sto/put-object! storage {::sto/content (sto/content "content")
:content-type "text/plain" :content-type "text/plain"
:other "data"}) :other "data"})
profile (th/create-profile* 1 {:is-active true}) profile (th/create-profile* 1 {:is-active true})
project (th/create-project* 1 {:team-id (:default-team-id profile) project (th/create-project* 1 {:team-id (:default-team-id profile)
@ -120,7 +120,7 @@
:media-id (:id sobject)})] :media-id (:id sobject)})]
(th/mark-file-deleted* {:id (:id file2)}) (th/mark-file-deleted* {:id (:id file2)})
@(sto/del-object! storage sobject) (sto/del-object! storage sobject)
(let [data {::th/type :duplicate-file (let [data {::th/type :duplicate-file
::rpc/profile-id (:id profile) ::rpc/profile-id (:id profile)
@ -157,9 +157,9 @@
(let [storage (-> (:app.storage/storage th/*system*) (let [storage (-> (:app.storage/storage th/*system*)
(configure-storage-backend)) (configure-storage-backend))
sobject @(sto/put-object! storage {::sto/content (sto/content "content") sobject (sto/put-object! storage {::sto/content (sto/content "content")
:content-type "text/plain" :content-type "text/plain"
:other "data"}) :other "data"})
profile (th/create-profile* 1 {:is-active true}) profile (th/create-profile* 1 {:is-active true})
project (th/create-project* 1 {:team-id (:default-team-id profile) project (th/create-project* 1 {:team-id (:default-team-id profile)
@ -230,9 +230,9 @@
(t/deftest duplicate-project-with-deleted-files (t/deftest duplicate-project-with-deleted-files
(let [storage (-> (:app.storage/storage th/*system*) (let [storage (-> (:app.storage/storage th/*system*)
(configure-storage-backend)) (configure-storage-backend))
sobject @(sto/put-object! storage {::sto/content (sto/content "content") sobject (sto/put-object! storage {::sto/content (sto/content "content")
:content-type "text/plain" :content-type "text/plain"
:other "data"}) :other "data"})
profile (th/create-profile* 1 {:is-active true}) profile (th/create-profile* 1 {:is-active true})
project (th/create-project* 1 {:team-id (:default-team-id profile) project (th/create-project* 1 {:team-id (:default-team-id profile)
:profile-id (:id profile)}) :profile-id (:id profile)})

View File

@ -42,8 +42,8 @@
(t/is (uuid? media-id)) (t/is (uuid? media-id))
(t/is (uuid? thumbnail-id)) (t/is (uuid? thumbnail-id))
(let [storage (:app.storage/storage th/*system*) (let [storage (:app.storage/storage th/*system*)
mobj1 @(sto/get-object storage media-id) mobj1 (sto/get-object storage media-id)
mobj2 @(sto/get-object storage thumbnail-id)] mobj2 (sto/get-object storage thumbnail-id)]
(t/is (sto/object? mobj1)) (t/is (sto/object? mobj1))
(t/is (sto/object? mobj2)) (t/is (sto/object? mobj2))
(t/is (= 122785 (:size mobj1))) (t/is (= 122785 (:size mobj1)))
@ -83,8 +83,8 @@
(t/is (uuid? media-id)) (t/is (uuid? media-id))
(t/is (uuid? thumbnail-id)) (t/is (uuid? thumbnail-id))
(let [storage (:app.storage/storage th/*system*) (let [storage (:app.storage/storage th/*system*)
mobj1 @(sto/get-object storage media-id) mobj1 (sto/get-object storage media-id)
mobj2 @(sto/get-object storage thumbnail-id)] mobj2 (sto/get-object storage thumbnail-id)]
(t/is (sto/object? mobj1)) (t/is (sto/object? mobj1))
(t/is (sto/object? mobj2)) (t/is (sto/object? mobj2))
(t/is (= 312043 (:size mobj1))) (t/is (= 312043 (:size mobj1)))
@ -162,8 +162,8 @@
(t/is (uuid? media-id)) (t/is (uuid? media-id))
(t/is (uuid? thumbnail-id)) (t/is (uuid? thumbnail-id))
(let [storage (:app.storage/storage th/*system*) (let [storage (:app.storage/storage th/*system*)
mobj1 @(sto/get-object storage media-id) mobj1 (sto/get-object storage media-id)
mobj2 @(sto/get-object storage thumbnail-id)] mobj2 (sto/get-object storage thumbnail-id)]
(t/is (sto/object? mobj1)) (t/is (sto/object? mobj1))
(t/is (sto/object? mobj2)) (t/is (sto/object? mobj2))
(t/is (= 122785 (:size mobj1))) (t/is (= 122785 (:size mobj1)))
@ -203,8 +203,8 @@
(t/is (uuid? media-id)) (t/is (uuid? media-id))
(t/is (uuid? thumbnail-id)) (t/is (uuid? thumbnail-id))
(let [storage (:app.storage/storage th/*system*) (let [storage (:app.storage/storage th/*system*)
mobj1 @(sto/get-object storage media-id) mobj1 (sto/get-object storage media-id)
mobj2 @(sto/get-object storage thumbnail-id)] mobj2 (sto/get-object storage thumbnail-id)]
(t/is (sto/object? mobj1)) (t/is (sto/object? mobj1))
(t/is (sto/object? mobj2)) (t/is (sto/object? mobj2))
(t/is (= 312043 (:size mobj1))) (t/is (= 312043 (:size mobj1)))

View File

@ -6,6 +6,7 @@
(ns backend-tests.rpc-team-test (ns backend-tests.rpc-team-test
(:require (:require
[app.common.logging :as l]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.db :as db] [app.db :as db]
[app.http :as http] [app.http :as http]

View File

@ -37,61 +37,61 @@
(let [storage (-> (:app.storage/storage th/*system*) (let [storage (-> (:app.storage/storage th/*system*)
(configure-storage-backend)) (configure-storage-backend))
content (sto/content "content") content (sto/content "content")
object @(sto/put-object! storage {::sto/content content object (sto/put-object! storage {::sto/content content
:content-type "text/plain" :content-type "text/plain"
:other "data"})] :other "data"})]
(t/is (sto/object? object)) (t/is (sto/object? object))
(t/is (fs/path? @(sto/get-object-path storage object))) (t/is (fs/path? (sto/get-object-path storage object)))
(t/is (nil? (:expired-at object))) (t/is (nil? (:expired-at object)))
(t/is (= :assets-fs (:backend object))) (t/is (= :assets-fs (:backend object)))
(t/is (= "data" (:other (meta object)))) (t/is (= "data" (:other (meta object))))
(t/is (= "text/plain" (:content-type (meta object)))) (t/is (= "text/plain" (:content-type (meta object))))
(t/is (= "content" (slurp @(sto/get-object-data storage object)))) (t/is (= "content" (slurp (sto/get-object-data storage object))))
(t/is (= "content" (slurp @(sto/get-object-path storage object)))) (t/is (= "content" (slurp (sto/get-object-path storage object))))
)) ))
(t/deftest put-and-retrieve-expired-object (t/deftest put-and-retrieve-expired-object
(let [storage (-> (:app.storage/storage th/*system*) (let [storage (-> (:app.storage/storage th/*system*)
(configure-storage-backend)) (configure-storage-backend))
content (sto/content "content") content (sto/content "content")
object @(sto/put-object! storage {::sto/content content object (sto/put-object! storage {::sto/content content
::sto/expired-at (dt/in-future {:seconds 1}) ::sto/expired-at (dt/in-future {:seconds 1})
:content-type "text/plain" :content-type "text/plain"
})] })]
(t/is (sto/object? object)) (t/is (sto/object? object))
(t/is (dt/instant? (:expired-at object))) (t/is (dt/instant? (:expired-at object)))
(t/is (dt/is-after? (:expired-at object) (dt/now))) (t/is (dt/is-after? (:expired-at object) (dt/now)))
(t/is (= object @(sto/get-object storage (:id object)))) (t/is (= object (sto/get-object storage (:id object))))
(th/sleep 1000) (th/sleep 1000)
(t/is (nil? @(sto/get-object storage (:id object)))) (t/is (nil? (sto/get-object storage (:id object))))
(t/is (nil? @(sto/get-object-data storage object))) (t/is (nil? (sto/get-object-data storage object)))
(t/is (nil? @(sto/get-object-url storage object))) (t/is (nil? (sto/get-object-url storage object)))
(t/is (nil? @(sto/get-object-path storage object))) (t/is (nil? (sto/get-object-path storage object)))
)) ))
(t/deftest put-and-delete-object (t/deftest put-and-delete-object
(let [storage (-> (:app.storage/storage th/*system*) (let [storage (-> (:app.storage/storage th/*system*)
(configure-storage-backend)) (configure-storage-backend))
content (sto/content "content") content (sto/content "content")
object @(sto/put-object! storage {::sto/content content object (sto/put-object! storage {::sto/content content
:content-type "text/plain" :content-type "text/plain"
:expired-at (dt/in-future {:seconds 1})})] :expired-at (dt/in-future {:seconds 1})})]
(t/is (sto/object? object)) (t/is (sto/object? object))
(t/is (true? @(sto/del-object! storage object))) (t/is (true? (sto/del-object! storage object)))
;; retrieving the same object should be not nil because the ;; retrieving the same object should be not nil because the
;; deletion is not immediate ;; deletion is not immediate
(t/is (some? @(sto/get-object-data storage object))) (t/is (some? (sto/get-object-data storage object)))
(t/is (some? @(sto/get-object-url storage object))) (t/is (some? (sto/get-object-url storage object)))
(t/is (some? @(sto/get-object-path storage object))) (t/is (some? (sto/get-object-path storage object)))
;; But you can't retrieve the object again because in database is ;; But you can't retrieve the object again because in database is
;; marked as deleted/expired. ;; marked as deleted/expired.
(t/is (nil? @(sto/get-object storage (:id object)))) (t/is (nil? (sto/get-object storage (:id object))))
)) ))
(t/deftest test-deleted-gc-task (t/deftest test-deleted-gc-task
@ -99,14 +99,14 @@
(configure-storage-backend)) (configure-storage-backend))
content1 (sto/content "content1") content1 (sto/content "content1")
content2 (sto/content "content2") content2 (sto/content "content2")
object1 @(sto/put-object! storage {::sto/content content1 object1 (sto/put-object! storage {::sto/content content1
::sto/expired-at (dt/now) ::sto/expired-at (dt/now)
:content-type "text/plain" :content-type "text/plain"
}) })
object2 @(sto/put-object! storage {::sto/content content2 object2 (sto/put-object! storage {::sto/content content2
::sto/expired-at (dt/in-past {:hours 2}) ::sto/expired-at (dt/in-past {:hours 2})
:content-type "text/plain" :content-type "text/plain"
})] })]
(th/sleep 200) (th/sleep 200)

View File

@ -23,15 +23,19 @@
com.cognitect/transit-cljs {:mvn/version "0.8.280"} com.cognitect/transit-cljs {:mvn/version "0.8.280"}
java-http-clj/java-http-clj {:mvn/version "0.4.3"} java-http-clj/java-http-clj {:mvn/version "0.4.3"}
funcool/promesa {:mvn/version "10.0.594"}
funcool/cuerdas {:mvn/version "2022.06.16-403"} funcool/cuerdas {:mvn/version "2022.06.16-403"}
funcool/promesa
{:git/tag "11.0-alpha13"
:git/sha "f6cab38"
:git/url "https://github.com/funcool/promesa.git"}
lambdaisland/uri {:mvn/version "1.13.95" lambdaisland/uri {:mvn/version "1.13.95"
:exclusions [org.clojure/data.json]} :exclusions [org.clojure/data.json]}
frankiesardo/linked {:mvn/version "1.3.0"} frankiesardo/linked {:mvn/version "1.3.0"}
funcool/datoteka {:mvn/version "3.0.66"} funcool/datoteka {:mvn/version "3.0.66"
:exclusions [funcool/promesa]}
com.sun.mail/jakarta.mail {:mvn/version "2.0.1"} com.sun.mail/jakarta.mail {:mvn/version "2.0.1"}
org.la4j/la4j {:mvn/version "0.6.0"} org.la4j/la4j {:mvn/version "0.6.0"}

View File

@ -178,6 +178,7 @@
(print-detail cause) (print-detail cause)
(recur cause)))))) (recur cause))))))
] ]
(with-out-str (with-out-str
(print-all cause))))) (print-all cause)))))

View File

@ -8,6 +8,7 @@
"A version parsing helper." "A version parsing helper."
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.geom.matrix :as gmt] [app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt] [app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh] [app.common.geom.shapes :as gsh]
@ -28,11 +29,6 @@
(def conjv (fnil conj [])) (def conjv (fnil conj []))
(def conjs (fnil conj #{})) (def conjs (fnil conj #{}))
(defn- raise
[err-str]
#?(:clj (throw (Exception. err-str))
:cljs (throw (js/Error. err-str))))
(defn- commit-change (defn- commit-change
([file change] ([file change]
(commit-change file change nil)) (commit-change file change nil))
@ -104,7 +100,9 @@
(defn setup-rect-selrect [{:keys [x y width height transform] :as obj}] (defn setup-rect-selrect [{:keys [x y width height transform] :as obj}]
(when-not (d/num? x y width height) (when-not (d/num? x y width height)
(raise "Coords not valid for object")) (ex/raise :type :assertion
:code :invalid-condition
:hint "Coords not valid for object"))
(let [rect (gsh/make-rect x y width height) (let [rect (gsh/make-rect x y width height)
center (gsh/center-rect rect) center (gsh/center-rect rect)
@ -121,7 +119,9 @@
[{:keys [content center transform transform-inverse] :as obj}] [{:keys [content center transform transform-inverse] :as obj}]
(when (or (empty? content) (nil? center)) (when (or (empty? content) (nil? center))
(raise "Path not valid")) (ex/raise :type :assertion
:code :invalid-condition
:hint "Path not valid"))
(let [transform (gmt/transform-in center transform) (let [transform (gmt/transform-in center transform)
transform-inverse (gmt/transform-in center transform-inverse) transform-inverse (gmt/transform-in center transform-inverse)

View File

@ -72,7 +72,7 @@
(apply matrix params))) (apply matrix params)))
(defn close? (defn close?
[m1 m2] [^Matrix m1 ^Matrix m2]
(and (mth/close? (.-a m1) (.-a m2)) (and (mth/close? (.-a m1) (.-a m2))
(mth/close? (.-b m1) (.-b m2)) (mth/close? (.-b m1) (.-b m2))
(mth/close? (.-c m1) (.-c m2)) (mth/close? (.-c m1) (.-c m2))
@ -80,7 +80,7 @@
(mth/close? (.-e m1) (.-e m2)) (mth/close? (.-e m1) (.-e m2))
(mth/close? (.-f m1) (.-f m2)))) (mth/close? (.-f m1) (.-f m2))))
(defn unit? [m1] (defn unit? [^Matrix m1]
(and (some? m1) (and (some? m1)
(mth/close? (.-a m1) 1) (mth/close? (.-a m1) 1)
(mth/close? (.-b m1) 0) (mth/close? (.-b m1) 0)

View File

@ -380,7 +380,6 @@
(defn rotate (defn rotate
"Rotates the point around center with an angle" "Rotates the point around center with an angle"
[p c angle] [p c angle]
(prn "ROTATE" p c angle)
(assert (point? p) "point instance expected") (assert (point? p) "point instance expected")
(assert (point? c) "point instance expected") (assert (point? c) "point instance expected")
(let [angle (mth/radians angle) (let [angle (mth/radians angle)

View File

@ -213,26 +213,28 @@
`(when (enabled? ~logger ~level) `(when (enabled? ~logger ~level)
(let [props# (cond-> (delay ~props) ~sync? deref) (let [props# (cond-> (delay ~props) ~sync? deref)
ts# (current-timestamp) ts# (current-timestamp)
context# *context*] context# *context*
(px/run! *default-executor* logfn# (fn []
(fn [] (let [props# (if ~sync? props# (deref props#))
(let [props# (if ~sync? props# (deref props#)) props# (into (d/ordered-map) props#)
props# (into (d/ordered-map) props#) cause# ~cause
cause# ~cause context# (d/without-nils
context# (d/without-nils (merge context# ~context))
(merge context# ~context)) lrecord# {::id (uuid/next)
lrecord# {::id (uuid/next) ::timestamp ts#
::timestamp ts# ::message (delay (build-message props#))
::message (delay (build-message props#)) ::props props#
::props props# ::context context#
::context context# ::level ~level
::level ~level ::logger ~logger}
::logger ~logger} lrecord# (cond-> lrecord#
lrecord# (cond-> lrecord# (some? cause#)
(some? cause#) (assoc ::cause cause#
(assoc ::cause cause# ::trace (delay (build-stack-trace cause#))))]
::trace (delay (build-stack-trace cause#))))] (swap! log-record (constantly lrecord#))))]
(swap! log-record (constantly lrecord#))))))))) (if ~sync?
(logfn#)
(px/exec! *default-executor* logfn#))))))
#?(:clj #?(:clj
(defn slf4j-log-handler (defn slf4j-log-handler
@ -243,12 +245,12 @@
(some? trace) (some? trace)
(str "\n" @trace))] (str "\n" @trace))]
(case level (case level
:trace (.trace ^Logger logger ^String message ^Throwable cause) :trace (.trace ^Logger logger ^String message)
:debug (.debug ^Logger logger ^String message ^Throwable cause) :debug (.debug ^Logger logger ^String message)
:info (.info ^Logger logger ^String message ^Throwable cause) :info (.info ^Logger logger ^String message)
:warn (.warn ^Logger logger ^String message ^Throwable cause) :warn (.warn ^Logger logger ^String message)
:error (.error ^Logger logger ^String message ^Throwable cause) :error (.error ^Logger logger ^String message)
:fatal (.error ^Logger logger ^String message ^Throwable cause) :fatal (.error ^Logger logger ^String message)
(throw (IllegalArgumentException. (str "invalid level:" level)))))))) (throw (IllegalArgumentException. (str "invalid level:" level))))))))
#?(:cljs #?(:cljs

View File

@ -103,7 +103,7 @@
(defn font-weight->name (defn font-weight->name
[weight] [weight]
(case weight (case (long weight)
100 "Hairline" 100 "Hairline"
200 "Extra Light" 200 "Extra Light"
300 "Light" 300 "Light"

View File

@ -252,6 +252,9 @@
#?(:clj #?(:clj
(s/def ::agent #(instance? clojure.lang.Agent %))) (s/def ::agent #(instance? clojure.lang.Agent %)))
#?(:clj
(s/def ::atom #(instance? clojure.lang.Atom %)))
(defn bytes? (defn bytes?
"Test if a first parameter is a byte "Test if a first parameter is a byte
array or not." array or not."

View File

@ -157,7 +157,7 @@
(defn- code-points->text (defn- code-points->text
[cpoints start end] [cpoints start end]
#?(:cljs (apply str (subvec cpoints start end)) #?(:cljs (apply str (subvec cpoints start end))
:clj (let [sb (StringBuilder. (- end start))] :clj (let [sb (StringBuilder. (- ^long end ^long start))]
(run! #(.appendCodePoint sb (int %)) (subvec cpoints start end)) (run! #(.appendCodePoint sb (int %)) (subvec cpoints start end))
(.toString sb)))) (.toString sb))))

View File

@ -17,43 +17,43 @@
[app.common.types.page :as ctp] [app.common.types.page :as ctp]
[app.common.types.file :as ctf])) [app.common.types.file :as ctf]))
(defspec transit-encode-decode-with-shape 30 (defspec transit-encode-decode-with-shape 10
(props/for-all (props/for-all
[fdata (s/gen ::cts/shape)] [fdata (s/gen ::cts/shape)]
(let [res (-> fdata transit/encode-str transit/decode-str)] (let [res (-> fdata transit/encode-str transit/decode-str)]
(t/is (= res fdata))))) (t/is (= res fdata)))))
(defspec types-shape-spec 10 (defspec types-shape-spec 5
(props/for-all (props/for-all
[fdata (s/gen ::cts/shape)] [fdata (s/gen ::cts/shape)]
(t/is (us/valid? ::cts/shape fdata)))) (t/is (us/valid? ::cts/shape fdata))))
(defspec types-page-spec 10 (defspec types-page-spec 5
(props/for-all (props/for-all
[fdata (s/gen ::ctp/page)] [fdata (s/gen ::ctp/page)]
(t/is (us/valid? ::ctp/page fdata)))) (t/is (us/valid? ::ctp/page fdata))))
(defspec types-file-colors-spec 30 (defspec types-file-colors-spec 10
(props/for-all (props/for-all
[fdata (s/gen ::ctf/colors)] [fdata (s/gen ::ctf/colors)]
(t/is (us/valid? ::ctf/colors fdata)))) (t/is (us/valid? ::ctf/colors fdata))))
(defspec types-file-recent-colors-spec 30 (defspec types-file-recent-colors-spec 10
(props/for-all (props/for-all
[fdata (s/gen ::ctf/recent-colors)] [fdata (s/gen ::ctf/recent-colors)]
(t/is (us/valid? ::ctf/recent-colors fdata)))) (t/is (us/valid? ::ctf/recent-colors fdata))))
(defspec types-file-typographies-spec 30 (defspec types-file-typographies-spec 10
(props/for-all (props/for-all
[fdata (s/gen ::ctf/typographies)] [fdata (s/gen ::ctf/typographies)]
(t/is (us/valid? ::ctf/typographies fdata)))) (t/is (us/valid? ::ctf/typographies fdata))))
(defspec types-file-media-spec 30 (defspec types-file-media-spec 10
(props/for-all (props/for-all
[fdata (s/gen ::ctf/media)] [fdata (s/gen ::ctf/media)]
(t/is (us/valid? ::ctf/media fdata)))) (t/is (us/valid? ::ctf/media fdata))))
(defspec types-file-components-spec 10 (defspec types-file-components-spec 1
(props/for-all (props/for-all
[fdata (s/gen ::ctf/components)] [fdata (s/gen ::ctf/components)]
(t/is (us/valid? ::ctf/components fdata)))) (t/is (us/valid? ::ctf/components fdata))))

View File

@ -14,10 +14,8 @@
[clojure.test.check.generators :as gen] [clojure.test.check.generators :as gen]
[clojure.test.check.properties :as props])) [clojure.test.check.properties :as props]))
(defspec non-repeating-uuid-next-1 5000 (defspec non-repeating-uuid-next-1 100
(props/for-all (props/for-all
[uuid1 (s/gen ::us/uuid) [uuid1 (s/gen ::us/uuid)
uuid2 (s/gen ::us/uuid)] uuid2 (s/gen ::us/uuid)]
(t/is (not= uuid1 uuid2)))) (t/is (not= uuid1 uuid2))))

View File

@ -1,14 +1,17 @@
listen_addresses = '*' listen_addresses = '*'
max_connections = 100 max_connections = 50
shared_buffers = 128MB shared_buffers = 256MB
temp_buffers = 8MB temp_buffers = 18MB
work_mem = 8MB work_mem = 18MB
dynamic_shared_memory_type = posix dynamic_shared_memory_type = posix
synchronous_commit = off synchronous_commit = off
wal_writer_delay = 900ms wal_writer_delay = 900ms
max_wal_size = 1GB max_wal_size = 1GB
min_wal_size = 80MB min_wal_size = 80MB
full_page_writes = off
min_wal_size=1GB
max_wal_size=4GB
# log_min_duration_statement = 0 # log_min_duration_statement = 0
log_timezone = 'Europe/Madrid' log_timezone = 'Europe/Madrid'
@ -19,4 +22,3 @@ lc_monetary = 'en_US.utf8'
lc_numeric = 'en_US.utf8' lc_numeric = 'en_US.utf8'
lc_time = 'en_US.utf8' lc_time = 'en_US.utf8'
default_text_search_config = 'pg_catalog.english' default_text_search_config = 'pg_catalog.english'

View File

@ -21,6 +21,7 @@
(derive :get-font-variants ::query) (derive :get-font-variants ::query)
(derive :get-profile ::query) (derive :get-profile ::query)
(derive :get-project ::query) (derive :get-project ::query)
(derive :get-projects ::query)
(derive :get-team-invitations ::query) (derive :get-team-invitations ::query)
(derive :get-team-members ::query) (derive :get-team-members ::query)
(derive :get-team-shared-files ::query) (derive :get-team-shared-files ::query)
@ -29,6 +30,9 @@
(derive :get-teams ::query) (derive :get-teams ::query)
(derive :get-view-only-bundle ::query) (derive :get-view-only-bundle ::query)
(derive :search-files ::query) (derive :search-files ::query)
(derive :retrieve-list-of-builtin-templates ::query)
(derive :get-unread-comment-threads ::query)
(derive :get-team-recent-files ::query)
(defn handle-response (defn handle-response
[{:keys [status body] :as response}] [{:keys [status body] :as response}]

View File

@ -46,9 +46,10 @@
(defonce state (defonce state
(ptk/store {:resolve ptk/resolve (ptk/store {:resolve ptk/resolve
:on-event on-event :on-event on-event
:on-error (fn [e] :on-error (fn [cause]
(.log js/console "ERROR!!" e) (when cause
(@on-error e))})) (log/error :hint "unexpected exception on store" :cause cause)
(@on-error cause)))}))
(defonce stream (defonce stream
(ptk/input-stream state)) (ptk/input-stream state))

View File

@ -7,6 +7,7 @@
(ns app.main.ui.auth.login (ns app.main.ui.auth.login
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.logging :as log]
[app.common.spec :as us] [app.common.spec :as us]
[app.config :as cf] [app.config :as cf]
[app.main.data.messages :as dm] [app.main.data.messages :as dm]
@ -38,7 +39,10 @@
(dom/prevent-default event) (dom/prevent-default event)
(->> (rp/command! :login-with-oidc (assoc params :provider provider)) (->> (rp/command! :login-with-oidc (assoc params :provider provider))
(rx/subs (fn [{:keys [redirect-uri] :as rsp}] (rx/subs (fn [{:keys [redirect-uri] :as rsp}]
(.replace js/location redirect-uri)) (if redirect-uri
(.replace js/location redirect-uri)
(log/error :hint "unexpected response from OIDC method"
:resp (pr-str rsp))))
(fn [{:keys [type code] :as error}] (fn [{:keys [type code] :as error}]
(cond (cond
(and (= type :restriction) (and (= type :restriction)