Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
joegallo committed Mar 17, 2011
0 parents commit 5683430
Show file tree
Hide file tree
Showing 5 changed files with 250 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .gitignore
@@ -0,0 +1,5 @@
pom.xml
*jar
/lib/
/classes/
.lein-deps-sum
61 changes: 61 additions & 0 deletions README.md
@@ -0,0 +1,61 @@
# Robert Bruce

Robert Bruce provides an easy way to execute a function and allow
failures to be automatically retried. It's named after
[Robert the Bruce](http://en.wikipedia.org/wiki/Robert_the_Bruce),
whose determination was inspired by the sight a spider trying many
time (and failing) to build a web.

Add this to your project.clj :dependencies list:

[robert/bruce "0.5.0"]

## Usage

(use '[robert.bruce :only [try-try-again]])

;; arguments are like trampoline, if you want to default options
(try-try-again some-fn)
(try-try-again some-fn arg1 arg2)
(try-try-again #(some-fn arg1 arg2))

;; 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 5000 :tries 100} #(some-fn arg1 arg2))

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

;; :sleep is used to specify how long to sleep
;; between retries, it can be a number, or false
;; or nil if you don't want to sleep,
;; default is 10 seconds (that is, 10000)
:sleep 100

;; :tries is used to specific how many tries
;; will be attempted, it can also be :unlimited
;; default is 5
:tries 100
;; if you want to 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 keyword - for out of the box decay algorithms
;; :exponential, :double, :golden-ratio
;; default is identity
:decay :exponential

;; if you want to only rety 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]}
#(some-fn arg1 arg2))

## License

Copyright (C) 2011 Joe Gallo

Distributed under the Eclipse Public License, the same as Clojure.
3 changes: 3 additions & 0 deletions project.clj
@@ -0,0 +1,3 @@
(defproject robert-bruce "0.5.0"
:description "trampolining retries for clojure"
:dependencies [[org.clojure/clojure "1.2.0"]])
89 changes: 89 additions & 0 deletions src/robert/bruce.clj
@@ -0,0 +1,89 @@
(ns robert.bruce
(:refer-clojure :exclude [double]))

(def default-options {:sleep 10000
:tries 5
:decay identity
:catch Exception})

(defn double [x]
(* 2 x))

(defn exponential [x]
(* Math/E x))

(defn golden-ratio [x]
(* 1.6180339887 x))

(defn catch
"internal function that returns a collection of exceptions to catch"
[options]
(let [catch (:catch options)]
(if (coll? catch)
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"
[options]
(let [d (:decay options)]
(cond (nil? d) identity
(fn? d) d
(number? d) #(* d %)
(keyword? d) @(ns-resolve 'robert.bruce (symbol (name d))))))

(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 options)
args (rest (drop-while (complement fn?) args))]
[options fn args]))

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

(defn update-tries [options]
"internal function that updates the number of tries that remain"
(update-in options [:tries] (if (= :unlimited (:tries options))
identity
dec)))

(defn update-sleep [options]
"internal function that updates sleep with the decay function"
(update-in options [:sleep] (if (:sleep options)
(decay options)
identity)))

(defn retry
"internal function that will actually retry with the specified options"
[options f]
(try
(f)
(catch Throwable t
(let [options (update-tries options)]
(if (try-again? options t)
(do
(when-let [sleep (:sleep options)]
(Thread/sleep (long sleep)))
#(retry (update-sleep options) f))
(throw t))))))

(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)]
(trampoline retry options #(apply fn args))))
92 changes: 92 additions & 0 deletions test/robert/test/bruce.clj
@@ -0,0 +1,92 @@
(ns robert.test.bruce
(:use [robert.bruce])
(:use [clojure.test])
(:refer-clojure :exclude [double])
(:import (java.io IOException)))

(deftest test-double
(is (= 2 (double 1))))

(deftest test-exponential
(is (= Math/E (exponential 1))))

(deftest test-golden-ratio
(is (= 1.6180339887 (golden-ratio 1))))

(deftest test-catch
(testing "catch allows a single exception or a collection"
(is (= [Exception] (catch {:catch Exception})))
(is (= [Exception] (catch {:catch [Exception]})))
(is (= [Exception IOException] (catch {:catch [Exception IOException]})))))

(deftest test-decay
(testing "decay allows nothing, a number, a function, or keywords"
(is (= 1 ((decay {}) 1)))
(is (= 1 ((decay {:decay 1}) 1)))
(is (= 2 ((decay {:decay 2}) 1)))
(is (= 2 ((decay {:decay double}) 1)))
(is (= 2 ((decay {:decay :double}) 1)))
(is (= Math/E ((decay {:decay :exponential}) 1)))
(is (= 1.6180339887 ((decay {:decay :golden-ratio}) 1)))))

(deftest test-parse
(testing "parse handles a variety of arguments correctly"
(is (= [default-options identity []]
(parse [identity])))
(is (= [default-options identity ["a" "b"]]
(parse [identity "a" "b"])))
(is (= [(assoc default-options :tries 100) identity []]
(parse [{:tries 100} identity])))
(is (= [(assoc default-options :tries 100) identity ["a" "b"]]
(parse [{:tries 100} identity "a" "b"]))))
(testing "parse merges your options with the default options"
(let [options {:a 1 :b 2 :sleep nil :tries 10}]
(is (= [(merge default-options options) identity []]
(parse [options identity]))))))

(deftest test-try-again?
(testing "first, the exception must be acceptable"
(is (try-again? {:catch [Exception] :tries 10} (IOException.)))
(is (not (try-again? {:catch [IOException] :tries 10} (Exception.)))))
(testing "second, there has to be enough tries left"
(is (try-again? {:catch [Exception] :tries :unlimited} (IOException.)))
(is (try-again? {:catch [Exception] :tries 10} (IOException.)))
(is (try-again? {:catch [Exception] :tries 1} (IOException.)))
(is (not (try-again? {:catch [Exception] :tries 0} (IOException.))))
(is (not (try-again? {:catch [Exception] :tries -1} (IOException.))))))

(deftest test-update-tries
(testing "tries should decrease by 1, unless it is :unlimited"
(is (= {:tries 0} (update-tries {:tries 1})))
(is (= {:tries 1} (update-tries {:tries 2})))
(is (= {:tries :unlimited} (update-tries {:tries :unlimited})))))

(deftest test-update-sleep
(testing "sleep should update with the decay function..."
(is (= 2 (:sleep (update-sleep {:sleep 1 :decay 2}))))
(is (= 2 (:sleep (update-sleep {:sleep 2 :decay identity})))))
(testing "unless it is false or nil"
(is (= false (:sleep (update-sleep {:sleep false :decay 2}))))
(is (= nil (:sleep (update-sleep {:sleep nil :decay identity}))))))

(deftest test-retry
(testing "success returns a result"
(is (= 2 (retry default-options #(+ 1 1)))))
(testing "failure returns a fn"
(is (fn? (retry (assoc default-options :sleep nil) #(/ 1 0)))))
(testing "unless you have run out of tries"
(is (thrown? ArithmeticException
(retry (assoc default-options
:sleep nil
:tries 1)
#(/ 1 0))))))

(deftest test-try-try-again
(testing "ten tries to do the job"
(let [times (atom 0)]
(is (thrown? ArithmeticException
(try-try-again {:sleep nil
:tries 10}
#(do (swap! times inc)
(/ 1 0)))))
(is (= 10 @times)))))

0 comments on commit 5683430

Please sign in to comment.