Skip to content

Commit 6845684

Browse files
committed
Add bank workload
In the bank test, we create a pool of simulated bank accounts, and transfer money between them using transactions which read two randomly selected accounts, subtract and increment their balances accordingly, and write the new account values back. Under snapshot isolation, the total of all accounts should be constant over time. We read the state of all accounts concurrently, and check for changes in the total, which suggests read skew or other snapshot isolation anomalies. Closes #67
1 parent f411d3b commit 6845684

File tree

2 files changed

+179
-1
lines changed

2 files changed

+179
-1
lines changed

src/tarantool/bank.clj

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
(ns tarantool.bank
2+
"Simulates transfers between bank accounts."
3+
(:require [clojure.tools.logging :refer [info warn]]
4+
[clojure.string :as str]
5+
[clojure.core.reducers :as r]
6+
[jepsen [cli :as cli]
7+
[client :as client]
8+
[checker :as checker]
9+
;[core :as jepsen]
10+
[control :as c]
11+
;[independent :as independent]
12+
[generator :as gen]]
13+
;[util :refer [timeout meh]]]
14+
[jepsen.tests.bank :as bank]
15+
[next.jdbc :as j]
16+
[next.jdbc.sql :as sql]
17+
[knossos.op :as op]
18+
[jepsen.checker.timeline :as timeline]
19+
[tarantool [client :as cl]
20+
[db :as db]]))
21+
22+
(def table-name "accounts")
23+
24+
(defrecord BankClient [conn]
25+
client/Client
26+
27+
(open! [this test node]
28+
(let [conn (cl/open node test)]
29+
(assoc this :conn conn :node node)))
30+
31+
(setup! [this test node]
32+
(locking BankClient
33+
(let [conn (cl/open node test)]
34+
(Thread/sleep 10000) ; wait for leader election and joining to a cluster
35+
(when (= node (first (db/primaries test)))
36+
(cl/with-conn-failure-retry conn
37+
(info (str "Creating table " table-name))
38+
(j/execute! conn [(str "CREATE TABLE IF NOT EXISTS " table-name
39+
"(id INT NOT NULL PRIMARY KEY,
40+
balance INT NOT NULL)")])
41+
(doseq [a (:accounts test)]
42+
(info "Populating account")
43+
(sql/insert! conn table-name {:id a
44+
:balance (if (= a (first (:accounts test)))
45+
100
46+
0)}))))
47+
(assoc this :conn conn :node node))))
48+
49+
(invoke! [this test op]
50+
;(with-txn op [c conn]
51+
(try
52+
(case (:f op)
53+
:read (->> (sql/query conn [(str "SELECT * FROM " table-name)])
54+
(map (juxt :ID :BALANCE))
55+
(into (sorted-map))
56+
(assoc op :type :ok, :value))
57+
58+
:transfer
59+
(let [{:keys [from to amount]} (:value op)
60+
con (cl/open (first (db/primaries test)) test)
61+
b1 (-> con
62+
(sql/query [(str "SELECT * FROM " table-name " WHERE id = ? ") from])
63+
first
64+
:BALANCE
65+
(- amount))
66+
b2 (-> con
67+
(sql/query [(str "SELECT * FROM " table-name " WHERE id = ? ") to])
68+
first
69+
:BALANCE
70+
(+ amount))]
71+
(cond (neg? b1)
72+
(assoc op :type :fail, :value [from b1])
73+
(neg? b2)
74+
(assoc op :type :fail, :value [to b2])
75+
true
76+
(do (j/execute! con ["UPDATE " table-name " SET balance = balance - ? WHERE id = ?" amount from])
77+
(j/execute! con ["UPDATE " table-name " SET balance = balance + ? WHERE id = ?" amount to])
78+
(assoc op :type :ok)))))))
79+
80+
(teardown! [_ test]
81+
(when-not (:leave-db-running? test)
82+
(info (str "Drop table" table-name))
83+
(cl/with-conn-failure-retry conn
84+
(j/execute! conn [(str "DROP TABLE IF EXISTS " table-name)]))))
85+
86+
(close! [_ test]))
87+
88+
(defn workload
89+
[opts]
90+
(assoc (bank/test opts)
91+
:client (BankClient. nil)))
92+
93+
; One bank account per table
94+
(defrecord MultiBankClient [conn tbl-created?]
95+
client/Client
96+
(open! [this test node]
97+
(assoc this :conn (cl/open node test)))
98+
99+
(setup! [this test node]
100+
(locking tbl-created?
101+
(let [conn (cl/open node test)]
102+
(Thread/sleep 10000) ; wait for leader election and joining to a cluster
103+
(when (= node (first (db/primaries test)))
104+
(when (compare-and-set! tbl-created? false true)
105+
(cl/with-conn-failure-retry conn
106+
(doseq [a (:accounts test)]
107+
(info "Creating table" table-name a)
108+
(j/execute! conn [(str "CREATE TABLE IF NOT EXISTS " table-name a
109+
"(id INT NOT NULL PRIMARY KEY,"
110+
"balance INT NOT NULL)")])
111+
(try
112+
(info "Populating account" a)
113+
(sql/insert! conn (str table-name a)
114+
{:id 0
115+
:balance (if (= a (first (:accounts test)))
116+
(:total-amount test)
117+
0)})
118+
(catch java.sql.SQLIntegrityConstraintViolationException e
119+
nil))))))
120+
(assoc this :conn conn :node node))))
121+
122+
(invoke! [this test op]
123+
;(with-txn op [c conn]
124+
(try
125+
(case (:f op)
126+
:read
127+
(->> (:accounts test)
128+
(map (fn [x]
129+
[x (->> (sql/query conn [(str "SELECT balance FROM " table-name
130+
x)]
131+
{:row-fn :BALANCE})
132+
first)]))
133+
(into (sorted-map))
134+
(assoc op :type :ok, :value))
135+
136+
:transfer
137+
(let [{:keys [from to amount]} (:value op)
138+
from (str table-name from)
139+
to (str table-name to)
140+
con (cl/open (first (db/primaries test)) test)
141+
b1 (-> con
142+
(sql/query [(str "SELECT balance FROM " from)])
143+
first
144+
:BALANCE
145+
(- amount))
146+
b2 (-> con
147+
(sql/query [(str "SELECT balance FROM " to)])
148+
first
149+
:BALANCE
150+
(+ amount))]
151+
(cond (neg? b1)
152+
(assoc op :type :fail, :error [:negative from b1])
153+
(neg? b2)
154+
(assoc op :type :fail, :error [:negative to b2])
155+
true
156+
(do (j/execute! con [(str "UPDATE " from " SET balance = balance - ? WHERE id = 0") amount])
157+
(j/execute! con [(str "UPDATE " to " SET balance = balance + ? WHERE id = 0") amount])
158+
(assoc op :type :ok)))))))
159+
160+
(teardown! [_ test]
161+
(when-not (:leave-db-running? test)
162+
(cl/with-conn-failure-retry conn
163+
(doseq [a (:accounts test)]
164+
(info "Drop table" table-name a)
165+
(j/execute! conn [(str "DROP TABLE IF EXISTS " table-name a)])))))
166+
167+
(close! [_ test]))
168+
169+
(defn multitable-workload
170+
[opts]
171+
(assoc (workload opts)
172+
:client (MultiBankClient. nil (atom false))))

src/tarantool/runner.clj

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
[jepsen.checker.timeline :as timeline]
1818
[jepsen.os.ubuntu :as ubuntu]
1919
[tarantool [db :as db]
20+
[bank :as bank]
2021
[errcode :as err]
2122
[nemesis :as nemesis]
2223
[register :as register]
@@ -39,7 +40,9 @@
3940
4041
Or, for some special cases where nemeses and workloads are coupled, we return
4142
a keyword here instead."
42-
{:set sets/workload
43+
{:bank bank/workload
44+
:bank-multitable bank/multitable-workload
45+
:set sets/workload
4346
:counter-inc counter/workload-inc
4447
:register register/workload})
4548

@@ -180,6 +183,9 @@
180183
:db (db/db (:version opts))
181184
:engine (:engine opts)
182185
:mvcc (:mvcc opts)
186+
:accounts (vec (range 10)) ; bank-specific option
187+
:max-transfer 10 ; bank-specific option$
188+
:total-amount 100 ; bank-specific option
183189
:pure-generators true
184190
:concurrency (if (and (< (:concurrency opts) minimal-concurrency)
185191
(= (:workload opts) :register))

0 commit comments

Comments
 (0)