Skip to content

Commit 8f1afbf

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. BIGINT support Interactive transactions in IPROTO #2016 tarantool/tarantool-java#63 Add bank tests with Lua functions Closes #67
1 parent f411d3b commit 8f1afbf

File tree

4 files changed

+421
-2
lines changed

4 files changed

+421
-2
lines changed

resources/tarantool/jepsen.lua

+45
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,51 @@ box.schema.func.create('_LEADER',
103103
param_list = {},
104104
exports = {'LUA', 'SQL'},
105105
is_deterministic = true})
106+
107+
box.schema.func.create('_WITHDRAW',
108+
{language = 'LUA',
109+
returns = 'boolean',
110+
body = [[function(table, from, to, amount)
111+
local s = box.space[table]
112+
box.begin()
113+
local b1 = s:get(from)[2] - amount
114+
local b2 = s:get(to)[2] + amount
115+
if b1 < 0 or b2 < 0 then
116+
return false
117+
end
118+
s:update(from, {{'-', 2, amount}})
119+
s:update(to, {{'+', 2, amount}})
120+
box.commit()
121+
122+
return true
123+
end]],
124+
is_sandboxed = false,
125+
param_list = {'string', 'integer', 'integer', 'integer'},
126+
exports = {'LUA', 'SQL'},
127+
is_deterministic = true})
128+
129+
box.schema.func.create('_WITHDRAW_MULTITABLE',
130+
{language = 'LUA',
131+
returns = 'boolean',
132+
body = [[function(table_from, table_to, amount)
133+
local space_from = box.space[table_from]
134+
local space_to = box.space[table_to]
135+
box.begin()
136+
local bal_from = space_from:get(0)[2] - amount
137+
local bal_to = space_to:get(0)[2] + amount
138+
if bal_from < 0 or bal_to < 0 then
139+
return false
140+
end
141+
space_from:update(0, {{'-', 2, amount}})
142+
space_to:update(0, {{'+', 2, amount}})
143+
box.commit()
144+
145+
return true
146+
end]],
147+
is_sandboxed = false,
148+
param_list = {'string', 'string', 'integer'},
149+
exports = {'LUA', 'SQL'},
150+
is_deterministic = true})
106151
end
107152

108153
box.once('jepsen', bootstrap)

src/tarantool/bank.clj

