-
Notifications
You must be signed in to change notification settings - Fork 725
/
Copy pathbank.clj
191 lines (174 loc) · 6.76 KB
/
bank.clj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
(ns jepsen.tests.bank
"Helper functions for doing bank tests, where you simulate transfers between
accounts, and verify that reads always show the same balance. The test map
should have these additional options:
:accounts A collection of account identifiers.
:total-amount Total amount to allocate.
:max-transfer The largest transfer we'll try to execute."
(:refer-clojure :exclude [read test])
(:require [clojure.core.reducers :as r]
[jepsen [checker :as checker]
[generator :as gen]
[history :as h]
[store :as store]
[util :as util]]
[jepsen.checker.perf :as perf]
[gnuplot.core :as g]))
(defn read
"A generator of read operations."
[_ _]
{:type :invoke, :f :read})
(defn transfer
"Generator of a transfer: a random amount between two randomly selected
accounts."
[test _]
{:type :invoke
:f :transfer
:value {:from (rand-nth (:accounts test))
:to (rand-nth (:accounts test))
:amount (+ 1 (rand-int (:max-transfer test)))}})
(def diff-transfer
"Transfers only between different accounts."
(gen/filter (fn [op] (not= (-> op :value :from)
(-> op :value :to)))
transfer))
(defn generator
"A mixture of reads and transfers for clients."
[]
(gen/mix [diff-transfer read]))
(defn err-badness
"Takes a bank error and returns a number, depending on its type. Bigger
numbers mean more egregious errors."
[test err]
(case (:type err)
:unexpected-key (count (:unexpected err))
:nil-balance (count (:nils err))
:wrong-total (Math/abs (float (/ (- (:total err) (:total-amount test))
(:total-amount test))))
:negative-value (- (reduce + (:negative err)))))
(defn check-op
"Takes a single op and returns errors in its balance"
[accts total negative-balances? op]
(let [ks (keys (:value op))
balances (vals (:value op))]
(cond (not-every? accts ks)
{:type :unexpected-key
:unexpected (remove accts ks)
:op op}
(some nil? balances)
{:type :nil-balance
:nils (->> (:value op)
(remove val)
(into {}))
:op op}
(not= total (reduce + balances))
{:type :wrong-total
:total (reduce + balances)
:op op}
(and (not negative-balances?) (some neg? balances))
{:type :negative-value
:negative (filter neg? balances)
:op op})))
(defn checker
"Verifies that all reads must sum to (:total test), and, unless
:negative-balances? is true, checks that all balances are
non-negative."
[checker-opts]
(reify checker/Checker
(check [this test history opts]
(let [accts (set (:accounts test))
total (:total-amount test)
reads (->> history
(h/filter (h/has-f? :read))
h/oks)
errors (->> reads
(r/map (partial check-op
accts
total
(:negative-balances? checker-opts)))
(r/filter identity)
(group-by :type))]
{:valid? (every? empty? (vals errors))
:read-count (count reads)
:error-count (reduce + (map count (vals errors)))
:first-error (util/min-by (comp :index :op) (map first (vals errors)))
:errors (->> errors
(map
(fn [[type errs]]
[type
(merge {:count (count errs)
:first (first errs)
:worst (util/max-by
(partial err-badness test)
errs)
:last (peek errs)}
(if (= type :wrong-total)
{:lowest (util/min-by :total errs)
:highest (util/max-by :total errs)}
{}))]))
(into {}))}))))
(defn ok-reads
"Filters a history to just OK reads. Returns nil if there are none."
[history]
(let [h (filter #(and (h/ok? %)
(= :read (:f %)))
history)]
(when (seq h)
(vec h))))
(defn by-node
"Groups operations by node."
[test history]
(let [nodes (:nodes test)
n (count nodes)]
(->> history
(r/filter (comp number? :process))
(group-by (fn [op]
(let [p (:process op)]
(nth nodes (mod p n))))))))
(defn points
"Turns a history into a seqeunce of [time total-of-accounts] points."
[history]
(mapv (fn [op]
[(util/nanos->secs (:time op))
(reduce + (remove nil? (vals (:value op))))])
history))
(defn plotter
"Renders a graph of balances over time"
[]
(reify checker/Checker
(check [this test history opts]
(when-let [reads (ok-reads history)]
(let [totals (->> reads
(by-node test)
(util/map-vals points))
colors (perf/qs->colors (keys totals))
path (.getCanonicalPath
(store/path! test (:subdirectory opts) "bank.png"))
preamble (concat (perf/preamble path)
[['set 'title (str (:name test) " bank")]
'[set ylabel "Total of all accounts"]])
series (for [[node data] totals]
{:title node
:with :points
:pointtype 2
:linetype (colors node)
:data data})]
(-> {:preamble preamble
:series series}
(perf/with-range)
(perf/with-nemeses history (:nemeses (:plot test)))
perf/plot!)
{:valid? true})))))
(defn test
"A partial test; bundles together some default choices for keys and amounts
with a generator and checker. Options:
:negative-balances? if true, doesn't verify that balances remain positive"
([]
(test {:negative-balances? false}))
([opts]
{:max-transfer 5
:total-amount 100
:accounts (vec (range 8))
:checker (checker/compose {:SI (checker opts)
:plot (plotter)})
:generator (generator)}))