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:     [{db_opts, [{server, server(DbType)}, {keepalive_interval, 2}, {start_interval, 10}]} | Config];
   63: init_per_testcase(_, Config) ->
   64:     DbType = ?config(db_type, Config),
   65:     set_opts(),
   66:     meck_db(DbType),
   67:     [{db_opts, [{server, server(DbType)}, {keepalive_interval, ?KEEPALIVE_INTERVAL},
   68:                 {start_interval, ?MAX_INTERVAL}]} | Config].
   69: 
   70: end_per_testcase(does_backoff_increase_to_a_point, Config) ->
   71:     meck_unload_rand(),
   72:     Db = ?config(db_type, Config),
   73:     meck_config_and_db_unload(Db),
   74:     unset_opts(),
   75:     Config;
   76: end_per_testcase(_, Config) ->
   77:     meck_config_and_db_unload(?config(db_type, Config)),
   78:     unset_opts(),
   79:     Config.
   80: 
   81: %% Test cases
   82: keepalive_interval(Config) ->
   83:     {ok, Pid} = gen_server:start(mongoose_rdbms, ?config(db_opts, Config), []),
   84:     timer:sleep(5500),
   85:     ?eq(5, query_calls(Config)),
   86:     true = erlang:exit(Pid, kill),
   87:     ok.
   88: 
   89: keepalive_exit(Config) ->
   90:     {ok, Pid} = gen_server:start(mongoose_rdbms, ?config(db_opts, Config), []),
   91:     Monitor = erlang:monitor(process, Pid),
   92:     timer:sleep(3500),
   93:     ?eq(3, query_calls(Config)),
   94:     meck_error(?config(db_type, Config)),
   95:     receive
   96:         {'DOWN', Monitor, process, Pid, {keepalive_failed, _}} ->
   97:             ok
   98:     after 1500 ->
   99:         ct:fail(no_down_message)
  100:     end.
  101: 
  102: %% 5 retries. Max retry 10. Initial retry 2.
  103: %% We should get a sequence: 2 -> 4 -> 10 -> 10 -> 10.
  104: does_backoff_increase_to_a_point(Config) ->
  105:     {error, _} = gen_server:start(mongoose_rdbms, ?config(db_opts, Config), []),
  106:     % We expect to have 2 at the beginning, then values up to 10 and 10 three times in total
  107:     receive_backoffs(2, 10, 3).
  108: 
  109: receive_backoffs(InitialValue, MaxValue, MaxCount) ->
  110:     receive_backoffs(InitialValue, MaxValue, MaxCount, 0).
  111: 
  112: receive_backoffs(ExpectedVal, MaxValue, MaxCountExpected, MaxCount) ->
  113:     receive
  114:         {backoff, MaxValue} when MaxCount =:= MaxCountExpected - 1 ->
  115:             ok;
  116:         {backoff, MaxValue} ->
  117:             receive_backoffs(MaxValue, MaxValue, MaxCountExpected, MaxCount + 1);
  118:         {backoff, ExpectedVal} ->
  119:             receive_backoffs(min(ExpectedVal * ExpectedVal, MaxValue), MaxValue, MaxCountExpected, MaxCount)
  120:     after 200 -> % Lower this
  121:             ct:fail(no_backoff)
  122:     end.
  123: 
  124: %% Mocks
  125: 
  126: meck_rand() ->
  127:     meck:new(rand, [unstick, no_link]),
  128:     Self = self(),
  129:     Fun = fun(Val) -> ct:log("sending backoff: ~p to pid: ~p~n", [Val, Self]), Self ! {backoff, Val}, 0 end,
  130:     meck:expect(rand, uniform, Fun).
  131: 
  132: meck_unload_rand() ->
  133:     meck:unload(rand).
  134: 
  135: set_opts() ->
  136:     [mongoose_config:set_opt(Key, Value) || {Key, Value} <- opts()].
  137: 
  138: unset_opts() ->
  139:     [mongoose_config:unset_opt(Key) || {Key, _Value} <- opts()].
  140: 
  141: opts() ->
  142:     [{all_metrics_are_global, false},
  143:      {max_fsm_queue, 1024}].
  144: 
  145: meck_db(odbc) ->
  146:     meck:new(eodbc, [no_link]),
  147:     meck:expect(mongoose_rdbms_odbc, disconnect, fun(_) -> ok end),
  148:     meck:expect(eodbc, connect, fun(_, _) -> {ok, self()} end),
  149:     meck:expect(eodbc, sql_query, fun(_Ref, _Query, _Timeout) -> {selected, ["row"]} end);
  150: meck_db(mysql) ->
  151:     meck:new(mysql, [no_link]),
  152:     meck:expect(mongoose_rdbms_mysql, disconnect, fun(_) -> ok end),
  153:     meck:expect(mysql, start_link, fun(_) -> {ok, self()} end),
  154:     meck:expect(mysql, query, fun(_Ref, _Query) -> {ok, [], []} end);
  155: meck_db(pgsql) ->
  156:     meck:new(epgsql, [no_link]),
  157:     meck:expect(mongoose_rdbms_pgsql, disconnect, fun(_) -> ok end),
  158:     meck:expect(epgsql, connect, fun(_) -> {ok, self()} end),
  159:     meck:expect(epgsql, squery,
  160:                 fun(_Ref, <<"SET", _/binary>>)       -> {ok, 0};
  161:                    (_Ref, [<<"SET", _/binary>> | _]) -> {ok, 0};
  162:                    (_Ref, [<<"SELECT", _/binary>>])  -> {ok, [], [{1}]} end).
  163: 
  164: meck_connection_error(pgsql) ->
  165:     meck:expect(epgsql, connect, fun(_) -> connection_error end);
  166: meck_connection_error(odbc) ->
  167:     meck:expect(eodbc, connect, fun(_, _) -> connection_error end);
  168: meck_connection_error(mysql) ->
  169:     meck:expect(mongoose_rdbms_mysql, connect, fun(_, _) -> {error, connection_error} end).
  170: 
  171: 
  172: meck_error(odbc) ->
  173:     meck:expect(eodbc, sql_query,
  174:                 fun(_Ref, _Query, _Timeout) ->
  175:                         {error, "connection broken"}
  176:                 end);
  177: meck_error(mysql) ->
  178:     meck:expect(mysql, query, fun(_Ref, _Query) -> {error, {123, "", <<"connection broken">>}} end);
  179: meck_error(pgsql) ->
  180:     meck:expect(epgsql, connect, fun(_) -> {ok, self()} end),
  181:     meck:expect(epgsql, squery,
  182:                 fun(_Ref, _Query) -> {error, {error, 2, 3, 4, <<"connection broken">>, 5}} end).
  183: 
  184: meck_config_and_db_unload(DbType) ->
  185:     do_meck_unload(DbType).
  186: 
  187: do_meck_unload(odbc) ->
  188:     meck:unload(eodbc);
  189: do_meck_unload(mysql) ->
  190:     meck:unload(mongoose_rdbms_mysql),
  191:     meck:unload(mysql);
  192: do_meck_unload(pgsql) ->
  193:     meck:unload(epgsql).
  194: 
  195: query_calls(Config) ->
  196:     DbType = ?config(db_type, Config),
  197:     {M, F} = mf(DbType),
  198:     meck:num_calls(M, F, a(DbType)).
  199: 
  200: mf(odbc) ->
  201:     {eodbc, sql_query};
  202: mf(mysql) ->
  203:     {mysql, query};
  204: mf(pgsql) ->
  205:     {epgsql, squery}.
  206: 
  207: a(odbc) ->
  208:     ['_', [?KEEPALIVE_QUERY], '_'];
  209: a(mysql) ->
  210:     ['_', [?KEEPALIVE_QUERY]];
  211: a(pgsql) ->
  212:     ['_', [?KEEPALIVE_QUERY]].
  213: 
  214: server(odbc) ->
  215:     "fake-connection-string";
  216: server(mysql) ->
  217:     {mysql, "fake-host", "fake-db", "fake-user", "fake-pass"};
  218: server(pgsql) ->
  219:     {pgsql, "fake-host", "fake-db", "fake-user", "fake-pass"}.