瀏覽代碼

Merge pull request #10681 from zhongwencool/sync-release-50-to-master

chore: sync release 50 to master
zhongwencool 2 年之前
父節點
當前提交
ab6afdb0d2
共有 100 個文件被更改,包括 3219 次插入213 次删除
  1. 17 0
      .ci/docker-compose-file/docker-compose-rabbitmq.yaml
  2. 2 2
      .ci/docker-compose-file/docker-compose-rocketmq.yaml
  3. 2 1
      Makefile
  4. 31 0
      apps/emqx/include/asserts.hrl
  5. 42 0
      apps/emqx/include/emqx_channel.hrl
  6. 1 0
      apps/emqx/include/emqx_hooks.hrl
  7. 2 2
      apps/emqx/include/emqx_release.hrl
  8. 5 0
      apps/emqx/priv/bpapi.versions
  9. 22 32
      apps/emqx/src/emqx_channel.erl
  10. 46 3
      apps/emqx/src/emqx_cm.erl
  11. 4 7
      apps/emqx/src/emqx_limiter/src/emqx_limiter_manager.erl
  12. 111 10
      apps/emqx/src/emqx_limiter/src/emqx_limiter_schema.erl
  13. 3 6
      apps/emqx/src/emqx_limiter/src/emqx_limiter_server.erl
  14. 1 1
      apps/emqx/src/emqx_limiter/src/emqx_limiter_server_sup.erl
  15. 29 22
      apps/emqx/src/emqx_listeners.erl
  16. 9 3
      apps/emqx/src/emqx_router_helper.erl
  17. 19 17
      apps/emqx/src/emqx_schema.erl
  18. 3 1
      apps/emqx/test/emqx_bpapi_static_checks.erl
  19. 3 12
      apps/emqx/test/emqx_ocsp_cache_SUITE.erl
  20. 84 12
      apps/emqx/test/emqx_ratelimiter_SUITE.erl
  21. 1 1
      apps/emqx_authn/src/emqx_authn.app.src
  22. 12 0
      apps/emqx_authn/src/emqx_authn_api.erl
  23. 6 4
      apps/emqx_authn/src/emqx_authn_app.erl
  24. 1 0
      apps/emqx_authn/src/emqx_authn_user_import_api.erl
  25. 3 2
      apps/emqx_bridge/src/emqx_bridge_api.erl
  26. 12 4
      apps/emqx_bridge/src/emqx_bridge_resource.erl
  27. 2 0
      apps/emqx_bridge_dynamo/docker-ct
  28. 0 0
      apps/emqx_bridge_dynamo/priv/dynamo/mqtt_acked.json
  29. 0 0
      apps/emqx_bridge_dynamo/priv/dynamo/mqtt_client.json
  30. 0 0
      apps/emqx_bridge_dynamo/priv/dynamo/mqtt_clientid_msg_map.json
  31. 0 0
      apps/emqx_bridge_dynamo/priv/dynamo/mqtt_msg.json
  32. 0 0
      apps/emqx_bridge_dynamo/priv/dynamo/mqtt_retain.json
  33. 0 0
      apps/emqx_bridge_dynamo/priv/dynamo/mqtt_sub.json
  34. 0 0
      apps/emqx_bridge_dynamo/priv/dynamo/mqtt_topic_msg_map.json
  35. 11 0
      apps/emqx_bridge_dynamo/rebar.config
  36. 2 2
      apps/emqx_bridge_dynamo/src/emqx_bridge_dynamo.app.src
  37. 2 2
      lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_dynamo.erl
  38. 4 4
      lib-ee/emqx_ee_connector/src/emqx_ee_connector_dynamo.erl
  39. 2 1
      lib-ee/emqx_ee_connector/src/emqx_ee_connector_dynamo_client.erl
  40. 11 3
      lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_dynamo_SUITE.erl
  41. 2 0
      apps/emqx_bridge_influxdb/docker-ct
  42. 8 0
      apps/emqx_bridge_influxdb/rebar.config
  43. 2 2
      apps/emqx_bridge_influxdb/src/emqx_bridge_influxdb.app.src
  44. 4 4
      lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_influxdb.erl
  45. 3 2
      lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl
  46. 4 4
      lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_influxdb_SUITE.erl
  47. 6 6
      lib-ee/emqx_ee_connector/test/emqx_ee_connector_influxdb_SUITE.erl
  48. 22 10
      lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_influxdb_tests.erl
  49. 1 0
      apps/emqx_bridge_iotdb/src/emqx_bridge_iotdb.erl
  50. 2 2
      apps/emqx_bridge_kafka/test/emqx_bridge_kafka_impl_producer_SUITE.erl
  51. 7 0
      apps/emqx_bridge_matrix/rebar.config
  52. 1 1
      apps/emqx_bridge_matrix/src/emqx_bridge_matrix.app.src
  53. 4 4
      lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_matrix.erl
  54. 2 0
      apps/emqx_bridge_pgsql/docker-ct
  55. 7 0
      apps/emqx_bridge_pgsql/rebar.config
  56. 1 1
      apps/emqx_bridge_pgsql/src/emqx_bridge_pgsql.app.src
  57. 2 2
      lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_pgsql.erl
  58. 1 1
      lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_pgsql_SUITE.erl
  59. 94 0
      apps/emqx_bridge_rabbitmq/BSL.txt
  60. 46 0
      apps/emqx_bridge_rabbitmq/README.md
  61. 1 0
      apps/emqx_bridge_rabbitmq/docker-ct
  62. 33 0
      apps/emqx_bridge_rabbitmq/rebar.config
  63. 9 0
      apps/emqx_bridge_rabbitmq/src/emqx_bridge_rabbitmq.app.src
  64. 124 0
      apps/emqx_bridge_rabbitmq/src/emqx_bridge_rabbitmq.erl
  65. 533 0
      apps/emqx_bridge_rabbitmq/src/emqx_bridge_rabbitmq_connector.erl
  66. 371 0
      apps/emqx_bridge_rabbitmq/test/emqx_bridge_rabbitmq_SUITE.erl
  67. 232 0
      apps/emqx_bridge_rabbitmq/test/emqx_bridge_rabbitmq_connector_SUITE.erl
  68. 2 0
      apps/emqx_bridge_rocketmq/docker-ct
  69. 8 0
      apps/emqx_bridge_rocketmq/rebar.config
  70. 2 2
      apps/emqx_bridge_rocketmq/src/emqx_bridge_rocketmq.app.src
  71. 3 3
      lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_rocketmq.erl
  72. 2 2
      lib-ee/emqx_ee_connector/src/emqx_ee_connector_rocketmq.erl
  73. 1 1
      lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_rocketmq_SUITE.erl
  74. 2 0
      apps/emqx_bridge_tdengine/docker-ct
  75. 8 0
      apps/emqx_bridge_tdengine/rebar.config
  76. 2 2
      apps/emqx_bridge_tdengine/src/emqx_bridge_tdengine.app.src
  77. 3 2
      lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_tdengine.erl
  78. 1 1
      lib-ee/emqx_ee_connector/src/emqx_ee_connector_tdengine.erl
  79. 1 1
      lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_tdengine_SUITE.erl
  80. 7 0
      apps/emqx_bridge_timescale/rebar.config
  81. 1 1
      apps/emqx_bridge_timescale/src/emqx_bridge_timescale.app.src
  82. 4 4
      lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_timescale.erl
  83. 81 0
      apps/emqx_conf/test/emqx_conf_schema_tests.erl
  84. 2 0
      apps/emqx_dashboard/src/emqx_dashboard_swagger.erl
  85. 5 1
      apps/emqx_dashboard/test/emqx_dashboard_api_test_helpers.erl
  86. 1 0
      apps/emqx_dashboard/test/emqx_dashboard_listener_SUITE.erl
  87. 94 0
      apps/emqx_eviction_agent/BSL.txt
  88. 35 0
      apps/emqx_eviction_agent/README.md
  89. 3 0
      apps/emqx_eviction_agent/etc/emqx_eviction_agent.conf
  90. 2 0
      apps/emqx_eviction_agent/rebar.config
  91. 21 0
      apps/emqx_eviction_agent/src/emqx_eviction_agent.app.src
  92. 3 0
      apps/emqx_eviction_agent/src/emqx_eviction_agent.appup.src
  93. 348 0
      apps/emqx_eviction_agent/src/emqx_eviction_agent.erl
  94. 85 0
      apps/emqx_eviction_agent/src/emqx_eviction_agent_api.erl
  95. 22 0
      apps/emqx_eviction_agent/src/emqx_eviction_agent_app.erl
  96. 358 0
      apps/emqx_eviction_agent/src/emqx_eviction_agent_channel.erl
  97. 30 0
      apps/emqx_eviction_agent/src/emqx_eviction_agent_cli.erl
  98. 21 0
      apps/emqx_eviction_agent/src/emqx_eviction_agent_conn_sup.erl
  99. 34 0
      apps/emqx_eviction_agent/src/emqx_eviction_agent_sup.erl
  100. 0 0
      apps/emqx_eviction_agent/src/proto/emqx_eviction_agent_proto_v1.erl

