feat(oracle): check whether target table exists
Fixes https://emqx.atlassian.net/browse/EMQX-9026
This commit is contained in:
parent
c9a2ddf98c
commit
5f10936091
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in New Issue