Improve appender rate limiting: now specified as ncalls / window-msecs

This commit is contained in:
Peter Taoussanis 2013-11-29 18:31:33 +07:00
parent 39e568f4fc
commit 5ac604d66d
8 changed files with 33 additions and 36 deletions

View File

@ -82,7 +82,8 @@
(def example-config (def example-config
"APPENDERS "APPENDERS
An appender is a map with keys: An appender is a map with keys:
:doc, :min-level, :enabled?, :async?, :limit-per-msecs, :fn :doc, :min-level, :enabled?, :async?, :fn,
:rate-limit ([ncalls-limit window-ms] form).
An appender's fn takes a single map with keys: An appender's fn takes a single map with keys:
:level, :throwable :level, :throwable
@ -132,14 +133,14 @@
:appenders :appenders
{:standard-out {:standard-out
{:doc "Prints to *out*/*err*. Enabled by default." {:doc "Prints to *out*/*err*. Enabled by default."
:min-level nil :enabled? true :async? false :limit-per-msecs nil :min-level nil :enabled? true :async? false :rate-limit nil
:fn (fn [{:keys [error? default-output]}] :fn (fn [{:keys [error? default-output]}]
(binding [*out* (if error? *err* *out*)] (binding [*out* (if error? *err* *out*)]
(str-println default-output)))} (str-println default-output)))}
:spit :spit
{:doc "Spits to `(:spit-filename :shared-appender-config)` file." {:doc "Spits to `(:spit-filename :shared-appender-config)` file."
:min-level nil :enabled? false :async? false :limit-per-msecs nil :min-level nil :enabled? false :async? false :rate-limit nil
:fn (fn [{:keys [ap-config default-output]}] :fn (fn [{:keys [ap-config default-output]}]
(when-let [filename (:spit-filename ap-config)] (when-let [filename (:spit-filename ap-config)]
(try (spit filename default-output :append true) (try (spit filename default-output :append true)
@ -154,38 +155,39 @@
(defn- wrap-appender-fn (defn- wrap-appender-fn
"Wraps compile-time appender fn with additional runtime capabilities "Wraps compile-time appender fn with additional runtime capabilities
controlled by compile-time config." controlled by compile-time config."
[{apfn :fn :keys [async? limit-per-msecs] :as appender}] [{apfn :fn :keys [async? rate-limit] :as appender}]
(let [limit-per-msecs (or (:max-message-per-msecs appender) (let [rate-limit (or rate-limit ; Backwards comp:
limit-per-msecs)] ; Backwards comp (if-let [x (:max-message-per-msecs appender)] [1 x]
(when-let [x (:limit-per-msecs appender)] [1 x])))]
(assert (or (nil? rate-limit) (vector? rate-limit)))
(->> ; Wrapping applies per appender, bottom-to-top (->> ; Wrapping applies per appender, bottom-to-top
apfn apfn
;; Rate limit support ;; Rate limit support
((fn [apfn] ((fn [apfn]
(if-not limit-per-msecs apfn ;; Compile-time:
(let [timers (atom {})] ; {:hash last-appended-time-msecs ...} (if-not rate-limit apfn
(fn [{ns :ns [x1 & _] :args :as apfn-args}] (let [[ncalls-limit window-ms] rate-limit
(let [now (System/currentTimeMillis) limiter-any (utils/rate-limiter ncalls-limit window-ms)
hash (str ns "/" x1) ; TODO Alternatives? ;; This is a little hand-wavy but it's a decent general
limit? (fn [last-msecs] ;; strategy and helps us from making this overly complex to
(and last-msecs (<= (- now last-msecs) ;; configure:
limit-per-msecs)))] limiter-specific (utils/rate-limiter (quot ncalls-limit 4)
window-ms)]
(when-not (limit? (@timers hash)) (fn [{:keys [ns args] :as apfn-args}]
(apfn apfn-args) ;; Runtime: (test smaller limit 1st):
(swap! timers assoc hash now)) (when-not (or (limiter-specific (str ns args)) (limiter-any))
(apfn apfn-args)))))))
(when (< (rand) 0.001) ; Occasionally garbage collect
(when-let [expired-timers (->> (keys @timers)
(remove #(limit? (@timers %)))
(seq))]
(apply swap! timers dissoc expired-timers)))))))))
;; Async (agent) support ;; Async (agent) support
((fn [apfn] ((fn [apfn]
;; Compile-time:
(if-not async? apfn (if-not async? apfn
(let [agent (agent nil :error-mode :continue)] (let [agent (agent nil :error-mode :continue)]
(fn [apfn-args] (send-off agent (fn [_] (apfn apfn-args))))))))))) (fn [apfn-args] ; Runtime:
(send-off agent (fn [_] (apfn apfn-args)))))))))))
(defn- make-timestamp-fn (defn- make-timestamp-fn
"Returns a unary fn that formats instants using given pattern string and an "Returns a unary fn that formats instants using given pattern string and an

View File

@ -11,8 +11,6 @@
"timestamps, etc.") "timestamps, etc.")
:min-level :debug :min-level :debug
:enabled? true :enabled? true
:async? false
:limit-per-msecs nil
:prefix-fn :ns :prefix-fn :ns
:fn (fn [{:keys [level prefix throwable message]}] :fn (fn [{:keys [level prefix throwable message]}]
(if throwable (if throwable

View File

@ -41,6 +41,6 @@
"Needs :irc config map in :shared-appender-config, e.g.: "Needs :irc config map in :shared-appender-config, e.g.:
{:host \"irc.example.org\" :port 6667 :nick \"logger\" {:host \"irc.example.org\" :port 6667 :nick \"logger\"
:name \"My Logger\" :chan \"#logs\"") :name \"My Logger\" :chan \"#logs\"")
:min-level :info :enabled? true :async? false :limit-per-msecs nil :min-level :info :enabled? true
:prefix-fn (fn [{:keys [level]}] (-> level name str/upper-case)) :prefix-fn (fn [{:keys [level]}] (-> level name str/upper-case))
:fn appender-fn}) :fn appender-fn})

View File

@ -42,5 +42,5 @@
:server {:host \"127.0.0.1\" :server {:host \"127.0.0.1\"
:port 27017}}") :port 27017}}")
:min-level :warn :enabled? true :async? true :min-level :warn :enabled? true :async? true
:max-message-per-msecs 1000 ; 1 entry / sec :rate-limit [1 1000] ; 1 entry / sec
:fn appender-fn}) :fn appender-fn})

View File

@ -11,7 +11,7 @@
^{:host \"mail.isp.net\" :user \"jsmith\" :pass \"sekrat!!1\"} ^{:host \"mail.isp.net\" :user \"jsmith\" :pass \"sekrat!!1\"}
{:from \"Bob's logger <me@draines.com>\" :to \"foo@example.com\"}") {:from \"Bob's logger <me@draines.com>\" :to \"foo@example.com\"}")
:min-level :error :enabled? true :async? true :min-level :error :enabled? true :async? true
:limit-per-msecs (* 1000 60 10) ; 1 subject / 10 mins :rate-limit [5 (* 1000 60 2)] ; 5 calls / 2 mins
:fn (fn [{:keys [ap-config prefix throwable args]}] :fn (fn [{:keys [ap-config prefix throwable args]}]
(when-let [postal-config (:postal ap-config)] (when-let [postal-config (:postal ap-config)]
(let [[subject & body] args] (let [[subject & body] args]

View File

@ -74,6 +74,4 @@
:backlog 5}") :backlog 5}")
:min-level nil :min-level nil
:enabled? true :enabled? true
:async? false
:limit-per-msecs nil
:fn appender-fn}) :fn appender-fn})

View File

@ -44,6 +44,5 @@
"Needs :socket config map in :shared-appender-config, e.g.: "Needs :socket config map in :shared-appender-config, e.g.:
{:listen-addr :all {:listen-addr :all
:port 9000}") :port 9000}")
:min-level :trace :enabled? true :async? false :min-level :trace :enabled? true
:max-message-per-msecs nil ; no rate limit by default
:fn appender-fn}) :fn appender-fn})

View File

@ -30,7 +30,7 @@
(swap! cache assoc args cv) (swap! cache assoc args cv)
@dv))))))))) @dv)))))))))
(defn rate-limit (defn rate-limiter
"Returns a `(fn [& [id]])` that returns either `nil` (limit okay) or number of "Returns a `(fn [& [id]])` that returns either `nil` (limit okay) or number of
msecs until next rate limit window (rate limited)." msecs until next rate limit window (rate limited)."
[ncalls-limit window-ms] [ncalls-limit window-ms]