Again
A Clojure library for retrying an operation based on a retry strategy.
Clojars
[listora/again "1.0.0"]
Usage
Require the library:
(require '[again.core :as again])
Again provides a very simple (too simple?) API for retrying an operation: given a retry strategy and an operation, the operation will be retried according to the provided strategy as long as it throws an exception. Only an exception is considered a failure - the library does not consider the return value of the operation.
A retry strategy is just a sequence of integers that represent a delay in
milliseconds before the operation is attempted again. Once the sequence runs
out, with-retries
will re-throw the last exception.
A fundamental design goal of the library is to allow an existing form to be
wrapped in with-retries
without any other code changes in order to enable
retries of that form.
Eg:
(original-form …)
Becomes:
(with-retries …
(original-form …))
A note on terminology: an attempt is an execution of the wrapped form, whereas a retry is any subsequent execution of the wrapped form after the initial failed attempt. Documentation in the earlier versions of this library used retry everywhere, but we've tried to make a cleaner distinction between the two terms since then.
Basic use case:
(require '[again.core :as again])
(again/with-retries
[100 1000 10000]
(my-operation …))
The above will attempt executing my-operation
four times, with 100ms
,
1000ms
and 10000ms
delays between each attempt.
The library provides a numbers of functions for generating and manipulating retry strategies. Most of the provided generators return strategies of infinite delay sequences. The infinite strategies can be restricted with the manipulator functions.
Advanced use case:
The advanced form allows you to pass in other options than just the retry strategy.
(require '[again.core :as again])
(defmulti log-attempt ::again/status)
(defmethod log-attempt :retry [s]
(swap! (::again/user-context s) assoc :retried? true)
(println "RETRY" s))
(defmethod log-attempt :success [s]
(if (-> s ::again/user-context deref :retried?)
(println "SUCCESS after" (::again/attempts s) "attempts" s)
(println "SUCCESS on first attempt" s)))
(defmethod log-attempt :failure [s]
(println "FAILURE" s))
(again/with-retries
{::again/callback log-attempt
::again/strategy [100 1000 10000]
::again/user-context (atom {})}
(my-operation …))
The above example is contrived (there's no need to set :retried?
in the user
context since the :success
callback could just check if ::again/attempts
is
greater than 1
), but it tries to show that:
- instead of a sequence of delays,
with-retries
also accepts a map as its first argument - the
:again.core/strategy
key is used to pass in the delay strategy - the
:again.core/callback
key can be used to specify a function that will get called after each attempt - the
:again.core/user-context
key can be used to specify a user-defined context object that will get passed to the callback function
The callback function and the context object allows (hopefully!) for arbitrary monitoring implementations where the results of each attempt can be eg logged to a monitoring system.
The callback is called with a map as its only argument:
{
;; the number of form executions - a positive integer
:again.core/attempts …
;; the exception that was thrown when the execution failed (not present
;; in the `:success` case)
:again.core/exception …
;; the sum of all delays thus far (in milliseconds)
:again.core/slept …
;; the result of the previous execution - `:success`, `:failure`, or
;; `:retry`
:again.core/status …
;; the `:again.core/user-context` value from the map passed to `with-retries`
:again.core/user-context …
}
The callback can also return the :again.core/fail
keyword to ignore the rest
of the retry strategy and throw the current exception from with-retries
(That
is, it provides a mechanism for early termination). For example, the callback
could check the exception's ex-data
and decide to fail the operation:
(again/with-retries
{::again/callback #(when (-> % ::again/exception ex-data :fail?) ::again/fail)
::again/strategy [100 1000 10000]}
(my-operation …))
Generators:
additive-strategy
- an infinite sequence of incrementally increasing delaysconstant-strategy
- an infinite sequence of the given delayimmediate-strategy
- an infinite sequence of0ms
delaysmultiplicative-strategy
- an infinite sequence of exponentially increasing delaysstop-strategy
-nil
, ie no retries
Manipulators:
clamp-delay
- limit the delay to a given numbermax-delay
- truncate the sequence once the delay between two attempts exceeds the given numbermax-duration
- truncate the sequence once the sum of all past delays exceeds the given numbermax-retries
- truncate the sequence after the given number of retriesrandomize-strategy
- scale each delay with a new random number
Exponential backoff example:
The generators and manipulators can be combined to create a desired retry
strategy. Eg an exponential backoff retry strategy with an initial delay of
500ms
and a multiplier of 1.5
, limited to either 10
retries or a maximum
combined delay of 10s
can be generated as follows:
(def exponential-backoff-strategy
(again/max-duration
10000
(again/max-retries
10
(again/randomize-strategy
0.5
(again/multiplicative-strategy 500 1.5)))))
We can also prepend a 0
to the strategy in order to execute the
first retry immediately:
(def exponential-backoff-strategy-with-immediate-retry
(cons 0 exponential-backoff-strategy))
License
Copyright © 2014–2017 Listora, Lauri Pesonen
Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.