transaction_tests.erl 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. %% MySQL/OTP – MySQL client library for Erlang/OTP
  2. %% Copyright (C) 2014 Viktor Söderqvist
  3. %%
  4. %% This file is part of MySQL/OTP.
  5. %%
  6. %% MySQL/OTP is free software: you can redistribute it and/or modify it under
  7. %% the terms of the GNU Lesser General Public License as published by the Free
  8. %% Software Foundation, either version 3 of the License, or (at your option)
  9. %% any later version.
  10. %%
  11. %% This program is distributed in the hope that it will be useful, but WITHOUT
  12. %% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13. %% FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
  14. %% more details.
  15. %%
  16. %% You should have received a copy of the GNU Lesser General Public License
  17. %% along with this program. If not, see <https://www.gnu.org/licenses/>.
  18. %% @doc This module performs test to an actual database.
  19. -module(transaction_tests).
  20. -include_lib("eunit/include/eunit.hrl").
  21. -define(user, "otptest").
  22. -define(password, "otptest").
  23. single_connection_test_() ->
  24. {setup,
  25. fun () ->
  26. {ok, Pid} = mysql:start_link([{user, ?user}, {password, ?password},
  27. {query_cache_time, 50},
  28. {log_warnings, false}]),
  29. ok = mysql:query(Pid, <<"DROP DATABASE IF EXISTS otptest">>),
  30. ok = mysql:query(Pid, <<"CREATE DATABASE otptest">>),
  31. ok = mysql:query(Pid, <<"USE otptest">>),
  32. ok = mysql:query(Pid, <<"CREATE TABLE foo (bar INT) engine=InnoDB">>),
  33. Pid
  34. end,
  35. fun (Pid) ->
  36. ok = mysql:query(Pid, <<"DROP DATABASE otptest">>),
  37. exit(Pid, normal)
  38. end,
  39. fun (Pid) ->
  40. [{"Simple atomic", fun () -> simple_atomic(Pid) end},
  41. {"Simple aborted", fun () -> simple_aborted(Pid) end},
  42. {"Nested atomic", fun () -> nested_atomic(Pid) end},
  43. {"Nested inner aborted", fun () -> nested_inner_aborted(Pid) end},
  44. {"Implicit commit", fun () -> implicit_commit(Pid) end}]
  45. end}.
  46. application_process_kill_test() ->
  47. {ok, Pid} = mysql:start_link([
  48. {user, ?user},
  49. {password, ?password},
  50. {query_cache_time, 50},
  51. {log_warnings, false}
  52. ]),
  53. unlink(Pid),
  54. Mref = erlang:monitor(process, Pid),
  55. ok = mysql:query(Pid, <<"DROP DATABASE IF EXISTS otptest">>),
  56. ok = mysql:query(Pid, <<"CREATE DATABASE otptest">>),
  57. ok = mysql:query(Pid, <<"USE otptest">>),
  58. ok = mysql:query(Pid, <<"CREATE TABLE foo (bar INT) engine=InnoDB">>),
  59. ?assertNot(mysql:in_transaction(Pid)),
  60. ?assert(is_process_alive(Pid)),
  61. Self = self(),
  62. AppPid = spawn(fun() ->
  63. mysql:transaction(Pid, fun () ->
  64. ok = mysql:query(Pid, "INSERT INTO foo (bar) VALUES (42)"),
  65. Self! killme,
  66. receive after 10000 -> throw(too_long) end,
  67. ok
  68. end)
  69. end),
  70. receive killme -> exit(AppPid, kill) end,
  71. receive
  72. {'DOWN', Mref, process, Pid, {application_process_died, AppPid}}->
  73. ok
  74. after 10000 ->
  75. throw(too_long)
  76. end,
  77. ?assertNot(is_process_alive(Pid)),
  78. {ok, Pid2} = mysql:start_link([
  79. {user, ?user},
  80. {password, ?password},
  81. {query_cache_time, 50},
  82. {log_warnings, false}
  83. ]),
  84. ok = mysql:query(Pid2, <<"USE otptest">>),
  85. ?assertMatch({ok, _, []}, mysql:query(Pid2, <<"SELECT * from foo where bar = 42">>)),
  86. ok = mysql:query(Pid2, <<"DROP DATABASE otptest">>),
  87. exit(Pid2, normal).
  88. simple_atomic(Pid) ->
  89. ?assertNot(mysql:in_transaction(Pid)),
  90. Result = mysql:transaction(Pid, fun () ->
  91. ok = mysql:query(Pid, "INSERT INTO foo (bar) VALUES (42)"),
  92. ?assert(mysql:in_transaction(Pid)),
  93. hello
  94. end),
  95. ?assertEqual({atomic, hello}, Result),
  96. ?assertNot(mysql:in_transaction(Pid)),
  97. ok = mysql:query(Pid, "DELETE FROM foo").
  98. simple_aborted(Pid) ->
  99. ok = mysql:query(Pid, "INSERT INTO foo VALUES (9)"),
  100. ?assertEqual({ok, [<<"bar">>], [[9]]},
  101. mysql:query(Pid, "SELECT bar FROM foo")),
  102. Result = mysql:transaction(Pid, fun () ->
  103. ok = mysql:query(Pid, "INSERT INTO foo VALUES (42)"),
  104. ?assertMatch({ok, _, [[2]]},
  105. mysql:query(Pid, "SELECT COUNT(*) FROM foo")),
  106. error(hello)
  107. end),
  108. ?assertMatch({aborted, {hello, Stacktrace}} when is_list(Stacktrace),
  109. Result),
  110. ?assertEqual({ok, [<<"bar">>], [[9]]},
  111. mysql:query(Pid, "SELECT bar FROM foo")),
  112. ok = mysql:query(Pid, "DELETE FROM foo"),
  113. %% Also check the abort Reason for throw and exit.
  114. ?assertEqual({aborted, {throw, foo}},
  115. mysql:transaction(Pid, fun () -> throw(foo) end)),
  116. ?assertEqual({aborted, foo},
  117. mysql:transaction(Pid, fun () -> exit(foo) end)).
  118. nested_atomic(Pid) ->
  119. OuterResult = mysql:transaction(Pid, fun () ->
  120. ok = mysql:query(Pid, "INSERT INTO foo VALUES (9)"),
  121. InnerResult = mysql:transaction(Pid, fun () ->
  122. ok = mysql:query(Pid, "INSERT INTO foo VALUES (42)"),
  123. inner
  124. end),
  125. ?assertEqual({atomic, inner}, InnerResult),
  126. outer
  127. end),
  128. ?assertMatch({ok, _, [[2]]}, mysql:query(Pid, "SELECT COUNT(*) FROM foo")),
  129. ok = mysql:query(Pid, "DELETE FROM foo"),
  130. ?assertEqual({atomic, outer}, OuterResult).
  131. nested_inner_aborted(Pid) ->
  132. OuterResult = mysql:transaction(Pid, fun () ->
  133. ok = mysql:query(Pid, "INSERT INTO foo VALUES (9)"),
  134. InnerResult = mysql:transaction(Pid, fun () ->
  135. ok = mysql:query(Pid, "INSERT INTO foo VALUES (42)"),
  136. throw(inner)
  137. end),
  138. ?assertEqual({aborted, {throw, inner}}, InnerResult),
  139. outer
  140. end),
  141. ?assertMatch({ok, _, [[9]]}, mysql:query(Pid, "SELECT bar FROM foo")),
  142. ok = mysql:query(Pid, "DELETE FROM foo"),
  143. ?assertEqual({atomic, outer}, OuterResult).
  144. implicit_commit(Conn) ->
  145. %% This causes an implicit commit in a nested transaction.
  146. Query = "ALTER TABLE foo ADD baz INT",
  147. ?assertError({implicit_commit, Query}, mysql:transaction(Conn, fun () ->
  148. mysql:transaction(Conn, fun () ->
  149. mysql:query(Conn, Query)
  150. end)
  151. end)),
  152. ?assertNot(mysql:in_transaction(Conn)).
  153. %% -----------------------------------------------------------------------------
  154. deadlock_test_() ->
  155. {setup,
  156. fun () ->
  157. {ok, Conn1} = mysql:start_link([{user, ?user}, {password, ?password}]),
  158. ok = mysql:query(Conn1, <<"CREATE DATABASE IF NOT EXISTS otptest">>),
  159. ok = mysql:query(Conn1, <<"USE otptest">>),
  160. ok = mysql:query(Conn1, <<"CREATE TABLE foo (k INT PRIMARY KEY, v INT)"
  161. " engine=InnoDB">>),
  162. ok = mysql:query(Conn1, "INSERT INTO foo (k,v) VALUES (1,0), (2,0)"),
  163. {ok, Conn2} = mysql:start_link([{user, ?user}, {password, ?password}]),
  164. ok = mysql:query(Conn2, <<"USE otptest">>),
  165. {Conn1, Conn2}
  166. end,
  167. fun ({Conn1, Conn2}) ->
  168. ok = mysql:query(Conn1, <<"DROP DATABASE otptest">>, 1000),
  169. exit(Conn1, normal),
  170. exit(Conn2, normal)
  171. end,
  172. fun (Conns) ->
  173. [{"Plain queries", fun () -> deadlock_plain_queries(Conns) end},
  174. {"Prep stmts", fun () -> deadlock_prepared_statements(Conns) end},
  175. {"No retry", fun () -> deadlock_no_retry(Conns) end},
  176. {"Lock wait timeout", fun () -> lock_wait_timeout(Conns) end}]
  177. end}.
  178. flush_inbox() ->
  179. receive _ -> flush_inbox() after 0 -> ok end.
  180. deadlock_plain_queries({Conn1, Conn2}) ->
  181. {ok, _, [[2]]} = mysql:query(Conn1, "SELECT COUNT(*) FROM foo"),
  182. MainPid = self(),
  183. %?debugMsg("\nExtra output from the deadlock test:"),
  184. %% Spawn worker 2 to lock rows; first in table foo, then in bar.
  185. Worker2 = spawn_link(fun () ->
  186. {atomic, ok} = mysql:transaction(Conn2, fun () ->
  187. MainPid ! start,
  188. ok = mysql:query(Conn2, "UPDATE foo SET v = 2 WHERE k = 2"),
  189. %% Sync. Send 'go' to worker 1 multiple times in case it restarts.
  190. MainPid ! go, MainPid ! go, MainPid ! go,
  191. receive go -> ok after 10000 -> throw(too_long) end,
  192. {atomic, ok} = mysql:transaction(Conn2, fun () ->
  193. %% Nested transaction, just to make sure we can handle nested.
  194. ok = mysql:query(Conn2, "UPDATE foo SET v = 2 WHERE k = 1")
  195. end),
  196. ok
  197. end),
  198. MainPid ! done
  199. end),
  200. %% Do worker 1's job and lock the rows in the opposite order.
  201. {atomic, ok} = mysql:transaction(Conn1, fun () ->
  202. MainPid ! start,
  203. ok = mysql:query(Conn1, "UPDATE foo SET v = 1 WHERE k = 1"),
  204. %% Sync. Send 'go' to worker 2 multiple times in case it restarts.
  205. Worker2 ! go, Worker2 ! go, Worker2 ! go,
  206. receive go -> ok after 10000 -> throw(too_long) end,
  207. {atomic, ok} = mysql:transaction(Conn1, fun () ->
  208. %% Nested transaction, just to make sure we can handle nested.
  209. ok = mysql:query(Conn1, "UPDATE foo SET v = 1 WHERE k = 2")
  210. end),
  211. ok
  212. end),
  213. %% Wait for a reply from worker 2 to make sure it is done.
  214. receive done -> ok end,
  215. %% None of the connections should be in a transaction at this point
  216. ?assertNot(mysql:in_transaction(Conn1)),
  217. ?assertNot(mysql:in_transaction(Conn2)),
  218. %% Make sure we got at least 3 start messages, i.e. at least 1 restart.
  219. ?assertEqual(ok, receive start -> ok after 0 -> no_worker_ever_started end),
  220. ?assertEqual(ok, receive start -> ok after 0 -> only_one_worker_started end),
  221. ?assertEqual(ok, receive start -> ok after 0 -> there_was_no_deadlock end),
  222. flush_inbox().
  223. %% This case is very similar to the above test. We use prepared statements
  224. %% instead of plain queries. (Some lines of code in the implementation differ.)
  225. deadlock_prepared_statements({Conn1, Conn2}) ->
  226. {ok, _, [[2]]} = mysql:query(Conn1, "SELECT COUNT(*) FROM foo"),
  227. {ok, upd} = mysql:prepare(Conn1, upd, "UPDATE foo SET v = ? WHERE k = ?"),
  228. {ok, upd} = mysql:prepare(Conn2, upd, "UPDATE foo SET v = ? WHERE k = ?"),
  229. MainPid = self(),
  230. %% Spawn worker 2 to lock rows; first in table foo, then in bar.
  231. Worker2 = spawn_link(fun () ->
  232. {atomic, ok} = mysql:transaction(Conn2, fun () ->
  233. MainPid ! start,
  234. ok = mysql:execute(Conn2, upd, [2, 2]),
  235. %% Sync. Send 'go' to worker 1 multiple times in case it restarts.
  236. MainPid ! go, MainPid ! go, MainPid ! go,
  237. receive go -> ok end,
  238. {atomic, ok} = mysql:transaction(Conn2, fun () ->
  239. %% Nested transaction, just to make sure we can handle nested.
  240. ok = mysql:execute(Conn2, upd, [2, 1])
  241. end),
  242. ok
  243. end, 2),
  244. MainPid ! done
  245. end),
  246. %% Do worker 1's job and lock the rows in the opposite order.
  247. {atomic, ok} = mysql:transaction(Conn1, fun () ->
  248. MainPid ! start,
  249. ok = mysql:execute(Conn1, upd, [1, 1]),
  250. %% Sync. Send 'go' to worker 2 multiple times in case it restarts.
  251. Worker2 ! go, Worker2 ! go, Worker2 ! go,
  252. receive go -> ok end,
  253. {atomic, ok} = mysql:transaction(Conn1, fun () ->
  254. %% Nested transaction, just to make sure we can handle nested.
  255. ok = mysql:execute(Conn1, upd, [1, 2])
  256. end),
  257. ok
  258. end, 2),
  259. %% Wait for a reply from worker 2.
  260. receive done -> ok end,
  261. %% None of the connections should be in a transaction at this point
  262. ?assertNot(mysql:in_transaction(Conn1)),
  263. ?assertNot(mysql:in_transaction(Conn2)),
  264. %% Make sure we got at least 3 start messages, i.e. at least 1 restart.
  265. ?assertEqual(ok, receive start -> ok after 0 -> no_worker_ever_started end),
  266. ?assertEqual(ok, receive start -> ok after 0 -> only_one_worker_started end),
  267. ?assertEqual(ok, receive start -> ok after 0 -> there_was_no_deadlock end),
  268. flush_inbox().
  269. deadlock_no_retry({Conn1, Conn2}) ->
  270. {ok, _, [[2]]} = mysql:query(Conn1, "SELECT COUNT(*) FROM foo"),
  271. MainPid = self(),
  272. %?debugMsg("\nExtra output from the deadlock test:"),
  273. %% Spawn worker 2 to lock rows; first in table foo, then in bar.
  274. Worker2 = spawn_link(fun () ->
  275. Result = mysql:transaction(Conn2, fun () ->
  276. MainPid ! start,
  277. ok = mysql:query(Conn2, "UPDATE foo SET v = 2 WHERE k = 2"),
  278. %% Sync. Send 'go' to worker 1 multiple times in case it restarts.
  279. MainPid ! go, MainPid ! go, MainPid ! go,
  280. receive go -> ok after 10000 -> throw(too_long) end,
  281. {atomic, ok} = mysql:transaction(Conn2, fun () ->
  282. %% Nested transaction, just to make sure we can handle nested.
  283. ok = mysql:query(Conn2, "UPDATE foo SET v = 2 WHERE k = 1")
  284. end),
  285. ok
  286. end, 0),
  287. MainPid ! {done, Result}
  288. end),
  289. %% Do worker 1's job and lock the rows in the opposite order.
  290. Result1 = mysql:transaction(Conn1, fun () ->
  291. MainPid ! start,
  292. ok = mysql:query(Conn1, "UPDATE foo SET v = 1 WHERE k = 1"),
  293. %% Sync. Send 'go' to worker 2 multiple times in case it restarts.
  294. Worker2 ! go, Worker2 ! go, Worker2 ! go,
  295. receive go -> ok after 10000 -> throw(too_long) end,
  296. {atomic, ok} = mysql:transaction(Conn1, fun () ->
  297. %% Nested transaction, just to make sure we can handle nested.
  298. ok = mysql:query(Conn1, "UPDATE foo SET v = 1 WHERE k = 2")
  299. end),
  300. ok
  301. end, 0),
  302. %% Wait for a reply from worker 2 to make sure it is done.
  303. Result2 = receive {done, Result} -> Result end,
  304. %% Check that one of them was ok, the other one was aborted.
  305. [ResultAborted, ResultAtomic] = lists:sort([Result1, Result2]),
  306. ?assertEqual({atomic, ok}, ResultAtomic),
  307. ?assertMatch({aborted,
  308. {{1213, <<"40001">>, <<"Deadlock", _/binary>>}, _Trace}},
  309. ResultAborted),
  310. %% None of the connections should be in a transaction at this point
  311. ?assertNot(mysql:in_transaction(Conn1)),
  312. ?assertNot(mysql:in_transaction(Conn2)),
  313. %% Make sure we got exactly 2 start messages, i.e. there was no restart.
  314. ?assertEqual(ok, receive start -> ok after 0 -> no_worker_ever_started end),
  315. ?assertEqual(ok, receive start -> ok after 0 -> only_one_worker_started end),
  316. ?assertEqual(ok, receive start -> there_was_a_restart after 0 -> ok end),
  317. flush_inbox().
  318. lock_wait_timeout({_Conn1, Conn2} = Conns) ->
  319. %% Set the lowest timeout possible to speed up the test.
  320. case mysql:query(Conn2, "SET innodb_lock_wait_timeout = 1") of
  321. ok ->
  322. lock_wait_timeout1(Conns);
  323. {error, {1238, _, <<"Variable 'innodb_lock_wait_timeout' is a read on",
  324. _/binary>>}} ->
  325. error_logger:info_msg("Can't set lock wait timeout in this server"
  326. " version. Skipping the lock wait timeout"
  327. " test.\n")
  328. end.
  329. %% Continuation of lock_wait_timeout/1.
  330. lock_wait_timeout1({Conn1, Conn2}) ->
  331. {ok, _, [[1]]} = mysql:query(Conn2, "SELECT COUNT(*) FROM foo WHERE k = 1"),
  332. MainPid = self(),
  333. %% Create a worker that takes the lock and sleeps on it.
  334. LockingWorker = spawn_link(fun () ->
  335. {atomic, ok} = mysql:transaction(Conn1, fun () ->
  336. ok = mysql:query(Conn1, "UPDATE foo SET v = 0 WHERE k = 1"),
  337. MainPid ! go,
  338. receive release -> ok end
  339. end),
  340. MainPid ! done
  341. end),
  342. %% Wait for the locking worker to take the lock.
  343. receive go -> ok end,
  344. {atomic, ok} = mysql:transaction(Conn2, fun () ->
  345. ?assertMatch({error, {1205, _, <<"Lock wait timeout", _/binary>>}},
  346. mysql:query(Conn2, "UPDATE foo SET v = 42 WHERE k = 1")),
  347. ok
  348. end),
  349. %% Wake the sleeping worker.
  350. LockingWorker ! release,
  351. receive done -> ok end,
  352. flush_inbox().