mirror of
https://github.com/penpot/penpot.git
synced 2025-01-09 08:20:45 -05:00
🐛 Fix many bugs on rlimit module
This commit is contained in:
parent
9c33dc529d
commit
6ad9a5aadb
4 changed files with 79 additions and 62 deletions
|
@ -1,6 +1,10 @@
|
||||||
|
;; Example rlimit.edn file
|
||||||
^{:refresh "30s"}
|
^{:refresh "30s"}
|
||||||
{:default
|
{:default
|
||||||
[[:default :window "200000/h"]]
|
[[:default :window "200000/h"]]
|
||||||
|
|
||||||
|
#{:query/teams}
|
||||||
|
[[:burst :bucket "5/1/5s"]]
|
||||||
|
|
||||||
#{:query/profile}
|
#{:query/profile}
|
||||||
[[:burst :bucket "100/60/1m"]]}
|
[[:burst :bucket "100/60/1m"]]}
|
||||||
|
|
|
@ -126,7 +126,8 @@
|
||||||
(with-meta
|
(with-meta
|
||||||
(fn [cfg params]
|
(fn [cfg params]
|
||||||
(-> (px/submit! executor #(f cfg params))
|
(-> (px/submit! executor #(f cfg params))
|
||||||
(p/bind p/wrap)))
|
(p/bind p/wrap)
|
||||||
|
(p/then' sv/wrap)))
|
||||||
mdata))
|
mdata))
|
||||||
|
|
||||||
(defn- wrap-audit
|
(defn- wrap-audit
|
||||||
|
@ -237,6 +238,8 @@
|
||||||
(s/def ::http-client fn?)
|
(s/def ::http-client fn?)
|
||||||
(s/def ::ldap (s/nilable map?))
|
(s/def ::ldap (s/nilable map?))
|
||||||
(s/def ::msgbus ::mbus/msgbus)
|
(s/def ::msgbus ::mbus/msgbus)
|
||||||
|
(s/def ::rlimit (s/nilable ::rlimit/rlimit))
|
||||||
|
|
||||||
(s/def ::public-uri ::us/not-empty-string)
|
(s/def ::public-uri ::us/not-empty-string)
|
||||||
(s/def ::sprops map?)
|
(s/def ::sprops map?)
|
||||||
|
|
||||||
|
@ -249,7 +252,7 @@
|
||||||
::msgbus
|
::msgbus
|
||||||
::http-client
|
::http-client
|
||||||
::rsem/semaphores
|
::rsem/semaphores
|
||||||
::rlimit/rlimit
|
::rlimit
|
||||||
::mtx/metrics
|
::mtx/metrics
|
||||||
::db/pool
|
::db/pool
|
||||||
::ldap]))
|
::ldap]))
|
||||||
|
|
|
@ -44,7 +44,6 @@
|
||||||
"
|
"
|
||||||
(: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]
|
||||||
|
@ -111,7 +110,7 @@
|
||||||
"m" :minutes
|
"m" :minutes
|
||||||
"s" :seconds
|
"s" :seconds
|
||||||
"w" :weeks)
|
"w" :weeks)
|
||||||
::key (dm/str "ratelimit.window." (d/name name))
|
::key (str "ratelimit.window." (d/name name))
|
||||||
::opts opts})
|
::opts opts})
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
:code :invalid-window-limit-opts
|
:code :invalid-window-limit-opts
|
||||||
|
@ -132,7 +131,7 @@
|
||||||
::interval interval
|
::interval interval
|
||||||
::opts opts
|
::opts opts
|
||||||
::params [(dt/->seconds interval) rate capacity]
|
::params [(dt/->seconds interval) rate capacity]
|
||||||
::key (dm/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)))))
|
||||||
|
@ -140,7 +139,7 @@
|
||||||
(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 [(dm/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))))]
|
||||||
(-> (redis/eval! redis script)
|
(-> (redis/eval! redis script)
|
||||||
(p/then (fn [result]
|
(p/then (fn [result]
|
||||||
|
@ -165,7 +164,7 @@
|
||||||
(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 [(dm/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)]))]
|
||||||
(-> (redis/eval! redis script)
|
(-> (redis/eval! redis script)
|
||||||
(p/then (fn [result]
|
(p/then (fn [result]
|
||||||
|
@ -197,67 +196,65 @@
|
||||||
(filter (complement ::lresult/allowed?))
|
(filter (complement ::lresult/allowed?))
|
||||||
(first))]
|
(first))]
|
||||||
|
|
||||||
(when (and rejected (contains? cf/flags :warn-rpc-rate-limits))
|
(when rejected
|
||||||
(l/warn :hint "rejected rate limit"
|
(l/warn :hint "rejected rate limit"
|
||||||
:user-id (dm/str user-id)
|
:user-id (str user-id)
|
||||||
:limit-service (-> rejected ::service name)
|
:limit-service (-> rejected ::service name)
|
||||||
:limit-name (-> rejected ::name name)
|
:limit-name (-> rejected ::name name)
|
||||||
:limit-strategy (-> rejected ::strategy name)))
|
:limit-strategy (-> rejected ::strategy name)))
|
||||||
|
|
||||||
{:enabled? true
|
{:enabled? true
|
||||||
:allowed? (some? rejected)
|
:allowed? (not (some? rejected))
|
||||||
:headers {"x-rate-limit-remaining" remaining
|
:headers {"x-rate-limit-remaining" remaining
|
||||||
"x-rate-limit-reset" reset}})))))
|
"x-rate-limit-reset" reset}})))))
|
||||||
|
|
||||||
(defn- handle-response
|
(defn- handle-response
|
||||||
[f cfg params rres]
|
[f cfg params result]
|
||||||
(if (:enabled? rres)
|
(if (:enabled? result)
|
||||||
(let [headers {"x-rate-limit-remaining" (:remaining rres)
|
(let [headers (:headers result)]
|
||||||
"x-rate-limit-reset" (:reset rres)}]
|
(when-not (:allowed? result)
|
||||||
(when-not (:allowed? rres)
|
|
||||||
(ex/raise :type :rate-limit
|
(ex/raise :type :rate-limit
|
||||||
:code :request-blocked
|
:code :request-blocked
|
||||||
:hint "rate limit reached"
|
:hint "rate limit reached"
|
||||||
::http/headers headers))
|
::http/headers headers))
|
||||||
(-> (f cfg params)
|
(-> (f cfg params)
|
||||||
(p/then (fn [response]
|
(p/then (fn [response]
|
||||||
(with-meta response
|
(vary-meta response update ::http/headers merge headers)))))
|
||||||
{::http/headers headers})))))
|
|
||||||
|
|
||||||
(f cfg params)))
|
(f cfg params)))
|
||||||
|
|
||||||
(defn wrap
|
(defn wrap
|
||||||
[{:keys [rlimit redis] :as cfg} f mdata]
|
[{:keys [rlimit redis] :as cfg} f mdata]
|
||||||
(let [skey (keyword (::rpc/type cfg) (->> mdata ::sv/spec name))
|
(if rlimit
|
||||||
sname (dm/str (::rpc/type cfg) "." (->> mdata ::sv/spec name))
|
(let [skey (keyword (::rpc/type cfg) (->> mdata ::sv/spec name))
|
||||||
default-rresp (p/resolved {:enabled? false})]
|
sname (str (::rpc/type cfg) "." (->> mdata ::sv/spec name))]
|
||||||
(if (or (contains? cf/flags :rpc-rate-limit)
|
|
||||||
(contains? cf/flags :soft-rpc-rate-limit))
|
|
||||||
(fn [cfg {:keys [::http/request] :as params}]
|
(fn [cfg {:keys [::http/request] :as params}]
|
||||||
(let [user-id (or (:profile-id params)
|
(let [uid (or (:profile-id params)
|
||||||
(some-> request parse-client-ip)
|
(some-> request parse-client-ip)
|
||||||
uuid/zero)
|
uuid/zero)
|
||||||
|
|
||||||
rresp (when (and user-id @enabled?)
|
rsp (when (and uid @enabled?)
|
||||||
(when-let [limits (get-in @rlimit [::limits skey])]
|
(when-let [limits (or (get-in @rlimit [::limits skey])
|
||||||
(let [redis (redis/get-or-connect redis ::rlimit default-options)
|
(get-in @rlimit [::limits :default]))]
|
||||||
limits (map #(assoc % ::service sname) limits)
|
(let [redis (redis/get-or-connect redis ::rlimit default-options)
|
||||||
rresp (-> (process-limits redis user-id limits (dt/now))
|
limits (map #(assoc % ::service sname) limits)
|
||||||
(p/catch (fn [cause]
|
resp (-> (process-limits redis uid limits (dt/now))
|
||||||
;; If we have an error on processing the
|
(p/catch (fn [cause]
|
||||||
;; rate-limit we just skip it for do not cause
|
;; If we have an error on processing the rate-limit we just skip
|
||||||
;; service interruption because of redis downtime
|
;; it for do not cause service interruption because of redis
|
||||||
;; or similar situation.
|
;; downtime or similar situation.
|
||||||
(l/error :hint "error on processing rate-limit" :cause cause)
|
(l/error :hint "error on processing rate-limit" :cause cause)
|
||||||
{:enabled? false})))]
|
{:enabled? false})))]
|
||||||
|
|
||||||
;; If soft rate are enabled, we process the rate-limit but return
|
;; If soft rate are enabled, we process the rate-limit but return unprotected
|
||||||
;; unprotected response.
|
;; response.
|
||||||
(and (contains? cf/flags :soft-rpc-rate-limit) rresp))))]
|
(if (contains? cf/flags :soft-rpc-rlimit)
|
||||||
|
(p/resolved {:enabled? false})
|
||||||
|
resp))))
|
||||||
|
|
||||||
(p/then (or rresp default-rresp)
|
rsp (or rsp (p/resolved {:enabled? false}))]
|
||||||
(partial handle-response f cfg params))))
|
|
||||||
f)))
|
(p/then rsp (partial handle-response f cfg params)))))
|
||||||
|
f))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; CONFIG WATCHER
|
;; CONFIG WATCHER
|
||||||
|
@ -376,20 +373,20 @@
|
||||||
(defmethod ig/pre-init-spec :app.rpc/rlimit [_]
|
(defmethod ig/pre-init-spec :app.rpc/rlimit [_]
|
||||||
(s/keys :req-un [::wrk/executor ::wrk/scheduler]))
|
(s/keys :req-un [::wrk/executor ::wrk/scheduler]))
|
||||||
|
|
||||||
(defmethod ig/init-key :app.rpc/rlimit
|
(defmethod ig/init-key ::rpc/rlimit
|
||||||
[_ {:keys [executor] :as params}]
|
[_ {:keys [executor] :as params}]
|
||||||
(let [state (agent {})]
|
(when (contains? cf/flags :rpc-rlimit)
|
||||||
|
(let [state (agent {})]
|
||||||
|
(set-error-handler! state on-refresh-error)
|
||||||
|
(set-error-mode! state :continue)
|
||||||
|
|
||||||
(set-error-handler! state on-refresh-error)
|
(when-let [path (get-config-path)]
|
||||||
(set-error-mode! state :continue)
|
(l/info :hint "initializing rlimit config reader" :path (str path))
|
||||||
|
|
||||||
(when-let [path (get-config-path)]
|
;; Initialize the state with initial refresh value
|
||||||
(l/info :hint "initializing rlimit config reader" :path (str path))
|
(send-via executor state (constantly {::refresh (dt/duration "5s")}))
|
||||||
|
|
||||||
;; Initialize the state with initial refresh value
|
;; Force a refresh
|
||||||
(send-via executor state (constantly {::refresh (dt/duration "5s")}))
|
(refresh-config (assoc params :path path :state state)))
|
||||||
|
|
||||||
;; Force a refresh
|
state)))
|
||||||
(refresh-config (assoc params :path path :state state)))
|
|
||||||
|
|
||||||
state))
|
|
||||||
|
|
|
@ -11,19 +11,32 @@
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[cuerdas.core :as str]))
|
[cuerdas.core :as str]))
|
||||||
|
|
||||||
(defrecord WrappedValue [obj]
|
;; A utilty wrapper object for wrap service responses that does not
|
||||||
|
;; implements the IObj interface that make possible attach metadata to
|
||||||
|
;; it.
|
||||||
|
|
||||||
|
(deftype MetadataWrapper [obj ^:unsynchronized-mutable metadata]
|
||||||
clojure.lang.IDeref
|
clojure.lang.IDeref
|
||||||
(deref [_] obj))
|
(deref [_] obj)
|
||||||
|
|
||||||
|
clojure.lang.IObj
|
||||||
|
(withMeta [_ meta]
|
||||||
|
(MetadataWrapper. obj meta))
|
||||||
|
|
||||||
|
(meta [_] metadata))
|
||||||
|
|
||||||
(defn wrap
|
(defn wrap
|
||||||
([]
|
"Conditionally wrap a value into MetadataWrapper instance. If the
|
||||||
(WrappedValue. nil))
|
object already implements IObj interface it will be returned as is."
|
||||||
|
([] (wrap nil))
|
||||||
([o]
|
([o]
|
||||||
(WrappedValue. o)))
|
(if (instance? clojure.lang.IObj o)
|
||||||
|
o
|
||||||
|
(MetadataWrapper. o {}))))
|
||||||
|
|
||||||
(defn wrapped?
|
(defn wrapped?
|
||||||
[o]
|
[o]
|
||||||
(instance? WrappedValue o))
|
(instance? MetadataWrapper o))
|
||||||
|
|
||||||
(defmacro defmethod
|
(defmacro defmethod
|
||||||
[sname & body]
|
[sname & body]
|
||||||
|
|
Loading…
Reference in a new issue