├── resources └── .keep ├── FUNDING.yml ├── flex.png ├── todo.md ├── .dir-locals.el ├── .gitignore ├── test └── s_exp │ └── flex_test.clj ├── src └── s_exp │ ├── flex │ ├── ex.clj │ ├── limit │ │ ├── fixed.clj │ │ ├── aimd.clj │ │ ├── gradient2.clj │ │ └── vegas.clj │ ├── middleware.clj │ ├── interceptor.clj │ ├── sampler │ │ └── windowed.clj │ └── protocols.clj │ └── flex.clj ├── deps.edn ├── .clj-kondo └── config.edn ├── dev └── playground.clj └── README.md /resources/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: mpenet 2 | -------------------------------------------------------------------------------- /flex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mpenet/flex/HEAD/flex.png -------------------------------------------------------------------------------- /todo.md: -------------------------------------------------------------------------------- 1 | * add headers with limit/sampler infos 2 | 3 | * add hooks 4 | 5 | * cljc 6 | 7 | * test 8 | 9 | * send metrics to grafana 10 | -------------------------------------------------------------------------------- /.dir-locals.el: -------------------------------------------------------------------------------- 1 | ((clojure-mode . ((cider-preferred-build-tool . clojure-cli) 2 | (cider-clojure-cli-global-options . "-A:dev:test")))) 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | *.jar 5 | *.class 6 | /.cpcache 7 | /.lein-* 8 | /.nrepl-history 9 | /.nrepl-port 10 | .hgignore 11 | .hg/ 12 | -------------------------------------------------------------------------------- /test/s_exp/flex_test.clj: -------------------------------------------------------------------------------- 1 | (ns s-exp.flex-test 2 | (:require [clojure.test :refer :all] 3 | ;; [s-exp.flex :refer :all] 4 | )) 5 | 6 | (deftest a-test 7 | (testing "FIXME, I fail." 8 | (is (= 0 1)))) 9 | -------------------------------------------------------------------------------- /src/s_exp/flex/ex.clj: -------------------------------------------------------------------------------- 1 | (ns s-exp.flex.ex 2 | (:require [exoscale.ex :as ex])) 3 | 4 | (defn ex-rejected 5 | [data] 6 | (ex/ex-info "Request rejected" 7 | [:s-exp.flex/rejected [:exoscale.ex/busy]] 8 | data)) 9 | 10 | (defn ex-rejected! 11 | [data] 12 | (throw (ex-rejected data))) 13 | -------------------------------------------------------------------------------- /src/s_exp/flex/limit/fixed.clj: -------------------------------------------------------------------------------- 1 | (ns s-exp.flex.limit.fixed 2 | (:require [s-exp.flex.protocols :as p])) 3 | 4 | (def defaults {:limit 20}) 5 | 6 | (defn make 7 | [opts] 8 | (let [{::keys [limit]} (merge defaults opts)] 9 | (reify 10 | clojure.lang.IDeref 11 | (deref [_] limit) 12 | p/Limit 13 | (-state [_] {:limit limit}) 14 | (-watch-limit! [_ k f]) 15 | (-update! [_ rtt in-flight dropped?] 16 | limit)))) 17 | -------------------------------------------------------------------------------- /src/s_exp/flex/middleware.clj: -------------------------------------------------------------------------------- 1 | (ns s-exp.flex.middleware 2 | (:require [s-exp.flex.protocols :as p] 3 | [s-exp.flex.ex :as ex])) 4 | 5 | (defn with-limiter 6 | [handler {:keys [limiter]}] 7 | (fn [req-map] 8 | (let [request (p/acquire! limiter)] 9 | (if (p/accepted? request) 10 | (try (handler req-map) 11 | (finally 12 | (p/complete! request))) 13 | (do (p/reject! request) 14 | (ex/ex-rejected! @request)))))) 15 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:deps {org.clojure/clojure {:mvn/version "1.11.0"} 2 | exoscale/ex {:mvn/version "0.4.0"} 3 | exoscale/interceptor {:mvn/version "0.1.10"}} 4 | 5 | :aliases 6 | {:dev {:extra-deps {ring/ring {:mvn/version "1.9.1"}}} 7 | 8 | :test {:extra-paths ["test"] 9 | :extra-deps {org.clojure/test.check {:mvn/version "RELEASE"} 10 | com.cognitect/test-runner 11 | {:git/url "https://github.com/cognitect-labs/test-runner" 12 | :sha "b6b3193fcc42659d7e46ecd1884a228993441182"}}} 13 | :test/runner {:main-opts ["-m" "cognitect.test-runner"]}}} 14 | -------------------------------------------------------------------------------- /.clj-kondo/config.edn: -------------------------------------------------------------------------------- 1 | {:linters 2 | {:clojure-lsp/unused-public-var 3 | {:level :warning 4 | :exclude #{s-exp.flex/limiter 5 | s-exp.flex.limit.fixed/make 6 | s-exp.flex.limit.aimd/make 7 | s-exp.flex.limit.gradient2/make 8 | s-exp.flex.limit.vegas/make 9 | s-exp.flex.protocols/state 10 | s-exp.flex.protocols/watch-limit! 11 | s-exp.flex.protocols/ignore! 12 | s-exp.flex.protocols/inc! 13 | s-exp.flex.protocols/rejected? 14 | s-exp.flex.interceptor/interceptor 15 | s-exp.flex.middleware/with-limiter}}}} 16 | -------------------------------------------------------------------------------- /src/s_exp/flex/interceptor.clj: -------------------------------------------------------------------------------- 1 | (ns s-exp.flex.interceptor 2 | (:require [s-exp.flex.protocols :as p] 3 | [s-exp.flex.ex :as ex])) 4 | 5 | (defn interceptor 6 | "Interceptor of `limiter` and `sampler` that will compute rtt avg for 7 | handler and reject requests past limited threshold" 8 | [{:keys [limiter]}] 9 | {:enter (fn [ctx] 10 | (let [request (p/acquire! limiter)] 11 | (cond-> (assoc ctx 12 | :s-exp.flex/request request) 13 | (not (p/accepted? request)) 14 | (assoc :exoscale.interceptor/error (ex/ex-rejected @request))))) 15 | 16 | :leave (fn [{:as ctx :s-exp.flex/keys [request]}] 17 | (when (p/accepted? request) 18 | (p/complete! request)) 19 | ctx) 20 | 21 | :error (fn [{:s-exp.flex/keys [request]} err] 22 | (if (p/accepted? request) 23 | (p/complete! request) 24 | (p/reject! request)) 25 | (throw err))}) 26 | -------------------------------------------------------------------------------- /src/s_exp/flex/sampler/windowed.clj: -------------------------------------------------------------------------------- 1 | (ns s-exp.flex.sampler.windowed 2 | "Averaging sampler based on naive ring buffer" 3 | (:require [s-exp.flex.protocols :as p])) 4 | 5 | (defn avg 6 | [xs] 7 | (if (seq xs) 8 | (long (/ (reduce + 0 xs) 9 | (count xs))) 10 | 0)) 11 | 12 | (defn ewma 13 | "Exponentially-weighted moving average" 14 | [xs a] 15 | (-> (reduce (fn [xs x] 16 | (conj xs 17 | (+ (* a x) 18 | (* (- 1 a) 19 | (peek xs))))) 20 | [(first xs)] 21 | (rest xs)) 22 | peek 23 | (or 0))) 24 | 25 | (defn make 26 | ([] (make {})) 27 | ([{::keys [length averaging-f] 28 | :or {length 100 29 | averaging-f #(avg %)}}] 30 | (let [q (atom [])] 31 | (reify p/Sampler 32 | (-sample! [_ rtt] 33 | (->> (swap-vals! q 34 | (fn [q-val] 35 | (conj (cond-> q-val 36 | (>= (count q-val) length) 37 | pop) 38 | rtt))) 39 | (map averaging-f))) 40 | clojure.lang.IDeref 41 | (deref [_] 42 | (averaging-f @q)))))) 43 | -------------------------------------------------------------------------------- /src/s_exp/flex/protocols.clj: -------------------------------------------------------------------------------- 1 | (ns s-exp.flex.protocols 2 | (:refer-clojure :exclude [time])) 3 | 4 | (defprotocol Limit 5 | (-state [this] 6 | "Returns limit current state") 7 | (-watch-limit! [this k f] 8 | "Adds watch `f` on limit changes for key `k`") 9 | (-update! [this rtt in-flight dropped?] 10 | "Updates limit state for current request `rtt`, number of `in-flight` requests and potentially `dropped?` status")) 11 | 12 | (defprotocol Limiter 13 | (-acquire! [this] "Attempts to acquire Request")) 14 | 15 | (defprotocol Request 16 | (-accepted? [this] "Returns true if the requests was accepted") 17 | (-complete! [this] "Mark the current request as complete, updates sample/limit") 18 | (-reject! [this] "Marks the request as rejectd")) 19 | 20 | (defprotocol Sampler 21 | (-sample! [this val] "Records avg rtts for context")) 22 | 23 | (defprotocol Counter 24 | (-inc! [this] "Increases in-flight requests count") 25 | (-dec! [this] "Decreases in-flight requests count")) 26 | 27 | (defprotocol Clock 28 | (-duration [this start-time] 29 | "Returns duration from `start-time` to `now` in nanosecs")) 30 | 31 | (def state -state) 32 | (def update! -update!) 33 | (def watch-limit! -watch-limit!) 34 | (def acquire! -acquire!) 35 | (def sample! -sample!) 36 | (def inc! -inc!) 37 | (def dec! -dec!) 38 | (def duration -duration) 39 | 40 | (def complete! -complete!) 41 | (def reject! -reject!) 42 | (def accepted? -accepted?) 43 | -------------------------------------------------------------------------------- /src/s_exp/flex/limit/aimd.clj: -------------------------------------------------------------------------------- 1 | (ns s-exp.flex.limit.aimd 2 | "Implementation of AIMD Limit, 3 | https://en.wikipedia.org/wiki/Additive_increase/multiplicative_decrease 4 | 5 | By default it will increase by 1 on low avg rtt and backoff by 0.9 6 | on elevated/ing latency, both functions of current limit are 7 | modifiable via config" 8 | (:require [s-exp.flex.protocols :as p])) 9 | 10 | (def defaults 11 | {:initial-limit 20 12 | ;; :round-fn (fn [rtt-avg] (*)) 13 | :dec-by 0.90 14 | :min-limit 20 15 | :max-limit 200 16 | :inc-limit inc 17 | :timeout 5000}) 18 | 19 | (defn- within-range 20 | [x min-limit max-limit] 21 | (-> x 22 | (min max-limit) 23 | (max min-limit))) 24 | 25 | (defn make 26 | ([] (make {})) 27 | ([opts] 28 | (let [{:as _opts 29 | :keys [initial-limit inc-limit dec-by 30 | timeout max-limit min-limit]} 31 | (merge defaults opts) 32 | limit (atom initial-limit)] 33 | (reify 34 | clojure.lang.IDeref 35 | (deref [_] @limit) 36 | 37 | p/Limit 38 | (-state [_] @limit) 39 | 40 | (-watch-limit! [_ k f] 41 | (add-watch limit 42 | k 43 | (fn [_k _r old new] 44 | (when (not= old new) 45 | (f new))))) 46 | 47 | (-update! [_ rtt in-flight dropped?] 48 | (swap! limit 49 | (fn [limit-val] 50 | (-> (if (or dropped? (> rtt timeout)) 51 | (long (* dec-by limit-val)) 52 | (inc-limit limit-val)) 53 | (within-range min-limit max-limit))))))))) 54 | -------------------------------------------------------------------------------- /dev/playground.clj: -------------------------------------------------------------------------------- 1 | (ns playground 2 | (:require [ring.adapter.jetty :as j] 3 | [exoscale.ex :as ex] 4 | [s-exp.flex.limit.aimd :as limit] 5 | [s-exp.flex :as f] 6 | [s-exp.flex.interceptor :as ix] 7 | [s-exp.flex.middleware] 8 | [exoscale.interceptor])) 9 | 10 | ;; (def tf (bound-fn* println)) 11 | ;; (remove-tap tf) 12 | ;; (add-tap tf) 13 | 14 | (defn ok-response [s] 15 | {:status 200 16 | :body (pr-str s)}) 17 | 18 | (defn rejected-response [s] 19 | {:status 420 20 | :body (format "Enhance your calm - %s" (ex-message s))}) 21 | 22 | (def limit (s-exp.flex.limit.aimd/make 23 | {:initial-limit 10 24 | :max-limit 20 25 | :min-limit 1})) 26 | 27 | (def limiter (f/limiter {:limit limit})) 28 | (def ix (ix/interceptor {:limiter limiter})) 29 | 30 | (defn interceptor-handler [request] 31 | (exoscale.interceptor/execute 32 | {:request request} 33 | [{:error (fn [_ctx err] 34 | (if (ex/type? err :s-exp.flex/rejected) 35 | (rejected-response @limiter) 36 | {:status 500 37 | :body (str "boom -" err)})) 38 | 39 | :leave :response} 40 | 41 | #'ix 42 | {:enter (fn [ctx] 43 | (Thread/sleep (rand-int 1000)) 44 | (assoc ctx 45 | :response 46 | (ok-response @limiter)))}])) 47 | 48 | (defn server+interceptor 49 | [] 50 | (j/run-jetty #'interceptor-handler 51 | {:port 8080 52 | :join? false})) 53 | 54 | (def resp-time (atom 500)) 55 | 56 | (defn server+middleware [] 57 | (let [handler (s-exp.flex.middleware/with-limiter 58 | (fn [_] 59 | (Thread/sleep 60 | ;; 1000 ; simulate stable 61 | (max 1 (swap! resp-time inc)) ; simulate slowing down 62 | ;; (max 1 (swap! resp-time dec)) ; simulate faster resp times 63 | ) 64 | (ok-response @limiter)) 65 | {:limiter limiter})] 66 | (j/run-jetty (fn [request] 67 | (ex/try+ (handler request) 68 | (catch :s-exp.flex/rejected _ 69 | (rejected-response @limiter)))) 70 | {:port 8080 71 | :join? false}))) 72 | 73 | (declare server) 74 | (try (.stop server) (catch Exception _ :boom)) 75 | (def server (server+middleware)) 76 | -------------------------------------------------------------------------------- /src/s_exp/flex.clj: -------------------------------------------------------------------------------- 1 | (ns s-exp.flex 2 | (:require [s-exp.flex.protocols :as p] 3 | [s-exp.flex.limit.aimd :as aimd] 4 | [s-exp.flex.sampler.windowed :as sampler]) 5 | (:import (java.util.concurrent.atomic AtomicLong))) 6 | 7 | (defn counter 8 | ([{:keys [initial] 9 | :or {initial 0}}] 10 | (let [atm (AtomicLong. initial)] 11 | (reify p/Counter 12 | (-inc! [_] (.incrementAndGet atm)) 13 | (-dec! [_] (.decrementAndGet atm)) 14 | clojure.lang.IDeref 15 | (deref [_] (.get atm)))))) 16 | 17 | (defn clock 18 | ([_opts] 19 | (reify p/Clock 20 | (-duration [_ t] (- (System/currentTimeMillis) t)) 21 | clojure.lang.IDeref 22 | (deref [_] (System/currentTimeMillis))))) 23 | 24 | (defn- noop [& _]) 25 | 26 | (defn request 27 | [{:keys [clock sampler counter limit ; deps 28 | accept? dropped?] 29 | :as limiter}] 30 | (let [current-limit @limit 31 | in-flight (p/-inc! counter) 32 | start-time @clock 33 | ;; whether we're still within acceptable limits 34 | accepted (accept? in-flight current-limit) 35 | {:s-exp.flex.hooks/keys [reject complete] 36 | :or {reject noop complete noop}} limiter] 37 | (reify p/Request 38 | (-accepted? [_] accepted) 39 | 40 | (-complete! [this] 41 | (let [rtt (p/duration clock start-time) 42 | in-flight @counter 43 | [old-rtt new-rtt] (p/sample! sampler rtt) 44 | ;; "drop" as in avg drop, not request drop 45 | dropped (dropped? old-rtt new-rtt)] 46 | (p/update! limit 47 | rtt 48 | in-flight 49 | dropped) 50 | (p/-dec! counter) 51 | (complete this rtt in-flight dropped))) 52 | 53 | (-reject! [this] 54 | (p/dec! counter) 55 | (reject this)) 56 | 57 | clojure.lang.IDeref 58 | (deref [_] 59 | {::current-limit current-limit 60 | ::start-time start-time 61 | ::accepted? accepted 62 | ::sample @sampler})))) 63 | 64 | (defn- limiter-defaults 65 | [{:keys [clock sampler limit counter quota accept? dropped?] 66 | :or {quota 1.0} 67 | :as opts}] 68 | (cond-> opts 69 | (not limit) 70 | (assoc :limit (aimd/make opts)) 71 | (not counter) 72 | (assoc :counter (s-exp.flex/counter opts)) 73 | 74 | (not sampler) 75 | (assoc :sampler (sampler/make opts)) 76 | 77 | (not clock) 78 | (assoc :clock (s-exp.flex/clock opts)) 79 | 80 | (not accept?) 81 | (assoc :accept? 82 | (fn accept? [in-flight current-limit] 83 | (<= in-flight (* quota current-limit)))) 84 | 85 | (not dropped?) 86 | (assoc :dropped? 87 | (fn dropped? [old-rtt new-rtt] 88 | (> new-rtt old-rtt))))) 89 | 90 | (defrecord Limiter [clock sampler limit counter accept? hooks] 91 | p/Limiter 92 | (-acquire! [this] (request this)) 93 | clojure.lang.IDeref 94 | (deref [_] 95 | {:in-flight @counter 96 | :limit @limit 97 | :sample @sampler})) 98 | 99 | (defn limiter 100 | [opts] 101 | (map->Limiter (limiter-defaults opts))) 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flex 2 | 3 | icon by https://thenounproject.com/ivankostriukov/ 4 | 5 | **WIP** 6 | 7 | > Hold your horses! 8 | 9 | Library that implements various methods from TCP congestion control to request 10 | *limiting*. It's said to be adaptive in the sense that the `concurrency limit` 11 | of a system will evolve over time as we observe/measure average request 12 | round trip time. 13 | 14 | We provide both a middleware and an interceptor that will limit 15 | concurrency according to the limit algo specified. 16 | 17 | A typical setup is composed of : 18 | 19 | * a *Limit*: defines how the `concurrency limit` of a context evolve from 20 | [RTT](https://en.wikipedia.org/wiki/Round-trip_delay) averages, dropped 21 | requests, timeout. Flex has an additive-increase/multiplicative-decrease 22 | (AIMD) implementation right now and a few others more experimental. 23 | 24 | * *Sampler*: samples RTT for a context in order to compute an average to be used 25 | later by `Limit` when computing a new `concurrency limit`. It's pluggable so you 26 | can imagine more fine grained sampling, time-boxed, multi-values (min/max), 27 | percentiles and whatnot. It's really easy to implement/extend. 28 | 29 | * *Request* : takes a `Sampler` and a `Limit`implementation and allows to 30 | performs recording of a request Lifecycle (accepted/rejected/completed) and 31 | trigger subsequent `concurrency limits` update. Typically upon every hit to a 32 | system a Request is *acquired* for checks/update. 33 | 34 | * *Limiter* (naming subject to change) : very thin component that will 35 | encapsulate a sampler/limit/request implementation and allow to `acquire` a 36 | *Request* to inspect/update the `concurrency limits` depending on the request 37 | lifecycle (accept/reject/etc). We can also assign quotas at this level: we can 38 | have many *Limiters* per *Limit*/*Sampler*, this allows to say client A should 39 | only be allowed 20% of the available concurrency limits against that system 40 | and so on (it's set per Limiter instance). 41 | 42 | How it works in practice: 43 | 44 | Typically when we observe that average rtt increase we might decrease the 45 | acceptable `concurrency limit` value and in case of average rtt decrease we 46 | would slowly increase that value. Depending on the *Limit* implementation used, 47 | the rate at which the `concurrency limit` increase/decrease happens can vary. 48 | Then the middleware or the interceptor or whatever you choose to implement on 49 | top, would compare the number of current in-flight requests against the current 50 | `concurrency limit` to decide to reject/accept/ignore it and then trigger an 51 | update of the `concurrency limit` accordingly. 52 | 53 | Over time we would see the actual `concurrency limit` of a service 54 | stabilise/converge to a level that is its actual acceptable rate for near 55 | optimal operation and it would adapt with the health of the system it protects: 56 | if the system is stable it will try to increase limits slowly up to `max-limit`, 57 | if it's struggling it will lower the limit and cause requests to be rejected at 58 | the edge. 59 | 60 | This strategies can be applied to many context, they can be used at 61 | client level, server, queues, executors, per API endpoint, etc... 62 | 63 | 64 | ## Installation 65 | 66 | `{:deps {com.s-exp/flex {:git/sha "..." :git/url "https://github.com/mpenet/flex"}}}` 67 | 68 | ## Usage 69 | 70 | For now just playing via https://github.com/mpenet/flex/blob/main/dev/playground.clj 71 | 72 | The [ring 73 | middleware](https://github.com/mpenet/flex/blob/main/src/s_exp/flex/middleware.clj) 74 | or the 75 | [interceptor](https://github.com/mpenet/flex/blob/main/src/s_exp/flex/interceptor.clj) 76 | are also decent examples of how things work on the surface. 77 | 78 | 79 | ## Options 80 | 81 | [wip] 82 | 83 | ## Examples 84 | 85 | [wip] 86 | 87 | ## License 88 | 89 | Copyright © 2022 Max Penet 90 | 91 | Distributed under the Eclipse Public License version 1.0. 92 | -------------------------------------------------------------------------------- /src/s_exp/flex/limit/gradient2.clj: -------------------------------------------------------------------------------- 1 | (ns s-exp.flex.limit.gradient2 2 | (:require [s-exp.flex.protocols :as p])) 3 | 4 | (defn factor 5 | [n] 6 | (/ 2.0 (int (inc n)))) 7 | 8 | (defn measurement-add 9 | [state-val sample] 10 | (let [{:keys [value sum count window warmup-window]} (:exp-avg-measurement state-val)] 11 | (if (< count warmup-window) 12 | (let [count (inc count) 13 | sum (+ sum (double sample))] 14 | (update state-val 15 | :exp-avg-measurement 16 | assoc 17 | :count count 18 | :sum (/ sum count) 19 | :value value)) 20 | (let [factor' (factor window)] 21 | (update state-val 22 | :exp-avg-measurement 23 | assoc 24 | :value (+ (* value (- 1 factor')) 25 | (* (double sample) factor'))))))) 26 | 27 | (def defaults {:initial-limit 20 28 | :limit 20 29 | :min-limit 20 30 | :max-concurrency 200 31 | :smoothing 0.2 32 | :long-window 600 33 | :rtt-tolerance 1.5 34 | :queue-size 4 35 | :last-rtt 0 36 | :exp-avg-measurement 37 | {:value 0.0 38 | :sum 0.0 39 | :count 0 40 | :window 10 41 | :warmup-window 600}}) 42 | 43 | (defn make 44 | ([] (make {})) 45 | ([opts] 46 | (let [{:as _opts 47 | :keys [initial-limit rtt-tolerance queue-size smoothing 48 | min-limit max-limit exp-avg-measurement]} (merge defaults opts) 49 | state (atom {:limit initial-limit 50 | :exp-avg-measurement exp-avg-measurement})] 51 | (reify 52 | clojure.lang.IDeref 53 | (deref [_] (:limit @state)) 54 | 55 | p/Limit 56 | (-state [_] @state) 57 | 58 | (-watch-limit! [_ k f] 59 | (add-watch state 60 | k 61 | (fn [_k _r old new] 62 | (when (not= (:limit old) (:limit new)) 63 | (f (:limit new)))))) 64 | 65 | (-update! [_ rtt start-time in-flight dropped?] 66 | (swap! state 67 | (fn [{:as state-val :keys [limit]}] 68 | (let [state-val (measurement-add state-val rtt) 69 | short-rtt (double rtt) 70 | long-rtt (-> state-val :exp-avg-measurement :value) 71 | state-val (cond-> state-val 72 | ;; If the long RTT is substantially larger than 73 | ;; the short RTT then reduce the 74 | ;; long RTT measurement. 75 | ;; This can happen when latency returns to normal 76 | ;; after a prolonged prior of 77 | ;; excessive load. 78 | ;; Reducing the long RTT without waiting for 79 | ;; the exponential smoothing helps 80 | ;; bring the system back to steady 81 | ;; state. 82 | (> (/ long-rtt short-rtt) 2) 83 | (update :exp-avg-measurement update :value #(* 0.95 %)))] 84 | ;; Don't grow the limit if we are app limited 85 | ;; FIXME check about the /2 86 | (if (> in-flight (/ limit 2)) 87 | state-val 88 | ;; Rtt could be higher than rtt_noload because of 89 | ;; smoothing rtt noload updates so set to 1.0 to 90 | ;; indicate no queuing. Otherwise calculate the 91 | ;; slope and don't allow it to be reduced by more 92 | ;; than half to avoid aggressive load-shedding due 93 | ;; to outliers. 94 | (let [gradient (max 0.5 (min 0.1 (* rtt-tolerance (/ long-rtt short-rtt)))) 95 | new-limit (+ (* limit gradient) queue-size) 96 | new-limit (+ (* limit (- 1 smoothing)) 97 | (* new-limit smoothing)) 98 | new-limit (max min-limit (min max-limit new-limit))] 99 | (assoc state-val :limit (int new-limit)))))))))))) 100 | -------------------------------------------------------------------------------- /src/s_exp/flex/limit/vegas.clj: -------------------------------------------------------------------------------- 1 | (ns s-exp.flex.limit.vegas 2 | "Implementation adapted from netflix/concurrency-limits 3 | https://github.com/Netflix/concurrency-limits/blob/master/LICENSE 4 | 5 | Limiter based on TCP Vegas where the limit increases by alpha if the queue_use 6 | is small (l < alpha) and decreases by alpha if the queue_use is large 7 | (l > beta). 8 | 9 | Queue size is calculated using the formula, 10 | queue_use = limit − BWE×RTTnoLoad = limit × (1 − RTTnoLoad/RTTactual) 11 | 12 | For traditional TCP Vegas alpha is typically 2-3 and beta is 13 | typically 4-6. To allow for better growth and stability at higher 14 | limits we set alpha=Max(3, 10% of the current limit) and beta=Max(6, 15 | 20% of the current limit)" 16 | (:require [s-exp.flex.protocols :as p]) 17 | (:import (java.util.concurrent ThreadLocalRandom))) 18 | 19 | (defn- log10 20 | [i] 21 | (int (Math/max (int 1) 22 | (int (Math/log10 i))))) 23 | 24 | (defn alpha 25 | [limit] 26 | (* 3 (log10 (int limit)))) 27 | 28 | (defn beta 29 | [limit] 30 | (* 6 (log10 (int limit)))) 31 | 32 | (defn threshold 33 | [limit] 34 | (* 6 (log10 (int limit)))) 35 | 36 | (defn limit-inc [limit] 37 | (+ (double limit) 38 | (log10 (int limit)))) 39 | 40 | (defn limit-dec [limit] 41 | (- (double limit) 42 | (log10 (int limit)))) 43 | 44 | (defn probe? 45 | [limit probe-count jitter probe-multiplier] 46 | (<= (* probe-count jitter probe-multiplier) 47 | limit)) 48 | 49 | (defn- smoothed-limit 50 | [new-limit max-limit smoothing limit] 51 | (int (+ (* limit (- 1 smoothing)) 52 | (* (max 1 53 | (min max-limit new-limit)) 54 | smoothing)))) 55 | 56 | (defn- estimated-limit 57 | [limit rtt-no-load max-limit smoothing rtt in-flight dropped?] 58 | (cond 59 | ;; Treat any drop (i.e timeout) as needing to reduce the limit 60 | dropped? 61 | (smoothed-limit (limit-dec limit) 62 | max-limit 63 | smoothing 64 | limit) 65 | 66 | ;; Prevent upward drift if not close to the limit 67 | (< (* 2 in-flight) limit) 68 | limit 69 | 70 | :else 71 | (let [beta-limit (beta (int limit)) 72 | queue-size (int (Math/ceil (* limit 73 | (- (/ (double rtt-no-load) 74 | rtt))))) 75 | new-limit (cond 76 | ;; Aggressive increase when no queuing 77 | (<= queue-size (threshold limit)) 78 | (+ beta-limit limit) 79 | 80 | ;; Increase the limit if queue is still manageable 81 | (< (alpha limit) queue-size) 82 | (limit-inc limit) 83 | 84 | ;; Detecting latency so decrease 85 | (> queue-size beta-limit) 86 | (limit-dec limit))] 87 | (if new-limit 88 | (smoothed-limit new-limit 89 | max-limit 90 | smoothing 91 | limit) 92 | limit)))) 93 | 94 | (def defaults 95 | {:initial-limit 20 96 | :max-limit 1000 97 | :probe-multiplier 30 98 | :smoothing 1.0}) 99 | 100 | (defn make 101 | [opts] 102 | (let [{:as _opts 103 | :keys [initial-limit max-limit probe-multiplier 104 | in-flight rtt smoothing]} 105 | (merge defaults opts) 106 | state (atom {:limit initial-limit 107 | :probe-count 0 108 | :probe-jitter (-> (ThreadLocalRandom/current) 109 | (.nextDouble 0.5 1)) 110 | :rtt-no-load 0})] 111 | (reify 112 | clojure.lang.IDeref 113 | (deref [_] (:limit @state)) 114 | 115 | p/Limit 116 | (-state [_] @state) 117 | (-watch-limit! [_ k f] 118 | (add-watch state 119 | k 120 | (fn [_k _r 121 | {old-limit :limit} 122 | {new-limit :limit}] 123 | (when (not= old-limit 124 | new-limit) 125 | (f new-limit))))) 126 | 127 | (-update! [_ rtt in-flight dropped?] 128 | (-> (swap! state 129 | (fn [{:keys [limit rtt-no-load probe-count probe-jitter] :as state-val}] 130 | (let [probe-count (inc probe-count)] 131 | (cond 132 | (probe? limit 133 | probe-count 134 | probe-jitter 135 | probe-multiplier) 136 | (assoc state-val 137 | :probe-jitter (-> (ThreadLocalRandom/current) 138 | (.nextDouble 0.5 1)) 139 | :probe-count 0 140 | :rtt-no-load rtt) 141 | 142 | (or (zero? rtt-no-load) 143 | (> rtt rtt-no-load)) 144 | (assoc state-val 145 | :probe-count probe-count 146 | :rtt-no-load rtt) 147 | 148 | :else 149 | (assoc state-val 150 | :probe-count probe-count 151 | :limit (estimated-limit limit rtt-no-load 152 | max-limit smoothing rtt 153 | in-flight dropped?)))))) 154 | :limit))))) 155 | --------------------------------------------------------------------------------