transaction_tests.erl 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  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. {with, [fun simple_atomic/1,
  40. fun simple_aborted/1,
  41. fun nested_atomic/1,
  42. fun nested_inner_aborted/1,
  43. fun implicit_commit/1]}}.
  44. simple_atomic(Pid) ->
  45. ?assertNot(mysql:in_transaction(Pid)),
  46. Result = mysql:transaction(Pid, fun () ->
  47. ok = mysql:query(Pid, "INSERT INTO foo (bar) VALUES (42)"),
  48. ?assert(mysql:in_transaction(Pid)),
  49. hello
  50. end),
  51. ?assertEqual({atomic, hello}, Result),
  52. ?assertNot(mysql:in_transaction(Pid)),
  53. ok = mysql:query(Pid, "DELETE FROM foo").
  54. simple_aborted(Pid) ->
  55. ok = mysql:query(Pid, "INSERT INTO foo VALUES (9)"),
  56. ?assertEqual({ok, [<<"bar">>], [[9]]},
  57. mysql:query(Pid, "SELECT bar FROM foo")),
  58. Result = mysql:transaction(Pid, fun () ->
  59. ok = mysql:query(Pid, "INSERT INTO foo VALUES (42)"),
  60. ?assertMatch({ok, _, [[2]]},
  61. mysql:query(Pid, "SELECT COUNT(*) FROM foo")),
  62. error(hello)
  63. end),
  64. ?assertMatch({aborted, {hello, Stacktrace}} when is_list(Stacktrace),
  65. Result),
  66. ?assertEqual({ok, [<<"bar">>], [[9]]},
  67. mysql:query(Pid, "SELECT bar FROM foo")),
  68. ok = mysql:query(Pid, "DELETE FROM foo"),
  69. %% Also check the abort Reason for throw and exit.
  70. ?assertEqual({aborted, {throw, foo}},
  71. mysql:transaction(Pid, fun () -> throw(foo) end)),
  72. ?assertEqual({aborted, foo},
  73. mysql:transaction(Pid, fun () -> exit(foo) end)).
  74. nested_atomic(Pid) ->
  75. OuterResult = mysql:transaction(Pid, fun () ->
  76. ok = mysql:query(Pid, "INSERT INTO foo VALUES (9)"),
  77. InnerResult = mysql:transaction(Pid, fun () ->
  78. ok = mysql:query(Pid, "INSERT INTO foo VALUES (42)"),
  79. inner
  80. end),
  81. ?assertEqual({atomic, inner}, InnerResult),
  82. outer
  83. end),
  84. ?assertMatch({ok, _, [[2]]}, mysql:query(Pid, "SELECT COUNT(*) FROM foo")),
  85. ok = mysql:query(Pid, "DELETE FROM foo"),
  86. ?assertEqual({atomic, outer}, OuterResult).
  87. nested_inner_aborted(Pid) ->
  88. OuterResult = mysql:transaction(Pid, fun () ->
  89. ok = mysql:query(Pid, "INSERT INTO foo VALUES (9)"),
  90. InnerResult = mysql:transaction(Pid, fun () ->
  91. ok = mysql:query(Pid, "INSERT INTO foo VALUES (42)"),
  92. throw(inner)
  93. end),
  94. ?assertEqual({aborted, {throw, inner}}, InnerResult),
  95. outer
  96. end),
  97. ?assertMatch({ok, _, [[9]]}, mysql:query(Pid, "SELECT bar FROM foo")),
  98. ok = mysql:query(Pid, "DELETE FROM foo"),
  99. ?assertEqual({atomic, outer}, OuterResult).
  100. implicit_commit(Conn) ->
  101. %% This causes an implicit commit in a nested transaction.
  102. Query = "ALTER TABLE foo ADD baz INT",
  103. ?assertError({implicit_commit, Query}, mysql:transaction(Conn, fun () ->
  104. mysql:transaction(Conn, fun () ->
  105. mysql:query(Conn, Query)
  106. end)
  107. end)),
  108. ?assertNot(mysql:in_transaction(Conn)).
  109. %% -----------------------------------------------------------------------------
  110. deadlock_test_() ->
  111. {setup,
  112. fun () ->
  113. {ok, Conn1} = mysql:start_link([{user, ?user}, {password, ?password}]),
  114. ok = mysql:query(Conn1, <<"CREATE DATABASE IF NOT EXISTS otptest">>),
  115. ok = mysql:query(Conn1, <<"USE otptest">>),
  116. ok = mysql:query(Conn1, <<"CREATE TABLE foo (k INT PRIMARY KEY, v INT)"
  117. " engine=InnoDB">>),
  118. ok = mysql:query(Conn1, "INSERT INTO foo (k,v) VALUES (1,0), (2,0)"),
  119. {ok, Conn2} = mysql:start_link([{user, ?user}, {password, ?password}]),
  120. ok = mysql:query(Conn2, <<"USE otptest">>),
  121. {Conn1, Conn2}
  122. end,
  123. fun ({Conn1, Conn2}) ->
  124. ok = mysql:query(Conn1, <<"DROP DATABASE otptest">>, 1000),
  125. exit(Conn1, normal),
  126. exit(Conn2, normal)
  127. end,
  128. fun (Conns) ->
  129. [{"Plain queries", fun () -> deadlock_plain_queries(Conns) end},
  130. {"Prep stmts", fun () -> deadlock_prepared_statements(Conns) end},
  131. {"Lock wait timeout", fun () -> lock_wait_timeout(Conns) end}]
  132. end}.
  133. flush_inbox() ->
  134. receive _ -> flush_inbox() after 0 -> ok end.
  135. deadlock_plain_queries({Conn1, Conn2}) ->
  136. {ok, _, [[2]]} = mysql:query(Conn1, "SELECT COUNT(*) FROM foo"),
  137. MainPid = self(),
  138. %?debugMsg("\nExtra output from the deadlock test:"),
  139. %% Spawn worker 2 to lock rows; first in table foo, then in bar.
  140. Worker2 = spawn_link(fun () ->
  141. {atomic, ok} = mysql:transaction(Conn2, fun () ->
  142. MainPid ! start,
  143. %?debugMsg("Worker 2: Starting. First get a lock on row 2."),
  144. ok = mysql:query(Conn2, "UPDATE foo SET v = 2 WHERE k = 2"),
  145. %?debugMsg("Worker 2: Got lock on foo. Now wait for signal from 1."),
  146. %% Sync. Send 'go' to worker 1 multiple times in case it restarts.
  147. MainPid ! go, MainPid ! go, MainPid ! go,
  148. receive go -> ok after 10000 -> throw(too_long) end,
  149. %?debugMsg("Worker 2: Got signal from 1. Now get a lock on row 1."),
  150. {atomic, ok} = mysql:transaction(Conn2, fun () ->
  151. %% Nested transaction, just to make sure we can handle nested.
  152. ok = mysql:query(Conn2, "UPDATE foo SET v = 2 WHERE k = 1")
  153. end),
  154. %?debugMsg("Worker 2: Got both locks and is done."),
  155. ok
  156. end),
  157. MainPid ! done
  158. end),
  159. %% Do worker 1's job and lock the rows in the opposite order.
  160. {atomic, ok} = mysql:transaction(Conn1, fun () ->
  161. MainPid ! start,
  162. %?debugMsg("Worker 1: Starting. First get a lock on row 1."),
  163. ok = mysql:query(Conn1, "UPDATE foo SET v = 1 WHERE k = 1"),
  164. %?debugMsg("Worker 1: Got lock on bar. Now wait for signal from 2."),
  165. %% Sync. Send 'go' to worker 2 multiple times in case it restarts.
  166. Worker2 ! go, Worker2 ! go, Worker2 ! go,
  167. receive go -> ok after 10000 -> throw(too_long) end,
  168. %?debugMsg("Worker 1: Got signal from 2. Now get lock on row 2."),
  169. {atomic, ok} = mysql:transaction(Conn1, fun () ->
  170. %% Nested transaction, just to make sure we can handle nested.
  171. ok = mysql:query(Conn1, "UPDATE foo SET v = 1 WHERE k = 2")
  172. end),
  173. %?debugMsg("Worker 1: Got both locks and is done."),
  174. ok
  175. end),
  176. %% Wait for a reply from worker 2 to make sure it is done.
  177. receive done -> ok end,
  178. %% None of the connections should be in a transaction at this point
  179. ?assertNot(mysql:in_transaction(Conn1)),
  180. ?assertNot(mysql:in_transaction(Conn2)),
  181. %% Make sure we got at least 3 start messages, i.e. at least 1 restart.
  182. ?assertEqual(ok, receive start -> ok after 0 -> no_worker_ever_started end),
  183. ?assertEqual(ok, receive start -> ok after 0 -> only_one_worker_started end),
  184. ?assertEqual(ok, receive start -> ok after 0 -> there_was_no_deadlock end),
  185. flush_inbox().
  186. %% This case is very similar to the above test. We use prepared statements
  187. %% instead of plain queries. (Some lines of code in the implementation differ.)
  188. deadlock_prepared_statements({Conn1, Conn2}) ->
  189. {ok, _, [[2]]} = mysql:query(Conn1, "SELECT COUNT(*) FROM foo"),
  190. {ok, upd} = mysql:prepare(Conn1, upd, "UPDATE foo SET v = ? WHERE k = ?"),
  191. {ok, upd} = mysql:prepare(Conn2, upd, "UPDATE foo SET v = ? WHERE k = ?"),
  192. MainPid = self(),
  193. %% Spawn worker 2 to lock rows; first in table foo, then in bar.
  194. Worker2 = spawn_link(fun () ->
  195. {atomic, ok} = mysql:transaction(Conn2, fun () ->
  196. MainPid ! start,
  197. ok = mysql:execute(Conn2, upd, [2, 2]),
  198. %% Sync. Send 'go' to worker 1 multiple times in case it restarts.
  199. MainPid ! go, MainPid ! go, MainPid ! go,
  200. receive go -> ok end,
  201. {atomic, ok} = mysql:transaction(Conn2, fun () ->
  202. %% Nested transaction, just to make sure we can handle nested.
  203. ok = mysql:execute(Conn2, upd, [2, 1])
  204. end),
  205. ok
  206. end),
  207. MainPid ! done
  208. end),
  209. %% Do worker 1's job and lock the rows in the opposite order.
  210. {atomic, ok} = mysql:transaction(Conn1, fun () ->
  211. MainPid ! start,
  212. ok = mysql:execute(Conn1, upd, [1, 1]),
  213. %% Sync. Send 'go' to worker 2 multiple times in case it restarts.
  214. Worker2 ! go, Worker2 ! go, Worker2 ! go,
  215. receive go -> ok end,
  216. {atomic, ok} = mysql:transaction(Conn1, fun () ->
  217. %% Nested transaction, just to make sure we can handle nested.
  218. ok = mysql:execute(Conn1, upd, [1, 2])
  219. end),
  220. ok
  221. end),
  222. %% Wait for a reply from worker 2.
  223. receive done -> ok end,
  224. %% None of the connections should be in a transaction at this point
  225. ?assertNot(mysql:in_transaction(Conn1)),
  226. ?assertNot(mysql:in_transaction(Conn2)),
  227. %% Make sure we got at least 3 start messages, i.e. at least 1 restart.
  228. ?assertEqual(ok, receive start -> ok after 0 -> no_worker_ever_started end),
  229. ?assertEqual(ok, receive start -> ok after 0 -> only_one_worker_started end),
  230. ?assertEqual(ok, receive start -> ok after 0 -> there_was_no_deadlock end),
  231. flush_inbox().
  232. lock_wait_timeout({Conn1, Conn2}) ->
  233. %% Set the lowest timeout possible to speed up the test.
  234. ok = mysql:query(Conn2, "SET innodb_lock_wait_timeout = 1"),
  235. {ok, _, [[1]]} = mysql:query(Conn2, "SELECT COUNT(*) FROM foo WHERE k = 1"),
  236. MainPid = self(),
  237. %% Create a worker that takes the lock and sleeps on it.
  238. LockingWorker = spawn_link(fun () ->
  239. {atomic, ok} = mysql:transaction(Conn1, fun () ->
  240. ok = mysql:query(Conn1, "UPDATE foo SET v = 0 WHERE k = 1"),
  241. MainPid ! go,
  242. receive release -> ok end
  243. end),
  244. MainPid ! done
  245. end),
  246. %% Wait for the locking worker to take the lock.
  247. receive go -> ok end,
  248. {aborted, Reason} = mysql:transaction(Conn2, fun () ->
  249. ok = mysql:query(Conn2, "UPDATE foo SET v = 42 WHERE k = 1")
  250. end),
  251. ?assertMatch({{1205, _, <<"Lock wait timeout", _/binary>>}, _Trace},
  252. Reason),
  253. %% Wake the sleeping worker.
  254. LockingWorker ! release,
  255. receive done -> ok end,
  256. flush_inbox().