+ 17 - 0
.ci/docker-compose-file/docker-compose-rabbitmq.yaml

@@ -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

+ 2 - 2
.ci/docker-compose-file/docker-compose-rocketmq.yaml

@@ -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

+ 2 - 1
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-%)

+ 31 - 0
apps/emqx/include/asserts.hrl

@@ -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
+        )
+    )
+).

+ 42 - 0
apps/emqx/include/emqx_channel.hrl

@@ -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
+]).

+ 1 - 0
apps/emqx/include/emqx_hooks.hrl

@@ -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.

+ 2 - 2
apps/emqx/include/emqx_release.hrl

@@ -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").

+ 5 - 0
apps/emqx/priv/bpapi.versions

@@ -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}.

+ 22 - 32
apps/emqx/src/emqx_channel.erl

@@ -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}.

+ 46 - 3
apps/emqx/src/emqx_cm.erl

@@ -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.
 

+ 4 - 7
apps/emqx/src/emqx_limiter/src/emqx_limiter_manager.erl

@@ -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);

+ 111 - 10
apps/emqx/src/emqx_limiter/src/emqx_limiter_schema.erl

@@ -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,15 +109,17 @@ roots() ->
     ].
 
 fields(limiter) ->
-    [
-        {Type,
-            ?HOCON(?R_REF(node_opts), #{
-                desc => ?DESC(Type),
-                importance => ?IMPORTANCE_HIDDEN,
-                aliases => alias_of_type(Type)
-            })}
-     || Type <- types()
-    ] ++
+    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()
+        ] ++
         [
             %% This is an undocumented feature, and it won't be support anymore
             {client,
@@ -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).

+ 3 - 6
apps/emqx/src/emqx_limiter/src/emqx_limiter_server.erl

@@ -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}

