335 lines
11 KiB
Clojure
335 lines
11 KiB
Clojure
|
|
(ns again.core-test
|
||
|
|
(:require [again.core :as a :refer [with-retries]]
|
||
|
|
[clojure.test :refer [is deftest testing]]
|
||
|
|
[clojure.test.check.clojure-test :refer [defspec]]
|
||
|
|
[clojure.test.check.generators :as gen]
|
||
|
|
[clojure.test.check.properties :as prop]))
|
||
|
|
|
||
|
|
(defspec spec-max-retries
|
||
|
|
200
|
||
|
|
(prop/for-all
|
||
|
|
[n gen/s-pos-int]
|
||
|
|
(let [s (a/max-retries n (repeat 0))]
|
||
|
|
(= (count s) n))))
|
||
|
|
|
||
|
|
(defspec spec-clamp-delay
|
||
|
|
200
|
||
|
|
(prop/for-all
|
||
|
|
[n gen/s-pos-int
|
||
|
|
max-delay gen/s-pos-int]
|
||
|
|
(let [s (a/max-retries
|
||
|
|
n
|
||
|
|
;; The increment is picked so that we'll cross max-delay on delay 3
|
||
|
|
(a/clamp-delay max-delay (a/additive-strategy 0 (/ max-delay 2))))]
|
||
|
|
(every? #(<= % max-delay) s))))
|
||
|
|
|
||
|
|
(defspec spec-max-delay
|
||
|
|
200
|
||
|
|
(prop/for-all
|
||
|
|
[n gen/s-pos-int
|
||
|
|
max-delay gen/s-pos-int]
|
||
|
|
(let [s (a/max-retries
|
||
|
|
n
|
||
|
|
(a/max-delay max-delay (a/additive-strategy 0 (/ max-delay 10))))]
|
||
|
|
(and (= (count s) (min n 10))
|
||
|
|
(every? #(<= % max-delay) s)))))
|
||
|
|
|
||
|
|
(defspec spec-max-duration
|
||
|
|
200
|
||
|
|
(prop/for-all
|
||
|
|
[d gen/s-pos-int]
|
||
|
|
(let [s (take (* 2 d) (a/max-duration d (a/constant-strategy 1)))]
|
||
|
|
(and (= (count s) d)
|
||
|
|
(= (reduce + s) d)))))
|
||
|
|
|
||
|
|
(deftest test-max-duration
|
||
|
|
(testing "with not enough delays to satisfy specified duration"
|
||
|
|
(is (= (a/max-duration 10000 [0]) [0]))))
|
||
|
|
|
||
|
|
(defspec spec-constant-strategy
|
||
|
|
200
|
||
|
|
(prop/for-all
|
||
|
|
[n gen/s-pos-int
|
||
|
|
delay gen/pos-int]
|
||
|
|
(let [s (a/max-retries n (a/constant-strategy delay))]
|
||
|
|
(and (= (count s) n)
|
||
|
|
(= (set s) #{delay})))))
|
||
|
|
|
||
|
|
(defspec spec-immediate-strategy
|
||
|
|
200
|
||
|
|
(prop/for-all
|
||
|
|
[n gen/s-pos-int]
|
||
|
|
(let [s (a/max-retries n (a/immediate-strategy))]
|
||
|
|
(and (= (count s) n)
|
||
|
|
(= (set s) #{0})))))
|
||
|
|
|
||
|
|
(defspec spec-additive-strategy
|
||
|
|
200
|
||
|
|
(prop/for-all
|
||
|
|
[n gen/s-pos-int
|
||
|
|
initial-delay gen/pos-int
|
||
|
|
increment gen/pos-int]
|
||
|
|
(let [s (a/max-retries n (a/additive-strategy initial-delay increment))
|
||
|
|
p (fn [[a b]] (= (+ a increment) b))]
|
||
|
|
(and (= (count s) n)
|
||
|
|
(= (first s) initial-delay)
|
||
|
|
(every? p (partition 2 1 s))))))
|
||
|
|
|
||
|
|
(defspec spec-multiplicative-strategy
|
||
|
|
200
|
||
|
|
(prop/for-all
|
||
|
|
[n gen/s-pos-int
|
||
|
|
initial-delay gen/s-pos-int
|
||
|
|
delay-multiplier (gen/elements [1.0 1.1 1.3 1.6 2.0 3.0 5.0 9.0 14.0 20.0])]
|
||
|
|
(let [s (a/max-retries
|
||
|
|
n
|
||
|
|
(a/multiplicative-strategy initial-delay delay-multiplier))
|
||
|
|
p (fn [[a b]] (= (* a delay-multiplier) b))]
|
||
|
|
(and (= (count s) n)
|
||
|
|
(= (first s) initial-delay)
|
||
|
|
(every? p (partition 2 1 s))))))
|
||
|
|
|
||
|
|
(defspec spec-randomize-delay
|
||
|
|
200
|
||
|
|
(prop/for-all
|
||
|
|
[rand-factor (gen/elements [0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9])
|
||
|
|
delay gen/s-pos-int]
|
||
|
|
(let [randomize-delay #'again.core/randomize-delay
|
||
|
|
min-delay (bigint (* delay (- 1 rand-factor)))
|
||
|
|
max-delay (bigint (inc (* delay (+ 1 rand-factor))))
|
||
|
|
rand-delay (randomize-delay rand-factor delay)]
|
||
|
|
(and (<= 0 rand-delay)
|
||
|
|
(<= min-delay rand-delay max-delay)))))
|
||
|
|
|
||
|
|
(defspec spec-randomize-strategy
|
||
|
|
200
|
||
|
|
(prop/for-all
|
||
|
|
[n gen/s-pos-int
|
||
|
|
rand-factor (gen/elements [0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9])]
|
||
|
|
(let [initial-delay 1000
|
||
|
|
s (a/max-retries
|
||
|
|
n
|
||
|
|
(a/randomize-strategy
|
||
|
|
rand-factor
|
||
|
|
(a/constant-strategy initial-delay)))
|
||
|
|
min-delay (bigint (* initial-delay (- 1 rand-factor)))
|
||
|
|
max-delay (bigint (inc (* initial-delay (+ 1 rand-factor))))]
|
||
|
|
(every? #(<= min-delay % max-delay) s))))
|
||
|
|
|
||
|
|
(deftest test-stop-strategy
|
||
|
|
(is (empty? (a/stop-strategy)) "stop strategy has no delays"))
|
||
|
|
|
||
|
|
(defn new-failing-fn
|
||
|
|
"Returns a map consisting of the following fields:
|
||
|
|
- `f` - a function that will succeed on the `n`th call
|
||
|
|
- `attempts` - an atom counting the number of executions of `f`
|
||
|
|
- `exception` - the exception that `f` throws until it succeeds"
|
||
|
|
[& [n]]
|
||
|
|
(let [n (or n Integer/MAX_VALUE)
|
||
|
|
attempts (atom 0)
|
||
|
|
exception (Exception. "retry")
|
||
|
|
f #(let [i (swap! attempts inc)]
|
||
|
|
(if (< i n)
|
||
|
|
(throw exception)
|
||
|
|
i))]
|
||
|
|
{:attempts attempts :exception exception :f f}))
|
||
|
|
|
||
|
|
(defn new-callback-fn
|
||
|
|
"Returns a map consisting of the following fields:
|
||
|
|
- `callback` - a callback function to pass to `with-retries` that will fail
|
||
|
|
the operation early after the `n`th call
|
||
|
|
- `args` - an atom recording the arguments passed to `callback`"
|
||
|
|
[& [n]]
|
||
|
|
(let [n (or n Integer/MAX_VALUE)
|
||
|
|
attempts (atom 0)
|
||
|
|
args (atom [])
|
||
|
|
callback #(let [i (swap! attempts inc)]
|
||
|
|
(swap! args conj %)
|
||
|
|
(when (< n i)
|
||
|
|
::a/fail))]
|
||
|
|
{:args args :callback callback}))
|
||
|
|
|
||
|
|
(defspec spec-with-retries
|
||
|
|
200
|
||
|
|
(prop/for-all
|
||
|
|
[strategy (gen/vector gen/s-pos-int)]
|
||
|
|
(let [{:keys [attempts f]} (new-failing-fn)
|
||
|
|
delays (atom [])]
|
||
|
|
(with-redefs [a/sleep #(swap! delays conj %)]
|
||
|
|
(try
|
||
|
|
(with-retries strategy (f))
|
||
|
|
(catch Exception _)))
|
||
|
|
|
||
|
|
(and (= @attempts (inc (count strategy)))
|
||
|
|
(= @delays strategy)))))
|
||
|
|
|
||
|
|
(deftest test-with-retries
|
||
|
|
(with-redefs [a/sleep (constantly nil)]
|
||
|
|
(testing "with-retries"
|
||
|
|
(testing "with non-nil return value"
|
||
|
|
(is (= (with-retries [] :ok) :ok) "returns form value"))
|
||
|
|
|
||
|
|
(testing "with nil return value"
|
||
|
|
(is (nil? (with-retries [] nil)) "returns form value"))
|
||
|
|
|
||
|
|
(testing "with user-context"
|
||
|
|
(let [context {:a :b}
|
||
|
|
{:keys [args callback]} (new-callback-fn)
|
||
|
|
options {::a/callback callback
|
||
|
|
::a/strategy []
|
||
|
|
::a/user-context context}
|
||
|
|
_ (with-retries options :ok)]
|
||
|
|
(is (= (count @args) 1) "calls callback hook once")
|
||
|
|
(is (= (::a/user-context (first @args)) context)
|
||
|
|
"calls callback hook with user context")))
|
||
|
|
|
||
|
|
(testing "with success on first try"
|
||
|
|
(let [{:keys [attempts f]} (new-failing-fn 1)
|
||
|
|
{:keys [args callback]} (new-callback-fn)]
|
||
|
|
(with-retries
|
||
|
|
{::a/callback callback
|
||
|
|
::a/strategy []}
|
||
|
|
(f))
|
||
|
|
(is (= @attempts 1) "executes operation once")
|
||
|
|
(is (= (count @args) 1) "calls callback hook once")
|
||
|
|
(is (= (first @args)
|
||
|
|
{::a/attempts 1
|
||
|
|
::a/slept 0
|
||
|
|
::a/status :success})
|
||
|
|
"calls callback hook with success")))
|
||
|
|
|
||
|
|
(testing "with success on second try"
|
||
|
|
(let [{:keys [attempts exception f]} (new-failing-fn 2)
|
||
|
|
{:keys [args callback]} (new-callback-fn)]
|
||
|
|
(with-retries
|
||
|
|
{::a/callback callback
|
||
|
|
::a/strategy [12]}
|
||
|
|
(f))
|
||
|
|
(is (= @attempts 2) "executes operation twice")
|
||
|
|
(is (= (count @args) 2) "calls callback hook twice")
|
||
|
|
(is (= (first @args)
|
||
|
|
{::a/attempts 1
|
||
|
|
::a/exception exception
|
||
|
|
::a/slept 0
|
||
|
|
::a/status :retry})
|
||
|
|
"calls callback hook with failure")
|
||
|
|
(is (= (second @args)
|
||
|
|
{::a/attempts 2
|
||
|
|
::a/slept 12
|
||
|
|
::a/status :success})
|
||
|
|
"calls callback hook with success")))
|
||
|
|
|
||
|
|
(testing "with permanent failure"
|
||
|
|
(let [{:keys [exception f]} (new-failing-fn)
|
||
|
|
{:keys [args callback]} (new-callback-fn)]
|
||
|
|
(is (thrown?
|
||
|
|
Exception
|
||
|
|
(with-retries
|
||
|
|
{::a/callback callback
|
||
|
|
::a/strategy [123]}
|
||
|
|
(f)))
|
||
|
|
"throws exception")
|
||
|
|
|
||
|
|
(is (= (count @args) 2) "calls callback hook twice")
|
||
|
|
(is (= (first @args)
|
||
|
|
{::a/attempts 1
|
||
|
|
::a/exception exception
|
||
|
|
::a/slept 0
|
||
|
|
::a/status :retry})
|
||
|
|
"calls callback hook with failure")
|
||
|
|
(is (= (second @args)
|
||
|
|
{::a/attempts 2
|
||
|
|
::a/exception exception
|
||
|
|
::a/slept 123
|
||
|
|
::a/status :failure})
|
||
|
|
"calls callback hook with permanent failure")))
|
||
|
|
|
||
|
|
(testing "with early failure"
|
||
|
|
(let [{:keys [exception f]} (new-failing-fn)
|
||
|
|
{:keys [args callback]} (new-callback-fn 1)]
|
||
|
|
(is (thrown?
|
||
|
|
Exception
|
||
|
|
(with-retries
|
||
|
|
{::a/callback callback
|
||
|
|
::a/strategy [1 2 3]}
|
||
|
|
(f)))
|
||
|
|
"throws exception")
|
||
|
|
|
||
|
|
(is (= (count @args) 2) "calls callback hook three twice")
|
||
|
|
(is (= (first @args)
|
||
|
|
{::a/attempts 1
|
||
|
|
::a/exception exception
|
||
|
|
::a/slept 0
|
||
|
|
::a/status :retry})
|
||
|
|
"first callback call")
|
||
|
|
(is (= (second @args)
|
||
|
|
{::a/attempts 2
|
||
|
|
::a/exception exception
|
||
|
|
::a/slept 1
|
||
|
|
::a/status :retry})
|
||
|
|
"last callback call"))))))
|
||
|
|
|
||
|
|
(defmulti log-attempt ::a/status)
|
||
|
|
|
||
|
|
(defmethod log-attempt :retry [s]
|
||
|
|
(if (< (count @(::a/user-context s)) 1)
|
||
|
|
(swap! (::a/user-context s) conj :retry)
|
||
|
|
(do
|
||
|
|
(swap! (::a/user-context s) conj :fail)
|
||
|
|
::a/fail)))
|
||
|
|
|
||
|
|
(defmethod log-attempt :success [s]
|
||
|
|
(swap! (::a/user-context s) conj :success))
|
||
|
|
|
||
|
|
(defmethod log-attempt :failure [s]
|
||
|
|
(swap! (::a/user-context s) conj :failure))
|
||
|
|
|
||
|
|
(defmethod log-attempt :default [s] (assert false))
|
||
|
|
|
||
|
|
(deftest test-multimethod-callback
|
||
|
|
(with-redefs [a/sleep (constantly nil)]
|
||
|
|
(testing "multi-method-callback"
|
||
|
|
(testing "with success"
|
||
|
|
(let [{:keys [f]} (new-failing-fn 2)
|
||
|
|
user-context (atom [])]
|
||
|
|
(with-retries
|
||
|
|
{::a/callback log-attempt
|
||
|
|
::a/strategy [1 2]
|
||
|
|
::a/user-context user-context}
|
||
|
|
(f))
|
||
|
|
(is (= (count @user-context) 2) "multimethod is called twice")
|
||
|
|
(is (= (first @user-context) :retry) "first call is a retry")
|
||
|
|
(is (= (second @user-context) :success) "second call is a success")))
|
||
|
|
|
||
|
|
(testing "with failure"
|
||
|
|
(let [{:keys [exception f]} (new-failing-fn)
|
||
|
|
user-context (atom [])]
|
||
|
|
(try
|
||
|
|
(with-retries
|
||
|
|
{::a/callback log-attempt
|
||
|
|
::a/strategy [1]
|
||
|
|
::a/user-context user-context}
|
||
|
|
(f))
|
||
|
|
(catch Exception e
|
||
|
|
(is (= e exception) "Unexpected exception")))
|
||
|
|
(is (= (count @user-context) 2) "multimethod is called twice")
|
||
|
|
(is (= (first @user-context) :retry) "first call is a retry")
|
||
|
|
(is (= (second @user-context) :failure) "second call is a failure")))
|
||
|
|
|
||
|
|
(testing "with early failure"
|
||
|
|
(let [{:keys [exception f]} (new-failing-fn)
|
||
|
|
user-context (atom [])]
|
||
|
|
(try
|
||
|
|
(with-retries
|
||
|
|
{::a/callback log-attempt
|
||
|
|
::a/strategy [1 2]
|
||
|
|
::a/user-context user-context}
|
||
|
|
(f))
|
||
|
|
(catch Exception e
|
||
|
|
(is (= e exception) "Unexpected exception")))
|
||
|
|
(is (= (count @user-context) 2) "multimethod is called three times")
|
||
|
|
(is (= (first @user-context) :retry) "first call is a retry")
|
||
|
|
(is (= (second @user-context) :fail) "second call is a fail"))))))
|
||
|
|
|
||
|
|
|