feat(oracle): check whether target table exists

Fixes https://emqx.atlassian.net/browse/EMQX-9026
This commit is contained in:
Paulo Zulato 2023-05-09 19:49:12 -03:00
parent c9a2ddf98c
commit 5f10936091
2 changed files with 185 additions and 50 deletions

View File

@ -179,18 +179,39 @@ sql_drop_table() ->
sql_check_table_exist() -> sql_check_table_exist() ->
"SELECT COUNT(*) FROM user_tables WHERE table_name = 'MQTT_TEST'". "SELECT COUNT(*) FROM user_tables WHERE table_name = 'MQTT_TEST'".
new_jamdb_connection(Config) ->
JamdbOpts = [
{host, ?config(oracle_host, Config)},
{port, ?config(oracle_port, Config)},
{user, "system"},
{password, "oracle"},
{sid, ?SID}
],
jamdb_oracle:start(JamdbOpts).
close_jamdb_connection(Conn) ->
jamdb_oracle:stop(Conn).
reset_table(Config) -> reset_table(Config) ->
ResourceId = resource_id(Config), {ok, Conn} = new_jamdb_connection(Config),
drop_table_if_exists(Config), try
{ok, [{proc_result, 0, _}]} = emqx_resource:simple_sync_query( ok = drop_table_if_exists(Conn),
ResourceId, {sql, sql_create_table()} {ok, [{proc_result, 0, _}]} = jamdb_oracle:sql_query(Conn, sql_create_table())
), after
close_jamdb_connection(Conn)
end,
ok. ok.
drop_table_if_exists(Conn) when is_pid(Conn) ->
{ok, [{proc_result, 0, _}]} = jamdb_oracle:sql_query(Conn, sql_drop_table()),
ok;
drop_table_if_exists(Config) -> drop_table_if_exists(Config) ->
ResourceId = resource_id(Config), {ok, Conn} = new_jamdb_connection(Config),
{ok, [{proc_result, 0, _}]} = try
emqx_resource:simple_sync_query(ResourceId, {query, sql_drop_table()}), ok = drop_table_if_exists(Conn)
after
close_jamdb_connection(Conn)
end,
ok. ok.
oracle_config(TestCase, _ConnectionType, Config) -> oracle_config(TestCase, _ConnectionType, Config) ->
@ -216,7 +237,7 @@ oracle_config(TestCase, _ConnectionType, Config) ->
" pool_size = 1\n" " pool_size = 1\n"
" sql = \"~s\"\n" " sql = \"~s\"\n"
" resource_opts = {\n" " resource_opts = {\n"
" health_check_interval = \"5s\"\n" " health_check_interval = \"15s\"\n"
" request_ttl = \"30s\"\n" " request_ttl = \"30s\"\n"
" query_mode = \"async\"\n" " query_mode = \"async\"\n"
" batch_size = 3\n" " batch_size = 3\n"
@ -349,13 +370,13 @@ t_sync_query(Config) ->
ResourceId = resource_id(Config), ResourceId = resource_id(Config),
?check_trace( ?check_trace(
begin begin
reset_table(Config),
?assertMatch({ok, _}, create_bridge_api(Config)), ?assertMatch({ok, _}, create_bridge_api(Config)),
?retry( ?retry(
_Sleep = 1_000, _Sleep = 1_000,
_Attempts = 20, _Attempts = 20,
?assertEqual({ok, connected}, emqx_resource_manager:health_check(ResourceId)) ?assertEqual({ok, connected}, emqx_resource_manager:health_check(ResourceId))
), ),
reset_table(Config),
MsgId = erlang:unique_integer(), MsgId = erlang:unique_integer(),
Params = #{ Params = #{
topic => ?config(mqtt_topic, Config), topic => ?config(mqtt_topic, Config),
@ -381,13 +402,13 @@ t_batch_sync_query(Config) ->
BridgeId = bridge_id(Config), BridgeId = bridge_id(Config),
?check_trace( ?check_trace(
begin begin
reset_table(Config),
?assertMatch({ok, _}, create_bridge_api(Config)), ?assertMatch({ok, _}, create_bridge_api(Config)),
?retry( ?retry(
_Sleep = 1_000, _Sleep = 1_000,
_Attempts = 30, _Attempts = 30,
?assertEqual({ok, connected}, emqx_resource_manager:health_check(ResourceId)) ?assertEqual({ok, connected}, emqx_resource_manager:health_check(ResourceId))
), ),
reset_table(Config),
MsgId = erlang:unique_integer(), MsgId = erlang:unique_integer(),
Params = #{ Params = #{
topic => ?config(mqtt_topic, Config), topic => ?config(mqtt_topic, Config),
@ -464,6 +485,7 @@ t_start_stop(Config) ->
ResourceId = resource_id(Config), ResourceId = resource_id(Config),
?check_trace( ?check_trace(
begin begin
reset_table(Config),
?assertMatch({ok, _}, create_bridge(Config)), ?assertMatch({ok, _}, create_bridge(Config)),
%% Since the connection process is async, we give it some time to %% Since the connection process is async, we give it some time to
%% stabilize and avoid flakiness. %% stabilize and avoid flakiness.
@ -515,6 +537,7 @@ t_on_get_status(Config) ->
ProxyHost = ?config(proxy_host, Config), ProxyHost = ?config(proxy_host, Config),
ProxyName = ?config(proxy_name, Config), ProxyName = ?config(proxy_name, Config),
ResourceId = resource_id(Config), ResourceId = resource_id(Config),
reset_table(Config),
?assertMatch({ok, _}, create_bridge(Config)), ?assertMatch({ok, _}, create_bridge(Config)),
%% Since the connection process is async, we give it some time to %% Since the connection process is async, we give it some time to
%% stabilize and avoid flakiness. %% stabilize and avoid flakiness.
@ -547,10 +570,45 @@ t_no_sid_nor_service_name(Config0) ->
), ),
ok. ok.
t_missing_table(Config) ->
ResourceId = resource_id(Config),
?check_trace(
begin
drop_table_if_exists(Config),
?assertMatch({ok, _}, create_bridge_api(Config)),
?retry(
_Sleep = 1_000,
_Attempts = 20,
?assertMatch(
{ok, Status} when Status =:= disconnected orelse Status =:= connecting,
emqx_resource_manager:health_check(ResourceId)
)
),
MsgId = erlang:unique_integer(),
Params = #{
topic => ?config(mqtt_topic, Config),
id => MsgId,
payload => ?config(oracle_name, Config),
retain => true
},
Message = {send_message, Params},
?assertMatch(
{error, {resource_error, #{reason := not_connected}}},
emqx_resource:simple_sync_query(ResourceId, Message)
),
ok
end,
fun(Trace) ->
?assertNotMatch([], ?of_kind(oracle_undefined_table, Trace)),
ok
end
).
t_table_removed(Config) -> t_table_removed(Config) ->
ResourceId = resource_id(Config), ResourceId = resource_id(Config),
?check_trace( ?check_trace(
begin begin
reset_table(Config),
?assertMatch({ok, _}, create_bridge_api(Config)), ?assertMatch({ok, _}, create_bridge_api(Config)),
?retry( ?retry(
_Sleep = 1_000, _Sleep = 1_000,

View File

@ -9,8 +9,6 @@
-include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/logger.hrl").
-include_lib("snabbkaffe/include/snabbkaffe.hrl"). -include_lib("snabbkaffe/include/snabbkaffe.hrl").
-define(ORACLE_DEFAULT_PORT, 1521).
%%==================================================================== %%====================================================================
%% Exports %% Exports
%%==================================================================== %%====================================================================
@ -26,7 +24,7 @@
]). ]).
%% callbacks for ecpool %% callbacks for ecpool
-export([connect/1, prepare_sql_to_conn/2]). -export([connect/1, prepare_sql_to_conn/3]).
%% Internal exports used to execute code with ecpool worker %% Internal exports used to execute code with ecpool worker
-export([ -export([
@ -39,18 +37,15 @@
oracle_host_options/0 oracle_host_options/0
]). ]).
-define(ACTION_SEND_MESSAGE, send_message). -define(ORACLE_DEFAULT_PORT, 1521).
-define(SYNC_QUERY_MODE, no_handover). -define(SYNC_QUERY_MODE, no_handover).
-define(DEFAULT_POOL_SIZE, 8).
-define(OPT_TIMEOUT, 30000).
-define(MAX_CURSORS, 10).
-define(ORACLE_HOST_OPTIONS, #{ -define(ORACLE_HOST_OPTIONS, #{
default_port => ?ORACLE_DEFAULT_PORT default_port => ?ORACLE_DEFAULT_PORT
}). }).
-define(MAX_CURSORS, 10).
-define(DEFAULT_POOL_SIZE, 8).
-define(OPT_TIMEOUT, 30000).
-type prepares() :: #{atom() => binary()}. -type prepares() :: #{atom() => binary()}.
-type params_tokens() :: #{atom() => list()}. -type params_tokens() :: #{atom() => list()}.
@ -105,7 +100,7 @@ on_start(
], ],
PoolName = InstId, PoolName = InstId,
Prepares = parse_prepare_sql(Config), Prepares = parse_prepare_sql(Config),
InitState = #{pool_name => PoolName, prepare_statement => #{}}, InitState = #{pool_name => PoolName},
State = maps:merge(InitState, Prepares), State = maps:merge(InitState, Prepares),
case emqx_resource_pool:start(InstId, ?MODULE, Options) of case emqx_resource_pool:start(InstId, ?MODULE, Options) of
ok -> ok ->
@ -148,7 +143,7 @@ on_query(
on_batch_query( on_batch_query(
InstId, InstId,
BatchReq, BatchReq,
#{pool_name := PoolName, params_tokens := Tokens, prepare_statement := Sts} = State #{pool_name := PoolName, params_tokens := Tokens, prepare_sql := Sts} = State
) -> ) ->
case BatchReq of case BatchReq of
[{Key, _} = Request | _] -> [{Key, _} = Request | _] ->
@ -241,7 +236,13 @@ on_get_status(_InstId, #{pool_name := Pool} = State) ->
connected; connected;
{ok, NState} -> {ok, NState} ->
%% return new state with prepared statements %% return new state with prepared statements
{connected, NState} {connected, NState};
{error, _Reason} ->
%% do not log error, it is logged in prepare_sql_to_conn
connecting;
{undefined_table, NState} ->
%% return new state indicating that we are connected but the target table is not created
{disconnected, NState, unhealthy_target}
end; end;
false -> false ->
disconnected disconnected
@ -250,11 +251,42 @@ on_get_status(_InstId, #{pool_name := Pool} = State) ->
do_get_status(Conn) -> do_get_status(Conn) ->
ok == element(1, jamdb_oracle:sql_query(Conn, "select 1 from dual")). ok == element(1, jamdb_oracle:sql_query(Conn, "select 1 from dual")).
do_check_prepares(#{prepare_sql := Prepares}) when is_map(Prepares) -> do_check_prepares(
ok; #{
do_check_prepares(State = #{pool_name := PoolName, prepare_sql := {error, Prepares}}) -> pool_name := PoolName,
{ok, Sts} = prepare_sql(Prepares, PoolName), prepare_sql := #{<<"send_message">> := SQL},
{ok, State#{prepare_sql => Prepares, prepare_statement := Sts}}. params_tokens := #{<<"send_message">> := Tokens}
} = State
) ->
% it's already connected. Verify if target table still exists
Workers = [Worker || {_WorkerName, Worker} <- ecpool:workers(PoolName)],
lists:foldl(
fun
(WorkerPid, ok) ->
{ok, Conn} = ecpool_worker:client(WorkerPid),
case check_if_table_exists(Conn, SQL, Tokens) of
{error, undefined_table} -> {undefined_table, State};
_ -> ok
end;
(_, Acc) ->
Acc
end,
ok,
Workers
);
do_check_prepares(
State = #{pool_name := PoolName, prepare_sql := {error, Prepares}, params_tokens := TokensMap}
) ->
case prepare_sql(Prepares, PoolName, TokensMap) of
%% remove the error
{ok, Sts} ->
{ok, State#{prepare_sql => Sts}};
{error, undefined_table} ->
%% indicate the error
{undefined_table, State#{prepare_sql => {error, Prepares}}};
{error, _Reason} = Error ->
Error
end.
%% =================================================================== %% ===================================================================
@ -312,36 +344,81 @@ parse_prepare_sql([], Prepares, Tokens) ->
params_tokens => Tokens params_tokens => Tokens
}. }.
init_prepare(State = #{prepare_sql := Prepares, pool_name := PoolName}) -> init_prepare(State = #{prepare_sql := Prepares, pool_name := PoolName, params_tokens := TokensMap}) ->
{ok, Sts} = prepare_sql(Prepares, PoolName), case prepare_sql(Prepares, PoolName, TokensMap) of
State#{prepare_statement := Sts}. {ok, Sts} ->
State#{prepare_sql := Sts};
Error ->
LogMeta = #{
msg => <<"Oracle Database init prepare statement failed">>, error => Error
},
?SLOG(error, LogMeta),
%% mark the prepare_sql as failed
State#{prepare_sql => {error, Prepares}}
end.
prepare_sql(Prepares, PoolName) when is_map(Prepares) -> prepare_sql(Prepares, PoolName, TokensMap) when is_map(Prepares) ->
prepare_sql(maps:to_list(Prepares), PoolName); prepare_sql(maps:to_list(Prepares), PoolName, TokensMap);
prepare_sql(Prepares, PoolName) -> prepare_sql(Prepares, PoolName, TokensMap) ->
Data = do_prepare_sql(Prepares, PoolName), case do_prepare_sql(Prepares, PoolName, TokensMap) of
{ok, _Sts} = Data, {ok, _Sts} = Ok ->
ecpool:add_reconnect_callback(PoolName, {?MODULE, prepare_sql_to_conn, [Prepares]}), %% prepare for reconnect
Data. ecpool:add_reconnect_callback(PoolName, {?MODULE, prepare_sql_to_conn, [Prepares]}),
Ok;
Error ->
Error
end.
do_prepare_sql(Prepares, PoolName) -> do_prepare_sql(Prepares, PoolName, TokensMap) ->
do_prepare_sql(ecpool:workers(PoolName), Prepares, PoolName, #{}). do_prepare_sql(ecpool:workers(PoolName), Prepares, PoolName, TokensMap, #{}).
do_prepare_sql([{_Name, Worker} | T], Prepares, PoolName, _LastSts) -> do_prepare_sql([{_Name, Worker} | T], Prepares, PoolName, TokensMap, _LastSts) ->
{ok, Conn} = ecpool_worker:client(Worker), {ok, Conn} = ecpool_worker:client(Worker),
{ok, Sts} = prepare_sql_to_conn(Conn, Prepares), case prepare_sql_to_conn(Conn, Prepares, TokensMap) of
do_prepare_sql(T, Prepares, PoolName, Sts); {ok, Sts} ->
do_prepare_sql([], _Prepares, _PoolName, LastSts) -> do_prepare_sql(T, Prepares, PoolName, TokensMap, Sts);
Error ->
Error
end;
do_prepare_sql([], _Prepares, _PoolName, _TokensMap, LastSts) ->
{ok, LastSts}. {ok, LastSts}.
prepare_sql_to_conn(Conn, Prepares) -> prepare_sql_to_conn(Conn, Prepares, TokensMap) ->
prepare_sql_to_conn(Conn, Prepares, #{}). prepare_sql_to_conn(Conn, Prepares, TokensMap, #{}).
prepare_sql_to_conn(Conn, [], Statements) when is_pid(Conn) -> {ok, Statements}; prepare_sql_to_conn(Conn, [], _TokensMap, Statements) when is_pid(Conn) -> {ok, Statements};
prepare_sql_to_conn(Conn, [{Key, SQL} | PrepareList], Statements) when is_pid(Conn) -> prepare_sql_to_conn(Conn, [{Key, SQL} | PrepareList], TokensMap, Statements) when is_pid(Conn) ->
LogMeta = #{msg => "Oracle Database Prepare Statement", name => Key, prepare_sql => SQL}, LogMeta = #{msg => "Oracle Database Prepare Statement", name => Key, prepare_sql => SQL},
Tokens = maps:get(Key, TokensMap, []),
?SLOG(info, LogMeta), ?SLOG(info, LogMeta),
prepare_sql_to_conn(Conn, PrepareList, Statements#{Key => SQL}). case check_if_table_exists(Conn, SQL, Tokens) of
ok ->
?SLOG(info, LogMeta#{result => success}),
prepare_sql_to_conn(Conn, PrepareList, TokensMap, Statements#{Key => SQL});
{error, undefined_table} = Error ->
%% Target table is not created
?SLOG(error, LogMeta#{result => failed, reason => "table does not exist"}),
?tp(oracle_undefined_table, #{}),
Error;
Error ->
Error
end.
check_if_table_exists(Conn, SQL, Tokens) ->
{Event, _Headers} = emqx_rule_events:eventmsg_publish(
emqx_message:make(<<"t/opic">>, "test query")
),
SqlQuery = "begin " ++ binary_to_list(SQL) ++ "; rollback; end;",
Params = emqx_placeholder:proc_sql(Tokens, Event),
case jamdb_oracle:sql_query(Conn, {SqlQuery, Params}) of
{ok, [{proc_result, 0, _Description}]} ->
ok;
{ok, [{proc_result, 6550, _Description}]} ->
%% Target table is not created
{error, undefined_table};
Reason ->
{error, Reason}
end.
to_bin(Bin) when is_binary(Bin) -> to_bin(Bin) when is_binary(Bin) ->
Bin; Bin;