+ 1 - 1
apps/emqx/src/emqx_limiter/src/emqx_limiter_server_sup.erl

@@ -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) ->

+ 29 - 22
apps/emqx/src/emqx_listeners.erl

@@ -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}) ->
-    lists:foreach(
-        fun(Type) ->
-            emqx_limiter_server:del_bucket(Id, Type)
-        end,
-        maps:keys(Limiters)
-    );
-del_limiter_bucket(_Id, _Cfg) ->
-    ok.
+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(Limiter)
+            )
+    end.
 
 enable_authn(Opts) ->
     maps:get(enable_authn, Opts, true).

+ 9 - 3
apps/emqx/src/emqx_router_helper.erl

@@ -167,9 +167,15 @@ handle_info(Info, State) ->
     {noreply, State}.
 
 terminate(_Reason, _State) ->
-    ok = ekka:unmonitor(membership),
-    emqx_stats:cancel_update(route_stats),
-    mnesia:unsubscribe({table, ?ROUTING_NODE, simple}).
+    try
+        ok = ekka:unmonitor(membership),
+        emqx_stats:cancel_update(route_stats),
+        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}.

+ 19 - 17
apps/emqx/src/emqx_schema.erl

@@ -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])).

+ 3 - 1
apps/emqx/test/emqx_bpapi_static_checks.erl

@@ -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").

+ 3 - 12
apps/emqx/test/emqx_ocsp_cache_SUITE.erl

@@ -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) ->

+ 84 - 12
apps/emqx/test/emqx_ratelimiter_SUITE.erl

@@ -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 - 1
apps/emqx_authn/src/emqx_authn.app.src

@@ -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]},

+ 12 - 0
apps/emqx_authn/src/emqx_authn_api.erl

@@ -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 => #{

+ 6 - 4
apps/emqx_authn/src/emqx_authn_app.erl

@@ -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()).

+ 1 - 0
apps/emqx_authn/src/emqx_authn_user_import_api.erl

@@ -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),

+ 3 - 2
apps/emqx_bridge/src/emqx_bridge_api.erl

@@ -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

+ 12 - 4
apps/emqx_bridge/src/emqx_bridge_resource.erl

@@ -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;

+ 2 - 0
apps/emqx_bridge_dynamo/docker-ct

@@ -0,0 +1,2 @@
+toxiproxy
+dynamo

lib-ee/emqx_ee_bridge/priv/dynamo/mqtt_acked.json → apps/emqx_bridge_dynamo/priv/dynamo/mqtt_acked.json


lib-ee/emqx_ee_bridge/priv/dynamo/mqtt_client.json → apps/emqx_bridge_dynamo/priv/dynamo/mqtt_client.json