+296
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
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+
[control :as c]
10+
[generator :as gen]]
11+
[jepsen.tests.bank :as bank]
12+
[next.jdbc :as j]
13+
[next.jdbc.sql :as sql]
14+
[knossos.op :as op]
15+
[jepsen.checker.timeline :as timeline]
16+
[tarantool [client :as cl]
17+
[db :as db]]))
18+
19+
(def table-name "accounts")
20+
21+
(defrecord BankClientWithLua [conn]
22+
client/Client
23+
24+
(open! [this test node]
25+
(let [conn (cl/open node test)]
26+
(assoc this :conn conn :node node)))
27+
28+
(setup! [this test node]
29+
(locking BankClientWithLua
30+
(let [conn (cl/open node test)]
31+
(Thread/sleep 10000) ; wait for leader election and joining to a cluster
32+
(when (= node (first (db/primaries test)))
33+
(cl/with-conn-failure-retry conn
34+
(info (str "Creating table " table-name))
35+
(j/execute! conn [(str "CREATE TABLE IF NOT EXISTS " table-name
36+
"(id INT NOT NULL PRIMARY KEY,
37+
balance INT NOT NULL)")])
38+
(doseq [a (:accounts test)]
39+
(info "Populating account")
40+
(sql/insert! conn table-name {:id a
41+
:balance (if (= a (first (:accounts test)))
42+
(:total-amount test)
43+
0)}))))
44+
(assoc this :conn conn :node node))))
45+
46+
(invoke! [this test op]
47+
(try
48+
(case (:f op)
49+
:read (->> (sql/query conn [(str "SELECT * FROM " table-name)])
50+
(map (juxt :ID :BALANCE))
51+
(into (sorted-map))
52+
(assoc op :type :ok, :value))
53+
54+
:transfer
55+
(let [{:keys [from to amount]} (:value op)
56+
con (cl/open (first (db/primaries test)) test)
57+
table (clojure.string/upper-case table-name)
58+
r (-> con
59+
(sql/query [(str "SELECT _WITHDRAW('" table "'," from "," to "," amount ")")])
60+
first
61+
:COLUMN_1)]
62+
(if (false? r)
63+
(assoc op :type :fail, :value {:from from :to to :amount amount})
64+
(assoc op :type :ok))))))
65+
66+
(teardown! [_ test]
67+
(when-not (:leave-db-running? test)
68+
(info (str "Drop table" table-name))
69+
(cl/with-conn-failure-retry conn
70+
(j/execute! conn [(str "DROP TABLE IF EXISTS " table-name)]))))
71+
72+
(close! [_ test]))
73+
74+
; One bank account per table
75+
(defrecord MultiBankClientWithLua [conn tbl-created?]
76+
client/Client
77+
(open! [this test node]
78+
(assoc this :conn (cl/open node test)))
79+
80+
(setup! [this test node]
81+
(locking tbl-created?
82+
(let [conn (cl/open node test)]
83+
(Thread/sleep 10000) ; wait for leader election and joining to a cluster
84+
(when (= node (first (db/primaries test)))
85+
(when (compare-and-set! tbl-created? false true)
86+
(cl/with-conn-failure-retry conn
87+
(doseq [a (:accounts test)]
88+
(info "Creating table" table-name a)
89+
(j/execute! conn [(str "CREATE TABLE IF NOT EXISTS " table-name a
90+
"(id INT NOT NULL PRIMARY KEY,"
91+
"balance INT NOT NULL)")])
92+
(info "Populating account" a)
93+
(sql/insert! conn (str table-name a)
94+
{:id 0
95+
:balance (if (= a (first (:accounts test)))
96+
(:total-amount test)
97+
0)})))))
98+
(assoc this :conn conn :node node))))
99+
100+
(invoke! [this test op]
101+
(try
102+
(case (:f op)
103+
:read
104+
(->> (:accounts test)
105+
(map (fn [x]
106+
[x (->> (sql/query conn [(str "SELECT balance FROM " table-name
107+
x)]
108+
{:row-fn :BALANCE})
109+
first)]))
110+
(into (sorted-map))
111+
(map (fn [[k {b :BALANCE}]] [k b]))
112+
(into {})
113+
(assoc op :type :ok, :value))
114+
115+
:transfer
116+
(let [{:keys [from to amount]} (:value op)
117+
from (str table-name from)
118+
to (str table-name to)
119+
con (cl/open (first (db/primaries test)) test)
120+
from_uppercase (clojure.string/upper-case from)
121+
to_uppercase (clojure.string/upper-case to)
122+
r (-> con
123+
(sql/query [(str "SELECT _WITHDRAW_MULTITABLE('" from_uppercase "','" to_uppercase "'," amount ")")])
124+
first
125+
:COLUMN_1)]
126+
(if (false? r)
127+
(assoc op :type :fail)
128+
(assoc op :type :ok))))))
129+
130+
(teardown! [_ test]
131+
(when-not (:leave-db-running? test)
132+
(cl/with-conn-failure-retry conn
133+
(doseq [a (:accounts test)]
134+
(info "Drop table" table-name a)
135+
(j/execute! conn [(str "DROP TABLE IF EXISTS " table-name a)])))))
136+
137+
(close! [_ test]))
138+
139+
(defrecord BankClient [conn]
140+
client/Client
141+
142+
(open! [this test node]
143+
(let [conn (cl/open node test)]
144+
(assoc this :conn conn :node node)))
145+
146+
(setup! [this test node]
147+
(locking BankClient
148+
(let [conn (cl/open node test)]
149+
(Thread/sleep 10000) ; wait for leader election and joining to a cluster
150+
(when (= node (first (db/primaries test)))
151+
(cl/with-conn-failure-retry conn
152+
(info (str "Creating table " table-name))
153+
(j/execute! conn [(str "CREATE TABLE IF NOT EXISTS " table-name
154+
"(id INT NOT NULL PRIMARY KEY,
155+
balance INT NOT NULL)")])
156+
(doseq [a (:accounts test)]
157+
(info "Populating account")
158+
(sql/insert! conn table-name {:id a
159+
:balance (if (= a (first (:accounts test)))
160+
(:total-amount test)
161+
0)}))))
162+
(assoc this :conn conn :node node))))
163+
164+
(invoke! [this test op]
165+
(try
166+
(case (:f op)
167+
:read (->> (sql/query conn [(str "SELECT * FROM " table-name)])
168+
(map (juxt :ID :BALANCE))
169+
(into (sorted-map))
170+
(assoc op :type :ok, :value))
171+
172+
:transfer
173+
; TODO: with-transaction is not supported due to
174+
; https://github.com/tarantool/tarantool/issues/2016
175+
(let [{:keys [from to amount]} (:value op)
176+
con (cl/open (first (db/primaries test)) test)
177+
b1 (-> con
178+
(sql/query [(str "SELECT * FROM " table-name " WHERE id = ? ") from])
179+
first
180+
:BALANCE
181+
(- amount))
182+
b2 (-> con
183+
(sql/query [(str "SELECT * FROM " table-name " WHERE id = ? ") to])
184+
first
185+
:BALANCE
186+
(+ amount))]
187+
(cond (or (neg? b1) (neg? b2))
188+
(assoc op :type :fail, :value {:from from :to to :amount amount})
189+
true
190+
(do (j/execute! con [(str "UPDATE " table-name " SET balance = balance - ? WHERE id = ?") amount from])
191+
(j/execute! con [(str "UPDATE " table-name " SET balance = balance + ? WHERE id = ?") amount to])
192+
(assoc op :type :ok)))))))
193+
194+
(teardown! [_ test]
195+
(when-not (:leave-db-running? test)
196+
(info (str "Drop table" table-name))
197+
(cl/with-conn-failure-retry conn
198+
(j/execute! conn [(str "DROP TABLE IF EXISTS " table-name)]))))
199+
200+
(close! [_ test]))
201+
202+
; One bank account per table
203+
(defrecord MultiBankClient [conn tbl-created?]
204+
client/Client
205+
(open! [this test node]
206+
(assoc this :conn (cl/open node test)))
207+
208+
(setup! [this test node]
209+
(locking tbl-created?
210+
(let [conn (cl/open node test)]
211+
(Thread/sleep 10000) ; wait for leader election and joining to a cluster
212+
(when (= node (first (db/primaries test)))
213+
(when (compare-and-set! tbl-created? false true)
214+
(cl/with-conn-failure-retry conn
215+
(doseq [a (:accounts test)]
216+
(info "Creating table" table-name a)
217+
(j/execute! conn [(str "CREATE TABLE IF NOT EXISTS " table-name a
218+
"(id INT NOT NULL PRIMARY KEY,"
219+
"balance INT NOT NULL)")])
220+
(info "Populating account" a)
221+
(sql/insert! conn (str table-name a)
222+
{:id 0
223+
:balance (if (= a (first (:accounts test)))
224+
(:total-amount test)
225+
0)})))))
226+
(assoc this :conn conn :node node))))
227+
228+
(invoke! [this test op]
229+
(try
230+
(case (:f op)
231+
:read
232+
(->> (:accounts test)
233+
(map (fn [x]
234+
[x (->> (sql/query conn [(str "SELECT balance FROM " table-name
235+
x)]
236+
{:row-fn :BALANCE})
237+
first)]))
238+
(into (sorted-map))
239+
(map (fn [[k {b :BALANCE}]] [k b]))
240+
(into {})
241+
(assoc op :type :ok, :value))
242+
243+
:transfer
244+
; TODO: with-transaction is not supported due to
245+
; https://github.com/tarantool/tarantool/issues/2016
246+
(let [{:keys [from to amount]} (:value op)
247+
from (str table-name from)
248+
to (str table-name to)
249+
con (cl/open (first (db/primaries test)) test)
250+
b1 (-> con
251+
(sql/query [(str "SELECT balance FROM " from)])
252+
first
253+
:BALANCE
254+
(- amount))
255+
b2 (-> con
256+
(sql/query [(str "SELECT balance FROM " to)])
257+
first
258+
:BALANCE
259+
(+ amount))]
260+
(cond (neg? b1)
261+
(assoc op :type :fail, :error [:negative from b1])
262+
(neg? b2)
263+
(assoc op :type :fail, :error [:negative to b2])
264+
true
265+
(do (j/execute! con [(str "UPDATE " from " SET balance = balance - ? WHERE id = 0") amount])
266+
(j/execute! con [(str "UPDATE " to " SET balance = balance + ? WHERE id = 0") amount])
267+
(assoc op :type :ok)))))))
268+
269+
(teardown! [_ test]
270+
(when-not (:leave-db-running? test)
271+
(cl/with-conn-failure-retry conn
272+
(doseq [a (:accounts test)]
273+
(info "Drop table" table-name a)
274+
(j/execute! conn [(str "DROP TABLE IF EXISTS " table-name a)])))))
275+
276+
(close! [_ test]))
277+
278+
(defn workload
279+
[opts]
280+
(assoc (bank/test opts)
281+
:client (BankClient. nil)))
282+
283+
(defn multitable-workload
284+
[opts]
285+
(assoc (workload opts)
286+
:client (MultiBankClient. nil (atom false))))
287+
288+
(defn workload-lua
289+
[opts]
290+
(assoc (workload opts)
291+
:client (BankClientWithLua. nil)))
292+
293+
(defn multitable-workload-lua
294+
[opts]
295+
(assoc (workload opts)
296+
:client (MultiBankClientWithLua. nil (atom false))))

0 commit comments

Comments
 (0)