From 34d2842a66434ed98bd8cf8e892ea567df51d6a0 Mon Sep 17 00:00:00 2001 From: Mark Sto Date: Fri, 9 May 2025 17:22:15 +0400 Subject: [PATCH 01/23] Streamline the `rate-limiter` implementation - extract the common sleep duration logic into the `->sleep-ms` fn - clearly separate millis from `nil` in code for fns return values - get rid of excessive `do-acquire` and `do-try-acquire` fns - move an actual sleeping into the `sleep` utility fn --- src/diehard/rate_limiter.clj | 75 +++++++++++++++--------------------- src/diehard/util.clj | 4 ++ 2 files changed, 35 insertions(+), 44 deletions(-) diff --git a/src/diehard/rate_limiter.clj b/src/diehard/rate_limiter.clj index 70f4f1f..b2d3d0f 100644 --- a/src/diehard/rate_limiter.clj +++ b/src/diehard/rate_limiter.clj @@ -1,4 +1,6 @@ -(ns diehard.rate-limiter) +(ns diehard.rate-limiter + (:require [diehard.util :as u]) + (:import [clojure.lang ExceptionInfo])) (defprotocol IRateLimiter (acquire! @@ -14,14 +16,6 @@ (declare refill acquire-sleep-ms try-acquire-sleep-ms) -(defn- do-acquire [rate-limiter permits] - (refill rate-limiter) - (acquire-sleep-ms rate-limiter permits)) - -(defn- do-try-acquire [rate-limiter permits max-wait-ms] - (refill rate-limiter) - (try-acquire-sleep-ms rate-limiter permits max-wait-ms)) - (defrecord TokenBucketRateLimiter [rate max-tokens ;; internal state state] @@ -29,24 +23,20 @@ (acquire! [this] (acquire! this 1)) (acquire! [this permits] - (let [sleep (do-acquire this permits)] - (when (> sleep 0) - (Thread/sleep ^long sleep)))) + (refill this) + (let [sleep-ms (acquire-sleep-ms this permits)] + (u/sleep sleep-ms))) (try-acquire [this] (try-acquire this 1)) (try-acquire [this permits] (try-acquire this permits 0)) (try-acquire [this permits wait-ms] - (let [sleep (do-try-acquire this permits wait-ms)] - (if (false? sleep) - false - (do - (when (> sleep 0) - (Thread/sleep ^long sleep)) - true))))) + (refill this) + (if-some [sleep-ms (try-acquire-sleep-ms this permits wait-ms)] + (do (u/sleep sleep-ms) true) + false))) (defn- refill [^TokenBucketRateLimiter rate-limiter] - ;; refill (let [now (System/currentTimeMillis)] (swap! (.-state rate-limiter) (fn [state] @@ -59,32 +49,29 @@ %)) (assoc :last-refill-ts now)))))) -(defn- acquire-sleep-ms [^TokenBucketRateLimiter rate-limiter permits] - (let [{pending-tokens :reserved-tokens} (swap! (.-state rate-limiter) - update :reserved-tokens + permits)] - (if (<= pending-tokens 0) - 0 - ;; time as milliseconds - (long (/ pending-tokens (.-rate rate-limiter)))))) +(defn- ->sleep-ms ^long [pending-tokens rate] + (if (<= pending-tokens 0) 0 (long (/ pending-tokens rate)))) -(defn- try-acquire-sleep-ms [^TokenBucketRateLimiter rate-limiter permits max-wait-ms] +(defn- acquire-sleep-ms + ^long [^TokenBucketRateLimiter rate-limiter permits] + (let [state (swap! (.-state rate-limiter) update :reserved-tokens + permits)] + (->sleep-ms (:reserved-tokens state) (.-rate rate-limiter)))) + +(defn- try-acquire-sleep-ms + ^long [^TokenBucketRateLimiter rate-limiter permits max-wait-ms] (try - (let [{pending-tokens :reserved-tokens} - (swap! (.-state rate-limiter) - (fn [state] - (update state :reserved-tokens - (fn [pending-tokens] - ;; test if we can pass in wait period - (if (<= (- (+ pending-tokens permits) - (* max-wait-ms (.-rate rate-limiter))) - 0) - (+ pending-tokens permits) - (throw (ex-info "Not enough permits." {:rate-limiter true})))))))] - (if (<= pending-tokens 0) - 0 - (long (/ pending-tokens (.-rate rate-limiter))))) - (catch clojure.lang.ExceptionInfo _ - false))) + (let [state (swap! (.-state rate-limiter) + (fn [state] + (update state :reserved-tokens + (fn [pending-tokens] + ;; test if we can pass in wait period + (if (<= (- (+ pending-tokens permits) + (* max-wait-ms (.-rate rate-limiter))) + 0) + (+ pending-tokens permits) + (throw (ex-info "Not enough permits" {:rate-limiter true})))))))] + (->sleep-ms (:reserved-tokens state) (.-rate rate-limiter))) + (catch ExceptionInfo _))) (def ^{:const true :no-doc true} allowed-rate-limiter-option-keys diff --git a/src/diehard/util.clj b/src/diehard/util.clj index c68905d..2dd2439 100644 --- a/src/diehard/util.clj +++ b/src/diehard/util.clj @@ -54,3 +54,7 @@ (reify EventListener (accept [_ e] (f e)))) + +(defn sleep [^long ms] + (when (pos? ms) + (Thread/sleep ms))) From 377e91e09a88f2934751bc78f649104266bc8d77 Mon Sep 17 00:00:00 2001 From: Mark Sto Date: Fri, 9 May 2025 17:43:34 +0400 Subject: [PATCH 02/23] Parametrize `rate-limiter` with an optional `sleep-fn` --- src/diehard/rate_limiter.clj | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/diehard/rate_limiter.clj b/src/diehard/rate_limiter.clj index b2d3d0f..40f201c 100644 --- a/src/diehard/rate_limiter.clj +++ b/src/diehard/rate_limiter.clj @@ -18,14 +18,14 @@ (defrecord TokenBucketRateLimiter [rate max-tokens ;; internal state - state] + state sleep-fn] IRateLimiter (acquire! [this] (acquire! this 1)) (acquire! [this permits] (refill this) (let [sleep-ms (acquire-sleep-ms this permits)] - (u/sleep sleep-ms))) + (sleep-fn sleep-ms))) (try-acquire [this] (try-acquire this 1)) (try-acquire [this permits] @@ -33,7 +33,7 @@ (try-acquire [this permits wait-ms] (refill this) (if-some [sleep-ms (try-acquire-sleep-ms this permits wait-ms)] - (do (u/sleep sleep-ms) true) + (do (sleep-fn sleep-ms) true) false))) (defn- refill [^TokenBucketRateLimiter rate-limiter] @@ -80,12 +80,15 @@ (defn rate-limiter "Create a default rate limiter with: * `rate`: permits per second (may be a floating point, e.g. 0.5 <=> 1 req every 2 sec) - * `max-cached-tokens`: the max size of tokens that the bucket can cache when it's idle" - [{:keys [rate max-cached-tokens] :as _opts}] + * `max-cached-tokens`: the max size of tokens that the bucket can cache when it's idle + * `sleep-fn`: a unary fn for custom 'sleep' semantics, by default `diehard.util/sleep`" + [{:keys [rate max-cached-tokens sleep-fn] :as _opts}] (if (some? rate) - (let [max-cached-tokens (or max-cached-tokens (int rate))] + (let [max-cached-tokens (or max-cached-tokens (int rate)) + sleep-fn (or sleep-fn u/sleep)] (TokenBucketRateLimiter. (/ (double rate) 1000) max-cached-tokens (atom {:reserved-tokens (double 0) - :last-refill-ts (long -1)}))) + :last-refill-ts (long -1)}) + sleep-fn)) (throw (IllegalArgumentException. ":rate is required for rate-limiter")))) From 6b85ed09d2e526bb5c6d0163610f04d10e0fa54b Mon Sep 17 00:00:00 2001 From: Mark Sto Date: Fri, 9 May 2025 17:44:51 +0400 Subject: [PATCH 03/23] Drop the unnecessary `allowed-rate-limiter-option-keys` var --- src/diehard/rate_limiter.clj | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/diehard/rate_limiter.clj b/src/diehard/rate_limiter.clj index 40f201c..2604bca 100644 --- a/src/diehard/rate_limiter.clj +++ b/src/diehard/rate_limiter.clj @@ -73,10 +73,6 @@ (->sleep-ms (:reserved-tokens state) (.-rate rate-limiter))) (catch ExceptionInfo _))) -(def ^{:const true :no-doc true} - allowed-rate-limiter-option-keys - #{:rate :max-cached-tokens}) - (defn rate-limiter "Create a default rate limiter with: * `rate`: permits per second (may be a floating point, e.g. 0.5 <=> 1 req every 2 sec) From 6201d1cfa89eb3e9833e1b5c2ac0cbee163b5694 Mon Sep 17 00:00:00 2001 From: Mark Sto Date: Fri, 9 May 2025 17:52:58 +0400 Subject: [PATCH 04/23] Update the specs and documentation --- src/diehard/core.clj | 7 +++---- src/diehard/spec.clj | 7 ++++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/diehard/core.clj b/src/diehard/core.clj index 625b5a5..97601e5 100644 --- a/src/diehard/core.clj +++ b/src/diehard/core.clj @@ -462,8 +462,8 @@ You can always check circuit breaker state with (defmacro ^{:doc "Create a rate limiter with options. -* `:rate` execution permits per second. -* `:max-cached-tokens` the max size of permits we can cache when idle"} +* `:rate` execution permits per second (may be a floating point, e.g. 0.5 <=> 1 req every 2 sec) +* `:max-cached-tokens` the max size of permit tokens that the bucket can cache when it's idle"} defratelimiter [name opts] `(def ~name (rl/rate-limiter (u/verify-opt-map-keys-with-spec :rate-limiter/rate-limiter-new ~opts)))) @@ -482,10 +482,9 @@ to given rate. Use `defratelimiter` to define a ratelimiter and use it as option By default it will wait forever until there is permits available. You can also specify a `max-wait-ms` to wait for a given time. If there's no permits in this period, this block -will throw a Clojure `ex-info`, with `ex-data` as +will throw a Clojure exception with a `:throttled true` entry in `ex-data`, as follows: ```clojure - (try (with-rate-limiter {:ratelimiter myfl :max-wait-ms 1000} diff --git a/src/diehard/spec.clj b/src/diehard/spec.clj index b47f87d..88186a1 100644 --- a/src/diehard/spec.clj +++ b/src/diehard/spec.clj @@ -135,14 +135,15 @@ :timeout/on-failure])) ;; rate limiter -; allow floating point numbers, so that we can pass numbers such as 0.5, to signify 1 req / 2 seconds -;(s/def :rate-limiter/rate int?) + (s/def :rate-limiter/rate number?) (s/def :rate-limiter/max-cached-tokens int?) +(s/def :rate-limiter/sleep-fn fn?) (s/def :rate-limiter/rate-limiter-new (only-keys :req-un [:rate-limiter/rate] - :opt-un [:rate-limiter/max-cached-tokens])) + :opt-un [:rate-limiter/max-cached-tokens + :rate-limiter/sleep-fn])) (s/def :rate-limiter/ratelimiter #(satisfies? dr/IRateLimiter %)) (s/def :rate-limiter/max-wait-ms int?) From e0399f4bbfe2713dc19618af3ceb3cd51d6ea7a9 Mon Sep 17 00:00:00 2001 From: Mark Sto Date: Fri, 9 May 2025 18:12:51 +0400 Subject: [PATCH 05/23] Fix a typo in tests --- test/diehard/core_test.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/diehard/core_test.clj b/test/diehard/core_test.clj index ca34a61..cd0f1ff 100644 --- a/test/diehard/core_test.clj +++ b/test/diehard/core_test.clj @@ -390,7 +390,7 @@ (let [counter0 (atom 0)] (try (while (< @counter0 200) - (with-rate-limiter {:ratelimiter my-rl + (with-rate-limiter {:ratelimiter my-rl3 :max-wait-ms 1} (my-fn counter0))) (is false) From 9e38dc47857341ff60453374da17d1498704404a Mon Sep 17 00:00:00 2001 From: Mark Sto Date: Fri, 9 May 2025 18:40:36 +0400 Subject: [PATCH 06/23] Improve on `try-acquire` exception handling --- src/diehard/rate_limiter.clj | 31 +++++++++++++++++-------------- test/diehard/core_test.clj | 4 +++- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/diehard/rate_limiter.clj b/src/diehard/rate_limiter.clj index 2604bca..05d2250 100644 --- a/src/diehard/rate_limiter.clj +++ b/src/diehard/rate_limiter.clj @@ -1,6 +1,5 @@ (ns diehard.rate-limiter - (:require [diehard.util :as u]) - (:import [clojure.lang ExceptionInfo])) + (:require [diehard.util :as u])) (defprotocol IRateLimiter (acquire! @@ -24,17 +23,14 @@ (acquire! this 1)) (acquire! [this permits] (refill this) - (let [sleep-ms (acquire-sleep-ms this permits)] - (sleep-fn sleep-ms))) + (acquire-sleep-ms this permits)) (try-acquire [this] (try-acquire this 1)) (try-acquire [this permits] (try-acquire this permits 0)) (try-acquire [this permits wait-ms] (refill this) - (if-some [sleep-ms (try-acquire-sleep-ms this permits wait-ms)] - (do (sleep-fn sleep-ms) true) - false))) + (try-acquire-sleep-ms this permits wait-ms))) (defn- refill [^TokenBucketRateLimiter rate-limiter] (let [now (System/currentTimeMillis)] @@ -53,12 +49,13 @@ (if (<= pending-tokens 0) 0 (long (/ pending-tokens rate)))) (defn- acquire-sleep-ms - ^long [^TokenBucketRateLimiter rate-limiter permits] - (let [state (swap! (.-state rate-limiter) update :reserved-tokens + permits)] - (->sleep-ms (:reserved-tokens state) (.-rate rate-limiter)))) + [^TokenBucketRateLimiter rate-limiter permits] + (let [state (swap! (.-state rate-limiter) update :reserved-tokens + permits) + sleep-ms (->sleep-ms (:reserved-tokens state) (.-rate rate-limiter))] + ((.-sleep-fn rate-limiter) sleep-ms))) (defn- try-acquire-sleep-ms - ^long [^TokenBucketRateLimiter rate-limiter permits max-wait-ms] + [^TokenBucketRateLimiter rate-limiter permits max-wait-ms] (try (let [state (swap! (.-state rate-limiter) (fn [state] @@ -69,9 +66,15 @@ (* max-wait-ms (.-rate rate-limiter))) 0) (+ pending-tokens permits) - (throw (ex-info "Not enough permits" {:rate-limiter true})))))))] - (->sleep-ms (:reserved-tokens state) (.-rate rate-limiter))) - (catch ExceptionInfo _))) + (throw (ex-info "Not enough permits" + {:rate-limiter true}))))))) + sleep-ms (->sleep-ms (:reserved-tokens state) (.-rate rate-limiter))] + ((.-sleep-fn rate-limiter) sleep-ms) + true) + (catch Exception e + (if-not (:rate-limiter (ex-data e)) + (throw e) + false)))) (defn rate-limiter "Create a default rate limiter with: diff --git a/test/diehard/core_test.clj b/test/diehard/core_test.clj index cd0f1ff..6810c70 100644 --- a/test/diehard/core_test.clj +++ b/test/diehard/core_test.clj @@ -2,7 +2,8 @@ (:require [clojure.test :refer :all] [diehard.circuit-breaker :as cb] [diehard.core :refer :all]) - (:import [dev.failsafe CircuitBreakerOpenException TimeoutExceededException] + (:import [clojure.lang IExceptionInfo] + [dev.failsafe CircuitBreakerOpenException TimeoutExceededException] [java.util.concurrent CountDownLatch])) (deftest test-retry @@ -395,6 +396,7 @@ (my-fn counter0))) (is false) (catch Exception e + (is (instance? IExceptionInfo e)) (is (:throttled (ex-data e)))))))) (testing "permits" From 5e6d50c114909aef532d102c5683c6142fd28fb1 Mon Sep 17 00:00:00 2001 From: Mark Sto Date: Fri, 9 May 2025 18:50:43 +0400 Subject: [PATCH 07/23] Rename `acquire!`/`try-acquire` logic implementing fns --- src/diehard/rate_limiter.clj | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/diehard/rate_limiter.clj b/src/diehard/rate_limiter.clj index 05d2250..b43c294 100644 --- a/src/diehard/rate_limiter.clj +++ b/src/diehard/rate_limiter.clj @@ -13,7 +13,7 @@ "Try to acquire given number of permits, allows blocking for at most `wait-ms` milliseconds. Return true if there are enough permits in permitted time.")) -(declare refill acquire-sleep-ms try-acquire-sleep-ms) +(declare refill do-acquire! do-try-acquire) (defrecord TokenBucketRateLimiter [rate max-tokens ;; internal state @@ -23,14 +23,14 @@ (acquire! this 1)) (acquire! [this permits] (refill this) - (acquire-sleep-ms this permits)) + (do-acquire! this permits)) (try-acquire [this] (try-acquire this 1)) (try-acquire [this permits] (try-acquire this permits 0)) (try-acquire [this permits wait-ms] (refill this) - (try-acquire-sleep-ms this permits wait-ms))) + (do-try-acquire this permits wait-ms))) (defn- refill [^TokenBucketRateLimiter rate-limiter] (let [now (System/currentTimeMillis)] @@ -48,13 +48,13 @@ (defn- ->sleep-ms ^long [pending-tokens rate] (if (<= pending-tokens 0) 0 (long (/ pending-tokens rate)))) -(defn- acquire-sleep-ms +(defn- do-acquire! [^TokenBucketRateLimiter rate-limiter permits] (let [state (swap! (.-state rate-limiter) update :reserved-tokens + permits) sleep-ms (->sleep-ms (:reserved-tokens state) (.-rate rate-limiter))] ((.-sleep-fn rate-limiter) sleep-ms))) -(defn- try-acquire-sleep-ms +(defn- do-try-acquire [^TokenBucketRateLimiter rate-limiter permits max-wait-ms] (try (let [state (swap! (.-state rate-limiter) From 919c8a864ee20fda2251b56f93be152435d648dd Mon Sep 17 00:00:00 2001 From: Mark Sto Date: Sun, 11 May 2025 17:48:28 +0400 Subject: [PATCH 08/23] Parametrize `sleep-fn` with state and permits to allow for having rollback semantics during an interruptible sleep --- src/diehard/rate_limiter.clj | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/diehard/rate_limiter.clj b/src/diehard/rate_limiter.clj index b43c294..73d85aa 100644 --- a/src/diehard/rate_limiter.clj +++ b/src/diehard/rate_limiter.clj @@ -52,7 +52,7 @@ [^TokenBucketRateLimiter rate-limiter permits] (let [state (swap! (.-state rate-limiter) update :reserved-tokens + permits) sleep-ms (->sleep-ms (:reserved-tokens state) (.-rate rate-limiter))] - ((.-sleep-fn rate-limiter) sleep-ms))) + ((.-sleep-fn rate-limiter) (.-state rate-limiter) permits sleep-ms))) (defn- do-try-acquire [^TokenBucketRateLimiter rate-limiter permits max-wait-ms] @@ -69,7 +69,7 @@ (throw (ex-info "Not enough permits" {:rate-limiter true}))))))) sleep-ms (->sleep-ms (:reserved-tokens state) (.-rate rate-limiter))] - ((.-sleep-fn rate-limiter) sleep-ms) + ((.-sleep-fn rate-limiter) (.-state rate-limiter) permits sleep-ms) true) (catch Exception e (if-not (:rate-limiter (ex-data e)) @@ -80,11 +80,13 @@ "Create a default rate limiter with: * `rate`: permits per second (may be a floating point, e.g. 0.5 <=> 1 req every 2 sec) * `max-cached-tokens`: the max size of tokens that the bucket can cache when it's idle - * `sleep-fn`: a unary fn for custom 'sleep' semantics, by default `diehard.util/sleep`" + * `sleep-fn`: a ternary fn of the current state, given permits and millis to sleep for + allowing for custom 'sleep' semantics; by default, calls `Thread/sleep`" [{:keys [rate max-cached-tokens sleep-fn] :as _opts}] (if (some? rate) (let [max-cached-tokens (or max-cached-tokens (int rate)) - sleep-fn (or sleep-fn u/sleep)] + sleep-fn (or sleep-fn + (fn [_state _permits ms] (u/sleep ms)))] (TokenBucketRateLimiter. (/ (double rate) 1000) max-cached-tokens (atom {:reserved-tokens (double 0) From ff389b39251956c8c5f2c452fc2b59fc3435d338 Mon Sep 17 00:00:00 2001 From: Mark Sto Date: Sun, 11 May 2025 17:53:43 +0400 Subject: [PATCH 09/23] Update the `core` ns docstring --- src/diehard/core.clj | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/diehard/core.clj b/src/diehard/core.clj index 97601e5..b191ae3 100644 --- a/src/diehard/core.clj +++ b/src/diehard/core.clj @@ -462,8 +462,12 @@ You can always check circuit breaker state with (defmacro ^{:doc "Create a rate limiter with options. -* `:rate` execution permits per second (may be a floating point, e.g. 0.5 <=> 1 req every 2 sec) -* `:max-cached-tokens` the max size of permit tokens that the bucket can cache when it's idle"} +* `:rate` execution permits per second (may be a floating point number, e.g. + 0.5 <=> 1 req every 2 sec) +* `:max-cached-tokens` the max size of permit tokens that the bucket can cache + when it's idle +* `:sleep-fn` a fn of the current state, given permits and millis to sleep for + allowing for custom 'sleep' semantics; by default, calls `Thread/sleep`"} defratelimiter [name opts] `(def ~name (rl/rate-limiter (u/verify-opt-map-keys-with-spec :rate-limiter/rate-limiter-new ~opts)))) From 36f6ee588b99486ca7846a73974851aabc346cc0 Mon Sep 17 00:00:00 2001 From: Mark Sto Date: Sat, 31 May 2025 18:23:34 +0400 Subject: [PATCH 10/23] Rethink the `sleep-fn` param of the `rate-limiter` (simplify it's contract) --- src/diehard/core.clj | 5 +++-- src/diehard/rate_limiter.clj | 30 +++++++++++++++++++++++------- src/diehard/util.clj | 4 ---- 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/src/diehard/core.clj b/src/diehard/core.clj index b191ae3..c129814 100644 --- a/src/diehard/core.clj +++ b/src/diehard/core.clj @@ -466,8 +466,9 @@ You can always check circuit breaker state with 0.5 <=> 1 req every 2 sec) * `:max-cached-tokens` the max size of permit tokens that the bucket can cache when it's idle -* `:sleep-fn` a fn of the current state, given permits and millis to sleep for - allowing for custom 'sleep' semantics; by default, calls `Thread/sleep`"} +* `:sleep-fn` a unary fn of millis to sleep for, allowing for custom sleep + semantics; by default, sleeps interruptedly; pass `uninterruptible-sleep` + to sleep uninterruptedly"} defratelimiter [name opts] `(def ~name (rl/rate-limiter (u/verify-opt-map-keys-with-spec :rate-limiter/rate-limiter-new ~opts)))) diff --git a/src/diehard/rate_limiter.clj b/src/diehard/rate_limiter.clj index 73d85aa..4ac88f1 100644 --- a/src/diehard/rate_limiter.clj +++ b/src/diehard/rate_limiter.clj @@ -1,5 +1,5 @@ (ns diehard.rate-limiter - (:require [diehard.util :as u])) + (:import [java.util.concurrent.locks LockSupport])) (defprotocol IRateLimiter (acquire! @@ -52,7 +52,7 @@ [^TokenBucketRateLimiter rate-limiter permits] (let [state (swap! (.-state rate-limiter) update :reserved-tokens + permits) sleep-ms (->sleep-ms (:reserved-tokens state) (.-rate rate-limiter))] - ((.-sleep-fn rate-limiter) (.-state rate-limiter) permits sleep-ms))) + ((.-sleep-fn rate-limiter) sleep-ms))) (defn- do-try-acquire [^TokenBucketRateLimiter rate-limiter permits max-wait-ms] @@ -69,24 +69,40 @@ (throw (ex-info "Not enough permits" {:rate-limiter true}))))))) sleep-ms (->sleep-ms (:reserved-tokens state) (.-rate rate-limiter))] - ((.-sleep-fn rate-limiter) (.-state rate-limiter) permits sleep-ms) + ((.-sleep-fn rate-limiter) sleep-ms) true) (catch Exception e (if-not (:rate-limiter (ex-data e)) (throw e) false)))) +(defn interruptible-sleep [^long ms] + (when (pos? ms) + (Thread/sleep ms))) + +(defn uninterruptible-sleep [^long ms] + (when (pos? ms) + (let [end-time-ns (+ (System/nanoTime) (* 1000000 ms))] + (loop [interrupted? false] + (let [remaining-ns (- end-time-ns (System/nanoTime))] + (if (<= remaining-ns 0) + (when interrupted? + (.interrupt (Thread/currentThread))) + (do + (LockSupport/parkNanos remaining-ns) + (recur (or interrupted? (Thread/interrupted)))))))))) + (defn rate-limiter "Create a default rate limiter with: * `rate`: permits per second (may be a floating point, e.g. 0.5 <=> 1 req every 2 sec) * `max-cached-tokens`: the max size of tokens that the bucket can cache when it's idle - * `sleep-fn`: a ternary fn of the current state, given permits and millis to sleep for - allowing for custom 'sleep' semantics; by default, calls `Thread/sleep`" + * `sleep-fn`: a unary fn of millis to sleep for, allowing for custom sleep semantics; + by default, sleeps interruptedly; pass `uninterruptible-sleep` to sleep + uninterruptedly" [{:keys [rate max-cached-tokens sleep-fn] :as _opts}] (if (some? rate) (let [max-cached-tokens (or max-cached-tokens (int rate)) - sleep-fn (or sleep-fn - (fn [_state _permits ms] (u/sleep ms)))] + sleep-fn (or sleep-fn interruptible-sleep)] (TokenBucketRateLimiter. (/ (double rate) 1000) max-cached-tokens (atom {:reserved-tokens (double 0) diff --git a/src/diehard/util.clj b/src/diehard/util.clj index 2dd2439..c68905d 100644 --- a/src/diehard/util.clj +++ b/src/diehard/util.clj @@ -54,7 +54,3 @@ (reify EventListener (accept [_ e] (f e)))) - -(defn sleep [^long ms] - (when (pos? ms) - (Thread/sleep ms))) From ca17b601bfd6cd0bc8f823781f6ce6371b763d4d Mon Sep 17 00:00:00 2001 From: Mark Sto Date: Sat, 31 May 2025 20:38:36 +0400 Subject: [PATCH 11/23] Re-impl `uninterruptible-sleep` fn w/o `LockSupport` (only handle exceptions) --- src/diehard/rate_limiter.clj | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/diehard/rate_limiter.clj b/src/diehard/rate_limiter.clj index 4ac88f1..16698c1 100644 --- a/src/diehard/rate_limiter.clj +++ b/src/diehard/rate_limiter.clj @@ -1,5 +1,4 @@ -(ns diehard.rate-limiter - (:import [java.util.concurrent.locks LockSupport])) +(ns diehard.rate-limiter) (defprotocol IRateLimiter (acquire! @@ -80,17 +79,26 @@ (when (pos? ms) (Thread/sleep ms))) +(def ^:private nanos-in-ms 1000000) + (defn uninterruptible-sleep [^long ms] (when (pos? ms) - (let [end-time-ns (+ (System/nanoTime) (* 1000000 ms))] - (loop [interrupted? false] - (let [remaining-ns (- end-time-ns (System/nanoTime))] - (if (<= remaining-ns 0) - (when interrupted? - (.interrupt (Thread/currentThread))) - (do - (LockSupport/parkNanos remaining-ns) - (recur (or interrupted? (Thread/interrupted)))))))))) + (let [end-time-ns (+ (System/nanoTime) (* nanos-in-ms ms))] + (with-local-vars [interrupted? false] + (try (loop [] + (let [remaining-ns (- end-time-ns (System/nanoTime))] + ;; required sleep duration fully consumed — exit + (when (< 0 remaining-ns) + (try + (Thread/sleep (quot remaining-ns nanos-in-ms) + (mod remaining-ns nanos-in-ms)) + ;; successful sleep — exit + (catch InterruptedException _ + (var-set interrupted? true))) + ;; can only recur from tail position + (when @interrupted? (recur))))) + (finally + (when @interrupted? (.interrupt (Thread/currentThread))))))))) (defn rate-limiter "Create a default rate limiter with: From fff7921da430c3934b82975f789e91faee66d3c9 Mon Sep 17 00:00:00 2001 From: Mark Sto Date: Sun, 8 Jun 2025 17:34:09 +0400 Subject: [PATCH 12/23] Add test cases for uninterruptible sleep --- test/diehard/rate_limiter_test.clj | 138 ++++++++++++++++++++--------- 1 file changed, 97 insertions(+), 41 deletions(-) diff --git a/test/diehard/rate_limiter_test.clj b/test/diehard/rate_limiter_test.clj index a82e820..210cea3 100644 --- a/test/diehard/rate_limiter_test.clj +++ b/test/diehard/rate_limiter_test.clj @@ -1,42 +1,98 @@ (ns diehard.rate-limiter-test - (:require [clojure.test :as t] - [diehard.rate-limiter :as r]) - (:import [java.util.concurrent Executors])) - -(t/deftest rate-limiter-test - (t/testing "base case" - (let [rate 1000 - threads 32 - pool (Executors/newFixedThreadPool threads) - rl (r/rate-limiter {:rate rate}) - counter (atom 0) - time-secs 2] - (doseq [_ (range threads)] - (.submit pool (cast Runnable (fn [] - (while true - (r/acquire! rl) - (swap! counter inc)))))) - (Thread/sleep (* time-secs 1000)) - (.shutdown pool) - (t/is (< (Math/abs (- @counter (* rate time-secs))) - ;; error tolerance 3% - (* 0.03 (* rate time-secs)))))) - - (t/testing "rate limits less than 1.0" - (let [rate 0.5 - threads 1 - pool (Executors/newFixedThreadPool threads) - rl (r/rate-limiter {:rate rate}) - counter (atom 0) - time-secs 5] - (doseq [_ (range threads)] - (.submit pool (cast Runnable (fn [] - (while true - (r/acquire! rl) - (swap! counter inc)))))) - (Thread/sleep (* time-secs 1000)) - (.shutdown pool) - (let [variance (Math/abs (- @counter (* rate time-secs)))] - (t/is (<= variance - ;; error tolerance: 0.5 -- larger, because we are dealing with slower ticks - 0.5)))))) + (:require [clojure.test :refer [deftest testing is]] + [diehard.rate-limiter :as rl]) + (:import [java.time Duration] + [java.util.concurrent ExecutorService Executors Future])) + +(def default-error + "Relative error in percent. 3% by default." + 0.03) + +(defn- approx== + ([expected actual] + (approx== expected actual (* default-error expected))) + ([expected actual tolerance] + (< (abs (- expected actual)) tolerance))) + +(defn- submit + ^Future [^ExecutorService executor ^Callable f] + (ExecutorService/.submit executor f)) + +(deftest rate-limiter-test + (testing "base case" + (testing "interruptible sleep" + (let [rate 1000 + rate-limiter (rl/rate-limiter {:rate rate}) + + threads 32 + pool (Executors/newFixedThreadPool threads) + + counter (atom 0) + time-secs 2] + (doseq [_ (range threads)] + (submit pool (fn [] + (while true + (rl/acquire! rate-limiter) + (swap! counter inc))))) + (Thread/sleep (Duration/ofSeconds time-secs)) + (ExecutorService/.shutdown pool) + (is (approx== (* rate time-secs) @counter)))) + + (testing "uninterruptible sleep" + (let [rate 1000 + rate-limiter (rl/rate-limiter {:rate rate + :sleep-fn rl/uninterruptible-sleep}) + + threads 32 + pool (Executors/newFixedThreadPool threads) + + counter (atom 0) + time-secs 2] + (doseq [_ (range threads)] + (submit pool (fn [] + (while true + (rl/acquire! rate-limiter) + (swap! counter inc))))) + (Thread/sleep (Duration/ofSeconds time-secs)) + (ExecutorService/.shutdown pool) + (is (approx== (* rate time-secs) @counter))))) + + (testing "rate less than 1.0" + (testing "interruptible sleep" + (let [rate 0.5 + rate-limiter (rl/rate-limiter {:rate rate}) + + threads 32 + pool (Executors/newFixedThreadPool threads) + + counter (atom 0) + time-secs 5] + (doseq [_ (range threads)] + (submit pool (fn [] + (while true + (rl/acquire! rate-limiter) + (swap! counter inc))))) + (Thread/sleep (Duration/ofSeconds time-secs)) + (ExecutorService/.shutdown pool) + ;; exact error tolerance, since we are dealing with much slower ticks + (is (approx== (* rate time-secs) @counter 1)))) + + (testing "uninterruptible sleep" + (let [rate 0.5 + rate-limiter (rl/rate-limiter {:rate rate + :sleep-fn rl/uninterruptible-sleep}) + + threads 32 + pool (Executors/newFixedThreadPool threads) + + counter (atom 0) + time-secs 5] + (doseq [_ (range threads)] + (submit pool (fn [] + (while true + (rl/acquire! rate-limiter) + (swap! counter inc))))) + (Thread/sleep (Duration/ofSeconds time-secs)) + (ExecutorService/.shutdown pool) + ;; exact error tolerance, since we are dealing with much slower ticks + (is (approx== (* rate time-secs) @counter 1)))))) From 11f43cb1428b2efddfc5dd2a00c5fc3f18d00d11 Mon Sep 17 00:00:00 2001 From: Mark Sto Date: Sun, 8 Jun 2025 17:47:05 +0400 Subject: [PATCH 13/23] Refactor rate limiter tests --- test/diehard/rate_limiter_test.clj | 87 ++++++++++-------------------- 1 file changed, 29 insertions(+), 58 deletions(-) diff --git a/test/diehard/rate_limiter_test.clj b/test/diehard/rate_limiter_test.clj index 210cea3..5b6912d 100644 --- a/test/diehard/rate_limiter_test.clj +++ b/test/diehard/rate_limiter_test.clj @@ -18,81 +18,52 @@ ^Future [^ExecutorService executor ^Callable f] (ExecutorService/.submit executor f)) +(defn- run-rate-limited-counting + [rate-limiter run-time-sec] + (let [threads 32 + pool (Executors/newFixedThreadPool threads) + + counter (atom 0)] + (doseq [_ (range threads)] + (submit pool (fn [] + (while true + (rl/acquire! rate-limiter) + (swap! counter inc))))) + (Thread/sleep (Duration/ofSeconds run-time-sec)) + (ExecutorService/.shutdown pool) + @counter)) + (deftest rate-limiter-test - (testing "base case" + (testing "high rates" (testing "interruptible sleep" (let [rate 1000 rate-limiter (rl/rate-limiter {:rate rate}) - - threads 32 - pool (Executors/newFixedThreadPool threads) - - counter (atom 0) - time-secs 2] - (doseq [_ (range threads)] - (submit pool (fn [] - (while true - (rl/acquire! rate-limiter) - (swap! counter inc))))) - (Thread/sleep (Duration/ofSeconds time-secs)) - (ExecutorService/.shutdown pool) - (is (approx== (* rate time-secs) @counter)))) + time-sec 2 + count (run-rate-limited-counting rate-limiter time-sec)] + (is (approx== (* rate time-sec) count)))) (testing "uninterruptible sleep" (let [rate 1000 rate-limiter (rl/rate-limiter {:rate rate :sleep-fn rl/uninterruptible-sleep}) + time-sec 2 + count (run-rate-limited-counting rate-limiter time-sec)] + (is (approx== (* rate time-sec) count))))) - threads 32 - pool (Executors/newFixedThreadPool threads) - - counter (atom 0) - time-secs 2] - (doseq [_ (range threads)] - (submit pool (fn [] - (while true - (rl/acquire! rate-limiter) - (swap! counter inc))))) - (Thread/sleep (Duration/ofSeconds time-secs)) - (ExecutorService/.shutdown pool) - (is (approx== (* rate time-secs) @counter))))) - - (testing "rate less than 1.0" + (testing "low rates (less than 1.0)" (testing "interruptible sleep" (let [rate 0.5 rate-limiter (rl/rate-limiter {:rate rate}) - - threads 32 - pool (Executors/newFixedThreadPool threads) - - counter (atom 0) - time-secs 5] - (doseq [_ (range threads)] - (submit pool (fn [] - (while true - (rl/acquire! rate-limiter) - (swap! counter inc))))) - (Thread/sleep (Duration/ofSeconds time-secs)) - (ExecutorService/.shutdown pool) + time-sec 5 + count (run-rate-limited-counting rate-limiter time-sec)] ;; exact error tolerance, since we are dealing with much slower ticks - (is (approx== (* rate time-secs) @counter 1)))) + (is (approx== (* rate time-sec) count 1)))) (testing "uninterruptible sleep" (let [rate 0.5 rate-limiter (rl/rate-limiter {:rate rate :sleep-fn rl/uninterruptible-sleep}) - - threads 32 - pool (Executors/newFixedThreadPool threads) - - counter (atom 0) - time-secs 5] - (doseq [_ (range threads)] - (submit pool (fn [] - (while true - (rl/acquire! rate-limiter) - (swap! counter inc))))) - (Thread/sleep (Duration/ofSeconds time-secs)) - (ExecutorService/.shutdown pool) + time-sec 5 + count (run-rate-limited-counting rate-limiter time-sec)] ;; exact error tolerance, since we are dealing with much slower ticks - (is (approx== (* rate time-secs) @counter 1)))))) + (is (approx== (* rate time-sec) count 1)))))) From bc69c83e9e70b87d42e74780d76632e9d99ad2fd Mon Sep 17 00:00:00 2001 From: Mark Sto Date: Sun, 8 Jun 2025 18:02:11 +0400 Subject: [PATCH 14/23] Showcase that scheduled tasks do not get terminated and thus start affecting each other due to threads depletion (not good wrt isolation) --- test/diehard/rate_limiter_test.clj | 38 ++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/test/diehard/rate_limiter_test.clj b/test/diehard/rate_limiter_test.clj index 5b6912d..8e5b622 100644 --- a/test/diehard/rate_limiter_test.clj +++ b/test/diehard/rate_limiter_test.clj @@ -2,7 +2,7 @@ (:require [clojure.test :refer [deftest testing is]] [diehard.rate-limiter :as rl]) (:import [java.time Duration] - [java.util.concurrent ExecutorService Executors Future])) + [java.util.concurrent ExecutorService Executors Future TimeUnit])) (def default-error "Relative error in percent. 3% by default." @@ -31,7 +31,9 @@ (swap! counter inc))))) (Thread/sleep (Duration/ofSeconds run-time-sec)) (ExecutorService/.shutdown pool) - @counter)) + (let [terminated? (ExecutorService/.awaitTermination pool 1 TimeUnit/SECONDS)] + {:terminated? terminated? + :count @counter}))) (deftest rate-limiter-test (testing "high rates" @@ -39,31 +41,47 @@ (let [rate 1000 rate-limiter (rl/rate-limiter {:rate rate}) time-sec 2 - count (run-rate-limited-counting rate-limiter time-sec)] - (is (approx== (* rate time-sec) count)))) + {:keys [terminated? count]} (run-rate-limited-counting rate-limiter + time-sec)] + (is terminated? + "Terminates successfully, without timeout") + (is (approx== (* rate time-sec) count) + "Count must be close to an expected value"))) (testing "uninterruptible sleep" (let [rate 1000 rate-limiter (rl/rate-limiter {:rate rate :sleep-fn rl/uninterruptible-sleep}) time-sec 2 - count (run-rate-limited-counting rate-limiter time-sec)] - (is (approx== (* rate time-sec) count))))) + {:keys [terminated? count]} (run-rate-limited-counting rate-limiter + time-sec)] + (is terminated? + "Terminates successfully, without timeout") + (is (approx== (* rate time-sec) count) + "Count must be close to an expected value")))) (testing "low rates (less than 1.0)" (testing "interruptible sleep" (let [rate 0.5 rate-limiter (rl/rate-limiter {:rate rate}) time-sec 5 - count (run-rate-limited-counting rate-limiter time-sec)] + {:keys [terminated? count]} (run-rate-limited-counting rate-limiter + time-sec)] + (is terminated? + "Terminates successfully, without timeout") ;; exact error tolerance, since we are dealing with much slower ticks - (is (approx== (* rate time-sec) count 1)))) + (is (approx== (* rate time-sec) count 1) + "Count must be close to an expected value"))) (testing "uninterruptible sleep" (let [rate 0.5 rate-limiter (rl/rate-limiter {:rate rate :sleep-fn rl/uninterruptible-sleep}) time-sec 5 - count (run-rate-limited-counting rate-limiter time-sec)] + {:keys [terminated? count]} (run-rate-limited-counting rate-limiter + time-sec)] + (is terminated? + "Terminates successfully, without timeout") ;; exact error tolerance, since we are dealing with much slower ticks - (is (approx== (* rate time-sec) count 1)))))) + (is (approx== (* rate time-sec) count 1) + "Count must be close to an expected value"))))) From 324e29b4da335c193655c7888167a7e965e1d484 Mon Sep 17 00:00:00 2001 From: Mark Sto Date: Sun, 8 Jun 2025 20:09:46 +0400 Subject: [PATCH 15/23] Await for termination of all tasks (depending on rate and thread count) --- test/diehard/rate_limiter_test.clj | 86 +++++++++++++++++++----------- 1 file changed, 55 insertions(+), 31 deletions(-) diff --git a/test/diehard/rate_limiter_test.clj b/test/diehard/rate_limiter_test.clj index 8e5b622..f4b8162 100644 --- a/test/diehard/rate_limiter_test.clj +++ b/test/diehard/rate_limiter_test.clj @@ -2,7 +2,8 @@ (:require [clojure.test :refer [deftest testing is]] [diehard.rate-limiter :as rl]) (:import [java.time Duration] - [java.util.concurrent ExecutorService Executors Future TimeUnit])) + [java.util.concurrent ExecutorService Executors Future TimeUnit] + [java.util.concurrent.atomic AtomicBoolean])) (def default-error "Relative error in percent. 3% by default." @@ -14,74 +15,97 @@ ([expected actual tolerance] (< (abs (- expected actual)) tolerance))) +(defn- total-wait-time + [rate n-threads] + (long (* (/ 1.0 rate) n-threads 1000))) + (defn- submit ^Future [^ExecutorService executor ^Callable f] (ExecutorService/.submit executor f)) (defn- run-rate-limited-counting - [rate-limiter run-time-sec] - (let [threads 32 - pool (Executors/newFixedThreadPool threads) - + [rate-limiter n-threads run-time-sec] + (let [pool (Executors/newFixedThreadPool n-threads) + running? (AtomicBoolean. true) counter (atom 0)] - (doseq [_ (range threads)] + (doseq [_ (range n-threads)] (submit pool (fn [] - (while true + (loop [] (rl/acquire! rate-limiter) - (swap! counter inc))))) + (when (AtomicBoolean/.get running?) + (swap! counter inc) + (recur)))))) (Thread/sleep (Duration/ofSeconds run-time-sec)) + (AtomicBoolean/.set running? false) (ExecutorService/.shutdown pool) - (let [terminated? (ExecutorService/.awaitTermination pool 1 TimeUnit/SECONDS)] - {:terminated? terminated? - :count @counter}))) + {:await-fn (fn [timeout-ms] + (ExecutorService/.awaitTermination + pool timeout-ms TimeUnit/MILLISECONDS)) + :count @counter})) (deftest rate-limiter-test (testing "high rates" (testing "interruptible sleep" (let [rate 1000 rate-limiter (rl/rate-limiter {:rate rate}) - time-sec 2 - {:keys [terminated? count]} (run-rate-limited-counting rate-limiter - time-sec)] - (is terminated? + run-time-sec 2 + n-threads 32 + ;; tenfold be enough to cover up thread switching costs + term-timeout-ms (* 10 (total-wait-time rate n-threads)) + {:keys [await-fn count]} (run-rate-limited-counting rate-limiter + n-threads + run-time-sec)] + (is (await-fn term-timeout-ms) "Terminates successfully, without timeout") - (is (approx== (* rate time-sec) count) + (is (approx== (* rate run-time-sec) count) "Count must be close to an expected value"))) (testing "uninterruptible sleep" (let [rate 1000 rate-limiter (rl/rate-limiter {:rate rate :sleep-fn rl/uninterruptible-sleep}) - time-sec 2 - {:keys [terminated? count]} (run-rate-limited-counting rate-limiter - time-sec)] - (is terminated? + run-time-sec 2 + n-threads 32 + ;; tenfold be enough to cover up thread switching costs + term-timeout-ms (* 10 (total-wait-time rate n-threads)) + {:keys [await-fn count]} (run-rate-limited-counting rate-limiter + n-threads + run-time-sec)] + (is (await-fn term-timeout-ms) "Terminates successfully, without timeout") - (is (approx== (* rate time-sec) count) + (is (approx== (* rate run-time-sec) count) "Count must be close to an expected value")))) (testing "low rates (less than 1.0)" (testing "interruptible sleep" (let [rate 0.5 rate-limiter (rl/rate-limiter {:rate rate}) - time-sec 5 - {:keys [terminated? count]} (run-rate-limited-counting rate-limiter - time-sec)] - (is terminated? + run-time-sec 5 + n-threads 2 + ;; each thread will have to sleep for ≈2 seconds + term-timeout-ms (total-wait-time rate n-threads) + {:keys [await-fn count]} (run-rate-limited-counting rate-limiter + n-threads + run-time-sec)] + (is (await-fn term-timeout-ms) "Terminates successfully, without timeout") ;; exact error tolerance, since we are dealing with much slower ticks - (is (approx== (* rate time-sec) count 1) + (is (approx== (* rate run-time-sec) count 1) "Count must be close to an expected value"))) (testing "uninterruptible sleep" (let [rate 0.5 rate-limiter (rl/rate-limiter {:rate rate :sleep-fn rl/uninterruptible-sleep}) - time-sec 5 - {:keys [terminated? count]} (run-rate-limited-counting rate-limiter - time-sec)] - (is terminated? + run-time-sec 5 + n-threads 2 + ;; each thread will have to sleep for ≈2 seconds + term-timeout-ms (total-wait-time rate n-threads) + {:keys [await-fn count]} (run-rate-limited-counting rate-limiter + n-threads + run-time-sec)] + (is (await-fn term-timeout-ms) "Terminates successfully, without timeout") ;; exact error tolerance, since we are dealing with much slower ticks - (is (approx== (* rate time-sec) count 1) + (is (approx== (* rate run-time-sec) count 1) "Count must be close to an expected value"))))) From 2c180fff90a0b3538b53a4d88aaaaf5e0e2ccd68 Mon Sep 17 00:00:00 2001 From: Mark Sto Date: Sun, 8 Jun 2025 20:44:12 +0400 Subject: [PATCH 16/23] Impl two separate ways of stopping scheduled tasks in order to exemplify a principal difference between the sleep semantics --- src/diehard/rate_limiter.clj | 3 +- test/diehard/rate_limiter_test.clj | 229 +++++++++++++++++++---------- 2 files changed, 151 insertions(+), 81 deletions(-) diff --git a/src/diehard/rate_limiter.clj b/src/diehard/rate_limiter.clj index 16698c1..73aab77 100644 --- a/src/diehard/rate_limiter.clj +++ b/src/diehard/rate_limiter.clj @@ -98,7 +98,8 @@ ;; can only recur from tail position (when @interrupted? (recur))))) (finally - (when @interrupted? (.interrupt (Thread/currentThread))))))))) + (when @interrupted? + (Thread/.interrupt (Thread/currentThread))))))))) (defn rate-limiter "Create a default rate limiter with: diff --git a/test/diehard/rate_limiter_test.clj b/test/diehard/rate_limiter_test.clj index f4b8162..125cae7 100644 --- a/test/diehard/rate_limiter_test.clj +++ b/test/diehard/rate_limiter_test.clj @@ -24,88 +24,157 @@ (ExecutorService/.submit executor f)) (defn- run-rate-limited-counting - [rate-limiter n-threads run-time-sec] - (let [pool (Executors/newFixedThreadPool n-threads) - running? (AtomicBoolean. true) - counter (atom 0)] - (doseq [_ (range n-threads)] - (submit pool (fn [] - (loop [] - (rl/acquire! rate-limiter) - (when (AtomicBoolean/.get running?) - (swap! counter inc) - (recur)))))) - (Thread/sleep (Duration/ofSeconds run-time-sec)) - (AtomicBoolean/.set running? false) - (ExecutorService/.shutdown pool) - {:await-fn (fn [timeout-ms] - (ExecutorService/.awaitTermination - pool timeout-ms TimeUnit/MILLISECONDS)) - :count @counter})) + [& {:keys [proceed-run? do-shutdown!]}] + (fn [rate-limiter n-threads run-time-sec] + (let [pool (Executors/newFixedThreadPool n-threads) + counter (atom 0)] + (doseq [_ (range n-threads)] + (submit pool (fn [] + (loop [] + (rl/acquire! rate-limiter) + (when (proceed-run?) + (swap! counter inc) + (recur)))))) + (Thread/sleep (Duration/ofSeconds run-time-sec)) + (do-shutdown! pool) + {:await-fn (fn [timeout-ms] + (ExecutorService/.awaitTermination + pool timeout-ms TimeUnit/MILLISECONDS)) + :count @counter}))) -(deftest rate-limiter-test +(defn- run-rate-limited-counting:interruption [] + (run-rate-limited-counting + :proceed-run? (constantly true) + :do-shutdown! ExecutorService/.shutdownNow)) + +(defn- run-rate-limited-counting:custom-flag [] + (let [running? (AtomicBoolean. true)] + (run-rate-limited-counting + :proceed-run? #(AtomicBoolean/.get running?) + :do-shutdown! (fn [pool] + (AtomicBoolean/.set running? false) + (ExecutorService/.shutdown pool))))) + +(deftest rate-limiter-at-high-rates-test (testing "high rates" - (testing "interruptible sleep" - (let [rate 1000 - rate-limiter (rl/rate-limiter {:rate rate}) - run-time-sec 2 - n-threads 32 - ;; tenfold be enough to cover up thread switching costs - term-timeout-ms (* 10 (total-wait-time rate n-threads)) - {:keys [await-fn count]} (run-rate-limited-counting rate-limiter - n-threads - run-time-sec)] - (is (await-fn term-timeout-ms) - "Terminates successfully, without timeout") - (is (approx== (* rate run-time-sec) count) - "Count must be close to an expected value"))) + (testing "with interruptible sleep" + (testing "and interruption signal to stop tasks" + (let [rate 1000 + rate-limiter (rl/rate-limiter {:rate rate}) + run-time-sec 2 + n-threads 32 + ;; tenfold be enough to cover up thread switching costs + term-timeout-ms (* 10 (total-wait-time rate n-threads)) + {:keys [await-fn count]} ((run-rate-limited-counting:interruption) + rate-limiter n-threads run-time-sec)] + (is (await-fn term-timeout-ms) + "Terminates successfully, without timeout") + (is (approx== (* rate run-time-sec) count) + "Count must be close to an expected value"))) + (testing "and custom running flag to stop tasks" + (let [rate 1000 + rate-limiter (rl/rate-limiter {:rate rate}) + run-time-sec 2 + n-threads 32 + ;; tenfold be enough to cover up thread switching costs + term-timeout-ms (* 10 (total-wait-time rate n-threads)) + {:keys [await-fn count]} ((run-rate-limited-counting:custom-flag) + rate-limiter n-threads run-time-sec)] + (is (await-fn term-timeout-ms) + "Terminates successfully, without timeout") + (is (approx== (* rate run-time-sec) count) + "Count must be close to an expected value")))) - (testing "uninterruptible sleep" - (let [rate 1000 - rate-limiter (rl/rate-limiter {:rate rate - :sleep-fn rl/uninterruptible-sleep}) - run-time-sec 2 - n-threads 32 - ;; tenfold be enough to cover up thread switching costs - term-timeout-ms (* 10 (total-wait-time rate n-threads)) - {:keys [await-fn count]} (run-rate-limited-counting rate-limiter - n-threads - run-time-sec)] - (is (await-fn term-timeout-ms) - "Terminates successfully, without timeout") - (is (approx== (* rate run-time-sec) count) - "Count must be close to an expected value")))) + (testing "with uninterruptible sleep" + (testing "and interruption signal to stop tasks" + (let [rate 1000 + rate-limiter (rl/rate-limiter {:rate rate + :sleep-fn rl/uninterruptible-sleep}) + run-time-sec 2 + n-threads 32 + ;; tenfold be enough to cover up thread switching costs + term-timeout-ms (* 10 (total-wait-time rate n-threads)) + {:keys [await-fn count]} ((run-rate-limited-counting:interruption) + rate-limiter n-threads run-time-sec)] + (is (false? (await-fn term-timeout-ms)) + "Cannot terminate due to (some) tasks still running") + (is (approx== (* rate run-time-sec) count) + "Count must be close to an expected value"))) + (testing "and custom running flag to stop tasks" + (let [rate 1000 + rate-limiter (rl/rate-limiter {:rate rate + :sleep-fn rl/uninterruptible-sleep}) + run-time-sec 2 + n-threads 32 + ;; tenfold be enough to cover up thread switching costs + term-timeout-ms (* 10 (total-wait-time rate n-threads)) + {:keys [await-fn count]} ((run-rate-limited-counting:custom-flag) + rate-limiter n-threads run-time-sec)] + (is (await-fn term-timeout-ms) + "Terminates successfully, without timeout") + (is (approx== (* rate run-time-sec) count) + "Count must be close to an expected value")))))) - (testing "low rates (less than 1.0)" - (testing "interruptible sleep" - (let [rate 0.5 - rate-limiter (rl/rate-limiter {:rate rate}) - run-time-sec 5 - n-threads 2 - ;; each thread will have to sleep for ≈2 seconds - term-timeout-ms (total-wait-time rate n-threads) - {:keys [await-fn count]} (run-rate-limited-counting rate-limiter - n-threads - run-time-sec)] - (is (await-fn term-timeout-ms) - "Terminates successfully, without timeout") - ;; exact error tolerance, since we are dealing with much slower ticks - (is (approx== (* rate run-time-sec) count 1) - "Count must be close to an expected value"))) +(deftest rate-limiter-at-low-rates-test + (testing "low rates" ; less than 1.0 + (testing "with interruptible sleep" + (testing "and interruption signal to stop tasks" + (let [rate 0.5 + rate-limiter (rl/rate-limiter {:rate rate}) + run-time-sec 5 + n-threads 2 + ;; each thread will have to sleep for ≈2 seconds + term-timeout-ms (total-wait-time rate n-threads) + {:keys [await-fn count]} ((run-rate-limited-counting:interruption) + rate-limiter n-threads run-time-sec)] + (is (await-fn term-timeout-ms) + "Terminates successfully, without timeout") + ;; exact error tolerance, since we are dealing with much slower ticks + (is (approx== (* rate run-time-sec) count 1) + "Count must be close to an expected value"))) + (testing "and custom running flag to stop tasks" + (let [rate 0.5 + rate-limiter (rl/rate-limiter {:rate rate}) + run-time-sec 5 + n-threads 2 + ;; each thread will have to sleep for ≈2 seconds + term-timeout-ms (total-wait-time rate n-threads) + {:keys [await-fn count]} ((run-rate-limited-counting:custom-flag) + rate-limiter n-threads run-time-sec)] + (is (await-fn term-timeout-ms) + "Terminates successfully, without timeout") + ;; exact error tolerance, since we are dealing with much slower ticks + (is (approx== (* rate run-time-sec) count 1) + "Count must be close to an expected value")))) - (testing "uninterruptible sleep" - (let [rate 0.5 - rate-limiter (rl/rate-limiter {:rate rate - :sleep-fn rl/uninterruptible-sleep}) - run-time-sec 5 - n-threads 2 - ;; each thread will have to sleep for ≈2 seconds - term-timeout-ms (total-wait-time rate n-threads) - {:keys [await-fn count]} (run-rate-limited-counting rate-limiter - n-threads - run-time-sec)] - (is (await-fn term-timeout-ms) - "Terminates successfully, without timeout") - ;; exact error tolerance, since we are dealing with much slower ticks - (is (approx== (* rate run-time-sec) count 1) - "Count must be close to an expected value"))))) + (testing "with uninterruptible sleep" + (testing "and interruption signal to stop tasks" + (let [rate 0.5 + rate-limiter (rl/rate-limiter {:rate rate + :sleep-fn rl/uninterruptible-sleep}) + run-time-sec 5 + n-threads 2 + ;; each thread will have to sleep for ≈2 seconds + term-timeout-ms (total-wait-time rate n-threads) + {:keys [await-fn count]} ((run-rate-limited-counting:interruption) + rate-limiter n-threads run-time-sec)] + (is (false? (await-fn term-timeout-ms)) + "Cannot terminate due to (some) tasks still running") + ;; exact error tolerance, since we are dealing with much slower ticks + (is (approx== (* rate run-time-sec) count 1) + "Count must be close to an expected value"))) + (testing "and custom running flag to stop tasks" + (let [rate 0.5 + rate-limiter (rl/rate-limiter {:rate rate + :sleep-fn rl/uninterruptible-sleep}) + run-time-sec 5 + n-threads 2 + ;; each thread will have to sleep for ≈2 seconds + term-timeout-ms (total-wait-time rate n-threads) + {:keys [await-fn count]} ((run-rate-limited-counting:custom-flag) + rate-limiter n-threads run-time-sec)] + (is (await-fn term-timeout-ms) + "Terminates successfully, without timeout") + ;; exact error tolerance, since we are dealing with much slower ticks + (is (approx== (* rate run-time-sec) count 1) + "Count must be close to an expected value")))))) From 626a38c310cb920f51f41a5097abe2d9af88c94b Mon Sep 17 00:00:00 2001 From: Mark Sto Date: Sun, 8 Jun 2025 21:33:13 +0400 Subject: [PATCH 17/23] Await for termination of all tasks (once again for those that need it) --- test/diehard/rate_limiter_test.clj | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/test/diehard/rate_limiter_test.clj b/test/diehard/rate_limiter_test.clj index 125cae7..1c397ae 100644 --- a/test/diehard/rate_limiter_test.clj +++ b/test/diehard/rate_limiter_test.clj @@ -27,11 +27,14 @@ [& {:keys [proceed-run? do-shutdown!]}] (fn [rate-limiter n-threads run-time-sec] (let [pool (Executors/newFixedThreadPool n-threads) + throw? (AtomicBoolean. false) counter (atom 0)] (doseq [_ (range n-threads)] (submit pool (fn [] (loop [] (rl/acquire! rate-limiter) + (when (AtomicBoolean/.get throw?) + (throw (Exception. "Hard stop"))) (when (proceed-run?) (swap! counter inc) (recur)))))) @@ -40,6 +43,7 @@ {:await-fn (fn [timeout-ms] (ExecutorService/.awaitTermination pool timeout-ms TimeUnit/MILLISECONDS)) + :throw-fn #(AtomicBoolean/.set throw? true) :count @counter}))) (defn- run-rate-limited-counting:interruption [] @@ -94,10 +98,14 @@ n-threads 32 ;; tenfold be enough to cover up thread switching costs term-timeout-ms (* 10 (total-wait-time rate n-threads)) - {:keys [await-fn count]} ((run-rate-limited-counting:interruption) - rate-limiter n-threads run-time-sec)] + {:keys [await-fn throw-fn count]} ((run-rate-limited-counting:interruption) + rate-limiter n-threads run-time-sec)] (is (false? (await-fn term-timeout-ms)) "Cannot terminate due to (some) tasks still running") + ;; the only way to actually stop all running tasks + (throw-fn) + (is (await-fn term-timeout-ms) + "Terminates successfully, without timeout THIS TIME") (is (approx== (* rate run-time-sec) count) "Count must be close to an expected value"))) (testing "and custom running flag to stop tasks" @@ -156,10 +164,14 @@ n-threads 2 ;; each thread will have to sleep for ≈2 seconds term-timeout-ms (total-wait-time rate n-threads) - {:keys [await-fn count]} ((run-rate-limited-counting:interruption) - rate-limiter n-threads run-time-sec)] + {:keys [await-fn throw-fn count]} ((run-rate-limited-counting:interruption) + rate-limiter n-threads run-time-sec)] (is (false? (await-fn term-timeout-ms)) "Cannot terminate due to (some) tasks still running") + ;; the only way to actually stop all running tasks + (throw-fn) + (is (await-fn term-timeout-ms) + "Terminates successfully, without timeout THIS TIME") ;; exact error tolerance, since we are dealing with much slower ticks (is (approx== (* rate run-time-sec) count 1) "Count must be close to an expected value"))) From 7da1242630eae74aab0cdf149707102f6e6ca43d Mon Sep 17 00:00:00 2001 From: Mark Sto Date: Sun, 8 Jun 2025 21:33:52 +0400 Subject: [PATCH 18/23] Pick up a better name for the `total-wait-time` fn --- test/diehard/rate_limiter_test.clj | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/test/diehard/rate_limiter_test.clj b/test/diehard/rate_limiter_test.clj index 1c397ae..2b26696 100644 --- a/test/diehard/rate_limiter_test.clj +++ b/test/diehard/rate_limiter_test.clj @@ -15,7 +15,7 @@ ([expected actual tolerance] (< (abs (- expected actual)) tolerance))) -(defn- total-wait-time +(defn- total-block-time [rate n-threads] (long (* (/ 1.0 rate) n-threads 1000))) @@ -68,7 +68,7 @@ run-time-sec 2 n-threads 32 ;; tenfold be enough to cover up thread switching costs - term-timeout-ms (* 10 (total-wait-time rate n-threads)) + term-timeout-ms (* 10 (total-block-time rate n-threads)) {:keys [await-fn count]} ((run-rate-limited-counting:interruption) rate-limiter n-threads run-time-sec)] (is (await-fn term-timeout-ms) @@ -81,7 +81,7 @@ run-time-sec 2 n-threads 32 ;; tenfold be enough to cover up thread switching costs - term-timeout-ms (* 10 (total-wait-time rate n-threads)) + term-timeout-ms (* 10 (total-block-time rate n-threads)) {:keys [await-fn count]} ((run-rate-limited-counting:custom-flag) rate-limiter n-threads run-time-sec)] (is (await-fn term-timeout-ms) @@ -97,7 +97,7 @@ run-time-sec 2 n-threads 32 ;; tenfold be enough to cover up thread switching costs - term-timeout-ms (* 10 (total-wait-time rate n-threads)) + term-timeout-ms (* 10 (total-block-time rate n-threads)) {:keys [await-fn throw-fn count]} ((run-rate-limited-counting:interruption) rate-limiter n-threads run-time-sec)] (is (false? (await-fn term-timeout-ms)) @@ -115,7 +115,7 @@ run-time-sec 2 n-threads 32 ;; tenfold be enough to cover up thread switching costs - term-timeout-ms (* 10 (total-wait-time rate n-threads)) + term-timeout-ms (* 10 (total-block-time rate n-threads)) {:keys [await-fn count]} ((run-rate-limited-counting:custom-flag) rate-limiter n-threads run-time-sec)] (is (await-fn term-timeout-ms) @@ -132,7 +132,7 @@ run-time-sec 5 n-threads 2 ;; each thread will have to sleep for ≈2 seconds - term-timeout-ms (total-wait-time rate n-threads) + term-timeout-ms (total-block-time rate n-threads) {:keys [await-fn count]} ((run-rate-limited-counting:interruption) rate-limiter n-threads run-time-sec)] (is (await-fn term-timeout-ms) @@ -146,7 +146,7 @@ run-time-sec 5 n-threads 2 ;; each thread will have to sleep for ≈2 seconds - term-timeout-ms (total-wait-time rate n-threads) + term-timeout-ms (total-block-time rate n-threads) {:keys [await-fn count]} ((run-rate-limited-counting:custom-flag) rate-limiter n-threads run-time-sec)] (is (await-fn term-timeout-ms) @@ -163,7 +163,7 @@ run-time-sec 5 n-threads 2 ;; each thread will have to sleep for ≈2 seconds - term-timeout-ms (total-wait-time rate n-threads) + term-timeout-ms (total-block-time rate n-threads) {:keys [await-fn throw-fn count]} ((run-rate-limited-counting:interruption) rate-limiter n-threads run-time-sec)] (is (false? (await-fn term-timeout-ms)) @@ -182,7 +182,7 @@ run-time-sec 5 n-threads 2 ;; each thread will have to sleep for ≈2 seconds - term-timeout-ms (total-wait-time rate n-threads) + term-timeout-ms (total-block-time rate n-threads) {:keys [await-fn count]} ((run-rate-limited-counting:custom-flag) rate-limiter n-threads run-time-sec)] (is (await-fn term-timeout-ms) From fac8c4f7b282d37c57e82cde99787f8074ef1ef5 Mon Sep 17 00:00:00 2001 From: Mark Sto Date: Sun, 8 Jun 2025 22:23:31 +0400 Subject: [PATCH 19/23] Add a test case for when a task logic throws an exception --- test/diehard/rate_limiter_test.clj | 100 ++++++++++++++++++++++++----- 1 file changed, 85 insertions(+), 15 deletions(-) diff --git a/test/diehard/rate_limiter_test.clj b/test/diehard/rate_limiter_test.clj index 2b26696..a51a964 100644 --- a/test/diehard/rate_limiter_test.clj +++ b/test/diehard/rate_limiter_test.clj @@ -27,23 +27,27 @@ [& {:keys [proceed-run? do-shutdown!]}] (fn [rate-limiter n-threads run-time-sec] (let [pool (Executors/newFixedThreadPool n-threads) - throw? (AtomicBoolean. false) + stop? (AtomicBoolean. false) counter (atom 0)] (doseq [_ (range n-threads)] (submit pool (fn [] - (loop [] - (rl/acquire! rate-limiter) - (when (AtomicBoolean/.get throw?) - (throw (Exception. "Hard stop"))) - (when (proceed-run?) - (swap! counter inc) - (recur)))))) + (try + (loop [] + (rl/acquire! rate-limiter) + (when (AtomicBoolean/.get stop?) + (throw (ex-info "Stopped" {:type ::stop}))) + (when (proceed-run?) + (swap! counter inc) + (recur))) + (catch Exception ex + (when-not (= ::stop (:type (ex-data ex))) + (throw ex))))))) (Thread/sleep (Duration/ofSeconds run-time-sec)) (do-shutdown! pool) {:await-fn (fn [timeout-ms] (ExecutorService/.awaitTermination pool timeout-ms TimeUnit/MILLISECONDS)) - :throw-fn #(AtomicBoolean/.set throw? true) + :stop-fn #(AtomicBoolean/.set stop? true) :count @counter}))) (defn- run-rate-limited-counting:interruption [] @@ -59,6 +63,16 @@ (AtomicBoolean/.set running? false) (ExecutorService/.shutdown pool))))) +(defn- run-rate-limited-counting:exception [] + (let [throw? (AtomicBoolean. false)] + (run-rate-limited-counting + :proceed-run? #(if (AtomicBoolean/.get throw?) + (throw (Exception. "Task failed!")) + true) + :do-shutdown! (fn [pool] + (AtomicBoolean/.set throw? true) + (ExecutorService/.shutdown pool))))) + (deftest rate-limiter-at-high-rates-test (testing "high rates" (testing "with interruptible sleep" @@ -86,6 +100,19 @@ rate-limiter n-threads run-time-sec)] (is (await-fn term-timeout-ms) "Terminates successfully, without timeout") + (is (approx== (* rate run-time-sec) count) + "Count must be close to an expected value"))) + (testing "when a task logic throws an exception" + (let [rate 1000 + rate-limiter (rl/rate-limiter {:rate rate}) + run-time-sec 2 + n-threads 32 + ;; tenfold be enough to cover up thread switching costs + term-timeout-ms (* 10 (total-block-time rate n-threads)) + {:keys [await-fn count]} ((run-rate-limited-counting:exception) + rate-limiter n-threads run-time-sec)] + (is (await-fn term-timeout-ms) + "Terminates successfully, without timeout") (is (approx== (* rate run-time-sec) count) "Count must be close to an expected value")))) @@ -98,12 +125,12 @@ n-threads 32 ;; tenfold be enough to cover up thread switching costs term-timeout-ms (* 10 (total-block-time rate n-threads)) - {:keys [await-fn throw-fn count]} ((run-rate-limited-counting:interruption) - rate-limiter n-threads run-time-sec)] + {:keys [await-fn stop-fn count]} ((run-rate-limited-counting:interruption) + rate-limiter n-threads run-time-sec)] (is (false? (await-fn term-timeout-ms)) "Cannot terminate due to (some) tasks still running") ;; the only way to actually stop all running tasks - (throw-fn) + (stop-fn) (is (await-fn term-timeout-ms) "Terminates successfully, without timeout THIS TIME") (is (approx== (* rate run-time-sec) count) @@ -120,6 +147,20 @@ rate-limiter n-threads run-time-sec)] (is (await-fn term-timeout-ms) "Terminates successfully, without timeout") + (is (approx== (* rate run-time-sec) count) + "Count must be close to an expected value"))) + (testing "when a task logic throws an exception" + (let [rate 1000 + rate-limiter (rl/rate-limiter {:rate rate + :sleep-fn rl/uninterruptible-sleep}) + run-time-sec 2 + n-threads 32 + ;; tenfold be enough to cover up thread switching costs + term-timeout-ms (* 10 (total-block-time rate n-threads)) + {:keys [await-fn count]} ((run-rate-limited-counting:exception) + rate-limiter n-threads run-time-sec)] + (is (await-fn term-timeout-ms) + "Terminates successfully, without timeout") (is (approx== (* rate run-time-sec) count) "Count must be close to an expected value")))))) @@ -152,6 +193,20 @@ (is (await-fn term-timeout-ms) "Terminates successfully, without timeout") ;; exact error tolerance, since we are dealing with much slower ticks + (is (approx== (* rate run-time-sec) count 1) + "Count must be close to an expected value"))) + (testing "when a task logic throws an exception" + (let [rate 0.5 + rate-limiter (rl/rate-limiter {:rate rate}) + run-time-sec 5 + n-threads 2 + ;; each thread will have to sleep for ≈2 seconds + term-timeout-ms (total-block-time rate n-threads) + {:keys [await-fn count]} ((run-rate-limited-counting:exception) + rate-limiter n-threads run-time-sec)] + (is (await-fn term-timeout-ms) + "Terminates successfully, without timeout") + ;; exact error tolerance, since we are dealing with much slower ticks (is (approx== (* rate run-time-sec) count 1) "Count must be close to an expected value")))) @@ -164,12 +219,12 @@ n-threads 2 ;; each thread will have to sleep for ≈2 seconds term-timeout-ms (total-block-time rate n-threads) - {:keys [await-fn throw-fn count]} ((run-rate-limited-counting:interruption) - rate-limiter n-threads run-time-sec)] + {:keys [await-fn stop-fn count]} ((run-rate-limited-counting:interruption) + rate-limiter n-threads run-time-sec)] (is (false? (await-fn term-timeout-ms)) "Cannot terminate due to (some) tasks still running") ;; the only way to actually stop all running tasks - (throw-fn) + (stop-fn) (is (await-fn term-timeout-ms) "Terminates successfully, without timeout THIS TIME") ;; exact error tolerance, since we are dealing with much slower ticks @@ -188,5 +243,20 @@ (is (await-fn term-timeout-ms) "Terminates successfully, without timeout") ;; exact error tolerance, since we are dealing with much slower ticks + (is (approx== (* rate run-time-sec) count 1) + "Count must be close to an expected value"))) + (testing "when a task logic throws an exception" + (let [rate 0.5 + rate-limiter (rl/rate-limiter {:rate rate + :sleep-fn rl/uninterruptible-sleep}) + run-time-sec 5 + n-threads 2 + ;; each thread will have to sleep for ≈2 seconds + term-timeout-ms (total-block-time rate n-threads) + {:keys [await-fn count]} ((run-rate-limited-counting:exception) + rate-limiter n-threads run-time-sec)] + (is (await-fn term-timeout-ms) + "Terminates successfully, without timeout") + ;; exact error tolerance, since we are dealing with much slower ticks (is (approx== (* rate run-time-sec) count 1) "Count must be close to an expected value")))))) From aeb62de6fc628680a013e65a9ea000beba488add Mon Sep 17 00:00:00 2001 From: Mark Sto Date: Sun, 8 Jun 2025 23:03:57 +0400 Subject: [PATCH 20/23] Explicitly show that all counting works as expected with a single special case of uninterruptible sleep and interruption signal that won't stop any tasks from running (add a comment there, that's as well expected, yet we don't want to overcomplicate everything because of this specific case) --- test/diehard/rate_limiter_test.clj | 94 ++++++++++++++++-------------- 1 file changed, 50 insertions(+), 44 deletions(-) diff --git a/test/diehard/rate_limiter_test.clj b/test/diehard/rate_limiter_test.clj index a51a964..7b4ad05 100644 --- a/test/diehard/rate_limiter_test.clj +++ b/test/diehard/rate_limiter_test.clj @@ -48,7 +48,7 @@ (ExecutorService/.awaitTermination pool timeout-ms TimeUnit/MILLISECONDS)) :stop-fn #(AtomicBoolean/.set stop? true) - :count @counter}))) + :counter counter}))) (defn- run-rate-limited-counting:interruption [] (run-rate-limited-counting @@ -83,11 +83,11 @@ n-threads 32 ;; tenfold be enough to cover up thread switching costs term-timeout-ms (* 10 (total-block-time rate n-threads)) - {:keys [await-fn count]} ((run-rate-limited-counting:interruption) - rate-limiter n-threads run-time-sec)] + {:keys [await-fn counter]} ((run-rate-limited-counting:interruption) + rate-limiter n-threads run-time-sec)] (is (await-fn term-timeout-ms) "Terminates successfully, without timeout") - (is (approx== (* rate run-time-sec) count) + (is (approx== (* rate run-time-sec) @counter) "Count must be close to an expected value"))) (testing "and custom running flag to stop tasks" (let [rate 1000 @@ -96,11 +96,11 @@ n-threads 32 ;; tenfold be enough to cover up thread switching costs term-timeout-ms (* 10 (total-block-time rate n-threads)) - {:keys [await-fn count]} ((run-rate-limited-counting:custom-flag) - rate-limiter n-threads run-time-sec)] + {:keys [await-fn counter]} ((run-rate-limited-counting:custom-flag) + rate-limiter n-threads run-time-sec)] (is (await-fn term-timeout-ms) "Terminates successfully, without timeout") - (is (approx== (* rate run-time-sec) count) + (is (approx== (* rate run-time-sec) @counter) "Count must be close to an expected value"))) (testing "when a task logic throws an exception" (let [rate 1000 @@ -109,11 +109,11 @@ n-threads 32 ;; tenfold be enough to cover up thread switching costs term-timeout-ms (* 10 (total-block-time rate n-threads)) - {:keys [await-fn count]} ((run-rate-limited-counting:exception) - rate-limiter n-threads run-time-sec)] + {:keys [await-fn counter]} ((run-rate-limited-counting:exception) + rate-limiter n-threads run-time-sec)] (is (await-fn term-timeout-ms) "Terminates successfully, without timeout") - (is (approx== (* rate run-time-sec) count) + (is (approx== (* rate run-time-sec) @counter) "Count must be close to an expected value")))) (testing "with uninterruptible sleep" @@ -125,16 +125,19 @@ n-threads 32 ;; tenfold be enough to cover up thread switching costs term-timeout-ms (* 10 (total-block-time rate n-threads)) - {:keys [await-fn stop-fn count]} ((run-rate-limited-counting:interruption) - rate-limiter n-threads run-time-sec)] + {:keys [await-fn stop-fn counter]} ((run-rate-limited-counting:interruption) + rate-limiter n-threads run-time-sec)] + ;; here we are forced to check the count before it's too late, + ;; since the task pool termination won't happen straight away + (is (approx== (* rate run-time-sec) @counter) + "Count must be close to an expected value") + (is (false? (await-fn term-timeout-ms)) "Cannot terminate due to (some) tasks still running") - ;; the only way to actually stop all running tasks + ;; the only way to actually stop all running tasks (isolation) (stop-fn) (is (await-fn term-timeout-ms) - "Terminates successfully, without timeout THIS TIME") - (is (approx== (* rate run-time-sec) count) - "Count must be close to an expected value"))) + "Terminates successfully, without timeout THIS TIME"))) (testing "and custom running flag to stop tasks" (let [rate 1000 rate-limiter (rl/rate-limiter {:rate rate @@ -143,11 +146,11 @@ n-threads 32 ;; tenfold be enough to cover up thread switching costs term-timeout-ms (* 10 (total-block-time rate n-threads)) - {:keys [await-fn count]} ((run-rate-limited-counting:custom-flag) - rate-limiter n-threads run-time-sec)] + {:keys [await-fn counter]} ((run-rate-limited-counting:custom-flag) + rate-limiter n-threads run-time-sec)] (is (await-fn term-timeout-ms) "Terminates successfully, without timeout") - (is (approx== (* rate run-time-sec) count) + (is (approx== (* rate run-time-sec) @counter) "Count must be close to an expected value"))) (testing "when a task logic throws an exception" (let [rate 1000 @@ -157,11 +160,11 @@ n-threads 32 ;; tenfold be enough to cover up thread switching costs term-timeout-ms (* 10 (total-block-time rate n-threads)) - {:keys [await-fn count]} ((run-rate-limited-counting:exception) - rate-limiter n-threads run-time-sec)] + {:keys [await-fn counter]} ((run-rate-limited-counting:exception) + rate-limiter n-threads run-time-sec)] (is (await-fn term-timeout-ms) "Terminates successfully, without timeout") - (is (approx== (* rate run-time-sec) count) + (is (approx== (* rate run-time-sec) @counter) "Count must be close to an expected value")))))) (deftest rate-limiter-at-low-rates-test @@ -174,12 +177,12 @@ n-threads 2 ;; each thread will have to sleep for ≈2 seconds term-timeout-ms (total-block-time rate n-threads) - {:keys [await-fn count]} ((run-rate-limited-counting:interruption) - rate-limiter n-threads run-time-sec)] + {:keys [await-fn counter]} ((run-rate-limited-counting:interruption) + rate-limiter n-threads run-time-sec)] (is (await-fn term-timeout-ms) "Terminates successfully, without timeout") ;; exact error tolerance, since we are dealing with much slower ticks - (is (approx== (* rate run-time-sec) count 1) + (is (approx== (* rate run-time-sec) @counter 1) "Count must be close to an expected value"))) (testing "and custom running flag to stop tasks" (let [rate 0.5 @@ -188,12 +191,12 @@ n-threads 2 ;; each thread will have to sleep for ≈2 seconds term-timeout-ms (total-block-time rate n-threads) - {:keys [await-fn count]} ((run-rate-limited-counting:custom-flag) - rate-limiter n-threads run-time-sec)] + {:keys [await-fn counter]} ((run-rate-limited-counting:custom-flag) + rate-limiter n-threads run-time-sec)] (is (await-fn term-timeout-ms) "Terminates successfully, without timeout") ;; exact error tolerance, since we are dealing with much slower ticks - (is (approx== (* rate run-time-sec) count 1) + (is (approx== (* rate run-time-sec) @counter 1) "Count must be close to an expected value"))) (testing "when a task logic throws an exception" (let [rate 0.5 @@ -202,12 +205,12 @@ n-threads 2 ;; each thread will have to sleep for ≈2 seconds term-timeout-ms (total-block-time rate n-threads) - {:keys [await-fn count]} ((run-rate-limited-counting:exception) - rate-limiter n-threads run-time-sec)] + {:keys [await-fn counter]} ((run-rate-limited-counting:exception) + rate-limiter n-threads run-time-sec)] (is (await-fn term-timeout-ms) "Terminates successfully, without timeout") ;; exact error tolerance, since we are dealing with much slower ticks - (is (approx== (* rate run-time-sec) count 1) + (is (approx== (* rate run-time-sec) @counter 1) "Count must be close to an expected value")))) (testing "with uninterruptible sleep" @@ -219,17 +222,20 @@ n-threads 2 ;; each thread will have to sleep for ≈2 seconds term-timeout-ms (total-block-time rate n-threads) - {:keys [await-fn stop-fn count]} ((run-rate-limited-counting:interruption) - rate-limiter n-threads run-time-sec)] + {:keys [await-fn stop-fn counter]} ((run-rate-limited-counting:interruption) + rate-limiter n-threads run-time-sec)] + ;; here we are forced to check the count before it's too late, + ;; since the task pool termination won't happen straight away + ;; exact error tolerance, since we are dealing with much slower ticks + (is (approx== (* rate run-time-sec) @counter 1) + "Count must be close to an expected value") + (is (false? (await-fn term-timeout-ms)) "Cannot terminate due to (some) tasks still running") - ;; the only way to actually stop all running tasks + ;; the only way to actually stop all running tasks (isolation) (stop-fn) (is (await-fn term-timeout-ms) - "Terminates successfully, without timeout THIS TIME") - ;; exact error tolerance, since we are dealing with much slower ticks - (is (approx== (* rate run-time-sec) count 1) - "Count must be close to an expected value"))) + "Terminates successfully, without timeout THIS TIME"))) (testing "and custom running flag to stop tasks" (let [rate 0.5 rate-limiter (rl/rate-limiter {:rate rate @@ -238,12 +244,12 @@ n-threads 2 ;; each thread will have to sleep for ≈2 seconds term-timeout-ms (total-block-time rate n-threads) - {:keys [await-fn count]} ((run-rate-limited-counting:custom-flag) - rate-limiter n-threads run-time-sec)] + {:keys [await-fn counter]} ((run-rate-limited-counting:custom-flag) + rate-limiter n-threads run-time-sec)] (is (await-fn term-timeout-ms) "Terminates successfully, without timeout") ;; exact error tolerance, since we are dealing with much slower ticks - (is (approx== (* rate run-time-sec) count 1) + (is (approx== (* rate run-time-sec) @counter 1) "Count must be close to an expected value"))) (testing "when a task logic throws an exception" (let [rate 0.5 @@ -253,10 +259,10 @@ n-threads 2 ;; each thread will have to sleep for ≈2 seconds term-timeout-ms (total-block-time rate n-threads) - {:keys [await-fn count]} ((run-rate-limited-counting:exception) - rate-limiter n-threads run-time-sec)] + {:keys [await-fn counter]} ((run-rate-limited-counting:exception) + rate-limiter n-threads run-time-sec)] (is (await-fn term-timeout-ms) "Terminates successfully, without timeout") ;; exact error tolerance, since we are dealing with much slower ticks - (is (approx== (* rate run-time-sec) count 1) + (is (approx== (* rate run-time-sec) @counter 1) "Count must be close to an expected value")))))) From b33bc6ed02ebb9274b4d9953df2e46382d0294ce Mon Sep 17 00:00:00 2001 From: Mark Sto Date: Sun, 8 Jun 2025 23:20:29 +0400 Subject: [PATCH 21/23] Reduce all visual clutter and unneeded repetition --- test/diehard/rate_limiter_test.clj | 317 ++++++++++++----------------- 1 file changed, 133 insertions(+), 184 deletions(-) diff --git a/test/diehard/rate_limiter_test.clj b/test/diehard/rate_limiter_test.clj index 7b4ad05..4a6cf35 100644 --- a/test/diehard/rate_limiter_test.clj +++ b/test/diehard/rate_limiter_test.clj @@ -25,9 +25,10 @@ (defn- run-rate-limited-counting [& {:keys [proceed-run? do-shutdown!]}] - (fn [rate-limiter n-threads run-time-sec] + (fn [rl-opts n-threads run-time-sec] (let [pool (Executors/newFixedThreadPool n-threads) stop? (AtomicBoolean. false) + rate-limiter (rl/rate-limiter rl-opts) counter (atom 0)] (doseq [_ (range n-threads)] (submit pool (fn [] @@ -75,194 +76,142 @@ (deftest rate-limiter-at-high-rates-test (testing "high rates" - (testing "with interruptible sleep" - (testing "and interruption signal to stop tasks" - (let [rate 1000 - rate-limiter (rl/rate-limiter {:rate rate}) - run-time-sec 2 - n-threads 32 - ;; tenfold be enough to cover up thread switching costs - term-timeout-ms (* 10 (total-block-time rate n-threads)) - {:keys [await-fn counter]} ((run-rate-limited-counting:interruption) - rate-limiter n-threads run-time-sec)] - (is (await-fn term-timeout-ms) - "Terminates successfully, without timeout") - (is (approx== (* rate run-time-sec) @counter) - "Count must be close to an expected value"))) - (testing "and custom running flag to stop tasks" - (let [rate 1000 - rate-limiter (rl/rate-limiter {:rate rate}) - run-time-sec 2 - n-threads 32 - ;; tenfold be enough to cover up thread switching costs - term-timeout-ms (* 10 (total-block-time rate n-threads)) - {:keys [await-fn counter]} ((run-rate-limited-counting:custom-flag) - rate-limiter n-threads run-time-sec)] - (is (await-fn term-timeout-ms) - "Terminates successfully, without timeout") - (is (approx== (* rate run-time-sec) @counter) - "Count must be close to an expected value"))) - (testing "when a task logic throws an exception" - (let [rate 1000 - rate-limiter (rl/rate-limiter {:rate rate}) - run-time-sec 2 - n-threads 32 - ;; tenfold be enough to cover up thread switching costs - term-timeout-ms (* 10 (total-block-time rate n-threads)) - {:keys [await-fn counter]} ((run-rate-limited-counting:exception) - rate-limiter n-threads run-time-sec)] - (is (await-fn term-timeout-ms) - "Terminates successfully, without timeout") - (is (approx== (* rate run-time-sec) @counter) - "Count must be close to an expected value")))) + (let [rate 1000 + run-time-sec 2 - (testing "with uninterruptible sleep" - (testing "and interruption signal to stop tasks" - (let [rate 1000 - rate-limiter (rl/rate-limiter {:rate rate - :sleep-fn rl/uninterruptible-sleep}) - run-time-sec 2 - n-threads 32 - ;; tenfold be enough to cover up thread switching costs - term-timeout-ms (* 10 (total-block-time rate n-threads)) - {:keys [await-fn stop-fn counter]} ((run-rate-limited-counting:interruption) - rate-limiter n-threads run-time-sec)] - ;; here we are forced to check the count before it's too late, - ;; since the task pool termination won't happen straight away - (is (approx== (* rate run-time-sec) @counter) - "Count must be close to an expected value") + exp-count (* rate run-time-sec) - (is (false? (await-fn term-timeout-ms)) - "Cannot terminate due to (some) tasks still running") - ;; the only way to actually stop all running tasks (isolation) - (stop-fn) - (is (await-fn term-timeout-ms) - "Terminates successfully, without timeout THIS TIME"))) - (testing "and custom running flag to stop tasks" - (let [rate 1000 - rate-limiter (rl/rate-limiter {:rate rate - :sleep-fn rl/uninterruptible-sleep}) - run-time-sec 2 - n-threads 32 - ;; tenfold be enough to cover up thread switching costs - term-timeout-ms (* 10 (total-block-time rate n-threads)) - {:keys [await-fn counter]} ((run-rate-limited-counting:custom-flag) - rate-limiter n-threads run-time-sec)] - (is (await-fn term-timeout-ms) - "Terminates successfully, without timeout") - (is (approx== (* rate run-time-sec) @counter) - "Count must be close to an expected value"))) - (testing "when a task logic throws an exception" - (let [rate 1000 - rate-limiter (rl/rate-limiter {:rate rate - :sleep-fn rl/uninterruptible-sleep}) - run-time-sec 2 - n-threads 32 - ;; tenfold be enough to cover up thread switching costs - term-timeout-ms (* 10 (total-block-time rate n-threads)) - {:keys [await-fn counter]} ((run-rate-limited-counting:exception) - rate-limiter n-threads run-time-sec)] - (is (await-fn term-timeout-ms) - "Terminates successfully, without timeout") - (is (approx== (* rate run-time-sec) @counter) - "Count must be close to an expected value")))))) + n-threads 32 + ;; tenfold be enough to cover up thread switching costs + term-timeout-ms (* 10 (total-block-time rate n-threads))] + + (testing "with interruptible sleep" + (testing "and interruption signal to stop tasks" + (let [{:keys [await-fn counter]} ((run-rate-limited-counting:interruption) + {:rate rate} n-threads run-time-sec)] + (is (await-fn term-timeout-ms) + "Terminates successfully, without timeout") + (is (approx== exp-count @counter) + "Count must be close to an expected value"))) + (testing "and custom running flag to stop tasks" + (let [{:keys [await-fn counter]} ((run-rate-limited-counting:custom-flag) + {:rate rate} n-threads run-time-sec)] + (is (await-fn term-timeout-ms) + "Terminates successfully, without timeout") + (is (approx== exp-count @counter) + "Count must be close to an expected value"))) + (testing "when a task logic throws an exception" + (let [{:keys [await-fn counter]} ((run-rate-limited-counting:exception) + {:rate rate} n-threads run-time-sec)] + (is (await-fn term-timeout-ms) + "Terminates successfully, without timeout") + (is (approx== exp-count @counter) + "Count must be close to an expected value")))) + + (testing "with uninterruptible sleep" + (testing "and interruption signal to stop tasks" + (let [{:keys [await-fn stop-fn counter]} ((run-rate-limited-counting:interruption) + {:rate rate + :sleep-fn rl/uninterruptible-sleep} + n-threads run-time-sec)] + ;; here we are forced to check the count before it's too late, + ;; since the task pool termination won't happen straight away + (is (approx== exp-count @counter) + "Count must be close to an expected value") + + (is (false? (await-fn term-timeout-ms)) + "Cannot terminate due to (some) tasks still running") + ;; the only way to actually stop all running tasks (isolation) + (stop-fn) + (is (await-fn term-timeout-ms) + "Terminates successfully, without timeout THIS TIME"))) + (testing "and custom running flag to stop tasks" + (let [{:keys [await-fn counter]} ((run-rate-limited-counting:custom-flag) + {:rate rate + :sleep-fn rl/uninterruptible-sleep} + n-threads run-time-sec)] + (is (await-fn term-timeout-ms) + "Terminates successfully, without timeout") + (is (approx== exp-count @counter) + "Count must be close to an expected value"))) + (testing "when a task logic throws an exception" + (let [{:keys [await-fn counter]} ((run-rate-limited-counting:exception) + {:rate rate + :sleep-fn rl/uninterruptible-sleep} + n-threads run-time-sec)] + (is (await-fn term-timeout-ms) + "Terminates successfully, without timeout") + (is (approx== exp-count @counter) + "Count must be close to an expected value"))))))) (deftest rate-limiter-at-low-rates-test (testing "low rates" ; less than 1.0 - (testing "with interruptible sleep" - (testing "and interruption signal to stop tasks" - (let [rate 0.5 - rate-limiter (rl/rate-limiter {:rate rate}) - run-time-sec 5 - n-threads 2 - ;; each thread will have to sleep for ≈2 seconds - term-timeout-ms (total-block-time rate n-threads) - {:keys [await-fn counter]} ((run-rate-limited-counting:interruption) - rate-limiter n-threads run-time-sec)] - (is (await-fn term-timeout-ms) - "Terminates successfully, without timeout") - ;; exact error tolerance, since we are dealing with much slower ticks - (is (approx== (* rate run-time-sec) @counter 1) - "Count must be close to an expected value"))) - (testing "and custom running flag to stop tasks" - (let [rate 0.5 - rate-limiter (rl/rate-limiter {:rate rate}) - run-time-sec 5 - n-threads 2 - ;; each thread will have to sleep for ≈2 seconds - term-timeout-ms (total-block-time rate n-threads) - {:keys [await-fn counter]} ((run-rate-limited-counting:custom-flag) - rate-limiter n-threads run-time-sec)] - (is (await-fn term-timeout-ms) - "Terminates successfully, without timeout") - ;; exact error tolerance, since we are dealing with much slower ticks - (is (approx== (* rate run-time-sec) @counter 1) - "Count must be close to an expected value"))) - (testing "when a task logic throws an exception" - (let [rate 0.5 - rate-limiter (rl/rate-limiter {:rate rate}) - run-time-sec 5 - n-threads 2 - ;; each thread will have to sleep for ≈2 seconds - term-timeout-ms (total-block-time rate n-threads) - {:keys [await-fn counter]} ((run-rate-limited-counting:exception) - rate-limiter n-threads run-time-sec)] - (is (await-fn term-timeout-ms) - "Terminates successfully, without timeout") - ;; exact error tolerance, since we are dealing with much slower ticks - (is (approx== (* rate run-time-sec) @counter 1) - "Count must be close to an expected value")))) + (let [rate 0.5 + run-time-sec 5 - (testing "with uninterruptible sleep" - (testing "and interruption signal to stop tasks" - (let [rate 0.5 - rate-limiter (rl/rate-limiter {:rate rate - :sleep-fn rl/uninterruptible-sleep}) - run-time-sec 5 - n-threads 2 - ;; each thread will have to sleep for ≈2 seconds - term-timeout-ms (total-block-time rate n-threads) - {:keys [await-fn stop-fn counter]} ((run-rate-limited-counting:interruption) - rate-limiter n-threads run-time-sec)] - ;; here we are forced to check the count before it's too late, - ;; since the task pool termination won't happen straight away + exp-count (* rate run-time-sec) ;; exact error tolerance, since we are dealing with much slower ticks - (is (approx== (* rate run-time-sec) @counter 1) - "Count must be close to an expected value") + tolerance 1 - (is (false? (await-fn term-timeout-ms)) - "Cannot terminate due to (some) tasks still running") - ;; the only way to actually stop all running tasks (isolation) - (stop-fn) - (is (await-fn term-timeout-ms) - "Terminates successfully, without timeout THIS TIME"))) - (testing "and custom running flag to stop tasks" - (let [rate 0.5 - rate-limiter (rl/rate-limiter {:rate rate - :sleep-fn rl/uninterruptible-sleep}) - run-time-sec 5 - n-threads 2 - ;; each thread will have to sleep for ≈2 seconds - term-timeout-ms (total-block-time rate n-threads) - {:keys [await-fn counter]} ((run-rate-limited-counting:custom-flag) - rate-limiter n-threads run-time-sec)] - (is (await-fn term-timeout-ms) - "Terminates successfully, without timeout") - ;; exact error tolerance, since we are dealing with much slower ticks - (is (approx== (* rate run-time-sec) @counter 1) - "Count must be close to an expected value"))) - (testing "when a task logic throws an exception" - (let [rate 0.5 - rate-limiter (rl/rate-limiter {:rate rate - :sleep-fn rl/uninterruptible-sleep}) - run-time-sec 5 - n-threads 2 - ;; each thread will have to sleep for ≈2 seconds - term-timeout-ms (total-block-time rate n-threads) - {:keys [await-fn counter]} ((run-rate-limited-counting:exception) - rate-limiter n-threads run-time-sec)] - (is (await-fn term-timeout-ms) - "Terminates successfully, without timeout") - ;; exact error tolerance, since we are dealing with much slower ticks - (is (approx== (* rate run-time-sec) @counter 1) - "Count must be close to an expected value")))))) + n-threads 2 + ;; each thread will have to sleep for ≈2s (that's why we have just 2) + term-timeout-ms (total-block-time rate n-threads)] + + (testing "with interruptible sleep" + (testing "and interruption signal to stop tasks" + (let [{:keys [await-fn counter]} ((run-rate-limited-counting:interruption) + {:rate rate} n-threads run-time-sec)] + (is (await-fn term-timeout-ms) + "Terminates successfully, without timeout") + (is (approx== exp-count @counter tolerance) + "Count must be close to an expected value"))) + (testing "and custom running flag to stop tasks" + (let [{:keys [await-fn counter]} ((run-rate-limited-counting:custom-flag) + {:rate rate} n-threads run-time-sec)] + (is (await-fn term-timeout-ms) + "Terminates successfully, without timeout") + (is (approx== exp-count @counter tolerance) + "Count must be close to an expected value"))) + (testing "when a task logic throws an exception" + (let [{:keys [await-fn counter]} ((run-rate-limited-counting:exception) + {:rate rate} n-threads run-time-sec)] + (is (await-fn term-timeout-ms) + "Terminates successfully, without timeout") + (is (approx== exp-count @counter tolerance) + "Count must be close to an expected value")))) + + (testing "with uninterruptible sleep" + (testing "and interruption signal to stop tasks" + (let [{:keys [await-fn stop-fn counter]} ((run-rate-limited-counting:interruption) + {:rate rate + :sleep-fn rl/uninterruptible-sleep} + n-threads run-time-sec)] + ;; here we are forced to check the count before it's too late, + ;; since the task pool termination won't happen straight away + (is (approx== exp-count @counter tolerance) + "Count must be close to an expected value") + + (is (false? (await-fn term-timeout-ms)) + "Cannot terminate due to (some) tasks still running") + ;; the only way to actually stop all running tasks (isolation) + (stop-fn) + (is (await-fn term-timeout-ms) + "Terminates successfully, without timeout THIS TIME"))) + (testing "and custom running flag to stop tasks" + (let [{:keys [await-fn counter]} ((run-rate-limited-counting:custom-flag) + {:rate rate + :sleep-fn rl/uninterruptible-sleep} + n-threads run-time-sec)] + (is (await-fn term-timeout-ms) + "Terminates successfully, without timeout") + (is (approx== exp-count @counter tolerance) + "Count must be close to an expected value"))) + (testing "when a task logic throws an exception" + (let [{:keys [await-fn counter]} ((run-rate-limited-counting:exception) + {:rate rate + :sleep-fn rl/uninterruptible-sleep} + n-threads run-time-sec)] + (is (await-fn term-timeout-ms) + "Terminates successfully, without timeout") + (is (approx== exp-count @counter tolerance) + "Count must be close to an expected value"))))))) From bcec59847a6cdc1a26acc0f10f6f047741111ebb Mon Sep 17 00:00:00 2001 From: Mark Sto Date: Sun, 8 Jun 2025 23:24:26 +0400 Subject: [PATCH 22/23] Satisfy the CI formatter --- test/diehard/rate_limiter_test.clj | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/test/diehard/rate_limiter_test.clj b/test/diehard/rate_limiter_test.clj index 4a6cf35..57fa481 100644 --- a/test/diehard/rate_limiter_test.clj +++ b/test/diehard/rate_limiter_test.clj @@ -47,32 +47,32 @@ (do-shutdown! pool) {:await-fn (fn [timeout-ms] (ExecutorService/.awaitTermination - pool timeout-ms TimeUnit/MILLISECONDS)) + pool timeout-ms TimeUnit/MILLISECONDS)) :stop-fn #(AtomicBoolean/.set stop? true) :counter counter}))) (defn- run-rate-limited-counting:interruption [] (run-rate-limited-counting - :proceed-run? (constantly true) - :do-shutdown! ExecutorService/.shutdownNow)) + :proceed-run? (constantly true) + :do-shutdown! ExecutorService/.shutdownNow)) (defn- run-rate-limited-counting:custom-flag [] (let [running? (AtomicBoolean. true)] (run-rate-limited-counting - :proceed-run? #(AtomicBoolean/.get running?) - :do-shutdown! (fn [pool] - (AtomicBoolean/.set running? false) - (ExecutorService/.shutdown pool))))) + :proceed-run? #(AtomicBoolean/.get running?) + :do-shutdown! (fn [pool] + (AtomicBoolean/.set running? false) + (ExecutorService/.shutdown pool))))) (defn- run-rate-limited-counting:exception [] (let [throw? (AtomicBoolean. false)] (run-rate-limited-counting - :proceed-run? #(if (AtomicBoolean/.get throw?) - (throw (Exception. "Task failed!")) - true) - :do-shutdown! (fn [pool] - (AtomicBoolean/.set throw? true) - (ExecutorService/.shutdown pool))))) + :proceed-run? #(if (AtomicBoolean/.get throw?) + (throw (Exception. "Task failed!")) + true) + :do-shutdown! (fn [pool] + (AtomicBoolean/.set throw? true) + (ExecutorService/.shutdown pool))))) (deftest rate-limiter-at-high-rates-test (testing "high rates" From 1083699325519a7e188f72c7e2aea6e50b376c1c Mon Sep 17 00:00:00 2001 From: Mark Sto Date: Sun, 8 Jun 2025 23:28:39 +0400 Subject: [PATCH 23/23] Fix for older JDK version on CI --- test/diehard/rate_limiter_test.clj | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/diehard/rate_limiter_test.clj b/test/diehard/rate_limiter_test.clj index 57fa481..658adc2 100644 --- a/test/diehard/rate_limiter_test.clj +++ b/test/diehard/rate_limiter_test.clj @@ -1,8 +1,7 @@ (ns diehard.rate-limiter-test (:require [clojure.test :refer [deftest testing is]] [diehard.rate-limiter :as rl]) - (:import [java.time Duration] - [java.util.concurrent ExecutorService Executors Future TimeUnit] + (:import [java.util.concurrent ExecutorService Executors Future TimeUnit] [java.util.concurrent.atomic AtomicBoolean])) (def default-error @@ -43,7 +42,7 @@ (catch Exception ex (when-not (= ::stop (:type (ex-data ex))) (throw ex))))))) - (Thread/sleep (Duration/ofSeconds run-time-sec)) + (Thread/sleep ^long (* 1000 run-time-sec)) (do-shutdown! pool) {:await-fn (fn [timeout-ms] (ExecutorService/.awaitTermination