lib-ee/emqx_ee_bridge/priv/dynamo/mqtt_clientid_msg_map.json → apps/emqx_bridge_dynamo/priv/dynamo/mqtt_clientid_msg_map.json


lib-ee/emqx_ee_bridge/priv/dynamo/mqtt_msg.json → apps/emqx_bridge_dynamo/priv/dynamo/mqtt_msg.json


lib-ee/emqx_ee_bridge/priv/dynamo/mqtt_retain.json → apps/emqx_bridge_dynamo/priv/dynamo/mqtt_retain.json


lib-ee/emqx_ee_bridge/priv/dynamo/mqtt_sub.json → apps/emqx_bridge_dynamo/priv/dynamo/mqtt_sub.json


lib-ee/emqx_ee_bridge/priv/dynamo/mqtt_topic_msg_map.json → apps/emqx_bridge_dynamo/priv/dynamo/mqtt_topic_msg_map.json


+ 11 - 0
apps/emqx_bridge_dynamo/rebar.config

@@ -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]}
+]}.

+ 2 - 2
apps/emqx_bridge_dynamo/src/emqx_bridge_dynamo.app.src

@@ -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, []}

+ 2 - 2
lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_dynamo.erl

@@ -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");

+ 4 - 4
lib-ee/emqx_ee_connector/src/emqx_ee_connector_dynamo.erl

@@ -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) ->

+ 2 - 1
lib-ee/emqx_ee_connector/src/emqx_ee_connector_dynamo_client.erl

@@ -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).
 

+ 11 - 3
lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_dynamo_SUITE.erl

@@ -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

+ 2 - 0
apps/emqx_bridge_influxdb/docker-ct

@@ -0,0 +1,2 @@
+toxiproxy
+influxdb

+ 8 - 0
apps/emqx_bridge_influxdb/rebar.config

@@ -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"}}
+]}.

+ 2 - 2
apps/emqx_bridge_influxdb/src/emqx_bridge_influxdb.app.src

@@ -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, []}

+ 4 - 4
lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_influxdb.erl

@@ -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.
 

+ 3 - 2
lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl

@@ -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

+ 4 - 4
lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_influxdb_SUITE.erl

@@ -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,

+ 6 - 6
lib-ee/emqx_ee_connector/test/emqx_ee_connector_influxdb_SUITE.erl

@@ -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">>),

+ 22 - 10
lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_influxdb_tests.erl

@@ -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.

+ 1 - 0
apps/emqx_bridge_iotdb/src/emqx_bridge_iotdb.erl

@@ -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
             })}

+ 2 - 2
apps/emqx_bridge_kafka/test/emqx_bridge_kafka_impl_producer_SUITE.erl

@@ -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),

+ 7 - 0
apps/emqx_bridge_matrix/rebar.config

@@ -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 - 1
apps/emqx_bridge_matrix/src/emqx_bridge_matrix.app.src

@@ -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, []},

+ 4 - 4
lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_matrix.erl

@@ -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.

+ 2 - 0
apps/emqx_bridge_pgsql/docker-ct

@@ -0,0 +1,2 @@
+toxiproxy
+pgsql

+ 7 - 0
apps/emqx_bridge_pgsql/rebar.config

@@ -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 - 1
apps/emqx_bridge_pgsql/src/emqx_bridge_pgsql.app.src

@@ -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, []},

+ 2 - 2
lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_pgsql.erl

@@ -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").

+ 1 - 1
lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_pgsql_SUITE.erl

@@ -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).

+ 94 - 0
apps/emqx_bridge_rabbitmq/BSL.txt

@@ -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.

+ 46 - 0
apps/emqx_bridge_rabbitmq/README.md

@@ -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).
+

+ 1 - 0
apps/emqx_bridge_rabbitmq/docker-ct

@@ -0,0 +1 @@
+rabbitmq

+ 33 - 0
apps/emqx_bridge_rabbitmq/rebar.config

@@ -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]}
+]}.

+ 9 - 0
apps/emqx_bridge_rabbitmq/src/emqx_bridge_rabbitmq.app.src

@@ -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, []}
+]}.

+ 124 - 0
apps/emqx_bridge_rabbitmq/src/emqx_bridge_rabbitmq.erl

@@ -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")})}.

+ 533 - 0
apps/emqx_bridge_rabbitmq/src/emqx_bridge_rabbitmq_connector.erl

@@ -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).

+ 371 - 0
apps/emqx_bridge_rabbitmq/test/emqx_bridge_rabbitmq_SUITE.erl

