Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 5683430
Showing
5 changed files
with
250 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
pom.xml | ||
*jar | ||
/lib/ | ||
/classes/ | ||
.lein-deps-sum |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
(defproject robert-bruce "0.5.0" | ||
:description "trampolining retries for clojure" | ||
:dependencies [[org.clojure/clojure "1.2.0"]]) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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)))) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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))))) |