transaction_tests.erl 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434
  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. mysql:stop(Pid)
  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. {timeout, 30, fun application_process_kill/0}.
  48. application_process_kill() ->
  49. %% This test case simulates a setup where the connection is owned by
  50. %% another process, e.g. a connection pool. An application process (e.g.
  51. %% a cowboy worker) is killed when it using a connection in a transaction.
  52. %% In this case, the connection should not go back in the pool as it would
  53. %% be in a bad state. Therefore, the connection is monitoring the process
  54. %% starting a transaction and kills itself if the caller dies.
  55. {ok, Pid} = mysql:start_link([
  56. {user, ?user},
  57. {password, ?password},
  58. {query_cache_time, 50},
  59. {log_warnings, false}
  60. ]),
  61. unlink(Pid),
  62. Mref = erlang:monitor(process, Pid),
  63. ok = mysql:query(Pid, <<"DROP DATABASE IF EXISTS otptest">>),
  64. ok = mysql:query(Pid, <<"CREATE DATABASE otptest">>),
  65. ok = mysql:query(Pid, <<"USE otptest">>),
  66. ok = mysql:query(Pid, <<"CREATE TABLE foo (bar INT) engine=InnoDB">>),
  67. ?assertNot(mysql:in_transaction(Pid)),
  68. ?assert(is_process_alive(Pid)),
  69. Self = self(),
  70. AppPid = spawn(fun() ->
  71. mysql:transaction(Pid, fun () ->
  72. ok = mysql:query(Pid, "INSERT INTO foo (bar) VALUES (42)"),
  73. Self! killme,
  74. receive after 10000 -> throw(too_long) end,
  75. ok
  76. end)
  77. end),
  78. %% Wait for the AppPid to be ready to be killed when in a transaction
  79. receive killme -> ok end,
  80. %% Kill AppPid, the process using the connection, capturing the noise
  81. {ok, ok, LoggedErrors} = error_logger_acc:capture(fun () ->
  82. exit(AppPid, kill),
  83. receive
  84. {'DOWN', Mref, process, Pid, {application_process_died, AppPid}} ->
  85. ok
  86. after 10000 ->
  87. throw(too_long)
  88. end
  89. end),
  90. %% Check that we got the expected error log noise
  91. ?assertMatch([{error, "Connection Id" ++ _}, %% from mysql_conn
  92. {error, "** Generic server" ++ _}, %% from gen_server
  93. {error_report, _}], LoggedErrors),
  94. ?assertNot(is_process_alive(Pid)),
  95. %% Check that the transaction was not commited
  96. {ok, Pid2} = mysql:start_link([
  97. {user, ?user},
  98. {password, ?password},
  99. {query_cache_time, 50},
  100. {log_warnings, false}
  101. ]),
  102. ok = mysql:query(Pid2, <<"USE otptest">>),
  103. ?assertMatch({ok, _, []},
  104. mysql:query(Pid2, <<"SELECT * from foo where bar = 42">>)),
  105. ok = mysql:query(Pid2, <<"DROP DATABASE otptest">>),
  106. mysql:stop(Pid2).
  107. simple_atomic(Pid) ->
  108. ?assertNot(mysql:in_transaction(Pid)),
  109. Result = mysql:transaction(Pid, fun () ->
  110. ok = mysql:query(Pid, "INSERT INTO foo (bar) VALUES (42)"),
  111. ?assert(mysql:in_transaction(Pid)),
  112. hello
  113. end),
  114. ?assertEqual({atomic, hello}, Result),
  115. ?assertNot(mysql:in_transaction(Pid)),
  116. ok = mysql:query(Pid, "DELETE FROM foo").
  117. simple_aborted(Pid) ->
  118. ok = mysql:query(Pid, "INSERT INTO foo VALUES (9)"),
  119. ?assertEqual({ok, [<<"bar">>], [[9]]},
  120. mysql:query(Pid, "SELECT bar FROM foo")),
  121. Result = mysql:transaction(Pid, fun () ->
  122. ok = mysql:query(Pid, "INSERT INTO foo VALUES (42)"),
  123. ?assertMatch({ok, _, [[2]]},
  124. mysql:query(Pid, "SELECT COUNT(*) FROM foo")),
  125. error(hello)
  126. end),
  127. ?assertMatch({aborted, {hello, Stacktrace}} when is_list(Stacktrace),
  128. Result),
  129. ?assertEqual({ok, [<<"bar">>], [[9]]},
  130. mysql:query(Pid, "SELECT bar FROM foo")),
  131. ok = mysql:query(Pid, "DELETE FROM foo"),
  132. %% Also check the abort Reason for throw and exit.
  133. ?assertEqual({aborted, {throw, foo}},
  134. mysql:transaction(Pid, fun () -> throw(foo) end)),
  135. ?assertEqual({aborted, foo},
  136. mysql:transaction(Pid, fun () -> exit(foo) end)).
  137. nested_atomic(Pid) ->
  138. OuterResult = mysql:transaction(Pid, fun () ->
  139. ok = mysql:query(Pid, "INSERT INTO foo VALUES (9)"),
  140. InnerResult = mysql:transaction(Pid, fun () ->
  141. ok = mysql:query(Pid, "INSERT INTO foo VALUES (42)"),
  142. inner
  143. end),
  144. ?assertEqual({atomic, inner}, InnerResult),
  145. outer
  146. end),
  147. ?assertMatch({ok, _, [[2]]}, mysql:query(Pid, "SELECT COUNT(*) FROM foo")),
  148. ok = mysql:query(Pid, "DELETE FROM foo"),
  149. ?assertEqual({atomic, outer}, OuterResult).
  150. nested_inner_aborted(Pid) ->
  151. OuterResult = mysql:transaction(Pid, fun () ->
  152. ok = mysql:query(Pid, "INSERT INTO foo VALUES (9)"),
  153. InnerResult = mysql:transaction(Pid, fun () ->
  154. ok = mysql:query(Pid, "INSERT INTO foo VALUES (42)"),
  155. throw(inner)
  156. end),
  157. ?assertEqual({aborted, {throw, inner}}, InnerResult),
  158. outer
  159. end),
  160. ?assertMatch({ok, _, [[9]]}, mysql:query(Pid, "SELECT bar FROM foo")),
  161. ok = mysql:query(Pid, "DELETE FROM foo"),
  162. ?assertEqual({atomic, outer}, OuterResult).
  163. implicit_commit(Conn) ->
  164. %% This causes an implicit commit in a nested transaction.
  165. Query = "ALTER TABLE foo ADD baz INT",
  166. ?assertError({implicit_commit, Query}, mysql:transaction(Conn, fun () ->
  167. mysql:transaction(Conn, fun () ->
  168. mysql:query(Conn, Query)
  169. end)
  170. end)),
  171. ?assertNot(mysql:in_transaction(Conn)).
  172. %% -----------------------------------------------------------------------------
  173. deadlock_test_() ->
  174. {setup,
  175. fun () ->
  176. {ok, Conn1} = mysql:start_link([{user, ?user}, {password, ?password}]),
  177. ok = mysql:query(Conn1, <<"CREATE DATABASE IF NOT EXISTS otptest">>),
  178. ok = mysql:query(Conn1, <<"USE otptest">>),
  179. ok = mysql:query(Conn1, <<"CREATE TABLE foo (k INT PRIMARY KEY, v INT)"
  180. " engine=InnoDB">>),
  181. ok = mysql:query(Conn1, "INSERT INTO foo (k,v) VALUES (1,0), (2,0)"),
  182. {ok, Conn2} = mysql:start_link([{user, ?user}, {password, ?password}]),
  183. ok = mysql:query(Conn2, <<"USE otptest">>),
  184. {ok, [_], [[VersionBin]]} = mysql:query(Conn1, "SELECT @@version"),
  185. {Conn1, Conn2, VersionBin}
  186. end,
  187. fun ({Conn1, Conn2, _VersionBin}) ->
  188. ok = mysql:query(Conn1, <<"DROP DATABASE otptest">>, 1000),
  189. mysql:stop(Conn1),
  190. mysql:stop(Conn2)
  191. end,
  192. fun ({_Conn1, _Conn2, <<"5.7.", _/binary>>}) ->
  193. fun () ->
  194. error_logger:info_msg(
  195. "The deadlock test fails in some MySQL versions so it is "
  196. "currently disabled for MySQL 5.7.x. TODO: Confirm if "
  197. "there is a bug or a changed behavior for this scenario.")
  198. end;
  199. ({_Conn1, _Conn2, <<"8.", _/binary>>}) ->
  200. fun () ->
  201. error_logger:info_msg(
  202. "The deadlock test fails in some MySQL versions so it is "
  203. "currently disabled for MySQL 8.x.y. TODO: Confirm if "
  204. "there is a bug or a changed behavior for this scenario.")
  205. end;
  206. ({Conn1, Conn2, _VersionBin}) ->
  207. Conns = {Conn1, Conn2},
  208. [{"Plain queries", fun () -> deadlock_plain_queries(Conns) end},
  209. {"Prep stmts", fun () -> deadlock_prepared_statements(Conns) end},
  210. {"No retry", fun () -> deadlock_no_retry(Conns) end},
  211. {"Lock wait timeout", fun () -> lock_wait_timeout(Conns) end}]
  212. end}.
  213. flush_inbox() ->
  214. receive _ -> flush_inbox() after 0 -> ok end.
  215. deadlock_plain_queries({Conn1, Conn2}) ->
  216. {ok, _, [[2]]} = mysql:query(Conn1, "SELECT COUNT(*) FROM foo"),
  217. MainPid = self(),
  218. %?debugMsg("\nExtra output from the deadlock test:"),
  219. %% Spawn worker 2 to lock rows; first in table foo, then in bar.
  220. Worker2 = spawn_link(fun () ->
  221. {atomic, ok} = mysql:transaction(Conn2, fun () ->
  222. MainPid ! start,
  223. ok = mysql:query(Conn2, "UPDATE foo SET v = 2 WHERE k = 2"),
  224. %% Sync. Send 'go' to worker 1 multiple times in case it restarts.
  225. MainPid ! go, MainPid ! go, MainPid ! go,
  226. receive go -> ok after 10000 -> throw(too_long) end,
  227. {atomic, ok} = mysql:transaction(Conn2, fun () ->
  228. %% Nested transaction, just to make sure we can handle nested.
  229. ok = mysql:query(Conn2, "UPDATE foo SET v = 2 WHERE k = 1")
  230. end),
  231. ok
  232. end),
  233. MainPid ! done
  234. end),
  235. %% Do worker 1's job and lock the rows in the opposite order.
  236. {atomic, ok} = mysql:transaction(Conn1, fun () ->
  237. MainPid ! start,
  238. ok = mysql:query(Conn1, "UPDATE foo SET v = 1 WHERE k = 1"),
  239. %% Sync. Send 'go' to worker 2 multiple times in case it restarts.
  240. Worker2 ! go, Worker2 ! go, Worker2 ! go,
  241. receive go -> ok after 10000 -> throw(too_long) end,
  242. {atomic, ok} = mysql:transaction(Conn1, fun () ->
  243. %% Nested transaction, just to make sure we can handle nested.
  244. ok = mysql:query(Conn1, "UPDATE foo SET v = 1 WHERE k = 2")
  245. end),
  246. ok
  247. end),
  248. %% Wait for a reply from worker 2 to make sure it is done.
  249. receive done -> ok end,
  250. %% None of the connections should be in a transaction at this point
  251. ?assertNot(mysql:in_transaction(Conn1)),
  252. ?assertNot(mysql:in_transaction(Conn2)),
  253. %% Make sure we got at least 3 start messages, i.e. at least 1 restart.
  254. ?assertEqual(ok, receive start -> ok after 0 -> no_worker_ever_started end),
  255. ?assertEqual(ok, receive start -> ok after 0 -> only_one_worker_started end),
  256. ?assertEqual(ok, receive start -> ok after 0 -> there_was_no_deadlock end),
  257. flush_inbox().
  258. %% This case is very similar to the above test. We use prepared statements
  259. %% instead of plain queries. (Some lines of code in the implementation differ.)
  260. deadlock_prepared_statements({Conn1, Conn2}) ->
  261. {ok, _, [[2]]} = mysql:query(Conn1, "SELECT COUNT(*) FROM foo"),
  262. {ok, upd} = mysql:prepare(Conn1, upd, "UPDATE foo SET v = ? WHERE k = ?"),
  263. {ok, upd} = mysql:prepare(Conn2, upd, "UPDATE foo SET v = ? WHERE k = ?"),
  264. MainPid = self(),
  265. %% Spawn worker 2 to lock rows; first in table foo, then in bar.
  266. Worker2 = spawn_link(fun () ->
  267. {atomic, ok} = mysql:transaction(Conn2, fun () ->
  268. MainPid ! start,
  269. ok = mysql:execute(Conn2, upd, [2, 2]),
  270. %% Sync. Send 'go' to worker 1 multiple times in case it restarts.
  271. MainPid ! go, MainPid ! go, MainPid ! go,
  272. receive go -> ok end,
  273. {atomic, ok} = mysql:transaction(Conn2, fun () ->
  274. %% Nested transaction, just to make sure we can handle nested.
  275. ok = mysql:execute(Conn2, upd, [2, 1])
  276. end),
  277. ok
  278. end, 2),
  279. MainPid ! done
  280. end),
  281. %% Do worker 1's job and lock the rows in the opposite order.
  282. {atomic, ok} = mysql:transaction(Conn1, fun () ->
  283. MainPid ! start,
  284. ok = mysql:execute(Conn1, upd, [1, 1]),
  285. %% Sync. Send 'go' to worker 2 multiple times in case it restarts.
  286. Worker2 ! go, Worker2 ! go, Worker2 ! go,
  287. receive go -> ok end,
  288. {atomic, ok} = mysql:transaction(Conn1, fun () ->
  289. %% Nested transaction, just to make sure we can handle nested.
  290. ok = mysql:execute(Conn1, upd, [1, 2])
  291. end),
  292. ok
  293. end, 2),
  294. %% Wait for a reply from worker 2.
  295. receive done -> ok end,
  296. %% None of the connections should be in a transaction at this point
  297. ?assertNot(mysql:in_transaction(Conn1)),
  298. ?assertNot(mysql:in_transaction(Conn2)),
  299. %% Make sure we got at least 3 start messages, i.e. at least 1 restart.
  300. ?assertEqual(ok, receive start -> ok after 0 -> no_worker_ever_started end),
  301. ?assertEqual(ok, receive start -> ok after 0 -> only_one_worker_started end),
  302. ?assertEqual(ok, receive start -> ok after 0 -> there_was_no_deadlock end),
  303. flush_inbox().
  304. deadlock_no_retry({Conn1, Conn2}) ->
  305. {ok, _, [[2]]} = mysql:query(Conn1, "SELECT COUNT(*) FROM foo"),
  306. MainPid = self(),
  307. %?debugMsg("\nExtra output from the deadlock test:"),
  308. %% Spawn worker 2 to lock rows; first in table foo, then in bar.
  309. Worker2 = spawn_link(fun () ->
  310. Result = mysql:transaction(Conn2, fun () ->
  311. MainPid ! start,
  312. ok = mysql:query(Conn2, "UPDATE foo SET v = 2 WHERE k = 2"),
  313. %% Sync. Send 'go' to worker 1 multiple times in case it restarts.
  314. MainPid ! go, MainPid ! go, MainPid ! go,
  315. receive go -> ok after 10000 -> throw(too_long) end,
  316. {atomic, ok} = mysql:transaction(Conn2, fun () ->
  317. %% Nested transaction, just to make sure we can handle nested.
  318. ok = mysql:query(Conn2, "UPDATE foo SET v = 2 WHERE k = 1")
  319. end),
  320. ok
  321. end, 0),
  322. MainPid ! {done, Result}
  323. end),
  324. %% Do worker 1's job and lock the rows in the opposite order.
  325. Result1 = mysql:transaction(Conn1, fun () ->
  326. MainPid ! start,
  327. ok = mysql:query(Conn1, "UPDATE foo SET v = 1 WHERE k = 1"),
  328. %% Sync. Send 'go' to worker 2 multiple times in case it restarts.
  329. Worker2 ! go, Worker2 ! go, Worker2 ! go,
  330. receive go -> ok after 10000 -> throw(too_long) end,
  331. {atomic, ok} = mysql:transaction(Conn1, fun () ->
  332. %% Nested transaction, just to make sure we can handle nested.
  333. ok = mysql:query(Conn1, "UPDATE foo SET v = 1 WHERE k = 2")
  334. end),
  335. ok
  336. end, 0),
  337. %% Wait for a reply from worker 2 to make sure it is done.
  338. Result2 = receive {done, Result} -> Result end,
  339. %% Check that one of them was ok, the other one was aborted.
  340. [ResultAborted, ResultAtomic] = lists:sort([Result1, Result2]),
  341. ?assertEqual({atomic, ok}, ResultAtomic),
  342. ?assertMatch({aborted,
  343. {{1213, <<"40001">>, <<"Deadlock", _/binary>>}, _Trace}},
  344. ResultAborted),
  345. %% None of the connections should be in a transaction at this point
  346. ?assertNot(mysql:in_transaction(Conn1)),
  347. ?assertNot(mysql:in_transaction(Conn2)),
  348. %% Make sure we got exactly 2 start messages, i.e. there was no restart.
  349. ?assertEqual(ok, receive start -> ok after 0 -> no_worker_ever_started end),
  350. ?assertEqual(ok, receive start -> ok after 0 -> only_one_worker_started end),
  351. ?assertEqual(ok, receive start -> there_was_a_restart after 0 -> ok end),
  352. flush_inbox().
  353. lock_wait_timeout({_Conn1, Conn2} = Conns) ->
  354. %% Set the lowest timeout possible to speed up the test.
  355. case mysql:query(Conn2, "SET innodb_lock_wait_timeout = 1") of
  356. ok ->
  357. lock_wait_timeout1(Conns);
  358. {error, {1238, _, <<"Variable 'innodb_lock_wait_timeout' is a read on",
  359. _/binary>>}} ->
  360. error_logger:info_msg("Can't set lock wait timeout in this server"
  361. " version. Skipping the lock wait timeout"
  362. " test.\n")
  363. end.
  364. %% Continuation of lock_wait_timeout/1.
  365. lock_wait_timeout1({Conn1, Conn2}) ->
  366. {ok, _, [[1]]} = mysql:query(Conn2, "SELECT COUNT(*) FROM foo WHERE k = 1"),
  367. MainPid = self(),
  368. %% Create a worker that takes the lock and sleeps on it.
  369. LockingWorker = spawn_link(fun () ->
  370. {atomic, ok} = mysql:transaction(Conn1, fun () ->
  371. ok = mysql:query(Conn1, "UPDATE foo SET v = 0 WHERE k = 1"),
  372. MainPid ! go,
  373. receive release -> ok end
  374. end),
  375. MainPid ! done
  376. end),
  377. %% Wait for the locking worker to take the lock.
  378. receive go -> ok end,
  379. {atomic, ok} = mysql:transaction(Conn2, fun () ->
  380. ?assertMatch({error, {1205, _, <<"Lock wait timeout", _/binary>>}},
  381. mysql:query(Conn2, "UPDATE foo SET v = 42 WHERE k = 1")),
  382. ok
  383. end),
  384. %% Wake the sleeping worker.
  385. LockingWorker ! release,
  386. receive done -> ok end,
  387. flush_inbox().