@@ -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">>}.

+ 232 - 0
apps/emqx_bridge_rabbitmq/test/emqx_bridge_rabbitmq_connector_SUITE.erl

@@ -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">>}.

+ 2 - 0
apps/emqx_bridge_rocketmq/docker-ct

@@ -0,0 +1,2 @@
+toxiproxy
+rocketmq

+ 8 - 0
apps/emqx_bridge_rocketmq/rebar.config

@@ -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"}}
+]}.

+ 2 - 2
apps/emqx_bridge_rocketmq/src/emqx_bridge_rocketmq.app.src

@@ -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, []}

+ 3 - 3
lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_rocketmq.erl

@@ -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")];

+ 2 - 2
lib-ee/emqx_ee_connector/src/emqx_ee_connector_rocketmq.erl

@@ -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).
 

+ 1 - 1
lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_rocketmq_SUITE.erl

@@ -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).

+ 2 - 0
apps/emqx_bridge_tdengine/docker-ct

@@ -0,0 +1,2 @@
+toxiproxy
+tdengine

+ 8 - 0
apps/emqx_bridge_tdengine/rebar.config

@@ -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"}}
+]}.

+ 2 - 2
apps/emqx_bridge_tdengine/src/emqx_bridge_tdengine.app.src

@@ -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, []}

+ 3 - 2
lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_tdengine.erl

@@ -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") ->

+ 1 - 1
lib-ee/emqx_ee_connector/src/emqx_ee_connector_tdengine.erl

@@ -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).
 

+ 1 - 1
lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_tdengine_SUITE.erl

@@ -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).

+ 7 - 0
apps/emqx_bridge_timescale/rebar.config

@@ -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 - 1
apps/emqx_bridge_timescale/src/emqx_bridge_timescale.app.src

@@ -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, []},

+ 4 - 4
lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_timescale.erl

@@ -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.

+ 81 - 0
apps/emqx_conf/test/emqx_conf_schema_tests.erl

@@ -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.

+ 2 - 0
apps/emqx_dashboard/src/emqx_dashboard_swagger.erl

@@ -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),

+ 5 - 1
apps/emqx_dashboard/test/emqx_dashboard_api_test_helpers.erl

@@ -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,

+ 1 - 0
apps/emqx_dashboard/test/emqx_dashboard_listener_SUITE.erl

@@ -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.

+ 94 - 0
apps/emqx_eviction_agent/BSL.txt

@@ -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.

+ 35 - 0
apps/emqx_eviction_agent/README.md

@@ -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).

+ 3 - 0
apps/emqx_eviction_agent/etc/emqx_eviction_agent.conf

@@ -0,0 +1,3 @@
+##--------------------------------------------------------------------
+## EMQX Eviction Agent Plugin
+##--------------------------------------------------------------------

+ 2 - 0
apps/emqx_eviction_agent/rebar.config

@@ -0,0 +1,2 @@
+{deps, [{emqx, {path, "../../apps/emqx"}}]}.
+{project_plugins, [erlfmt]}.

+ 21 - 0
apps/emqx_eviction_agent/src/emqx_eviction_agent.app.src

@@ -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"}
+    ]}
+]}.

+ 3 - 0
apps/emqx_eviction_agent/src/emqx_eviction_agent.appup.src

@@ -0,0 +1,3 @@
+%% -*- mode: erlang -*-
+%% Unless you know what you are doing, DO NOT edit manually!!
+{VSN, [{<<".*">>, []}], [{<<".*">>, []}]}.

+ 348 - 0
apps/emqx_eviction_agent/src/emqx_eviction_agent.erl

@@ -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).

+ 85 - 0
apps/emqx_eviction_agent/src/emqx_eviction_agent_api.erl

@@ -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})}
+    ].

+ 22 - 0
apps/emqx_eviction_agent/src/emqx_eviction_agent_app.erl

@@ -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().

+ 358 - 0
apps/emqx_eviction_agent/src/emqx_eviction_agent_channel.erl

@@ -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).

+ 30 - 0
apps/emqx_eviction_agent/src/emqx_eviction_agent_cli.erl

@@ -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"}]
+    ).

+ 21 - 0
apps/emqx_eviction_agent/src/emqx_eviction_agent_conn_sup.erl

@@ -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},
+            []
+        }}.

+ 34 - 0
apps/emqx_eviction_agent/src/emqx_eviction_agent_sup.erl

@@ -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
apps/emqx_eviction_agent/src/proto/emqx_eviction_agent_proto_v1.erl


Some files were not shown because too many files changed in this diff