Skip to content

Commit 8f60ae8

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. There are two kind of test: first one stores bank accounts in a single space and second one stores each bank account in a separate space. Original version of tests were used only SQL commands to manipulate accounts but `transfer` operation requires using transactions which are not supported in Tarantool [1]. So second version of tests has been added where sequences of SQL commands required atomicity were replaced by Lua functions `_WITHDRAW` and `_WITHDRAW_MULTITABLE`. 1. tarantool/tarantool-java#63 Closes #67
1 parent 7e9d44d commit 8f60ae8

File tree

4 files changed

+401
-2
lines changed

4 files changed

+401
-2
lines changed

resources/tarantool/jepsen.lua

+49
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,55 @@ box.schema.func.create('_LEADER',
103103
param_list = {},
104104
exports = {'LUA', 'SQL'},
105105
is_deterministic = true})
106+
107+
--[[ Function transfers money between two accounts presented by tuples in a table
108+
and returns true in case of success and false in other cases. ]]
109+
box.schema.func.create('_WITHDRAW',
110+
{language = 'LUA',
111+
returns = 'boolean',
112+
body = [[function(table, from, to, amount)
113+
local s = box.space[table]
114+
box.begin()
115+
local b1 = s:get(from)[2] - amount
116+
local b2 = s:get(to)[2] + amount
117+
if b1 < 0 or b2 < 0 then
118+
return false
119+
end
120+
s:update(from, {{'-', 2, amount}})
121+
s:update(to, {{'+', 2, amount}})
122+
box.commit()
123+
124+
return true
125+
end]],
126+
is_sandboxed = false,
127+
param_list = {'string', 'integer', 'integer', 'integer'},
128+
exports = {'LUA', 'SQL'},
129+
is_deterministic = true})
130+
131+
--[[ Function transfers money between two accounts presented by different tables
132+
and returns true in case of success and false in other cases. ]]
133+
box.schema.func.create('_WITHDRAW_MULTITABLE',
134+
{language = 'LUA',
135+
returns = 'boolean',
136+
body = [[function(table_from, table_to, amount)
137+
local space_from = box.space[table_from]
138+
local space_to = box.space[table_to]
139+
box.begin()
140+
local bal_from = space_from:get(0)[2] - amount
141+
local bal_to = space_to:get(0)[2] + amount
142+
if bal_from < 0 or bal_to < 0 then
143+
return false
144+
end
145+
space_from:update(0, {{'-', 2, amount}})
146+
space_to:update(0, {{'+', 2, amount}})
147+
box.commit()
148+
149+
return true
150+
end]],
151+
is_sandboxed = false,
152+
param_list = {'string', 'string', 'integer'},
153+
exports = {'LUA', 'SQL'},
154+
is_deterministic = true})
106155
end
107156

108157
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)