Current section
Files
Jump to
Current section
Files
test/conn_mgr_SUITE.erl
%%%-------------------------------------------------------------------
%%% File : Emysql/test/conn_mgr_SUITE.erl
%%% Descr : Suite #7 - Testing connection manager.
%%% Authors : R. Richardson, H. Diedrich
%%% Created : 04/06/2012 ransomr
%%% Changed : 04/07/2012 hd - added tests w/faster race provocation
%%% Requires : Erlang 14B (prior may not have ct_run)
%%%-------------------------------------------------------------------
%%%
%%% Run from Emysql/:
%%% make test
%%%
%%% Results see:
%%% test/index.html
%%%
%%%-------------------------------------------------------------------
-module(conn_mgr_SUITE).
-compile(export_all).
-include_lib("common_test/include/ct.hrl").
-include_lib("../include/emysql.hrl").
%% Faster or more realistic testing of races of issue #9. Both are reliable.
-define(TRY_RACE, lock_and_pass_connection). % faster, focused on problem zone.
% -define(TRY_RACE, make_queries). % takes a minute, with real queries.
% List of test cases.
% Test cases have self explanatory names.
%%--------------------------------------------------------------------
all() ->
[
stuck_waiting_1, % former two_procs
stuck_waiting_2,
%pool_leak_1, % former no_lock_timeout
pool_leak_2,
dying_client_does_not_lock_the_connection_out
].
%%--------------------------------------------------------------------
init_per_suite(Config) ->
% if this fails, focus on environment_SUITE to fix test setup.
crypto:start(),
application:start(emysql),
emysql:add_pool(test_pool, 1,
test_helper:test_u(), test_helper:test_p(), "localhost", 3306,
"hello_database", utf8),
Config.
% clean up
%%--------------------------------------------------------------------
end_per_suite(_) ->
ok.
init_per_testcase(dying_client_does_not_lock_the_connection_out, Config) ->
LockTimeout = emysql_app:lock_timeout(),
DefaultTimeout = emysql_app:default_timeout(),
application:set_env(emysql, lock_timeout, 15000),
application:set_env(emysql, default_timeout, 15000),
[{old_sysconfig, [{lock_timeout, LockTimeout}, {default_timeout, DefaultTimeout}]} | Config];
init_per_testcase(_, Config) ->
Config.
end_per_testcase(dying_client_does_not_lock_the_connection_out, Config) ->
[ application:set_env(emysql, K, V) || {K, V} <- proplists:get_value(old_sysconfig, Config) ],
Config;
end_per_testcase(_, Config) ->
Config.
%% Test Case: Test two processes trying to share one connection
%% Test for Issue 9
%% Original name: two_procs(_)
%%--------------------------------------------------------------------
stuck_waiting_1(_) ->
Num = 2,
process_flag(trap_exit, true),
[spawn_link(fun test_proc/0)
|| _ <- lists:seq(1,Num)],
[receive
{'EXIT', _, Reason} ->
normal = Reason
end
|| _ <- lists:seq(1,Num)],
ok.
%% Process that will do a bunch of requests against mysql
test_proc() ->
[
#result_packet{} = emysql:execute(test_pool, "describe hello_table;")
|| _ <- lists:seq(1,1000)
].
%% Test Case: Make sure that the pool is still usable after a lock timeout
%% Test for race on connection where the connection is sent to a process
%% at the exact same time it hits the timeout.
%% See ransomr's first comment on Issue 9
%% Original name: no_lock_timeout(_)
%%--------------------------------------------------------------------
pool_leak_1(_) ->
Num = 2,
OldEnv = application:get_env(emysql, lock_timeout),
%% This is really hard to repro - the timing is very tricky.
%% On my machine a timeout of 1 seems to reproduce it after about 10 attempts.
%% Once it happens the connection is lost from the pool, so the pool needs to be reset.
application:set_env(emysql, lock_timeout, 1),
process_flag(trap_exit, true),
[spawn_link(fun test_proc/0)
|| _ <- lists:seq(1,Num)],
%% We expect one process to timeout, but the other should exit normally
receive
{'EXIT', _, Reason1} ->
connection_lock_timeout = Reason1
end,
receive
{'EXIT', _, Reason2} ->
normal = Reason2
end,
case OldEnv of
undefined ->
application:unset_env(emysql, lock_timeout);
{ok, Timeout} ->
application:set_env(emysql, lock_timeout, Timeout)
end,
ok.
%% Test Case: Test two processes trying to share one connection.
%% Test for race Issue #9.
%% If one process times out, then it got stuck likely by the first of
%% the races described by Ransom in Issue #9.
%% This is a mod of Ransom's original two_proc test that replicates
%% the race more consistently on some machines. /hd
%%--------------------------------------------------------------------
stuck_waiting_2(_) ->
% parellel processes, fast loops of sql command, timout factor vs need.
Processes = 2,
Loops = 10000,
LenienceFactor = Loops * 2,
% find needed time for the sql command
{Micro, _} = timer:tc(?MODULE, ?TRY_RACE, [1]),
Timeout = trunc(Micro / 1000 * LenienceFactor) + 100,
ct:log("Seen execution time: ~p �s (microseconds)~n", [Micro]),
ct:log("Set timeout: ~p ms (milliseconds)~n", [Timeout]),
%% Warn about time when run with actual MySQL query for TRY_RACE.
Timeout > 5000 andalso
ct:print("This race test will take approx. ~p seconds.~n", [round(Timeout/1000)]),
% set timeout
OldEnv = application:get_env(emysql, lock_timeout),
application:set_env(emysql, lock_timeout, Timeout),
process_flag(trap_exit, true),
[spawn_link(conn_mgr_SUITE, ?TRY_RACE, [Loops])
|| _ <- lists:seq(1,Processes)],
[receive
{'EXIT', _, normal} ->
ct:log("One process complete, other could now use connections.", []);
{'EXIT', _, connection_lock_timeout} ->
ct:log("One process exits stuck in timeout.", []),
ct:log("Timout was set to ~pms to be sure it can even outlast complete starvation by the other process and really indicatetes that even a free connection could be assigned to this process. Loops: ~p, used time for one sample SQL operation: ~p ms.",
[Timeout, Loops, Micro/1000]),
exit(issue9_stuck_waiting);
{'EXIT', _, Reason} -> exit({unexpected_error, Reason})
end
|| _ <- lists:seq(1,Processes)],
case OldEnv of
undefined ->
application:unset_env(emysql, lock_timeout);
{ok, Timeout} ->
application:set_env(emysql, lock_timeout, Timeout)
end,
ok.
%% Test Case: Make sure that no connections are lost from the pool.
%% Test for race Issue #9.
%% Test for race on connection where the connection is sent to a
%% process at the exact same time it hits the timeout.
%% This is a mod of Ransom's original pool_leak_1 test that
%% replicates the race more consistently on some machines. /hd
%%--------------------------------------------------------------------
pool_leak_2(_) ->
Processes = 100,
Loops = 10,
EmptyGb = gb_trees:empty(),
%% find this output via test/index.html
[Pool] = emysql_conn_mgr:pools(),
ct:log("Pool available at start: ~p~n", [Pool#pool.available]),
%% preliminary test
[#emysql_connection{pool_id = test_pool,
prepared = EmptyGb,
locked_at = undefined,
alive = true,
test_period = 0}] = queue:to_list(Pool#pool.available),
%% Brief timeout for test
OldEnv = application:get_env(emysql, lock_timeout),
application:set_env(emysql, lock_timeout, 1),
%% Make 100 processes grap and release connections in parallel
process_flag(trap_exit, true),
[spawn_link(?MODULE, lock_and_pass_connection, [Loops])
% Don't use ?TRY_RACE here, it races too rarely.
|| _ <- lists:seq(1,Processes)],
[receive _ -> ok end
|| _ <- lists:seq(1,Processes)],
%% Test if pool still has one connection (and available)
[Pool1] = emysql_conn_mgr:pools(),
ct:log("Pool available after test: ~p~n", [Pool1#pool.available]),
case queue:to_list(Pool1#pool.available) of
[#emysql_connection{pool_id = test_pool,
prepared = EmptyGb,
locked_at = undefined,
alive = true,
test_period = 0}] -> ok;
[] -> exit(issue9_pool_leak);
E -> exit({unexpected_error, E})
end,
%% Refit default timout for other tests -- TODO: fix for all tests.
case OldEnv of
undefined ->
application:unset_env(emysql, lock_timeout);
{ok, Timeout} ->
application:set_env(emysql, lock_timeout, Timeout)
end,
ok.
%% TRY_RACE: process that gets and releases a connection in fastest succession.
%% Substitute for make_queries/1, executing and also provoking races faster.
lock_and_pass_connection(Loops) ->
[ case emysql_conn_mgr:wait_for_connection(test_pool) of
Connection ->
emysql_conn_mgr:pass_connection(Connection)
end
|| _ <- lists:seq(1,Loops)
].
%% Alternate payload of TRY_RACE. Less canceled down stand-in for above.
%% Process that will do a bunch of requests against mysql
make_queries(Loops) ->
[ #result_packet{} = emysql:execute(test_pool, "describe hello_table")
|| _ <- lists:seq(1,Loops)
].
%% Test Case: Make sure that the pool is still usable after a waiting client dies
dying_client_does_not_lock_the_connection_out(_Config) ->
#result_packet{} = emysql:execute(test_pool, "select 1"),
F1 = fun() -> emysql:execute(test_pool, "select sleep(5);", 15000) end,
Client = spawn(F1),
timer:sleep(1000),
exit(Client, kill),
timer:sleep(1000),
#result_packet{} = emysql:execute(test_pool, "select 1").