501 lines
16 KiB
Erlang
501 lines
16 KiB
Erlang
%%--------------------------------------------------------------------
|
|
%% Copyright (c) 2020-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
|
%%
|
|
%% Licensed under the Apache License, Version 2.0 (the "License");
|
|
%% you may not use this file except in compliance with the License.
|
|
%% You may obtain a copy of the License at
|
|
%%
|
|
%% http://www.apache.org/licenses/LICENSE-2.0
|
|
%%
|
|
%% Unless required by applicable law or agreed to in writing, software
|
|
%% distributed under the License is distributed on an "AS IS" BASIS,
|
|
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
%% See the License for the specific language governing permissions and
|
|
%% limitations under the License.
|
|
%%--------------------------------------------------------------------
|
|
-module(emqx_connector_pgsql).
|
|
|
|
-include("emqx_connector.hrl").
|
|
-include_lib("typerefl/include/types.hrl").
|
|
-include_lib("emqx/include/logger.hrl").
|
|
-include_lib("hocon/include/hoconsc.hrl").
|
|
-include_lib("epgsql/include/epgsql.hrl").
|
|
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
|
|
|
|
-export([roots/0, fields/1]).
|
|
|
|
-behaviour(emqx_resource).
|
|
|
|
%% callbacks of behaviour emqx_resource
|
|
-export([
|
|
callback_mode/0,
|
|
on_start/2,
|
|
on_stop/2,
|
|
on_query/3,
|
|
on_batch_query/3,
|
|
on_get_status/2
|
|
]).
|
|
|
|
-export([connect/1]).
|
|
|
|
-export([
|
|
query/3,
|
|
prepared_query/3,
|
|
execute_batch/3
|
|
]).
|
|
|
|
%% for ecpool workers usage
|
|
-export([do_get_status/1, prepare_sql_to_conn/2]).
|
|
|
|
-define(PGSQL_HOST_OPTIONS, #{
|
|
default_port => ?PGSQL_DEFAULT_PORT
|
|
}).
|
|
|
|
-type prepares() :: #{atom() => binary()}.
|
|
-type params_tokens() :: #{atom() => list()}.
|
|
|
|
-type state() ::
|
|
#{
|
|
pool_name := binary(),
|
|
prepare_sql := prepares(),
|
|
params_tokens := params_tokens(),
|
|
prepare_statement := epgsql:statement()
|
|
}.
|
|
|
|
%%=====================================================================
|
|
|
|
roots() ->
|
|
[{config, #{type => hoconsc:ref(?MODULE, config)}}].
|
|
|
|
fields(config) ->
|
|
[{server, server()}] ++
|
|
adjust_fields(emqx_connector_schema_lib:relational_db_fields()) ++
|
|
emqx_connector_schema_lib:ssl_fields() ++
|
|
emqx_connector_schema_lib:prepare_statement_fields().
|
|
|
|
server() ->
|
|
Meta = #{desc => ?DESC("server")},
|
|
emqx_schema:servers_sc(Meta, ?PGSQL_HOST_OPTIONS).
|
|
|
|
adjust_fields(Fields) ->
|
|
lists:map(
|
|
fun
|
|
({username, OrigUsernameFn}) ->
|
|
{username, fun
|
|
(required) ->
|
|
true;
|
|
(Any) ->
|
|
OrigUsernameFn(Any)
|
|
end};
|
|
(Field) ->
|
|
Field
|
|
end,
|
|
Fields
|
|
).
|
|
|
|
%% ===================================================================
|
|
callback_mode() -> always_sync.
|
|
|
|
-spec on_start(binary(), hoconsc:config()) -> {ok, state()} | {error, _}.
|
|
on_start(
|
|
InstId,
|
|
#{
|
|
server := Server,
|
|
database := DB,
|
|
username := User,
|
|
pool_size := PoolSize,
|
|
ssl := SSL
|
|
} = Config
|
|
) ->
|
|
#{hostname := Host, port := Port} = emqx_schema:parse_server(Server, ?PGSQL_HOST_OPTIONS),
|
|
?SLOG(info, #{
|
|
msg => "starting_postgresql_connector",
|
|
connector => InstId,
|
|
config => emqx_utils:redact(Config)
|
|
}),
|
|
SslOpts =
|
|
case maps:get(enable, SSL) of
|
|
true ->
|
|
[
|
|
%% note: this is converted to `required' in
|
|
%% `conn_opts/2', and there's a boolean guard
|
|
%% there; if this is set to `required' here,
|
|
%% that'll require changing `conn_opts/2''s guard.
|
|
{ssl, true},
|
|
{ssl_opts, emqx_tls_lib:to_client_opts(SSL)}
|
|
];
|
|
false ->
|
|
[{ssl, false}]
|
|
end,
|
|
Options = [
|
|
{host, Host},
|
|
{port, Port},
|
|
{username, User},
|
|
{password, emqx_secret:wrap(maps:get(password, Config, ""))},
|
|
{database, DB},
|
|
{auto_reconnect, ?AUTO_RECONNECT_INTERVAL},
|
|
{pool_size, PoolSize}
|
|
],
|
|
State = parse_prepare_sql(Config),
|
|
case emqx_resource_pool:start(InstId, ?MODULE, Options ++ SslOpts) of
|
|
ok ->
|
|
{ok, init_prepare(State#{pool_name => InstId, prepare_statement => #{}})};
|
|
{error, Reason} ->
|
|
?tp(
|
|
pgsql_connector_start_failed,
|
|
#{error => Reason}
|
|
),
|
|
{error, Reason}
|
|
end.
|
|
|
|
on_stop(InstId, _State) ->
|
|
?SLOG(info, #{
|
|
msg => "stopping postgresql connector",
|
|
connector => InstId
|
|
}),
|
|
emqx_resource_pool:stop(InstId).
|
|
|
|
on_query(InstId, {TypeOrKey, NameOrSQL}, State) ->
|
|
on_query(InstId, {TypeOrKey, NameOrSQL, []}, State);
|
|
on_query(
|
|
InstId,
|
|
{TypeOrKey, NameOrSQL, Params},
|
|
#{pool_name := PoolName} = State
|
|
) ->
|
|
?SLOG(debug, #{
|
|
msg => "postgresql connector received sql query",
|
|
connector => InstId,
|
|
type => TypeOrKey,
|
|
sql => NameOrSQL,
|
|
state => State
|
|
}),
|
|
Type = pgsql_query_type(TypeOrKey),
|
|
{NameOrSQL2, Data} = proc_sql_params(TypeOrKey, NameOrSQL, Params, State),
|
|
Res = on_sql_query(InstId, PoolName, Type, NameOrSQL2, Data),
|
|
handle_result(Res).
|
|
|
|
pgsql_query_type(sql) ->
|
|
query;
|
|
pgsql_query_type(query) ->
|
|
query;
|
|
pgsql_query_type(prepared_query) ->
|
|
prepared_query;
|
|
%% for bridge
|
|
pgsql_query_type(_) ->
|
|
pgsql_query_type(prepared_query).
|
|
|
|
on_batch_query(
|
|
InstId,
|
|
BatchReq,
|
|
#{pool_name := PoolName, params_tokens := Tokens, prepare_statement := Sts} = State
|
|
) ->
|
|
case BatchReq of
|
|
[{Key, _} = Request | _] ->
|
|
BinKey = to_bin(Key),
|
|
case maps:get(BinKey, Tokens, undefined) of
|
|
undefined ->
|
|
Log = #{
|
|
connector => InstId,
|
|
first_request => Request,
|
|
state => State,
|
|
msg => "batch prepare not implemented"
|
|
},
|
|
?SLOG(error, Log),
|
|
{error, {unrecoverable_error, batch_prepare_not_implemented}};
|
|
TokenList ->
|
|
{_, Datas} = lists:unzip(BatchReq),
|
|
Datas2 = [emqx_placeholder:proc_sql(TokenList, Data) || Data <- Datas],
|
|
St = maps:get(BinKey, Sts),
|
|
case on_sql_query(InstId, PoolName, execute_batch, St, Datas2) of
|
|
{error, _Error} = Result ->
|
|
handle_result(Result);
|
|
{_Column, Results} ->
|
|
handle_batch_result(Results, 0)
|
|
end
|
|
end;
|
|
_ ->
|
|
Log = #{
|
|
connector => InstId,
|
|
request => BatchReq,
|
|
state => State,
|
|
msg => "invalid request"
|
|
},
|
|
?SLOG(error, Log),
|
|
{error, {unrecoverable_error, invalid_request}}
|
|
end.
|
|
|
|
proc_sql_params(query, SQLOrKey, Params, _State) ->
|
|
{SQLOrKey, Params};
|
|
proc_sql_params(prepared_query, SQLOrKey, Params, _State) ->
|
|
{SQLOrKey, Params};
|
|
proc_sql_params(TypeOrKey, SQLOrData, Params, #{params_tokens := ParamsTokens}) ->
|
|
Key = to_bin(TypeOrKey),
|
|
case maps:get(Key, ParamsTokens, undefined) of
|
|
undefined ->
|
|
{SQLOrData, Params};
|
|
Tokens ->
|
|
{Key, emqx_placeholder:proc_sql(Tokens, SQLOrData)}
|
|
end.
|
|
|
|
on_sql_query(InstId, PoolName, Type, NameOrSQL, Data) ->
|
|
try ecpool:pick_and_do(PoolName, {?MODULE, Type, [NameOrSQL, Data]}, no_handover) of
|
|
{error, Reason} = Result ->
|
|
?tp(
|
|
pgsql_connector_query_return,
|
|
#{error => Reason}
|
|
),
|
|
?SLOG(error, #{
|
|
msg => "postgresql connector do sql query failed",
|
|
connector => InstId,
|
|
type => Type,
|
|
sql => NameOrSQL,
|
|
reason => Reason
|
|
}),
|
|
case Reason of
|
|
ecpool_empty ->
|
|
{error, {recoverable_error, Reason}};
|
|
{error, error, _, undefined_table, _, _} ->
|
|
{error, {unrecoverable_error, Reason}};
|
|
_ ->
|
|
Result
|
|
end;
|
|
Result ->
|
|
?tp(
|
|
pgsql_connector_query_return,
|
|
#{result => Result}
|
|
),
|
|
Result
|
|
catch
|
|
error:function_clause:Stacktrace ->
|
|
?SLOG(error, #{
|
|
msg => "postgresql connector do sql query failed",
|
|
connector => InstId,
|
|
type => Type,
|
|
sql => NameOrSQL,
|
|
reason => function_clause,
|
|
stacktrace => Stacktrace
|
|
}),
|
|
{error, {unrecoverable_error, invalid_request}}
|
|
end.
|
|
|
|
on_get_status(_InstId, #{pool_name := PoolName} = State) ->
|
|
case emqx_resource_pool:health_check_workers(PoolName, fun ?MODULE:do_get_status/1) of
|
|
true ->
|
|
case do_check_prepares(State) of
|
|
ok ->
|
|
connected;
|
|
{ok, NState} ->
|
|
%% return new state with prepared statements
|
|
{connected, NState};
|
|
{error, {undefined_table, NState}} ->
|
|
%% return new state indicating that we are connected but the target table is not created
|
|
{disconnected, NState, unhealthy_target};
|
|
{error, _Reason} ->
|
|
%% do not log error, it is logged in prepare_sql_to_conn
|
|
connecting
|
|
end;
|
|
false ->
|
|
connecting
|
|
end.
|
|
|
|
do_get_status(Conn) ->
|
|
ok == element(1, epgsql:squery(Conn, "SELECT count(1) AS T")).
|
|
|
|
do_check_prepares(
|
|
#{
|
|
pool_name := PoolName,
|
|
prepare_sql := #{<<"send_message">> := SQL}
|
|
} = State
|
|
) ->
|
|
% it's already connected. Verify if target table still exists
|
|
Workers = [Worker || {_WorkerName, Worker} <- ecpool:workers(PoolName)],
|
|
lists:foldl(
|
|
fun
|
|
(WorkerPid, ok) ->
|
|
case ecpool_worker:client(WorkerPid) of
|
|
{ok, Conn} ->
|
|
case epgsql:parse2(Conn, "get_status", SQL, []) of
|
|
{error, {_, _, _, undefined_table, _, _}} ->
|
|
{error, {undefined_table, State}};
|
|
_ ->
|
|
ok
|
|
end;
|
|
_ ->
|
|
ok
|
|
end;
|
|
(_, Acc) ->
|
|
Acc
|
|
end,
|
|
ok,
|
|
Workers
|
|
);
|
|
do_check_prepares(#{prepare_sql := Prepares}) when is_map(Prepares) ->
|
|
ok;
|
|
do_check_prepares(State = #{pool_name := PoolName, prepare_sql := {error, Prepares}}) ->
|
|
%% retry to prepare
|
|
case prepare_sql(Prepares, PoolName) of
|
|
{ok, Sts} ->
|
|
%% remove the error
|
|
{ok, State#{prepare_sql => Prepares, prepare_statement := Sts}};
|
|
{error, undefined_table} ->
|
|
%% indicate the error
|
|
{error, {undefined_table, State#{prepare_sql => {error, Prepares}}}};
|
|
Error ->
|
|
{error, Error}
|
|
end.
|
|
|
|
%% ===================================================================
|
|
|
|
connect(Opts) ->
|
|
Host = proplists:get_value(host, Opts),
|
|
Username = proplists:get_value(username, Opts),
|
|
Password = emqx_secret:unwrap(proplists:get_value(password, Opts)),
|
|
case epgsql:connect(Host, Username, Password, conn_opts(Opts)) of
|
|
{ok, _Conn} = Ok ->
|
|
Ok;
|
|
{error, Reason} ->
|
|
{error, Reason}
|
|
end.
|
|
|
|
query(Conn, SQL, Params) ->
|
|
epgsql:equery(Conn, SQL, Params).
|
|
|
|
prepared_query(Conn, Name, Params) ->
|
|
epgsql:prepared_query2(Conn, Name, Params).
|
|
|
|
execute_batch(Conn, Statement, Params) ->
|
|
epgsql:execute_batch(Conn, Statement, Params).
|
|
|
|
conn_opts(Opts) ->
|
|
conn_opts(Opts, []).
|
|
conn_opts([], Acc) ->
|
|
Acc;
|
|
conn_opts([Opt = {database, _} | Opts], Acc) ->
|
|
conn_opts(Opts, [Opt | Acc]);
|
|
conn_opts([{ssl, Bool} | Opts], Acc) when is_boolean(Bool) ->
|
|
Flag =
|
|
case Bool of
|
|
true -> required;
|
|
false -> false
|
|
end,
|
|
conn_opts(Opts, [{ssl, Flag} | Acc]);
|
|
conn_opts([Opt = {port, _} | Opts], Acc) ->
|
|
conn_opts(Opts, [Opt | Acc]);
|
|
conn_opts([Opt = {timeout, _} | Opts], Acc) ->
|
|
conn_opts(Opts, [Opt | Acc]);
|
|
conn_opts([Opt = {ssl_opts, _} | Opts], Acc) ->
|
|
conn_opts(Opts, [Opt | Acc]);
|
|
conn_opts([_Opt | Opts], Acc) ->
|
|
conn_opts(Opts, Acc).
|
|
|
|
parse_prepare_sql(Config) ->
|
|
SQL =
|
|
case maps:get(prepare_statement, Config, undefined) of
|
|
undefined ->
|
|
case maps:get(sql, Config, undefined) of
|
|
undefined -> #{};
|
|
Template -> #{<<"send_message">> => Template}
|
|
end;
|
|
Any ->
|
|
Any
|
|
end,
|
|
parse_prepare_sql(maps:to_list(SQL), #{}, #{}).
|
|
|
|
parse_prepare_sql([{Key, H} | T], Prepares, Tokens) ->
|
|
{PrepareSQL, ParamsTokens} = emqx_placeholder:preproc_sql(H, '$n'),
|
|
parse_prepare_sql(
|
|
T, Prepares#{Key => PrepareSQL}, Tokens#{Key => ParamsTokens}
|
|
);
|
|
parse_prepare_sql([], Prepares, Tokens) ->
|
|
#{
|
|
prepare_sql => Prepares,
|
|
params_tokens => Tokens
|
|
}.
|
|
|
|
init_prepare(State = #{prepare_sql := Prepares, pool_name := PoolName}) ->
|
|
case maps:size(Prepares) of
|
|
0 ->
|
|
State;
|
|
_ ->
|
|
case prepare_sql(Prepares, PoolName) of
|
|
{ok, Sts} ->
|
|
State#{prepare_statement := Sts};
|
|
Error ->
|
|
LogMeta = #{
|
|
msg => <<"PostgreSQL init prepare statement failed">>, error => Error
|
|
},
|
|
?SLOG(error, LogMeta),
|
|
%% mark the prepare_sql as failed
|
|
State#{prepare_sql => {error, Prepares}}
|
|
end
|
|
end.
|
|
|
|
prepare_sql(Prepares, PoolName) when is_map(Prepares) ->
|
|
prepare_sql(maps:to_list(Prepares), PoolName);
|
|
prepare_sql(Prepares, PoolName) ->
|
|
case do_prepare_sql(Prepares, PoolName) of
|
|
{ok, _Sts} = Ok ->
|
|
%% prepare for reconnect
|
|
ecpool:add_reconnect_callback(PoolName, {?MODULE, prepare_sql_to_conn, [Prepares]}),
|
|
Ok;
|
|
Error ->
|
|
Error
|
|
end.
|
|
|
|
do_prepare_sql(Prepares, PoolName) ->
|
|
do_prepare_sql(ecpool:workers(PoolName), Prepares, #{}).
|
|
|
|
do_prepare_sql([{_Name, Worker} | T], Prepares, _LastSts) ->
|
|
{ok, Conn} = ecpool_worker:client(Worker),
|
|
case prepare_sql_to_conn(Conn, Prepares) of
|
|
{ok, Sts} ->
|
|
do_prepare_sql(T, Prepares, Sts);
|
|
Error ->
|
|
Error
|
|
end;
|
|
do_prepare_sql([], _Prepares, LastSts) ->
|
|
{ok, LastSts}.
|
|
|
|
prepare_sql_to_conn(Conn, Prepares) ->
|
|
prepare_sql_to_conn(Conn, Prepares, #{}).
|
|
|
|
prepare_sql_to_conn(Conn, [], Statements) when is_pid(Conn) -> {ok, Statements};
|
|
prepare_sql_to_conn(Conn, [{Key, SQL} | PrepareList], Statements) when is_pid(Conn) ->
|
|
LogMeta = #{msg => "PostgreSQL Prepare Statement", name => Key, prepare_sql => SQL},
|
|
?SLOG(info, LogMeta),
|
|
case epgsql:parse2(Conn, Key, SQL, []) of
|
|
{ok, Statement} ->
|
|
prepare_sql_to_conn(Conn, PrepareList, Statements#{Key => Statement});
|
|
{error, {error, error, _, undefined_table, _, _} = Error} ->
|
|
%% Target table is not created
|
|
?tp(pgsql_undefined_table, #{}),
|
|
?SLOG(error, LogMeta#{msg => "PostgreSQL parse failed", error => Error}),
|
|
{error, undefined_table};
|
|
{error, Error} = Other ->
|
|
?SLOG(error, LogMeta#{msg => "PostgreSQL parse failed", error => Error}),
|
|
Other
|
|
end.
|
|
|
|
to_bin(Bin) when is_binary(Bin) ->
|
|
Bin;
|
|
to_bin(Atom) when is_atom(Atom) ->
|
|
erlang:atom_to_binary(Atom).
|
|
|
|
handle_result({error, {recoverable_error, _Error}} = Res) ->
|
|
Res;
|
|
handle_result({error, {unrecoverable_error, _Error}} = Res) ->
|
|
Res;
|
|
handle_result({error, disconnected}) ->
|
|
{error, {recoverable_error, disconnected}};
|
|
handle_result({error, Error}) ->
|
|
{error, {unrecoverable_error, Error}};
|
|
handle_result(Res) ->
|
|
Res.
|
|
|
|
handle_batch_result([{ok, Count} | Rest], Acc) ->
|
|
handle_batch_result(Rest, Acc + Count);
|
|
handle_batch_result([{error, Error} | _Rest], _Acc) ->
|
|
{error, {unrecoverable_error, Error}};
|
|
handle_batch_result([], Acc) ->
|
|
{ok, Acc}.
|