1: -module(mongoose_rdbms_SUITE).
    2: -include_lib("eunit/include/eunit.hrl").
    3: -include_lib("common_test/include/ct.hrl").
    4: -include("mongoose.hrl").
    5: 
    6: -compile([export_all, nowarn_export_all]).
    7: 
    8: -define(_eq(E, I), ?_assertEqual(E, I)).
    9: -define(eq(E, I), ?assertEqual(E, I)).
   10: -define(ne(E, I), ?assert(E =/= I)).
   11: 
   12: -define(KEEPALIVE_INTERVAL, 1).
   13: -define(KEEPALIVE_QUERY, <<"SELECT 1;">>).
   14: -define(MAX_INTERVAL, 30).
   15: 
   16: all() ->
   17:     [{group, odbc},
   18:      {group, mysql},
   19:      {group, pgsql}].
   20: 
   21: init_per_suite(Config) ->
   22:     application:ensure_all_started(exometer_core),
   23:     Config.
   24: 
   25: end_per_suite(_Config) ->
   26:     meck:unload(),
   27:     application:stop(exometer_core).
   28: 
   29: groups() ->
   30:     [{odbc, [], tests()},
   31:      {mysql, [], tests()},
   32:      {pgsql, [], tests()}].
   33: 
   34: tests() ->
   35:     [keepalive_interval,
   36:      does_backoff_increase_to_a_point,
   37:      keepalive_exit].
   38: 
   39: init_per_group(odbc, Config) ->
   40:     case code:ensure_loaded(eodbc) of
   41:         {module, eodbc} ->
   42:             mongoose_backend:init(global, mongoose_rdbms, [], #{backend => odbc}),
   43:             [{db_type, odbc} | Config];
   44:         _ ->
   45:             {skip, no_odbc_application}
   46:     end;
   47: init_per_group(Group, Config) ->
   48:     mongoose_backend:init(global, mongoose_rdbms, [], #{backend => Group}),
   49:     [{db_type, Group} | Config].
   50: 
   51: end_per_group(_, Config) ->
   52:     % clean up after mongoose_backend:init
   53:     persistent_term:erase({backend_module, global, mongoose_rdbms}),
   54:     Config.
   55: 
   56: init_per_testcase(does_backoff_increase_to_a_point, Config) ->
   57:     DbType = ?config(db_type, Config),
   58:     set_opts(),
   59:     meck_db(DbType),
   60:     meck_connection_error(DbType),
   61:     meck_rand(),
   62:     ServerOpts = server_opts(DbType),
   63:     [{db_opts, ServerOpts#{query_timeout => 5000, keepalive_interval => 2, max_start_interval => 10}} | Config];
   64: init_per_testcase(_, Config) ->
   65:     DbType = ?config(db_type, Config),
   66:     set_opts(),
   67:     meck_db(DbType),
   68:     ServerOpts = server_opts(DbType),
   69:     [{db_opts, ServerOpts#{query_timeout => 5000, keepalive_interval => ?KEEPALIVE_INTERVAL,
   70:                            max_start_interval => ?MAX_INTERVAL}} | Config].
   71: 
   72: end_per_testcase(does_backoff_increase_to_a_point, Config) ->
   73:     meck_unload_rand(),
   74:     Db = ?config(db_type, Config),
   75:     meck_config_and_db_unload(Db),
   76:     unset_opts(),
   77:     Config;
   78: end_per_testcase(_, Config) ->
   79:     meck_config_and_db_unload(?config(db_type, Config)),
   80:     unset_opts(),
   81:     Config.
   82: 
   83: %% Test cases
   84: keepalive_interval(Config) ->
   85:     {ok, Pid} = gen_server:start(mongoose_rdbms, ?config(db_opts, Config), []),
   86:     timer:sleep(5500),
   87:     ?eq(5, query_calls(Config)),
   88:     true = erlang:exit(Pid, kill),
   89:     ok.
   90: 
   91: keepalive_exit(Config) ->
   92:     {ok, Pid} = gen_server:start(mongoose_rdbms, ?config(db_opts, Config), []),
   93:     Monitor = erlang:monitor(process, Pid),
   94:     timer:sleep(3500),
   95:     ?eq(3, query_calls(Config)),
   96:     meck_error(?config(db_type, Config)),
   97:     receive
   98:         {'DOWN', Monitor, process, Pid, {keepalive_failed, _}} ->
   99:             ok
  100:     after 1500 ->
  101:         ct:fail(no_down_message)
  102:     end.
  103: 
  104: %% 5 retries. Max retry 10. Initial retry 2.
  105: %% We should get a sequence: 2 -> 4 -> 10 -> 10 -> 10.
  106: does_backoff_increase_to_a_point(Config) ->
  107:     {error, _} = gen_server:start(mongoose_rdbms, ?config(db_opts, Config), []),
  108:     % We expect to have 2 at the beginning, then values up to 10 and 10 three times in total
  109:     receive_backoffs(2, 10, 3).
  110: 
  111: receive_backoffs(InitialValue, MaxValue, MaxCount) ->
  112:     receive_backoffs(InitialValue, MaxValue, MaxCount, 0).
  113: 
  114: receive_backoffs(ExpectedVal, MaxValue, MaxCountExpected, MaxCount) ->
  115:     receive
  116:         {backoff, MaxValue} when MaxCount =:= MaxCountExpected - 1 ->
  117:             ok;
  118:         {backoff, MaxValue} ->
  119:             receive_backoffs(MaxValue, MaxValue, MaxCountExpected, MaxCount + 1);
  120:         {backoff, ExpectedVal} ->
  121:             receive_backoffs(min(ExpectedVal * ExpectedVal, MaxValue), MaxValue, MaxCountExpected, MaxCount)
  122:     after 200 -> % Lower this
  123:             ct:fail(no_backoff)
  124:     end.
  125: 
  126: %% Mocks
  127: 
  128: meck_rand() ->
  129:     meck:new(rand, [unstick, no_link]),
  130:     Self = self(),
  131:     Fun = fun(Val) -> ct:log("sending backoff: ~p to pid: ~p~n", [Val, Self]), Self ! {backoff, Val}, 0 end,
  132:     meck:expect(rand, uniform, Fun).
  133: 
  134: meck_unload_rand() ->
  135:     meck:unload(rand).
  136: 
  137: set_opts() ->
  138:     mongoose_config:set_opts(opts()).
  139: 
  140: unset_opts() ->
  141:     mongoose_config:erase_opts().
  142: 
  143: opts() ->
  144:     #{all_metrics_are_global => false,
  145:       max_fsm_queue => 1024}.
  146: 
  147: meck_db(odbc) ->
  148:     meck:new(eodbc, [no_link]),
  149:     meck:expect(mongoose_rdbms_odbc, disconnect, fun(_) -> ok end),
  150:     meck:expect(eodbc, connect, fun(_, _) -> {ok, self()} end),
  151:     meck:expect(eodbc, sql_query, fun(_Ref, _Query, _Timeout) -> {selected, ["row"]} end);
  152: meck_db(mysql) ->
  153:     meck:new(mysql, [no_link]),
  154:     meck:expect(mongoose_rdbms_mysql, disconnect, fun(_) -> ok end),
  155:     meck:expect(mysql, start_link, fun(_) -> {ok, self()} end),
  156:     meck:expect(mysql, query, fun(_Ref, _Query) -> {ok, [], []} end);
  157: meck_db(pgsql) ->
  158:     meck:new(epgsql, [no_link]),
  159:     meck:expect(mongoose_rdbms_pgsql, disconnect, fun(_) -> ok end),
  160:     meck:expect(epgsql, connect, fun(_) -> {ok, self()} end),
  161:     meck:expect(epgsql, squery,
  162:                 fun(_Ref, <<"SET", _/binary>>)       -> {ok, 0};
  163:                    (_Ref, [<<"SET", _/binary>> | _]) -> {ok, 0};
  164:                    (_Ref, [<<"SELECT", _/binary>>])  -> {ok, [], [{1}]} end).
  165: 
  166: meck_connection_error(pgsql) ->
  167:     meck:expect(epgsql, connect, fun(_) -> connection_error end);
  168: meck_connection_error(odbc) ->
  169:     meck:expect(eodbc, connect, fun(_, _) -> connection_error end);
  170: meck_connection_error(mysql) ->
  171:     meck:expect(mongoose_rdbms_mysql, connect, fun(_, _) -> {error, connection_error} end).
  172: 
  173: 
  174: meck_error(odbc) ->
  175:     meck:expect(eodbc, sql_query,
  176:                 fun(_Ref, _Query, _Timeout) ->
  177:                         {error, "connection broken"}
  178:                 end);
  179: meck_error(mysql) ->
  180:     meck:expect(mysql, query, fun(_Ref, _Query) -> {error, {123, "", <<"connection broken">>}} end);
  181: meck_error(pgsql) ->
  182:     meck:expect(epgsql, connect, fun(_) -> {ok, self()} end),
  183:     meck:expect(epgsql, squery,
  184:                 fun(_Ref, _Query) -> {error, {error, 2, 3, 4, <<"connection broken">>, 5}} end).
  185: 
  186: meck_config_and_db_unload(DbType) ->
  187:     do_meck_unload(DbType).
  188: 
  189: do_meck_unload(odbc) ->
  190:     meck:unload(eodbc);
  191: do_meck_unload(mysql) ->
  192:     meck:unload(mongoose_rdbms_mysql),
  193:     meck:unload(mysql);
  194: do_meck_unload(pgsql) ->
  195:     meck:unload(epgsql).
  196: 
  197: query_calls(Config) ->
  198:     DbType = ?config(db_type, Config),
  199:     {M, F} = mf(DbType),
  200:     meck:num_calls(M, F, a(DbType)).
  201: 
  202: mf(odbc) ->
  203:     {eodbc, sql_query};
  204: mf(mysql) ->
  205:     {mysql, query};
  206: mf(pgsql) ->
  207:     {epgsql, squery}.
  208: 
  209: a(odbc) ->
  210:     ['_', [?KEEPALIVE_QUERY], '_'];
  211: a(mysql) ->
  212:     ['_', [?KEEPALIVE_QUERY]];
  213: a(pgsql) ->
  214:     ['_', [?KEEPALIVE_QUERY]].
  215: 
  216: server_opts(odbc) ->
  217:     #{driver => odbc, settings => "fake-connection-string"};
  218: server_opts(mysql) ->
  219:     #{driver => mysql, host => "fake-host", port => 3306,
  220:       database => "fake-db", username => "fake-user", password => "fake-pass"};
  221: server_opts(pgsql) ->
  222:     #{driver => pgsql, host => "fake-host", port => 5432,
  223:       database => "fake-db", username => "fake-user", password => "fake-pass"}.