Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generalize retry criteria #5

Merged
merged 22 commits into from
May 8, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
ad262ef
doc strings for built-in decay functions
scgilardi Mar 26, 2012
3fe0aa3
add :return? option and provide its built-in function values
scgilardi Mar 26, 2012
7360b3d
correct formatting for decay doc string
scgilardi Mar 26, 2012
b74bac4
decay -> resolve-decay, add resolve-return, more bulletproofing
scgilardi Mar 26, 2012
873b5e1
remove :try from default-options (not an option), add init-options
scgilardi Mar 26, 2012
230d9a9
switch to using the pre-resolved :decay option value
scgilardi Mar 26, 2012
fc86b3c
generalize failure to include "bad" return values
scgilardi Mar 26, 2012
f08c442
adjust tests for resolve-decay
scgilardi Mar 26, 2012
b2b6453
test resolve-return
scgilardi Mar 26, 2012
400f9da
adjust tests for :try not being in default-options any more
scgilardi Mar 26, 2012
f9d40aa
add tests for failure handling based on :return?
scgilardi Mar 26, 2012
40db2a9
update docs for failure handling based on :return?
scgilardi Mar 26, 2012
8e0fa68
improve decay doc strings
scgilardi Mar 26, 2012
86111dc
improve :always doc string
scgilardi Mar 26, 2012
6e4b823
cleanliness tweaks
scgilardi Mar 26, 2012
8cf45f9
take advantage of init-options
scgilardi Mar 26, 2012
c9817b0
add test based on :return? to test-retry
scgilardi Mar 26, 2012
7eaa680
resolve-return: d -> r
scgilardi Mar 26, 2012
ce9d97c
refine keyword resolution
scgilardi Mar 26, 2012
aa34693
test for bad decay value
scgilardi Mar 26, 2012
8b289fe
enforce at least one arg to try-try-again, simplify parse
scgilardi Mar 26, 2012
819b6c0
make parse take args just like try-try-again
scgilardi Mar 26, 2012
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 34 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,13 @@ Add this to your project.clj :dependencies list:

