Merge pull request #10681 from zhongwencool/sync-release-50-to-master
chore: sync release 50 to master
This commit is contained in:
commit
ab6afdb0d2
|
@ -0,0 +1,17 @@
|
|||
version: '3.9'
|
||||
|
||||
services:
|
||||
rabbitmq:
|
||||
container_name: rabbitmq
|
||||
image: rabbitmq:3.11-management
|
||||
|
||||
restart: always
|
||||
expose:
|
||||
- "15672"
|
||||
- "5672"
|
||||
# We don't want to take ports from the host
|
||||
# ports:
|
||||
# - "15672:15672"
|
||||
# - "5672:5672"
|
||||
networks:
|
||||
- emqx_bridge
|
|
@ -25,8 +25,8 @@ services:
|
|||
- ./rocketmq/conf/broker.conf:/etc/rocketmq/broker.conf
|
||||
environment:
|
||||
NAMESRV_ADDR: "rocketmq_namesrv:9876"
|
||||
JAVA_OPTS: " -Duser.home=/opt"
|
||||
JAVA_OPT_EXT: "-server -Xms1024m -Xmx1024m -Xmn1024m"
|
||||
JAVA_OPTS: " -Duser.home=/opt -Drocketmq.broker.diskSpaceWarningLevelRatio=0.99"
|
||||
JAVA_OPT_EXT: "-server -Xms512m -Xmx512m -Xmn512m"
|
||||
command: ./mqbroker -c /etc/rocketmq/broker.conf
|
||||
depends_on:
|
||||
- mqnamesrv
|
||||
|
|
3
Makefile
3
Makefile
|
@ -15,7 +15,7 @@ endif
|
|||
|
||||
# Dashbord version
|
||||
# from https://github.com/emqx/emqx-dashboard5
|
||||
export EMQX_DASHBOARD_VERSION ?= v1.2.4
|
||||
export EMQX_DASHBOARD_VERSION ?= v1.2.4-1
|
||||
export EMQX_EE_DASHBOARD_VERSION ?= e1.0.6
|
||||
|
||||
# `:=` should be used here, otherwise the `$(shell ...)` will be executed every time when the variable is used
|
||||
|
@ -179,6 +179,7 @@ clean-all:
|
|||
@rm -f rebar.lock
|
||||
@rm -rf deps
|
||||
@rm -rf _build
|
||||
@rm -f emqx_dialyzer_*_plt
|
||||
|
||||
.PHONY: deps-all
|
||||
deps-all: $(REBAR) $(PROFILES:%=deps-%)
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 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.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
%% This file contains common macros for testing.
|
||||
%% It must not be used anywhere except in test suites.
|
||||
|
||||
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
|
||||
|
||||
-define(assertWaitEvent(Code, EventMatch, Timeout),
|
||||
?assertMatch(
|
||||
{_, {ok, EventMatch}},
|
||||
?wait_async_action(
|
||||
Code,
|
||||
EventMatch,
|
||||
Timeout
|
||||
)
|
||||
)
|
||||
).
|
|
@ -0,0 +1,42 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2017-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.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-define(CHANNEL_METRICS, [
|
||||
recv_pkt,
|
||||
recv_msg,
|
||||
'recv_msg.qos0',
|
||||
'recv_msg.qos1',
|
||||
'recv_msg.qos2',
|
||||
'recv_msg.dropped',
|
||||
'recv_msg.dropped.await_pubrel_timeout',
|
||||
send_pkt,
|
||||
send_msg,
|
||||
'send_msg.qos0',
|
||||
'send_msg.qos1',
|
||||
'send_msg.qos2',
|
||||
'send_msg.dropped',
|
||||
'send_msg.dropped.expired',
|
||||
'send_msg.dropped.queue_full',
|
||||
'send_msg.dropped.too_large'
|
||||
]).
|
||||
|
||||
-define(INFO_KEYS, [
|
||||
conninfo,
|
||||
conn_state,
|
||||
clientinfo,
|
||||
session,
|
||||
will_msg
|
||||
]).
|
|
@ -34,6 +34,7 @@
|
|||
-define(HP_BRIDGE, 870).
|
||||
-define(HP_DELAY_PUB, 860).
|
||||
%% apps that can stop the hooks chain from continuing
|
||||
-define(HP_NODE_REBALANCE, 110).
|
||||
-define(HP_EXHOOK, 100).
|
||||
|
||||
%% == Lowest Priority = 0, don't change this value as the plugins may depend on it.
|
||||
|
|
|
@ -32,10 +32,10 @@
|
|||
%% `apps/emqx/src/bpapi/README.md'
|
||||
|
||||
%% Community edition
|
||||
-define(EMQX_RELEASE_CE, "5.0.24").
|
||||
-define(EMQX_RELEASE_CE, "5.0.25-rc.1").
|
||||
|
||||
%% Enterprise edition
|
||||
-define(EMQX_RELEASE_EE, "5.0.3-rc.1").
|
||||
-define(EMQX_RELEASE_EE, "5.0.4-alpha.1").
|
||||
|
||||
%% the HTTP API version
|
||||
-define(EMQX_API_VERSION, "5.0").
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
{emqx_conf,2}.
|
||||
{emqx_dashboard,1}.
|
||||
{emqx_delayed,1}.
|
||||
{emqx_eviction_agent,1}.
|
||||
{emqx_exhook,1}.
|
||||
{emqx_gateway_api_listeners,1}.
|
||||
{emqx_gateway_cm,1}.
|
||||
|
@ -26,6 +27,10 @@
|
|||
{emqx_mgmt_cluster,1}.
|
||||
{emqx_mgmt_trace,1}.
|
||||
{emqx_mgmt_trace,2}.
|
||||
{emqx_node_rebalance,1}.
|
||||
{emqx_node_rebalance_api,1}.
|
||||
{emqx_node_rebalance_evacuation,1}.
|
||||
{emqx_node_rebalance_status,1}.
|
||||
{emqx_persistent_session,1}.
|
||||
{emqx_plugin_libs,1}.
|
||||
{emqx_plugins,1}.
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
-module(emqx_channel).
|
||||
|
||||
-include("emqx.hrl").
|
||||
-include("emqx_channel.hrl").
|
||||
-include("emqx_mqtt.hrl").
|
||||
-include("logger.hrl").
|
||||
-include("types.hrl").
|
||||
|
@ -57,6 +58,12 @@
|
|||
clear_keepalive/1
|
||||
]).
|
||||
|
||||
%% Export for emqx_channel implementations
|
||||
-export([
|
||||
maybe_nack/1,
|
||||
maybe_mark_as_delivered/2
|
||||
]).
|
||||
|
||||
%% Exports for CT
|
||||
-export([set_field/3]).
|
||||
|
||||
|
@ -69,7 +76,7 @@
|
|||
]
|
||||
).
|
||||
|
||||
-export_type([channel/0, opts/0]).
|
||||
-export_type([channel/0, opts/0, conn_state/0]).
|
||||
|
||||
-record(channel, {
|
||||
%% MQTT ConnInfo
|
||||
|
@ -131,33 +138,6 @@
|
|||
quota_timer => expire_quota_limit
|
||||
}).
|
||||
|
||||
-define(CHANNEL_METRICS, [
|
||||
recv_pkt,
|
||||
recv_msg,
|
||||
'recv_msg.qos0',
|
||||
'recv_msg.qos1',
|
||||
'recv_msg.qos2',
|
||||
'recv_msg.dropped',
|
||||
'recv_msg.dropped.await_pubrel_timeout',
|
||||
send_pkt,
|
||||
send_msg,
|
||||
'send_msg.qos0',
|
||||
'send_msg.qos1',
|
||||
'send_msg.qos2',
|
||||
'send_msg.dropped',
|
||||
'send_msg.dropped.expired',
|
||||
'send_msg.dropped.queue_full',
|
||||
'send_msg.dropped.too_large'
|
||||
]).
|
||||
|
||||
-define(INFO_KEYS, [
|
||||
conninfo,
|
||||
conn_state,
|
||||
clientinfo,
|
||||
session,
|
||||
will_msg
|
||||
]).
|
||||
|
||||
-define(LIMITER_ROUTING, message_routing).
|
||||
|
||||
-dialyzer({no_match, [shutdown/4, ensure_timer/2, interval/2]}).
|
||||
|
@ -1078,10 +1058,12 @@ handle_out(unsuback, {PacketId, _ReasonCodes}, Channel) ->
|
|||
handle_out(disconnect, ReasonCode, Channel) when is_integer(ReasonCode) ->
|
||||
ReasonName = disconnect_reason(ReasonCode),
|
||||
handle_out(disconnect, {ReasonCode, ReasonName}, Channel);
|
||||
handle_out(disconnect, {ReasonCode, ReasonName}, Channel = ?IS_MQTT_V5) ->
|
||||
Packet = ?DISCONNECT_PACKET(ReasonCode),
|
||||
handle_out(disconnect, {ReasonCode, ReasonName}, Channel) ->
|
||||
handle_out(disconnect, {ReasonCode, ReasonName, #{}}, Channel);
|
||||
handle_out(disconnect, {ReasonCode, ReasonName, Props}, Channel = ?IS_MQTT_V5) ->
|
||||
Packet = ?DISCONNECT_PACKET(ReasonCode, Props),
|
||||
{ok, [{outgoing, Packet}, {close, ReasonName}], Channel};
|
||||
handle_out(disconnect, {_ReasonCode, ReasonName}, Channel) ->
|
||||
handle_out(disconnect, {_ReasonCode, ReasonName, _Props}, Channel) ->
|
||||
{ok, {close, ReasonName}, Channel};
|
||||
handle_out(auth, {ReasonCode, Properties}, Channel) ->
|
||||
{ok, ?AUTH_PACKET(ReasonCode, Properties), Channel};
|
||||
|
@ -1198,13 +1180,19 @@ handle_call(
|
|||
{takeover, 'end'},
|
||||
Channel = #channel{
|
||||
session = Session,
|
||||
pendings = Pendings
|
||||
pendings = Pendings,
|
||||
conninfo = #{clientid := ClientId}
|
||||
}
|
||||
) ->
|
||||
ok = emqx_session:takeover(Session),
|
||||
%% TODO: Should not drain deliver here (side effect)
|
||||
Delivers = emqx_utils:drain_deliver(),
|
||||
AllPendings = lists:append(Delivers, Pendings),
|
||||
?tp(
|
||||
debug,
|
||||
emqx_channel_takeover_end,
|
||||
#{clientid => ClientId}
|
||||
),
|
||||
disconnect_and_shutdown(takenover, AllPendings, Channel);
|
||||
handle_call(list_authz_cache, Channel) ->
|
||||
{reply, emqx_authz_cache:list_authz_cache(), Channel};
|
||||
|
@ -1276,6 +1264,8 @@ handle_info(die_if_test = Info, Channel) ->
|
|||
die_if_test_compiled(),
|
||||
?SLOG(error, #{msg => "unexpected_info", info => Info}),
|
||||
{ok, Channel};
|
||||
handle_info({disconnect, ReasonCode, ReasonName, Props}, Channel) ->
|
||||
handle_out(disconnect, {ReasonCode, ReasonName, Props}, Channel);
|
||||
handle_info(Info, Channel) ->
|
||||
?SLOG(error, #{msg => "unexpected_info", info => Info}),
|
||||
{ok, Channel}.
|
||||
|
|
|
@ -23,6 +23,8 @@
|
|||
-include("logger.hrl").
|
||||
-include("types.hrl").
|
||||
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
|
||||
-include_lib("stdlib/include/qlc.hrl").
|
||||
-include_lib("stdlib/include/ms_transform.hrl").
|
||||
|
||||
-export([start_link/0]).
|
||||
|
||||
|
@ -72,6 +74,12 @@
|
|||
get_session_confs/2
|
||||
]).
|
||||
|
||||
%% Client management
|
||||
-export([
|
||||
channel_with_session_table/1,
|
||||
live_connection_table/1
|
||||
]).
|
||||
|
||||
%% gen_server callbacks
|
||||
-export([
|
||||
init/1,
|
||||
|
@ -593,6 +601,40 @@ all_channels() ->
|
|||
Pat = [{{'_', '$1'}, [], ['$1']}],
|
||||
ets:select(?CHAN_TAB, Pat).
|
||||
|
||||
%% @doc Get clientinfo for all clients with sessions
|
||||
channel_with_session_table(ConnModuleList) ->
|
||||
Ms = ets:fun2ms(
|
||||
fun({{ClientId, _ChanPid}, Info, _Stats}) ->
|
||||
{ClientId, Info}
|
||||
end
|
||||
),
|
||||
Table = ets:table(?CHAN_INFO_TAB, [{traverse, {select, Ms}}]),
|
||||
ConnModules = sets:from_list(ConnModuleList, [{version, 2}]),
|
||||
qlc:q([
|
||||
{ClientId, ConnState, ConnInfo, ClientInfo}
|
||||
|| {ClientId, #{
|
||||
conn_state := ConnState,
|
||||
clientinfo := ClientInfo,
|
||||
conninfo := #{clean_start := false, conn_mod := ConnModule} = ConnInfo
|
||||
}} <-
|
||||
Table,
|
||||
sets:is_element(ConnModule, ConnModules)
|
||||
]).
|
||||
|
||||
%% @doc Get all local connection query handle
|
||||
live_connection_table(ConnModules) ->
|
||||
Ms = lists:map(fun live_connection_ms/1, ConnModules),
|
||||
Table = ets:table(?CHAN_CONN_TAB, [{traverse, {select, Ms}}]),
|
||||
qlc:q([{ClientId, ChanPid} || {ClientId, ChanPid} <- Table, is_channel_connected(ChanPid)]).
|
||||
|
||||
live_connection_ms(ConnModule) ->
|
||||
{{{'$1', '$2'}, ConnModule}, [], [{{'$1', '$2'}}]}.
|
||||
|
||||
is_channel_connected(ChanPid) when node(ChanPid) =:= node() ->
|
||||
ets:member(?CHAN_LIVE_TAB, ChanPid);
|
||||
is_channel_connected(_ChanPid) ->
|
||||
false.
|
||||
|
||||
%% @doc Get all registered clientIDs. Debug/test interface
|
||||
all_client_ids() ->
|
||||
Pat = [{{'$1', '_'}, [], ['$1']}],
|
||||
|
@ -693,7 +735,8 @@ code_change(_OldVsn, State, _Extra) ->
|
|||
%%--------------------------------------------------------------------
|
||||
|
||||
clean_down({ChanPid, ClientId}) ->
|
||||
do_unregister_channel({ClientId, ChanPid}).
|
||||
do_unregister_channel({ClientId, ChanPid}),
|
||||
ok = ?tp(debug, emqx_cm_clean_down, #{client_id => ClientId}).
|
||||
|
||||
stats_fun() ->
|
||||
lists:foreach(fun update_stats/1, ?CHAN_STATS).
|
||||
|
@ -719,12 +762,12 @@ get_chann_conn_mod(ClientId, ChanPid) ->
|
|||
wrap_rpc(emqx_cm_proto_v1:get_chann_conn_mod(ClientId, ChanPid)).
|
||||
|
||||
mark_channel_connected(ChanPid) ->
|
||||
?tp(emqx_cm_connected_client_count_inc, #{}),
|
||||
?tp(emqx_cm_connected_client_count_inc, #{chan_pid => ChanPid}),
|
||||
ets:insert_new(?CHAN_LIVE_TAB, {ChanPid, true}),
|
||||
ok.
|
||||
|
||||
mark_channel_disconnected(ChanPid) ->
|
||||
?tp(emqx_cm_connected_client_count_dec, #{}),
|
||||
?tp(emqx_cm_connected_client_count_dec, #{chan_pid => ChanPid}),
|
||||
ets:delete(?CHAN_LIVE_TAB, ChanPid),
|
||||
ok.
|
||||
|
||||
|
|
|
@ -131,11 +131,9 @@ delete_root(Type) ->
|
|||
delete_bucket(?ROOT_ID, Type).
|
||||
|
||||
post_config_update([limiter], _Config, NewConf, _OldConf, _AppEnvs) ->
|
||||
Types = lists:delete(client, maps:keys(NewConf)),
|
||||
_ = [on_post_config_update(Type, NewConf) || Type <- Types],
|
||||
ok;
|
||||
post_config_update([limiter, Type], _Config, NewConf, _OldConf, _AppEnvs) ->
|
||||
on_post_config_update(Type, NewConf).
|
||||
Conf = emqx_limiter_schema:convert_node_opts(NewConf),
|
||||
_ = [on_post_config_update(Type, Cfg) || {Type, Cfg} <- maps:to_list(Conf)],
|
||||
ok.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% @doc
|
||||
|
@ -279,8 +277,7 @@ format_status(_Opt, Status) ->
|
|||
%%--------------------------------------------------------------------
|
||||
%% Internal functions
|
||||
%%--------------------------------------------------------------------
|
||||
on_post_config_update(Type, NewConf) ->
|
||||
Config = maps:get(Type, NewConf),
|
||||
on_post_config_update(Type, Config) ->
|
||||
case emqx_limiter_server:whereis(Type) of
|
||||
undefined ->
|
||||
start_server(Type, Config);
|
||||
|
|
|
@ -32,9 +32,14 @@
|
|||
get_bucket_cfg_path/2,
|
||||
desc/1,
|
||||
types/0,
|
||||
short_paths/0,
|
||||
calc_capacity/1,
|
||||
extract_with_type/2,
|
||||
default_client_config/0
|
||||
default_client_config/0,
|
||||
short_paths_fields/1,
|
||||
get_listener_opts/1,
|
||||
get_node_opts/1,
|
||||
convert_node_opts/1
|
||||
]).
|
||||
|
||||
-define(KILOBYTE, 1024).
|
||||
|
@ -104,11 +109,13 @@ roots() ->
|
|||
].
|
||||
|
||||
fields(limiter) ->
|
||||
short_paths_fields(?MODULE) ++
|
||||
[
|
||||
{Type,
|
||||
?HOCON(?R_REF(node_opts), #{
|
||||
desc => ?DESC(Type),
|
||||
importance => ?IMPORTANCE_HIDDEN,
|
||||
required => {false, recursively},
|
||||
aliases => alias_of_type(Type)
|
||||
})}
|
||||
|| Type <- types()
|
||||
|
@ -203,6 +210,14 @@ fields(listener_client_fields) ->
|
|||
fields(Type) ->
|
||||
simple_bucket_field(Type).
|
||||
|
||||
short_paths_fields(DesModule) ->
|
||||
[
|
||||
{Name,
|
||||
?HOCON(rate(), #{desc => ?DESC(DesModule, Name), required => false, example => Example})}
|
||||
|| {Name, Example} <-
|
||||
lists:zip(short_paths(), [<<"1000/s">>, <<"1000/s">>, <<"100MB/s">>])
|
||||
].
|
||||
|
||||
desc(limiter) ->
|
||||
"Settings for the rate limiter.";
|
||||
desc(node_opts) ->
|
||||
|
@ -236,6 +251,9 @@ get_bucket_cfg_path(Type, BucketName) ->
|
|||
types() ->
|
||||
[bytes, messages, connection, message_routing, internal].
|
||||
|
||||
short_paths() ->
|
||||
[max_conn_rate, messages_rate, bytes_rate].
|
||||
|
||||
calc_capacity(#{rate := infinity}) ->
|
||||
infinity;
|
||||
calc_capacity(#{rate := Rate, burst := Burst}) ->
|
||||
|
@ -266,6 +284,50 @@ default_client_config() ->
|
|||
failure_strategy => force
|
||||
}.
|
||||
|
||||
default_bucket_config() ->
|
||||
#{
|
||||
rate => infinity,
|
||||
burst => 0,
|
||||
initial => 0
|
||||
}.
|
||||
|
||||
get_listener_opts(Conf) ->
|
||||
Limiter = maps:get(limiter, Conf, undefined),
|
||||
ShortPaths = maps:with(short_paths(), Conf),
|
||||
get_listener_opts(Limiter, ShortPaths).
|
||||
|
||||
get_node_opts(Type) ->
|
||||
Opts = emqx:get_config([limiter, Type], default_bucket_config()),
|
||||
case type_to_short_path_name(Type) of
|
||||
undefined ->
|
||||
Opts;
|
||||
Name ->
|
||||
case emqx:get_config([limiter, Name], undefined) of
|
||||
undefined ->
|
||||
Opts;
|
||||
Rate ->
|
||||
Opts#{rate := Rate}
|
||||
end
|
||||
end.
|
||||
|
||||
convert_node_opts(Conf) ->
|
||||
DefBucket = default_bucket_config(),
|
||||
ShorPaths = short_paths(),
|
||||
Fun = fun
|
||||
%% The `client` in the node options was deprecated
|
||||
(client, _Value, Acc) ->
|
||||
Acc;
|
||||
(Name, Value, Acc) ->
|
||||
case lists:member(Name, ShorPaths) of
|
||||
true ->
|
||||
Type = short_path_name_to_type(Name),
|
||||
Acc#{Type => DefBucket#{rate => Value}};
|
||||
_ ->
|
||||
Acc#{Name => Value}
|
||||
end
|
||||
end,
|
||||
maps:fold(Fun, #{}, Conf).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Internal functions
|
||||
%%--------------------------------------------------------------------
|
||||
|
@ -476,3 +538,42 @@ merge_client_bucket(Type, _, {ok, BucketVal}) ->
|
|||
#{Type => BucketVal};
|
||||
merge_client_bucket(_, _, _) ->
|
||||
undefined.
|
||||
|
||||
short_path_name_to_type(max_conn_rate) ->
|
||||
connection;
|
||||
short_path_name_to_type(messages_rate) ->
|
||||
messages;
|
||||
short_path_name_to_type(bytes_rate) ->
|
||||
bytes.
|
||||
|
||||
type_to_short_path_name(connection) ->
|
||||
max_conn_rate;
|
||||
type_to_short_path_name(messages) ->
|
||||
messages_rate;
|
||||
type_to_short_path_name(bytes) ->
|
||||
bytes_rate;
|
||||
type_to_short_path_name(_) ->
|
||||
undefined.
|
||||
|
||||
get_listener_opts(Limiter, ShortPaths) when map_size(ShortPaths) =:= 0 ->
|
||||
Limiter;
|
||||
get_listener_opts(undefined, ShortPaths) ->
|
||||
convert_listener_short_paths(ShortPaths);
|
||||
get_listener_opts(Limiter, ShortPaths) ->
|
||||
Shorts = convert_listener_short_paths(ShortPaths),
|
||||
emqx_utils_maps:deep_merge(Limiter, Shorts).
|
||||
|
||||
convert_listener_short_paths(ShortPaths) ->
|
||||
DefBucket = default_bucket_config(),
|
||||
DefClient = default_client_config(),
|
||||
Fun = fun(Name, Rate, Acc) ->
|
||||
Type = short_path_name_to_type(Name),
|
||||
case Name of
|
||||
max_conn_rate ->
|
||||
Acc#{Type => DefBucket#{rate => Rate}};
|
||||
_ ->
|
||||
Client = maps:get(client, Acc, #{}),
|
||||
Acc#{client => Client#{Type => DefClient#{rate => Rate}}}
|
||||
end
|
||||
end,
|
||||
maps:fold(Fun, #{}, ShortPaths).
|
||||
|
|
|
@ -481,7 +481,7 @@ dispatch_burst_to_buckets([], _, Alloced, Buckets) ->
|
|||
|
||||
-spec init_tree(emqx_limiter_schema:limiter_type()) -> state().
|
||||
init_tree(Type) when is_atom(Type) ->
|
||||
Cfg = emqx:get_config([limiter, Type]),
|
||||
Cfg = emqx_limiter_schema:get_node_opts(Type),
|
||||
init_tree(Type, Cfg).
|
||||
|
||||
init_tree(Type, #{rate := Rate} = Cfg) ->
|
||||
|
@ -625,13 +625,10 @@ find_referenced_bucket(Id, Type, #{rate := Rate} = Cfg) when Rate =/= infinity -
|
|||
{error, invalid_bucket}
|
||||
end;
|
||||
%% this is a node-level reference
|
||||
find_referenced_bucket(Id, Type, _) ->
|
||||
case emqx:get_config([limiter, Type], undefined) of
|
||||
find_referenced_bucket(_Id, Type, _) ->
|
||||
case emqx_limiter_schema:get_node_opts(Type) of
|
||||
#{rate := infinity} ->
|
||||
false;
|
||||
undefined ->
|
||||
?SLOG(error, #{msg => "invalid limiter type", type => Type, id => Id}),
|
||||
{error, invalid_bucket};
|
||||
NodeCfg ->
|
||||
{ok, Bucket} = emqx_limiter_manager:find_root(Type),
|
||||
{ok, Bucket, NodeCfg}
|
||||
|
|
|
@ -86,7 +86,7 @@ init([]) ->
|
|||
%% Internal functions
|
||||
%%--==================================================================
|
||||
make_child(Type) ->
|
||||
Cfg = emqx:get_config([limiter, Type]),
|
||||
Cfg = emqx_limiter_schema:get_node_opts(Type),
|
||||
make_child(Type, Cfg).
|
||||
|
||||
make_child(Type, Cfg) ->
|
||||
|
|
|
@ -347,7 +347,8 @@ do_start_listener(Type, ListenerName, #{bind := ListenOn} = Opts) when
|
|||
Type == tcp; Type == ssl
|
||||
->
|
||||
Id = listener_id(Type, ListenerName),
|
||||
add_limiter_bucket(Id, Opts),
|
||||
Limiter = limiter(Opts),
|
||||
add_limiter_bucket(Id, Limiter),
|
||||
esockd:open(
|
||||
Id,
|
||||
ListenOn,
|
||||
|
@ -356,7 +357,7 @@ do_start_listener(Type, ListenerName, #{bind := ListenOn} = Opts) when
|
|||
#{
|
||||
listener => {Type, ListenerName},
|
||||
zone => zone(Opts),
|
||||
limiter => limiter(Opts),
|
||||
limiter => Limiter,
|
||||
enable_authn => enable_authn(Opts)
|
||||
}
|
||||
]}
|
||||
|
@ -366,9 +367,10 @@ do_start_listener(Type, ListenerName, #{bind := ListenOn} = Opts) when
|
|||
Type == ws; Type == wss
|
||||
->
|
||||
Id = listener_id(Type, ListenerName),
|
||||
add_limiter_bucket(Id, Opts),
|
||||
Limiter = limiter(Opts),
|
||||
add_limiter_bucket(Id, Limiter),
|
||||
RanchOpts = ranch_opts(Type, ListenOn, Opts),
|
||||
WsOpts = ws_opts(Type, ListenerName, Opts),
|
||||
WsOpts = ws_opts(Type, ListenerName, Opts, Limiter),
|
||||
case Type of
|
||||
ws -> cowboy:start_clear(Id, RanchOpts, WsOpts);
|
||||
wss -> cowboy:start_tls(Id, RanchOpts, WsOpts)
|
||||
|
@ -415,20 +417,22 @@ do_start_listener(quic, ListenerName, #{bind := Bind} = Opts) ->
|
|||
Password -> [{password, str(Password)}]
|
||||
end ++
|
||||
optional_quic_listener_opts(Opts),
|
||||
Limiter = limiter(Opts),
|
||||
ConnectionOpts = #{
|
||||
conn_callback => emqx_quic_connection,
|
||||
peer_unidi_stream_count => maps:get(peer_unidi_stream_count, Opts, 1),
|
||||
peer_bidi_stream_count => maps:get(peer_bidi_stream_count, Opts, 10),
|
||||
zone => zone(Opts),
|
||||
listener => {quic, ListenerName},
|
||||
limiter => limiter(Opts)
|
||||
limiter => Limiter
|
||||
},
|
||||
StreamOpts = #{
|
||||
stream_callback => emqx_quic_stream,
|
||||
active => 1
|
||||
},
|
||||
|
||||
Id = listener_id(quic, ListenerName),
|
||||
add_limiter_bucket(Id, Opts),
|
||||
add_limiter_bucket(Id, Limiter),
|
||||
quicer:start_listener(
|
||||
Id,
|
||||
ListenOn,
|
||||
|
@ -532,12 +536,12 @@ esockd_opts(ListenerId, Type, Opts0) ->
|
|||
end
|
||||
).
|
||||
|
||||
ws_opts(Type, ListenerName, Opts) ->
|
||||
ws_opts(Type, ListenerName, Opts, Limiter) ->
|
||||
WsPaths = [
|
||||
{emqx_utils_maps:deep_get([websocket, mqtt_path], Opts, "/mqtt"), emqx_ws_connection, #{
|
||||
zone => zone(Opts),
|
||||
listener => {Type, ListenerName},
|
||||
limiter => limiter(Opts),
|
||||
limiter => Limiter,
|
||||
enable_authn => enable_authn(Opts)
|
||||
}}
|
||||
],
|
||||
|
@ -651,28 +655,31 @@ zone(Opts) ->
|
|||
maps:get(zone, Opts, undefined).
|
||||
|
||||
limiter(Opts) ->
|
||||
maps:get(limiter, Opts, undefined).
|
||||
emqx_limiter_schema:get_listener_opts(Opts).
|
||||
|
||||
add_limiter_bucket(Id, #{limiter := Limiter}) ->
|
||||
add_limiter_bucket(_Id, undefined) ->
|
||||
ok;
|
||||
add_limiter_bucket(Id, Limiter) ->
|
||||
maps:fold(
|
||||
fun(Type, Cfg, _) ->
|
||||
emqx_limiter_server:add_bucket(Id, Type, Cfg)
|
||||
end,
|
||||
ok,
|
||||
maps:without([client], Limiter)
|
||||
);
|
||||
add_limiter_bucket(_Id, _Cfg) ->
|
||||
ok.
|
||||
).
|
||||
|
||||
del_limiter_bucket(Id, #{limiter := Limiters}) ->
|
||||
del_limiter_bucket(Id, Conf) ->
|
||||
case limiter(Conf) of
|
||||
undefined ->
|
||||
ok;
|
||||
Limiter ->
|
||||
lists:foreach(
|
||||
fun(Type) ->
|
||||
emqx_limiter_server:del_bucket(Id, Type)
|
||||
end,
|
||||
maps:keys(Limiters)
|
||||
);
|
||||
del_limiter_bucket(_Id, _Cfg) ->
|
||||
ok.
|
||||
maps:keys(Limiter)
|
||||
)
|
||||
end.
|
||||
|
||||
enable_authn(Opts) ->
|
||||
maps:get(enable_authn, Opts, true).
|
||||
|
|
|
@ -167,9 +167,15 @@ handle_info(Info, State) ->
|
|||
{noreply, State}.
|
||||
|
||||
terminate(_Reason, _State) ->
|
||||
try
|
||||
ok = ekka:unmonitor(membership),
|
||||
emqx_stats:cancel_update(route_stats),
|
||||
mnesia:unsubscribe({table, ?ROUTING_NODE, simple}).
|
||||
mnesia:unsubscribe({table, ?ROUTING_NODE, simple})
|
||||
catch
|
||||
exit:{noproc, {gen_server, call, [mria_membership, _]}} ->
|
||||
?SLOG(warning, #{msg => "mria_membership_down"}),
|
||||
ok
|
||||
end.
|
||||
|
||||
code_change(_OldVsn, State, _Extra) ->
|
||||
{ok, State}.
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
-type bar_separated_list() :: list().
|
||||
-type ip_port() :: tuple() | integer().
|
||||
-type cipher() :: map().
|
||||
-type port_number() :: 1..65536.
|
||||
-type port_number() :: 1..65535.
|
||||
-type server_parse_option() :: #{
|
||||
default_port => port_number(),
|
||||
no_port => boolean(),
|
||||
|
@ -135,7 +135,8 @@
|
|||
cipher/0,
|
||||
comma_separated_atoms/0,
|
||||
url/0,
|
||||
json_binary/0
|
||||
json_binary/0,
|
||||
port_number/0
|
||||
]).
|
||||
|
||||
-export([namespace/0, roots/0, roots/1, fields/1, desc/1, tags/0]).
|
||||
|
@ -2001,7 +2002,8 @@ base_listener(Bind) ->
|
|||
listener_fields
|
||||
),
|
||||
#{
|
||||
desc => ?DESC(base_listener_limiter)
|
||||
desc => ?DESC(base_listener_limiter),
|
||||
importance => ?IMPORTANCE_HIDDEN
|
||||
}
|
||||
)},
|
||||
{"enable_authn",
|
||||
|
@ -2012,7 +2014,7 @@ base_listener(Bind) ->
|
|||
default => true
|
||||
}
|
||||
)}
|
||||
].
|
||||
] ++ emqx_limiter_schema:short_paths_fields(?MODULE).
|
||||
|
||||
desc("persistent_session_store") ->
|
||||
"Settings for message persistence.";
|
||||
|
@ -2187,8 +2189,8 @@ filter(Opts) ->
|
|||
|
||||
%% @private This function defines the SSL opts which are commonly used by
|
||||
%% SSL listener and client.
|
||||
-spec common_ssl_opts_schema(map()) -> hocon_schema:field_schema().
|
||||
common_ssl_opts_schema(Defaults) ->
|
||||
-spec common_ssl_opts_schema(map(), server | client) -> hocon_schema:field_schema().
|
||||
common_ssl_opts_schema(Defaults, Type) ->
|
||||
D = fun(Field) -> maps:get(to_atom(Field), Defaults, undefined) end,
|
||||
Df = fun(Field, Default) -> maps:get(to_atom(Field), Defaults, Default) end,
|
||||
Collection = maps:get(versions, Defaults, tls_all_available),
|
||||
|
@ -2198,7 +2200,7 @@ common_ssl_opts_schema(Defaults) ->
|
|||
sc(
|
||||
binary(),
|
||||
#{
|
||||
default => D("cacertfile"),
|
||||
default => cert_file("cacert.pem", Type),
|
||||
required => false,
|
||||
desc => ?DESC(common_ssl_opts_schema_cacertfile)
|
||||
}
|
||||
|
@ -2207,7 +2209,7 @@ common_ssl_opts_schema(Defaults) ->
|
|||
sc(
|
||||
binary(),
|
||||
#{
|
||||
default => D("certfile"),
|
||||
default => cert_file("cert.pem", Type),
|
||||
required => false,
|
||||
desc => ?DESC(common_ssl_opts_schema_certfile)
|
||||
}
|
||||
|
@ -2216,7 +2218,7 @@ common_ssl_opts_schema(Defaults) ->
|
|||
sc(
|
||||
binary(),
|
||||
#{
|
||||
default => D("keyfile"),
|
||||
default => cert_file("key.pem", Type),
|
||||
required => false,
|
||||
desc => ?DESC(common_ssl_opts_schema_keyfile)
|
||||
}
|
||||
|
@ -2314,7 +2316,7 @@ common_ssl_opts_schema(Defaults) ->
|
|||
server_ssl_opts_schema(Defaults, IsRanchListener) ->
|
||||
D = fun(Field) -> maps:get(to_atom(Field), Defaults, undefined) end,
|
||||
Df = fun(Field, Default) -> maps:get(to_atom(Field), Defaults, Default) end,
|
||||
common_ssl_opts_schema(Defaults) ++
|
||||
common_ssl_opts_schema(Defaults, server) ++
|
||||
[
|
||||
{"dhfile",
|
||||
sc(
|
||||
|
@ -2440,7 +2442,7 @@ crl_outer_validator(_SSLOpts) ->
|
|||
%% @doc Make schema for SSL client.
|
||||
-spec client_ssl_opts_schema(map()) -> hocon_schema:field_schema().
|
||||
client_ssl_opts_schema(Defaults) ->
|
||||
common_ssl_opts_schema(Defaults) ++
|
||||
common_ssl_opts_schema(Defaults, client) ++
|
||||
[
|
||||
{"enable",
|
||||
sc(
|
||||
|
@ -3260,13 +3262,10 @@ default_listener(ws) ->
|
|||
};
|
||||
default_listener(SSLListener) ->
|
||||
%% The env variable is resolved in emqx_tls_lib by calling naive_env_interpolate
|
||||
CertFile = fun(Name) ->
|
||||
iolist_to_binary("${EMQX_ETC_DIR}/" ++ filename:join(["certs", Name]))
|
||||
end,
|
||||
SslOptions = #{
|
||||
<<"cacertfile">> => CertFile(<<"cacert.pem">>),
|
||||
<<"certfile">> => CertFile(<<"cert.pem">>),
|
||||
<<"keyfile">> => CertFile(<<"key.pem">>)
|
||||
<<"cacertfile">> => cert_file(<<"cacert.pem">>, server),
|
||||
<<"certfile">> => cert_file(<<"cert.pem">>, server),
|
||||
<<"keyfile">> => cert_file(<<"key.pem">>, server)
|
||||
},
|
||||
case SSLListener of
|
||||
ssl ->
|
||||
|
@ -3383,3 +3382,6 @@ ensure_default_listener(#{<<"default">> := _} = Map, _ListenerType) ->
|
|||
ensure_default_listener(Map, ListenerType) ->
|
||||
NewMap = Map#{<<"default">> => default_listener(ListenerType)},
|
||||
keep_default_tombstone(NewMap, #{}).
|
||||
|
||||
cert_file(_File, client) -> undefined;
|
||||
cert_file(File, server) -> iolist_to_binary(filename:join(["${EMQX_ETC_DIR}", "certs", File])).
|
||||
|
|
|
@ -47,7 +47,9 @@
|
|||
-type param_types() :: #{emqx_bpapi:var_name() => _Type}.
|
||||
|
||||
%% Applications and modules we wish to ignore in the analysis:
|
||||
-define(IGNORED_APPS, "gen_rpc, recon, redbug, observer_cli, snabbkaffe, ekka, mria").
|
||||
-define(IGNORED_APPS,
|
||||
"gen_rpc, recon, redbug, observer_cli, snabbkaffe, ekka, mria, amqp_client, rabbit_common"
|
||||
).
|
||||
-define(IGNORED_MODULES, "emqx_rpc").
|
||||
%% List of known RPC backend modules:
|
||||
-define(RPC_MODULES, "gen_rpc, erpc, rpc, emqx_rpc").
|
||||
|
|
|
@ -967,20 +967,11 @@ do_t_validations(_Config) ->
|
|||
{error, {_, _, ResRaw3}} = update_listener_via_api(ListenerId, ListenerData3),
|
||||
#{<<"code">> := <<"BAD_REQUEST">>, <<"message">> := MsgRaw3} =
|
||||
emqx_utils_json:decode(ResRaw3, [return_maps]),
|
||||
%% we can't remove certfile now, because it has default value.
|
||||
?assertMatch(
|
||||
#{
|
||||
<<"mismatches">> :=
|
||||
#{
|
||||
<<"listeners:ssl_not_required_bind">> :=
|
||||
#{
|
||||
<<"reason">> :=
|
||||
<<"Server certificate must be defined when using OCSP stapling">>
|
||||
}
|
||||
}
|
||||
},
|
||||
emqx_utils_json:decode(MsgRaw3, [return_maps])
|
||||
<<"{bad_ssl_config,#{file_read => enoent,pem_check => invalid_pem", _/binary>>,
|
||||
MsgRaw3
|
||||
),
|
||||
|
||||
ok.
|
||||
|
||||
t_unknown_error_fetching_ocsp_response(_Config) ->
|
||||
|
|
|
@ -47,7 +47,7 @@ all() ->
|
|||
emqx_common_test_helpers:all(?MODULE).
|
||||
|
||||
init_per_suite(Config) ->
|
||||
ok = emqx_common_test_helpers:load_config(emqx_limiter_schema, ?BASE_CONF),
|
||||
load_conf(),
|
||||
emqx_common_test_helpers:start_apps([?APP]),
|
||||
Config.
|
||||
|
||||
|
@ -55,13 +55,15 @@ end_per_suite(_Config) ->
|
|||
emqx_common_test_helpers:stop_apps([?APP]).
|
||||
|
||||
init_per_testcase(_TestCase, Config) ->
|
||||
emqx_config:erase(limiter),
|
||||
load_conf(),
|
||||
Config.
|
||||
|
||||
end_per_testcase(_TestCase, Config) ->
|
||||
Config.
|
||||
|
||||
load_conf() ->
|
||||
emqx_common_test_helpers:load_config(emqx_limiter_schema, ?BASE_CONF).
|
||||
ok = emqx_common_test_helpers:load_config(emqx_limiter_schema, ?BASE_CONF).
|
||||
|
||||
init_config() ->
|
||||
emqx_config:init_load(emqx_limiter_schema, ?BASE_CONF).
|
||||
|
@ -313,8 +315,8 @@ t_capacity(_) ->
|
|||
%% Test Cases Global Level
|
||||
%%--------------------------------------------------------------------
|
||||
t_collaborative_alloc(_) ->
|
||||
GlobalMod = fun(#{message_routing := MR} = Cfg) ->
|
||||
Cfg#{message_routing := MR#{rate := ?RATE("600/1s")}}
|
||||
GlobalMod = fun(Cfg) ->
|
||||
Cfg#{message_routing => #{rate => ?RATE("600/1s"), burst => 0}}
|
||||
end,
|
||||
|
||||
Bucket1 = fun(#{client := Cli} = Bucket) ->
|
||||
|
@ -353,11 +355,11 @@ t_collaborative_alloc(_) ->
|
|||
).
|
||||
|
||||
t_burst(_) ->
|
||||
GlobalMod = fun(#{message_routing := MR} = Cfg) ->
|
||||
GlobalMod = fun(Cfg) ->
|
||||
Cfg#{
|
||||
message_routing := MR#{
|
||||
rate := ?RATE("200/1s"),
|
||||
burst := ?RATE("400/1s")
|
||||
message_routing => #{
|
||||
rate => ?RATE("200/1s"),
|
||||
burst => ?RATE("400/1s")
|
||||
}
|
||||
}
|
||||
end,
|
||||
|
@ -653,16 +655,16 @@ t_not_exists_instance(_) ->
|
|||
),
|
||||
|
||||
?assertEqual(
|
||||
{error, invalid_bucket},
|
||||
{ok, infinity},
|
||||
emqx_limiter_server:connect(?FUNCTION_NAME, not_exists, Cfg)
|
||||
),
|
||||
ok.
|
||||
|
||||
t_create_instance_with_node(_) ->
|
||||
GlobalMod = fun(#{message_routing := MR} = Cfg) ->
|
||||
GlobalMod = fun(Cfg) ->
|
||||
Cfg#{
|
||||
message_routing := MR#{rate := ?RATE("200/1s")},
|
||||
messages := MR#{rate := ?RATE("200/1s")}
|
||||
message_routing => #{rate => ?RATE("200/1s"), burst => 0},
|
||||
messages => #{rate => ?RATE("200/1s"), burst => 0}
|
||||
}
|
||||
end,
|
||||
|
||||
|
@ -739,6 +741,68 @@ t_esockd_htb_consume(_) ->
|
|||
?assertMatch({ok, _}, C2R),
|
||||
ok.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Test Cases short paths
|
||||
%%--------------------------------------------------------------------
|
||||
t_node_short_paths(_) ->
|
||||
CfgStr = <<"limiter {max_conn_rate = \"1000\", messages_rate = \"100\", bytes_rate = \"10\"}">>,
|
||||
ok = emqx_common_test_helpers:load_config(emqx_limiter_schema, CfgStr),
|
||||
Accessor = fun emqx_limiter_schema:get_node_opts/1,
|
||||
?assertMatch(#{rate := 100.0}, Accessor(connection)),
|
||||
?assertMatch(#{rate := 10.0}, Accessor(messages)),
|
||||
?assertMatch(#{rate := 1.0}, Accessor(bytes)),
|
||||
?assertMatch(#{rate := infinity}, Accessor(message_routing)),
|
||||
?assertEqual(undefined, emqx:get_config([limiter, connection], undefined)).
|
||||
|
||||
t_compatibility_for_node_short_paths(_) ->
|
||||
CfgStr =
|
||||
<<"limiter {max_conn_rate = \"1000\", connection.rate = \"500\", bytes.rate = \"200\"}">>,
|
||||
ok = emqx_common_test_helpers:load_config(emqx_limiter_schema, CfgStr),
|
||||
Accessor = fun emqx_limiter_schema:get_node_opts/1,
|
||||
?assertMatch(#{rate := 100.0}, Accessor(connection)),
|
||||
?assertMatch(#{rate := 20.0}, Accessor(bytes)).
|
||||
|
||||
t_listener_short_paths(_) ->
|
||||
CfgStr = <<
|
||||
""
|
||||
"listeners.tcp.default {max_conn_rate = \"1000\", messages_rate = \"100\", bytes_rate = \"10\"}"
|
||||
""
|
||||
>>,
|
||||
ok = emqx_common_test_helpers:load_config(emqx_schema, CfgStr),
|
||||
ListenerOpt = emqx:get_config([listeners, tcp, default]),
|
||||
?assertMatch(
|
||||
#{
|
||||
client := #{
|
||||
messages := #{rate := 10.0},
|
||||
bytes := #{rate := 1.0}
|
||||
},
|
||||
connection := #{rate := 100.0}
|
||||
},
|
||||
emqx_limiter_schema:get_listener_opts(ListenerOpt)
|
||||
).
|
||||
|
||||
t_compatibility_for_listener_short_paths(_) ->
|
||||
CfgStr = <<
|
||||
"" "listeners.tcp.default {max_conn_rate = \"1000\", limiter.connection.rate = \"500\"}" ""
|
||||
>>,
|
||||
ok = emqx_common_test_helpers:load_config(emqx_schema, CfgStr),
|
||||
ListenerOpt = emqx:get_config([listeners, tcp, default]),
|
||||
?assertMatch(
|
||||
#{
|
||||
connection := #{rate := 100.0}
|
||||
},
|
||||
emqx_limiter_schema:get_listener_opts(ListenerOpt)
|
||||
).
|
||||
|
||||
t_no_limiter_for_listener(_) ->
|
||||
CfgStr = <<>>,
|
||||
ok = emqx_common_test_helpers:load_config(emqx_schema, CfgStr),
|
||||
ListenerOpt = emqx:get_config([listeners, tcp, default]),
|
||||
?assertEqual(
|
||||
undefined,
|
||||
emqx_limiter_schema:get_listener_opts(ListenerOpt)
|
||||
).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%%% Internal functions
|
||||
%%--------------------------------------------------------------------
|
||||
|
@ -1043,3 +1107,11 @@ make_create_test_data_with_infinity_node(FakeInstnace) ->
|
|||
%% client = C bucket = B C > B
|
||||
{MkA(1000, 100), IsRefLimiter(FakeInstnace)}
|
||||
].
|
||||
|
||||
parse_schema(ConfigString) ->
|
||||
{ok, RawConf} = hocon:binary(ConfigString, #{format => map}),
|
||||
hocon_tconf:check_plain(
|
||||
emqx_limiter_schema,
|
||||
RawConf,
|
||||
#{required => false, atom_key => false}
|
||||
).
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
%% -*- mode: erlang -*-
|
||||
{application, emqx_authn, [
|
||||
{description, "EMQX Authentication"},
|
||||
{vsn, "0.1.18"},
|
||||
{vsn, "0.1.19"},
|
||||
{modules, []},
|
||||
{registered, [emqx_authn_sup, emqx_authn_registry]},
|
||||
{applications, [kernel, stdlib, emqx_resource, emqx_connector, ehttpc, epgsql, mysql, jose]},
|
||||
|
|
|
@ -228,6 +228,7 @@ schema("/listeners/:listener_id/authentication") ->
|
|||
'operationId' => listener_authenticators,
|
||||
get => #{
|
||||
tags => ?API_TAGS_SINGLE,
|
||||
deprecated => true,
|
||||
description => ?DESC(listeners_listener_id_authentication_get),
|
||||
parameters => [param_listener_id()],
|
||||
responses => #{
|
||||
|
@ -239,6 +240,7 @@ schema("/listeners/:listener_id/authentication") ->
|
|||
},
|
||||
post => #{
|
||||
tags => ?API_TAGS_SINGLE,
|
||||
deprecated => true,
|
||||
description => ?DESC(listeners_listener_id_authentication_post),
|
||||
parameters => [param_listener_id()],
|
||||
'requestBody' => emqx_dashboard_swagger:schema_with_examples(
|
||||
|
@ -260,6 +262,7 @@ schema("/listeners/:listener_id/authentication/:id") ->
|
|||
'operationId' => listener_authenticator,
|
||||
get => #{
|
||||
tags => ?API_TAGS_SINGLE,
|
||||
deprecated => true,
|
||||
description => ?DESC(listeners_listener_id_authentication_id_get),
|
||||
parameters => [param_listener_id(), param_auth_id()],
|
||||
responses => #{
|
||||
|
@ -272,6 +275,7 @@ schema("/listeners/:listener_id/authentication/:id") ->
|
|||
},
|
||||
put => #{
|
||||
tags => ?API_TAGS_SINGLE,
|
||||
deprecated => true,
|
||||
description => ?DESC(listeners_listener_id_authentication_id_put),
|
||||
parameters => [param_listener_id(), param_auth_id()],
|
||||
'requestBody' => emqx_dashboard_swagger:schema_with_examples(
|
||||
|
@ -287,6 +291,7 @@ schema("/listeners/:listener_id/authentication/:id") ->
|
|||
},
|
||||
delete => #{
|
||||
tags => ?API_TAGS_SINGLE,
|
||||
deprecated => true,
|
||||
description => ?DESC(listeners_listener_id_authentication_id_delete),
|
||||
parameters => [param_listener_id(), param_auth_id()],
|
||||
responses => #{
|
||||
|
@ -300,6 +305,7 @@ schema("/listeners/:listener_id/authentication/:id/status") ->
|
|||
'operationId' => listener_authenticator_status,
|
||||
get => #{
|
||||
tags => ?API_TAGS_SINGLE,
|
||||
deprecated => true,
|
||||
description => ?DESC(listeners_listener_id_authentication_id_status_get),
|
||||
parameters => [param_listener_id(), param_auth_id()],
|
||||
responses => #{
|
||||
|
@ -330,6 +336,7 @@ schema("/listeners/:listener_id/authentication/:id/position/:position") ->
|
|||
'operationId' => listener_authenticator_position,
|
||||
put => #{
|
||||
tags => ?API_TAGS_SINGLE,
|
||||
deprecated => true,
|
||||
description => ?DESC(listeners_listener_id_authentication_id_position_put),
|
||||
parameters => [param_listener_id(), param_auth_id(), param_position()],
|
||||
responses => #{
|
||||
|
@ -393,6 +400,7 @@ schema("/listeners/:listener_id/authentication/:id/users") ->
|
|||
'operationId' => listener_authenticator_users,
|
||||
post => #{
|
||||
tags => ?API_TAGS_SINGLE,
|
||||
deprecated => true,
|
||||
description => ?DESC(listeners_listener_id_authentication_id_users_post),
|
||||
parameters => [param_auth_id(), param_listener_id()],
|
||||
'requestBody' => emqx_dashboard_swagger:schema_with_examples(
|
||||
|
@ -410,6 +418,7 @@ schema("/listeners/:listener_id/authentication/:id/users") ->
|
|||
},
|
||||
get => #{
|
||||
tags => ?API_TAGS_SINGLE,
|
||||
deprecated => true,
|
||||
description => ?DESC(listeners_listener_id_authentication_id_users_get),
|
||||
parameters => [
|
||||
param_listener_id(),
|
||||
|
@ -479,6 +488,7 @@ schema("/listeners/:listener_id/authentication/:id/users/:user_id") ->
|
|||
'operationId' => listener_authenticator_user,
|
||||
get => #{
|
||||
tags => ?API_TAGS_SINGLE,
|
||||
deprecated => true,
|
||||
description => ?DESC(listeners_listener_id_authentication_id_users_user_id_get),
|
||||
parameters => [param_listener_id(), param_auth_id(), param_user_id()],
|
||||
responses => #{
|
||||
|
@ -491,6 +501,7 @@ schema("/listeners/:listener_id/authentication/:id/users/:user_id") ->
|
|||
},
|
||||
put => #{
|
||||
tags => ?API_TAGS_SINGLE,
|
||||
deprecated => true,
|
||||
description => ?DESC(listeners_listener_id_authentication_id_users_user_id_put),
|
||||
parameters => [param_listener_id(), param_auth_id(), param_user_id()],
|
||||
'requestBody' => emqx_dashboard_swagger:schema_with_example(
|
||||
|
@ -508,6 +519,7 @@ schema("/listeners/:listener_id/authentication/:id/users/:user_id") ->
|
|||
},
|
||||
delete => #{
|
||||
tags => ?API_TAGS_SINGLE,
|
||||
deprecated => true,
|
||||
description => ?DESC(listeners_listener_id_authentication_id_users_user_id_delete),
|
||||
parameters => [param_listener_id(), param_auth_id(), param_user_id()],
|
||||
responses => #{
|
||||
|
|
|
@ -72,7 +72,7 @@ chain_configs() ->
|
|||
[global_chain_config() | listener_chain_configs()].
|
||||
|
||||
global_chain_config() ->
|
||||
{?GLOBAL, emqx:get_config([?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_BINARY], [])}.
|
||||
{?GLOBAL, emqx:get_config([?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_ATOM], [])}.
|
||||
|
||||
listener_chain_configs() ->
|
||||
lists:map(
|
||||
|
@ -83,9 +83,11 @@ listener_chain_configs() ->
|
|||
).
|
||||
|
||||
auth_config_path(ListenerID) ->
|
||||
[<<"listeners">>] ++
|
||||
binary:split(atom_to_binary(ListenerID), <<":">>) ++
|
||||
[?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_BINARY].
|
||||
Names = [
|
||||
binary_to_existing_atom(N, utf8)
|
||||
|| N <- binary:split(atom_to_binary(ListenerID), <<":">>)
|
||||
],
|
||||
[listeners] ++ Names ++ [?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_ATOM].
|
||||
|
||||
provider_types() ->
|
||||
lists:map(fun({Type, _Module}) -> Type end, emqx_authn:providers()).
|
||||
|
|
|
@ -72,6 +72,7 @@ schema("/listeners/:listener_id/authentication/:id/import_users") ->
|
|||
'operationId' => listener_authenticator_import_users,
|
||||
post => #{
|
||||
tags => ?API_TAGS_SINGLE,
|
||||
deprecated => true,
|
||||
description => ?DESC(listeners_listener_id_authentication_id_import_users_post),
|
||||
parameters => [emqx_authn_api:param_listener_id(), emqx_authn_api:param_auth_id()],
|
||||
'requestBody' => emqx_dashboard_swagger:file_schema(filename),
|
||||
|
|
|
@ -54,13 +54,14 @@
|
|||
|
||||
-define(BRIDGE_NOT_FOUND(BRIDGE_TYPE, BRIDGE_NAME),
|
||||
?NOT_FOUND(
|
||||
<<"Bridge lookup failed: bridge named '", (BRIDGE_NAME)/binary, "' of type ",
|
||||
<<"Bridge lookup failed: bridge named '", (bin(BRIDGE_NAME))/binary, "' of type ",
|
||||
(bin(BRIDGE_TYPE))/binary, " does not exist.">>
|
||||
)
|
||||
).
|
||||
|
||||
%% Don't turn bridge_name to atom, it's maybe not a existing atom.
|
||||
-define(TRY_PARSE_ID(ID, EXPR),
|
||||
try emqx_bridge_resource:parse_bridge_id(Id) of
|
||||
try emqx_bridge_resource:parse_bridge_id(Id, #{atom_name => false}) of
|
||||
{BridgeType, BridgeName} ->
|
||||
EXPR
|
||||
catch
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
resource_id/2,
|
||||
bridge_id/2,
|
||||
parse_bridge_id/1,
|
||||
parse_bridge_id/2,
|
||||
bridge_hookpoint/1,
|
||||
bridge_hookpoint_to_bridge_id/1
|
||||
]).
|
||||
|
@ -86,11 +87,15 @@ bridge_id(BridgeType, BridgeName) ->
|
|||
Type = bin(BridgeType),
|
||||
<<Type/binary, ":", Name/binary>>.
|
||||
|
||||
-spec parse_bridge_id(list() | binary() | atom()) -> {atom(), binary()}.
|
||||
parse_bridge_id(BridgeId) ->
|
||||
parse_bridge_id(BridgeId, #{atom_name => true}).
|
||||
|
||||
-spec parse_bridge_id(list() | binary() | atom(), #{atom_name => boolean()}) ->
|
||||
{atom(), atom() | binary()}.
|
||||
parse_bridge_id(BridgeId, Opts) ->
|
||||
case string:split(bin(BridgeId), ":", all) of
|
||||
[Type, Name] ->
|
||||
{to_type_atom(Type), validate_name(Name)};
|
||||
{to_type_atom(Type), validate_name(Name, Opts)};
|
||||
_ ->
|
||||
invalid_data(
|
||||
<<"should be of pattern {type}:{name}, but got ", BridgeId/binary>>
|
||||
|
@ -105,13 +110,16 @@ bridge_hookpoint_to_bridge_id(?BRIDGE_HOOKPOINT(BridgeId)) ->
|
|||
bridge_hookpoint_to_bridge_id(_) ->
|
||||
{error, bad_bridge_hookpoint}.
|
||||
|
||||
validate_name(Name0) ->
|
||||
validate_name(Name0, Opts) ->
|
||||
Name = unicode:characters_to_list(Name0, utf8),
|
||||
case is_list(Name) andalso Name =/= [] of
|
||||
true ->
|
||||
case lists:all(fun is_id_char/1, Name) of
|
||||
true ->
|
||||
Name0;
|
||||
case maps:get(atom_name, Opts, true) of
|
||||
true -> list_to_existing_atom(Name);
|
||||
false -> Name0
|
||||
end;
|
||||
false ->
|
||||
invalid_data(<<"bad name: ", Name0/binary>>)
|
||||
end;
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
toxiproxy
|
||||
dynamo
|
|
@ -0,0 +1,11 @@
|
|||
%% -*- mode: erlang; -*-
|
||||
{erl_opts, [debug_info]}.
|
||||
{deps, [ {erlcloud, {git, "https://github.com/emqx/erlcloud.git", {tag, "3.5.16-emqx-1"}}}
|
||||
, {emqx_connector, {path, "../../apps/emqx_connector"}}
|
||||
, {emqx_resource, {path, "../../apps/emqx_resource"}}
|
||||
, {emqx_bridge, {path, "../../apps/emqx_bridge"}}
|
||||
]}.
|
||||
|
||||
{shell, [
|
||||
{apps, [emqx_bridge_dynamo]}
|
||||
]}.
|
|
@ -1,8 +1,8 @@
|
|||
{application, emqx_bridge_dynamo, [
|
||||
{description, "EMQX Enterprise Dynamo Bridge"},
|
||||
{vsn, "0.1.0"},
|
||||
{vsn, "0.1.1"},
|
||||
{registered, []},
|
||||
{applications, [kernel, stdlib]},
|
||||
{applications, [kernel, stdlib, erlcloud]},
|
||||
{env, []},
|
||||
{modules, []},
|
||||
{links, []}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%--------------------------------------------------------------------
|
||||
-module(emqx_ee_bridge_dynamo).
|
||||
-module(emqx_bridge_dynamo).
|
||||
|
||||
-include_lib("typerefl/include/types.hrl").
|
||||
-include_lib("hocon/include/hoconsc.hrl").
|
||||
|
@ -89,7 +89,7 @@ fields("config") ->
|
|||
}
|
||||
)}
|
||||
] ++
|
||||
(emqx_ee_connector_dynamo:fields(config) --
|
||||
(emqx_bridge_dynamo_connector:fields(config) --
|
||||
emqx_connector_schema_lib:prepare_statement_fields());
|
||||
fields("creation_opts") ->
|
||||
emqx_resource_schema:fields("creation_opts");
|
|
@ -2,7 +2,7 @@
|
|||
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_ee_connector_dynamo).
|
||||
-module(emqx_bridge_dynamo_connector).
|
||||
|
||||
-behaviour(emqx_resource).
|
||||
|
||||
|
@ -131,7 +131,7 @@ on_batch_query(_InstanceId, Query, _State) ->
|
|||
|
||||
on_get_status(_InstanceId, #{pool_name := Pool}) ->
|
||||
Health = emqx_resource_pool:health_check_workers(
|
||||
Pool, {emqx_ee_connector_dynamo_client, is_connected, []}
|
||||
Pool, {emqx_bridge_dynamo_connector_client, is_connected, []}
|
||||
),
|
||||
status_result(Health).
|
||||
|
||||
|
@ -154,7 +154,7 @@ do_query(
|
|||
),
|
||||
Result = ecpool:pick_and_do(
|
||||
PoolName,
|
||||
{emqx_ee_connector_dynamo_client, query, [Table, Query, Templates]},
|
||||
{emqx_bridge_dynamo_connector_client, query, [Table, Query, Templates]},
|
||||
no_handover
|
||||
),
|
||||
|
||||
|
@ -181,7 +181,7 @@ do_query(
|
|||
|
||||
connect(Opts) ->
|
||||
Options = proplists:get_value(config, Opts),
|
||||
{ok, _Pid} = Result = emqx_ee_connector_dynamo_client:start_link(Options),
|
||||
{ok, _Pid} = Result = emqx_bridge_dynamo_connector_client:start_link(Options),
|
||||
Result.
|
||||
|
||||
parse_template(Config) ->
|
|
@ -1,7 +1,8 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%--------------------------------------------------------------------
|
||||
-module(emqx_ee_connector_dynamo_client).
|
||||
|
||||
-module(emqx_bridge_dynamo_connector_client).
|
||||
|
||||
-behaviour(gen_server).
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
%% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_ee_bridge_dynamo_SUITE).
|
||||
-module(emqx_bridge_dynamo_SUITE).
|
||||
|
||||
-compile(nowarn_export_all).
|
||||
-compile(export_all).
|
||||
|
@ -24,6 +24,14 @@
|
|||
|
||||
-define(GET_CONFIG(KEY__, CFG__), proplists:get_value(KEY__, CFG__)).
|
||||
|
||||
%% How to run it locally (all commands are run in $PROJ_ROOT dir):
|
||||
%% run ct in docker container
|
||||
%% run script:
|
||||
%% ```bash
|
||||
%% ./scripts/ct/run.sh --ci --app apps/emqx_bridge_dynamo -- \
|
||||
%% --name 'test@127.0.0.1' -c -v --readable true \
|
||||
%% --suite apps/emqx_bridge_dynamo/test/emqx_bridge_dynamo_SUITE.erl
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% CT boilerplate
|
||||
%%------------------------------------------------------------------------------
|
||||
|
@ -224,7 +232,7 @@ query_resource(Config, Request) ->
|
|||
ResourceID = emqx_bridge_resource:resource_id(BridgeType, Name),
|
||||
emqx_resource:query(ResourceID, Request, #{timeout => 1_000}).
|
||||
|
||||
%% create a table, use the lib-ee/emqx_ee_bridge/priv/dynamo/mqtt_msg.json as template
|
||||
%% create a table, use the apps/emqx_bridge_dynamo/priv/dynamo/mqtt_msg.json as template
|
||||
create_table(Config) ->
|
||||
directly_setup_dynamo(),
|
||||
delete_table(Config),
|
||||
|
@ -251,7 +259,7 @@ directly_setup_dynamo() ->
|
|||
|
||||
directly_query(Query) ->
|
||||
directly_setup_dynamo(),
|
||||
emqx_ee_connector_dynamo_client:execute(Query, ?TABLE_BIN).
|
||||
emqx_bridge_dynamo_connector_client:execute(Query, ?TABLE_BIN).
|
||||
|
||||
directly_get_payload(Key) ->
|
||||
case directly_query({get_item, {<<"id">>, Key}}) of
|
|
@ -0,0 +1,2 @@
|
|||
toxiproxy
|
||||
influxdb
|
|
@ -0,0 +1,8 @@
|
|||
{erl_opts, [debug_info]}.
|
||||
|
||||
{deps, [
|
||||
{influxdb, {git, "https://github.com/emqx/influxdb-client-erl", {tag, "1.1.9"}}},
|
||||
{emqx_connector, {path, "../../apps/emqx_connector"}},
|
||||
{emqx_resource, {path, "../../apps/emqx_resource"}},
|
||||
{emqx_bridge, {path, "../../apps/emqx_bridge"}}
|
||||
]}.
|
|
@ -1,8 +1,8 @@
|
|||
{application, emqx_bridge_influxdb, [
|
||||
{description, "EMQX Enterprise InfluxDB Bridge"},
|
||||
{vsn, "0.1.0"},
|
||||
{vsn, "0.1.1"},
|
||||
{registered, []},
|
||||
{applications, [kernel, stdlib]},
|
||||
{applications, [kernel, stdlib, influxdb]},
|
||||
{env, []},
|
||||
{modules, []},
|
||||
{links, []}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%--------------------------------------------------------------------
|
||||
-module(emqx_ee_bridge_influxdb).
|
||||
-module(emqx_bridge_influxdb).
|
||||
|
||||
-include_lib("emqx/include/logger.hrl").
|
||||
-include_lib("emqx_connector/include/emqx_connector.hrl").
|
||||
|
@ -134,7 +134,7 @@ influxdb_bridge_common_fields() ->
|
|||
emqx_resource_schema:fields("resource_opts").
|
||||
|
||||
connector_fields(Type) ->
|
||||
emqx_ee_connector_influxdb:fields(Type).
|
||||
emqx_bridge_influxdb_connector:fields(Type).
|
||||
|
||||
type_name_fields(Type) ->
|
||||
[
|
||||
|
@ -147,9 +147,9 @@ desc("config") ->
|
|||
desc(Method) when Method =:= "get"; Method =:= "put"; Method =:= "post" ->
|
||||
["Configuration for InfluxDB using `", string:to_upper(Method), "` method."];
|
||||
desc(influxdb_api_v1) ->
|
||||
?DESC(emqx_ee_connector_influxdb, "influxdb_api_v1");
|
||||
?DESC(emqx_bridge_influxdb_connector, "influxdb_api_v1");
|
||||
desc(influxdb_api_v2) ->
|
||||
?DESC(emqx_ee_connector_influxdb, "influxdb_api_v2");
|
||||
?DESC(emqx_bridge_influxdb_connector, "influxdb_api_v2");
|
||||
desc(_) ->
|
||||
undefined.
|
||||
|
|
@ -1,9 +1,8 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%--------------------------------------------------------------------
|
||||
-module(emqx_ee_connector_influxdb).
|
||||
-module(emqx_bridge_influxdb_connector).
|
||||
|
||||
-include("emqx_ee_connector.hrl").
|
||||
-include_lib("emqx_connector/include/emqx_connector.hrl").
|
||||
|
||||
-include_lib("hocon/include/hoconsc.hrl").
|
||||
|
@ -40,6 +39,8 @@
|
|||
|
||||
-type ts_precision() :: ns | us | ms | s.
|
||||
|
||||
-define(INFLUXDB_DEFAULT_PORT, 8086).
|
||||
|
||||
%% influxdb servers don't need parse
|
||||
-define(INFLUXDB_HOST_OPTIONS, #{
|
||||
default_port => ?INFLUXDB_DEFAULT_PORT
|
|
@ -1,7 +1,7 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%--------------------------------------------------------------------
|
||||
-module(emqx_ee_bridge_influxdb_SUITE).
|
||||
-module(emqx_bridge_influxdb_SUITE).
|
||||
|
||||
-compile(nowarn_export_all).
|
||||
-compile(export_all).
|
||||
|
@ -583,7 +583,7 @@ t_start_already_started(Config) ->
|
|||
emqx_bridge_schema, InfluxDBConfigString
|
||||
),
|
||||
?check_trace(
|
||||
emqx_ee_connector_influxdb:on_start(ResourceId, InfluxDBConfigMap),
|
||||
emqx_bridge_influxdb_connector:on_start(ResourceId, InfluxDBConfigMap),
|
||||
fun(Result, Trace) ->
|
||||
?assertMatch({ok, _}, Result),
|
||||
?assertMatch([_], ?of_kind(influxdb_connector_start_already_started, Trace)),
|
||||
|
@ -985,7 +985,7 @@ t_write_failure(Config) ->
|
|||
?assertMatch([_ | _], Trace),
|
||||
[#{result := Result} | _] = Trace,
|
||||
?assert(
|
||||
not emqx_ee_connector_influxdb:is_unrecoverable_error(Result),
|
||||
not emqx_bridge_influxdb_connector:is_unrecoverable_error(Result),
|
||||
#{got => Result}
|
||||
);
|
||||
async ->
|
||||
|
@ -993,7 +993,7 @@ t_write_failure(Config) ->
|
|||
?assertMatch([#{action := nack} | _], Trace),
|
||||
[#{result := Result} | _] = Trace,
|
||||
?assert(
|
||||
not emqx_ee_connector_influxdb:is_unrecoverable_error(Result),
|
||||
not emqx_bridge_influxdb_connector:is_unrecoverable_error(Result),
|
||||
#{got => Result}
|
||||
)
|
||||
end,
|
|
@ -2,16 +2,16 @@
|
|||
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_ee_connector_influxdb_SUITE).
|
||||
-module(emqx_bridge_influxdb_connector_SUITE).
|
||||
|
||||
-compile(nowarn_export_all).
|
||||
-compile(export_all).
|
||||
|
||||
-include("emqx_connector.hrl").
|
||||
-include_lib("emqx_connector/include/emqx_connector.hrl").
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
-include_lib("common_test/include/ct.hrl").
|
||||
|
||||
-define(INFLUXDB_RESOURCE_MOD, emqx_ee_connector_influxdb).
|
||||
-define(INFLUXDB_RESOURCE_MOD, emqx_bridge_influxdb_connector).
|
||||
|
||||
all() ->
|
||||
emqx_common_test_helpers:all(?MODULE).
|
||||
|
@ -65,7 +65,7 @@ t_lifecycle(Config) ->
|
|||
Host = ?config(influxdb_tcp_host, Config),
|
||||
Port = ?config(influxdb_tcp_port, Config),
|
||||
perform_lifecycle_check(
|
||||
<<"emqx_ee_connector_influxdb_SUITE">>,
|
||||
<<"emqx_bridge_influxdb_connector_SUITE">>,
|
||||
influxdb_config(Host, Port, false, <<"verify_none">>)
|
||||
).
|
||||
|
||||
|
@ -124,7 +124,7 @@ perform_lifecycle_check(PoolName, InitialConfig) ->
|
|||
?assertEqual({error, not_found}, emqx_resource:get_instance(PoolName)).
|
||||
|
||||
t_tls_verify_none(Config) ->
|
||||
PoolName = <<"emqx_ee_connector_influxdb_SUITE">>,
|
||||
PoolName = <<"emqx_bridge_influxdb_connector_SUITE">>,
|
||||
Host = ?config(influxdb_tls_host, Config),
|
||||
Port = ?config(influxdb_tls_port, Config),
|
||||
InitialConfig = influxdb_config(Host, Port, true, <<"verify_none">>),
|
||||
|
@ -135,7 +135,7 @@ t_tls_verify_none(Config) ->
|
|||
ok.
|
||||
|
||||
t_tls_verify_peer(Config) ->
|
||||
PoolName = <<"emqx_ee_connector_influxdb_SUITE">>,
|
||||
PoolName = <<"emqx_bridge_influxdb_connector_SUITE">>,
|
||||
Host = ?config(influxdb_tls_host, Config),
|
||||
Port = ?config(influxdb_tls_port, Config),
|
||||
InitialConfig = influxdb_config(Host, Port, true, <<"verify_peer">>),
|
|
@ -1,7 +1,7 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%--------------------------------------------------------------------
|
||||
-module(emqx_ee_bridge_influxdb_tests).
|
||||
-module(emqx_bridge_influxdb_tests).
|
||||
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
|
@ -192,7 +192,9 @@
|
|||
fields => [{"field", "\"field\\4\""}],
|
||||
timestamp => undefined
|
||||
}},
|
||||
{"m5\\,mA,tag=\\=tag5\\=,\\,tag_a\\,=tag\\ 5a,tag_b=tag5b \\ field\\ =field5,field\\ _\\ a=field5a,\\,field_b\\ =\\=\\,\\ field5b ${timestamp5}",
|
||||
{
|
||||
"m5\\,mA,tag=\\=tag5\\=,\\,tag_a\\,=tag\\ 5a,tag_b=tag5b \\ field\\ =field5,"
|
||||
"field\\ _\\ a=field5a,\\,field_b\\ =\\=\\,\\ field5b ${timestamp5}",
|
||||
#{
|
||||
measurement => "m5,mA",
|
||||
tags => [{"tag", "=tag5="}, {",tag_a,", "tag 5a"}, {"tag_b", "tag5b"}],
|
||||
|
@ -200,7 +202,8 @@
|
|||
{" field ", "field5"}, {"field _ a", "field5a"}, {",field_b ", "=, field5b"}
|
||||
],
|
||||
timestamp => "${timestamp5}"
|
||||
}},
|
||||
}
|
||||
},
|
||||
{"m6,tag=tag6,tag_a=tag6a,tag_b=tag6b field=\"field6\",field_a=\"field6a\",field_b=\"field6b\"",
|
||||
#{
|
||||
measurement => "m6",
|
||||
|
@ -208,20 +211,26 @@
|
|||
fields => [{"field", "field6"}, {"field_a", "field6a"}, {"field_b", "field6b"}],
|
||||
timestamp => undefined
|
||||
}},
|
||||
{"\\ \\ m7\\ \\ ,tag=\\ tag\\,7\\ ,tag_a=\"tag7a\",tag_b\\,tag1=tag7b field=\"field7\",field_a=field7a,field_b=\"field7b\\\\\n\"",
|
||||
{
|
||||
"\\ \\ m7\\ \\ ,tag=\\ tag\\,7\\ ,tag_a=\"tag7a\",tag_b\\,tag1=tag7b field=\"field7\","
|
||||
"field_a=field7a,field_b=\"field7b\\\\\n\"",
|
||||
#{
|
||||
measurement => " m7 ",
|
||||
tags => [{"tag", " tag,7 "}, {"tag_a", "\"tag7a\""}, {"tag_b,tag1", "tag7b"}],
|
||||
fields => [{"field", "field7"}, {"field_a", "field7a"}, {"field_b", "field7b\\\n"}],
|
||||
timestamp => undefined
|
||||
}},
|
||||
{"m8,tag=tag8,tag_a=\"tag8a\",tag_b=tag8b field=\"field8\",field_a=field8a,field_b=\"\\\"field\\\" = 8b\" ${timestamp8}",
|
||||
}
|
||||
},
|
||||
{
|
||||
"m8,tag=tag8,tag_a=\"tag8a\",tag_b=tag8b field=\"field8\",field_a=field8a,"
|
||||
"field_b=\"\\\"field\\\" = 8b\" ${timestamp8}",
|
||||
#{
|
||||
measurement => "m8",
|
||||
tags => [{"tag", "tag8"}, {"tag_a", "\"tag8a\""}, {"tag_b", "tag8b"}],
|
||||
fields => [{"field", "field8"}, {"field_a", "field8a"}, {"field_b", "\"field\" = 8b"}],
|
||||
timestamp => "${timestamp8}"
|
||||
}},
|
||||
}
|
||||
},
|
||||
{"m\\9,tag=tag9,tag_a=\"tag9a\",tag_b=tag9b field\\=field=\"field9\",field_a=field9a,field_b=\"\" ${timestamp9}",
|
||||
#{
|
||||
measurement => "m\\9",
|
||||
|
@ -263,7 +272,9 @@
|
|||
fields => [{"field", "\"field\\4\""}],
|
||||
timestamp => undefined
|
||||
}},
|
||||
{" m5\\,mA,tag=\\=tag5\\=,\\,tag_a\\,=tag\\ 5a,tag_b=tag5b \\ field\\ =field5,field\\ _\\ a=field5a,\\,field_b\\ =\\=\\,\\ field5b ${timestamp5} ",
|
||||
{
|
||||
" m5\\,mA,tag=\\=tag5\\=,\\,tag_a\\,=tag\\ 5a,tag_b=tag5b \\ field\\ =field5,"
|
||||
"field\\ _\\ a=field5a,\\,field_b\\ =\\=\\,\\ field5b ${timestamp5} ",
|
||||
#{
|
||||
measurement => "m5,mA",
|
||||
tags => [{"tag", "=tag5="}, {",tag_a,", "tag 5a"}, {"tag_b", "tag5b"}],
|
||||
|
@ -271,7 +282,8 @@
|
|||
{" field ", "field5"}, {"field _ a", "field5a"}, {",field_b ", "=, field5b"}
|
||||
],
|
||||
timestamp => "${timestamp5}"
|
||||
}},
|
||||
}
|
||||
},
|
||||
{" m6,tag=tag6,tag_a=tag6a,tag_b=tag6b field=\"field6\",field_a=\"field6a\",field_b=\"field6b\" ",
|
||||
#{
|
||||
measurement => "m6",
|
||||
|
@ -330,7 +342,7 @@ to_influx_lines(RawLines) ->
|
|||
try
|
||||
%% mute error logs from this call
|
||||
emqx_logger:set_primary_log_level(none),
|
||||
emqx_ee_bridge_influxdb:to_influx_lines(RawLines)
|
||||
emqx_bridge_influxdb:to_influx_lines(RawLines)
|
||||
after
|
||||
emqx_logger:set_primary_log_level(OldLevel)
|
||||
end.
|
|
@ -54,6 +54,7 @@ fields(auth_basic) ->
|
|||
mk(binary(), #{
|
||||
required => true,
|
||||
desc => ?DESC("config_auth_basic_password"),
|
||||
format => <<"password">>,
|
||||
sensitive => true,
|
||||
converter => fun emqx_schema:password_converter/2
|
||||
})}
|
||||
|
|
|
@ -583,7 +583,7 @@ config(Args0, More) ->
|
|||
ct:pal("Running tests with conf:\n~p", [Conf]),
|
||||
InstId = maps:get("instance_id", Args),
|
||||
<<"bridge:", BridgeId/binary>> = InstId,
|
||||
{Type, Name} = emqx_bridge_resource:parse_bridge_id(BridgeId),
|
||||
{Type, Name} = emqx_bridge_resource:parse_bridge_id(BridgeId, #{atom_name => false}),
|
||||
TypeBin = atom_to_binary(Type),
|
||||
hocon_tconf:check_plain(
|
||||
emqx_bridge_schema,
|
||||
|
@ -596,7 +596,7 @@ config(Args0, More) ->
|
|||
hocon_config(Args) ->
|
||||
InstId = maps:get("instance_id", Args),
|
||||
<<"bridge:", BridgeId/binary>> = InstId,
|
||||
{_Type, Name} = emqx_bridge_resource:parse_bridge_id(BridgeId),
|
||||
{_Type, Name} = emqx_bridge_resource:parse_bridge_id(BridgeId, #{atom_name => false}),
|
||||
AuthConf = maps:get("authentication", Args),
|
||||
AuthTemplate = iolist_to_binary(hocon_config_template_authentication(AuthConf)),
|
||||
AuthConfRendered = bbmustache:render(AuthTemplate, AuthConf),
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{erl_opts, [debug_info]}.
|
||||
|
||||
{deps, [
|
||||
{emqx_connector, {path, "../../apps/emqx_connector"}},
|
||||
{emqx_resource, {path, "../../apps/emqx_resource"}},
|
||||
{emqx_bridge, {path, "../../apps/emqx_bridge"}}
|
||||
]}.
|
|
@ -1,6 +1,6 @@
|
|||
{application, emqx_bridge_matrix, [
|
||||
{description, "EMQX Enterprise MatrixDB Bridge"},
|
||||
{vsn, "0.1.0"},
|
||||
{vsn, "0.1.1"},
|
||||
{registered, []},
|
||||
{applications, [kernel, stdlib]},
|
||||
{env, []},
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%--------------------------------------------------------------------
|
||||
-module(emqx_ee_bridge_matrix).
|
||||
-module(emqx_bridge_matrix).
|
||||
|
||||
-export([
|
||||
conn_bridge_examples/1
|
||||
|
@ -22,7 +22,7 @@ conn_bridge_examples(Method) ->
|
|||
#{
|
||||
<<"matrix">> => #{
|
||||
summary => <<"Matrix Bridge">>,
|
||||
value => emqx_ee_bridge_pgsql:values(Method, matrix)
|
||||
value => emqx_bridge_pgsql:values(Method, matrix)
|
||||
}
|
||||
}
|
||||
].
|
||||
|
@ -34,9 +34,9 @@ namespace() -> "bridge_matrix".
|
|||
roots() -> [].
|
||||
|
||||
fields("post") ->
|
||||
emqx_ee_bridge_pgsql:fields("post", matrix);
|
||||
emqx_bridge_pgsql:fields("post", matrix);
|
||||
fields(Method) ->
|
||||
emqx_ee_bridge_pgsql:fields(Method).
|
||||
emqx_bridge_pgsql:fields(Method).
|
||||
|
||||
desc(_) ->
|
||||
undefined.
|
|
@ -0,0 +1,2 @@
|
|||
toxiproxy
|
||||
pgsql
|
|
@ -0,0 +1,7 @@
|
|||
{erl_opts, [debug_info]}.
|
||||
|
||||
{deps, [
|
||||
{emqx_connector, {path, "../../apps/emqx_connector"}},
|
||||
{emqx_resource, {path, "../../apps/emqx_resource"}},
|
||||
{emqx_bridge, {path, "../../apps/emqx_bridge"}}
|
||||
]}.
|
|
@ -1,6 +1,6 @@
|
|||
{application, emqx_bridge_pgsql, [
|
||||
{description, "EMQX Enterprise PostgreSQL Bridge"},
|
||||
{vsn, "0.1.0"},
|
||||
{vsn, "0.1.1"},
|
||||
{registered, []},
|
||||
{applications, [kernel, stdlib]},
|
||||
{env, []},
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%--------------------------------------------------------------------
|
||||
-module(emqx_ee_bridge_pgsql).
|
||||
-module(emqx_bridge_pgsql).
|
||||
|
||||
-include_lib("typerefl/include/types.hrl").
|
||||
-include_lib("hocon/include/hoconsc.hrl").
|
|
@ -2,7 +2,7 @@
|
|||
%% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_ee_bridge_pgsql_SUITE).
|
||||
-module(emqx_bridge_pgsql_SUITE).
|
||||
|
||||
-compile(nowarn_export_all).
|
||||
-compile(export_all).
|
|
@ -0,0 +1,94 @@
|
|||
Business Source License 1.1
|
||||
|
||||
Licensor: Hangzhou EMQ Technologies Co., Ltd.
|
||||
Licensed Work: EMQX Enterprise Edition
|
||||
The Licensed Work is (c) 2023
|
||||
Hangzhou EMQ Technologies Co., Ltd.
|
||||
Additional Use Grant: Students and educators are granted right to copy,
|
||||
modify, and create derivative work for research
|
||||
or education.
|
||||
Change Date: 2027-02-01
|
||||
Change License: Apache License, Version 2.0
|
||||
|
||||
For information about alternative licensing arrangements for the Software,
|
||||
please contact Licensor: https://www.emqx.com/en/contact
|
||||
|
||||
Notice
|
||||
|
||||
The Business Source License (this document, or the “License”) is not an Open
|
||||
Source license. However, the Licensed Work will eventually be made available
|
||||
under an Open Source License, as stated in this License.
|
||||
|
||||
License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved.
|
||||
“Business Source License” is a trademark of MariaDB Corporation Ab.
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
|
||||
Business Source License 1.1
|
||||
|
||||
Terms
|
||||
|
||||
The Licensor hereby grants you the right to copy, modify, create derivative
|
||||
works, redistribute, and make non-production use of the Licensed Work. The
|
||||
Licensor may make an Additional Use Grant, above, permitting limited
|
||||
production use.
|
||||
|
||||
Effective on the Change Date, or the fourth anniversary of the first publicly
|
||||
available distribution of a specific version of the Licensed Work under this
|
||||
License, whichever comes first, the Licensor hereby grants you rights under
|
||||
the terms of the Change License, and the rights granted in the paragraph
|
||||
above terminate.
|
||||
|
||||
If your use of the Licensed Work does not comply with the requirements
|
||||
currently in effect as described in this License, you must purchase a
|
||||
commercial license from the Licensor, its affiliated entities, or authorized
|
||||
resellers, or you must refrain from using the Licensed Work.
|
||||
|
||||
All copies of the original and modified Licensed Work, and derivative works
|
||||
of the Licensed Work, are subject to this License. This License applies
|
||||
separately for each version of the Licensed Work and the Change Date may vary
|
||||
for each version of the Licensed Work released by Licensor.
|
||||
|
||||
You must conspicuously display this License on each original or modified copy
|
||||
of the Licensed Work. If you receive the Licensed Work in original or
|
||||
modified form from a third party, the terms and conditions set forth in this
|
||||
License apply to your use of that work.
|
||||
|
||||
Any use of the Licensed Work in violation of this License will automatically
|
||||
terminate your rights under this License for the current and all other
|
||||
versions of the Licensed Work.
|
||||
|
||||
This License does not grant you any right in any trademark or logo of
|
||||
Licensor or its affiliates (provided that you may use a trademark or logo of
|
||||
Licensor as expressly required by this License).
|
||||
|
||||
TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
|
||||
AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
|
||||
EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
|
||||
TITLE.
|
||||
|
||||
MariaDB hereby grants you permission to use this License’s text to license
|
||||
your works, and to refer to it using the trademark “Business Source License”,
|
||||
as long as you comply with the Covenants of Licensor below.
|
||||
|
||||
Covenants of Licensor
|
||||
|
||||
In consideration of the right to use this License’s text and the “Business
|
||||
Source License” name and trademark, Licensor covenants to MariaDB, and to all
|
||||
other recipients of the licensed work to be provided by Licensor:
|
||||
|
||||
1. To specify as the Change License the GPL Version 2.0 or any later version,
|
||||
or a license that is compatible with GPL Version 2.0 or a later version,
|
||||
where “compatible” means that software provided under the Change License can
|
||||
be included in a program with software provided under GPL Version 2.0 or a
|
||||
later version. Licensor may specify additional Change Licenses without
|
||||
limitation.
|
||||
|
||||
2. To either: (a) specify an additional grant of rights to use that does not
|
||||
impose any additional restriction on the right granted in this License, as
|
||||
the Additional Use Grant; or (b) insert the text “None”.
|
||||
|
||||
3. To specify a Change Date.
|
||||
|
||||
4. Not to modify this License in any other way.
|
|
@ -0,0 +1,46 @@
|
|||
# EMQX RabbitMQ Bridge
|
||||
|
||||
[RabbitMQ](https://www.rabbitmq.com/) is a powerful, open-source message broker
|
||||
that facilitates asynchronous communication between different components of an
|
||||
application. Built on the Advanced Message Queuing Protocol (AMQP), RabbitMQ
|
||||
enables the reliable transmission of messages by decoupling the sender and
|
||||
receiver components. This separation allows for increased scalability,
|
||||
robustness, and flexibility in application architecture.
|
||||
|
||||
RabbitMQ is commonly used for a wide range of purposes, such as distributing
|
||||
tasks among multiple workers, enabling event-driven architectures, and
|
||||
implementing publish-subscribe patterns. It is a popular choice for
|
||||
microservices, distributed systems, and real-time applications, providing an
|
||||
efficient way to handle varying workloads and ensuring message delivery in
|
||||
complex environments.
|
||||
|
||||
This application is used to connect EMQX and RabbitMQ. User can create a rule
|
||||
and easily ingest IoT data into RabbitMQ by leveraging
|
||||
[EMQX Rules](https://docs.emqx.com/en/enterprise/v5.0/data-integration/rules.html).
|
||||
|
||||
|
||||
# Documentation
|
||||
|
||||
- Refer to the [RabbitMQ bridge documentation](https://docs.emqx.com/en/enterprise/v5.0/data-integration/data-bridge-rabbitmq.html)
|
||||
for how to use EMQX dashboard to ingest IoT data into RabbitMQ.
|
||||
- Refer to [EMQX Rules](https://docs.emqx.com/en/enterprise/v5.0/data-integration/rules.html)
|
||||
for an introduction to the EMQX rules engine.
|
||||
|
||||
|
||||
# HTTP APIs
|
||||
|
||||
- Several APIs are provided for bridge management, which includes create bridge,
|
||||
update bridge, get bridge, stop or restart bridge and list bridges etc.
|
||||
|
||||
Refer to [API Docs - Bridges](https://docs.emqx.com/en/enterprise/v5.0/admin/api-docs.html#tag/Bridges) for more detailed information.
|
||||
|
||||
|
||||
# Contributing
|
||||
|
||||
Please see our [contributing.md](../../CONTRIBUTING.md).
|
||||
|
||||
|
||||
# License
|
||||
|
||||
EMQ Business Source License 1.1, refer to [LICENSE](BSL.txt).
|
||||
|
|
@ -0,0 +1 @@
|
|||
rabbitmq
|
|
@ -0,0 +1,33 @@
|
|||
%% -*- mode: erlang; -*-
|
||||
{erl_opts, [debug_info]}.
|
||||
{deps, [
|
||||
%% The following two are dependencies of rabbit_common
|
||||
{thoas, {git, "https://github.com/emqx/thoas.git", {tag, "v1.0.0"}}}
|
||||
, {credentials_obfuscation, {git, "https://github.com/emqx/credentials-obfuscation.git", {tag, "v3.2.0"}}}
|
||||
%% The v3.11.13_with_app_src tag, employed in the next two dependencies,
|
||||
%% represents a fork of the official RabbitMQ v3.11.13 tag. This fork diverges
|
||||
%% from the official version as it includes app and hrl files
|
||||
%% generated by make files in subdirectories deps/rabbit_common and
|
||||
%% deps/amqp_client (app files are also relocated from the ebin to the src
|
||||
%% directory). This modification ensures compatibility with rebar3, as
|
||||
%% rabbit_common and amqp_client utilize the erlang.mk build tool.
|
||||
%% Similar changes are probably needed when upgrading to newer versions
|
||||
%% of rabbit_common and amqp_client. There are hex packages for rabbit_common and
|
||||
%% amqp_client, but they are not used here as we don't want to depend on
|
||||
%% packages that we don't have control over.
|
||||
, {rabbit_common, {git_subdir,
|
||||
"https://github.com/emqx/rabbitmq-server.git",
|
||||
{tag, "v3.11.13-emqx"},
|
||||
"deps/rabbit_common"}}
|
||||
, {amqp_client, {git_subdir,
|
||||
"https://github.com/emqx/rabbitmq-server.git",
|
||||
{tag, "v3.11.13-emqx"},
|
||||
"deps/amqp_client"}}
|
||||
, {emqx_connector, {path, "../../apps/emqx_connector"}}
|
||||
, {emqx_resource, {path, "../../apps/emqx_resource"}}
|
||||
, {emqx_bridge, {path, "../../apps/emqx_bridge"}}
|
||||
]}.
|
||||
|
||||
{shell, [
|
||||
{apps, [emqx_bridge_rabbitmq]}
|
||||
]}.
|
|
@ -0,0 +1,9 @@
|
|||
{application, emqx_bridge_rabbitmq, [
|
||||
{description, "EMQX Enterprise RabbitMQ Bridge"},
|
||||
{vsn, "0.1.0"},
|
||||
{registered, []},
|
||||
{applications, [kernel, stdlib, ecql, rabbit_common, amqp_client]},
|
||||
{env, []},
|
||||
{modules, []},
|
||||
{links, []}
|
||||
]}.
|
|
@ -0,0 +1,124 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%--------------------------------------------------------------------
|
||||
-module(emqx_bridge_rabbitmq).
|
||||
|
||||
-include_lib("emqx_bridge/include/emqx_bridge.hrl").
|
||||
-include_lib("typerefl/include/types.hrl").
|
||||
-include_lib("hocon/include/hoconsc.hrl").
|
||||
-include_lib("emqx_resource/include/emqx_resource.hrl").
|
||||
|
||||
-import(hoconsc, [mk/2, enum/1, ref/2]).
|
||||
|
||||
-export([
|
||||
conn_bridge_examples/1
|
||||
]).
|
||||
|
||||
-export([
|
||||
namespace/0,
|
||||
roots/0,
|
||||
fields/1,
|
||||
desc/1
|
||||
]).
|
||||
|
||||
%% -------------------------------------------------------------------------------------------------
|
||||
%% Callback used by HTTP API
|
||||
%% -------------------------------------------------------------------------------------------------
|
||||
|
||||
conn_bridge_examples(Method) ->
|
||||
[
|
||||
#{
|
||||
<<"rabbitmq">> => #{
|
||||
summary => <<"RabbitMQ Bridge">>,
|
||||
value => values(Method, "rabbitmq")
|
||||
}
|
||||
}
|
||||
].
|
||||
|
||||
values(_Method, Type) ->
|
||||
#{
|
||||
enable => true,
|
||||
type => Type,
|
||||
name => <<"foo">>,
|
||||
server => <<"localhost">>,
|
||||
port => 5672,
|
||||
username => <<"guest">>,
|
||||
password => <<"******">>,
|
||||
pool_size => 8,
|
||||
timeout => 5,
|
||||
virtual_host => <<"/">>,
|
||||
heartbeat => <<"30s">>,
|
||||
auto_reconnect => <<"2s">>,
|
||||
exchange => <<"messages">>,
|
||||
exchange_type => <<"topic">>,
|
||||
routing_key => <<"my_routing_key">>,
|
||||
durable => false,
|
||||
payload_template => <<"">>,
|
||||
resource_opts => #{
|
||||
worker_pool_size => 8,
|
||||
health_check_interval => ?HEALTHCHECK_INTERVAL_RAW,
|
||||
auto_restart_interval => ?AUTO_RESTART_INTERVAL_RAW,
|
||||
batch_size => ?DEFAULT_BATCH_SIZE,
|
||||
batch_time => ?DEFAULT_BATCH_TIME,
|
||||
query_mode => async,
|
||||
max_buffer_bytes => ?DEFAULT_BUFFER_BYTES
|
||||
}
|
||||
}.
|
||||
|
||||
%% -------------------------------------------------------------------------------------------------
|
||||
%% Hocon Schema Definitions
|
||||
%% -------------------------------------------------------------------------------------------------
|
||||
|
||||
namespace() -> "bridge_rabbitmq".
|
||||
|
||||
roots() -> [].
|
||||
|
||||
fields("config") ->
|
||||
[
|
||||
{enable, mk(boolean(), #{desc => ?DESC("config_enable"), default => true})},
|
||||
{local_topic,
|
||||
mk(
|
||||
binary(),
|
||||
#{desc => ?DESC("local_topic"), default => undefined}
|
||||
)},
|
||||
{resource_opts,
|
||||
mk(
|
||||
ref(?MODULE, "creation_opts"),
|
||||
#{
|
||||
required => false,
|
||||
default => #{},
|
||||
desc => ?DESC(emqx_resource_schema, <<"resource_opts">>)
|
||||
}
|
||||
)}
|
||||
] ++
|
||||
emqx_bridge_rabbitmq_connector:fields(config);
|
||||
fields("creation_opts") ->
|
||||
emqx_resource_schema:fields("creation_opts");
|
||||
fields("post") ->
|
||||
fields("post", rabbitmq);
|
||||
fields("put") ->
|
||||
fields("config");
|
||||
fields("get") ->
|
||||
emqx_bridge_schema:status_fields() ++ fields("post").
|
||||
|
||||
fields("post", Type) ->
|
||||
[type_field(Type), name_field() | fields("config")].
|
||||
|
||||
desc("config") ->
|
||||
?DESC("desc_config");
|
||||
desc(Method) when Method =:= "get"; Method =:= "put"; Method =:= "post" ->
|
||||
["Configuration for RabbitMQ using `", string:to_upper(Method), "` method."];
|
||||
desc("creation_opts" = Name) ->
|
||||
emqx_resource_schema:desc(Name);
|
||||
desc(_) ->
|
||||
undefined.
|
||||
|
||||
%% -------------------------------------------------------------------------------------------------
|
||||
%% internal
|
||||
%% -------------------------------------------------------------------------------------------------
|
||||
|
||||
type_field(Type) ->
|
||||
{type, mk(enum([Type]), #{required => true, desc => ?DESC("desc_type")})}.
|
||||
|
||||
name_field() ->
|
||||
{name, mk(binary(), #{required => true, desc => ?DESC("desc_name")})}.
|
|
@ -0,0 +1,533 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_bridge_rabbitmq_connector).
|
||||
|
||||
-include_lib("emqx_connector/include/emqx_connector.hrl").
|
||||
-include_lib("emqx_resource/include/emqx_resource.hrl").
|
||||
-include_lib("typerefl/include/types.hrl").
|
||||
-include_lib("emqx/include/logger.hrl").
|
||||
-include_lib("hocon/include/hoconsc.hrl").
|
||||
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
|
||||
|
||||
%% Needed to create RabbitMQ connection
|
||||
-include_lib("amqp_client/include/amqp_client.hrl").
|
||||
|
||||
-behaviour(emqx_resource).
|
||||
-behaviour(hocon_schema).
|
||||
-behaviour(ecpool_worker).
|
||||
|
||||
%% hocon_schema callbacks
|
||||
-export([roots/0, fields/1]).
|
||||
|
||||
%% HTTP API callbacks
|
||||
-export([values/1]).
|
||||
|
||||
%% emqx_resource callbacks
|
||||
-export([
|
||||
%% Required callbacks
|
||||
on_start/2,
|
||||
on_stop/2,
|
||||
callback_mode/0,
|
||||
%% Optional callbacks
|
||||
on_get_status/2,
|
||||
on_query/3,
|
||||
is_buffer_supported/0,
|
||||
on_batch_query/3
|
||||
]).
|
||||
|
||||
%% callbacks for ecpool_worker
|
||||
-export([connect/1]).
|
||||
|
||||
%% Internal callbacks
|
||||
-export([publish_messages/3]).
|
||||
|
||||
roots() ->
|
||||
[{config, #{type => hoconsc:ref(?MODULE, config)}}].
|
||||
|
||||
fields(config) ->
|
||||
[
|
||||
{server,
|
||||
hoconsc:mk(
|
||||
typerefl:binary(),
|
||||
#{
|
||||
default => <<"localhost">>,
|
||||
desc => ?DESC("server")
|
||||
}
|
||||
)},
|
||||
{port,
|
||||
hoconsc:mk(
|
||||
emqx_schema:port_number(),
|
||||
#{
|
||||
default => 5672,
|
||||
desc => ?DESC("server")
|
||||
}
|
||||
)},
|
||||
{username,
|
||||
hoconsc:mk(
|
||||
typerefl:binary(),
|
||||
#{
|
||||
required => true,
|
||||
desc => ?DESC("username")
|
||||
}
|
||||
)},
|
||||
{password, fun emqx_connector_schema_lib:password/1},
|
||||
{pool_size,
|
||||
hoconsc:mk(
|
||||
typerefl:pos_integer(),
|
||||
#{
|
||||
default => 8,
|
||||
desc => ?DESC("pool_size")
|
||||
}
|
||||
)},
|
||||
{timeout,
|
||||
hoconsc:mk(
|
||||
emqx_schema:duration_ms(),
|
||||
#{
|
||||
default => <<"5s">>,
|
||||
desc => ?DESC("timeout")
|
||||
}
|
||||
)},
|
||||
{wait_for_publish_confirmations,
|
||||
hoconsc:mk(
|
||||
boolean(),
|
||||
#{
|
||||
default => true,
|
||||
desc => ?DESC("wait_for_publish_confirmations")
|
||||
}
|
||||
)},
|
||||
{publish_confirmation_timeout,
|
||||
hoconsc:mk(
|
||||
emqx_schema:duration_ms(),
|
||||
#{
|
||||
default => <<"30s">>,
|
||||
desc => ?DESC("timeout")
|
||||
}
|
||||
)},
|
||||
|
||||
{virtual_host,
|
||||
hoconsc:mk(
|
||||
typerefl:binary(),
|
||||
#{
|
||||
default => <<"/">>,
|
||||
desc => ?DESC("virtual_host")
|
||||
}
|
||||
)},
|
||||
{heartbeat,
|
||||
hoconsc:mk(
|
||||
emqx_schema:duration_ms(),
|
||||
#{
|
||||
default => <<"30s">>,
|
||||
desc => ?DESC("heartbeat")
|
||||
}
|
||||
)},
|
||||
%% Things related to sending messages to RabbitMQ
|
||||
{exchange,
|
||||
hoconsc:mk(
|
||||
typerefl:binary(),
|
||||
#{
|
||||
required => true,
|
||||
desc => ?DESC("exchange")
|
||||
}
|
||||
)},
|
||||
{routing_key,
|
||||
hoconsc:mk(
|
||||
typerefl:binary(),
|
||||
#{
|
||||
required => true,
|
||||
desc => ?DESC("routing_key")
|
||||
}
|
||||
)},
|
||||
{delivery_mode,
|
||||
hoconsc:mk(
|
||||
hoconsc:enum([non_persistent, persistent]),
|
||||
#{
|
||||
default => non_persistent,
|
||||
desc => ?DESC("delivery_mode")
|
||||
}
|
||||
)},
|
||||
{payload_template,
|
||||
hoconsc:mk(
|
||||
binary(),
|
||||
#{
|
||||
default => <<"${.}">>,
|
||||
desc => ?DESC("payload_template")
|
||||
}
|
||||
)}
|
||||
].
|
||||
|
||||
values(post) ->
|
||||
maps:merge(values(put), #{name => <<"connector">>});
|
||||
values(get) ->
|
||||
values(post);
|
||||
values(put) ->
|
||||
#{
|
||||
server => <<"localhost">>,
|
||||
port => 5672,
|
||||
enable => true,
|
||||
pool_size => 8,
|
||||
type => rabbitmq,
|
||||
username => <<"guest">>,
|
||||
password => <<"******">>,
|
||||
routing_key => <<"my_routing_key">>,
|
||||
payload_template => <<"">>
|
||||
};
|
||||
values(_) ->
|
||||
#{}.
|
||||
|
||||
%% ===================================================================
|
||||
%% Callbacks defined in emqx_resource
|
||||
%% ===================================================================
|
||||
|
||||
%% emqx_resource callback
|
||||
|
||||
callback_mode() -> always_sync.
|
||||
|
||||
%% emqx_resource callback
|
||||
|
||||
-spec is_buffer_supported() -> boolean().
|
||||
is_buffer_supported() ->
|
||||
%% We want to make use of EMQX's buffer mechanism
|
||||
false.
|
||||
|
||||
%% emqx_resource callback called when the resource is started
|
||||
|
||||
-spec on_start(resource_id(), term()) -> {ok, resource_state()} | {error, _}.
|
||||
on_start(
|
||||
InstanceID,
|
||||
#{
|
||||
pool_size := PoolSize,
|
||||
payload_template := PayloadTemplate,
|
||||
password := Password,
|
||||
delivery_mode := InitialDeliveryMode
|
||||
} = InitialConfig
|
||||
) ->
|
||||
DeliveryMode =
|
||||
case InitialDeliveryMode of
|
||||
non_persistent -> 1;
|
||||
persistent -> 2
|
||||
end,
|
||||
Config = InitialConfig#{
|
||||
password => emqx_secret:wrap(Password),
|
||||
delivery_mode => DeliveryMode
|
||||
},
|
||||
?SLOG(info, #{
|
||||
msg => "starting_rabbitmq_connector",
|
||||
connector => InstanceID,
|
||||
config => emqx_utils:redact(Config)
|
||||
}),
|
||||
Options = [
|
||||
{config, Config},
|
||||
%% The pool_size is read by ecpool and decides the number of workers in
|
||||
%% the pool
|
||||
{pool_size, PoolSize},
|
||||
{pool, InstanceID}
|
||||
],
|
||||
ProcessedTemplate = emqx_plugin_libs_rule:preproc_tmpl(PayloadTemplate),
|
||||
State = #{
|
||||
poolname => InstanceID,
|
||||
processed_payload_template => ProcessedTemplate,
|
||||
config => Config
|
||||
},
|
||||
case emqx_resource_pool:start(InstanceID, ?MODULE, Options) of
|
||||
ok ->
|
||||
{ok, State};
|
||||
{error, Reason} ->
|
||||
LogMessage =
|
||||
#{
|
||||
msg => "rabbitmq_connector_start_failed",
|
||||
error_reason => Reason,
|
||||
config => emqx_utils:redact(Config)
|
||||
},
|
||||
?SLOG(info, LogMessage),
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
%% emqx_resource callback called when the resource is stopped
|
||||
|
||||
-spec on_stop(resource_id(), resource_state()) -> term().
|
||||
on_stop(
|
||||
ResourceID,
|
||||
#{poolname := PoolName} = _State
|
||||
) ->
|
||||
?SLOG(info, #{
|
||||
msg => "stopping RabbitMQ connector",
|
||||
connector => ResourceID
|
||||
}),
|
||||
Workers = [Worker || {_WorkerName, Worker} <- ecpool:workers(PoolName)],
|
||||
Clients = [
|
||||
begin
|
||||
{ok, Client} = ecpool_worker:client(Worker),
|
||||
Client
|
||||
end
|
||||
|| Worker <- Workers
|
||||
],
|
||||
%% We need to stop the pool before stopping the workers as the pool monitors the workers
|
||||
StopResult = emqx_resource_pool:stop(PoolName),
|
||||
lists:foreach(fun stop_worker/1, Clients),
|
||||
StopResult.
|
||||
|
||||
stop_worker({Channel, Connection}) ->
|
||||
amqp_channel:close(Channel),
|
||||
amqp_connection:close(Connection).
|
||||
|
||||
%% This is the callback function that is called by ecpool when the pool is
|
||||
%% started
|
||||
|
||||
-spec connect(term()) -> {ok, {pid(), pid()}, map()} | {error, term()}.
|
||||
connect(Options) ->
|
||||
Config = proplists:get_value(config, Options),
|
||||
try
|
||||
create_rabbitmq_connection_and_channel(Config)
|
||||
catch
|
||||
_:{error, Reason} ->
|
||||
?SLOG(error, #{
|
||||
msg => "rabbitmq_connector_connection_failed",
|
||||
error_type => error,
|
||||
error_reason => Reason,
|
||||
config => emqx_utils:redact(Config)
|
||||
}),
|
||||
{error, Reason};
|
||||
Type:Reason ->
|
||||
?SLOG(error, #{
|
||||
msg => "rabbitmq_connector_connection_failed",
|
||||
error_type => Type,
|
||||
error_reason => Reason,
|
||||
config => emqx_utils:redact(Config)
|
||||
}),
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
create_rabbitmq_connection_and_channel(Config) ->
|
||||
#{
|
||||
server := Host,
|
||||
port := Port,
|
||||
username := Username,
|
||||
password := WrappedPassword,
|
||||
timeout := Timeout,
|
||||
virtual_host := VirtualHost,
|
||||
heartbeat := Heartbeat,
|
||||
wait_for_publish_confirmations := WaitForPublishConfirmations
|
||||
} = Config,
|
||||
Password = emqx_secret:unwrap(WrappedPassword),
|
||||
RabbitMQConnectionOptions =
|
||||
#amqp_params_network{
|
||||
host = erlang:binary_to_list(Host),
|
||||
port = Port,
|
||||
username = Username,
|
||||
password = Password,
|
||||
connection_timeout = Timeout,
|
||||
virtual_host = VirtualHost,
|
||||
heartbeat = Heartbeat
|
||||
},
|
||||
{ok, RabbitMQConnection} =
|
||||
case amqp_connection:start(RabbitMQConnectionOptions) of
|
||||
{ok, Connection} ->
|
||||
{ok, Connection};
|
||||
{error, Reason} ->
|
||||
erlang:error({error, Reason})
|
||||
end,
|
||||
{ok, RabbitMQChannel} =
|
||||
case amqp_connection:open_channel(RabbitMQConnection) of
|
||||
{ok, Channel} ->
|
||||
{ok, Channel};
|
||||
{error, OpenChannelErrorReason} ->
|
||||
erlang:error({error, OpenChannelErrorReason})
|
||||
end,
|
||||
%% We need to enable confirmations if we want to wait for them
|
||||
case WaitForPublishConfirmations of
|
||||
true ->
|
||||
case amqp_channel:call(RabbitMQChannel, #'confirm.select'{}) of
|
||||
#'confirm.select_ok'{} ->
|
||||
ok;
|
||||
Error ->
|
||||
ConfirmModeErrorReason =
|
||||
erlang:iolist_to_binary(
|
||||
io_lib:format(
|
||||
"Could not enable RabbitMQ confirmation mode ~p",
|
||||
[Error]
|
||||
)
|
||||
),
|
||||
erlang:error({error, ConfirmModeErrorReason})
|
||||
end;
|
||||
false ->
|
||||
ok
|
||||
end,
|
||||
{ok, {RabbitMQConnection, RabbitMQChannel}, #{
|
||||
supervisees => [RabbitMQConnection, RabbitMQChannel]
|
||||
}}.
|
||||
|
||||
%% emqx_resource callback called to check the status of the resource
|
||||
|
||||
-spec on_get_status(resource_id(), term()) ->
|
||||
{connected, resource_state()} | {disconnected, resource_state(), binary()}.
|
||||
on_get_status(
|
||||
_InstId,
|
||||
#{
|
||||
poolname := PoolName
|
||||
} = State
|
||||
) ->
|
||||
Workers = [Worker || {_WorkerName, Worker} <- ecpool:workers(PoolName)],
|
||||
Clients = [
|
||||
begin
|
||||
{ok, Client} = ecpool_worker:client(Worker),
|
||||
Client
|
||||
end
|
||||
|| Worker <- Workers
|
||||
],
|
||||
CheckResults = [
|
||||
check_worker(Client)
|
||||
|| Client <- Clients
|
||||
],
|
||||
Connected = length(CheckResults) > 0 andalso lists:all(fun(R) -> R end, CheckResults),
|
||||
case Connected of
|
||||
true ->
|
||||
{connected, State};
|
||||
false ->
|
||||
{disconnected, State, <<"not_connected">>}
|
||||
end;
|
||||
on_get_status(
|
||||
_InstId,
|
||||
State
|
||||
) ->
|
||||
{disconnect, State, <<"not_connected: no connection pool in state">>}.
|
||||
|
||||
check_worker({Channel, Connection}) ->
|
||||
erlang:is_process_alive(Channel) andalso erlang:is_process_alive(Connection).
|
||||
|
||||
%% emqx_resource callback that is called when a non-batch query is received
|
||||
|
||||
-spec on_query(resource_id(), Request, resource_state()) -> query_result() when
|
||||
Request :: {RequestType, Data},
|
||||
RequestType :: send_message,
|
||||
Data :: map().
|
||||
on_query(
|
||||
ResourceID,
|
||||
{RequestType, Data},
|
||||
#{
|
||||
poolname := PoolName,
|
||||
processed_payload_template := PayloadTemplate,
|
||||
config := Config
|
||||
} = State
|
||||
) ->
|
||||
?SLOG(debug, #{
|
||||
msg => "RabbitMQ connector received query",
|
||||
connector => ResourceID,
|
||||
type => RequestType,
|
||||
data => Data,
|
||||
state => emqx_utils:redact(State)
|
||||
}),
|
||||
MessageData = format_data(PayloadTemplate, Data),
|
||||
ecpool:pick_and_do(
|
||||
PoolName,
|
||||
{?MODULE, publish_messages, [Config, [MessageData]]},
|
||||
no_handover
|
||||
).
|
||||
|
||||
%% emqx_resource callback that is called when a batch query is received
|
||||
|
||||
-spec on_batch_query(resource_id(), BatchReq, resource_state()) -> query_result() when
|
||||
BatchReq :: nonempty_list({'send_message', map()}).
|
||||
on_batch_query(
|
||||
ResourceID,
|
||||
BatchReq,
|
||||
State
|
||||
) ->
|
||||
?SLOG(debug, #{
|
||||
msg => "RabbitMQ connector received batch query",
|
||||
connector => ResourceID,
|
||||
data => BatchReq,
|
||||
state => emqx_utils:redact(State)
|
||||
}),
|
||||
%% Currently we only support batch requests with the send_message key
|
||||
{Keys, MessagesToInsert} = lists:unzip(BatchReq),
|
||||
ensure_keys_are_of_type_send_message(Keys),
|
||||
%% Pick out the payload template
|
||||
#{
|
||||
processed_payload_template := PayloadTemplate,
|
||||
poolname := PoolName,
|
||||
config := Config
|
||||
} = State,
|
||||
%% Create batch payload
|
||||
FormattedMessages = [
|
||||
format_data(PayloadTemplate, Data)
|
||||
|| Data <- MessagesToInsert
|
||||
],
|
||||
%% Publish the messages
|
||||
ecpool:pick_and_do(
|
||||
PoolName,
|
||||
{?MODULE, publish_messages, [Config, FormattedMessages]},
|
||||
no_handover
|
||||
).
|
||||
|
||||
publish_messages(
|
||||
{_Connection, Channel},
|
||||
#{
|
||||
delivery_mode := DeliveryMode,
|
||||
routing_key := RoutingKey,
|
||||
exchange := Exchange,
|
||||
wait_for_publish_confirmations := WaitForPublishConfirmations,
|
||||
publish_confirmation_timeout := PublishConfirmationTimeout
|
||||
} = _Config,
|
||||
Messages
|
||||
) ->
|
||||
MessageProperties = #'P_basic'{
|
||||
headers = [],
|
||||
delivery_mode = DeliveryMode
|
||||
},
|
||||
Method = #'basic.publish'{
|
||||
exchange = Exchange,
|
||||
routing_key = RoutingKey
|
||||
},
|
||||
_ = [
|
||||
amqp_channel:cast(
|
||||
Channel,
|
||||
Method,
|
||||
#amqp_msg{
|
||||
payload = Message,
|
||||
props = MessageProperties
|
||||
}
|
||||
)
|
||||
|| Message <- Messages
|
||||
],
|
||||
case WaitForPublishConfirmations of
|
||||
true ->
|
||||
case amqp_channel:wait_for_confirms(Channel, PublishConfirmationTimeout) of
|
||||
true ->
|
||||
ok;
|
||||
false ->
|
||||
erlang:error(
|
||||
{recoverable_error,
|
||||
<<"RabbitMQ: Got NACK when waiting for message acknowledgment.">>}
|
||||
);
|
||||
timeout ->
|
||||
erlang:error(
|
||||
{recoverable_error,
|
||||
<<"RabbitMQ: Timeout when waiting for message acknowledgment.">>}
|
||||
)
|
||||
end;
|
||||
false ->
|
||||
ok
|
||||
end.
|
||||
|
||||
ensure_keys_are_of_type_send_message(Keys) ->
|
||||
case lists:all(fun is_send_message_atom/1, Keys) of
|
||||
true ->
|
||||
ok;
|
||||
false ->
|
||||
erlang:error(
|
||||
{unrecoverable_error,
|
||||
<<"Unexpected type for batch message (Expected send_message)">>}
|
||||
)
|
||||
end.
|
||||
|
||||
is_send_message_atom(send_message) ->
|
||||
true;
|
||||
is_send_message_atom(_) ->
|
||||
false.
|
||||
|
||||
format_data([], Msg) ->
|
||||
emqx_utils_json:encode(Msg);
|
||||
format_data(Tokens, Msg) ->
|
||||
emqx_plugin_libs_rule:proc_tmpl(Tokens, Msg).
|
|
@ -0,0 +1,371 @@
|
|||
%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_bridge_rabbitmq_SUITE).
|
||||
|
||||
-compile(nowarn_export_all).
|
||||
-compile(export_all).
|
||||
|
||||
-include_lib("emqx_connector/include/emqx_connector.hrl").
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
-include_lib("stdlib/include/assert.hrl").
|
||||
-include_lib("amqp_client/include/amqp_client.hrl").
|
||||
|
||||
%% See comment in
|
||||
%% lib-ee/emqx_ee_connector/test/ee_connector_rabbitmq_SUITE.erl for how to
|
||||
%% run this without bringing up the whole CI infrastucture
|
||||
|
||||
rabbit_mq_host() ->
|
||||
<<"rabbitmq">>.
|
||||
|
||||
rabbit_mq_port() ->
|
||||
5672.
|
||||
|
||||
rabbit_mq_exchange() ->
|
||||
<<"messages">>.
|
||||
|
||||
rabbit_mq_queue() ->
|
||||
<<"test_queue">>.
|
||||
|
||||
rabbit_mq_routing_key() ->
|
||||
<<"test_routing_key">>.
|
||||
|
||||
get_channel_connection(Config) ->
|
||||
proplists:get_value(channel_connection, Config).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Common Test Setup, Teardown and Testcase List
|
||||
%%------------------------------------------------------------------------------
|
||||
|
||||
init_per_suite(Config) ->
|
||||
% snabbkaffe:fix_ct_logging(),
|
||||
case
|
||||
emqx_common_test_helpers:is_tcp_server_available(
|
||||
erlang:binary_to_list(rabbit_mq_host()), rabbit_mq_port()
|
||||
)
|
||||
of
|
||||
true ->
|
||||
emqx_common_test_helpers:render_and_load_app_config(emqx_conf),
|
||||
ok = emqx_common_test_helpers:start_apps([emqx_conf, emqx_bridge]),
|
||||
ok = emqx_connector_test_helpers:start_apps([emqx_resource]),
|
||||
{ok, _} = application:ensure_all_started(emqx_connector),
|
||||
{ok, _} = application:ensure_all_started(emqx_ee_connector),
|
||||
{ok, _} = application:ensure_all_started(emqx_ee_bridge),
|
||||
{ok, _} = application:ensure_all_started(amqp_client),
|
||||
emqx_mgmt_api_test_util:init_suite(),
|
||||
ChannelConnection = setup_rabbit_mq_exchange_and_queue(),
|
||||
[{channel_connection, ChannelConnection} | Config];
|
||||
false ->
|
||||
case os:getenv("IS_CI") of
|
||||
"yes" ->
|
||||
throw(no_rabbitmq);
|
||||
_ ->
|
||||
{skip, no_rabbitmq}
|
||||
end
|
||||
end.
|
||||
|
||||
setup_rabbit_mq_exchange_and_queue() ->
|
||||
%% Create an exachange and a queue
|
||||
{ok, Connection} =
|
||||
amqp_connection:start(#amqp_params_network{
|
||||
host = erlang:binary_to_list(rabbit_mq_host()),
|
||||
port = rabbit_mq_port()
|
||||
}),
|
||||
{ok, Channel} = amqp_connection:open_channel(Connection),
|
||||
%% Create an exchange
|
||||
#'exchange.declare_ok'{} =
|
||||
amqp_channel:call(
|
||||
Channel,
|
||||
#'exchange.declare'{
|
||||
exchange = rabbit_mq_exchange(),
|
||||
type = <<"topic">>
|
||||
}
|
||||
),
|
||||
%% Create a queue
|
||||
#'queue.declare_ok'{} =
|
||||
amqp_channel:call(
|
||||
Channel,
|
||||
#'queue.declare'{queue = rabbit_mq_queue()}
|
||||
),
|
||||
%% Bind the queue to the exchange
|
||||
#'queue.bind_ok'{} =
|
||||
amqp_channel:call(
|
||||
Channel,
|
||||
#'queue.bind'{
|
||||
queue = rabbit_mq_queue(),
|
||||
exchange = rabbit_mq_exchange(),
|
||||
routing_key = rabbit_mq_routing_key()
|
||||
}
|
||||
),
|
||||
#{
|
||||
connection => Connection,
|
||||
channel => Channel
|
||||
}.
|
||||
|
||||
end_per_suite(Config) ->
|
||||
#{
|
||||
connection := Connection,
|
||||
channel := Channel
|
||||
} = get_channel_connection(Config),
|
||||
emqx_mgmt_api_test_util:end_suite(),
|
||||
ok = emqx_common_test_helpers:stop_apps([emqx_conf]),
|
||||
ok = emqx_connector_test_helpers:stop_apps([emqx_resource]),
|
||||
_ = application:stop(emqx_connector),
|
||||
_ = application:stop(emqx_ee_connector),
|
||||
_ = application:stop(emqx_bridge),
|
||||
%% Close the channel
|
||||
ok = amqp_channel:close(Channel),
|
||||
%% Close the connection
|
||||
ok = amqp_connection:close(Connection).
|
||||
|
||||
init_per_testcase(_, Config) ->
|
||||
Config.
|
||||
|
||||
end_per_testcase(_, _Config) ->
|
||||
ok.
|
||||
|
||||
all() ->
|
||||
emqx_common_test_helpers:all(?MODULE).
|
||||
|
||||
rabbitmq_config(Config) ->
|
||||
%%SQL = maps:get(sql, Config, sql_insert_template_for_bridge()),
|
||||
BatchSize = maps:get(batch_size, Config, 1),
|
||||
BatchTime = maps:get(batch_time_ms, Config, 0),
|
||||
Name = atom_to_binary(?MODULE),
|
||||
Server = maps:get(server, Config, rabbit_mq_host()),
|
||||
Port = maps:get(port, Config, rabbit_mq_port()),
|
||||
Template = maps:get(payload_template, Config, <<"">>),
|
||||
ConfigString =
|
||||
io_lib:format(
|
||||
"bridges.rabbitmq.~s {\n"
|
||||
" enable = true\n"
|
||||
" server = \"~s\"\n"
|
||||
" port = ~p\n"
|
||||
" username = \"guest\"\n"
|
||||
" password = \"guest\"\n"
|
||||
" routing_key = \"~s\"\n"
|
||||
" exchange = \"~s\"\n"
|
||||
" payload_template = \"~s\"\n"
|
||||
" resource_opts = {\n"
|
||||
" batch_size = ~b\n"
|
||||
" batch_time = ~bms\n"
|
||||
" }\n"
|
||||
"}\n",
|
||||
[
|
||||
Name,
|
||||
Server,
|
||||
Port,
|
||||
rabbit_mq_routing_key(),
|
||||
rabbit_mq_exchange(),
|
||||
Template,
|
||||
BatchSize,
|
||||
BatchTime
|
||||
]
|
||||
),
|
||||
ct:pal(ConfigString),
|
||||
parse_and_check(ConfigString, <<"rabbitmq">>, Name).
|
||||
|
||||
parse_and_check(ConfigString, BridgeType, Name) ->
|
||||
{ok, RawConf} = hocon:binary(ConfigString, #{format => map}),
|
||||
hocon_tconf:check_plain(emqx_bridge_schema, RawConf, #{required => false, atom_key => false}),
|
||||
#{<<"bridges">> := #{BridgeType := #{Name := RetConfig}}} = RawConf,
|
||||
RetConfig.
|
||||
|
||||
make_bridge(Config) ->
|
||||
Type = <<"rabbitmq">>,
|
||||
Name = atom_to_binary(?MODULE),
|
||||
BridgeConfig = rabbitmq_config(Config),
|
||||
{ok, _} = emqx_bridge:create(
|
||||
Type,
|
||||
Name,
|
||||
BridgeConfig
|
||||
),
|
||||
emqx_bridge_resource:bridge_id(Type, Name).
|
||||
|
||||
delete_bridge() ->
|
||||
Type = <<"rabbitmq">>,
|
||||
Name = atom_to_binary(?MODULE),
|
||||
{ok, _} = emqx_bridge:remove(Type, Name),
|
||||
ok.
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Test Cases
|
||||
%%------------------------------------------------------------------------------
|
||||
|
||||
t_make_delete_bridge(_Config) ->
|
||||
make_bridge(#{}),
|
||||
%% Check that the new brige is in the list of bridges
|
||||
Bridges = emqx_bridge:list(),
|
||||
Name = atom_to_binary(?MODULE),
|
||||
IsRightName =
|
||||
fun
|
||||
(#{name := BName}) when BName =:= Name ->
|
||||
true;
|
||||
(_) ->
|
||||
false
|
||||
end,
|
||||
?assert(lists:any(IsRightName, Bridges)),
|
||||
delete_bridge(),
|
||||
BridgesAfterDelete = emqx_bridge:list(),
|
||||
?assertNot(lists:any(IsRightName, BridgesAfterDelete)),
|
||||
ok.
|
||||
|
||||
t_make_delete_bridge_non_existing_server(_Config) ->
|
||||
make_bridge(#{server => <<"non_existing_server">>, port => 3174}),
|
||||
%% Check that the new brige is in the list of bridges
|
||||
Bridges = emqx_bridge:list(),
|
||||
Name = atom_to_binary(?MODULE),
|
||||
IsRightName =
|
||||
fun
|
||||
(#{name := BName}) when BName =:= Name ->
|
||||
true;
|
||||
(_) ->
|
||||
false
|
||||
end,
|
||||
?assert(lists:any(IsRightName, Bridges)),
|
||||
delete_bridge(),
|
||||
BridgesAfterDelete = emqx_bridge:list(),
|
||||
?assertNot(lists:any(IsRightName, BridgesAfterDelete)),
|
||||
ok.
|
||||
|
||||
t_send_message_query(Config) ->
|
||||
BridgeID = make_bridge(#{batch_size => 1}),
|
||||
Payload = #{<<"key">> => 42, <<"data">> => <<"RabbitMQ">>, <<"timestamp">> => 10000},
|
||||
%% This will use the SQL template included in the bridge
|
||||
emqx_bridge:send_message(BridgeID, Payload),
|
||||
%% Check that the data got to the database
|
||||
?assertEqual(Payload, receive_simple_test_message(Config)),
|
||||
delete_bridge(),
|
||||
ok.
|
||||
|
||||
t_send_message_query_with_template(Config) ->
|
||||
BridgeID = make_bridge(#{
|
||||
batch_size => 1,
|
||||
payload_template =>
|
||||
<<
|
||||
"{"
|
||||
" \\\"key\\\": ${key},"
|
||||
" \\\"data\\\": \\\"${data}\\\","
|
||||
" \\\"timestamp\\\": ${timestamp},"
|
||||
" \\\"secret\\\": 42"
|
||||
"}"
|
||||
>>
|
||||
}),
|
||||
Payload = #{
|
||||
<<"key">> => 7,
|
||||
<<"data">> => <<"RabbitMQ">>,
|
||||
<<"timestamp">> => 10000
|
||||
},
|
||||
emqx_bridge:send_message(BridgeID, Payload),
|
||||
%% Check that the data got to the database
|
||||
ExpectedResult = Payload#{
|
||||
<<"secret">> => 42
|
||||
},
|
||||
?assertEqual(ExpectedResult, receive_simple_test_message(Config)),
|
||||
delete_bridge(),
|
||||
ok.
|
||||
|
||||
t_send_simple_batch(Config) ->
|
||||
BridgeConf =
|
||||
#{
|
||||
batch_size => 100
|
||||
},
|
||||
BridgeID = make_bridge(BridgeConf),
|
||||
Payload = #{<<"key">> => 42, <<"data">> => <<"RabbitMQ">>, <<"timestamp">> => 10000},
|
||||
emqx_bridge:send_message(BridgeID, Payload),
|
||||
?assertEqual(Payload, receive_simple_test_message(Config)),
|
||||
delete_bridge(),
|
||||
ok.
|
||||
|
||||
t_send_simple_batch_with_template(Config) ->
|
||||
BridgeConf =
|
||||
#{
|
||||
batch_size => 100,
|
||||
payload_template =>
|
||||
<<
|
||||
"{"
|
||||
" \\\"key\\\": ${key},"
|
||||
" \\\"data\\\": \\\"${data}\\\","
|
||||
" \\\"timestamp\\\": ${timestamp},"
|
||||
" \\\"secret\\\": 42"
|
||||
"}"
|
||||
>>
|
||||
},
|
||||
BridgeID = make_bridge(BridgeConf),
|
||||
Payload = #{
|
||||
<<"key">> => 7,
|
||||
<<"data">> => <<"RabbitMQ">>,
|
||||
<<"timestamp">> => 10000
|
||||
},
|
||||
emqx_bridge:send_message(BridgeID, Payload),
|
||||
ExpectedResult = Payload#{
|
||||
<<"secret">> => 42
|
||||
},
|
||||
?assertEqual(ExpectedResult, receive_simple_test_message(Config)),
|
||||
delete_bridge(),
|
||||
ok.
|
||||
|
||||
t_heavy_batching(Config) ->
|
||||
NumberOfMessages = 20000,
|
||||
BridgeConf = #{
|
||||
batch_size => 10173,
|
||||
batch_time_ms => 50
|
||||
},
|
||||
BridgeID = make_bridge(BridgeConf),
|
||||
SendMessage = fun(Key) ->
|
||||
Payload = #{
|
||||
<<"key">> => Key
|
||||
},
|
||||
emqx_bridge:send_message(BridgeID, Payload)
|
||||
end,
|
||||
[SendMessage(Key) || Key <- lists:seq(1, NumberOfMessages)],
|
||||
AllMessages = lists:foldl(
|
||||
fun(_, Acc) ->
|
||||
Message = receive_simple_test_message(Config),
|
||||
#{<<"key">> := Key} = Message,
|
||||
Acc#{Key => true}
|
||||
end,
|
||||
#{},
|
||||
lists:seq(1, NumberOfMessages)
|
||||
),
|
||||
?assertEqual(NumberOfMessages, maps:size(AllMessages)),
|
||||
delete_bridge(),
|
||||
ok.
|
||||
|
||||
receive_simple_test_message(Config) ->
|
||||
#{channel := Channel} = get_channel_connection(Config),
|
||||
#'basic.consume_ok'{consumer_tag = ConsumerTag} =
|
||||
amqp_channel:call(
|
||||
Channel,
|
||||
#'basic.consume'{
|
||||
queue = rabbit_mq_queue()
|
||||
}
|
||||
),
|
||||
receive
|
||||
%% This is the first message received
|
||||
#'basic.consume_ok'{} ->
|
||||
ok
|
||||
end,
|
||||
receive
|
||||
{#'basic.deliver'{delivery_tag = DeliveryTag}, Content} ->
|
||||
%% Ack the message
|
||||
amqp_channel:cast(Channel, #'basic.ack'{delivery_tag = DeliveryTag}),
|
||||
%% Cancel the consumer
|
||||
#'basic.cancel_ok'{consumer_tag = ConsumerTag} =
|
||||
amqp_channel:call(Channel, #'basic.cancel'{consumer_tag = ConsumerTag}),
|
||||
emqx_utils_json:decode(Content#amqp_msg.payload)
|
||||
end.
|
||||
|
||||
rabbitmq_config() ->
|
||||
Config =
|
||||
#{
|
||||
server => rabbit_mq_host(),
|
||||
port => 5672,
|
||||
exchange => rabbit_mq_exchange(),
|
||||
routing_key => rabbit_mq_routing_key()
|
||||
},
|
||||
#{<<"config">> => Config}.
|
||||
|
||||
test_data() ->
|
||||
#{<<"msg_field">> => <<"Hello">>}.
|
|
@ -0,0 +1,232 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_bridge_rabbitmq_connector_SUITE).
|
||||
|
||||
-compile(nowarn_export_all).
|
||||
-compile(export_all).
|
||||
|
||||
-include("emqx_connector.hrl").
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
-include_lib("stdlib/include/assert.hrl").
|
||||
-include_lib("amqp_client/include/amqp_client.hrl").
|
||||
|
||||
%% This test SUITE requires a running RabbitMQ instance. If you don't want to
|
||||
%% bring up the whole CI infrastuctucture with the `scripts/ct/run.sh` script
|
||||
%% you can create a clickhouse instance with the following command.
|
||||
%% 5672 is the default port for AMQP 0-9-1 and 15672 is the default port for
|
||||
%% the HTTP managament interface.
|
||||
%%
|
||||
%% docker run -it --rm --name rabbitmq -p 127.0.0.1:5672:5672 -p 127.0.0.1:15672:15672 rabbitmq:3.11-management
|
||||
|
||||
rabbit_mq_host() ->
|
||||
<<"rabbitmq">>.
|
||||
|
||||
rabbit_mq_port() ->
|
||||
5672.
|
||||
|
||||
rabbit_mq_exchange() ->
|
||||
<<"test_exchange">>.
|
||||
|
||||
rabbit_mq_queue() ->
|
||||
<<"test_queue">>.
|
||||
|
||||
rabbit_mq_routing_key() ->
|
||||
<<"test_routing_key">>.
|
||||
|
||||
all() ->
|
||||
emqx_common_test_helpers:all(?MODULE).
|
||||
|
||||
init_per_suite(Config) ->
|
||||
case
|
||||
emqx_common_test_helpers:is_tcp_server_available(
|
||||
erlang:binary_to_list(rabbit_mq_host()), rabbit_mq_port()
|
||||
)
|
||||
of
|
||||
true ->
|
||||
ok = emqx_common_test_helpers:start_apps([emqx_conf]),
|
||||
ok = emqx_connector_test_helpers:start_apps([emqx_resource]),
|
||||
{ok, _} = application:ensure_all_started(emqx_connector),
|
||||
{ok, _} = application:ensure_all_started(emqx_ee_connector),
|
||||
{ok, _} = application:ensure_all_started(amqp_client),
|
||||
ChannelConnection = setup_rabbit_mq_exchange_and_queue(),
|
||||
[{channel_connection, ChannelConnection} | Config];
|
||||
false ->
|
||||
case os:getenv("IS_CI") of
|
||||
"yes" ->
|
||||
throw(no_rabbitmq);
|
||||
_ ->
|
||||
{skip, no_rabbitmq}
|
||||
end
|
||||
end.
|
||||
|
||||
setup_rabbit_mq_exchange_and_queue() ->
|
||||
%% Create an exachange and a queue
|
||||
{ok, Connection} =
|
||||
amqp_connection:start(#amqp_params_network{
|
||||
host = erlang:binary_to_list(rabbit_mq_host()),
|
||||
port = rabbit_mq_port()
|
||||
}),
|
||||
{ok, Channel} = amqp_connection:open_channel(Connection),
|
||||
%% Create an exchange
|
||||
#'exchange.declare_ok'{} =
|
||||
amqp_channel:call(
|
||||
Channel,
|
||||
#'exchange.declare'{
|
||||
exchange = rabbit_mq_exchange(),
|
||||
type = <<"topic">>
|
||||
}
|
||||
),
|
||||
%% Create a queue
|
||||
#'queue.declare_ok'{} =
|
||||
amqp_channel:call(
|
||||
Channel,
|
||||
#'queue.declare'{queue = rabbit_mq_queue()}
|
||||
),
|
||||
%% Bind the queue to the exchange
|
||||
#'queue.bind_ok'{} =
|
||||
amqp_channel:call(
|
||||
Channel,
|
||||
#'queue.bind'{
|
||||
queue = rabbit_mq_queue(),
|
||||
exchange = rabbit_mq_exchange(),
|
||||
routing_key = rabbit_mq_routing_key()
|
||||
}
|
||||
),
|
||||
#{
|
||||
connection => Connection,
|
||||
channel => Channel
|
||||
}.
|
||||
|
||||
get_channel_connection(Config) ->
|
||||
proplists:get_value(channel_connection, Config).
|
||||
|
||||
end_per_suite(Config) ->
|
||||
#{
|
||||
connection := Connection,
|
||||
channel := Channel
|
||||
} = get_channel_connection(Config),
|
||||
ok = emqx_common_test_helpers:stop_apps([emqx_conf]),
|
||||
ok = emqx_connector_test_helpers:stop_apps([emqx_resource]),
|
||||
_ = application:stop(emqx_connector),
|
||||
%% Close the channel
|
||||
ok = amqp_channel:close(Channel),
|
||||
%% Close the connection
|
||||
ok = amqp_connection:close(Connection).
|
||||
|
||||
% %%------------------------------------------------------------------------------
|
||||
% %% Testcases
|
||||
% %%------------------------------------------------------------------------------
|
||||
|
||||
t_lifecycle(Config) ->
|
||||
perform_lifecycle_check(
|
||||
erlang:atom_to_binary(?MODULE),
|
||||
rabbitmq_config(),
|
||||
Config
|
||||
).
|
||||
|
||||
perform_lifecycle_check(ResourceID, InitialConfig, TestConfig) ->
|
||||
#{
|
||||
channel := Channel
|
||||
} = get_channel_connection(TestConfig),
|
||||
{ok, #{config := CheckedConfig}} =
|
||||
emqx_resource:check_config(emqx_bridge_rabbitmq_connector, InitialConfig),
|
||||
{ok, #{
|
||||
state := #{poolname := PoolName} = State,
|
||||
status := InitialStatus
|
||||
}} =
|
||||
emqx_resource:create_local(
|
||||
ResourceID,
|
||||
?CONNECTOR_RESOURCE_GROUP,
|
||||
emqx_bridge_rabbitmq_connector,
|
||||
CheckedConfig,
|
||||
#{}
|
||||
),
|
||||
?assertEqual(InitialStatus, connected),
|
||||
%% Instance should match the state and status of the just started resource
|
||||
{ok, ?CONNECTOR_RESOURCE_GROUP, #{
|
||||
state := State,
|
||||
status := InitialStatus
|
||||
}} =
|
||||
emqx_resource:get_instance(ResourceID),
|
||||
?assertEqual({ok, connected}, emqx_resource:health_check(ResourceID)),
|
||||
%% Perform query as further check that the resource is working as expected
|
||||
perform_query(ResourceID, Channel),
|
||||
?assertEqual(ok, emqx_resource:stop(ResourceID)),
|
||||
%% Resource will be listed still, but state will be changed and healthcheck will fail
|
||||
%% as the worker no longer exists.
|
||||
{ok, ?CONNECTOR_RESOURCE_GROUP, #{
|
||||
state := State,
|
||||
status := StoppedStatus
|
||||
}} = emqx_resource:get_instance(ResourceID),
|
||||
?assertEqual(stopped, StoppedStatus),
|
||||
?assertEqual({error, resource_is_stopped}, emqx_resource:health_check(ResourceID)),
|
||||
% Resource healthcheck shortcuts things by checking ets. Go deeper by checking pool itself.
|
||||
?assertEqual({error, not_found}, ecpool:stop_sup_pool(PoolName)),
|
||||
% Can call stop/1 again on an already stopped instance
|
||||
?assertEqual(ok, emqx_resource:stop(ResourceID)),
|
||||
% Make sure it can be restarted and the healthchecks and queries work properly
|
||||
?assertEqual(ok, emqx_resource:restart(ResourceID)),
|
||||
% async restart, need to wait resource
|
||||
timer:sleep(500),
|
||||
{ok, ?CONNECTOR_RESOURCE_GROUP, #{status := InitialStatus}} =
|
||||
emqx_resource:get_instance(ResourceID),
|
||||
?assertEqual({ok, connected}, emqx_resource:health_check(ResourceID)),
|
||||
%% Check that everything is working again by performing a query
|
||||
perform_query(ResourceID, Channel),
|
||||
% Stop and remove the resource in one go.
|
||||
?assertEqual(ok, emqx_resource:remove_local(ResourceID)),
|
||||
?assertEqual({error, not_found}, ecpool:stop_sup_pool(PoolName)),
|
||||
% Should not even be able to get the resource data out of ets now unlike just stopping.
|
||||
?assertEqual({error, not_found}, emqx_resource:get_instance(ResourceID)).
|
||||
|
||||
% %%------------------------------------------------------------------------------
|
||||
% %% Helpers
|
||||
% %%------------------------------------------------------------------------------
|
||||
|
||||
perform_query(PoolName, Channel) ->
|
||||
%% Send message to queue:
|
||||
ok = emqx_resource:query(PoolName, {query, test_data()}),
|
||||
%% Get the message from queue:
|
||||
ok = receive_simple_test_message(Channel).
|
||||
|
||||
receive_simple_test_message(Channel) ->
|
||||
#'basic.consume_ok'{consumer_tag = ConsumerTag} =
|
||||
amqp_channel:call(
|
||||
Channel,
|
||||
#'basic.consume'{
|
||||
queue = rabbit_mq_queue()
|
||||
}
|
||||
),
|
||||
receive
|
||||
%% This is the first message received
|
||||
#'basic.consume_ok'{} ->
|
||||
ok
|
||||
end,
|
||||
receive
|
||||
{#'basic.deliver'{delivery_tag = DeliveryTag}, Content} ->
|
||||
Expected = test_data(),
|
||||
?assertEqual(Expected, emqx_utils_json:decode(Content#amqp_msg.payload)),
|
||||
%% Ack the message
|
||||
amqp_channel:cast(Channel, #'basic.ack'{delivery_tag = DeliveryTag}),
|
||||
%% Cancel the consumer
|
||||
#'basic.cancel_ok'{consumer_tag = ConsumerTag} =
|
||||
amqp_channel:call(Channel, #'basic.cancel'{consumer_tag = ConsumerTag}),
|
||||
ok
|
||||
end.
|
||||
|
||||
rabbitmq_config() ->
|
||||
Config =
|
||||
#{
|
||||
server => rabbit_mq_host(),
|
||||
port => 5672,
|
||||
username => <<"guest">>,
|
||||
password => <<"guest">>,
|
||||
exchange => rabbit_mq_exchange(),
|
||||
routing_key => rabbit_mq_routing_key()
|
||||
},
|
||||
#{<<"config">> => Config}.
|
||||
|
||||
test_data() ->
|
||||
#{<<"msg_field">> => <<"Hello">>}.
|
|
@ -0,0 +1,2 @@
|
|||
toxiproxy
|
||||
rocketmq
|
|
@ -0,0 +1,8 @@
|
|||
{erl_opts, [debug_info]}.
|
||||
|
||||
{deps, [
|
||||
{rocketmq, {git, "https://github.com/emqx/rocketmq-client-erl.git", {tag, "v0.5.1"}}},
|
||||
{emqx_connector, {path, "../../apps/emqx_connector"}},
|
||||
{emqx_resource, {path, "../../apps/emqx_resource"}},
|
||||
{emqx_bridge, {path, "../../apps/emqx_bridge"}}
|
||||
]}.
|
|
@ -1,8 +1,8 @@
|
|||
{application, emqx_bridge_rocketmq, [
|
||||
{description, "EMQX Enterprise RocketMQ Bridge"},
|
||||
{vsn, "0.1.0"},
|
||||
{vsn, "0.1.1"},
|
||||
{registered, []},
|
||||
{applications, [kernel, stdlib]},
|
||||
{applications, [kernel, stdlib, rocketmq]},
|
||||
{env, []},
|
||||
{modules, []},
|
||||
{links, []}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%--------------------------------------------------------------------
|
||||
-module(emqx_ee_bridge_rocketmq).
|
||||
-module(emqx_bridge_rocketmq).
|
||||
|
||||
-include_lib("typerefl/include/types.hrl").
|
||||
-include_lib("hocon/include/hoconsc.hrl").
|
||||
|
@ -82,7 +82,7 @@ fields("config") ->
|
|||
#{desc => ?DESC("local_topic"), required => false}
|
||||
)}
|
||||
] ++ emqx_resource_schema:fields("resource_opts") ++
|
||||
(emqx_ee_connector_rocketmq:fields(config) --
|
||||
(emqx_bridge_rocketmq_connector:fields(config) --
|
||||
emqx_connector_schema_lib:prepare_statement_fields());
|
||||
fields("post") ->
|
||||
[type_field(), name_field() | fields("config")];
|
|
@ -1,8 +1,8 @@
|
|||
%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_ee_connector_rocketmq).
|
||||
-module(emqx_bridge_rocketmq_connector).
|
||||
|
||||
-behaviour(emqx_resource).
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_ee_bridge_rocketmq_SUITE).
|
||||
-module(emqx_bridge_rocketmq_SUITE).
|
||||
|
||||
-compile(nowarn_export_all).
|
||||
-compile(export_all).
|
|
@ -0,0 +1,2 @@
|
|||
toxiproxy
|
||||
tdengine
|
|
@ -0,0 +1,8 @@
|
|||
{erl_opts, [debug_info]}.
|
||||
|
||||
{deps, [
|
||||
{tdengine, {git, "https://github.com/emqx/tdengine-client-erl", {tag, "0.1.6"}}},
|
||||
{emqx_connector, {path, "../../apps/emqx_connector"}},
|
||||
{emqx_resource, {path, "../../apps/emqx_resource"}},
|
||||
{emqx_bridge, {path, "../../apps/emqx_bridge"}}
|
||||
]}.
|
|
@ -1,8 +1,8 @@
|
|||
{application, emqx_bridge_tdengine, [
|
||||
{description, "EMQX Enterprise TDEngine Bridge"},
|
||||
{vsn, "0.1.0"},
|
||||
{vsn, "0.1.1"},
|
||||
{registered, []},
|
||||
{applications, [kernel, stdlib]},
|
||||
{applications, [kernel, stdlib, tdengine]},
|
||||
{env, []},
|
||||
{modules, []},
|
||||
{links, []}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%--------------------------------------------------------------------
|
||||
-module(emqx_ee_bridge_tdengine).
|
||||
-module(emqx_bridge_tdengine).
|
||||
|
||||
-include_lib("typerefl/include/types.hrl").
|
||||
-include_lib("hocon/include/hoconsc.hrl").
|
||||
|
@ -81,7 +81,8 @@ fields("config") ->
|
|||
binary(),
|
||||
#{desc => ?DESC("local_topic"), default => undefined}
|
||||
)}
|
||||
] ++ emqx_resource_schema:fields("resource_opts") ++ emqx_ee_connector_tdengine:fields(config);
|
||||
] ++ emqx_resource_schema:fields("resource_opts") ++
|
||||
emqx_bridge_tdengine_connector:fields(config);
|
||||
fields("post") ->
|
||||
[type_field(), name_field() | fields("config")];
|
||||
fields("put") ->
|
|
@ -2,7 +2,7 @@
|
|||
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_ee_connector_tdengine).
|
||||
-module(emqx_bridge_tdengine_connector).
|
||||
|
||||
-behaviour(emqx_resource).
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
%% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_ee_bridge_tdengine_SUITE).
|
||||
-module(emqx_bridge_tdengine_SUITE).
|
||||
|
||||
-compile(nowarn_export_all).
|
||||
-compile(export_all).
|
|
@ -0,0 +1,7 @@
|
|||
{erl_opts, [debug_info]}.
|
||||
|
||||
{deps, [
|
||||
{emqx_connector, {path, "../../apps/emqx_connector"}},
|
||||
{emqx_resource, {path, "../../apps/emqx_resource"}},
|
||||
{emqx_bridge, {path, "../../apps/emqx_bridge"}}
|
||||
]}.
|
|
@ -1,6 +1,6 @@
|
|||
{application, emqx_bridge_timescale, [
|
||||
{description, "EMQX Enterprise TimescaleDB Bridge"},
|
||||
{vsn, "0.1.0"},
|
||||
{vsn, "0.1.1"},
|
||||
{registered, []},
|
||||
{applications, [kernel, stdlib]},
|
||||
{env, []},
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%--------------------------------------------------------------------
|
||||
-module(emqx_ee_bridge_timescale).
|
||||
-module(emqx_bridge_timescale).
|
||||
|
||||
-export([
|
||||
conn_bridge_examples/1
|
||||
|
@ -22,7 +22,7 @@ conn_bridge_examples(Method) ->
|
|||
#{
|
||||
<<"timescale">> => #{
|
||||
summary => <<"Timescale Bridge">>,
|
||||
value => emqx_ee_bridge_pgsql:values(Method, timescale)
|
||||
value => emqx_bridge_pgsql:values(Method, timescale)
|
||||
}
|
||||
}
|
||||
].
|
||||
|
@ -34,9 +34,9 @@ namespace() -> "bridge_timescale".
|
|||
roots() -> [].
|
||||
|
||||
fields("post") ->
|
||||
emqx_ee_bridge_pgsql:fields("post", timescale);
|
||||
emqx_bridge_pgsql:fields("post", timescale);
|
||||
fields(Method) ->
|
||||
emqx_ee_bridge_pgsql:fields(Method).
|
||||
emqx_bridge_pgsql:fields(Method).
|
||||
|
||||
desc(_) ->
|
||||
undefined.
|
|
@ -316,6 +316,87 @@ authn_validations_test() ->
|
|||
?assertEqual(<<"application/json">>, maps:get(<<"accept">>, Headers4), Headers4),
|
||||
ok.
|
||||
|
||||
%% erlfmt-ignore
|
||||
-define(LISTENERS,
|
||||
"""
|
||||
listeners.ssl.default.bind = 9999
|
||||
listeners.wss.default.bind = 9998
|
||||
listeners.wss.default.ssl_options.cacertfile = \"mytest/certs/cacert.pem\"
|
||||
listeners.wss.new.bind = 9997
|
||||
listeners.wss.new.websocket.mqtt_path = \"/my-mqtt\"
|
||||
"""
|
||||
).
|
||||
|
||||
listeners_test() ->
|
||||
BaseConf = to_bin(?BASE_CONF, ["emqx1@127.0.0.1", "emqx1@127.0.0.1"]),
|
||||
|
||||
Conf = <<BaseConf/binary, ?LISTENERS>>,
|
||||
{ok, ConfMap0} = hocon:binary(Conf, #{format => richmap}),
|
||||
{_, ConfMap} = hocon_tconf:map_translate(emqx_conf_schema, ConfMap0, #{format => richmap}),
|
||||
#{<<"listeners">> := Listeners} = hocon_util:richmap_to_map(ConfMap),
|
||||
#{
|
||||
<<"tcp">> := #{<<"default">> := Tcp},
|
||||
<<"ws">> := #{<<"default">> := Ws},
|
||||
<<"wss">> := #{<<"default">> := DefaultWss, <<"new">> := NewWss},
|
||||
<<"ssl">> := #{<<"default">> := Ssl}
|
||||
} = Listeners,
|
||||
DefaultCacertFile = <<"${EMQX_ETC_DIR}/certs/cacert.pem">>,
|
||||
DefaultCertFile = <<"${EMQX_ETC_DIR}/certs/cert.pem">>,
|
||||
DefaultKeyFile = <<"${EMQX_ETC_DIR}/certs/key.pem">>,
|
||||
?assertMatch(
|
||||
#{
|
||||
<<"bind">> := {{0, 0, 0, 0}, 1883},
|
||||
<<"enabled">> := true
|
||||
},
|
||||
Tcp
|
||||
),
|
||||
?assertMatch(
|
||||
#{
|
||||
<<"bind">> := {{0, 0, 0, 0}, 8083},
|
||||
<<"enabled">> := true,
|
||||
<<"websocket">> := #{<<"mqtt_path">> := "/mqtt"}
|
||||
},
|
||||
Ws
|
||||
),
|
||||
?assertMatch(
|
||||
#{
|
||||
<<"bind">> := 9999,
|
||||
<<"ssl_options">> := #{
|
||||
<<"cacertfile">> := DefaultCacertFile,
|
||||
<<"certfile">> := DefaultCertFile,
|
||||
<<"keyfile">> := DefaultKeyFile
|
||||
}
|
||||
},
|
||||
Ssl
|
||||
),
|
||||
?assertMatch(
|
||||
#{
|
||||
<<"bind">> := 9998,
|
||||
<<"websocket">> := #{<<"mqtt_path">> := "/mqtt"},
|
||||
<<"ssl_options">> :=
|
||||
#{
|
||||
<<"cacertfile">> := <<"mytest/certs/cacert.pem">>,
|
||||
<<"certfile">> := DefaultCertFile,
|
||||
<<"keyfile">> := DefaultKeyFile
|
||||
}
|
||||
},
|
||||
DefaultWss
|
||||
),
|
||||
?assertMatch(
|
||||
#{
|
||||
<<"bind">> := 9997,
|
||||
<<"websocket">> := #{<<"mqtt_path">> := "/my-mqtt"},
|
||||
<<"ssl_options">> :=
|
||||
#{
|
||||
<<"cacertfile">> := DefaultCacertFile,
|
||||
<<"certfile">> := DefaultCertFile,
|
||||
<<"keyfile">> := DefaultKeyFile
|
||||
}
|
||||
},
|
||||
NewWss
|
||||
),
|
||||
ok.
|
||||
|
||||
authentication_headers(Conf) ->
|
||||
[#{<<"headers">> := Headers}] = hocon_maps:get("authentication", Conf),
|
||||
Headers.
|
||||
|
|
|
@ -846,6 +846,8 @@ typename_to_spec("bucket_name()", _Mod) ->
|
|||
#{type => string, example => <<"retainer">>};
|
||||
typename_to_spec("json_binary()", _Mod) ->
|
||||
#{type => string, example => <<"{\"a\": [1,true]}">>};
|
||||
typename_to_spec("port_number()", _Mod) ->
|
||||
range("1..65535");
|
||||
typename_to_spec(Name, Mod) ->
|
||||
Spec = range(Name),
|
||||
Spec1 = remote_module_type(Spec, Name, Mod),
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
set_default_config/0,
|
||||
set_default_config/1,
|
||||
set_default_config/2,
|
||||
set_default_config/3,
|
||||
request/2,
|
||||
request/3,
|
||||
request/4,
|
||||
|
@ -40,11 +41,14 @@ set_default_config(DefaultUsername) ->
|
|||
set_default_config(DefaultUsername, false).
|
||||
|
||||
set_default_config(DefaultUsername, HAProxyEnabled) ->
|
||||
set_default_config(DefaultUsername, HAProxyEnabled, #{}).
|
||||
|
||||
set_default_config(DefaultUsername, HAProxyEnabled, Opts) ->
|
||||
Config = #{
|
||||
listeners => #{
|
||||
http => #{
|
||||
enable => true,
|
||||
bind => 18083,
|
||||
bind => maps:get(bind, Opts, 18083),
|
||||
inet6 => false,
|
||||
ipv6_v6only => false,
|
||||
max_connections => 512,
|
||||
|
|
|
@ -25,6 +25,7 @@ all() ->
|
|||
emqx_common_test_helpers:all(?MODULE).
|
||||
|
||||
init_per_suite(Config) ->
|
||||
emqx_common_test_helpers:load_config(emqx_dashboard_schema, <<"dashboard {}">>),
|
||||
emqx_mgmt_api_test_util:init_suite([emqx_conf]),
|
||||
ok = change_i18n_lang(en),
|
||||
Config.
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
Business Source License 1.1
|
||||
|
||||
Licensor: Hangzhou EMQ Technologies Co., Ltd.
|
||||
Licensed Work: EMQX Enterprise Edition
|
||||
The Licensed Work is (c) 2023
|
||||
Hangzhou EMQ Technologies Co., Ltd.
|
||||
Additional Use Grant: Students and educators are granted right to copy,
|
||||
modify, and create derivative work for research
|
||||
or education.
|
||||
Change Date: 2027-02-01
|
||||
Change License: Apache License, Version 2.0
|
||||
|
||||
For information about alternative licensing arrangements for the Software,
|
||||
please contact Licensor: https://www.emqx.com/en/contact
|
||||
|
||||
Notice
|
||||
|
||||
The Business Source License (this document, or the “License”) is not an Open
|
||||
Source license. However, the Licensed Work will eventually be made available
|
||||
under an Open Source License, as stated in this License.
|
||||
|
||||
License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved.
|
||||
“Business Source License” is a trademark of MariaDB Corporation Ab.
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
|
||||
Business Source License 1.1
|
||||
|
||||
Terms
|
||||
|
||||
The Licensor hereby grants you the right to copy, modify, create derivative
|
||||
works, redistribute, and make non-production use of the Licensed Work. The
|
||||
Licensor may make an Additional Use Grant, above, permitting limited
|
||||
production use.
|
||||
|
||||
Effective on the Change Date, or the fourth anniversary of the first publicly
|
||||
available distribution of a specific version of the Licensed Work under this
|
||||
License, whichever comes first, the Licensor hereby grants you rights under
|
||||
the terms of the Change License, and the rights granted in the paragraph
|
||||
above terminate.
|
||||
|
||||
If your use of the Licensed Work does not comply with the requirements
|
||||
currently in effect as described in this License, you must purchase a
|
||||
commercial license from the Licensor, its affiliated entities, or authorized
|
||||
resellers, or you must refrain from using the Licensed Work.
|
||||
|
||||
All copies of the original and modified Licensed Work, and derivative works
|
||||
of the Licensed Work, are subject to this License. This License applies
|
||||
separately for each version of the Licensed Work and the Change Date may vary
|
||||
for each version of the Licensed Work released by Licensor.
|
||||
|
||||
You must conspicuously display this License on each original or modified copy
|
||||
of the Licensed Work. If you receive the Licensed Work in original or
|
||||
modified form from a third party, the terms and conditions set forth in this
|
||||
License apply to your use of that work.
|
||||
|
||||
Any use of the Licensed Work in violation of this License will automatically
|
||||
terminate your rights under this License for the current and all other
|
||||
versions of the Licensed Work.
|
||||
|
||||
This License does not grant you any right in any trademark or logo of
|
||||
Licensor or its affiliates (provided that you may use a trademark or logo of
|
||||
Licensor as expressly required by this License).
|
||||
|
||||
TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
|
||||
AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
|
||||
EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
|
||||
TITLE.
|
||||
|
||||
MariaDB hereby grants you permission to use this License’s text to license
|
||||
your works, and to refer to it using the trademark “Business Source License”,
|
||||
as long as you comply with the Covenants of Licensor below.
|
||||
|
||||
Covenants of Licensor
|
||||
|
||||
In consideration of the right to use this License’s text and the “Business
|
||||
Source License” name and trademark, Licensor covenants to MariaDB, and to all
|
||||
other recipients of the licensed work to be provided by Licensor:
|
||||
|
||||
1. To specify as the Change License the GPL Version 2.0 or any later version,
|
||||
or a license that is compatible with GPL Version 2.0 or a later version,
|
||||
where “compatible” means that software provided under the Change License can
|
||||
be included in a program with software provided under GPL Version 2.0 or a
|
||||
later version. Licensor may specify additional Change Licenses without
|
||||
limitation.
|
||||
|
||||
2. To either: (a) specify an additional grant of rights to use that does not
|
||||
impose any additional restriction on the right granted in this License, as
|
||||
the Additional Use Grant; or (b) insert the text “None”.
|
||||
|
||||
3. To specify a Change Date.
|
||||
|
||||
4. Not to modify this License in any other way.
|
|
@ -0,0 +1,35 @@
|
|||
# EMQX Eviction Agent
|
||||
|
||||
`emqx_eviction_agent` is a part of the node evacuation/node rebalance feature in EMQX.
|
||||
It is a low-level application that encapsulates working with actual MQTT connections.
|
||||
|
||||
## Application Responsibilities
|
||||
|
||||
`emqx_eviction_agent` application:
|
||||
|
||||
* Blocks incoming connection to the node it is running on.
|
||||
* Serves as a facade for connection/session eviction operations.
|
||||
* Reports blocking status via HTTP API.
|
||||
|
||||
The `emqx_eviction_agent` is relatively passive and has no eviction/rebalancing logic. It allows
|
||||
`emqx_node_rebalance` to perform eviction/rebalancing operations using high-level API, without having to deal with
|
||||
MQTT connections directly.
|
||||
|
||||
## EMQX Integration
|
||||
|
||||
`emqx_eviction_agent` interacts with the following EMQX components:
|
||||
* `emqx_cm` - to get the list of active MQTT connections;
|
||||
* `emqx_hooks` subsystem - to block/unblock incoming connections;
|
||||
* `emqx_channel` and the corresponding connection modules to perform the eviction.
|
||||
|
||||
## User Facing API
|
||||
|
||||
The application provided a very simple API (CLI and HTTP) to inspect the current blocking status.
|
||||
|
||||
# Documentation
|
||||
|
||||
The rebalancing concept is described in the corresponding [EIP](https://github.com/emqx/eip/blob/main/active/0020-node-rebalance.md).
|
||||
|
||||
# Contributing
|
||||
|
||||
Please see our [contributing.md](../../CONTRIBUTING.md).
|
|
@ -0,0 +1,3 @@
|
|||
##--------------------------------------------------------------------
|
||||
## EMQX Eviction Agent Plugin
|
||||
##--------------------------------------------------------------------
|
|
@ -0,0 +1,2 @@
|
|||
{deps, [{emqx, {path, "../../apps/emqx"}}]}.
|
||||
{project_plugins, [erlfmt]}.
|
|
@ -0,0 +1,21 @@
|
|||
{application, emqx_eviction_agent, [
|
||||
{description, "EMQX Eviction Agent"},
|
||||
{vsn, "5.0.0"},
|
||||
{registered, [
|
||||
emqx_eviction_agent_sup,
|
||||
emqx_eviction_agent,
|
||||
emqx_eviction_agent_conn_sup
|
||||
]},
|
||||
{applications, [
|
||||
kernel,
|
||||
stdlib,
|
||||
emqx_ctl
|
||||
]},
|
||||
{mod, {emqx_eviction_agent_app, []}},
|
||||
{env, []},
|
||||
{modules, []},
|
||||
{links, [
|
||||
{"Homepage", "https://www.emqx.com/"},
|
||||
{"Github", "https://github.com/emqx"}
|
||||
]}
|
||||
]}.
|
|
@ -0,0 +1,3 @@
|
|||
%% -*- mode: erlang -*-
|
||||
%% Unless you know what you are doing, DO NOT edit manually!!
|
||||
{VSN, [{<<".*">>, []}], [{<<".*">>, []}]}.
|
|
@ -0,0 +1,348 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_eviction_agent).
|
||||
|
||||
-include_lib("emqx/include/emqx_mqtt.hrl").
|
||||
-include_lib("emqx/include/logger.hrl").
|
||||
-include_lib("emqx/include/types.hrl").
|
||||
-include_lib("emqx/include/emqx_hooks.hrl").
|
||||
|
||||
-include_lib("stdlib/include/qlc.hrl").
|
||||
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
|
||||
|
||||
-export([
|
||||
start_link/0,
|
||||
enable/2,
|
||||
disable/1,
|
||||
status/0,
|
||||
connection_count/0,
|
||||
session_count/0,
|
||||
session_count/1,
|
||||
evict_connections/1,
|
||||
evict_sessions/2,
|
||||
evict_sessions/3,
|
||||
evict_session_channel/3
|
||||
]).
|
||||
|
||||
-behaviour(gen_server).
|
||||
|
||||
-export([
|
||||
init/1,
|
||||
handle_call/3,
|
||||
handle_info/2,
|
||||
handle_cast/2,
|
||||
code_change/3
|
||||
]).
|
||||
|
||||
-export([
|
||||
on_connect/2,
|
||||
on_connack/3
|
||||
]).
|
||||
|
||||
-export([
|
||||
hook/0,
|
||||
unhook/0
|
||||
]).
|
||||
|
||||
-export_type([server_reference/0]).
|
||||
|
||||
-define(CONN_MODULES, [
|
||||
emqx_connection, emqx_ws_connection, emqx_quic_connection, emqx_eviction_agent_channel
|
||||
]).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% APIs
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-type server_reference() :: binary() | undefined.
|
||||
-type status() :: {enabled, conn_stats()} | disabled.
|
||||
-type conn_stats() :: #{
|
||||
connections := non_neg_integer(),
|
||||
sessions := non_neg_integer()
|
||||
}.
|
||||
-type kind() :: atom().
|
||||
|
||||
-spec start_link() -> startlink_ret().
|
||||
start_link() ->
|
||||
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
|
||||
|
||||
-spec enable(kind(), server_reference()) -> ok_or_error(eviction_agent_busy).
|
||||
enable(Kind, ServerReference) ->
|
||||
gen_server:call(?MODULE, {enable, Kind, ServerReference}).
|
||||
|
||||
-spec disable(kind()) -> ok.
|
||||
disable(Kind) ->
|
||||
gen_server:call(?MODULE, {disable, Kind}).
|
||||
|
||||
-spec status() -> status().
|
||||
status() ->
|
||||
case enable_status() of
|
||||
{enabled, _Kind, _ServerReference} ->
|
||||
{enabled, stats()};
|
||||
disabled ->
|
||||
disabled
|
||||
end.
|
||||
|
||||
-spec evict_connections(pos_integer()) -> ok_or_error(disabled).
|
||||
evict_connections(N) ->
|
||||
case enable_status() of
|
||||
{enabled, _Kind, ServerReference} ->
|
||||
ok = do_evict_connections(N, ServerReference);
|
||||
disabled ->
|
||||
{error, disabled}
|
||||
end.
|
||||
|
||||
-spec evict_sessions(pos_integer(), node() | [node()]) -> ok_or_error(disabled).
|
||||
evict_sessions(N, Node) when is_atom(Node) ->
|
||||
evict_sessions(N, [Node]);
|
||||
evict_sessions(N, Nodes) when is_list(Nodes) andalso length(Nodes) > 0 ->
|
||||
evict_sessions(N, Nodes, any).
|
||||
|
||||
-spec evict_sessions(pos_integer(), node() | [node()], atom()) -> ok_or_error(disabled).
|
||||
evict_sessions(N, Node, ConnState) when is_atom(Node) ->
|
||||
evict_sessions(N, [Node], ConnState);
|
||||
evict_sessions(N, Nodes, ConnState) when
|
||||
is_list(Nodes) andalso length(Nodes) > 0
|
||||
->
|
||||
case enable_status() of
|
||||
{enabled, _Kind, _ServerReference} ->
|
||||
ok = do_evict_sessions(N, Nodes, ConnState);
|
||||
disabled ->
|
||||
{error, disabled}
|
||||
end.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% gen_server callbacks
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
init([]) ->
|
||||
_ = persistent_term:erase(?MODULE),
|
||||
{ok, #{}}.
|
||||
|
||||
%% enable
|
||||
handle_call({enable, Kind, ServerReference}, _From, St) ->
|
||||
Reply =
|
||||
case enable_status() of
|
||||
disabled ->
|
||||
ok = persistent_term:put(?MODULE, {enabled, Kind, ServerReference});
|
||||
{enabled, Kind, _ServerReference} ->
|
||||
ok = persistent_term:put(?MODULE, {enabled, Kind, ServerReference});
|
||||
{enabled, _OtherKind, _ServerReference} ->
|
||||
{error, eviction_agent_busy}
|
||||
end,
|
||||
{reply, Reply, St};
|
||||
%% disable
|
||||
handle_call({disable, Kind}, _From, St) ->
|
||||
Reply =
|
||||
case enable_status() of
|
||||
disabled ->
|
||||
{error, disabled};
|
||||
{enabled, Kind, _ServerReference} ->
|
||||
_ = persistent_term:erase(?MODULE),
|
||||
ok;
|
||||
{enabled, _OtherKind, _ServerReference} ->
|
||||
{error, eviction_agent_busy}
|
||||
end,
|
||||
{reply, Reply, St};
|
||||
handle_call(Msg, _From, St) ->
|
||||
?SLOG(warning, #{msg => "unknown_call", call => Msg, state => St}),
|
||||
{reply, {error, unknown_call}, St}.
|
||||
|
||||
handle_info(Msg, St) ->
|
||||
?SLOG(warning, #{msg => "unknown_msg", info => Msg, state => St}),
|
||||
{noreply, St}.
|
||||
|
||||
handle_cast(Msg, St) ->
|
||||
?SLOG(warning, #{msg => "unknown_cast", cast => Msg, state => St}),
|
||||
{noreply, St}.
|
||||
|
||||
code_change(_Vsn, State, _Extra) ->
|
||||
{ok, State}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Hook callbacks
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
on_connect(_ConnInfo, _Props) ->
|
||||
case enable_status() of
|
||||
{enabled, _Kind, _ServerReference} ->
|
||||
{stop, {error, ?RC_USE_ANOTHER_SERVER}};
|
||||
disabled ->
|
||||
ignore
|
||||
end.
|
||||
|
||||
on_connack(
|
||||
#{proto_name := <<"MQTT">>, proto_ver := ?MQTT_PROTO_V5},
|
||||
use_another_server,
|
||||
Props
|
||||
) ->
|
||||
case enable_status() of
|
||||
{enabled, _Kind, ServerReference} ->
|
||||
{ok, Props#{'Server-Reference' => ServerReference}};
|
||||
disabled ->
|
||||
{ok, Props}
|
||||
end;
|
||||
on_connack(_ClientInfo, _Reason, Props) ->
|
||||
{ok, Props}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Hook funcs
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
hook() ->
|
||||
?tp(debug, eviction_agent_hook, #{}),
|
||||
ok = emqx_hooks:put('client.connack', {?MODULE, on_connack, []}, ?HP_NODE_REBALANCE),
|
||||
ok = emqx_hooks:put('client.connect', {?MODULE, on_connect, []}, ?HP_NODE_REBALANCE).
|
||||
|
||||
unhook() ->
|
||||
?tp(debug, eviction_agent_unhook, #{}),
|
||||
ok = emqx_hooks:del('client.connect', {?MODULE, on_connect}),
|
||||
ok = emqx_hooks:del('client.connack', {?MODULE, on_connack}).
|
||||
|
||||
enable_status() ->
|
||||
persistent_term:get(?MODULE, disabled).
|
||||
|
||||
% connection management
|
||||
stats() ->
|
||||
#{
|
||||
connections => connection_count(),
|
||||
sessions => session_count()
|
||||
}.
|
||||
|
||||
connection_table() ->
|
||||
emqx_cm:live_connection_table(?CONN_MODULES).
|
||||
|
||||
connection_count() ->
|
||||
table_count(connection_table()).
|
||||
|
||||
channel_with_session_table(any) ->
|
||||
qlc:q([
|
||||
{ClientId, ConnInfo, ClientInfo}
|
||||
|| {ClientId, _, ConnInfo, ClientInfo} <-
|
||||
emqx_cm:channel_with_session_table(?CONN_MODULES)
|
||||
]);
|
||||
channel_with_session_table(RequiredConnState) ->
|
||||
qlc:q([
|
||||
{ClientId, ConnInfo, ClientInfo}
|
||||
|| {ClientId, ConnState, ConnInfo, ClientInfo} <-
|
||||
emqx_cm:channel_with_session_table(?CONN_MODULES),
|
||||
RequiredConnState =:= ConnState
|
||||
]).
|
||||
|
||||
session_count() ->
|
||||
session_count(any).
|
||||
|
||||
session_count(ConnState) ->
|
||||
table_count(channel_with_session_table(ConnState)).
|
||||
|
||||
table_count(QH) ->
|
||||
qlc:fold(fun(_, Acc) -> Acc + 1 end, 0, QH).
|
||||
|
||||
take_connections(N) ->
|
||||
ChanQH = qlc:q([ChanPid || {_ClientId, ChanPid} <- connection_table()]),
|
||||
ChanPidCursor = qlc:cursor(ChanQH),
|
||||
ChanPids = qlc:next_answers(ChanPidCursor, N),
|
||||
ok = qlc:delete_cursor(ChanPidCursor),
|
||||
ChanPids.
|
||||
|
||||
take_channel_with_sessions(N, ConnState) ->
|
||||
ChanPidCursor = qlc:cursor(channel_with_session_table(ConnState)),
|
||||
Channels = qlc:next_answers(ChanPidCursor, N),
|
||||
ok = qlc:delete_cursor(ChanPidCursor),
|
||||
Channels.
|
||||
|
||||
do_evict_connections(N, ServerReference) when N > 0 ->
|
||||
ChanPids = take_connections(N),
|
||||
ok = lists:foreach(
|
||||
fun(ChanPid) ->
|
||||
disconnect_channel(ChanPid, ServerReference)
|
||||
end,
|
||||
ChanPids
|
||||
).
|
||||
|
||||
do_evict_sessions(N, Nodes, ConnState) when N > 0 ->
|
||||
Channels = take_channel_with_sessions(N, ConnState),
|
||||
ok = lists:foreach(
|
||||
fun({ClientId, ConnInfo, ClientInfo}) ->
|
||||
evict_session_channel(Nodes, ClientId, ConnInfo, ClientInfo)
|
||||
end,
|
||||
Channels
|
||||
).
|
||||
|
||||
evict_session_channel(Nodes, ClientId, ConnInfo, ClientInfo) ->
|
||||
Node = select_random(Nodes),
|
||||
?SLOG(
|
||||
info,
|
||||
#{
|
||||
msg => "evict_session_channel",
|
||||
client_id => ClientId,
|
||||
node => Node,
|
||||
conn_info => ConnInfo,
|
||||
client_info => ClientInfo
|
||||
}
|
||||
),
|
||||
case emqx_eviction_agent_proto_v1:evict_session_channel(Node, ClientId, ConnInfo, ClientInfo) of
|
||||
{badrpc, Reason} ->
|
||||
?SLOG(
|
||||
error,
|
||||
#{
|
||||
msg => "evict_session_channel_rpc_error",
|
||||
client_id => ClientId,
|
||||
node => Node,
|
||||
reason => Reason
|
||||
}
|
||||
),
|
||||
{error, Reason};
|
||||
{error, Reason} = Error ->
|
||||
?SLOG(
|
||||
error,
|
||||
#{
|
||||
msg => "evict_session_channel_error",
|
||||
client_id => ClientId,
|
||||
node => Node,
|
||||
reason => Reason
|
||||
}
|
||||
),
|
||||
Error;
|
||||
Res ->
|
||||
Res
|
||||
end.
|
||||
|
||||
-spec evict_session_channel(
|
||||
emqx_types:clientid(),
|
||||
emqx_types:conninfo(),
|
||||
emqx_types:clientinfo()
|
||||
) -> supervisor:startchild_ret().
|
||||
evict_session_channel(ClientId, ConnInfo, ClientInfo) ->
|
||||
?SLOG(info, #{
|
||||
msg => "evict_session_channel",
|
||||
client_id => ClientId,
|
||||
conn_info => ConnInfo,
|
||||
client_info => ClientInfo
|
||||
}),
|
||||
Result = emqx_eviction_agent_channel:start_supervised(
|
||||
#{
|
||||
conninfo => ConnInfo,
|
||||
clientinfo => ClientInfo
|
||||
}
|
||||
),
|
||||
?SLOG(
|
||||
info,
|
||||
#{
|
||||
msg => "evict_session_channel_result",
|
||||
client_id => ClientId,
|
||||
result => Result
|
||||
}
|
||||
),
|
||||
Result.
|
||||
|
||||
disconnect_channel(ChanPid, ServerReference) ->
|
||||
ChanPid !
|
||||
{disconnect, ?RC_USE_ANOTHER_SERVER, use_another_server, #{
|
||||
'Server-Reference' => ServerReference
|
||||
}}.
|
||||
|
||||
select_random(List) when length(List) > 0 ->
|
||||
lists:nth(rand:uniform(length(List)), List).
|
|
@ -0,0 +1,85 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_eviction_agent_api).
|
||||
|
||||
-behaviour(minirest_api).
|
||||
|
||||
-include_lib("typerefl/include/types.hrl").
|
||||
-include_lib("hocon/include/hoconsc.hrl").
|
||||
-include_lib("emqx/include/logger.hrl").
|
||||
|
||||
%% Swagger specs from hocon schema
|
||||
-export([
|
||||
api_spec/0,
|
||||
paths/0,
|
||||
schema/1,
|
||||
namespace/0
|
||||
]).
|
||||
|
||||
-export([
|
||||
fields/1,
|
||||
roots/0
|
||||
]).
|
||||
|
||||
%% API callbacks
|
||||
-export([
|
||||
'/node_eviction/status'/2
|
||||
]).
|
||||
|
||||
-import(hoconsc, [mk/2, ref/1, ref/2]).
|
||||
|
||||
namespace() -> "node_eviction".
|
||||
|
||||
api_spec() ->
|
||||
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true}).
|
||||
|
||||
paths() ->
|
||||
[
|
||||
"/node_eviction/status"
|
||||
].
|
||||
|
||||
schema("/node_eviction/status") ->
|
||||
#{
|
||||
'operationId' => '/node_eviction/status',
|
||||
get => #{
|
||||
tags => [<<"node_eviction">>],
|
||||
summary => <<"Get node eviction status">>,
|
||||
description => ?DESC("node_eviction_status_get"),
|
||||
responses => #{
|
||||
200 => schema_status()
|
||||
}
|
||||
}
|
||||
}.
|
||||
|
||||
'/node_eviction/status'(_Bindings, _Params) ->
|
||||
case emqx_eviction_agent:status() of
|
||||
disabled ->
|
||||
{200, #{status => disabled}};
|
||||
{enabled, Stats} ->
|
||||
{200, #{
|
||||
status => enabled,
|
||||
stats => Stats
|
||||
}}
|
||||
end.
|
||||
|
||||
schema_status() ->
|
||||
mk(hoconsc:union([ref(status_enabled), ref(status_disabled)]), #{}).
|
||||
|
||||
roots() -> [].
|
||||
|
||||
fields(status_enabled) ->
|
||||
[
|
||||
{status, mk(enabled, #{default => enabled})},
|
||||
{stats, ref(stats)}
|
||||
];
|
||||
fields(stats) ->
|
||||
[
|
||||
{connections, mk(integer(), #{})},
|
||||
{sessions, mk(integer(), #{})}
|
||||
];
|
||||
fields(status_disabled) ->
|
||||
[
|
||||
{status, mk(disabled, #{default => disabled})}
|
||||
].
|
|
@ -0,0 +1,22 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_eviction_agent_app).
|
||||
|
||||
-behaviour(application).
|
||||
|
||||
-export([
|
||||
start/2,
|
||||
stop/1
|
||||
]).
|
||||
|
||||
start(_Type, _Args) ->
|
||||
ok = emqx_eviction_agent:hook(),
|
||||
{ok, Sup} = emqx_eviction_agent_sup:start_link(),
|
||||
ok = emqx_eviction_agent_cli:load(),
|
||||
{ok, Sup}.
|
||||
|
||||
stop(_State) ->
|
||||
ok = emqx_eviction_agent:unhook(),
|
||||
ok = emqx_eviction_agent_cli:unload().
|
|
@ -0,0 +1,358 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
%% MQTT Channel
|
||||
-module(emqx_eviction_agent_channel).
|
||||
|
||||
-include_lib("emqx/include/emqx.hrl").
|
||||
-include_lib("emqx/include/emqx_channel.hrl").
|
||||
-include_lib("emqx/include/emqx_mqtt.hrl").
|
||||
-include_lib("emqx/include/logger.hrl").
|
||||
-include_lib("emqx/include/types.hrl").
|
||||
|
||||
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
|
||||
|
||||
-export([
|
||||
start_link/1,
|
||||
start_supervised/1,
|
||||
call/2,
|
||||
call/3,
|
||||
cast/2,
|
||||
stop/1
|
||||
]).
|
||||
|
||||
-export([
|
||||
init/1,
|
||||
handle_call/3,
|
||||
handle_cast/2,
|
||||
handle_info/2,
|
||||
terminate/2,
|
||||
code_change/3
|
||||
]).
|
||||
|
||||
-type opts() :: #{
|
||||
conninfo := emqx_types:conninfo(),
|
||||
clientinfo := emqx_types:clientinfo()
|
||||
}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% API
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-spec start_supervised(opts()) -> supervisor:startchild_ret().
|
||||
start_supervised(#{clientinfo := #{clientid := ClientId}} = Opts) ->
|
||||
RandomId = integer_to_binary(erlang:unique_integer([positive])),
|
||||
ClientIdBin = bin_clientid(ClientId),
|
||||
Id = <<ClientIdBin/binary, "-", RandomId/binary>>,
|
||||
ChildSpec = #{
|
||||
id => Id,
|
||||
start => {?MODULE, start_link, [Opts]},
|
||||
restart => temporary,
|
||||
shutdown => 5000,
|
||||
type => worker,
|
||||
modules => [?MODULE]
|
||||
},
|
||||
supervisor:start_child(
|
||||
emqx_eviction_agent_conn_sup,
|
||||
ChildSpec
|
||||
).
|
||||
|
||||
-spec start_link(opts()) -> startlink_ret().
|
||||
start_link(Opts) ->
|
||||
gen_server:start_link(?MODULE, [Opts], []).
|
||||
|
||||
-spec cast(pid(), term()) -> ok.
|
||||
cast(Pid, Req) ->
|
||||
gen_server:cast(Pid, Req).
|
||||
|
||||
-spec call(pid(), term()) -> term().
|
||||
call(Pid, Req) ->
|
||||
call(Pid, Req, infinity).
|
||||
|
||||
-spec call(pid(), term(), timeout()) -> term().
|
||||
call(Pid, Req, Timeout) ->
|
||||
gen_server:call(Pid, Req, Timeout).
|
||||
|
||||
-spec stop(pid()) -> ok.
|
||||
stop(Pid) ->
|
||||
gen_server:stop(Pid).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% gen_server API
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
init([#{conninfo := OldConnInfo, clientinfo := #{clientid := ClientId} = OldClientInfo}]) ->
|
||||
process_flag(trap_exit, true),
|
||||
ClientInfo = clientinfo(OldClientInfo),
|
||||
ConnInfo = conninfo(OldConnInfo),
|
||||
case open_session(ConnInfo, ClientInfo) of
|
||||
{ok, Channel0} ->
|
||||
case set_expiry_timer(Channel0) of
|
||||
{ok, Channel1} ->
|
||||
?SLOG(
|
||||
info,
|
||||
#{
|
||||
msg => "channel_initialized",
|
||||
clientid => ClientId,
|
||||
node => node()
|
||||
}
|
||||
),
|
||||
ok = emqx_cm:mark_channel_disconnected(self()),
|
||||
{ok, Channel1, hibernate};
|
||||
{error, Reason} ->
|
||||
{stop, Reason}
|
||||
end;
|
||||
{error, Reason} ->
|
||||
{stop, Reason}
|
||||
end.
|
||||
|
||||
handle_call(kick, _From, Channel) ->
|
||||
{stop, kicked, ok, Channel};
|
||||
handle_call(discard, _From, Channel) ->
|
||||
{stop, discarded, ok, Channel};
|
||||
handle_call({takeover, 'begin'}, _From, #{session := Session} = Channel) ->
|
||||
{reply, Session, Channel#{takeover => true}};
|
||||
handle_call(
|
||||
{takeover, 'end'},
|
||||
_From,
|
||||
#{
|
||||
session := Session,
|
||||
clientinfo := #{clientid := ClientId},
|
||||
pendings := Pendings
|
||||
} = Channel
|
||||
) ->
|
||||
ok = emqx_session:takeover(Session),
|
||||
%% TODO: Should not drain deliver here (side effect)
|
||||
Delivers = emqx_utils:drain_deliver(),
|
||||
AllPendings = lists:append(Delivers, Pendings),
|
||||
?tp(
|
||||
debug,
|
||||
emqx_channel_takeover_end,
|
||||
#{clientid => ClientId}
|
||||
),
|
||||
{stop, normal, AllPendings, Channel};
|
||||
handle_call(list_acl_cache, _From, Channel) ->
|
||||
{reply, [], Channel};
|
||||
handle_call({quota, _Policy}, _From, Channel) ->
|
||||
{reply, ok, Channel};
|
||||
handle_call(Req, _From, Channel) ->
|
||||
?SLOG(
|
||||
error,
|
||||
#{
|
||||
msg => "unexpected_call",
|
||||
req => Req
|
||||
}
|
||||
),
|
||||
{reply, ignored, Channel}.
|
||||
|
||||
handle_info(Deliver = {deliver, _Topic, _Msg}, Channel) ->
|
||||
Delivers = [Deliver | emqx_utils:drain_deliver()],
|
||||
{noreply, handle_deliver(Delivers, Channel)};
|
||||
handle_info(expire_session, Channel) ->
|
||||
{stop, expired, Channel};
|
||||
handle_info(Info, Channel) ->
|
||||
?SLOG(
|
||||
error,
|
||||
#{
|
||||
msg => "unexpected_info",
|
||||
info => Info
|
||||
}
|
||||
),
|
||||
{noreply, Channel}.
|
||||
|
||||
handle_cast(Msg, Channel) ->
|
||||
?SLOG(error, #{msg => "unexpected_cast", cast => Msg}),
|
||||
{noreply, Channel}.
|
||||
|
||||
terminate(Reason, #{conninfo := ConnInfo, clientinfo := ClientInfo, session := Session} = Channel) ->
|
||||
ok = cancel_expiry_timer(Channel),
|
||||
(Reason =:= expired) andalso emqx_persistent_session:persist(ClientInfo, ConnInfo, Session),
|
||||
emqx_session:terminate(ClientInfo, Reason, Session).
|
||||
|
||||
code_change(_OldVsn, Channel, _Extra) ->
|
||||
{ok, Channel}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Internal functions
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
handle_deliver(
|
||||
Delivers,
|
||||
#{
|
||||
takeover := true,
|
||||
pendings := Pendings,
|
||||
session := Session,
|
||||
clientinfo := #{clientid := ClientId} = ClientInfo
|
||||
} = Channel
|
||||
) ->
|
||||
%% NOTE: Order is important here. While the takeover is in
|
||||
%% progress, the session cannot enqueue messages, since it already
|
||||
%% passed on the queue to the new connection in the session state.
|
||||
NPendings = lists:append(
|
||||
Pendings,
|
||||
emqx_session:ignore_local(ClientInfo, emqx_channel:maybe_nack(Delivers), ClientId, Session)
|
||||
),
|
||||
Channel#{pendings => NPendings};
|
||||
handle_deliver(
|
||||
Delivers,
|
||||
#{
|
||||
takeover := false,
|
||||
session := Session,
|
||||
clientinfo := #{clientid := ClientId} = ClientInfo
|
||||
} = Channel
|
||||
) ->
|
||||
Delivers1 = emqx_channel:maybe_nack(Delivers),
|
||||
Delivers2 = emqx_session:ignore_local(ClientInfo, Delivers1, ClientId, Session),
|
||||
NSession = emqx_session:enqueue(ClientInfo, Delivers2, Session),
|
||||
NChannel = persist(NSession, Channel),
|
||||
%% We consider queued/dropped messages as delivered since they are now in the session state.
|
||||
emqx_channel:maybe_mark_as_delivered(Session, Delivers),
|
||||
NChannel.
|
||||
|
||||
cancel_expiry_timer(#{expiry_timer := TRef}) when is_reference(TRef) ->
|
||||
_ = erlang:cancel_timer(TRef),
|
||||
ok;
|
||||
cancel_expiry_timer(_) ->
|
||||
ok.
|
||||
|
||||
set_expiry_timer(#{conninfo := ConnInfo} = Channel) ->
|
||||
case maps:get(expiry_interval, ConnInfo) of
|
||||
?UINT_MAX ->
|
||||
{ok, Channel};
|
||||
I when I > 0 ->
|
||||
Timer = erlang:send_after(timer:seconds(I), self(), expire_session),
|
||||
{ok, Channel#{expiry_timer => Timer}};
|
||||
_ ->
|
||||
{error, should_be_expired}
|
||||
end.
|
||||
|
||||
open_session(ConnInfo, #{clientid := ClientId} = ClientInfo) ->
|
||||
Channel = channel(ConnInfo, ClientInfo),
|
||||
case emqx_cm:open_session(_CleanSession = false, ClientInfo, ConnInfo) of
|
||||
{ok, #{present := false}} ->
|
||||
?SLOG(
|
||||
info,
|
||||
#{
|
||||
msg => "no_session",
|
||||
clientid => ClientId,
|
||||
node => node()
|
||||
}
|
||||
),
|
||||
{error, no_session};
|
||||
{ok, #{session := Session, present := true, pendings := Pendings0}} ->
|
||||
?SLOG(
|
||||
info,
|
||||
#{
|
||||
msg => "session_opened",
|
||||
clientid => ClientId,
|
||||
node => node()
|
||||
}
|
||||
),
|
||||
Pendings1 = lists:usort(lists:append(Pendings0, emqx_utils:drain_deliver())),
|
||||
NSession = emqx_session:enqueue(
|
||||
ClientInfo,
|
||||
emqx_session:ignore_local(
|
||||
ClientInfo,
|
||||
emqx_channel:maybe_nack(Pendings1),
|
||||
ClientId,
|
||||
Session
|
||||
),
|
||||
Session
|
||||
),
|
||||
NChannel = Channel#{session => NSession},
|
||||
ok = emqx_cm:insert_channel_info(ClientId, info(NChannel), stats(NChannel)),
|
||||
?SLOG(
|
||||
info,
|
||||
#{
|
||||
msg => "channel_info_updated",
|
||||
clientid => ClientId,
|
||||
node => node()
|
||||
}
|
||||
),
|
||||
{ok, NChannel};
|
||||
{error, Reason} = Error ->
|
||||
?SLOG(
|
||||
error,
|
||||
#{
|
||||
msg => "session_open_failed",
|
||||
clientid => ClientId,
|
||||
node => node(),
|
||||
reason => Reason
|
||||
}
|
||||
),
|
||||
Error
|
||||
end.
|
||||
|
||||
conninfo(OldConnInfo) ->
|
||||
DisconnectedAt = maps:get(disconnected_at, OldConnInfo, erlang:system_time(millisecond)),
|
||||
ConnInfo0 = maps:with(
|
||||
[
|
||||
socktype,
|
||||
sockname,
|
||||
peername,
|
||||
peercert,
|
||||
clientid,
|
||||
clean_start,
|
||||
receive_maximum,
|
||||
expiry_interval,
|
||||
connected_at,
|
||||
disconnected_at,
|
||||
keepalive
|
||||
],
|
||||
OldConnInfo
|
||||
),
|
||||
ConnInfo0#{
|
||||
conn_mod => ?MODULE,
|
||||
connected => false,
|
||||
disconnected_at => DisconnectedAt
|
||||
}.
|
||||
|
||||
clientinfo(OldClientInfo) ->
|
||||
maps:with(
|
||||
[
|
||||
zone,
|
||||
protocol,
|
||||
peerhost,
|
||||
sockport,
|
||||
clientid,
|
||||
username,
|
||||
is_bridge,
|
||||
is_superuser,
|
||||
mountpoint
|
||||
],
|
||||
OldClientInfo
|
||||
).
|
||||
|
||||
channel(ConnInfo, ClientInfo) ->
|
||||
#{
|
||||
conninfo => ConnInfo,
|
||||
clientinfo => ClientInfo,
|
||||
expiry_timer => undefined,
|
||||
takeover => false,
|
||||
resuming => false,
|
||||
pendings => []
|
||||
}.
|
||||
|
||||
persist(Session, #{clientinfo := ClientInfo, conninfo := ConnInfo} = Channel) ->
|
||||
Session1 = emqx_persistent_session:persist(ClientInfo, ConnInfo, Session),
|
||||
Channel#{session => Session1}.
|
||||
|
||||
info(Channel) ->
|
||||
#{
|
||||
conninfo => maps:get(conninfo, Channel, undefined),
|
||||
clientinfo => maps:get(clientinfo, Channel, undefined),
|
||||
session => emqx_utils:maybe_apply(
|
||||
fun emqx_session:info/1,
|
||||
maps:get(session, Channel, undefined)
|
||||
),
|
||||
conn_state => disconnected
|
||||
}.
|
||||
|
||||
stats(#{session := Session}) ->
|
||||
lists:append(emqx_session:stats(Session), emqx_pd:get_counters(?CHANNEL_METRICS)).
|
||||
|
||||
bin_clientid(ClientId) when is_binary(ClientId) ->
|
||||
ClientId;
|
||||
bin_clientid(ClientId) when is_atom(ClientId) ->
|
||||
atom_to_binary(ClientId).
|
|
@ -0,0 +1,30 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_eviction_agent_cli).
|
||||
|
||||
%% APIs
|
||||
-export([
|
||||
load/0,
|
||||
unload/0,
|
||||
cli/1
|
||||
]).
|
||||
|
||||
load() ->
|
||||
emqx_ctl:register_command(eviction, {?MODULE, cli}, []).
|
||||
|
||||
unload() ->
|
||||
emqx_ctl:unregister_command(eviction).
|
||||
|
||||
cli(["status"]) ->
|
||||
case emqx_eviction_agent:status() of
|
||||
disabled ->
|
||||
emqx_ctl:print("Eviction status: disabled~n");
|
||||
{enabled, _Stats} ->
|
||||
emqx_ctl:print("Eviction status: enabled~n")
|
||||
end;
|
||||
cli(_) ->
|
||||
emqx_ctl:usage(
|
||||
[{"eviction status", "Get current node eviction status"}]
|
||||
).
|
|
@ -0,0 +1,21 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_eviction_agent_conn_sup).
|
||||
|
||||
-behaviour(supervisor).
|
||||
|
||||
-export([start_link/0]).
|
||||
|
||||
-export([init/1]).
|
||||
|
||||
start_link() ->
|
||||
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||
|
||||
init([]) ->
|
||||
{ok,
|
||||
{
|
||||
#{strategy => one_for_one, intensity => 10, period => 3600},
|
||||
[]
|
||||
}}.
|
|
@ -0,0 +1,34 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_eviction_agent_sup).
|
||||
|
||||
-behaviour(supervisor).
|
||||
|
||||
-export([start_link/0]).
|
||||
|
||||
-export([init/1]).
|
||||
|
||||
start_link() ->
|
||||
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||
|
||||
init([]) ->
|
||||
Childs = [
|
||||
child_spec(worker, emqx_eviction_agent, []),
|
||||
child_spec(supervisor, emqx_eviction_agent_conn_sup, [])
|
||||
],
|
||||
{ok, {
|
||||
#{strategy => one_for_one, intensity => 10, period => 3600},
|
||||
Childs
|
||||
}}.
|
||||
|
||||
child_spec(Type, Mod, Args) ->
|
||||
#{
|
||||
id => Mod,
|
||||
start => {Mod, start_link, Args},
|
||||
restart => permanent,
|
||||
shutdown => 5000,
|
||||
type => Type,
|
||||
modules => [Mod]
|
||||
}.
|
|
@ -0,0 +1,27 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_eviction_agent_proto_v1).
|
||||
|
||||
-behaviour(emqx_bpapi).
|
||||
|
||||
-export([
|
||||
introduced_in/0,
|
||||
|
||||
evict_session_channel/4
|
||||
]).
|
||||
|
||||
-include_lib("emqx/include/bpapi.hrl").
|
||||
|
||||
introduced_in() ->
|
||||
"5.0.22".
|
||||
|
||||
-spec evict_session_channel(
|
||||
node(),
|
||||
emqx_types:clientid(),
|
||||
emqx_types:conninfo(),
|
||||
emqx_types:clientinfo()
|
||||
) -> supervisor:startchild_err() | emqx_rpc:badrpc().
|
||||
evict_session_channel(Node, ClientId, ConnInfo, ClientInfo) ->
|
||||
rpc:call(Node, emqx_eviction_agent, evict_session_channel, [ClientId, ConnInfo, ClientInfo]).
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue