Skip to content
This repository was archived by the owner on Sep 18, 2019. It is now read-only.

Commit 7b2d205

Browse files
author
Joe Wass
committed
Add backoff namespace and functions.
1 parent a856e3f commit 7b2d205

File tree

3 files changed

+201
-1
lines changed

3 files changed

+201
-1
lines changed

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Common components for various services of Event Data. Tests run in Docker, but t
44

55
## To use
66

7-
[event-data-common "0.1.6"]
7+
[event-data-common "0.1.8"]
88

99
## Components
1010

@@ -32,6 +32,10 @@ Send updates to the Status Service.
3232

3333
Various date and time functions, mostly connected to archiving.
3434

35+
### Backoff
36+
37+
Try and re-try functions in a threadpool. For robust connection to external systems.
38+
3539
## Testing
3640

3741
Unit tests:

src/event_data_common/backoff.clj

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
(ns event-data-common.backoff
2+
"Try a function, with square backoff.
3+
Uses a threadpool, so can run lots of concurrent things at once."
4+
(:require [clojure.core.async :as async]
5+
[overtone.at-at :as at-at]))
6+
7+
(def pool (at-at/mk-pool))
8+
9+
(defn try-backoff
10+
"Try a function. If it throws an exception, back off.
11+
The exception is passed to error-f. If it returns true, keep trying.
12+
If errors continue to max-attempts, call terminate-f.
13+
Finally-f is called on finish, on success or failure.
14+
15+
Back-off starts with sleep-ms milliseconds and doubles each time."
16+
; Don't use try-try-again because it blocks the thread.
17+
[f sleep-ms max-attempts error-f terminate-f finally-f]
18+
; Recurse until no more attempts.
19+
(if (zero? max-attempts)
20+
(do
21+
(terminate-f)
22+
(finally-f))
23+
(try
24+
(f)
25+
; If an exception is raised, finally-f isn't called this time round.
26+
(finally-f)
27+
(catch Exception e
28+
(do
29+
(error-f e)
30+
(at-at/after sleep-ms
31+
#(try-backoff f (* 2 sleep-ms) (dec max-attempts) error-f terminate-f finally-f)
32+
pool))))))
33+
34+
(defn get-pool-size
35+
[]
36+
(count (at-at/scheduled-jobs pool)))
37+
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
(ns event-data-common.backoff-tests
2+
(:require [clojure.test :refer :all]
3+
[event-data-common.backoff :as backoff]
4+
[clojure.core.async :as async]))
5+
6+
; Because try-backoff executes asynchronously, we need to pause the test until the right behaviour has taken place.
7+
; Monitors are implemented as promises which are delivered by callbacks when the expected target has been reached.
8+
9+
(deftest ^:unit immediate-success
10+
(let [f-counter (atom 0)
11+
error-counter (atom 0)
12+
terminate-counter (atom 0)
13+
finally-counter (atom 0)
14+
15+
finally-called (promise)
16+
17+
f (fn [] (swap! f-counter inc))
18+
19+
error-f (fn [ex] (swap! error-counter inc))
20+
terminate-f (fn [] (swap! terminate-counter inc))
21+
finally-f (fn [] (swap! finally-counter inc)
22+
(deliver finally-called 1))]
23+
24+
(testing "Callbacks should be called the correct number of times on immediate success."
25+
; Run a function that works first time.
26+
; Zero timer because we don't care about timing, but do want fast tests.
27+
(backoff/try-backoff f 0 5 error-f terminate-f finally-f)
28+
29+
; Wait for finally to be called.
30+
; @finally-called
31+
32+
(is (= 1 @f-counter) "f should have been called once if it succeeded.")
33+
(is (= 0 @error-counter) "error should not be called on success")
34+
(is (= 0 @terminate-counter) "terminate should not be called on success")
35+
(is (= 1 @finally-counter) "finally should always be called once"))))
36+
37+
(deftest ^:unit initial-failure
38+
(let [f-counter (atom 0)
39+
error-counter (atom 0)
40+
terminate-counter (atom 0)
41+
finally-counter (atom 0)
42+
43+
finally-called (promise)
44+
45+
f (fn []
46+
; Exceptions 3 times, then OK.
47+
(if (<= (swap! f-counter inc) 3)
48+
(throw (new Exception "No running in pool area!"))
49+
:everthing-is-fine))
50+
51+
error-f (fn [ex] (swap! error-counter inc))
52+
terminate-f (fn [] (swap! terminate-counter inc))
53+
finally-f (fn [] (swap! finally-counter inc)
54+
(deliver finally-called 1))]
55+
56+
(testing "Callbacks should be called the correct number of times on immediate failure but eventual success."
57+
; Run a function that works first time.
58+
; Zero timer because we don't care about timing, but do want fast tests.
59+
(backoff/try-backoff f 0 5 error-f terminate-f finally-f)
60+
61+
; Wait for finally to be called.
62+
@finally-called
63+
64+
(is (= 4 @f-counter) "f should have been called every time until it succeeds")
65+
(is (= 3 @error-counter) "error should have been called every error")
66+
(is (= 0 @terminate-counter) "terminate should not be called on eventual success")
67+
(is (= 1 @finally-counter) "finally should always be called once"))))
68+
69+
(deftest ^:unit ultimate-failure
70+
(let [f-counter (atom 0)
71+
error-counter (atom 0)
72+
terminate-counter (atom 0)
73+
finally-counter (atom 0)
74+
75+
finally-called (promise)
76+
77+
f (fn []
78+
(swap! f-counter inc)
79+
(throw (new Exception "It'll never work.")))
80+
81+
error-f (fn [ex] (swap! error-counter inc))
82+
terminate-f (fn [] (swap! terminate-counter inc))
83+
finally-f (fn [] (swap! finally-counter inc)
84+
(deliver finally-called 1))]
85+
86+
(testing "Callbacks should be called the correct number of times on continued failure."
87+
; Run a function that works first time.
88+
; Zero timer because we don't care about timing, but do want fast tests.
89+
(backoff/try-backoff f 0 5 error-f terminate-f finally-f)
90+
91+
; Wait for finally to be called.
92+
@finally-called
93+
94+
(is (= 5 @f-counter) "f should have been called every time until it succeeds")
95+
(is (= 5 @error-counter) "error should have been called every error")
96+
(is (= 1 @terminate-counter) "terminate should not be called on eventual success")
97+
(is (= 1 @finally-counter) "finally should always be called once"))))
98+
99+
(deftest ^:unit can-run-concurrently
100+
(let [f-counter-1 (atom 0)
101+
error-counter-1 (atom 0)
102+
terminate-counter-1 (atom 0)
103+
finally-counter-1 (atom 0)
104+
105+
f-counter-2 (atom 0)
106+
error-counter-2 (atom 0)
107+
terminate-counter-2 (atom 0)
108+
finally-counter-2 (atom 0)
109+
110+
finally-called-1 (promise)
111+
finally-called-2 (promise)
112+
113+
snoop-f-counter-1 (atom nil)
114+
115+
; First one errors three times then succeeds.
116+
f-1 (fn []
117+
(condp = (swap! f-counter-1 inc)
118+
0 (throw (new Exception ""))
119+
1 (throw (new Exception ""))
120+
3 (throw (new Exception ""))
121+
4 :ok))
122+
123+
; Second one runs at the same time, but on the third attempt looks at the counter for
124+
; the first, to demonstrate that it is running at the same time.
125+
f-2 (fn []
126+
(condp = (swap! f-counter-2 inc)
127+
0 (throw (new Exception ""))
128+
1 (throw (new Exception ""))
129+
; On the third attempt, save a copy of the counter for the first function.
130+
2 (do (reset! snoop-f-counter-1 @f-counter-1)
131+
(throw (new Exception "")))
132+
3 (throw (new Exception ""))
133+
4 :ok))
134+
135+
error-f-1 (fn [ex] (swap! error-counter-1 inc))
136+
terminate-f-1 (fn [] (swap! terminate-counter-1 inc))
137+
finally-f-1 (fn [] (swap! finally-counter-1 inc)
138+
(deliver finally-called-1 1))
139+
140+
141+
error-f-2 (fn [ex] (swap! error-counter-2 inc))
142+
terminate-f-2 (fn [] (swap! terminate-counter-2 inc))
143+
finally-f-2 (fn [] (swap! finally-counter-2 inc)
144+
(deliver finally-called-2 1))
145+
146+
147+
]
148+
149+
(testing "Functions should run concurrently."
150+
; Start the second one after the first.
151+
; 100 milliseconds is enought to be confident for tests.
152+
(backoff/try-backoff f-1 100 5 error-f-1 terminate-f-1 finally-f-1)
153+
(backoff/try-backoff f-2 100 5 error-f-2 terminate-f-2 finally-f-2)
154+
155+
; Only wait for the first one to finish.
156+
@finally-called-1
157+
158+
(is (= 1 @finally-counter-1) "finally have been called for first")
159+
(is (> @snoop-f-counter-1 0) "first counter should have been incremented beyond zero by the time the second failed on a subsequent attempt"))))

0 commit comments

Comments
 (0)