;; but with the addition of a first options arg, if you don't
(try-try-again {:sleep 5000 :tries 100} #(some-fn arg1 arg2))
(try-try-again {:sleep nil :tries 100} #(some-fn arg1 arg2))
(try-try-again {:sleep nil :tries 100} some-fn arg1 arg2)
(try-try-again {:decay :exponential :tries 100} #(some-fn arg1 arg2))

;; by default, a failure is detected when the function throws an
;; exception. options can refine that criterion based on the type of
;; exception thrown or by classifying some return values as failures.

(try-try-again {;; all options are optional

;; :sleep is used to specify how long to sleep
Expand All @@ -41,30 +45,51 @@ Add this to your project.clj :dependencies list:
;; if you want your sleep amount to change over
;; time, you can provide a decay function:
;; a number - your sleep will be multiplied by it
;; a function - your sleep will passed into it
;; a function - your sleep will passed into it, or
;; a keyword - for out of the box decay algorithms
;; :exponential, :double, :golden-ratio
;; default is identity
:decay :exponential

;; if the function signals failure by its return value
;; rather than (or in addition to) throwing
;; exceptions, you can provide a return? predicate to
;; detect failures. The predicate will be given a
;; candidate return value and should return truthy if
;; the value should be returned or falsey to request a
;; retry. The :return? option value should be either:
;; a function of one argument, or
;; a keyword - for out of the box predicates:
;; :always, :truthy?, :falsey?
;; default is :always
:return? :truthy?

;; if you want to only retry when particular
;; exceptions are thrown, you can add a :catch
;; clause. it works with either a single type
;; or a collection.
;; default is Exception
:catch [java.io.IOException java.sql.SQLException]

;; if you would like a function to be called on
;; each failure (for instance, logging each failed
;; attempt), you can specify an error-hook.
;; The error that occurred is passed into the
;; error-hook function as an argument.
;; The error-hook method can also force a
;; short-circuit failure by returning false, or
;; if you would like a function to be called on each
;; failure (for instance, logging each failed
;; attempt), you can specify an error-hook. The error
;; that occurred (an exception or a return value that
;; the :return? predicate classified as indicating
;; failure) is passed into the error-hook function as
;; an argument. The error-hook function can also force
;; a short-circuit failure by returning false, or
;; force an additional retry by returning true.

:error-hook (fn [e] (println "I got an error:" e))}
#(some-fn arg1 arg2))

;; When retries are exhausted, try-try-again will either:
;; - throw the last exception caught if the last try generated an
;; exception, or
;; - return the last return value that the :return? predicate
;; classified as indicating falure.

;; In addition, four dynamic variables are bound in both the
;; passed in and error-hook functions:
;; *first-try* - true if this is the first try
Expand Down
134 changes: 90 additions & 44 deletions src/robert/bruce.clj
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,40 @@
(def default-options {:sleep 10000
:tries 5
:decay identity
:return? :always
:catch Exception
:try 1 ;; try is not overrideable
:error-hook (constantly nil)})

(defn double [x]
(defn double
"decay option: 2^n"
[x]
(* 2 x))

(defn exponential [x]
(defn exponential
"decay option: e^n"
[x]
(* Math/E x))

(defn golden-ratio [x]
(defn golden-ratio
"decay option: φ^n"
[x]
(* 1.6180339887 x))

(defn always
":return? option (default): any return value indicates success"
[x]
true)

(defn truthy?
":return? option: only truthy return values indicate success"
[x]
(boolean x))

(defn falsey?
":return? option: only falsey return values indicate success"
[x]
(not x))

(defn catch
"internal function that returns a collection of exceptions to catch"
[options]
Expand All @@ -26,39 +47,58 @@
catch
[catch])))

(defn decay
"internal function that returns of a function that implements the selected
decay strategy, said function will take a number as an operand and return a
number as a result"
(defn resolve-keyword
"resolves k as a function named in this namespace"
[k]
(when-let [f (->> k name symbol (ns-resolve 'robert.bruce))]
@f))

(defn resolve-decay
"internal function that resolves the value of :decay in options to a
decay strategy: a function that accepts a current time delay
milliseconds in and returns a new time delay milliseconds"
[{d :decay :as options}]
(if-let [d (cond (nil? d) identity
(fn? d) d
(number? d) #(* d %)
(keyword? d) (resolve-keyword d))]
(assoc options :decay d)
(throw (IllegalArgumentException.
(str "Unrecognized :decay option " d)))))

(defn resolve-return
"internal function that resolves the value of :return? in options to
a return? filter: a function that accepts a candidate return value
and returns truthy to approve it being returned or falsey to request
a retry."
[{r :return? :as options}]
(if-let [r (cond (nil? r) always
(fn? r) r
(keyword? r) (resolve-keyword r))]
(assoc options :return? r)
(throw (IllegalArgumentException.
(str "Unrecognized :return? option " r)))))

(defn init-options
[options]
(let [d (:decay options)]
(cond (nil? d) identity
(fn? d) d
(number? d) #(* d %)
(keyword? d) @(ns-resolve 'robert.bruce (symbol (name d))))))
(-> options (assoc :try 1) resolve-decay resolve-return))

(defn parse
"internal function that parses arguments into usable bits"
[args]
(let [argc (count args)
fn (first (filter fn? args))
options (if (and (> argc 1)
(map? (first args)))
(first args)
{})
options (merge default-options
(select-keys (meta fn)
(keys default-options))
options)
options (assoc options :try 1)
args (rest (drop-while (complement fn?) args))]
[options fn args]))
[arg & args]
(let [[arg-options [f & args]] (if (map? arg)
[arg args]
[nil (cons arg args)])
meta-options (select-keys (meta f) (keys default-options))
options (merge default-options meta-options arg-options)]
[options f args]))

(defn try-again?
"internal function that determines whether we try again"
[options t]
[options error]
(let [tries (:tries options)]
(and (some #(isa? (type t) %) (catch options))
(and (or (not (instance? Throwable error))
(some #(isa? (type error) %) (catch options)))
(or (= :unlimited (keyword tries))
(pos? tries)))))

Expand All @@ -75,7 +115,7 @@ number as a result"
"internal function that updates sleep with the decay function"
[options]
(update-in options [:sleep] (if (:sleep options)
(decay options)
(:decay options)
identity)))

(def ^{:dynamic true} *first-try* nil)
Expand All @@ -90,29 +130,35 @@ number as a result"
*first-try* (= 1 (:try options))
*last-try* (= 1 (:tries options))
*error* (::error options)]
(try
(let [ret (f)]
(if (instance? IObj ret)
(vary-meta ret assoc :tries *try*)
ret))
(catch Throwable t
(let [options (-> options
(let [{:keys [returned thrown]}
(try
{:returned (f)}
(catch Throwable t
{:thrown t}))]
(if (and (not thrown) ((:return? options) returned))
(if (instance? IObj returned)
(vary-meta returned assoc :tries *try*)
returned)
(let [error (or thrown returned)
options (-> options
update-tries
(assoc ::error t))
continue ((:error-hook options) t)]
(assoc ::error error))
continue ((:error-hook options) error)]
(if (or (true? continue)
(and (not (false? continue))
(try-again? options t)))
(try-again? options error)))
(do
(when-let [sleep (:sleep options)]
(Thread/sleep (long sleep)))
#(retry (update-sleep options) f))
(throw t)))))))

(if thrown
(throw thrown)
returned)))))))

(defn try-try-again
"if at first you don't succeed, intelligent retry trampolining"
{:arglists '([fn] [fn & args] [options fn] [options fn & args])}
[& args]
(let [[options fn args] (parse args)]
[arg & args]
(let [[options fn args] (apply parse arg args)
options (init-options options)]
(trampoline retry options #(apply fn args))))
Loading