We use something very similar to the below code. Most of the code originates from aws-api. The retry code is generic and can be used for all sorts of stuff.
Where you want to retry is often use-case dependent. Something to keep in mind.
(defn with-retry-sync
"Calls work-fn until retriable? is false or backoff returns nil. If work-fn
throws, the exception will be passed to retriable?. If it is not retriable, the
exception will be thrown. work-fn is a function of no arguments. retriable? is
passed the result or exception from calling work-fn. backoff is a function of
the number of times work-fn has been called."
[work-fn retriable? backoff]
(let [maybe-throw #(if (instance? Throwable %) (throw %) %)]
(loop [retries 0]
(let [resp (try (work-fn) (catch Throwable t t))]
(if (retriable? resp)
(if-let [bo (backoff retries)]
(do
(Thread/sleep bo)
(recur (inc retries)))
(maybe-throw resp))
(maybe-throw resp))))))
(defn capped-exponential-backoff
"Returns a function of the num-retries (so far), which returns the
lesser of max-backoff and an exponentially increasing multiple of
base, or nil when (>= num-retries max-retries).
See with-retry to see how it is used.
Alpha. Subject to change."
([] (capped-exponential-backoff 100 10000 8))
([base max-backoff max-retries]
(fn [num-retries]
(when (< num-retries max-retries)
(min max-backoff
(* base (bit-shift-left 1 num-retries)))))))
(defn capped-exponential-backoff-with-jitter
([] (capped-exponential-backoff-with-jitter {}))
([{:keys [base
max-backoff
max-retries
max-jitter-ms]
:or {base 100
max-backoff 20000
max-retries 4
max-jitter-ms 100}}]
(let [backoff-fn (capped-exponential-backoff
base
max-backoff
max-retries)]
(fn [num-retries]
(when-let [backoff-ms (backoff-fn num-retries)]
;; adding "jitter" can help reduce throttling on retries:
;; https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
(min (+ backoff-ms (rand-int max-jitter-ms)) max-backoff))))))
(defn default-retriable?
"Returns true if x is an anomaly or if it is an ExceptionInfo with an anomaly
in its ex-data."
[x]
(or
(contains? #{:cognitect.anomalies/busy
:cognitect.anomalies/unavailable}
(:cognitect.anomalies/category x))
(and (instance? clojure.lang.ExceptionInfo x)
(default-retriable? (ex-data x)))))
(defn connect
[client arg-map]
(with-retry-sync #(d/connect client arg-map)
default-retriable? (capped-exponential-backoff-with-jitter)))