From 08e1566490c8e8c1bfa7d7753a55df3339f0031f Mon Sep 17 00:00:00 2001 From: Mayer Maximilian Date: Mon, 24 Oct 2022 09:51:49 +0200 Subject: [PATCH 0001/1802] feat: Add pod disruption budget to helm chart Now it is possible to define the pod disruption budget. Closes: #8222 --- deploy/charts/emqx/templates/pdb.yaml | 18 ++++++++++++++++++ deploy/charts/emqx/values.yaml | 7 +++++++ 2 files changed, 25 insertions(+) create mode 100644 deploy/charts/emqx/templates/pdb.yaml diff --git a/deploy/charts/emqx/templates/pdb.yaml b/deploy/charts/emqx/templates/pdb.yaml new file mode 100644 index 000000000..a3f233064 --- /dev/null +++ b/deploy/charts/emqx/templates/pdb.yaml @@ -0,0 +1,18 @@ +{{- if and (.Values.pdb.enabled) (.Capabilities.APIVersions.Has "policy/v1/PodDisruptionBudget") }} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "emqx.fullname" . }}-pdb + namespace: {{ .Release.Namespace }} + labels: + app.kubernetes.io/name: {{ include "emqx.name" . }} + helm.sh/chart: {{ include "emqx.chart" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +spec: + maxUnavailable: {{ .Values.pdb.maxUnavailable }} + selector: + matchLabels: + app.kubernetes.io/name: {{ include "emqx.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} diff --git a/deploy/charts/emqx/values.yaml b/deploy/charts/emqx/values.yaml index b648f070f..7af0eff71 100644 --- a/deploy/charts/emqx/values.yaml +++ b/deploy/charts/emqx/values.yaml @@ -211,3 +211,10 @@ ssl: issuer: name: letsencrypt-dns kind: ClusterIssuer + +## Setting PodDisruptionBudget. +## ref: https://kubernetes.io/docs/tasks/run-application/configure-pdb +## +pdb: + enabled: false + maxUnavailable: 1 From b9d012e07247b91a04dfd64248ff059bbae3774a Mon Sep 17 00:00:00 2001 From: Erik Timan Date: Mon, 2 Jan 2023 14:43:49 +0100 Subject: [PATCH 0002/1802] refactor(emqx_resource): ingress bridge counter Unify code paths for resource metrics by removing emqx_resource:inc_received/1 and adding emqx_resource_metrics:received_inc/1 & friends. --- apps/emqx_connector/src/emqx_connector_mqtt.erl | 2 +- apps/emqx_resource/src/emqx_resource.erl | 5 +---- apps/emqx_resource/src/emqx_resource_metrics.erl | 16 ++++++++++++++++ 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/apps/emqx_connector/src/emqx_connector_mqtt.erl b/apps/emqx_connector/src/emqx_connector_mqtt.erl index 6c73a14c0..522f15ccf 100644 --- a/apps/emqx_connector/src/emqx_connector_mqtt.erl +++ b/apps/emqx_connector/src/emqx_connector_mqtt.erl @@ -136,7 +136,7 @@ drop_bridge(Name) -> %% When use this bridge as a data source, ?MODULE:on_message_received will be called %% if the bridge received msgs from the remote broker. on_message_received(Msg, HookPoint, ResId) -> - emqx_resource:inc_received(ResId), + emqx_resource_metrics:received_inc(ResId), emqx:run_hook(HookPoint, [Msg]). %% =================================================================== diff --git a/apps/emqx_resource/src/emqx_resource.erl b/apps/emqx_resource/src/emqx_resource.erl index 30934e0e7..aff66c287 100644 --- a/apps/emqx_resource/src/emqx_resource.erl +++ b/apps/emqx_resource/src/emqx_resource.erl @@ -111,7 +111,7 @@ list_group_instances/1 ]). --export([inc_received/1, apply_reply_fun/2]). +-export([apply_reply_fun/2]). -optional_callbacks([ on_query/3, @@ -467,8 +467,5 @@ apply_reply_fun(From, Result) -> %% ================================================================================= -inc_received(ResId) -> - emqx_metrics_worker:inc(?RES_METRICS, ResId, 'received'). - filter_instances(Filter) -> [Id || #{id := Id, mod := Mod} <- list_instances_verbose(), Filter(Id, Mod)]. diff --git a/apps/emqx_resource/src/emqx_resource_metrics.erl b/apps/emqx_resource/src/emqx_resource_metrics.erl index ff764ab3c..64f014918 100644 --- a/apps/emqx_resource/src/emqx_resource_metrics.erl +++ b/apps/emqx_resource/src/emqx_resource_metrics.erl @@ -55,6 +55,9 @@ matched_inc/1, matched_inc/2, matched_get/1, + received_inc/1, + received_inc/2, + received_get/1, retried_inc/1, retried_inc/2, retried_get/1, @@ -87,6 +90,7 @@ events() -> inflight, matched, queuing, + received, retried_failed, retried_success, success @@ -134,6 +138,8 @@ handle_telemetry_event( emqx_metrics_worker:inc(?RES_METRICS, ID, 'failed', Val); matched -> emqx_metrics_worker:inc(?RES_METRICS, ID, 'matched', Val); + received -> + emqx_metrics_worker:inc(?RES_METRICS, ID, 'received', Val); retried_failed -> emqx_metrics_worker:inc(?RES_METRICS, ID, 'retried', Val), emqx_metrics_worker:inc(?RES_METRICS, ID, 'failed', Val), @@ -309,6 +315,16 @@ matched_inc(ID, Val) -> matched_get(ID) -> emqx_metrics_worker:get(?RES_METRICS, ID, 'matched'). +%% @doc The number of messages that have been received from a bridge +received_inc(ID) -> + received_inc(ID, 1). + +received_inc(ID, Val) -> + telemetry:execute([?TELEMETRY_PREFIX, received], #{counter_inc => Val}, #{resource_id => ID}). + +received_get(ID) -> + emqx_metrics_worker:get(?RES_METRICS, ID, 'received'). + %% @doc The number of times message sends have been retried retried_inc(ID) -> retried_inc(ID, 1). From 3796ce31ad25f32dd0c40c35e6b17d3d8bcc400f Mon Sep 17 00:00:00 2001 From: Ilya Averyanov Date: Fri, 30 Dec 2022 14:11:27 +0300 Subject: [PATCH 0003/1802] chore(readme): remove legacy Travis mentions --- .editorconfig | 5 ----- CONTRIBUTING.md | 2 +- README-CN.md | 2 +- README-RU.md | 2 +- README.md | 2 +- 5 files changed, 4 insertions(+), 9 deletions(-) diff --git a/.editorconfig b/.editorconfig index c563aa10d..719028b4d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -20,8 +20,3 @@ indent_size = 4 # Tab indentation (no size specified) [Makefile] indent_style = tab - -# Matches the exact files either package.json or .travis.yml -[{.travis.yml}] -indent_style = space -indent_size = 2 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 118e9a046..272a602e9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -55,7 +55,7 @@ Must be one of the following: - **chore**: Updating grunt tasks etc; no production code change - **perf**: A code change that improves performance - **test**: Adding missing tests, refactoring tests; no production code change -- **build**: Changes that affect the CI/CD pipeline or build system or external dependencies (example scopes: travis, jenkins, makefile) +- **build**: Changes that affect the CI/CD pipeline or build system or external dependencies (example scopes: jenkins, makefile) - **ci**: Changes provided by DevOps for CI purposes. - **revert**: Reverts a previous commit. diff --git a/README-CN.md b/README-CN.md index 7e8cdd8a7..193e5ab98 100644 --- a/README-CN.md +++ b/README-CN.md @@ -1,7 +1,7 @@ # EMQX [![GitHub Release](https://img.shields.io/github/release/emqx/emqx?color=brightgreen&label=Release)](https://github.com/emqx/emqx/releases) -[![Build Status](https://img.shields.io/travis/emqx/emqx?label=Build)](https://travis-ci.org/emqx/emqx) +[![Build Status](https://github.com/emqx/emqx/actions/workflows/run_test_cases.yaml/badge.svg)](https://github.com/emqx/emqx/actions/workflows/run_test_cases.yaml) [![Coverage Status](https://img.shields.io/coveralls/github/emqx/emqx/master?label=Coverage)](https://coveralls.io/github/emqx/emqx?branch=master) [![Docker Pulls](https://img.shields.io/docker/pulls/emqx/emqx?label=Docker%20Pulls)](https://hub.docker.com/r/emqx/emqx) [![Slack](https://img.shields.io/badge/Slack-EMQ-39AE85?logo=slack)](https://slack-invite.emqx.io/) diff --git a/README-RU.md b/README-RU.md index 8a35177af..fb5ff9608 100644 --- a/README-RU.md +++ b/README-RU.md @@ -1,7 +1,7 @@ # Брокер EMQX [![GitHub Release](https://img.shields.io/github/release/emqx/emqx?color=brightgreen&label=Release)](https://github.com/emqx/emqx/releases) -[![Build Status](https://img.shields.io/travis/emqx/emqx?label=Build)](https://travis-ci.org/emqx/emqx) +[![Build Status](https://github.com/emqx/emqx/actions/workflows/run_test_cases.yaml/badge.svg)](https://github.com/emqx/emqx/actions/workflows/run_test_cases.yaml) [![Coverage Status](https://img.shields.io/coveralls/github/emqx/emqx/master?label=Coverage)](https://coveralls.io/github/emqx/emqx?branch=master) [![Docker Pulls](https://img.shields.io/docker/pulls/emqx/emqx?label=Docker%20Pulls)](https://hub.docker.com/r/emqx/emqx) [![Slack](https://img.shields.io/badge/Slack-EMQ-39AE85?logo=slack)](https://slack-invite.emqx.io/) diff --git a/README.md b/README.md index 1831ced60..94baba04f 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # EMQX [![GitHub Release](https://img.shields.io/github/release/emqx/emqx?color=brightgreen&label=Release)](https://github.com/emqx/emqx/releases) -[![Build Status](https://img.shields.io/travis/emqx/emqx?label=Build)](https://travis-ci.org/emqx/emqx) +[![Build Status](https://github.com/emqx/emqx/actions/workflows/run_test_cases.yaml/badge.svg)](https://github.com/emqx/emqx/actions/workflows/run_test_cases.yaml) [![Coverage Status](https://img.shields.io/coveralls/github/emqx/emqx/master?label=Coverage)](https://coveralls.io/github/emqx/emqx?branch=master) [![Docker Pulls](https://img.shields.io/docker/pulls/emqx/emqx?label=Docker%20Pulls)](https://hub.docker.com/r/emqx/emqx) [![Slack](https://img.shields.io/badge/Slack-EMQ-39AE85?logo=slack)](https://slack-invite.emqx.io/) From 2f208c3ab973ea0bcf0ba2d7e85d1c8ec1072b1e Mon Sep 17 00:00:00 2001 From: ieQu1 <99872536+ieQu1@users.noreply.github.com> Date: Wed, 14 Dec 2022 10:39:44 +0100 Subject: [PATCH 0004/1802] fix(cluster_rpc): Detect stopped replicant nodes --- apps/emqx_conf/src/emqx_cluster_rpc.erl | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/apps/emqx_conf/src/emqx_cluster_rpc.erl b/apps/emqx_conf/src/emqx_cluster_rpc.erl index d8b195587..fe701049c 100644 --- a/apps/emqx_conf/src/emqx_cluster_rpc.erl +++ b/apps/emqx_conf/src/emqx_cluster_rpc.erl @@ -512,7 +512,7 @@ do_alarm(Fun, Res, #{tnx_id := Id} = Meta) -> wait_for_all_nodes_commit(TnxId, Delay, Remain) -> Lagging = lagging_nodes(TnxId), - Stopped = stopped_nodes(), + Stopped = Lagging -- mria_mnesia:running_nodes(), case Lagging -- Stopped of [] when Stopped =:= [] -> ok; @@ -537,9 +537,10 @@ wait_for_nodes_commit(RequiredSyncs, TnxId, Delay, Remain) -> [] -> ok; Lagging -> - case stopped_nodes() of + Stopped = Lagging -- mria_mnesia:running_nodes(), + case Stopped of [] -> {peers_lagging, Lagging}; - Stopped -> {stopped_nodes, Stopped} + _ -> {stopped_nodes, Stopped} end end end. @@ -558,9 +559,6 @@ commit_status_trans(Operator, TnxId) -> Result = '$2', mnesia:select(?CLUSTER_COMMIT, [{MatchHead, [Guard], [Result]}]). -stopped_nodes() -> - ekka_cluster:info(stopped_nodes). - get_retry_ms() -> emqx_conf:get([node, cluster_call, retry_interval], timer:minutes(1)). From ced5a2705337beb6c06fd31c4206b572d1648a49 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Mon, 2 Jan 2023 21:30:02 +0100 Subject: [PATCH 0005/1802] test: fix emqx_conf test cases --- apps/emqx_conf/src/emqx_conf.app.src | 2 +- apps/emqx_conf/test/emqx_cluster_rpc_SUITE.erl | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/emqx_conf/src/emqx_conf.app.src b/apps/emqx_conf/src/emqx_conf.app.src index 6d7e9ef9c..3d18083f8 100644 --- a/apps/emqx_conf/src/emqx_conf.app.src +++ b/apps/emqx_conf/src/emqx_conf.app.src @@ -1,6 +1,6 @@ {application, emqx_conf, [ {description, "EMQX configuration management"}, - {vsn, "0.1.8"}, + {vsn, "0.1.9"}, {registered, []}, {mod, {emqx_conf_app, []}}, {applications, [kernel, stdlib]}, diff --git a/apps/emqx_conf/test/emqx_cluster_rpc_SUITE.erl b/apps/emqx_conf/test/emqx_cluster_rpc_SUITE.erl index d4b28e946..4c449f580 100644 --- a/apps/emqx_conf/test/emqx_cluster_rpc_SUITE.erl +++ b/apps/emqx_conf/test/emqx_cluster_rpc_SUITE.erl @@ -48,11 +48,14 @@ init_per_suite(Config) -> meck:new(emqx_alarm, [non_strict, passthrough, no_link]), meck:expect(emqx_alarm, activate, 3, ok), meck:expect(emqx_alarm, deactivate, 3, ok), + meck:new(mria_mnesia, [non_strict, passthrough, no_link]), + meck:expect(mria_mnesia, running_nodes, 0, [?NODE1, {node(), ?NODE2}, {node(), ?NODE3}]), Config. end_per_suite(_Config) -> ekka:stop(), mria:stop(), + meck:unload(mria_mnesia), mria_mnesia:delete_schema(), meck:unload(emqx_alarm), ok. From d6c8a106da85d103e14b4afaf1c07f03333e153c Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Mon, 2 Jan 2023 17:58:12 -0300 Subject: [PATCH 0006/1802] test(gcp_pubsub): fix flaky test --- .../test/emqx_ee_bridge_gcp_pubsub_SUITE.erl | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_gcp_pubsub_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_gcp_pubsub_SUITE.erl index a8ba91175..f68c1391f 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_gcp_pubsub_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_gcp_pubsub_SUITE.erl @@ -524,6 +524,29 @@ install_telemetry_handler(TestCase) -> end), Tid. +wait_until_gauge_is(GaugeName, ExpectedValue, Timeout) -> + Events = receive_all_events(GaugeName, Timeout), + case lists:last(Events) of + #{measurements := #{gauge_set := ExpectedValue}} -> + ok; + #{measurements := #{gauge_set := Value}} -> + ct:fail( + "gauge ~p didn't reach expected value ~p; last value: ~p", + [GaugeName, ExpectedValue, Value] + ) + end. + +receive_all_events(EventName, Timeout) -> + receive_all_events(EventName, Timeout, []). + +receive_all_events(EventName, Timeout, Acc) -> + receive + {telemetry, #{name := [_, _, EventName]} = Event} -> + receive_all_events(EventName, Timeout, [Event | Acc]) + after Timeout -> + lists:reverse(Acc) + end. + wait_telemetry_event(TelemetryTable, EventName, ResourceId) -> wait_telemetry_event(TelemetryTable, EventName, ResourceId, #{timeout => 5_000, n_events => 1}). @@ -803,6 +826,8 @@ t_publish_success_batch(Config) -> ResourceId, #{timeout => 15_000, n_events => NumMessages} ), + wait_until_gauge_is(queuing, 0, _Timeout = 400), + wait_until_gauge_is(inflight, 0, _Timeout = 400), assert_metrics( #{ batching => 0, From 0014dfef3bdd60523c767b8671a9035ff72535e1 Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Fri, 30 Dec 2022 11:25:09 -0300 Subject: [PATCH 0007/1802] chore(docker): add option to stop local testing containers (v5.0) --- scripts/ct/run.sh | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/scripts/ct/run.sh b/scripts/ct/run.sh index 756f1520f..fc28c3a90 100755 --- a/scripts/ct/run.sh +++ b/scripts/ct/run.sh @@ -14,6 +14,7 @@ help() { echo "--suites SUITE1,SUITE2: Comma separated SUITE names to run. e.g. apps/emqx/test/emqx_SUITE.erl" echo "--console: Start EMQX in console mode but do not run test cases" echo "--attach: Attach to the Erlang docker container without running any test case" + echo "--stop: Stop running containers for the given app" echo "--only-up: Only start the testbed but do not run CT" echo "--keep-up: Keep the testbed running after CT" } @@ -24,6 +25,7 @@ KEEP_UP='no' ONLY_UP='no' SUITES='' ATTACH='no' +STOP='no' while [ "$#" -gt 0 ]; do case $1 in -h|--help) @@ -46,6 +48,10 @@ while [ "$#" -gt 0 ]; do ATTACH='yes' shift 1 ;; + --stop) + STOP='yes' + shift 1 + ;; --console) CONSOLE='yes' shift 1 @@ -155,8 +161,10 @@ else export UID_GID="$ORIG_UID_GID" fi -# shellcheck disable=2086 # no quotes for F_OPTIONS -docker-compose $F_OPTIONS up -d --build --remove-orphans +if [ "$STOP" = 'no' ]; then + # shellcheck disable=2086 # no quotes for F_OPTIONS + docker-compose $F_OPTIONS up -d --build --remove-orphans +fi # /emqx is where the source dir is mounted to the Erlang container # in .ci/docker-compose-file/docker-compose.yaml @@ -183,7 +191,10 @@ fi set +e -if [ "$ATTACH" = 'yes' ]; then +if [ "$STOP" = 'yes' ]; then + # shellcheck disable=2086 # no quotes for F_OPTIONS + docker-compose $F_OPTIONS down --remove-orphans +elif [ "$ATTACH" = 'yes' ]; then docker exec -it "$ERLANG_CONTAINER" bash restore_ownership elif [ "$CONSOLE" = 'yes' ]; then From 7bbdc209934a47b5a195ab116723c2cc774471b1 Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Mon, 2 Jan 2023 17:31:26 -0300 Subject: [PATCH 0008/1802] docs(mongodb_connector): fix duration field descriptions https://emqx.atlassian.net/browse/EMQX-8642 Adds different descriptions for the different fields of type duration for MongoDB. Based off: https://www.mongodb.com/docs/manual/reference/connection-string/ --- .../i18n/emqx_connector_mongo.conf | 106 ++++++++++++++++-- .../src/emqx_connector_mongo.erl | 28 ++--- 2 files changed, 112 insertions(+), 22 deletions(-) diff --git a/apps/emqx_connector/i18n/emqx_connector_mongo.conf b/apps/emqx_connector/i18n/emqx_connector_mongo.conf index e43f7bc33..919ace891 100644 --- a/apps/emqx_connector/i18n/emqx_connector_mongo.conf +++ b/apps/emqx_connector/i18n/emqx_connector_mongo.conf @@ -106,15 +106,103 @@ The MongoDB default port 27017 is used if `[:Port]` is not specified. } } - duration { - desc { - en: "Time interval, such as timeout or TTL." - zh: "时间间隔,例如超时或 TTL。" - } - label: { - en: "Time Interval" - zh: "时间间隔" - } + overflow_ttl { + desc { + en: "Period of time before workers that exceed the configured pool size (\"overflow\") to be terminated." + zh: "超过配置的池子大小(\"溢出\")的工人被终止前的时间段。" + } + label { + en: "Overflow TTL" + zh: "溢出TTL" + } + } + + overflow_check_period { + desc { + en: "Period for checking if there are more workers than configured (\"overflow\")." + zh: "检查是否有超过配置的工人的周期(\"溢出\")。" + } + label { + en: "Overflow Check Period" + zh: "溢出检查期" + } + } + + local_threshold { + desc { + en: "The size of the latency window for selecting among multiple suitable MongoDB instances." + zh: "在多个合适的MongoDB实例中进行选择的延迟窗口的大小。" + } + label { + en: "Local Threshold" + zh: "本地阈值" + } + } + + connect_timeout { + desc { + en: "The duration to attempt a connection before timing out." + zh: "在超时前尝试连接的持续时间。" + } + label { + en: "Connect Timeout" + zh: "连接超时" + } + } + + socket_timeout { + desc { + en: "The duration to attempt to send or to receive on a socket before the attempt times out." + zh: "在尝试超时之前,在套接字上尝试发送或接收的持续时间。" + } + label { + en: "Socket Timeout" + zh: "插座超时" + } + } + + server_selection_timeout { + desc { + en: "Specifies how long to block for server selection before throwing an exception." + zh: "指定在抛出异常之前为服务器选择阻断多长时间。" + } + label { + en: "Server Selection Timeout" + zh: "服务器选择超时" + } + } + + wait_queue_timeout { + desc { + en: "The maximum duration that a worker can wait for a connection to become available." + zh: "工作者等待连接可用的最长时间。" + } + label { + en: "Wait Queue Timeout" + zh: "等待队列超时" + } + } + + heartbeat_period { + desc { + en: "Controls when the driver checks the state of the MongoDB deployment. Specify the interval between checks, counted from the end of the previous check until the beginning of the next one." + zh: "控制驱动程序何时检查MongoDB部署的状态。指定检查的间隔时间,从上一次检查结束到下一次检查开始计算。" + } + label { + en: "Heartbeat period" + zh: "心跳期" + } + } + + min_heartbeat_period { + desc { + en: "Controls the minimum amount of time to wait between heartbeats." + zh: "控制心跳之间的最小等待时间。" + } + label { + en: "Minimum Heartbeat Period" + zh: "最小心跳周期" + } } max_overflow { diff --git a/apps/emqx_connector/src/emqx_connector_mongo.erl b/apps/emqx_connector/src/emqx_connector_mongo.erl index 407fa39a8..f9e703d20 100644 --- a/apps/emqx_connector/src/emqx_connector_mongo.erl +++ b/apps/emqx_connector/src/emqx_connector_mongo.erl @@ -102,15 +102,15 @@ fields(topology) -> [ {pool_size, fun emqx_connector_schema_lib:pool_size/1}, {max_overflow, fun max_overflow/1}, - {overflow_ttl, fun duration/1}, - {overflow_check_period, fun duration/1}, - {local_threshold_ms, fun duration/1}, - {connect_timeout_ms, fun duration/1}, - {socket_timeout_ms, fun duration/1}, - {server_selection_timeout_ms, fun duration/1}, - {wait_queue_timeout_ms, fun duration/1}, - {heartbeat_frequency_ms, fun duration/1}, - {min_heartbeat_frequency_ms, fun duration/1} + {overflow_ttl, duration("overflow_ttl")}, + {overflow_check_period, duration("overflow_check_period")}, + {local_threshold_ms, duration("local_threshold")}, + {connect_timeout_ms, duration("connect_timeout")}, + {socket_timeout_ms, duration("socket_timeout")}, + {server_selection_timeout_ms, duration("server_selection_timeout")}, + {wait_queue_timeout_ms, duration("wait_queue_timeout")}, + {heartbeat_frequency_ms, duration("heartbeat_period")}, + {min_heartbeat_frequency_ms, duration("min_heartbeat_period")} ]. desc(single) -> @@ -403,10 +403,12 @@ r_mode(desc) -> ?DESC("r_mode"); r_mode(default) -> master; r_mode(_) -> undefined. -duration(type) -> emqx_schema:duration_ms(); -duration(desc) -> ?DESC("duration"); -duration(required) -> false; -duration(_) -> undefined. +duration(Desc) -> + #{ + type => emqx_schema:duration_ms(), + required => false, + desc => ?DESC(Desc) + }. max_overflow(type) -> non_neg_integer(); max_overflow(desc) -> ?DESC("max_overflow"); From b016695cb1a30ad5f3d68a1a66539e531428f73b Mon Sep 17 00:00:00 2001 From: firest Date: Thu, 22 Dec 2022 15:39:38 +0800 Subject: [PATCH 0009/1802] fix(bridges): obfuscate the password in bridges API responses --- apps/emqx/src/emqx_misc.erl | 127 ++++++++++++++++++++++- apps/emqx_bridge/src/emqx_bridge_api.erl | 36 ++++++- 2 files changed, 157 insertions(+), 6 deletions(-) diff --git a/apps/emqx/src/emqx_misc.erl b/apps/emqx/src/emqx_misc.erl index 0e7b29869..483b99587 100644 --- a/apps/emqx/src/emqx_misc.erl +++ b/apps/emqx/src/emqx_misc.erl @@ -68,7 +68,7 @@ nolink_apply/2 ]). --export([clamp/3]). +-export([clamp/3, redact/1, redact/2, is_redacted/2, is_redacted/3]). -dialyzer({nowarn_function, [nolink_apply/2]}). @@ -556,6 +556,75 @@ try_to_existing_atom(Convert, Data, Encoding) -> _:Reason -> {error, Reason} end. +is_sensitive_key(token) -> true; +is_sensitive_key("token") -> true; +is_sensitive_key(<<"token">>) -> true; +is_sensitive_key(password) -> true; +is_sensitive_key("password") -> true; +is_sensitive_key(<<"password">>) -> true; +is_sensitive_key(secret) -> true; +is_sensitive_key("secret") -> true; +is_sensitive_key(<<"secret">>) -> true; +is_sensitive_key(_) -> false. + +redact(Term) -> + do_redact(Term, fun is_sensitive_key/1). + +redact(Term, Checker) -> + do_redact(Term, fun(V) -> + is_sensitive_key(V) orelse Checker(V) + end). + +do_redact(L, Checker) when is_list(L) -> + lists:map(fun(E) -> do_redact(E, Checker) end, L); +do_redact(M, Checker) when is_map(M) -> + maps:map( + fun(K, V) -> + do_redact(K, V, Checker) + end, + M + ); +do_redact({Key, Value}, Checker) -> + case Checker(Key) of + true -> + {Key, redact_v(Value)}; + false -> + {do_redact(Key, Checker), do_redact(Value, Checker)} + end; +do_redact(T, Checker) when is_tuple(T) -> + Elements = erlang:tuple_to_list(T), + Redact = do_redact(Elements, Checker), + erlang:list_to_tuple(Redact); +do_redact(Any, _Checker) -> + Any. + +do_redact(K, V, Checker) -> + case Checker(K) of + true -> + redact_v(V); + false -> + do_redact(V, Checker) + end. + +-define(REDACT_VAL, "******"). +redact_v(V) when is_binary(V) -> <>; +redact_v(_V) -> ?REDACT_VAL. + +is_redacted(K, V) -> + do_is_redacted(K, V, fun is_sensitive_key/1). + +is_redacted(K, V, Fun) -> + do_is_redacted(K, V, fun(E) -> + is_sensitive_key(E) orelse Fun(E) + end). + +do_is_redacted(K, ?REDACT_VAL, Fun) -> + Fun(K); +do_is_redacted(K, <>, Fun) -> + Fun(K); +do_is_redacted(_K, _V, _Fun) -> + false. + -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). @@ -568,6 +637,62 @@ ipv6_probe_test() -> ok end. +redact_test_() -> + Case = fun(Type, KeyT) -> + Key = + case Type of + atom -> KeyT; + string -> erlang:atom_to_list(KeyT); + binary -> erlang:atom_to_binary(KeyT) + end, + + ?assert(is_sensitive_key(Key)), + + %% direct + ?assertEqual({Key, ?REDACT_VAL}, redact({Key, foo})), + ?assertEqual(#{Key => ?REDACT_VAL}, redact(#{Key => foo})), + ?assertEqual({Key, Key, Key}, redact({Key, Key, Key})), + ?assertEqual({[{Key, ?REDACT_VAL}], bar}, redact({[{Key, foo}], bar})), + + %% 1 level nested + ?assertEqual([{Key, ?REDACT_VAL}], redact([{Key, foo}])), + ?assertEqual([#{Key => ?REDACT_VAL}], redact([#{Key => foo}])), + + %% 2 level nested + ?assertEqual(#{opts => [{Key, ?REDACT_VAL}]}, redact(#{opts => [{Key, foo}]})), + ?assertEqual(#{opts => #{Key => ?REDACT_VAL}}, redact(#{opts => #{Key => foo}})), + ?assertEqual({opts, [{Key, ?REDACT_VAL}]}, redact({opts, [{Key, foo}]})), + + %% 3 level nested + ?assertEqual([#{opts => [{Key, ?REDACT_VAL}]}], redact([#{opts => [{Key, foo}]}])), + ?assertEqual([{opts, [{Key, ?REDACT_VAL}]}], redact([{opts, [{Key, foo}]}])), + ?assertEqual([{opts, [#{Key => ?REDACT_VAL}]}], redact([{opts, [#{Key => foo}]}])) + end, + + Types = [atom, string, binary], + Keys = [ + token, + password, + secret + ], + [{case_name(Type, Key), fun() -> Case(Type, Key) end} || Key <- Keys, Type <- Types]. + +redact2_test_() -> + Case = fun(Key, Checker) -> + ?assertEqual({Key, ?REDACT_VAL}, redact({Key, foo}, Checker)), + ?assertEqual(#{Key => ?REDACT_VAL}, redact(#{Key => foo}, Checker)), + ?assertEqual({Key, Key, Key}, redact({Key, Key, Key}, Checker)), + ?assertEqual({[{Key, ?REDACT_VAL}], bar}, redact({[{Key, foo}], bar}, Checker)) + end, + + Checker = fun(E) -> E =:= passcode end, + + Keys = [secret, passcode], + [{case_name(atom, Key), fun() -> Case(Key, Checker) end} || Key <- Keys]. + +case_name(Type, Key) -> + lists:concat([Type, "-", Key]). + -endif. pub_props_to_packet(Properties) -> diff --git a/apps/emqx_bridge/src/emqx_bridge_api.erl b/apps/emqx_bridge/src/emqx_bridge_api.erl index e649e5215..1debc90c4 100644 --- a/apps/emqx_bridge/src/emqx_bridge_api.erl +++ b/apps/emqx_bridge/src/emqx_bridge_api.erl @@ -409,11 +409,13 @@ schema("/nodes/:node/bridges/:id/operation/:operation") -> '/bridges/:id'(get, #{bindings := #{id := Id}}) -> ?TRY_PARSE_ID(Id, lookup_from_all_nodes(BridgeType, BridgeName, 200)); '/bridges/:id'(put, #{bindings := #{id := Id}, body := Conf0}) -> - Conf = filter_out_request_body(Conf0), + Conf1 = filter_out_request_body(Conf0), ?TRY_PARSE_ID( Id, case emqx_bridge:lookup(BridgeType, BridgeName) of {ok, _} -> + RawConf = emqx:get_raw_config([bridges, BridgeType, BridgeName], #{}), + Conf = deobfuscate(Conf1, RawConf), case ensure_bridge_created(BridgeType, BridgeName, Conf) of ok -> lookup_from_all_nodes(BridgeType, BridgeName, 200); @@ -604,12 +606,12 @@ format_bridge_info([FirstBridge | _] = Bridges) -> Res = maps:remove(node, FirstBridge), NodeStatus = collect_status(Bridges), NodeMetrics = collect_metrics(Bridges), - Res#{ + redact(Res#{ status => aggregate_status(NodeStatus), node_status => NodeStatus, metrics => aggregate_metrics(NodeMetrics), node_metrics => NodeMetrics - }. + }). collect_status(Bridges) -> [maps:with([node, status], B) || B <- Bridges]. @@ -676,13 +678,13 @@ format_resp( Node ) -> RawConfFull = fill_defaults(Type, RawConf), - RawConfFull#{ + redact(RawConfFull#{ type => Type, name => maps:get(<<"name">>, RawConf, BridgeName), node => Node, status => Status, metrics => format_metrics(Metrics) - }. + }). format_metrics(#{ counters := #{ @@ -806,3 +808,27 @@ call_operation(Node, OperFunc, BridgeType, BridgeName) -> {error, _} -> {400, error_msg('INVALID_NODE', <<"invalid node">>)} end. + +redact(Term) -> + emqx_misc:redact(Term). + +deobfuscate(NewConf, OldConf) -> + maps:fold( + fun(K, V, Acc) -> + case maps:find(K, OldConf) of + error -> + Acc#{K => V}; + {ok, OldV} when is_map(V), is_map(OldV) -> + Acc#{K => deobfuscate(V, OldV)}; + {ok, OldV} -> + case emqx_misc:is_redacted(K, V) of + true -> + Acc#{K => OldV}; + _ -> + Acc#{K => V} + end + end + end, + #{}, + NewConf + ). From b43be50a987d72a09c0b323bf7a59bae7c5af647 Mon Sep 17 00:00:00 2001 From: firest Date: Fri, 30 Dec 2022 16:35:04 +0800 Subject: [PATCH 0010/1802] test: add the `redacted` test case for bridges api --- .../test/emqx_bridge_api_SUITE.erl | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl b/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl index 74e712c6f..60f770df8 100644 --- a/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl +++ b/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl @@ -514,6 +514,39 @@ t_reset_bridges(Config) -> {ok, 204, <<>>} = request(delete, uri(["bridges", BridgeID]), []), {ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []). +t_with_redact_update(_Config) -> + Name = <<"redact_update">>, + Type = <<"mqtt">>, + Password = <<"123456">>, + Template = #{ + <<"type">> => Type, + <<"name">> => Name, + <<"server">> => <<"127.0.0.1:1883">>, + <<"username">> => <<"test">>, + <<"password">> => Password, + <<"ingress">> => + #{<<"remote">> => #{<<"topic">> => <<"t/#">>}} + }, + + {ok, 201, _} = request( + post, + uri(["bridges"]), + Template + ), + + %% update with redacted config + Conf = emqx_misc:redact(Template), + BridgeID = emqx_bridge_resource:bridge_id(Type, Name), + {ok, 200, _ResBin} = request( + put, + uri(["bridges", BridgeID]), + Conf + ), + RawConf = emqx:get_raw_config([bridges, Type, Name]), + Value = maps:get(<<"password">>, RawConf), + ?assertEqual(Password, Value), + ok. + request(Method, Url, Body) -> request(<<"bridge_admin">>, Method, Url, Body). From 69c7b41b5227a744789d371d1d1b002725b37fdb Mon Sep 17 00:00:00 2001 From: firest Date: Mon, 2 Jan 2023 21:52:11 +0800 Subject: [PATCH 0011/1802] chore: bump version && update changes --- changes/v5.0.14-en.md | 2 ++ changes/v5.0.14-zh.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/changes/v5.0.14-en.md b/changes/v5.0.14-en.md index 7ca8200b6..f61211bb8 100644 --- a/changes/v5.0.14-en.md +++ b/changes/v5.0.14-en.md @@ -7,6 +7,8 @@ `env EMQX_BRIDGES__MQTT__XYZ__SERVER='"localhost:1883"'`. Now it's possible to set it without quote as `env EMQX_BRIDGES__MQTT__XYZ__SERVER='localhost:1883'`. +- Obfuscated sensitive data in the response when querying `bridges` information by API [#9593](https://github.com/emqx/emqx/pull/9593/). + ## Bug Fixes - Fix an issue where testing the GCP PubSub could leak memory, and an issue where its JWT token would fail to refresh a second time. [#9641](https://github.com/emqx/emqx/pull/9641) diff --git a/changes/v5.0.14-zh.md b/changes/v5.0.14-zh.md index 2667fabef..c96bef305 100644 --- a/changes/v5.0.14-zh.md +++ b/changes/v5.0.14-zh.md @@ -7,6 +7,8 @@ `env EMQX_BRIDGES__MQTT__XYZ__SERVER='"localhost:1883"'`。 此修复后,可以不使用引号,例如 `env EMQX_BRIDGES__MQTT__XYZ__SERVER='localhost:1883'`。 +- 通过 API 查询 `bridges` 信息时将混淆响应中的敏感数据 [#9593](https://github.com/emqx/emqx/pull/9593/)。 + ## 修复 - 修复了测试GCP PubSub可能泄露内存的问题,以及其JWT令牌第二次刷新失败的问题。 [#9640](https://github.com/emqx/emqx/pull/9640) From c3cfbae3c2b8a11fbb305a8eff99313e3a9d9bf1 Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Tue, 3 Jan 2023 11:38:09 -0300 Subject: [PATCH 0012/1802] docs: improve descriptions Thanks to @qzhuyan for the corrections --- apps/emqx_connector/i18n/emqx_connector_mongo.conf | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/emqx_connector/i18n/emqx_connector_mongo.conf b/apps/emqx_connector/i18n/emqx_connector_mongo.conf index 919ace891..a598c084d 100644 --- a/apps/emqx_connector/i18n/emqx_connector_mongo.conf +++ b/apps/emqx_connector/i18n/emqx_connector_mongo.conf @@ -109,7 +109,7 @@ The MongoDB default port 27017 is used if `[:Port]` is not specified. overflow_ttl { desc { en: "Period of time before workers that exceed the configured pool size (\"overflow\") to be terminated." - zh: "超过配置的池子大小(\"溢出\")的工人被终止前的时间段。" + zh: "当池内工人太多时,等待多久清除多余工人。" } label { en: "Overflow TTL" @@ -124,7 +124,7 @@ The MongoDB default port 27017 is used if `[:Port]` is not specified. } label { en: "Overflow Check Period" - zh: "溢出检查期" + zh: "溢出检查周期" } } @@ -142,7 +142,7 @@ The MongoDB default port 27017 is used if `[:Port]` is not specified. connect_timeout { desc { en: "The duration to attempt a connection before timing out." - zh: "在超时前尝试连接的持续时间。" + zh: "超时重连的等待时间。" } label { en: "Connect Timeout" @@ -157,7 +157,7 @@ The MongoDB default port 27017 is used if `[:Port]` is not specified. } label { en: "Socket Timeout" - zh: "插座超时" + zh: "套接字操作超时" } } @@ -197,7 +197,7 @@ The MongoDB default port 27017 is used if `[:Port]` is not specified. min_heartbeat_period { desc { en: "Controls the minimum amount of time to wait between heartbeats." - zh: "控制心跳之间的最小等待时间。" + zh: "心跳间的最小间隙" } label { en: "Minimum Heartbeat Period" From 4feaf5916f6312db3545d195defbea07e7ee9bc0 Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Tue, 3 Jan 2023 11:46:58 -0300 Subject: [PATCH 0013/1802] fix(mongodb): add `type` and `name` fields to the `get`/`post` API docs https://emqx.atlassian.net/browse/EMQX-8643 Adds thoses fields to MongoDB API docs so the frontend may use them to render properly. --- .../i18n/emqx_ee_bridge_mongodb.conf | 22 +++++++++++++++++ .../src/emqx_ee_bridge_mongodb.erl | 24 ++++++++++++++----- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_mongodb.conf b/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_mongodb.conf index fef3663ef..f8009f0a4 100644 --- a/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_mongodb.conf +++ b/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_mongodb.conf @@ -64,4 +64,26 @@ emqx_ee_bridge_mongodb { zh: "MongoDB(独立)配置" } } + + desc_type { + desc { + en: """The Bridge Type.""" + zh: """桥接类型。""" + } + label { + en: "Bridge Type" + zh: "桥接类型" + } + } + + desc_name { + desc { + en: """Bridge name.""" + zh: """桥接名称。""" + } + label { + en: "Bridge Name" + zh: "桥接名称" + } + } } diff --git a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_mongodb.erl b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_mongodb.erl index 3836e2caa..516c75f65 100644 --- a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_mongodb.erl +++ b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_mongodb.erl @@ -46,11 +46,11 @@ fields(mongodb_sharded) -> fields(mongodb_single) -> emqx_connector_mongo:fields(single) ++ fields("config"); fields("post_rs") -> - fields(mongodb_rs); + fields(mongodb_rs) ++ type_and_name_fields(mongodb_rs); fields("post_sharded") -> - fields(mongodb_sharded); + fields(mongodb_sharded) ++ type_and_name_fields(mongodb_sharded); fields("post_single") -> - fields(mongodb_single); + fields(mongodb_single) ++ type_and_name_fields(mongodb_single); fields("put_rs") -> fields(mongodb_rs); fields("put_sharded") -> @@ -58,11 +58,17 @@ fields("put_sharded") -> fields("put_single") -> fields(mongodb_single); fields("get_rs") -> - emqx_bridge_schema:metrics_status_fields() ++ fields(mongodb_rs); + emqx_bridge_schema:metrics_status_fields() ++ + fields(mongodb_rs) ++ + type_and_name_fields(mongodb_rs); fields("get_sharded") -> - emqx_bridge_schema:metrics_status_fields() ++ fields(mongodb_sharded); + emqx_bridge_schema:metrics_status_fields() ++ + fields(mongodb_sharded) ++ + type_and_name_fields(mongodb_sharded); fields("get_single") -> - emqx_bridge_schema:metrics_status_fields() ++ fields(mongodb_single). + emqx_bridge_schema:metrics_status_fields() ++ + fields(mongodb_single) ++ + type_and_name_fields(mongodb_single). conn_bridge_examples(Method) -> [ @@ -103,6 +109,12 @@ desc(_) -> %% Internal fns %%================================================================================================= +type_and_name_fields(MongoType) -> + [ + {type, mk(MongoType, #{required => true, desc => ?DESC("desc_type")})}, + {name, mk(binary(), #{required => true, desc => ?DESC("desc_name")})} + ]. + values(mongodb_rs = MongoType, Method) -> TypeOpts = #{ servers => <<"localhost:27017, localhost:27018">>, From 29425514ca426a71f51528b6eb879c2abace4c05 Mon Sep 17 00:00:00 2001 From: Ivan Dyachkov Date: Tue, 3 Jan 2023 19:45:11 +0100 Subject: [PATCH 0014/1802] chore: use @emqx/emqx-review-board team in codeowners --- .github/CODEOWNERS | 66 ++++++++++++++++++++++------------------------ 1 file changed, 31 insertions(+), 35 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 3033b7965..a74fcb27e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,42 +1,38 @@ ## Default -* @zmstone @ieQu1 @terry-xiaoyu @qzhuyan @HJianBo @zhongwencool - -## MQTT -/apps/emqx_connector/src/mqtt/ @qzhuyan -/apps/emqx/*/*mqtt* @qzhuyan +* @emqx/emqx-review-board ## apps -/apps/emqx/ @lafirest @thalesmg @HJianBo @ieQu1 -/apps/emqx_authn/ @savonarola @JimMoen @HJianBo -/apps/emqx_authz/ @savonarola @JimMoen @HJianBo -/apps/emqx_auto_subscribe/ @thalesmg @HJianBo -/apps/emqx_bridge/ @terry-xiaoyu @thalesmg -/apps/emqx_conf/ @ieQu1 @thalesmg -/apps/emqx_connector/ @terry-xiaoyu @JimMoen -/apps/emqx_dashboard/ @lafirest @JimMoen -/apps/emqx_exhook/ @lafirest @HJianBo @JimMoen -/apps/emqx_gateway/ @HJianBo @lafirest -/apps/emqx_machine/ @thalesmg @terry-xiaoyu @ieQu1 -/apps/emqx_management/ @HJianBo @lafirest @sstrigler -/apps/emqx_modules/ @thalesmg @terry-xiaoyu @HJianBo -/apps/emqx_plugin_libs/ @terry-xiaoyu @lafirest -/apps/emqx_plugins/ @thalesmg @JimMoen @ieQu1 -/apps/emqx_prometheus/ @JimMoen @ieQu1 -/apps/emqx_psk/ @lafirest @thalesmg @terry-xiaoyu -/apps/emqx_resource/ @terry-xiaoyu @thalesmg -/apps/emqx_retainer/ @lafirest @ieQu1 @thalesmg -/apps/emqx_rule_engine/ @terry-xiaoyu @HJianBo @kjellwinblad -/apps/emqx_slow_subs/ @lafirest @HJianBo -/apps/emqx_statsd/ @JimMoen @HJianBo +/apps/emqx/ @emqx/emqx-review-board @lafirest @thalesmg +/apps/emqx_authn/ @emqx/emqx-review-board @JimMoen @savonarola +/apps/emqx_authz/ @emqx/emqx-review-board @JimMoen @savonarola +/apps/emqx_auto_subscribe/ @emqx/emqx-review-board @thalesmg +/apps/emqx_bridge/ @emqx/emqx-review-board @thalesmg +/apps/emqx_conf/ @emqx/emqx-review-board @thalesmg +/apps/emqx_connector/ @emqx/emqx-review-board @JimMoen +/apps/emqx_dashboard/ @emqx/emqx-review-board @JimMoen @lafirest +/apps/emqx_exhook/ @emqx/emqx-review-board @JimMoen @lafirest +/apps/emqx_gateway/ @emqx/emqx-review-board @lafirest +/apps/emqx_machine/ @emqx/emqx-review-board @thalesmg +/apps/emqx_management/ @emqx/emqx-review-board @lafirest @sstrigler +/apps/emqx_modules/ @emqx/emqx-review-board @thalesmg +/apps/emqx_plugin_libs/ @emqx/emqx-review-board @lafirest +/apps/emqx_plugins/ @emqx/emqx-review-board @JimMoen @thalesmg +/apps/emqx_prometheus/ @emqx/emqx-review-board @JimMoen +/apps/emqx_psk/ @emqx/emqx-review-board @lafirest @thalesmg +/apps/emqx_resource/ @emqx/emqx-review-board @thalesmg +/apps/emqx_retainer/ @emqx/emqx-review-board @lafirest @thalesmg +/apps/emqx_rule_engine/ @emqx/emqx-review-board @kjellwinblad +/apps/emqx_slow_subs/ @emqx/emqx-review-board @lafirest +/apps/emqx_statsd/ @emqx/emqx-review-board @JimMoen ## other -/lib-ee/ @thalesmg -/bin/ @zmstone @thalesmg @terry-xiaoyu @id -/rel/ @zmstone @thalesmg @id +/lib-ee/ @emqx/emqx-review-board @thalesmg +/bin/ @emqx/emqx-review-board @thalesmg @id +/rel/ @emqx/emqx-review-board @thalesmg @id ## CI -/.github/ @id -/.ci/ @id -/scripts/ @id -/build @id -/deploy/ @id +/.github/ @emqx/emqx-review-board @id +/.ci/ @emqx/emqx-review-board @id +/scripts/ @emqx/emqx-review-board @id +/build @emqx/emqx-review-board @id +/deploy/ @emqx/emqx-review-board @id From eb7dca3691b7258688003aa89f9fbad2f1c47363 Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Tue, 3 Jan 2023 16:50:42 -0300 Subject: [PATCH 0015/1802] chore(rule_engine): reset metrics when disabling a rule https://emqx.atlassian.net/browse/EMQX-8502 When a bridge is disabled, its metrics are reset. With this change, we make rule actions behave like that: disabling a rule will reset its metrics. --- .../src/emqx_rule_engine.app.src | 2 +- .../emqx_rule_engine/src/emqx_rule_engine.erl | 2 +- .../test/emqx_rule_engine_api_SUITE.erl | 32 +++++++++++++++++++ changes/v5.0.14-en.md | 2 ++ changes/v5.0.14-zh.md | 2 ++ 5 files changed, 38 insertions(+), 2 deletions(-) diff --git a/apps/emqx_rule_engine/src/emqx_rule_engine.app.src b/apps/emqx_rule_engine/src/emqx_rule_engine.app.src index edca35839..ea0bf6f9e 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_engine.app.src +++ b/apps/emqx_rule_engine/src/emqx_rule_engine.app.src @@ -2,7 +2,7 @@ {application, emqx_rule_engine, [ {description, "EMQX Rule Engine"}, % strict semver, bump manually! - {vsn, "5.0.6"}, + {vsn, "5.0.7"}, {modules, []}, {registered, [emqx_rule_engine_sup, emqx_rule_engine]}, {applications, [kernel, stdlib, rulesql, getopt]}, diff --git a/apps/emqx_rule_engine/src/emqx_rule_engine.erl b/apps/emqx_rule_engine/src/emqx_rule_engine.erl index 0fab91389..f15290547 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_engine.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_engine.erl @@ -268,7 +268,7 @@ load_hooks_for_rule(#{from := Topics}) -> maybe_add_metrics_for_rule(Id) -> case emqx_metrics_worker:has_metrics(rule_metrics, Id) of true -> - ok; + ok = reset_metrics_for_rule(Id); false -> ok = emqx_metrics_worker:create_metrics(rule_metrics, Id, ?METRICS, ?RATE_METRICS) end. diff --git a/apps/emqx_rule_engine/test/emqx_rule_engine_api_SUITE.erl b/apps/emqx_rule_engine/test/emqx_rule_engine_api_SUITE.erl index cbe43eaa6..82a305009 100644 --- a/apps/emqx_rule_engine/test/emqx_rule_engine_api_SUITE.erl +++ b/apps/emqx_rule_engine/test/emqx_rule_engine_api_SUITE.erl @@ -189,6 +189,38 @@ t_list_rule_api(_Config) -> ?assertEqual(maps:get(data, Result1), maps:get(data, Result6)), ok. +t_reset_metrics_on_disable(_Config) -> + Params = #{ + <<"description">> => <<"A simple rule">>, + <<"enable">> => true, + <<"actions">> => [#{<<"function">> => <<"console">>}], + <<"sql">> => <<"SELECT * from \"t/1\"">>, + <<"name">> => atom_to_binary(?FUNCTION_NAME) + }, + {201, #{id := RuleId}} = emqx_rule_engine_api:'/rules'(post, #{body => Params}), + + %% generate some fake metrics + emqx_metrics_worker:inc(rule_metrics, RuleId, 'matched', 10), + emqx_metrics_worker:inc(rule_metrics, RuleId, 'passed', 10), + {200, #{metrics := Metrics0}} = emqx_rule_engine_api:'/rules/:id/metrics'( + get, + #{bindings => #{id => RuleId}} + ), + ?assertMatch(#{passed := 10, matched := 10}, Metrics0), + + %% disable the rule; metrics should be reset + {200, _Rule2} = emqx_rule_engine_api:'/rules/:id'(put, #{ + bindings => #{id => RuleId}, + body => Params#{<<"enable">> := false} + }), + + {200, #{metrics := Metrics1}} = emqx_rule_engine_api:'/rules/:id/metrics'( + get, + #{bindings => #{id => RuleId}} + ), + ?assertMatch(#{passed := 0, matched := 0}, Metrics1), + ok. + test_rule_params() -> #{ body => #{ diff --git a/changes/v5.0.14-en.md b/changes/v5.0.14-en.md index f61211bb8..090790a28 100644 --- a/changes/v5.0.14-en.md +++ b/changes/v5.0.14-en.md @@ -9,6 +9,8 @@ - Obfuscated sensitive data in the response when querying `bridges` information by API [#9593](https://github.com/emqx/emqx/pull/9593/). +- Made rule engine behavior more consistent with bridge behavior regarding metrics: if a rule engine is disabled, its metrics are now reset [](). + ## Bug Fixes - Fix an issue where testing the GCP PubSub could leak memory, and an issue where its JWT token would fail to refresh a second time. [#9641](https://github.com/emqx/emqx/pull/9641) diff --git a/changes/v5.0.14-zh.md b/changes/v5.0.14-zh.md index c96bef305..b3bed3d7e 100644 --- a/changes/v5.0.14-zh.md +++ b/changes/v5.0.14-zh.md @@ -9,6 +9,8 @@ - 通过 API 查询 `bridges` 信息时将混淆响应中的敏感数据 [#9593](https://github.com/emqx/emqx/pull/9593/)。 +- 使得规则引擎的行为与桥梁的指标行为更加一致:如果一个规则引擎被禁用,其指标现在会被重置 []()。 + ## 修复 - 修复了测试GCP PubSub可能泄露内存的问题,以及其JWT令牌第二次刷新失败的问题。 [#9640](https://github.com/emqx/emqx/pull/9640) From 5df485df17d8f3ef44852f99b072be3f36a49954 Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Wed, 4 Jan 2023 10:38:21 -0300 Subject: [PATCH 0016/1802] test: attempt to fix flaky test --- lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_gcp_pubsub_SUITE.erl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_gcp_pubsub_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_gcp_pubsub_SUITE.erl index f68c1391f..70125596c 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_gcp_pubsub_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_gcp_pubsub_SUITE.erl @@ -1136,6 +1136,8 @@ do_econnrefused_or_timeout_test(Config, Error) -> wait_telemetry_event(TelemetryTable, success, ResourceId, #{ timeout => 10_000, n_events => 2 }), + wait_until_gauge_is(inflight, 0, _Timeout = 400), + wait_until_gauge_is(queuing, 0, _Timeout = 400), assert_metrics( #{ batching => 0, From 2f13bfd4527074d6a5868c406a531edda371079b Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Wed, 4 Jan 2023 09:47:14 -0300 Subject: [PATCH 0017/1802] fix(retainer): change mnesia table storage types during update https://emqx.atlassian.net/browse/EMQX-8650 --- apps/emqx_retainer/src/emqx_retainer.app.src | 2 +- apps/emqx_retainer/src/emqx_retainer.erl | 23 ++++++--- .../test/emqx_retainer_api_SUITE.erl | 51 +++++++++++++++++++ changes/v5.0.14-en.md | 2 + changes/v5.0.14-zh.md | 2 + 5 files changed, 71 insertions(+), 9 deletions(-) diff --git a/apps/emqx_retainer/src/emqx_retainer.app.src b/apps/emqx_retainer/src/emqx_retainer.app.src index f61468d9b..d151ad4e7 100644 --- a/apps/emqx_retainer/src/emqx_retainer.app.src +++ b/apps/emqx_retainer/src/emqx_retainer.app.src @@ -2,7 +2,7 @@ {application, emqx_retainer, [ {description, "EMQX Retainer"}, % strict semver, bump manually! - {vsn, "5.0.8"}, + {vsn, "5.0.9"}, {modules, []}, {registered, [emqx_retainer_sup]}, {applications, [kernel, stdlib, emqx]}, diff --git a/apps/emqx_retainer/src/emqx_retainer.erl b/apps/emqx_retainer/src/emqx_retainer.erl index aa1260033..b81ea2446 100644 --- a/apps/emqx_retainer/src/emqx_retainer.erl +++ b/apps/emqx_retainer/src/emqx_retainer.erl @@ -321,16 +321,23 @@ update_config( OldConf ) -> #{ - backend := BackendCfg, + backend := #{ + type := BackendType, + storage_type := StorageType + }, msg_clear_interval := ClearInterval } = NewConf, - #{backend := OldBackendCfg} = OldConf, - - StorageType = maps:get(type, BackendCfg), - OldStrorageType = maps:get(type, OldBackendCfg), - case OldStrorageType of - StorageType -> + #{ + backend := #{ + type := OldBackendType, + storage_type := OldStorageType + } + } = OldConf, + SameBackendType = BackendType =:= OldBackendType, + SameStorageType = StorageType =:= OldStorageType, + case SameBackendType andalso SameStorageType of + true -> State#{ clear_timer := check_timer( ClearTimer, @@ -338,7 +345,7 @@ update_config( clear_expired ) }; - _ -> + false -> State2 = disable_retainer(State), enable_retainer(State2, NewConf) end. diff --git a/apps/emqx_retainer/test/emqx_retainer_api_SUITE.erl b/apps/emqx_retainer/test/emqx_retainer_api_SUITE.erl index aee6aa4e4..a64e9a7df 100644 --- a/apps/emqx_retainer/test/emqx_retainer_api_SUITE.erl +++ b/apps/emqx_retainer/test/emqx_retainer_api_SUITE.erl @@ -31,6 +31,7 @@ all() -> emqx_common_test_helpers:all(?MODULE). init_per_suite(Config) -> + emqx_common_test_helpers:clear_screen(), application:load(emqx_conf), ok = ekka:start(), ok = mria_rlog:wait_for_shards([?CLUSTER_RPC_SHARD], infinity), @@ -219,6 +220,56 @@ t_lookup_and_delete(_) -> ok = emqtt:disconnect(C1). +t_change_storage_type(_Config) -> + Path = api_path(["mqtt", "retainer"]), + {ok, ConfJson} = request_api(get, Path), + RawConf = emqx_json:decode(ConfJson, [return_maps]), + %% pre-conditions + ?assertMatch( + #{ + <<"backend">> := #{ + <<"type">> := <<"built_in_database">>, + <<"storage_type">> := <<"ram">> + }, + <<"enable">> := true + }, + RawConf + ), + ?assertEqual(ram_copies, mnesia:table_info(?TAB_INDEX_META, storage_type)), + ?assertEqual(ram_copies, mnesia:table_info(?TAB_MESSAGE, storage_type)), + ?assertEqual(ram_copies, mnesia:table_info(?TAB_INDEX, storage_type)), + + ChangedConf = emqx_map_lib:deep_merge( + RawConf, + #{ + <<"backend">> => + #{<<"storage_type">> => <<"disc">>} + } + ), + {ok, UpdateResJson} = request_api( + put, + Path, + [], + auth_header_(), + ChangedConf + ), + UpdatedRawConf = emqx_json:decode(UpdateResJson, [return_maps]), + ?assertMatch( + #{ + <<"backend">> := #{ + <<"type">> := <<"built_in_database">>, + <<"storage_type">> := <<"disc">> + }, + <<"enable">> := true + }, + UpdatedRawConf + ), + ?assertEqual(disc_copies, mnesia:table_info(?TAB_INDEX_META, storage_type)), + ?assertEqual(disc_copies, mnesia:table_info(?TAB_MESSAGE, storage_type)), + ?assertEqual(disc_copies, mnesia:table_info(?TAB_INDEX, storage_type)), + + ok. + %%-------------------------------------------------------------------- %% HTTP Request %%-------------------------------------------------------------------- diff --git a/changes/v5.0.14-en.md b/changes/v5.0.14-en.md index f61211bb8..35dd836bf 100644 --- a/changes/v5.0.14-en.md +++ b/changes/v5.0.14-en.md @@ -14,3 +14,5 @@ - Fix an issue where testing the GCP PubSub could leak memory, and an issue where its JWT token would fail to refresh a second time. [#9641](https://github.com/emqx/emqx/pull/9641) - Fix the problem of data loss and bad match when the MySQL driver is disconnected [#9638](https://github.com/emqx/emqx/pull/9638). + +- Fixed an issue where changing the storage type of the built-in database retainer would not take effect without restarting the node [#9676](https://github.com/emqx/emqx/pull/9676). diff --git a/changes/v5.0.14-zh.md b/changes/v5.0.14-zh.md index c96bef305..f097b8559 100644 --- a/changes/v5.0.14-zh.md +++ b/changes/v5.0.14-zh.md @@ -14,3 +14,5 @@ - 修复了测试GCP PubSub可能泄露内存的问题,以及其JWT令牌第二次刷新失败的问题。 [#9640](https://github.com/emqx/emqx/pull/9640) - 修复 MySQL 驱动断开连接时出现的数据丢失和匹配错误的问题 [#9638](https://github.com/emqx/emqx/pull/9638)。 + +- 修复了如果不重新启动节点,改变保留消息的存储类型将不会生效的问题 [#9676](https://github.com/emqx/emqx/pull/9676)。 From 51ad27cb4bb9fe3539dc39a0f160582ee43a9fcb Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Wed, 4 Jan 2023 12:11:35 -0300 Subject: [PATCH 0018/1802] test(retainer): assert that retained messages are not lost when changing storage type --- .../src/emqx_retainer_mnesia.erl | 4 +- .../test/emqx_retainer_api_SUITE.erl | 59 +++++++++++++++---- 2 files changed, 51 insertions(+), 12 deletions(-) diff --git a/apps/emqx_retainer/src/emqx_retainer_mnesia.erl b/apps/emqx_retainer/src/emqx_retainer_mnesia.erl index 69a6a877a..cadb9110f 100644 --- a/apps/emqx_retainer/src/emqx_retainer_mnesia.erl +++ b/apps/emqx_retainer/src/emqx_retainer_mnesia.erl @@ -146,7 +146,9 @@ store_retained(_, Msg = #message{topic = Topic}) -> reason => table_is_full }); false -> - do_store_retained(Msg, Tokens, ExpiryTime) + do_store_retained(Msg, Tokens, ExpiryTime), + ?tp(message_retained, #{topic => Topic}), + ok end. clear_expired(_) -> diff --git a/apps/emqx_retainer/test/emqx_retainer_api_SUITE.erl b/apps/emqx_retainer/test/emqx_retainer_api_SUITE.erl index a64e9a7df..ba96887a2 100644 --- a/apps/emqx_retainer/test/emqx_retainer_api_SUITE.erl +++ b/apps/emqx_retainer/test/emqx_retainer_api_SUITE.erl @@ -31,7 +31,6 @@ all() -> emqx_common_test_helpers:all(?MODULE). init_per_suite(Config) -> - emqx_common_test_helpers:clear_screen(), application:load(emqx_conf), ok = ekka:start(), ok = mria_rlog:wait_for_shards([?CLUSTER_RPC_SHARD], infinity), @@ -104,11 +103,12 @@ t_messages(_) -> end, ?check_trace( - ?wait_async_action( - lists:foreach(Each, lists:seq(1, 5)), - #{?snk_kind := message_retained, topic := <<"retained/A">>}, - 500 - ), + {ok, {ok, _}} = + ?wait_async_action( + lists:foreach(Each, lists:seq(1, 5)), + #{?snk_kind := message_retained, topic := <<"retained/A">>}, + 500 + ), [] ), @@ -150,11 +150,12 @@ t_messages_page(_) -> end, ?check_trace( - ?wait_async_action( - lists:foreach(Each, lists:seq(1, 5)), - #{?snk_kind := message_retained, topic := <<"retained/A">>}, - 500 - ), + {ok, {ok, _}} = + ?wait_async_action( + lists:foreach(Each, lists:seq(1, 5)), + #{?snk_kind := message_retained, topic := <<"retained/A">>}, + 500 + ), [] ), Page = 4, @@ -238,6 +239,23 @@ t_change_storage_type(_Config) -> ?assertEqual(ram_copies, mnesia:table_info(?TAB_INDEX_META, storage_type)), ?assertEqual(ram_copies, mnesia:table_info(?TAB_MESSAGE, storage_type)), ?assertEqual(ram_copies, mnesia:table_info(?TAB_INDEX, storage_type)), + %% insert some retained messages + {ok, C0} = emqtt:start_link([{clean_start, true}, {proto_ver, v5}]), + {ok, _} = emqtt:connect(C0), + ok = snabbkaffe:start_trace(), + Topic = <<"retained">>, + Payload = <<"retained">>, + {ok, {ok, _}} = + ?wait_async_action( + emqtt:publish(C0, Topic, Payload, [{qos, 0}, {retain, true}]), + #{?snk_kind := message_retained, topic := Topic}, + 500 + ), + emqtt:stop(C0), + ok = snabbkaffe:stop(), + {ok, MsgsJson0} = request_api(get, api_path(["mqtt", "retainer", "messages"])), + #{data := Msgs0, meta := _} = decode_json(MsgsJson0), + ?assertEqual(1, length(Msgs0)), ChangedConf = emqx_map_lib:deep_merge( RawConf, @@ -267,6 +285,25 @@ t_change_storage_type(_Config) -> ?assertEqual(disc_copies, mnesia:table_info(?TAB_INDEX_META, storage_type)), ?assertEqual(disc_copies, mnesia:table_info(?TAB_MESSAGE, storage_type)), ?assertEqual(disc_copies, mnesia:table_info(?TAB_INDEX, storage_type)), + %% keep retained messages + {ok, MsgsJson1} = request_api(get, api_path(["mqtt", "retainer", "messages"])), + #{data := Msgs1, meta := _} = decode_json(MsgsJson1), + ?assertEqual(1, length(Msgs1)), + {ok, C1} = emqtt:start_link([{clean_start, true}, {proto_ver, v5}]), + {ok, _} = emqtt:connect(C1), + {ok, _, _} = emqtt:subscribe(C1, Topic), + + receive + {publish, #{topic := T, payload := P, retain := R}} -> + ?assertEqual(Payload, P), + ?assertEqual(Topic, T), + ?assert(R), + ok + after 500 -> + emqtt:stop(C1), + ct:fail("should have preserved retained messages") + end, + emqtt:stop(C1), ok. From 93c205db7c87813ed4e5ae30c878943e23b5de0a Mon Sep 17 00:00:00 2001 From: ieQu1 <99872536+ieQu1@users.noreply.github.com> Date: Thu, 5 Jan 2023 12:17:27 +0100 Subject: [PATCH 0019/1802] fix(router_helper): Reduce log verbosity of expected events --- apps/emqx/src/emqx_router_helper.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx/src/emqx_router_helper.erl b/apps/emqx/src/emqx_router_helper.erl index 6dd479323..e2d54b99e 100644 --- a/apps/emqx/src/emqx_router_helper.erl +++ b/apps/emqx/src/emqx_router_helper.erl @@ -144,7 +144,7 @@ handle_info({mnesia_table_event, {delete, {?ROUTING_NODE, _Node}, _}}, State) -> %% ignore {noreply, State}; handle_info({mnesia_table_event, Event}, State) -> - ?SLOG(error, #{msg => "unexpected_mnesia_table_event", event => Event}), + ?SLOG(debug, #{msg => "unexpected_mnesia_table_event", event => Event}), {noreply, State}; handle_info({nodedown, Node}, State = #{nodes := Nodes}) -> global:trans( From 6278d48bd7f3491bd1daf98c88014c204fc003e6 Mon Sep 17 00:00:00 2001 From: ieQu1 <99872536+ieQu1@users.noreply.github.com> Date: Wed, 4 Jan 2023 21:37:38 +0100 Subject: [PATCH 0020/1802] ci: Change how the changelog is created --- .github/PULL_REQUEST_TEMPLATE/v5.md | 2 +- changes/v5.0.14-en.md | 20 ---------- changes/v5.0.14-zh.md | 20 ---------- changes/v5.0.14/feat-9593.en.md | 1 + changes/v5.0.14/feat-9593.zh.md | 1 + changes/v5.0.14/feat-9614.en.md | 4 ++ changes/v5.0.14/feat-9614.zh.md | 4 ++ changes/v5.0.14/feat-9674.en.md | 1 + changes/v5.0.14/feat-9674.zh.md | 1 + changes/v5.0.14/fix-9638.en.md | 1 + changes/v5.0.14/fix-9638.zh.md | 1 + changes/v5.0.14/fix-9641.en.md | 1 + changes/v5.0.14/fix-9641.zh.md | 1 + scripts/format-changelog.sh | 62 +++++++++++++++++++++++++++++ 14 files changed, 79 insertions(+), 41 deletions(-) delete mode 100644 changes/v5.0.14-en.md delete mode 100644 changes/v5.0.14-zh.md create mode 100644 changes/v5.0.14/feat-9593.en.md create mode 100644 changes/v5.0.14/feat-9593.zh.md create mode 100644 changes/v5.0.14/feat-9614.en.md create mode 100644 changes/v5.0.14/feat-9614.zh.md create mode 100644 changes/v5.0.14/feat-9674.en.md create mode 100644 changes/v5.0.14/feat-9674.zh.md create mode 100644 changes/v5.0.14/fix-9638.en.md create mode 100644 changes/v5.0.14/fix-9638.zh.md create mode 100644 changes/v5.0.14/fix-9641.en.md create mode 100644 changes/v5.0.14/fix-9641.zh.md create mode 100755 scripts/format-changelog.sh diff --git a/.github/PULL_REQUEST_TEMPLATE/v5.md b/.github/PULL_REQUEST_TEMPLATE/v5.md index a0db01e1a..d4104ca5d 100644 --- a/.github/PULL_REQUEST_TEMPLATE/v5.md +++ b/.github/PULL_REQUEST_TEMPLATE/v5.md @@ -5,7 +5,7 @@ Please convert it to a draft if any of the following conditions are not met. Rev - [ ] Added tests for the changes - [ ] Changed lines covered in coverage report -- [ ] Change log has been added to `changes/` dir +- [ ] Change log has been added to `changes//(feat|fix)-.en.md` and `.zh.md` files - [ ] For internal contributor: there is a jira ticket to track this change - [ ] If there should be document changes, a PR to emqx-docs.git is sent, or a jira ticket is created to follow up - [ ] Schema changes are backward compatible diff --git a/changes/v5.0.14-en.md b/changes/v5.0.14-en.md deleted file mode 100644 index 4c06da9d8..000000000 --- a/changes/v5.0.14-en.md +++ /dev/null @@ -1,20 +0,0 @@ -# v5.0.14 - -## Enhancements - -- Make possible to configure `host:port` from environment variables without quotes [#9614](https://github.com/emqx/emqx/pull/9614). - Prior to this change, when overriding a `host:port` config value from environment variable, one has to quote it as: - `env EMQX_BRIDGES__MQTT__XYZ__SERVER='"localhost:1883"'`. - Now it's possible to set it without quote as `env EMQX_BRIDGES__MQTT__XYZ__SERVER='localhost:1883'`. - -- Obfuscated sensitive data in the response when querying `bridges` information by API [#9593](https://github.com/emqx/emqx/pull/9593/). - -- Made rule engine behavior more consistent with bridge behavior regarding metrics: if a rule engine is disabled, its metrics are now reset [](). - -## Bug Fixes - -- Fix an issue where testing the GCP PubSub could leak memory, and an issue where its JWT token would fail to refresh a second time. [#9641](https://github.com/emqx/emqx/pull/9641) - -- Fix the problem of data loss and bad match when the MySQL driver is disconnected [#9638](https://github.com/emqx/emqx/pull/9638). - -- Fixed an issue where changing the storage type of the built-in database retainer would not take effect without restarting the node [#9676](https://github.com/emqx/emqx/pull/9676). diff --git a/changes/v5.0.14-zh.md b/changes/v5.0.14-zh.md deleted file mode 100644 index 4d27c7f29..000000000 --- a/changes/v5.0.14-zh.md +++ /dev/null @@ -1,20 +0,0 @@ -# v5.0.14 - -## 增强 - -- 允许环境变量重载 `host:port` 值时不使用引号 [#9614](https://github.com/emqx/emqx/pull/9614)。 - 在此修复前,环境变量中使用 `host:port` 这种配置时,用户必须使用引号,例如: - `env EMQX_BRIDGES__MQTT__XYZ__SERVER='"localhost:1883"'`。 - 此修复后,可以不使用引号,例如 `env EMQX_BRIDGES__MQTT__XYZ__SERVER='localhost:1883'`。 - -- 通过 API 查询 `bridges` 信息时将混淆响应中的敏感数据 [#9593](https://github.com/emqx/emqx/pull/9593/)。 - -- 使得规则引擎的行为与桥梁的指标行为更加一致:如果一个规则引擎被禁用,其指标现在会被重置 []()。 - -## 修复 - -- 修复了测试GCP PubSub可能泄露内存的问题,以及其JWT令牌第二次刷新失败的问题。 [#9640](https://github.com/emqx/emqx/pull/9640) - -- 修复 MySQL 驱动断开连接时出现的数据丢失和匹配错误的问题 [#9638](https://github.com/emqx/emqx/pull/9638)。 - -- 修复了如果不重新启动节点,改变保留消息的存储类型将不会生效的问题 [#9676](https://github.com/emqx/emqx/pull/9676)。 diff --git a/changes/v5.0.14/feat-9593.en.md b/changes/v5.0.14/feat-9593.en.md new file mode 100644 index 000000000..d6db43a79 --- /dev/null +++ b/changes/v5.0.14/feat-9593.en.md @@ -0,0 +1 @@ +Obfuscated sensitive data in the response when querying `bridges` information by API. diff --git a/changes/v5.0.14/feat-9593.zh.md b/changes/v5.0.14/feat-9593.zh.md new file mode 100644 index 000000000..fd3133050 --- /dev/null +++ b/changes/v5.0.14/feat-9593.zh.md @@ -0,0 +1 @@ +通过 API 查询 `bridges` 信息时将混淆响应中的敏感数据。 diff --git a/changes/v5.0.14/feat-9614.en.md b/changes/v5.0.14/feat-9614.en.md new file mode 100644 index 000000000..2ccf62c76 --- /dev/null +++ b/changes/v5.0.14/feat-9614.en.md @@ -0,0 +1,4 @@ +Make possible to configure `host:port` from environment variables without quotes. +Prior to this change, when overriding a `host:port` config value from environment variable, one has to quote it as: +`env EMQX_BRIDGES__MQTT__XYZ__SERVER='"localhost:1883"'`. +Now it's possible to set it without quote as `env EMQX_BRIDGES__MQTT__XYZ__SERVER='localhost:1883'`. diff --git a/changes/v5.0.14/feat-9614.zh.md b/changes/v5.0.14/feat-9614.zh.md new file mode 100644 index 000000000..7f67d92af --- /dev/null +++ b/changes/v5.0.14/feat-9614.zh.md @@ -0,0 +1,4 @@ +允许环境变量重载 `host:port` 值时不使用引号。 +在此修复前,环境变量中使用 `host:port` 这种配置时,用户必须使用引号,例如: +`env EMQX_BRIDGES__MQTT__XYZ__SERVER='"localhost:1883"'`。 +此修复后,可以不使用引号,例如 `env EMQX_BRIDGES__MQTT__XYZ__SERVER='localhost:1883'`。 diff --git a/changes/v5.0.14/feat-9674.en.md b/changes/v5.0.14/feat-9674.en.md new file mode 100644 index 000000000..a87725b43 --- /dev/null +++ b/changes/v5.0.14/feat-9674.en.md @@ -0,0 +1 @@ +Made rule engine behavior more consistent with bridge behavior regarding metrics: if a rule engine is disabled, its metrics are now reset diff --git a/changes/v5.0.14/feat-9674.zh.md b/changes/v5.0.14/feat-9674.zh.md new file mode 100644 index 000000000..c9d776525 --- /dev/null +++ b/changes/v5.0.14/feat-9674.zh.md @@ -0,0 +1 @@ +使得规则引擎的行为与桥梁的指标行为更加一致:如果一个规则引擎被禁用,其指标现在会被重置。 diff --git a/changes/v5.0.14/fix-9638.en.md b/changes/v5.0.14/fix-9638.en.md new file mode 100644 index 000000000..7a9265fb4 --- /dev/null +++ b/changes/v5.0.14/fix-9638.en.md @@ -0,0 +1 @@ +Fix the problem of data loss and bad match when the MySQL driver is disconnected. diff --git a/changes/v5.0.14/fix-9638.zh.md b/changes/v5.0.14/fix-9638.zh.md new file mode 100644 index 000000000..6ae2a3c1e --- /dev/null +++ b/changes/v5.0.14/fix-9638.zh.md @@ -0,0 +1 @@ +修复 MySQL 驱动断开连接时出现的数据丢失和匹配错误的问题。 diff --git a/changes/v5.0.14/fix-9641.en.md b/changes/v5.0.14/fix-9641.en.md new file mode 100644 index 000000000..29924de62 --- /dev/null +++ b/changes/v5.0.14/fix-9641.en.md @@ -0,0 +1 @@ +Fix an issue where testing the GCP PubSub could leak memory, and an issue where its JWT token would fail to refresh a second time. diff --git a/changes/v5.0.14/fix-9641.zh.md b/changes/v5.0.14/fix-9641.zh.md new file mode 100644 index 000000000..7b9e6d55c --- /dev/null +++ b/changes/v5.0.14/fix-9641.zh.md @@ -0,0 +1 @@ +修复了测试GCP PubSub可能泄露内存的问题,以及其JWT令牌第二次刷新失败的问题。 diff --git a/scripts/format-changelog.sh b/scripts/format-changelog.sh new file mode 100755 index 000000000..862b42c17 --- /dev/null +++ b/scripts/format-changelog.sh @@ -0,0 +1,62 @@ +#!/bin/bash +set -euo pipefail +shopt -s nullglob +export LANG=C.UTF-8 + +[ "$#" -ne 2 ] && { + echo "Usage $0 " 1>&2; + exit 1 +} + +version="${1}" +language="${2}" + +changes_dir="$(git rev-parse --show-toplevel)/changes/${version}" + +item() { + local filename pr indent + filename="${1}" + pr="$(echo "${filename}" | sed -E 's/.*-([0-9]+)\.(en|zh)\.md$/\1/')" + indent="- [#${pr}](https://github.com/emqx/emqx/pull/${pr}) " + while read -r line; do + echo "${indent}${line}" + indent=" " + done < "${filename}" + echo +} + +section() { + local section_name=$1 + for i in "${changes_dir}"/"${section_name}"-*."${language}".md; do + item "${i}" + done +} + +if [ "${language}" = "en" ]; then + cat <&2; + exit 1 +fi From e33409132e08b1829b64652d5c2e3a1f9a8d0e5a Mon Sep 17 00:00:00 2001 From: ieQu1 <99872536+ieQu1@users.noreply.github.com> Date: Thu, 5 Jan 2023 11:49:24 +0100 Subject: [PATCH 0021/1802] ci: Add generate_changlelog script to scripts/rel/cut.sh --- scripts/rel/cut.sh | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/scripts/rel/cut.sh b/scripts/rel/cut.sh index d28955bca..d35176600 100755 --- a/scripts/rel/cut.sh +++ b/scripts/rel/cut.sh @@ -206,9 +206,18 @@ if [ -d "${CHECKS_DIR}" ]; then done fi +generate_changelog () { + local CHANGES_EN_MD="changes/${TAG}-en.md" CHANGES_ZH_MD="changes/${TAG}-zh.md" + ./scripts/format-changelog.sh "${TAG}" "en" > "$CHANGES_EN_MD" + ./scripts/format-changelog.sh "${TAG}" "zh" > "$CHANGES_ZH_MD" + git add "$CHANGES_EN_MD" "$CHANGES_ZH_MD" + [ -n "$(git status -s)" ] && git commit -m "chore: Generate changelog for ${TAG}" +} + if [ "$DRYRUN" = 'yes' ]; then logmsg "Release tag is ready to be created with command: git tag $TAG" else + generate_changelog git tag "$TAG" logmsg "$TAG is created OK." fi From 5d9f9671e9974b122256b14d29a46a54e2c33afe Mon Sep 17 00:00:00 2001 From: ieQu1 <99872536+ieQu1@users.noreply.github.com> Date: Tue, 3 Jan 2023 21:13:37 +0100 Subject: [PATCH 0022/1802] feat(emqx_metrics): Sliding window samples --- apps/emqx/src/emqx_metrics_worker.erl | 271 ++++++++++++++++--- apps/emqx/test/emqx_metrics_worker_SUITE.erl | 29 +- changes/v5.0.14/feat-9671.en.md | 1 + changes/v5.0.14/feat-9671.zh.md | 1 + 4 files changed, 253 insertions(+), 49 deletions(-) create mode 100644 changes/v5.0.14/feat-9671.en.md create mode 100644 changes/v5.0.14/feat-9671.zh.md diff --git a/apps/emqx/src/emqx_metrics_worker.erl b/apps/emqx/src/emqx_metrics_worker.erl index 241ba599f..5f41346cb 100644 --- a/apps/emqx/src/emqx_metrics_worker.erl +++ b/apps/emqx/src/emqx_metrics_worker.erl @@ -31,6 +31,7 @@ -export([ inc/3, inc/4, + observe/4, get/3, get_gauge/3, set_gauge/5, @@ -38,6 +39,8 @@ get_gauges/2, delete_gauges/2, get_rate/2, + get_slide/2, + get_slide/3, get_counters/2, create_metrics/3, create_metrics/4, @@ -67,7 +70,16 @@ -define(SAMPLING, 1). -endif. --export_type([metrics/0, handler_name/0, metric_id/0]). +-export_type([metrics/0, handler_name/0, metric_id/0, metric_spec/0]). + +% Default +-type metric_type() :: + %% Simple counter + counter + %% Sliding window average + | slide. + +-type metric_spec() :: {metric_type(), atom()}. -type rate() :: #{ current := float(), @@ -77,6 +89,7 @@ -type metrics() :: #{ counters := #{metric_name() => integer()}, gauges := #{metric_name() => integer()}, + slides := #{metric_name() => number()}, rate := #{metric_name() => rate()} }. -type handler_name() :: atom(). @@ -103,9 +116,22 @@ last5m_smpl = [] :: list() }). +-record(slide_datapoint, { + sum :: non_neg_integer(), + samples :: non_neg_integer(), + time :: non_neg_integer() +}). + +-record(slide, { + %% Total number of samples through the history + n_samples = 0 :: non_neg_integer(), + datapoints = [] :: [#slide_datapoint{}] +}). + -record(state, { metric_ids = sets:new(), - rates :: undefined | #{metric_id() => #rate{}} + rates :: #{metric_id() => #{metric_name() => #rate{}}} | undefined, + slides = #{} :: #{metric_id() => #{metric_name() => #slide{}}} }). %%------------------------------------------------------------------------------ @@ -126,14 +152,18 @@ child_spec(ChldName, Name) -> modules => [emqx_metrics_worker] }. --spec create_metrics(handler_name(), metric_id(), [metric_name()]) -> ok | {error, term()}. +-spec create_metrics(handler_name(), metric_id(), [metric_spec() | metric_name()]) -> + ok | {error, term()}. create_metrics(Name, Id, Metrics) -> - create_metrics(Name, Id, Metrics, Metrics). + Metrics1 = desugar(Metrics), + Counters = filter_counters(Metrics1), + create_metrics(Name, Id, Metrics1, Counters). --spec create_metrics(handler_name(), metric_id(), [metric_name()], [metric_name()]) -> +-spec create_metrics(handler_name(), metric_id(), [metric_spec() | metric_name()], [atom()]) -> ok | {error, term()}. create_metrics(Name, Id, Metrics, RateMetrics) -> - gen_server:call(Name, {create_metrics, Id, Metrics, RateMetrics}). + Metrics1 = desugar(Metrics), + gen_server:call(Name, {create_metrics, Id, Metrics1, RateMetrics}). -spec clear_metrics(handler_name(), metric_id()) -> ok. clear_metrics(Name, Id) -> @@ -156,7 +186,7 @@ get(Name, Id, Metric) -> not_found -> 0; Ref when is_atom(Metric) -> - counters:get(Ref, idx_metric(Name, Id, Metric)); + counters:get(Ref, idx_metric(Name, Id, counter, Metric)); Ref when is_integer(Metric) -> counters:get(Ref, Metric) end. @@ -171,21 +201,37 @@ get_counters(Name, Id) -> fun(_Metric, Index) -> get(Name, Id, Index) end, - get_indexes(Name, Id) + get_indexes(Name, counter, Id) ). +-spec get_slide(handler_name(), metric_id()) -> map(). +get_slide(Name, Id) -> + gen_server:call(Name, {get_slide, Id}). + +%% Get the average for a specified sliding window period. +%% +%% It will only account for the samples recorded in the past `Window' seconds. +-spec get_slide(handler_name(), metric_id(), non_neg_integer()) -> number(). +get_slide(Name, Id, Window) -> + gen_server:call(Name, {get_slide, Id, Window}). + -spec reset_counters(handler_name(), metric_id()) -> ok. reset_counters(Name, Id) -> - Indexes = maps:values(get_indexes(Name, Id)), - Ref = get_ref(Name, Id), - lists:foreach(fun(Idx) -> counters:put(Ref, Idx, 0) end, Indexes). + case get_ref(Name, Id) of + not_found -> + ok; + Ref -> + #{size := Size} = counters:info(Ref), + lists:foreach(fun(Idx) -> counters:put(Ref, Idx, 0) end, lists:seq(1, Size)) + end. -spec get_metrics(handler_name(), metric_id()) -> metrics(). get_metrics(Name, Id) -> #{ rate => get_rate(Name, Id), counters => get_counters(Name, Id), - gauges => get_gauges(Name, Id) + gauges => get_gauges(Name, Id), + slides => get_slide(Name, Id) }. -spec inc(handler_name(), metric_id(), atom()) -> ok. @@ -194,7 +240,37 @@ inc(Name, Id, Metric) -> -spec inc(handler_name(), metric_id(), metric_name(), integer()) -> ok. inc(Name, Id, Metric, Val) -> - counters:add(get_ref(Name, Id), idx_metric(Name, Id, Metric), Val). + counters:add(get_ref(Name, Id), idx_metric(Name, Id, counter, Metric), Val). + +%% Add a sample to the slide. +%% +%% Slide is short for "sliding window average" type of metric. +%% +%% It allows to monitor an average of some observed values in time, +%% and it's mainly used for performance analysis. For example, it can +%% be used to report run time of operations. +%% +%% Consider an example: +%% +%% ``` +%% emqx_metrics_worker:create_metrics(Name, Id, [{slide, a}]), +%% emqx_metrics_worker:observe(Name, Id, a, 10), +%% emqx_metrics_worker:observe(Name, Id, a, 30), +%% #{a := 20} = emqx_metrics_worker:get_slide(Name, Id, _Window = 1). +%% ''' +%% +%% After recording 2 samples, this metric becomes 20 (the average of 10 and 30). +%% +%% But after 1 second it becomes 0 again, unless new samples are recorded. +%% +-spec observe(handler_name(), metric_id(), atom(), integer()) -> ok. +observe(Name, Id, Metric, Val) -> + #{ref := CRef, slide := Idx} = maps:get(Id, get_pterm(Name)), + Index = maps:get(Metric, Idx), + %% Update sum: + counters:add(CRef, Index, Val), + %% Update number of samples: + counters:add(CRef, Index + 1, 1). -spec set_gauge(handler_name(), metric_id(), worker_id(), metric_name(), integer()) -> ok. set_gauge(Name, Id, WorkerId, Metric, Val) -> @@ -300,9 +376,9 @@ handle_call({get_rate, Id}, _From, State = #state{rates = Rates}) -> handle_call( {create_metrics, Id, Metrics, RateMetrics}, _From, - State = #state{metric_ids = MIDs, rates = Rates} + State = #state{metric_ids = MIDs, rates = Rates, slides = Slides} ) -> - case RateMetrics -- Metrics of + case RateMetrics -- filter_counters(Metrics) of [] -> RatePerId = maps:from_list([{M, #rate{}} || M <- RateMetrics]), Rate1 = @@ -310,9 +386,11 @@ handle_call( undefined -> #{Id => RatePerId}; _ -> Rates#{Id => RatePerId} end, + Slides1 = Slides#{Id => create_slides(Metrics)}, {reply, create_counters(get_self_name(), Id, Metrics), State#state{ metric_ids = sets:add_element(Id, MIDs), - rates = Rate1 + rates = Rate1, + slides = Slides1 }}; _ -> {reply, {error, not_super_set_of, {RateMetrics, Metrics}}, State} @@ -320,7 +398,7 @@ handle_call( handle_call( {delete_metrics, Id}, _From, - State = #state{metric_ids = MIDs, rates = Rates} + State = #state{metric_ids = MIDs, rates = Rates, slides = Slides} ) -> Name = get_self_name(), delete_counters(Name, Id), @@ -331,29 +409,43 @@ handle_call( case Rates of undefined -> undefined; _ -> maps:remove(Id, Rates) - end + end, + slides = maps:remove(Id, Slides) }}; handle_call( {reset_metrics, Id}, _From, - State = #state{rates = Rates} + State = #state{rates = Rates, slides = Slides} ) -> - Name = get_self_name(), - delete_gauges(Name, Id), - {reply, reset_counters(Name, Id), State#state{ + delete_gauges(get_self_name(), Id), + NewRates = + case Rates of + undefined -> + undefined; + _ -> + ResetRate = + maps:map( + fun(_Key, _Value) -> #rate{} end, + maps:get(Id, Rates, #{}) + ), + maps:put(Id, ResetRate, Rates) + end, + SlideSpecs = [{slide, I} || I <- maps:keys(maps:get(Id, Slides, #{}))], + NewSlides = Slides#{Id => create_slides(SlideSpecs)}, + {reply, reset_counters(get_self_name(), Id), State#state{ rates = - case Rates of - undefined -> - undefined; - _ -> - ResetRate = - maps:map( - fun(_Key, _Value) -> #rate{} end, - maps:get(Id, Rates, #{}) - ), - maps:put(Id, ResetRate, Rates) - end + NewRates, + slides = NewSlides }}; +handle_call({get_slide, Id}, _From, State = #state{slides = Slides}) -> + SlidesForID = maps:get(Id, Slides, #{}), + {reply, maps:map(fun(Metric, Slide) -> do_get_slide(Id, Metric, Slide) end, SlidesForID), + State}; +handle_call({get_slide, Id, Window}, _From, State = #state{slides = Slides}) -> + SlidesForID = maps:get(Id, Slides, #{}), + {reply, + maps:map(fun(Metric, Slide) -> do_get_slide(Window, Id, Metric, Slide) end, SlidesForID), + State}; handle_call(_Request, _From, State) -> {reply, ok, State}. @@ -363,7 +455,7 @@ handle_cast(_Msg, State) -> handle_info(ticking, State = #state{rates = undefined}) -> erlang:send_after(timer:seconds(?SAMPLING), self(), ticking), {noreply, State}; -handle_info(ticking, State = #state{rates = Rates0}) -> +handle_info(ticking, State = #state{rates = Rates0, slides = Slides0}) -> Rates = maps:map( fun(Id, RatesPerID) -> @@ -376,8 +468,20 @@ handle_info(ticking, State = #state{rates = Rates0}) -> end, Rates0 ), + Slides = + maps:map( + fun(Id, SlidesPerID) -> + maps:map( + fun(Metric, Slide) -> + update_slide(Id, Metric, Slide) + end, + SlidesPerID + ) + end, + Slides0 + ), erlang:send_after(timer:seconds(?SAMPLING), self(), ticking), - {noreply, State#state{rates = Rates}}; + {noreply, State#state{rates = Rates, slides = Slides}}; handle_info(_Info, State) -> {noreply, State}. @@ -408,17 +512,18 @@ create_counters(_Name, _Id, []) -> error({create_counter_error, must_provide_a_list_of_metrics}); create_counters(Name, Id, Metrics) -> %% backup the old counters - OlderCounters = maps:with(Metrics, get_counters(Name, Id)), + OlderCounters = maps:with(filter_counters(Metrics), get_counters(Name, Id)), %% create the new counter - Size = length(Metrics), - Indexes = maps:from_list(lists:zip(Metrics, lists:seq(1, Size))), + {Size, Indexes} = create_metric_indexes(Metrics), Counters = get_pterm(Name), CntrRef = counters:new(Size, [write_concurrency]), persistent_term:put( ?CntrRef(Name), - Counters#{Id => #{ref => CntrRef, indexes => Indexes}} + Counters#{Id => Indexes#{ref => CntrRef}} ), - %% restore the old counters + %% Restore the old counters. Slides are not restored, since they + %% are periodically zeroed anyway. We do lose some samples in the + %% current interval, but that's acceptable for now. lists:foreach( fun({Metric, N}) -> inc(Name, Id, Metric, N) @@ -426,6 +531,16 @@ create_counters(Name, Id, Metrics) -> maps:to_list(OlderCounters) ). +create_metric_indexes(Metrics) -> + create_metric_indexes(Metrics, 1, [], []). + +create_metric_indexes([], Size, Counters, Slides) -> + {Size, #{counter => maps:from_list(Counters), slide => maps:from_list(Slides)}}; +create_metric_indexes([{counter, Id} | Rest], Index, Counters, Slides) -> + create_metric_indexes(Rest, Index + 1, [{Id, Index} | Counters], Slides); +create_metric_indexes([{slide, Id} | Rest], Index, Counters, Slides) -> + create_metric_indexes(Rest, Index + 2, Counters, [{Id, Index} | Slides]). + delete_counters(Name, Id) -> persistent_term:put(?CntrRef(Name), maps:remove(Id, get_pterm(Name))). @@ -435,12 +550,12 @@ get_ref(Name, Id) -> error -> not_found end. -idx_metric(Name, Id, Metric) -> - maps:get(Metric, get_indexes(Name, Id)). +idx_metric(Name, Id, Type, Metric) -> + maps:get(Metric, get_indexes(Name, Type, Id)). -get_indexes(Name, Id) -> +get_indexes(Name, Type, Id) -> case maps:find(Id, get_pterm(Name)) of - {ok, #{indexes := Indexes}} -> Indexes; + {ok, #{Type := Indexes}} -> Indexes; error -> #{} end. @@ -488,6 +603,53 @@ calculate_rate(CurrVal, #rate{ tick = Tick + 1 }. +do_get_slide(Id, Metric, S = #slide{n_samples = NSamples}) -> + #{ + n_samples => NSamples, + current => do_get_slide(2, Id, Metric, S), + last5m => do_get_slide(?SECS_5M, Id, Metric, S) + }. + +do_get_slide(Window, Id, Metric, #slide{datapoints = DP0}) -> + Datapoint = get_slide_datapoint(Id, Metric), + {N, Sum} = get_slide_window(os:system_time(second) - Window, [Datapoint | DP0], 0, 0), + case N > 0 of + true -> Sum div N; + false -> 0 + end. + +get_slide_window(_StartTime, [], N, S) -> + {N, S}; +get_slide_window(StartTime, [#slide_datapoint{time = T} | _], N, S) when T < StartTime -> + {N, S}; +get_slide_window(StartTime, [#slide_datapoint{samples = N, sum = S} | Rest], AccN, AccS) -> + get_slide_window(StartTime, Rest, AccN + N, AccS + S). + +get_slide_datapoint(Id, Metric) -> + Name = get_self_name(), + CRef = get_ref(Name, Id), + Index = idx_metric(Name, Id, slide, Metric), + Total = counters:get(CRef, Index), + N = counters:get(CRef, Index + 1), + #slide_datapoint{ + sum = Total, + samples = N, + time = os:system_time(second) + }. + +update_slide(Id, Metric, Slide0 = #slide{n_samples = NSamples, datapoints = DPs}) -> + Datapoint = get_slide_datapoint(Id, Metric), + %% Reset counters: + Name = get_self_name(), + CRef = get_ref(Name, Id), + Index = idx_metric(Name, Id, slide, Metric), + counters:put(CRef, Index, 0), + counters:put(CRef, Index + 1, 0), + Slide0#slide{ + datapoints = [Datapoint | lists:droplast(DPs)], + n_samples = Datapoint#slide_datapoint.samples + NSamples + }. + format_rates_of_id(RatesPerId) -> maps:map( fun(_Metric, Rates) -> @@ -510,6 +672,27 @@ precision(Float, N) -> Base = math:pow(10, N), round(Float * Base) / Base. +desugar(Metrics) -> + lists:map( + fun + (Atom) when is_atom(Atom) -> + {counter, Atom}; + (Spec = {_, _}) -> + Spec + end, + Metrics + ). + +filter_counters(Metrics) -> + [K || {counter, K} <- Metrics]. + +create_slides(Metrics) -> + EmptyDatapoints = [ + #slide_datapoint{sum = 0, samples = 0, time = 0} + || _ <- lists:seq(1, ?SECS_5M div ?SAMPLING) + ], + maps:from_list([{K, #slide{datapoints = EmptyDatapoints}} || {slide, K} <- Metrics]). + get_self_name() -> {registered_name, Name} = process_info(self(), registered_name), Name. diff --git a/apps/emqx/test/emqx_metrics_worker_SUITE.erl b/apps/emqx/test/emqx_metrics_worker_SUITE.erl index 113e8650f..194c9cc99 100644 --- a/apps/emqx/test/emqx_metrics_worker_SUITE.erl +++ b/apps/emqx/test/emqx_metrics_worker_SUITE.erl @@ -46,7 +46,7 @@ end_per_testcase(_, _Config) -> ok. t_get_metrics(_) -> - Metrics = [a, b, c], + Metrics = [a, b, c, {slide, d}], Id = <<"testid">>, ok = emqx_metrics_worker:create_metrics(?NAME, Id, Metrics), %% all the metrics are set to zero at start @@ -73,6 +73,8 @@ t_get_metrics(_) -> ok = emqx_metrics_worker:set_gauge(?NAME, Id, worker_id0, inflight, 5), ok = emqx_metrics_worker:set_gauge(?NAME, Id, worker_id1, inflight, 7), ok = emqx_metrics_worker:set_gauge(?NAME, Id, worker_id2, queuing, 9), + ok = emqx_metrics_worker:observe(?NAME, Id, d, 10), + ok = emqx_metrics_worker:observe(?NAME, Id, d, 30), ct:sleep(1500), ?LET( #{ @@ -89,6 +91,9 @@ t_get_metrics(_) -> a := 1, b := 1, c := 2 + } = Counters, + slides := #{ + d := #{n_samples := 2, last5m := 20, current := _} } }, emqx_metrics_worker:get_metrics(?NAME, Id), @@ -100,7 +105,8 @@ t_get_metrics(_) -> ?assert(MaxB > 0), ?assert(MaxC > 0), ?assert(Inflight == 12), - ?assert(Queuing == 9) + ?assert(Queuing == 9), + ?assertNot(maps:is_key(d, Counters)) } ), ok = emqx_metrics_worker:clear_metrics(?NAME, Id). @@ -117,6 +123,7 @@ t_clear_metrics(_Config) -> c := #{current := 0.0, max := 0.0, last5m := 0.0} }, gauges := #{}, + slides := #{}, counters := #{ a := 0, b := 0, @@ -138,14 +145,15 @@ t_clear_metrics(_Config) -> #{ counters => #{}, gauges => #{}, - rate => #{current => 0.0, last5m => 0.0, max => 0.0} + rate => #{current => 0.0, last5m => 0.0, max => 0.0}, + slides => #{} }, emqx_metrics_worker:get_metrics(?NAME, Id) ), ok. t_reset_metrics(_) -> - Metrics = [a, b, c], + Metrics = [a, b, c, {slide, d}], Id = <<"testid">>, ok = emqx_metrics_worker:create_metrics(?NAME, Id, Metrics), %% all the metrics are set to zero at start @@ -161,6 +169,9 @@ t_reset_metrics(_) -> a := 0, b := 0, c := 0 + }, + slides := #{ + d := #{n_samples := 0, last5m := 0, current := 0} } }, emqx_metrics_worker:get_metrics(?NAME, Id) @@ -172,7 +183,12 @@ t_reset_metrics(_) -> ok = emqx_metrics_worker:set_gauge(?NAME, Id, worker_id0, inflight, 5), ok = emqx_metrics_worker:set_gauge(?NAME, Id, worker_id1, inflight, 7), ok = emqx_metrics_worker:set_gauge(?NAME, Id, worker_id2, queuing, 9), + ok = emqx_metrics_worker:observe(?NAME, Id, d, 100), + ok = emqx_metrics_worker:observe(?NAME, Id, d, 200), ct:sleep(1500), + ?assertMatch( + #{d := #{n_samples := 2}}, emqx_metrics_worker:get_slide(?NAME, <<"testid">>) + ), ok = emqx_metrics_worker:reset_metrics(?NAME, Id), ?LET( #{ @@ -186,6 +202,9 @@ t_reset_metrics(_) -> a := 0, b := 0, c := 0 + }, + slides := #{ + d := #{n_samples := 0, last5m := 0, current := 0} } }, emqx_metrics_worker:get_metrics(?NAME, Id), @@ -202,7 +221,7 @@ t_reset_metrics(_) -> ok = emqx_metrics_worker:clear_metrics(?NAME, Id). t_get_metrics_2(_) -> - Metrics = [a, b, c], + Metrics = [a, b, c, {slide, d}], Id = <<"testid">>, ok = emqx_metrics_worker:create_metrics( ?NAME, diff --git a/changes/v5.0.14/feat-9671.en.md b/changes/v5.0.14/feat-9671.en.md new file mode 100644 index 000000000..dd5ed5e4d --- /dev/null +++ b/changes/v5.0.14/feat-9671.en.md @@ -0,0 +1 @@ +Implement sliding window average metrics. diff --git a/changes/v5.0.14/feat-9671.zh.md b/changes/v5.0.14/feat-9671.zh.md new file mode 100644 index 000000000..cbcae01fe --- /dev/null +++ b/changes/v5.0.14/feat-9671.zh.md @@ -0,0 +1 @@ +实施滑动窗口平均度量。 From 5bd9f110d669fb75998cd043e2cfea08f0eecc54 Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Mon, 2 Jan 2023 13:57:44 -0300 Subject: [PATCH 0023/1802] test: attempt to reduce flakiness --- .../test/emqx_ee_bridge_redis_SUITE.erl | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_redis_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_redis_SUITE.erl index 9f597e101..6286a3718 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_redis_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_redis_SUITE.erl @@ -220,16 +220,24 @@ t_check_replay(Config) -> begin ?wait_async_action( with_down_failure(Config, ProxyName, fun() -> - ct:sleep(100), - lists:foreach( - fun(_) -> - _ = publish_message(Topic, <<"test_payload">>) - end, - lists:seq(1, ?BATCH_SIZE) - ) + {_, {ok, _}} = + ?wait_async_action( + lists:foreach( + fun(_) -> + _ = publish_message(Topic, <<"test_payload">>) + end, + lists:seq(1, ?BATCH_SIZE) + ), + #{ + ?snk_kind := redis_ee_connector_send_done, + batch := true, + result := {error, _} + }, + 10_000 + ) end), #{?snk_kind := redis_ee_connector_send_done, batch := true, result := {ok, _}}, - 10000 + 10_000 ) end, fun(Trace) -> From 8e59319bfe255e617c54b9d778c6ab4f920a9f61 Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Mon, 2 Jan 2023 15:04:53 -0300 Subject: [PATCH 0024/1802] fix(kafka_producer): fix message loss when kafka connection is down --- .../kafka/emqx_bridge_impl_kafka_producer.erl | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/lib-ee/emqx_ee_bridge/src/kafka/emqx_bridge_impl_kafka_producer.erl b/lib-ee/emqx_ee_bridge/src/kafka/emqx_bridge_impl_kafka_producer.erl index 6b46de35e..666ab3f8e 100644 --- a/lib-ee/emqx_ee_bridge/src/kafka/emqx_bridge_impl_kafka_producer.erl +++ b/lib-ee/emqx_ee_bridge/src/kafka/emqx_bridge_impl_kafka_producer.erl @@ -176,10 +176,22 @@ on_kafka_ack(_Partition, _Offset, _Extra) -> %% Maybe need to bump some counters? ok. -on_get_status(_InstId, #{client_id := ClientID}) -> - case wolff:check_connectivity(ClientID) of - ok -> connected; - _ -> disconnected +on_get_status(_InstId, #{producers := Producers}) -> + %% Just to pick some producer. + RandomVal = emqx_guid:gen(), + FakeMsg = [#{key => RandomVal, value => RandomVal}], + %% Note: we must not check the connectivity to Kafka itself, but + %% only if there are producers available. Otherwise, if Kafka + %% goes down, the resource will be considered down and messages + %% *will not* be sent to the wolff producers and buffered, + %% effectively losing the message as there are no buffer workers + %% for Kafka producer. + try wolff_producers:pick_producer(Producers, FakeMsg) of + {_Partition, _Pid} -> + connected + catch + error:{producer_down, _} -> + disconnected end. %% Parse comma separated host:port list into a [{Host,Port}] list From 0fd8880d0a36b654e5d5e6209205dc0874995498 Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Mon, 2 Jan 2023 16:08:26 -0300 Subject: [PATCH 0025/1802] fix(kafka_producer): avoid multiplication of metrics when bridge is recreated --- .../kafka/emqx_bridge_impl_kafka_producer.erl | 63 ++++++++----------- 1 file changed, 26 insertions(+), 37 deletions(-) diff --git a/lib-ee/emqx_ee_bridge/src/kafka/emqx_bridge_impl_kafka_producer.erl b/lib-ee/emqx_ee_bridge/src/kafka/emqx_bridge_impl_kafka_producer.erl index 666ab3f8e..a012a7ab0 100644 --- a/lib-ee/emqx_ee_bridge/src/kafka/emqx_bridge_impl_kafka_producer.erl +++ b/lib-ee/emqx_ee_bridge/src/kafka/emqx_bridge_impl_kafka_producer.erl @@ -1,8 +1,10 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved. %%-------------------------------------------------------------------- -module(emqx_bridge_impl_kafka_producer). +-include_lib("emqx_resource/include/emqx_resource.hrl"). + %% callbacks of behaviour emqx_resource -export([ callback_mode/0, @@ -36,7 +38,7 @@ on_start(InstId, Config) -> %% TODO: change this to `kafka_producer` after refactoring for kafka_consumer BridgeType = kafka, ResourceID = emqx_bridge_resource:resource_id(BridgeType, BridgeName), - _ = maybe_install_wolff_telemetry_handlers(InstId, ResourceID), + _ = maybe_install_wolff_telemetry_handlers(ResourceID), %% it's a bug if producer config is not found %% the caller should not try to start a producer if %% there is no producer config @@ -79,7 +81,8 @@ on_start(InstId, Config) -> {ok, #{ message_template => compile_message_template(MessageTemplate), client_id => ClientId, - producers => Producers + producers => Producers, + resource_id => ResourceID }}; {error, Reason2} -> ?SLOG(error, #{ @@ -105,7 +108,7 @@ on_start(InstId, Config) -> throw(failed_to_start_kafka_producer) end. -on_stop(InstanceID, #{client_id := ClientID, producers := Producers}) -> +on_stop(_InstanceID, #{client_id := ClientID, producers := Producers, resource_id := ResourceID}) -> _ = with_log_at_error( fun() -> wolff:stop_and_delete_supervised_producers(Producers) end, #{ @@ -121,7 +124,7 @@ on_stop(InstanceID, #{client_id := ClientID, producers := Producers}) -> } ), with_log_at_error( - fun() -> uninstall_telemetry_handlers(InstanceID) end, + fun() -> uninstall_telemetry_handlers(ResourceID) end, #{ msg => "failed_to_uninstall_telemetry_handlers", client_id => ClientID @@ -176,27 +179,14 @@ on_kafka_ack(_Partition, _Offset, _Extra) -> %% Maybe need to bump some counters? ok. -on_get_status(_InstId, #{producers := Producers}) -> - %% Just to pick some producer. - RandomVal = emqx_guid:gen(), - FakeMsg = [#{key => RandomVal, value => RandomVal}], - %% Note: we must not check the connectivity to Kafka itself, but - %% only if there are producers available. Otherwise, if Kafka - %% goes down, the resource will be considered down and messages - %% *will not* be sent to the wolff producers and buffered, - %% effectively losing the message as there are no buffer workers - %% for Kafka producer. - try wolff_producers:pick_producer(Producers, FakeMsg) of - {_Partition, _Pid} -> - connected - catch - error:{producer_down, _} -> - disconnected - end. +on_get_status(_InstId, _State) -> + connected. %% Parse comma separated host:port list into a [{Host,Port}] list -hosts(Hosts) -> - emqx_schema:parse_servers(Hosts, emqx_ee_bridge_kafka:host_opts()). +hosts(Hosts) when is_binary(Hosts) -> + hosts(binary_to_list(Hosts)); +hosts(Hosts) when is_list(Hosts) -> + kpro:parse_endpoints(Hosts). %% Extra socket options, such as sndbuf size etc. socket_opts(Opts) when is_map(Opts) -> @@ -252,14 +242,10 @@ producers_config(BridgeName, ClientId, Input) -> mode := BufferMode, per_partition_limit := PerPartitionLimit, segment_bytes := SegmentBytes, - memory_overload_protection := MemOLP0 + memory_overload_protection := MemOLP } } = Input, - MemOLP = - case os:type() of - {unix, linux} -> MemOLP0; - _ -> false - end, + {OffloadMode, ReplayqDir} = case BufferMode of memory -> {false, false}; @@ -396,20 +382,23 @@ handle_telemetry_event(_EventId, _Metrics, _MetaData, _HandlerConfig) -> %% Event that we do not handle ok. --spec telemetry_handler_id(emqx_resource:resource_id()) -> binary(). -telemetry_handler_id(InstanceID) -> - <<"emqx-bridge-kafka-producer-", InstanceID/binary>>. +%% Note: don't use the instance/manager ID, as that changes everytime +%% the bridge is recreated, and will lead to multiplication of +%% metrics. +-spec telemetry_handler_id(resource_id()) -> binary(). +telemetry_handler_id(ResourceID) -> + <<"emqx-bridge-kafka-producer-", ResourceID/binary>>. -uninstall_telemetry_handlers(InstanceID) -> - HandlerID = telemetry_handler_id(InstanceID), +uninstall_telemetry_handlers(ResourceID) -> + HandlerID = telemetry_handler_id(ResourceID), telemetry:detach(HandlerID). -maybe_install_wolff_telemetry_handlers(InstanceID, ResourceID) -> +maybe_install_wolff_telemetry_handlers(ResourceID) -> %% Attach event handlers for Kafka telemetry events. If a handler with the %% handler id already exists, the attach_many function does nothing telemetry:attach_many( %% unique handler id - telemetry_handler_id(InstanceID), + telemetry_handler_id(ResourceID), [ [wolff, dropped], [wolff, dropped_queue_full], From bf3983e7c46e54a7e68ba713291624f35a430c1a Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Tue, 27 Dec 2022 11:23:13 -0300 Subject: [PATCH 0026/1802] feat(buffer_worker): use offload mode for `replayq` To avoid confusion for the users as to what persistence guarantees we offer when buffering bridges/resources, we will always enable offload mode for `replayq`. With this, when the buffer size is above the max segment size, it'll flush the queue to disk, but on recovery after a restart it'll clean the existing segments rather than resuming from them. --- apps/emqx_resource/src/emqx_resource_worker.erl | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/emqx_resource/src/emqx_resource_worker.erl b/apps/emqx_resource/src/emqx_resource_worker.erl index 5f7cdf7e0..f04f7b1bd 100644 --- a/apps/emqx_resource/src/emqx_resource_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_worker.erl @@ -139,10 +139,13 @@ init({Id, Index, Opts}) -> true -> replayq:open(#{ dir => disk_queue_dir(Id, Index), - seg_bytes => SegBytes, + marshaller => fun ?MODULE:queue_item_marshaller/1, max_total_bytes => TotalBytes, - sizer => fun ?MODULE:estimate_size/1, - marshaller => fun ?MODULE:queue_item_marshaller/1 + %% we don't want to retain the queue after + %% resource restarts. + offload => true, + seg_bytes => SegBytes, + sizer => fun ?MODULE:estimate_size/1 }); false -> undefined From fd360ac6c0b52e9e39e3b3f130fd2ff431a2c3d1 Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Thu, 29 Dec 2022 14:16:17 -0300 Subject: [PATCH 0027/1802] feat(buffer_worker): refactor buffer/resource workers to always use queue This makes the buffer/resource workers always use `replayq` for queuing, along with collecting multiple requests in a single call. This is done to avoid long message queues for the buffer workers and rely on `replayq`'s capabilities of offloading to disk and detecting overflow. Also, this deprecates the `enable_batch` and `enable_queue` resource creation options, as: i) queuing is now always enables; ii) batch_size > 1 <=> batch_enabled. The corresponding metric `dropped.queue_not_enabled` is dropped, along with `batching`. The batching is too ephemeral, especially considering a default batch time of 20 ms, and is not shown in the dashboard, so it was removed. --- Makefile | 1 + apps/emqx/test/emqx_common_test_helpers.erl | 7 +- apps/emqx_bridge/i18n/emqx_bridge_schema.conf | 23 +- apps/emqx_bridge/include/emqx_bridge.hrl | 10 +- apps/emqx_bridge/src/emqx_bridge_api.erl | 14 +- .../src/schema/emqx_bridge_mqtt_config.erl | 1 - .../src/schema/emqx_bridge_schema.erl | 5 +- .../test/emqx_bridge_mqtt_SUITE.erl | 1 - .../i18n/emqx_resource_schema_i18n.conf | 4 +- apps/emqx_resource/include/emqx_resource.hrl | 4 +- .../src/emqx_resource_manager.erl | 1 - .../src/emqx_resource_metrics.erl | 58 -- .../src/emqx_resource_worker.erl | 629 +++++++++++------- .../src/schema/emqx_resource_schema.erl | 2 + .../test/emqx_connector_demo.erl | 55 +- .../test/emqx_resource_SUITE.erl | 537 +++++++++++++-- changes/v5.0.14/feat-9642.en.md | 1 + changes/v5.0.14/feat-9642.zh.md | 1 + changes/v5.0.14/fix-9642.en.md | 3 + changes/v5.0.14/fix-9642.zh.md | 3 + .../src/emqx_ee_bridge_mysql.erl | 2 - .../src/emqx_ee_bridge_redis.erl | 3 +- .../emqx_bridge_impl_kafka_producer_SUITE.erl | 4 - .../test/emqx_ee_bridge_gcp_pubsub_SUITE.erl | 83 +-- .../test/emqx_ee_bridge_influxdb_SUITE.erl | 31 +- .../test/emqx_ee_bridge_mysql_SUITE.erl | 26 +- .../test/emqx_ee_bridge_redis_SUITE.erl | 10 +- scripts/ct/run.sh | 1 + 28 files changed, 1012 insertions(+), 508 deletions(-) create mode 100644 changes/v5.0.14/feat-9642.en.md create mode 100644 changes/v5.0.14/feat-9642.zh.md create mode 100644 changes/v5.0.14/fix-9642.en.md create mode 100644 changes/v5.0.14/fix-9642.zh.md diff --git a/Makefile b/Makefile index c2b33786b..79107cba9 100644 --- a/Makefile +++ b/Makefile @@ -88,6 +88,7 @@ define gen-app-ct-target $1-ct: $(REBAR) @$(SCRIPTS)/pre-compile.sh $(PROFILE) @ENABLE_COVER_COMPILE=1 $(REBAR) ct -c -v \ + --readable=$(CT_READABLE) \ --name $(CT_NODE_NAME) \ --cover_export_name $(CT_COVER_EXPORT_PREFIX)-$(subst /,-,$1) \ --suite $(shell $(SCRIPTS)/find-suites.sh $1) diff --git a/apps/emqx/test/emqx_common_test_helpers.erl b/apps/emqx/test/emqx_common_test_helpers.erl index 61196a645..f440dfc5a 100644 --- a/apps/emqx/test/emqx_common_test_helpers.erl +++ b/apps/emqx/test/emqx_common_test_helpers.erl @@ -447,8 +447,11 @@ is_all_tcp_servers_available(Servers) -> is_tcp_server_available(Host, Port) end, case lists:partition(Fun, Servers) of - {_, []} -> true; - {_, Unavail} -> ct:print("Unavailable servers: ~p", [Unavail]) + {_, []} -> + true; + {_, Unavail} -> + ct:print("Unavailable servers: ~p", [Unavail]), + false end. -spec is_tcp_server_available( diff --git a/apps/emqx_bridge/i18n/emqx_bridge_schema.conf b/apps/emqx_bridge/i18n/emqx_bridge_schema.conf index b62a2ee68..d575f09bc 100644 --- a/apps/emqx_bridge/i18n/emqx_bridge_schema.conf +++ b/apps/emqx_bridge/i18n/emqx_bridge_schema.conf @@ -78,17 +78,6 @@ emqx_bridge_schema { } } - metric_batching { - desc { - en: """Count of messages that are currently accumulated in memory waiting for sending in one batch.""" - zh: """当前积压在内存里,等待批量发送的消息个数""" - } - label: { - en: "Batched" - zh: "等待批量发送" - } - } - metric_dropped { desc { en: """Count of messages dropped.""" @@ -120,16 +109,6 @@ emqx_bridge_schema { zh: "队列已满被丢弃" } } - metric_dropped_queue_not_enabled { - desc { - en: """Count of messages dropped due to the queue is not enabled.""" - zh: """因为队列未启用被丢弃的消息个数。""" - } - label: { - en: "Dropped Queue Disabled" - zh: "队列未启用被丢弃" - } - } metric_dropped_resource_not_found { desc { en: """Count of messages dropped due to the resource is not found.""" @@ -193,7 +172,7 @@ emqx_bridge_schema { } } - metric_sent_inflight { + metric_inflight { desc { en: """Count of messages that were sent asynchronously but ACKs are not yet received.""" zh: """已异步地发送但没有收到 ACK 的消息个数。""" diff --git a/apps/emqx_bridge/include/emqx_bridge.hrl b/apps/emqx_bridge/include/emqx_bridge.hrl index ff639066f..d8229cc77 100644 --- a/apps/emqx_bridge/include/emqx_bridge.hrl +++ b/apps/emqx_bridge/include/emqx_bridge.hrl @@ -16,16 +16,14 @@ -define(EMPTY_METRICS, ?METRICS( - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ) ). -define(METRICS( - Batched, Dropped, DroppedOther, DroppedQueueFull, - DroppedQueueNotEnabled, DroppedResourceNotFound, DroppedResourceStopped, Matched, @@ -40,11 +38,9 @@ Rcvd ), #{ - 'batching' => Batched, 'dropped' => Dropped, 'dropped.other' => DroppedOther, 'dropped.queue_full' => DroppedQueueFull, - 'dropped.queue_not_enabled' => DroppedQueueNotEnabled, 'dropped.resource_not_found' => DroppedResourceNotFound, 'dropped.resource_stopped' => DroppedResourceStopped, 'matched' => Matched, @@ -61,11 +57,9 @@ ). -define(metrics( - Batched, Dropped, DroppedOther, DroppedQueueFull, - DroppedQueueNotEnabled, DroppedResourceNotFound, DroppedResourceStopped, Matched, @@ -80,11 +74,9 @@ Rcvd ), #{ - 'batching' := Batched, 'dropped' := Dropped, 'dropped.other' := DroppedOther, 'dropped.queue_full' := DroppedQueueFull, - 'dropped.queue_not_enabled' := DroppedQueueNotEnabled, 'dropped.resource_not_found' := DroppedResourceNotFound, 'dropped.resource_stopped' := DroppedResourceStopped, 'matched' := Matched, diff --git a/apps/emqx_bridge/src/emqx_bridge_api.erl b/apps/emqx_bridge/src/emqx_bridge_api.erl index 1debc90c4..0d0d2e9ad 100644 --- a/apps/emqx_bridge/src/emqx_bridge_api.erl +++ b/apps/emqx_bridge/src/emqx_bridge_api.erl @@ -207,7 +207,6 @@ info_example_basic(webhook) -> auto_restart_interval => 15000, query_mode => async, async_inflight_window => 100, - enable_queue => false, max_queue_bytes => 100 * 1024 * 1024 } }; @@ -233,7 +232,6 @@ mqtt_main_example() -> health_check_interval => <<"15s">>, auto_restart_interval => <<"60s">>, query_mode => sync, - enable_queue => false, max_queue_bytes => 100 * 1024 * 1024 }, ssl => #{ @@ -634,11 +632,11 @@ aggregate_metrics(AllMetrics) -> fun( #{ metrics := ?metrics( - M1, M2, M3, M4, M5, M6, M7, M8, M9, M10, M11, M12, M13, M14, M15, M16, M17 + M1, M2, M3, M4, M5, M6, M7, M8, M9, M10, M11, M12, M13, M14, M15 ) }, ?metrics( - N1, N2, N3, N4, N5, N6, N7, N8, N9, N10, N11, N12, N13, N14, N15, N16, N17 + N1, N2, N3, N4, N5, N6, N7, N8, N9, N10, N11, N12, N13, N14, N15 ) ) -> ?METRICS( @@ -656,9 +654,7 @@ aggregate_metrics(AllMetrics) -> M12 + N12, M13 + N13, M14 + N14, - M15 + N15, - M16 + N16, - M17 + N17 + M15 + N15 ) end, InitMetrics, @@ -691,7 +687,6 @@ format_metrics(#{ 'dropped' := Dropped, 'dropped.other' := DroppedOther, 'dropped.queue_full' := DroppedQueueFull, - 'dropped.queue_not_enabled' := DroppedQueueNotEnabled, 'dropped.resource_not_found' := DroppedResourceNotFound, 'dropped.resource_stopped' := DroppedResourceStopped, 'matched' := Matched, @@ -705,15 +700,12 @@ format_metrics(#{ matched := #{current := Rate, last5m := Rate5m, max := RateMax} } }) -> - Batched = maps:get('batching', Gauges, 0), Queued = maps:get('queuing', Gauges, 0), SentInflight = maps:get('inflight', Gauges, 0), ?METRICS( - Batched, Dropped, DroppedOther, DroppedQueueFull, - DroppedQueueNotEnabled, DroppedResourceNotFound, DroppedResourceStopped, Matched, diff --git a/apps/emqx_bridge/src/schema/emqx_bridge_mqtt_config.erl b/apps/emqx_bridge/src/schema/emqx_bridge_mqtt_config.erl index 4e35e38aa..7bd83d139 100644 --- a/apps/emqx_bridge/src/schema/emqx_bridge_mqtt_config.erl +++ b/apps/emqx_bridge/src/schema/emqx_bridge_mqtt_config.erl @@ -82,7 +82,6 @@ default_resource_opts() -> #{ <<"async_inflight_window">> => 100, <<"auto_restart_interval">> => <<"60s">>, - <<"enable_queue">> => false, <<"health_check_interval">> => <<"15s">>, <<"max_queue_bytes">> => <<"1GB">>, <<"query_mode">> => <<"sync">>, diff --git a/apps/emqx_bridge/src/schema/emqx_bridge_schema.erl b/apps/emqx_bridge/src/schema/emqx_bridge_schema.erl index c2358da51..b4159e0a0 100644 --- a/apps/emqx_bridge/src/schema/emqx_bridge_schema.erl +++ b/apps/emqx_bridge/src/schema/emqx_bridge_schema.erl @@ -128,12 +128,9 @@ fields(bridges) -> ] ++ ee_fields_bridges(); fields("metrics") -> [ - {"batching", mk(integer(), #{desc => ?DESC("metric_batching")})}, {"dropped", mk(integer(), #{desc => ?DESC("metric_dropped")})}, {"dropped.other", mk(integer(), #{desc => ?DESC("metric_dropped_other")})}, {"dropped.queue_full", mk(integer(), #{desc => ?DESC("metric_dropped_queue_full")})}, - {"dropped.queue_not_enabled", - mk(integer(), #{desc => ?DESC("metric_dropped_queue_not_enabled")})}, {"dropped.resource_not_found", mk(integer(), #{desc => ?DESC("metric_dropped_resource_not_found")})}, {"dropped.resource_stopped", @@ -142,7 +139,7 @@ fields("metrics") -> {"queuing", mk(integer(), #{desc => ?DESC("metric_queuing")})}, {"retried", mk(integer(), #{desc => ?DESC("metric_retried")})}, {"failed", mk(integer(), #{desc => ?DESC("metric_sent_failed")})}, - {"inflight", mk(integer(), #{desc => ?DESC("metric_sent_inflight")})}, + {"inflight", mk(integer(), #{desc => ?DESC("metric_inflight")})}, {"success", mk(integer(), #{desc => ?DESC("metric_sent_success")})}, {"rate", mk(float(), #{desc => ?DESC("metric_rate")})}, {"rate_max", mk(float(), #{desc => ?DESC("metric_rate_max")})}, diff --git a/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl b/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl index 90d19eccf..ae4fc4692 100644 --- a/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl +++ b/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl @@ -662,7 +662,6 @@ t_mqtt_conn_bridge_egress_reconnect(_) -> <<"reconnect_interval">> => <<"1s">>, <<"resource_opts">> => #{ <<"worker_pool_size">> => 2, - <<"enable_queue">> => true, <<"query_mode">> => <<"sync">>, %% to make it check the healthy quickly <<"health_check_interval">> => <<"0.5s">> diff --git a/apps/emqx_resource/i18n/emqx_resource_schema_i18n.conf b/apps/emqx_resource/i18n/emqx_resource_schema_i18n.conf index 332dfdd8c..ae8b15ad7 100644 --- a/apps/emqx_resource/i18n/emqx_resource_schema_i18n.conf +++ b/apps/emqx_resource/i18n/emqx_resource_schema_i18n.conf @@ -126,8 +126,8 @@ When disabled the messages are buffered in RAM only.""" batch_size { desc { - en: """Maximum batch count.""" - zh: """批量请求大小。""" + en: """Maximum batch count. If equal to 1, there's effectively no batching.""" + zh: """批量请求大小。如果等于1,实际上就没有批处理。""" } label { en: """Batch size""" diff --git a/apps/emqx_resource/include/emqx_resource.hrl b/apps/emqx_resource/include/emqx_resource.hrl index 6929cece9..d7b080ae8 100644 --- a/apps/emqx_resource/include/emqx_resource.hrl +++ b/apps/emqx_resource/include/emqx_resource.hrl @@ -64,10 +64,8 @@ %% If the resource disconnected, we can set to retry starting the resource %% periodically. auto_restart_interval => pos_integer(), - enable_batch => boolean(), batch_size => pos_integer(), batch_time => pos_integer(), - enable_queue => boolean(), max_queue_bytes => pos_integer(), query_mode => query_mode(), resume_interval => pos_integer(), @@ -90,7 +88,7 @@ -define(DEFAULT_QUEUE_SIZE_RAW, <<"100MB">>). %% count --define(DEFAULT_BATCH_SIZE, 100). +-define(DEFAULT_BATCH_SIZE, 1). %% milliseconds -define(DEFAULT_BATCH_TIME, 20). diff --git a/apps/emqx_resource/src/emqx_resource_manager.erl b/apps/emqx_resource/src/emqx_resource_manager.erl index 26ce5d6f7..8ad3fdd80 100644 --- a/apps/emqx_resource/src/emqx_resource_manager.erl +++ b/apps/emqx_resource/src/emqx_resource_manager.erl @@ -136,7 +136,6 @@ create(MgrId, ResId, Group, ResourceType, Config, Opts) -> 'success', 'failed', 'dropped', - 'dropped.queue_not_enabled', 'dropped.queue_full', 'dropped.resource_not_found', 'dropped.resource_stopped', diff --git a/apps/emqx_resource/src/emqx_resource_metrics.erl b/apps/emqx_resource/src/emqx_resource_metrics.erl index 64f014918..455be9c22 100644 --- a/apps/emqx_resource/src/emqx_resource_metrics.erl +++ b/apps/emqx_resource/src/emqx_resource_metrics.erl @@ -24,9 +24,6 @@ ]). -export([ - batching_set/3, - batching_shift/3, - batching_get/1, inflight_set/3, inflight_get/1, queuing_set/3, @@ -40,9 +37,6 @@ dropped_queue_full_inc/1, dropped_queue_full_inc/2, dropped_queue_full_get/1, - dropped_queue_not_enabled_inc/1, - dropped_queue_not_enabled_inc/2, - dropped_queue_not_enabled_get/1, dropped_resource_not_found_inc/1, dropped_resource_not_found_inc/2, dropped_resource_not_found_get/1, @@ -80,10 +74,8 @@ events() -> [ [?TELEMETRY_PREFIX, Event] || Event <- [ - batching, dropped_other, dropped_queue_full, - dropped_queue_not_enabled, dropped_resource_not_found, dropped_resource_stopped, failed, @@ -125,9 +117,6 @@ handle_telemetry_event( dropped_queue_full -> emqx_metrics_worker:inc(?RES_METRICS, ID, 'dropped', Val), emqx_metrics_worker:inc(?RES_METRICS, ID, 'dropped.queue_full', Val); - dropped_queue_not_enabled -> - emqx_metrics_worker:inc(?RES_METRICS, ID, 'dropped', Val), - emqx_metrics_worker:inc(?RES_METRICS, ID, 'dropped.queue_not_enabled', Val); dropped_resource_not_found -> emqx_metrics_worker:inc(?RES_METRICS, ID, 'dropped', Val), emqx_metrics_worker:inc(?RES_METRICS, ID, 'dropped.resource_not_found', Val); @@ -160,8 +149,6 @@ handle_telemetry_event( _HandlerConfig ) -> case Event of - batching -> - emqx_metrics_worker:set_gauge(?RES_METRICS, ID, WorkerID, 'batching', Val); inflight -> emqx_metrics_worker:set_gauge(?RES_METRICS, ID, WorkerID, 'inflight', Val); queuing -> @@ -169,45 +156,12 @@ handle_telemetry_event( _ -> ok end; -handle_telemetry_event( - [?TELEMETRY_PREFIX, Event], - _Measurements = #{gauge_shift := Val}, - _Metadata = #{resource_id := ID, worker_id := WorkerID}, - _HandlerConfig -) -> - case Event of - batching -> - emqx_metrics_worker:shift_gauge(?RES_METRICS, ID, WorkerID, 'batching', Val); - _ -> - ok - end; handle_telemetry_event(_EventName, _Measurements, _Metadata, _HandlerConfig) -> ok. %% Gauges (value can go both up and down): %% -------------------------------------- -%% @doc Count of messages that are currently accumulated in memory waiting for -%% being sent in one batch -batching_set(ID, WorkerID, Val) -> - telemetry:execute( - [?TELEMETRY_PREFIX, batching], - #{gauge_set => Val}, - #{resource_id => ID, worker_id => WorkerID} - ). - -batching_shift(_ID, _WorkerID = undefined, _Val) -> - ok; -batching_shift(ID, WorkerID, Val) -> - telemetry:execute( - [?TELEMETRY_PREFIX, batching], - #{gauge_shift => Val}, - #{resource_id => ID, worker_id => WorkerID} - ). - -batching_get(ID) -> - emqx_metrics_worker:get_gauge(?RES_METRICS, ID, 'batching'). - %% @doc Count of batches of messages that are currently %% queuing. [Gauge] queuing_set(ID, WorkerID, Val) -> @@ -269,18 +223,6 @@ dropped_queue_full_inc(ID, Val) -> dropped_queue_full_get(ID) -> emqx_metrics_worker:get(?RES_METRICS, ID, 'dropped.queue_full'). -%% @doc Count of messages dropped because the queue was not enabled -dropped_queue_not_enabled_inc(ID) -> - dropped_queue_not_enabled_inc(ID, 1). - -dropped_queue_not_enabled_inc(ID, Val) -> - telemetry:execute([?TELEMETRY_PREFIX, dropped_queue_not_enabled], #{counter_inc => Val}, #{ - resource_id => ID - }). - -dropped_queue_not_enabled_get(ID) -> - emqx_metrics_worker:get(?RES_METRICS, ID, 'dropped.queue_not_enabled'). - %% @doc Count of messages dropped because the resource was not found dropped_resource_not_found_inc(ID) -> dropped_resource_not_found_inc(ID, 1). diff --git a/apps/emqx_resource/src/emqx_resource_worker.erl b/apps/emqx_resource/src/emqx_resource_worker.erl index f04f7b1bd..15b279f71 100644 --- a/apps/emqx_resource/src/emqx_resource_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_worker.erl @@ -54,8 +54,12 @@ -export([reply_after_query/7, batch_reply_after_query/7]). +-elvis([{elvis_style, dont_repeat_yourself, disable}]). + -define(Q_ITEM(REQUEST), {q_item, REQUEST}). +-define(COLLECT_REQ_LIMIT, 1000). +-define(SEND_REQ(FROM, REQUEST), {'$send_req', FROM, REQUEST}). -define(QUERY(FROM, REQUEST, SENT), {query, FROM, REQUEST, SENT}). -define(REPLY(FROM, REQUEST, SENT, RESULT), {reply, FROM, REQUEST, SENT, RESULT}). -define(EXPAND(RESULT, BATCH), [ @@ -64,12 +68,23 @@ ]). -type id() :: binary(). --type query() :: {query, from(), request()}. +-type index() :: pos_integer(). +-type query() :: {query, request(), query_opts()}. +-type queue_query() :: ?QUERY(from(), request(), HasBeenSent :: boolean()). -type request() :: term(). --type from() :: pid() | reply_fun(). - --callback batcher_flush(Acc :: [{from(), request()}], CbState :: term()) -> - {{from(), result()}, NewCbState :: term()}. +-type from() :: pid() | reply_fun() | request_from(). +-type request_from() :: undefined | gen_statem:from(). +-type state() :: blocked | running. +-type data() :: #{ + id => id(), + index => index(), + name => atom(), + batch_size => pos_integer(), + batch_time => timer:time(), + queue => replayq:q(), + resume_interval => timer:time(), + tref => undefined | timer:tref() +}. callback_mode() -> [state_functions, state_enter]. @@ -80,11 +95,13 @@ start_link(Id, Index, Opts) -> sync_query(Id, Request, Opts) -> PickKey = maps:get(pick_key, Opts, self()), Timeout = maps:get(timeout, Opts, infinity), + emqx_resource_metrics:matched_inc(Id), pick_call(Id, PickKey, {query, Request, Opts}, Timeout). -spec async_query(id(), request(), query_opts()) -> Result :: term(). async_query(Id, Request, Opts) -> PickKey = maps:get(pick_key, Opts, self()), + emqx_resource_metrics:matched_inc(Id), pick_cast(Id, PickKey, {query, Request, Opts}). %% simple query the resource without batching and queuing messages. @@ -97,7 +114,9 @@ simple_sync_query(Id, Request) -> %% would mess up the metrics anyway. `undefined' is ignored by %% `emqx_resource_metrics:*_shift/3'. Index = undefined, - Result = call_query(sync, Id, Index, ?QUERY(self(), Request, false), #{}), + QueryOpts = #{}, + emqx_resource_metrics:matched_inc(Id), + Result = call_query(sync, Id, Index, ?QUERY(self(), Request, false), QueryOpts), _ = handle_query_result(Id, Result, false, false), Result. @@ -110,7 +129,9 @@ simple_async_query(Id, Request, ReplyFun) -> %% would mess up the metrics anyway. `undefined' is ignored by %% `emqx_resource_metrics:*_shift/3'. Index = undefined, - Result = call_query(async, Id, Index, ?QUERY(ReplyFun, Request, false), #{}), + QueryOpts = #{}, + emqx_resource_metrics:matched_inc(Id), + Result = call_query(async, Id, Index, ?QUERY(ReplyFun, Request, false), QueryOpts), _ = handle_query_result(Id, Result, false, false), Result. @@ -126,6 +147,7 @@ block(ServerRef, Query) -> resume(ServerRef) -> gen_statem:cast(ServerRef, resume). +-spec init({id(), pos_integer(), map()}) -> gen_statem:init_result(state(), data()). init({Id, Index, Opts}) -> process_flag(trap_exit, true), true = gproc_pool:connect_worker(Id, {Id, Index}), @@ -134,24 +156,19 @@ init({Id, Index, Opts}) -> SegBytes0 = maps:get(queue_seg_bytes, Opts, ?DEFAULT_QUEUE_SEG_SIZE), TotalBytes = maps:get(max_queue_bytes, Opts, ?DEFAULT_QUEUE_SIZE), SegBytes = min(SegBytes0, TotalBytes), - Queue = - case maps:get(enable_queue, Opts, false) of - true -> - replayq:open(#{ - dir => disk_queue_dir(Id, Index), - marshaller => fun ?MODULE:queue_item_marshaller/1, - max_total_bytes => TotalBytes, - %% we don't want to retain the queue after - %% resource restarts. - offload => true, - seg_bytes => SegBytes, - sizer => fun ?MODULE:estimate_size/1 - }); - false -> - undefined - end, + QueueOpts = + #{ + dir => disk_queue_dir(Id, Index), + marshaller => fun ?MODULE:queue_item_marshaller/1, + max_total_bytes => TotalBytes, + %% we don't want to retain the queue after + %% resource restarts. + offload => true, + seg_bytes => SegBytes, + sizer => fun ?MODULE:estimate_size/1 + }, + Queue = replayq:open(QueueOpts), emqx_resource_metrics:queuing_set(Id, Index, queue_count(Queue)), - emqx_resource_metrics:batching_set(Id, Index, 0), emqx_resource_metrics:inflight_set(Id, Index, 0), InfltWinSZ = maps:get(async_inflight_window, Opts, ?DEFAULT_INFLIGHT), ok = inflight_new(Name, InfltWinSZ, Id, Index), @@ -160,19 +177,17 @@ init({Id, Index, Opts}) -> id => Id, index => Index, name => Name, - enable_batch => maps:get(enable_batch, Opts, false), batch_size => BatchSize, batch_time => maps:get(batch_time, Opts, ?DEFAULT_BATCH_TIME), queue => Queue, resume_interval => maps:get(resume_interval, Opts, HCItvl), - acc => [], - acc_left => BatchSize, tref => undefined }, {ok, blocked, St, {next_event, cast, resume}}. -running(enter, _, _St) -> - keep_state_and_data; +running(enter, _, St) -> + ?tp(resource_worker_enter_running, #{}), + maybe_flush(St); running(cast, resume, _St) -> keep_state_and_data; running(cast, block, St) -> @@ -182,22 +197,22 @@ running( ) when is_list(Batch) -> - Q1 = maybe_append_queue(Id, Index, Q, [?Q_ITEM(Query) || Query <- Batch]), + Q1 = append_queue(Id, Index, Q, Batch), {next_state, blocked, St#{queue := Q1}}; -running({call, From}, {query, Request, _Opts}, St) -> - query_or_acc(From, Request, St); -running(cast, {query, Request, Opts}, St) -> - ReplyFun = maps:get(async_reply_fun, Opts, undefined), - query_or_acc(ReplyFun, Request, St); +running(info, ?SEND_REQ(_From, _Req) = Request0, Data) -> + handle_query_requests(Request0, Data); running(info, {flush, Ref}, St = #{tref := {_TRef, Ref}}) -> flush(St#{tref := undefined}); +running(internal, flush, St) -> + flush(St); running(info, {flush, _Ref}, _St) -> keep_state_and_data; running(info, Info, _St) -> - ?SLOG(error, #{msg => unexpected_msg, info => Info}), + ?SLOG(error, #{msg => unexpected_msg, state => running, info => Info}), keep_state_and_data. blocked(enter, _, #{resume_interval := ResumeT} = _St) -> + ?tp(resource_worker_enter_blocked, #{}), {keep_state_and_data, {state_timeout, ResumeT, resume}}; blocked(cast, block, _St) -> keep_state_and_data; @@ -206,33 +221,37 @@ blocked( ) when is_list(Batch) -> - Q1 = maybe_append_queue(Id, Index, Q, [?Q_ITEM(Query) || Query <- Batch]), + Q1 = append_queue(Id, Index, Q, Batch), {keep_state, St#{queue := Q1}}; blocked(cast, resume, St) -> do_resume(St); blocked(state_timeout, resume, St) -> do_resume(St); -blocked({call, From}, {query, Request, _Opts}, #{id := Id, index := Index, queue := Q} = St) -> +blocked(info, ?SEND_REQ(ReqFrom, {query, Request, Opts}), Data0) -> + #{ + id := Id, + index := Index, + queue := Q + } = Data0, + From = + case ReqFrom of + undefined -> maps:get(async_reply_fun, Opts, undefined); + From1 -> From1 + end, Error = ?RESOURCE_ERROR(blocked, "resource is blocked"), - _ = reply_caller(Id, ?REPLY(From, Request, false, Error)), - {keep_state, St#{ - queue := maybe_append_queue(Id, Index, Q, [?Q_ITEM(?QUERY(From, Request, false))]) - }}; -blocked(cast, {query, Request, Opts}, #{id := Id, index := Index, queue := Q} = St) -> - ReplyFun = maps:get(async_reply_fun, Opts, undefined), - Error = ?RESOURCE_ERROR(blocked, "resource is blocked"), - _ = reply_caller(Id, ?REPLY(ReplyFun, Request, false, Error)), - {keep_state, St#{ - queue := maybe_append_queue(Id, Index, Q, [?Q_ITEM(?QUERY(ReplyFun, Request, false))]) - }}. + HasBeenSent = false, + _ = reply_caller(Id, ?REPLY(From, Request, HasBeenSent, Error)), + NewQ = append_queue(Id, Index, Q, [?QUERY(From, Request, HasBeenSent)]), + Data = Data0#{queue := NewQ}, + {keep_state, Data}; +blocked(info, {flush, _Ref}, _Data) -> + keep_state_and_data; +blocked(info, Info, _Data) -> + ?SLOG(error, #{msg => unexpected_msg, state => blocked, info => Info}), + keep_state_and_data. terminate(_Reason, #{id := Id, index := Index, queue := Q}) -> - GaugeFns = - [ - fun emqx_resource_metrics:batching_set/3, - fun emqx_resource_metrics:inflight_set/3 - ], - lists:foreach(fun(Fn) -> Fn(Id, Index, 0) end, GaugeFns), + emqx_resource_metrics:inflight_set(Id, Index, 0), emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q)), gproc_pool:disconnect_worker(Id, {Id, Index}). @@ -255,43 +274,71 @@ code_change(_OldVsn, State, _Extra) -> ). pick_call(Id, Key, Query, Timeout) -> - ?PICK(Id, Key, gen_statem:call(Pid, Query, {clean_timeout, Timeout})). + ?PICK(Id, Key, begin + Caller = self(), + MRef = erlang:monitor(process, Pid, [{alias, reply_demonitor}]), + From = {Caller, MRef}, + erlang:send(Pid, ?SEND_REQ(From, Query)), + receive + {MRef, Response} -> + erlang:demonitor(MRef, [flush]), + Response; + {'DOWN', MRef, process, Pid, Reason} -> + error({worker_down, Reason}) + after Timeout -> + erlang:demonitor(MRef, [flush]), + receive + {MRef, Response} -> + Response + after 0 -> + error(timeout) + end + end + end). pick_cast(Id, Key, Query) -> - ?PICK(Id, Key, gen_statem:cast(Pid, Query)). + ?PICK(Id, Key, begin + From = undefined, + erlang:send(Pid, ?SEND_REQ(From, Query)), + ok + end). -do_resume(#{id := Id, name := Name} = St) -> +do_resume(#{id := Id, name := Name} = Data) -> case inflight_get_first(Name) of empty -> - retry_queue(St); + retry_queue(Data); {Ref, FirstQuery} -> %% We retry msgs in inflight window sync, as if we send them %% async, they will be appended to the end of inflight window again. - retry_inflight_sync(Id, Ref, FirstQuery, Name, St) + retry_inflight_sync(Id, Ref, FirstQuery, Name, Data) end. -retry_queue(#{queue := undefined} = St) -> - {next_state, running, St}; retry_queue( #{ - queue := Q, + queue := Q0, id := Id, index := Index, - enable_batch := false, + batch_size := 1, + name := Name, resume_interval := ResumeT - } = St + } = Data0 ) -> - case get_first_n_from_queue(Q, 1) of - [] -> - {next_state, running, St}; - [?QUERY(_, Request, HasSent) = Query] -> - QueryOpts = #{inflight_name => maps:get(name, St)}, + %% no batching + case get_first_n_from_queue(Q0, 1) of + empty -> + {next_state, running, Data0}; + {Q1, QAckRef, [?QUERY(_, Request, HasBeenSent) = Query]} -> + QueryOpts = #{inflight_name => Name}, Result = call_query(configured, Id, Index, Query, QueryOpts), - case reply_caller(Id, ?REPLY(undefined, Request, HasSent, Result)) of + Reply = ?REPLY(undefined, Request, HasBeenSent, Result), + case reply_caller(Id, Reply) of true -> - {keep_state, St, {state_timeout, ResumeT, resume}}; + {keep_state, Data0, {state_timeout, ResumeT, resume}}; false -> - retry_queue(St#{queue := drop_head(Q, Id, Index)}) + ok = replayq:ack(Q1, QAckRef), + emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), + Data = Data0#{queue := Q1}, + retry_queue(Data) end end; retry_queue( @@ -299,101 +346,202 @@ retry_queue( queue := Q, id := Id, index := Index, - enable_batch := true, batch_size := BatchSize, + name := Name, resume_interval := ResumeT - } = St + } = Data0 ) -> + %% batching case get_first_n_from_queue(Q, BatchSize) of - [] -> - {next_state, running, St}; - Batch0 -> - QueryOpts = #{inflight_name => maps:get(name, St)}, + empty -> + {next_state, running, Data0}; + {Q1, QAckRef, Batch0} -> + QueryOpts = #{inflight_name => Name}, Result = call_query(configured, Id, Index, Batch0, QueryOpts), %% The caller has been replied with ?RESOURCE_ERROR(blocked, _) before saving into the queue, %% we now change the 'from' field to 'undefined' so it will not reply the caller again. - Batch = [?QUERY(undefined, Request, HasSent) || ?QUERY(_, Request, HasSent) <- Batch0], + Batch = [ + ?QUERY(undefined, Request, HasBeenSent0) + || ?QUERY(_, Request, HasBeenSent0) <- Batch0 + ], case batch_reply_caller(Id, Result, Batch) of true -> - {keep_state, St, {state_timeout, ResumeT, resume}}; + ?tp(resource_worker_retry_queue_batch_failed, #{batch => Batch}), + {keep_state, Data0, {state_timeout, ResumeT, resume}}; false -> - retry_queue(St#{queue := drop_first_n_from_queue(Q, length(Batch), Id, Index)}) + ?tp(resource_worker_retry_queue_batch_succeeded, #{batch => Batch}), + ok = replayq:ack(Q1, QAckRef), + emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), + Data = Data0#{queue := Q1}, + retry_queue(Data) end end. retry_inflight_sync( Id, Ref, - ?QUERY(_, _, HasSent) = Query, + QueryOrBatch, Name, - #{index := Index, resume_interval := ResumeT} = St0 + #{index := Index, resume_interval := ResumeT} = Data0 ) -> - Result = call_query(sync, Id, Index, Query, #{}), - case handle_query_result(Id, Result, HasSent, false) of - %% Send failed because resource down + QueryOpts = #{}, + %% if we are retrying an inflight query, it has been sent + HasBeenSent = true, + Result = call_query(sync, Id, Index, QueryOrBatch, QueryOpts), + BlockWorker = false, + case handle_query_result(Id, Result, HasBeenSent, BlockWorker) of + %% Send failed because resource is down true -> - {keep_state, St0, {state_timeout, ResumeT, resume}}; + {keep_state, Data0, {state_timeout, ResumeT, resume}}; %% Send ok or failed but the resource is working false -> inflight_drop(Name, Ref, Id, Index), - do_resume(St0) + do_resume(Data0) end. -query_or_acc( - From, - Request, - #{ - enable_batch := true, - acc := Acc, - acc_left := Left, - index := Index, - id := Id - } = St0 -) -> - Acc1 = [?QUERY(From, Request, false) | Acc], - emqx_resource_metrics:batching_shift(Id, Index, 1), - St = St0#{acc := Acc1, acc_left := Left - 1}, - case Left =< 1 of - true -> flush(St); - false -> {keep_state, ensure_flush_timer(St)} - end; -query_or_acc(From, Request, #{enable_batch := false, queue := Q, id := Id, index := Index} = St) -> - QueryOpts = #{ - inflight_name => maps:get(name, St) - }, - Result = call_query(configured, Id, Index, ?QUERY(From, Request, false), QueryOpts), - case reply_caller(Id, ?REPLY(From, Request, false, Result)) of - true -> - Query = ?QUERY(From, Request, false), - {next_state, blocked, St#{queue := maybe_append_queue(Id, Index, Q, [?Q_ITEM(Query)])}}; - false -> - {keep_state, St} - end. - -flush(#{acc := []} = St) -> - {keep_state, St}; -flush( +%% Called during the `running' state only. +-spec handle_query_requests(?SEND_REQ(request_from(), request()), data()) -> data(). +handle_query_requests(Request0, Data0) -> #{ id := Id, index := Index, - acc := Batch0, - batch_size := Size, - queue := Q0 - } = St -) -> - Batch = lists:reverse(Batch0), - QueryOpts = #{ - inflight_name => maps:get(name, St) - }, - emqx_resource_metrics:batching_shift(Id, Index, -length(Batch)), - Result = call_query(configured, Id, Index, Batch, QueryOpts), - St1 = cancel_flush_timer(St#{acc_left := Size, acc := []}), - case batch_reply_caller(Id, Result, Batch) of + queue := Q + } = Data0, + Requests = collect_requests([Request0], ?COLLECT_REQ_LIMIT), + QueueItems = + lists:map( + fun + (?SEND_REQ(undefined = _From, {query, Req, Opts})) -> + ReplyFun = maps:get(async_reply_fun, Opts, undefined), + HasBeenSent = false, + ?QUERY(ReplyFun, Req, HasBeenSent); + (?SEND_REQ(From, {query, Req, _Opts})) -> + HasBeenSent = false, + ?QUERY(From, Req, HasBeenSent) + end, + Requests + ), + NewQ = append_queue(Id, Index, Q, QueueItems), + Data = Data0#{queue := NewQ}, + maybe_flush(Data). + +maybe_flush(Data) -> + #{ + batch_size := BatchSize, + queue := Q + } = Data, + QueueCount = queue_count(Q), + case QueueCount >= BatchSize of true -> - Q1 = maybe_append_queue(Id, Index, Q0, [?Q_ITEM(Query) || Query <- Batch]), - {next_state, blocked, St1#{queue := Q1}}; + flush(Data); false -> - {keep_state, St1} + {keep_state, ensure_flush_timer(Data)} + end. + +%% Called during the `running' state only. +-spec flush(data()) -> gen_statem:event_handler_result(state(), data()). +flush(Data0) -> + #{ + batch_size := BatchSize, + queue := Q0 + } = Data0, + case replayq:count(Q0) of + 0 -> + Data = cancel_flush_timer(Data0), + {keep_state, Data}; + _ -> + {Q1, QAckRef, Batch0} = replayq:pop(Q0, #{count_limit => BatchSize}), + Batch = [Item || ?Q_ITEM(Item) <- Batch0], + IsBatch = BatchSize =/= 1, + do_flush(Data0, #{ + new_queue => Q1, + is_batch => IsBatch, + batch => Batch, + ack_ref => QAckRef + }) + end. + +-spec do_flush(data(), #{ + is_batch := boolean(), + batch := [?QUERY(from(), request(), boolean())], + ack_ref := replayq:ack_ref() +}) -> + gen_statem:event_handler_result(state(), data()). +do_flush(Data0, #{is_batch := false, batch := Batch, ack_ref := QAckRef, new_queue := Q1}) -> + #{ + id := Id, + index := Index, + name := Name + } = Data0, + %% unwrap when not batching (i.e., batch size == 1) + [?QUERY(From, CoreReq, HasBeenSent) = Request] = Batch, + QueryOpts = #{inflight_name => Name}, + Result = call_query(configured, Id, Index, Request, QueryOpts), + IsAsync = is_async(Id), + Data1 = cancel_flush_timer(Data0), + Reply = ?REPLY(From, CoreReq, HasBeenSent, Result), + case {reply_caller(Id, Reply), IsAsync} of + %% failed and is not async; keep the request in the queue to + %% be retried + {true, false} -> + {next_state, blocked, Data1}; + %% failed and is async; remove the request from the queue, as + %% it is already in inflight table + {true, true} -> + ok = replayq:ack(Q1, QAckRef), + emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), + Data = Data1#{queue := Q1}, + {next_state, blocked, Data}; + %% success; just ack + {false, _} -> + ok = replayq:ack(Q1, QAckRef), + emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), + Data2 = Data1#{queue := Q1}, + case replayq:count(Q1) > 0 of + true -> + {keep_state, Data2, [{next_event, internal, flush}]}; + false -> + {keep_state, Data2} + end + end; +do_flush(Data0, #{is_batch := true, batch := Batch, ack_ref := QAckRef, new_queue := Q1}) -> + #{ + id := Id, + index := Index, + batch_size := BatchSize, + name := Name + } = Data0, + QueryOpts = #{inflight_name => Name}, + Result = call_query(configured, Id, Index, Batch, QueryOpts), + IsAsync = is_async(Id), + Data1 = cancel_flush_timer(Data0), + case {batch_reply_caller(Id, Result, Batch), IsAsync} of + %% failed and is not async; keep the request in the queue to + %% be retried + {true, false} -> + {next_state, blocked, Data1}; + %% failed and is async; remove the request from the queue, as + %% it is already in inflight table + {true, true} -> + ok = replayq:ack(Q1, QAckRef), + emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), + Data = Data1#{queue := Q1}, + {next_state, blocked, Data}; + %% success; just ack + {false, _} -> + ok = replayq:ack(Q1, QAckRef), + emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), + CurrentCount = replayq:count(Q1), + Data2 = Data1#{queue := Q1}, + case {CurrentCount > 0, CurrentCount >= BatchSize} of + {false, _} -> + {keep_state, Data2}; + {true, true} -> + {keep_state, Data2, [{next_event, internal, flush}]}; + {true, false} -> + Data3 = ensure_flush_timer(Data2), + {keep_state, Data3} + end end. batch_reply_caller(Id, BatchResult, Batch) -> @@ -408,11 +556,12 @@ batch_reply_caller(Id, BatchResult, Batch) -> ). reply_caller(Id, Reply) -> - reply_caller(Id, Reply, false). + BlockWorker = false, + reply_caller(Id, Reply, BlockWorker). -reply_caller(Id, ?REPLY(undefined, _, HasSent, Result), BlockWorker) -> - handle_query_result(Id, Result, HasSent, BlockWorker); -reply_caller(Id, ?REPLY({ReplyFun, Args}, _, HasSent, Result), BlockWorker) when +reply_caller(Id, ?REPLY(undefined, _, HasBeenSent, Result), BlockWorker) -> + handle_query_result(Id, Result, HasBeenSent, BlockWorker); +reply_caller(Id, ?REPLY({ReplyFun, Args}, _, HasBeenSent, Result), BlockWorker) when is_function(ReplyFun) -> _ = @@ -420,52 +569,52 @@ reply_caller(Id, ?REPLY({ReplyFun, Args}, _, HasSent, Result), BlockWorker) when {async_return, _} -> no_reply_for_now; _ -> apply(ReplyFun, Args ++ [Result]) end, - handle_query_result(Id, Result, HasSent, BlockWorker); -reply_caller(Id, ?REPLY(From, _, HasSent, Result), BlockWorker) -> + handle_query_result(Id, Result, HasBeenSent, BlockWorker); +reply_caller(Id, ?REPLY(From, _, HasBeenSent, Result), BlockWorker) -> gen_statem:reply(From, Result), - handle_query_result(Id, Result, HasSent, BlockWorker). + handle_query_result(Id, Result, HasBeenSent, BlockWorker). -handle_query_result(Id, ?RESOURCE_ERROR_M(exception, Msg), HasSent, BlockWorker) -> +handle_query_result(Id, ?RESOURCE_ERROR_M(exception, Msg), HasBeenSent, BlockWorker) -> ?SLOG(error, #{msg => resource_exception, info => Msg}), - inc_sent_failed(Id, HasSent), + inc_sent_failed(Id, HasBeenSent), BlockWorker; -handle_query_result(_Id, ?RESOURCE_ERROR_M(NotWorking, _), _HasSent, _) when +handle_query_result(_Id, ?RESOURCE_ERROR_M(NotWorking, _), _HasBeenSent, _) when NotWorking == not_connected; NotWorking == blocked -> true; -handle_query_result(Id, ?RESOURCE_ERROR_M(not_found, Msg), _HasSent, BlockWorker) -> +handle_query_result(Id, ?RESOURCE_ERROR_M(not_found, Msg), _HasBeenSent, BlockWorker) -> ?SLOG(error, #{id => Id, msg => resource_not_found, info => Msg}), emqx_resource_metrics:dropped_resource_not_found_inc(Id), BlockWorker; -handle_query_result(Id, ?RESOURCE_ERROR_M(stopped, Msg), _HasSent, BlockWorker) -> +handle_query_result(Id, ?RESOURCE_ERROR_M(stopped, Msg), _HasBeenSent, BlockWorker) -> ?SLOG(error, #{id => Id, msg => resource_stopped, info => Msg}), emqx_resource_metrics:dropped_resource_stopped_inc(Id), BlockWorker; -handle_query_result(Id, ?RESOURCE_ERROR_M(Reason, _), _HasSent, BlockWorker) -> +handle_query_result(Id, ?RESOURCE_ERROR_M(Reason, _), _HasBeenSent, BlockWorker) -> ?SLOG(error, #{id => Id, msg => other_resource_error, reason => Reason}), emqx_resource_metrics:dropped_other_inc(Id), BlockWorker; -handle_query_result(Id, {error, {recoverable_error, Reason}}, _HasSent, _BlockWorker) -> +handle_query_result(Id, {error, {recoverable_error, Reason}}, _HasBeenSent, _BlockWorker) -> %% the message will be queued in replayq or inflight window, %% i.e. the counter 'queuing' or 'dropped' will increase, so we pretend that we have not %% sent this message. ?SLOG(warning, #{id => Id, msg => recoverable_error, reason => Reason}), true; -handle_query_result(Id, {error, Reason}, HasSent, BlockWorker) -> +handle_query_result(Id, {error, Reason}, HasBeenSent, BlockWorker) -> ?SLOG(error, #{id => Id, msg => send_error, reason => Reason}), - inc_sent_failed(Id, HasSent), + inc_sent_failed(Id, HasBeenSent), BlockWorker; -handle_query_result(_Id, {async_return, inflight_full}, _HasSent, _BlockWorker) -> +handle_query_result(_Id, {async_return, inflight_full}, _HasBeenSent, _BlockWorker) -> true; -handle_query_result(Id, {async_return, {error, Msg}}, HasSent, BlockWorker) -> +handle_query_result(Id, {async_return, {error, Msg}}, HasBeenSent, BlockWorker) -> ?SLOG(error, #{id => Id, msg => async_send_error, info => Msg}), - inc_sent_failed(Id, HasSent), + inc_sent_failed(Id, HasBeenSent), BlockWorker; -handle_query_result(_Id, {async_return, ok}, _HasSent, BlockWorker) -> +handle_query_result(_Id, {async_return, ok}, _HasBeenSent, BlockWorker) -> BlockWorker; -handle_query_result(Id, Result, HasSent, BlockWorker) -> +handle_query_result(Id, Result, HasBeenSent, BlockWorker) -> assert_ok_result(Result), - inc_sent_success(Id, HasSent), + inc_sent_success(Id, HasBeenSent), BlockWorker. call_query(QM0, Id, Index, Query, QueryOpts) -> @@ -478,13 +627,10 @@ call_query(QM0, Id, Index, Query, QueryOpts) -> _ -> QM0 end, CM = maps:get(callback_mode, Data), - emqx_resource_metrics:matched_inc(Id), apply_query_fun(call_mode(QM, CM), Mod, Id, Index, Query, ResSt, QueryOpts); {ok, _Group, #{status := stopped}} -> - emqx_resource_metrics:matched_inc(Id), ?RESOURCE_ERROR(stopped, "resource stopped or disabled"); {ok, _Group, #{status := S}} when S == connecting; S == disconnected -> - emqx_resource_metrics:matched_inc(Id), ?RESOURCE_ERROR(not_connected, "resource not connected"); {error, not_found} -> ?RESOURCE_ERROR(not_found, "resource not found") @@ -516,7 +662,7 @@ apply_query_fun(async, Mod, Id, Index, ?QUERY(_, Request, _) = Query, ResSt, Que Name = maps:get(inflight_name, QueryOpts, undefined), ?APPLY_RESOURCE( call_query_async, - case inflight_is_full(Name) of + case is_inflight_full(Name) of true -> {async_return, inflight_full}; false -> @@ -538,26 +684,26 @@ apply_query_fun(async, Mod, Id, Index, [?QUERY(_, _, _) | _] = Batch, ResSt, Que Name = maps:get(inflight_name, QueryOpts, undefined), ?APPLY_RESOURCE( call_batch_query_async, - case inflight_is_full(Name) of + case is_inflight_full(Name) of true -> {async_return, inflight_full}; false -> ReplyFun = fun ?MODULE:batch_reply_after_query/7, Ref = make_message_ref(), - Args = {ReplyFun, [self(), Id, Index, Name, Ref, Batch]}, + ReplyFunAndArgs = {ReplyFun, [self(), Id, Index, Name, Ref, Batch]}, Requests = [Request || ?QUERY(_From, Request, _) <- Batch], ok = inflight_append(Name, Ref, Batch, Id, Index), - Result = Mod:on_batch_query_async(Id, Requests, Args, ResSt), + Result = Mod:on_batch_query_async(Id, Requests, ReplyFunAndArgs, ResSt), {async_return, Result} end, Batch ). -reply_after_query(Pid, Id, Index, Name, Ref, ?QUERY(From, Request, HasSent), Result) -> +reply_after_query(Pid, Id, Index, Name, Ref, ?QUERY(From, Request, HasBeenSent), Result) -> %% NOTE: 'inflight' is the count of messages that were sent async %% but received no ACK, NOT the number of messages queued in the %% inflight window. - case reply_caller(Id, ?REPLY(From, Request, HasSent, Result)) of + case reply_caller(Id, ?REPLY(From, Request, HasBeenSent, Result)) of true -> ?MODULE:block(Pid); false -> @@ -576,7 +722,7 @@ batch_reply_after_query(Pid, Id, Index, Name, Ref, Batch, Result) -> end. drop_inflight_and_resume(Pid, Name, Ref, Id, Index) -> - case inflight_is_full(Name) of + case is_inflight_full(Name) of true -> inflight_drop(Name, Ref, Id, Index), ?MODULE:resume(Pid); @@ -594,10 +740,8 @@ queue_item_marshaller(Bin) when is_binary(Bin) -> estimate_size(QItem) -> size(queue_item_marshaller(QItem)). -maybe_append_queue(Id, _Index, undefined, _Items) -> - emqx_resource_metrics:dropped_queue_not_enabled_inc(Id), - undefined; -maybe_append_queue(Id, Index, Q, Items) -> +-spec append_queue(id(), index(), replayq:q(), [queue_query()]) -> replayq:q(). +append_queue(Id, Index, Q, Queries) -> Q2 = case replayq:overflow(Q) of Overflow when Overflow =< 0 -> @@ -611,42 +755,38 @@ maybe_append_queue(Id, Index, Q, Items) -> ?SLOG(error, #{msg => drop_query, reason => queue_full, dropped => Dropped}), Q1 end, + Items = [?Q_ITEM(X) || X <- Queries], Q3 = replayq:append(Q2, Items), emqx_resource_metrics:queuing_set(Id, Index, replayq:count(Q3)), + ?tp(resource_worker_appended_to_queue, #{id => Id, items => Queries}), Q3. +-spec get_first_n_from_queue(replayq:q(), pos_integer()) -> + empty | {replayq:q(), replayq:ack_ref(), [?Q_ITEM(?QUERY(_From, _Request, _HasBeenSent))]}. get_first_n_from_queue(Q, N) -> - get_first_n_from_queue(Q, N, []). - -get_first_n_from_queue(_Q, 0, Acc) -> - lists:reverse(Acc); -get_first_n_from_queue(Q, N, Acc) when N > 0 -> - case replayq:peek(Q) of - empty -> Acc; - ?Q_ITEM(Query) -> get_first_n_from_queue(Q, N - 1, [Query | Acc]) + case replayq:count(Q) of + 0 -> + empty; + _ -> + {NewQ, QAckRef, Items} = replayq:pop(Q, #{count_limit => N}), + Queries = [X || ?Q_ITEM(X) <- Items], + {NewQ, QAckRef, Queries} end. -drop_first_n_from_queue(Q, 0, _Id, _Index) -> - Q; -drop_first_n_from_queue(Q, N, Id, Index) when N > 0 -> - drop_first_n_from_queue(drop_head(Q, Id, Index), N - 1, Id, Index). - -drop_head(Q, Id, Index) -> - {NewQ, AckRef, _} = replayq:pop(Q, #{count_limit => 1}), - ok = replayq:ack(NewQ, AckRef), - emqx_resource_metrics:queuing_set(Id, Index, replayq:count(NewQ)), - NewQ. - %%============================================================================== %% the inflight queue for async query --define(SIZE_REF, -1). +-define(MAX_SIZE_REF, -1). +-define(SIZE_REF, -2). inflight_new(Name, InfltWinSZ, Id, Index) -> _ = ets:new(Name, [named_table, ordered_set, public, {write_concurrency, true}]), - inflight_append(Name, ?SIZE_REF, {max_size, InfltWinSZ}, Id, Index), + inflight_append(Name, ?MAX_SIZE_REF, {max_size, InfltWinSZ}, Id, Index), + %% we use this counter because we might deal with batches as + %% elements. + inflight_append(Name, ?SIZE_REF, 0, Id, Index), ok. inflight_get_first(Name) -> - case ets:next(Name, ?SIZE_REF) of + case ets:next(Name, ?MAX_SIZE_REF) of '$end_of_table' -> empty; Ref -> @@ -659,31 +799,42 @@ inflight_get_first(Name) -> end end. -inflight_is_full(undefined) -> +is_inflight_full(undefined) -> false; -inflight_is_full(Name) -> - [{_, {max_size, MaxSize}}] = ets:lookup(Name, ?SIZE_REF), - Size = inflight_size(Name), +is_inflight_full(Name) -> + [{_, {max_size, MaxSize}}] = ets:lookup(Name, ?MAX_SIZE_REF), + %% we consider number of batches rather than number of messages + %% because one batch request may hold several messages. + Size = inflight_num_batches(Name), Size >= MaxSize. -inflight_size(Name) -> - %% Note: we subtract 1 because there's a metadata row that hold - %% the maximum size value. - MetadataRowCount = 1, +inflight_num_batches(Name) -> + %% Note: we subtract 2 because there're 2 metadata rows that hold + %% the maximum size value and the number of messages. + MetadataRowCount = 2, case ets:info(Name, size) of undefined -> 0; Size -> max(0, Size - MetadataRowCount) end. +inflight_num_msgs(Name) -> + [{_, Size}] = ets:lookup(Name, ?SIZE_REF), + Size. + inflight_append(undefined, _Ref, _Query, _Id, _Index) -> ok; -inflight_append(Name, Ref, [?QUERY(_, _, _) | _] = Batch, Id, Index) -> - ets:insert(Name, {Ref, [?QUERY(From, Req, true) || ?QUERY(From, Req, _) <- Batch]}), - emqx_resource_metrics:inflight_set(Id, Index, inflight_size(Name)), +inflight_append(Name, Ref, [?QUERY(_, _, _) | _] = Batch0, Id, Index) -> + Batch = mark_as_sent(Batch0), + ets:insert(Name, {Ref, Batch}), + BatchSize = length(Batch), + ets:update_counter(Name, ?SIZE_REF, {2, BatchSize}), + emqx_resource_metrics:inflight_set(Id, Index, inflight_num_msgs(Name)), ok; -inflight_append(Name, Ref, ?QUERY(From, Req, _), Id, Index) -> - ets:insert(Name, {Ref, ?QUERY(From, Req, true)}), - emqx_resource_metrics:inflight_set(Id, Index, inflight_size(Name)), +inflight_append(Name, Ref, ?QUERY(_From, _Req, _HasBeenSent) = Query0, Id, Index) -> + Query = mark_as_sent(Query0), + ets:insert(Name, {Ref, Query}), + ets:update_counter(Name, ?SIZE_REF, {2, 1}), + emqx_resource_metrics:inflight_set(Id, Index, inflight_num_msgs(Name)), ok; inflight_append(Name, Ref, Data, _Id, _Index) -> ets:insert(Name, {Ref, Data}), @@ -694,20 +845,26 @@ inflight_append(Name, Ref, Data, _Id, _Index) -> inflight_drop(undefined, _, _Id, _Index) -> ok; inflight_drop(Name, Ref, Id, Index) -> - ets:delete(Name, Ref), - emqx_resource_metrics:inflight_set(Id, Index, inflight_size(Name)), + Count = + case ets:take(Name, Ref) of + [{Ref, ?QUERY(_, _, _)}] -> 1; + [{Ref, [?QUERY(_, _, _) | _] = Batch}] -> length(Batch); + _ -> 0 + end, + Count > 0 andalso ets:update_counter(Name, ?SIZE_REF, {2, -Count, 0, 0}), + emqx_resource_metrics:inflight_set(Id, Index, inflight_num_msgs(Name)), ok. %%============================================================================== -inc_sent_failed(Id, _HasSent = true) -> +inc_sent_failed(Id, _HasBeenSent = true) -> emqx_resource_metrics:retried_failed_inc(Id); -inc_sent_failed(Id, _HasSent) -> +inc_sent_failed(Id, _HasBeenSent) -> emqx_resource_metrics:failed_inc(Id). -inc_sent_success(Id, _HasSent = true) -> +inc_sent_success(Id, _HasBeenSent = true) -> emqx_resource_metrics:retried_success_inc(Id); -inc_sent_success(Id, _HasSent) -> +inc_sent_success(Id, _HasBeenSent) -> emqx_resource_metrics:success_inc(Id). call_mode(sync, _) -> sync; @@ -728,8 +885,6 @@ assert_ok_result(R) when is_tuple(R) -> assert_ok_result(R) -> error({not_ok_result, R}). -queue_count(undefined) -> - 0; queue_count(Q) -> replayq:count(Q). @@ -744,12 +899,12 @@ disk_queue_dir(Id, Index) -> QDir = binary_to_list(Id) ++ ":" ++ integer_to_list(Index), filename:join([emqx:data_dir(), "resource_worker", node(), QDir]). -ensure_flush_timer(St = #{tref := undefined, batch_time := T}) -> +ensure_flush_timer(Data = #{tref := undefined, batch_time := T}) -> Ref = make_ref(), TRef = erlang:send_after(T, self(), {flush, Ref}), - St#{tref => {TRef, Ref}}; -ensure_flush_timer(St) -> - St. + Data#{tref => {TRef, Ref}}; +ensure_flush_timer(Data) -> + Data. cancel_flush_timer(St = #{tref := undefined}) -> St; @@ -759,3 +914,31 @@ cancel_flush_timer(St = #{tref := {TRef, _Ref}}) -> make_message_ref() -> erlang:unique_integer([monotonic, positive]). + +collect_requests(Acc, Limit) -> + Count = length(Acc), + do_collect_requests(Acc, Count, Limit). + +do_collect_requests(Acc, Count, Limit) when Count >= Limit -> + lists:reverse(Acc); +do_collect_requests(Acc, Count, Limit) -> + receive + ?SEND_REQ(_From, _Req) = Request -> + do_collect_requests([Request | Acc], Count + 1, Limit) + after 0 -> + lists:reverse(Acc) + end. + +mark_as_sent(Batch) when is_list(Batch) -> + lists:map(fun mark_as_sent/1, Batch); +mark_as_sent(?QUERY(From, Req, _)) -> + HasBeenSent = true, + ?QUERY(From, Req, HasBeenSent). + +is_async(ResourceId) -> + case emqx_resource_manager:ets_lookup(ResourceId) of + {ok, _Group, #{query_mode := QM, callback_mode := CM}} -> + call_mode(QM, CM) =:= async; + _ -> + false + end. diff --git a/apps/emqx_resource/src/schema/emqx_resource_schema.erl b/apps/emqx_resource/src/schema/emqx_resource_schema.erl index 0e25d01b8..d105b21ef 100644 --- a/apps/emqx_resource/src/schema/emqx_resource_schema.erl +++ b/apps/emqx_resource/src/schema/emqx_resource_schema.erl @@ -83,12 +83,14 @@ query_mode(_) -> undefined. enable_batch(type) -> boolean(); enable_batch(required) -> false; enable_batch(default) -> true; +enable_batch(deprecated) -> {since, "v5.0.14"}; enable_batch(desc) -> ?DESC("enable_batch"); enable_batch(_) -> undefined. enable_queue(type) -> boolean(); enable_queue(required) -> false; enable_queue(default) -> false; +enable_queue(deprecated) -> {since, "v5.0.14"}; enable_queue(desc) -> ?DESC("enable_queue"); enable_queue(_) -> undefined. diff --git a/apps/emqx_resource/test/emqx_connector_demo.erl b/apps/emqx_resource/test/emqx_connector_demo.erl index 7af0607cb..692895548 100644 --- a/apps/emqx_resource/test/emqx_connector_demo.erl +++ b/apps/emqx_resource/test/emqx_connector_demo.erl @@ -17,6 +17,7 @@ -module(emqx_connector_demo). -include_lib("typerefl/include/types.hrl"). +-include_lib("snabbkaffe/include/snabbkaffe.hrl"). -behaviour(emqx_resource). @@ -28,6 +29,7 @@ on_query/3, on_query_async/4, on_batch_query/3, + on_batch_query_async/4, on_get_status/2 ]). @@ -36,6 +38,8 @@ %% callbacks for emqx_resource config schema -export([roots/0]). +-define(CM_KEY, {?MODULE, callback_mode}). + roots() -> [ {name, fun name/1}, @@ -51,7 +55,6 @@ register(required) -> true; register(default) -> false; register(_) -> undefined. --define(CM_KEY, {?MODULE, callback_mode}). callback_mode() -> persistent_term:get(?CM_KEY). @@ -60,17 +63,12 @@ set_callback_mode(Mode) -> on_start(_InstId, #{create_error := true}) -> error("some error"); -on_start(InstId, #{name := Name, stop_error := true} = Opts) -> - Register = maps:get(register, Opts, false), - {ok, Opts#{ - id => InstId, - stop_error => true, - pid => spawn_counter_process(Name, Register) - }}; on_start(InstId, #{name := Name} = Opts) -> Register = maps:get(register, Opts, false), + StopError = maps:get(stop_error, Opts, false), {ok, Opts#{ id => InstId, + stop_error => StopError, pid => spawn_counter_process(Name, Register) }}. @@ -95,8 +93,11 @@ on_query(_InstId, {inc_counter, N}, #{pid := Pid}) -> From = {self(), ReqRef}, Pid ! {From, {inc, N}}, receive - {ReqRef, ok} -> ok; - {ReqRef, incorrect_status} -> {error, {recoverable_error, incorrect_status}} + {ReqRef, ok} -> + ?tp(connector_demo_inc_counter, #{n => N}), + ok; + {ReqRef, incorrect_status} -> + {error, {recoverable_error, incorrect_status}} after 1000 -> {error, timeout} end; @@ -127,18 +128,30 @@ on_query_async(_InstId, get_counter, ReplyFun, #{pid := Pid}) -> ok. on_batch_query(InstId, BatchReq, State) -> - %% Requests can be either 'get_counter' or 'inc_counter', but cannot be mixed. + %% Requests can be either 'get_counter' or 'inc_counter', but + %% cannot be mixed. case hd(BatchReq) of {inc_counter, _} -> - batch_inc_counter(InstId, BatchReq, State); + batch_inc_counter(sync, InstId, BatchReq, State); get_counter -> - batch_get_counter(InstId, State) + batch_get_counter(sync, InstId, State) end. -batch_inc_counter(InstId, BatchReq, State) -> +on_batch_query_async(InstId, BatchReq, ReplyFunAndArgs, State) -> + %% Requests can be either 'get_counter' or 'inc_counter', but + %% cannot be mixed. + case hd(BatchReq) of + {inc_counter, _} -> + batch_inc_counter({async, ReplyFunAndArgs}, InstId, BatchReq, State); + get_counter -> + batch_get_counter({async, ReplyFunAndArgs}, InstId, State) + end. + +batch_inc_counter(CallMode, InstId, BatchReq, State) -> TotalN = lists:foldl( fun ({inc_counter, N}, Total) -> + ?tp(connector_demo_batch_inc_individual, #{n => N}), Total + N; (Req, _Total) -> error({mixed_requests_not_allowed, {inc_counter, Req}}) @@ -146,10 +159,17 @@ batch_inc_counter(InstId, BatchReq, State) -> 0, BatchReq ), - on_query(InstId, {inc_counter, TotalN}, State). + case CallMode of + sync -> + on_query(InstId, {inc_counter, TotalN}, State); + {async, ReplyFunAndArgs} -> + on_query_async(InstId, {inc_counter, TotalN}, ReplyFunAndArgs, State) + end. -batch_get_counter(InstId, State) -> - on_query(InstId, get_counter, State). +batch_get_counter(sync, InstId, State) -> + on_query(InstId, get_counter, State); +batch_get_counter({async, ReplyFunAndArgs}, InstId, State) -> + on_query_async(InstId, get_counter, ReplyFunAndArgs, State). on_get_status(_InstId, #{health_check_error := true}) -> disconnected; @@ -187,6 +207,7 @@ counter_loop( {inc, N, ReplyFun} when Status == running -> %ct:pal("async counter recv: ~p", [{inc, N}]), apply_reply(ReplyFun, ok), + ?tp(connector_demo_inc_counter_async, #{n => N}), State#{counter => Num + N}; {{FromPid, ReqRef}, {inc, N}} when Status == running -> %ct:pal("sync counter recv: ~p", [{inc, N}]), diff --git a/apps/emqx_resource/test/emqx_resource_SUITE.erl b/apps/emqx_resource/test/emqx_resource_SUITE.erl index 8a6b179d5..4bea0a1ee 100644 --- a/apps/emqx_resource/test/emqx_resource_SUITE.erl +++ b/apps/emqx_resource/test/emqx_resource_SUITE.erl @@ -30,6 +30,8 @@ -define(RESOURCE_ERROR(REASON), {error, {resource_error, #{reason := REASON}}}). -define(TRACE_OPTS, #{timetrap => 10000, timeout => 1000}). +-import(emqx_common_test_helpers, [on_exit/1]). + all() -> emqx_common_test_helpers:all(?MODULE). @@ -37,11 +39,15 @@ groups() -> []. init_per_testcase(_, Config) -> + ct:timetrap({seconds, 30}), emqx_connector_demo:set_callback_mode(always_sync), Config. end_per_testcase(_, _Config) -> - _ = emqx_resource:remove(?ID). + snabbkaffe:stop(), + _ = emqx_resource:remove(?ID), + emqx_common_test_helpers:call_janitor(), + ok. init_per_suite(Config) -> code:ensure_loaded(?TEST_RESOURCE), @@ -140,6 +146,7 @@ t_create_remove_local(_) -> ?assertNot(is_process_alive(Pid)). t_do_not_start_after_created(_) -> + ct:pal("creating resource"), {ok, _} = emqx_resource:create_local( ?ID, ?DEFAULT_RESOURCE_GROUP, @@ -159,16 +166,19 @@ t_do_not_start_after_created(_) -> ), %% start the resource manually.. + ct:pal("starting resource manually"), ok = emqx_resource:start(?ID), {ok, #{pid := Pid}} = emqx_resource:query(?ID, get_state), ?assert(is_process_alive(Pid)), %% restart the resource + ct:pal("restarting resource"), ok = emqx_resource:restart(?ID), ?assertNot(is_process_alive(Pid)), {ok, #{pid := Pid2}} = emqx_resource:query(?ID, get_state), ?assert(is_process_alive(Pid2)), + ct:pal("removing resource"), ok = emqx_resource:remove_local(?ID), ?assertNot(is_process_alive(Pid2)). @@ -207,12 +217,13 @@ t_query_counter(_) -> ok = emqx_resource:remove_local(?ID). t_batch_query_counter(_) -> + BatchSize = 100, {ok, _} = emqx_resource:create_local( ?ID, ?DEFAULT_RESOURCE_GROUP, ?TEST_RESOURCE, #{name => test_resource, register => true}, - #{enable_batch => true, query_mode => sync} + #{batch_size => BatchSize, query_mode => sync} ), ?check_trace( @@ -225,15 +236,26 @@ t_batch_query_counter(_) -> end ), + NMsgs = 1_000, ?check_trace( ?TRACE_OPTS, - inc_counter_in_parallel(1000), + begin + NEvents = round(math:ceil(NMsgs / BatchSize)), + {ok, SRef} = snabbkaffe:subscribe( + ?match_event(#{?snk_kind := connector_demo_inc_counter}), + NEvents, + _Timeout = 10_000 + ), + inc_counter_in_parallel(NMsgs), + {ok, _} = snabbkaffe:receive_events(SRef), + ok + end, fun(Trace) -> QueryTrace = ?of_kind(call_batch_query, Trace), ?assertMatch([#{batch := BatchReq} | _] when length(BatchReq) > 1, QueryTrace) end ), - {ok, 1000} = emqx_resource:query(?ID, get_counter), + {ok, NMsgs} = emqx_resource:query(?ID, get_counter), ok = emqx_resource:remove_local(?ID). @@ -243,20 +265,28 @@ t_query_counter_async_query(_) -> ?DEFAULT_RESOURCE_GROUP, ?TEST_RESOURCE, #{name => test_resource, register => true}, - #{query_mode => async, enable_batch => false} + #{query_mode => async, batch_size => 1} ), ?assertMatch({ok, 0}, emqx_resource:simple_sync_query(?ID, get_counter)), + NMsgs = 1_000, ?check_trace( ?TRACE_OPTS, - inc_counter_in_parallel(1000), + begin + {ok, SRef} = snabbkaffe:subscribe( + ?match_event(#{?snk_kind := connector_demo_inc_counter}), + NMsgs, + _Timeout = 60_000 + ), + inc_counter_in_parallel(NMsgs), + {ok, _} = snabbkaffe:receive_events(SRef), + ok + end, fun(Trace) -> - %% the callback_mode if 'emqx_connector_demo' is 'always_sync'. + %% the callback_mode of 'emqx_connector_demo' is 'always_sync'. QueryTrace = ?of_kind(call_query, Trace), ?assertMatch([#{query := {query, _, {inc_counter, 1}, _}} | _], QueryTrace) end ), - %% wait for 1s to make sure all the aysnc query is sent to the resource. - timer:sleep(1000), %% simple query ignores the query_mode and batching settings in the resource_worker ?check_trace( ?TRACE_OPTS, @@ -285,20 +315,32 @@ t_query_counter_async_callback(_) -> ?DEFAULT_RESOURCE_GROUP, ?TEST_RESOURCE, #{name => test_resource, register => true}, - #{query_mode => async, enable_batch => false, async_inflight_window => 1000000} + #{ + query_mode => async, + batch_size => 1, + async_inflight_window => 1000000 + } ), ?assertMatch({ok, 0}, emqx_resource:simple_sync_query(?ID, get_counter)), + NMsgs = 1_000, ?check_trace( ?TRACE_OPTS, - inc_counter_in_parallel(1000, ReqOpts), + begin + {ok, SRef} = snabbkaffe:subscribe( + ?match_event(#{?snk_kind := connector_demo_inc_counter_async}), + NMsgs, + _Timeout = 60_000 + ), + inc_counter_in_parallel(NMsgs, ReqOpts), + {ok, _} = snabbkaffe:receive_events(SRef), + ok + end, fun(Trace) -> QueryTrace = ?of_kind(call_query_async, Trace), ?assertMatch([#{query := {query, _, {inc_counter, 1}, _}} | _], QueryTrace) end ), - %% wait for 1s to make sure all the aysnc query is sent to the resource. - timer:sleep(1000), %% simple query ignores the query_mode and batching settings in the resource_worker ?check_trace( ?TRACE_OPTS, @@ -325,12 +367,29 @@ t_query_counter_async_callback(_) -> t_query_counter_async_inflight(_) -> emqx_connector_demo:set_callback_mode(async_if_possible), + MetricsTab = ets:new(metrics_tab, [ordered_set, public]), + ok = telemetry:attach_many( + ?FUNCTION_NAME, + emqx_resource_metrics:events(), + fun(Event, Measurements, Meta, _Config) -> + ets:insert( + MetricsTab, + {erlang:monotonic_time(), #{ + event => Event, measurements => Measurements, metadata => Meta + }} + ), + ok + end, + unused_config + ), + on_exit(fun() -> telemetry:detach(?FUNCTION_NAME) end), Tab0 = ets:new(?FUNCTION_NAME, [bag, public]), - Insert0 = fun(Tab, Result) -> - ets:insert(Tab, {make_ref(), Result}) + Insert0 = fun(Tab, Ref, Result) -> + ct:pal("inserting ~p", [{Ref, Result}]), + ets:insert(Tab, {Ref, Result}) end, - ReqOpts = #{async_reply_fun => {Insert0, [Tab0]}}, + ReqOpts = fun() -> #{async_reply_fun => {Insert0, [Tab0, make_ref()]}} end, WindowSize = 15, {ok, _} = emqx_resource:create_local( ?ID, @@ -339,11 +398,10 @@ t_query_counter_async_inflight(_) -> #{name => test_resource, register => true}, #{ query_mode => async, - enable_batch => false, + batch_size => 1, async_inflight_window => WindowSize, worker_pool_size => 1, - resume_interval => 300, - enable_queue => false + resume_interval => 300 } ), ?assertMatch({ok, 0}, emqx_resource:simple_sync_query(?ID, get_counter)), @@ -360,40 +418,76 @@ t_query_counter_async_inflight(_) -> ?assertMatch([#{query := {query, _, {inc_counter, 1}, _}} | _], QueryTrace) end ), + tap_metrics(?LINE), %% this will block the resource_worker as the inflight window is full now - ok = emqx_resource:query(?ID, {inc_counter, 1}), + {ok, {ok, _}} = + ?wait_async_action( + emqx_resource:query(?ID, {inc_counter, 2}), + #{?snk_kind := resource_worker_enter_blocked}, + 1_000 + ), ?assertMatch(0, ets:info(Tab0, size)), - %% sleep to make the resource_worker resume some times - timer:sleep(2000), + tap_metrics(?LINE), %% send query now will fail because the resource is blocked. Insert = fun(Tab, Ref, Result) -> - ets:insert(Tab, {Ref, Result}) + ct:pal("inserting ~p", [{Ref, Result}]), + ets:insert(Tab, {Ref, Result}), + ?tp(tmp_query_inserted, #{}) end, - ok = emqx_resource:query(?ID, {inc_counter, 1}, #{ - async_reply_fun => {Insert, [Tab0, tmp_query]} - }), - timer:sleep(100), + {ok, {ok, _}} = + ?wait_async_action( + emqx_resource:query(?ID, {inc_counter, 3}, #{ + async_reply_fun => {Insert, [Tab0, tmp_query]} + }), + #{?snk_kind := tmp_query_inserted}, + 1_000 + ), + %% since this counts as a failure, it'll be enqueued and retried + %% later, when the resource is unblocked. ?assertMatch([{_, {error, {resource_error, #{reason := blocked}}}}], ets:take(Tab0, tmp_query)), + tap_metrics(?LINE), - %% all response should be received after the resource is resumed. + %% all responses should be received after the resource is resumed. + {ok, SRef0} = snabbkaffe:subscribe( + ?match_event(#{?snk_kind := connector_demo_inc_counter_async}), + %% +1 because the tmp_query above will be retried and succeed + %% this time. + WindowSize + 1, + _Timeout = 60_000 + ), ?assertMatch(ok, emqx_resource:simple_sync_query(?ID, resume)), - timer:sleep(1000), + tap_metrics(?LINE), + {ok, _} = snabbkaffe:receive_events(SRef0), + %% since the previous tmp_query was enqueued to be retried, we + %% take it again from the table; this time, it should have + %% succeeded. + ?assertMatch([{tmp_query, ok}], ets:take(Tab0, tmp_query)), ?assertEqual(WindowSize, ets:info(Tab0, size)), + tap_metrics(?LINE), %% send async query, this time everything should be ok. Num = 10, ?check_trace( ?TRACE_OPTS, - inc_counter_in_parallel(Num, ReqOpts), + begin + {ok, SRef} = snabbkaffe:subscribe( + ?match_event(#{?snk_kind := connector_demo_inc_counter_async}), + Num, + _Timeout = 60_000 + ), + inc_counter_in_parallel(Num, ReqOpts), + {ok, _} = snabbkaffe:receive_events(SRef), + ok + end, fun(Trace) -> QueryTrace = ?of_kind(call_query_async, Trace), - ?assertMatch([#{query := {query, _, {inc_counter, 1}, _}} | _], QueryTrace) + ?assertMatch([#{query := {query, _, {inc_counter, _}, _}} | _], QueryTrace) end ), - timer:sleep(1000), - ?assertEqual(WindowSize + Num, ets:info(Tab0, size)), + ?assertEqual(WindowSize + Num, ets:info(Tab0, size), #{tab => ets:tab2list(Tab0)}), + tap_metrics(?LINE), %% block the resource ?assertMatch(ok, emqx_resource:simple_sync_query(?ID, block)), @@ -411,27 +505,253 @@ t_query_counter_async_inflight(_) -> ok = emqx_resource:query(?ID, {inc_counter, 1}), Sent = WindowSize + Num + WindowSize, + {ok, SRef1} = snabbkaffe:subscribe( + ?match_event(#{?snk_kind := connector_demo_inc_counter_async}), + WindowSize, + _Timeout = 60_000 + ), ?assertMatch(ok, emqx_resource:simple_sync_query(?ID, resume)), - timer:sleep(1000), + {ok, _} = snabbkaffe:receive_events(SRef1), ?assertEqual(Sent, ets:info(Tab0, size)), {ok, Counter} = emqx_resource:simple_sync_query(?ID, get_counter), ct:pal("get_counter: ~p, sent: ~p", [Counter, Sent]), ?assert(Sent =< Counter), - {ok, _, #{metrics := #{counters := C}}} = emqx_resource:get_instance(?ID), - ct:pal("metrics: ~p", [C]), - {ok, IncorrectStatusCount} = emqx_resource:simple_sync_query(?ID, get_incorrect_status_count), - %% The `simple_sync_query' we just did also increases the matched - %% count, hence the + 1. - ExtraSimpleCallCount = IncorrectStatusCount + 1, + %% give the metrics some time to stabilize. + ct:sleep(1000), + #{counters := C, gauges := G} = tap_metrics(?LINE), ?assertMatch( - #{matched := M, success := Ss, dropped := Dp, 'retried.success' := Rs} when - M == Ss + Dp - Rs + ExtraSimpleCallCount, - C, #{ - metrics => C, - extra_simple_call_count => ExtraSimpleCallCount + counters := + #{matched := M, success := Ss, dropped := Dp}, + gauges := #{queuing := Qing, inflight := Infl} + } when + M == Ss + Dp + Qing + Infl, + #{counters => C, gauges => G}, + #{ + metrics => #{counters => C, gauges => G}, + results => ets:tab2list(Tab0), + metrics_trace => ets:tab2list(MetricsTab) + } + ), + ?assert( + lists:all( + fun + ({_, ok}) -> true; + (_) -> false + end, + ets:tab2list(Tab0) + ) + ), + ok = emqx_resource:remove_local(?ID). + +t_query_counter_async_inflight_batch(_) -> + emqx_connector_demo:set_callback_mode(async_if_possible), + MetricsTab = ets:new(metrics_tab, [ordered_set, public]), + ok = telemetry:attach_many( + ?FUNCTION_NAME, + emqx_resource_metrics:events(), + fun(Event, Measurements, Meta, _Config) -> + ets:insert( + MetricsTab, + {erlang:monotonic_time(), #{ + event => Event, measurements => Measurements, metadata => Meta + }} + ), + ok + end, + unused_config + ), + on_exit(fun() -> telemetry:detach(?FUNCTION_NAME) end), + + Tab0 = ets:new(?FUNCTION_NAME, [bag, public]), + Insert0 = fun(Tab, Ref, Result) -> + ct:pal("inserting ~p", [{Ref, Result}]), + ets:insert(Tab, {Ref, Result}) + end, + ReqOpts = fun() -> #{async_reply_fun => {Insert0, [Tab0, make_ref()]}} end, + BatchSize = 2, + WindowSize = 3, + {ok, _} = emqx_resource:create_local( + ?ID, + ?DEFAULT_RESOURCE_GROUP, + ?TEST_RESOURCE, + #{name => test_resource, register => true}, + #{ + query_mode => async, + batch_size => BatchSize, + async_inflight_window => WindowSize, + worker_pool_size => 1, + resume_interval => 300 + } + ), + ?assertMatch({ok, 0}, emqx_resource:simple_sync_query(?ID, get_counter)), + + %% block the resource + ?assertMatch(ok, emqx_resource:simple_sync_query(?ID, block)), + + %% send async query to make the inflight window full + NumMsgs = BatchSize * WindowSize, + ?check_trace( + begin + {ok, SRef} = snabbkaffe:subscribe( + ?match_event(#{?snk_kind := call_batch_query_async}), + WindowSize, + _Timeout = 60_000 + ), + inc_counter_in_parallel(NumMsgs, ReqOpts), + {ok, _} = snabbkaffe:receive_events(SRef), + ok + end, + fun(Trace) -> + QueryTrace = ?of_kind(call_batch_query_async, Trace), + ?assertMatch( + [ + #{ + batch := [ + {query, _, {inc_counter, 1}, _}, + {query, _, {inc_counter, 1}, _} + ] + } + | _ + ], + QueryTrace + ) + end + ), + tap_metrics(?LINE), + + ?check_trace( + begin + %% this will block the resource_worker as the inflight window is full now + {ok, {ok, _}} = + ?wait_async_action( + emqx_resource:query(?ID, {inc_counter, 2}), + #{?snk_kind := resource_worker_enter_blocked}, + 5_000 + ), + ?assertMatch(0, ets:info(Tab0, size)), + ok + end, + [] + ), + + tap_metrics(?LINE), + %% send query now will fail because the resource is blocked. + Insert = fun(Tab, Ref, Result) -> + ct:pal("inserting ~p", [{Ref, Result}]), + ets:insert(Tab, {Ref, Result}), + ?tp(tmp_query_inserted, #{}) + end, + {ok, {ok, _}} = + ?wait_async_action( + emqx_resource:query(?ID, {inc_counter, 3}, #{ + async_reply_fun => {Insert, [Tab0, tmp_query]} + }), + #{?snk_kind := tmp_query_inserted}, + 1_000 + ), + %% since this counts as a failure, it'll be enqueued and retried + %% later, when the resource is unblocked. + ?assertMatch([{_, {error, {resource_error, #{reason := blocked}}}}], ets:take(Tab0, tmp_query)), + tap_metrics(?LINE), + + %% all responses should be received after the resource is resumed. + {ok, SRef0} = snabbkaffe:subscribe( + ?match_event(#{?snk_kind := connector_demo_inc_counter_async}), + %% +1 because the tmp_query above will be retried and succeed + %% this time. + WindowSize + 1, + _Timeout = 60_000 + ), + ?assertMatch(ok, emqx_resource:simple_sync_query(?ID, resume)), + tap_metrics(?LINE), + {ok, _} = snabbkaffe:receive_events(SRef0), + %% since the previous tmp_query was enqueued to be retried, we + %% take it again from the table; this time, it should have + %% succeeded. + ?assertMatch([{tmp_query, ok}], ets:take(Tab0, tmp_query)), + ?assertEqual(NumMsgs, ets:info(Tab0, size), #{tab => ets:tab2list(Tab0)}), + tap_metrics(?LINE), + + %% send async query, this time everything should be ok. + NumBatches1 = 3, + NumMsgs1 = BatchSize * NumBatches1, + ?check_trace( + ?TRACE_OPTS, + begin + {ok, SRef} = snabbkaffe:subscribe( + ?match_event(#{?snk_kind := connector_demo_inc_counter_async}), + NumBatches1, + _Timeout = 60_000 + ), + inc_counter_in_parallel(NumMsgs1, ReqOpts), + {ok, _} = snabbkaffe:receive_events(SRef), + ok + end, + fun(Trace) -> + QueryTrace = ?of_kind(call_batch_query_async, Trace), + ?assertMatch( + [#{batch := [{query, _, {inc_counter, _}, _} | _]} | _], + QueryTrace + ) + end + ), + ?assertEqual( + NumMsgs + NumMsgs1, + ets:info(Tab0, size), + #{tab => ets:tab2list(Tab0)} + ), + tap_metrics(?LINE), + + %% block the resource + ?assertMatch(ok, emqx_resource:simple_sync_query(?ID, block)), + %% again, send async query to make the inflight window full + ?check_trace( + ?TRACE_OPTS, + inc_counter_in_parallel(WindowSize, ReqOpts), + fun(Trace) -> + QueryTrace = ?of_kind(call_batch_query_async, Trace), + ?assertMatch( + [#{batch := [{query, _, {inc_counter, _}, _} | _]} | _], + QueryTrace + ) + end + ), + + %% this will block the resource_worker + ok = emqx_resource:query(?ID, {inc_counter, 1}), + + Sent = NumMsgs + NumMsgs1 + WindowSize, + {ok, SRef1} = snabbkaffe:subscribe( + ?match_event(#{?snk_kind := connector_demo_inc_counter_async}), + WindowSize, + _Timeout = 60_000 + ), + ?assertMatch(ok, emqx_resource:simple_sync_query(?ID, resume)), + {ok, _} = snabbkaffe:receive_events(SRef1), + ?assertEqual(Sent, ets:info(Tab0, size)), + + {ok, Counter} = emqx_resource:simple_sync_query(?ID, get_counter), + ct:pal("get_counter: ~p, sent: ~p", [Counter, Sent]), + ?assert(Sent =< Counter), + + %% give the metrics some time to stabilize. + ct:sleep(1000), + #{counters := C, gauges := G} = tap_metrics(?LINE), + ?assertMatch( + #{ + counters := + #{matched := M, success := Ss, dropped := Dp}, + gauges := #{queuing := Qing, inflight := Infl} + } when + M == Ss + Dp + Qing + Infl, + #{counters => C, gauges => G}, + #{ + metrics => #{counters => C, gauges => G}, + results => ets:tab2list(Tab0), + metrics_trace => ets:tab2list(MetricsTab) } ), ?assert( @@ -506,9 +826,9 @@ t_stop_start(_) -> %% add some metrics to test their persistence WorkerID0 = <<"worker:0">>, WorkerID1 = <<"worker:1">>, - emqx_resource_metrics:batching_set(?ID, WorkerID0, 2), - emqx_resource_metrics:batching_set(?ID, WorkerID1, 3), - ?assertEqual(5, emqx_resource_metrics:batching_get(?ID)), + emqx_resource_metrics:inflight_set(?ID, WorkerID0, 2), + emqx_resource_metrics:inflight_set(?ID, WorkerID1, 3), + ?assertEqual(5, emqx_resource_metrics:inflight_get(?ID)), {ok, _} = emqx_resource:check_and_recreate( ?ID, @@ -522,7 +842,7 @@ t_stop_start(_) -> ?assert(is_process_alive(Pid0)), %% metrics are reset when recreating - ?assertEqual(0, emqx_resource_metrics:batching_get(?ID)), + ?assertEqual(0, emqx_resource_metrics:inflight_get(?ID)), ok = emqx_resource:stop(?ID), @@ -541,11 +861,11 @@ t_stop_start(_) -> ?assert(is_process_alive(Pid1)), %% now stop while resetting the metrics - emqx_resource_metrics:batching_set(?ID, WorkerID0, 1), - emqx_resource_metrics:batching_set(?ID, WorkerID1, 4), - ?assertEqual(5, emqx_resource_metrics:batching_get(?ID)), + emqx_resource_metrics:inflight_set(?ID, WorkerID0, 1), + emqx_resource_metrics:inflight_set(?ID, WorkerID1, 4), + ?assertEqual(5, emqx_resource_metrics:inflight_get(?ID)), ok = emqx_resource:stop(?ID), - ?assertEqual(0, emqx_resource_metrics:batching_get(?ID)), + ?assertEqual(0, emqx_resource_metrics:inflight_get(?ID)), ok. @@ -641,18 +961,22 @@ create_dry_run_local_succ() -> ?assertEqual(undefined, whereis(test_resource)). t_create_dry_run_local_failed(_) -> + ct:timetrap({seconds, 120}), + ct:pal("creating with creation error"), Res1 = emqx_resource:create_dry_run_local( ?TEST_RESOURCE, #{create_error => true} ), ?assertMatch({error, _}, Res1), + ct:pal("creating with health check error"), Res2 = emqx_resource:create_dry_run_local( ?TEST_RESOURCE, #{name => test_resource, health_check_error => true} ), ?assertMatch({error, _}, Res2), + ct:pal("creating with stop error"), Res3 = emqx_resource:create_dry_run_local( ?TEST_RESOURCE, #{name => test_resource, stop_error => true} @@ -689,16 +1013,116 @@ t_auto_retry(_) -> ), ?assertEqual(ok, Res). +t_retry_batch(_Config) -> + {ok, _} = emqx_resource:create( + ?ID, + ?DEFAULT_RESOURCE_GROUP, + ?TEST_RESOURCE, + #{name => test_resource}, + #{ + query_mode => async, + batch_size => 5, + worker_pool_size => 1, + resume_interval => 1_000 + } + ), + + ?assertMatch(ok, emqx_resource:simple_sync_query(?ID, block)), + Matched0 = emqx_resource_metrics:matched_get(?ID), + ?assertEqual(1, Matched0), + + %% these requests will batch together and fail; the buffer worker + %% will enter the `blocked' state and they'll be retried later, + %% after it unblocks. + Payloads = lists:seq(1, 5), + NumPayloads = length(Payloads), + ExpectedCount = 15, + + ?check_trace( + begin + {ok, {ok, _}} = + ?wait_async_action( + lists:foreach( + fun(N) -> + ok = emqx_resource:query(?ID, {inc_counter, N}) + end, + Payloads + ), + #{?snk_kind := resource_worker_enter_blocked}, + 5_000 + ), + %% now the individual messages should have been counted + Matched1 = emqx_resource_metrics:matched_get(?ID), + ?assertEqual(Matched0 + NumPayloads, Matched1), + + %% wait for two more retries while the failure is enabled; the + %% batch shall remain enqueued. + {ok, _} = + snabbkaffe:block_until( + ?match_n_events(2, #{?snk_kind := resource_worker_retry_queue_batch_failed}), + 5_000 + ), + %% should not have increased the matched count with the retries + Matched2 = emqx_resource_metrics:matched_get(?ID), + ?assertEqual(Matched1, Matched2), + + %% now unblock the buffer worker so it may retry the batch, + %% but it'll still fail + {ok, {ok, _}} = + ?wait_async_action( + ok = emqx_resource:simple_sync_query(?ID, resume), + #{?snk_kind := resource_worker_retry_queue_batch_succeeded}, + 5_000 + ), + %% 1 more because of the `resume' call + Matched3 = emqx_resource_metrics:matched_get(?ID), + ?assertEqual(Matched2 + 1, Matched3), + + {ok, Counter} = emqx_resource:simple_sync_query(?ID, get_counter), + {Counter, Matched3} + end, + fun({Counter, Matched3}, Trace) -> + %% 1 original attempt + 2 failed retries + final + %% successful attempt. + %% each time should be the original batch (no duplicate + %% elements or reordering). + ExpectedSeenPayloads = lists:flatten(lists:duplicate(4, Payloads)), + ?assertEqual( + ExpectedSeenPayloads, + ?projection(n, ?of_kind(connector_demo_batch_inc_individual, Trace)) + ), + ?assertMatch( + [#{n := ExpectedCount}], + ?of_kind(connector_demo_inc_counter, Trace) + ), + ?assertEqual(ExpectedCount, Counter), + %% matched should count only the original requests, and not retries + %% + 1 for `resume' call + %% + 1 for `block' call + %% + 1 for `get_counter' call + %% and the message count (1 time) + Matched4 = emqx_resource_metrics:matched_get(?ID), + ?assertEqual(Matched3 + 1, Matched4), + ok + end + ), + ok. + %%------------------------------------------------------------------------------ %% Helpers %%------------------------------------------------------------------------------ inc_counter_in_parallel(N) -> inc_counter_in_parallel(N, #{}). -inc_counter_in_parallel(N, Opts) -> +inc_counter_in_parallel(N, Opts0) -> Parent = self(), Pids = [ erlang:spawn(fun() -> + Opts = + case is_function(Opts0) of + true -> Opts0(); + false -> Opts0 + end, emqx_resource:query(?ID, {inc_counter, 1}, Opts), Parent ! {complete, self()} end) @@ -719,3 +1143,8 @@ bin_config() -> config() -> {ok, Config} = hocon:binary(bin_config()), Config. + +tap_metrics(Line) -> + {ok, _, #{metrics := #{counters := C, gauges := G}}} = emqx_resource:get_instance(?ID), + ct:pal("metrics (l. ~b): ~p", [Line, #{counters => C, gauges => G}]), + #{counters => C, gauges => G}. diff --git a/changes/v5.0.14/feat-9642.en.md b/changes/v5.0.14/feat-9642.en.md new file mode 100644 index 000000000..19de4d946 --- /dev/null +++ b/changes/v5.0.14/feat-9642.en.md @@ -0,0 +1 @@ +Deprecates `enable_batch` and `enable_queue` options for bridges/resources. After this change, queuing is always enabled for bridges, and batching is controlled by the `batch_size` option: `batch_size > 1` means batching will be enabled. diff --git a/changes/v5.0.14/feat-9642.zh.md b/changes/v5.0.14/feat-9642.zh.md new file mode 100644 index 000000000..9db3acc47 --- /dev/null +++ b/changes/v5.0.14/feat-9642.zh.md @@ -0,0 +1 @@ +废弃了桥梁/资源的`enable_batch`和`enable_queue`选项 。 在这一改变之后,队列总是被启用,而批处理由`batch_size`选项控制:`batch_size > 1`意味着批处理将被启用。 diff --git a/changes/v5.0.14/fix-9642.en.md b/changes/v5.0.14/fix-9642.en.md new file mode 100644 index 000000000..12906299e --- /dev/null +++ b/changes/v5.0.14/fix-9642.en.md @@ -0,0 +1,3 @@ +Fix some issues that could lead to wrong bridge metrics. +Fix and issue that could lead to message loss and wrong metrics with Kafka Producer bridge when Kafka or the connection to it is down. +Fix some issues that could lead to the same message being delivered more than once when using batching for bridges and when the batch was retried. diff --git a/changes/v5.0.14/fix-9642.zh.md b/changes/v5.0.14/fix-9642.zh.md new file mode 100644 index 000000000..2565c422c --- /dev/null +++ b/changes/v5.0.14/fix-9642.zh.md @@ -0,0 +1,3 @@ +修复一些可能导致错误桥接指标的问题。 +修复当Kafka或其连接中断时,可能导致Kafka Producer桥的消息丢失和错误指标的问题。 +修复一些问题,这些问题可能导致在为桥接使用批处理时,同一消息被多次传递,以及批处理被重试时。 diff --git a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_mysql.erl b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_mysql.erl index c445e9a56..bf5d2e140 100644 --- a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_mysql.erl +++ b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_mysql.erl @@ -58,11 +58,9 @@ values(post) -> worker_pool_size => 1, health_check_interval => ?HEALTHCHECK_INTERVAL_RAW, auto_restart_interval => ?AUTO_RESTART_INTERVAL_RAW, - enable_batch => true, batch_size => ?DEFAULT_BATCH_SIZE, batch_time => ?DEFAULT_BATCH_TIME, query_mode => async, - enable_queue => false, max_queue_bytes => ?DEFAULT_QUEUE_SIZE } }; diff --git a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_redis.erl b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_redis.erl index 42c4d12e8..727a6df4b 100644 --- a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_redis.erl +++ b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_redis.erl @@ -79,8 +79,7 @@ values(common, RedisType, SpecificOpts) -> auto_reconnect => true, command_template => [<<"LPUSH">>, <<"MSGS">>, <<"${payload}">>], resource_opts => #{ - enable_batch => false, - batch_size => 100, + batch_size => 1, batch_time => <<"20ms">> }, ssl => #{enable => false} diff --git a/lib-ee/emqx_ee_bridge/test/emqx_bridge_impl_kafka_producer_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_bridge_impl_kafka_producer_SUITE.erl index da4c1007a..bdde21c76 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_bridge_impl_kafka_producer_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_bridge_impl_kafka_producer_SUITE.erl @@ -287,11 +287,9 @@ kafka_bridge_rest_api_helper(Config) -> ?assertEqual(0, emqx_resource_metrics:dropped_get(ResourceId)), ?assertEqual(0, emqx_resource_metrics:failed_get(ResourceId)), ?assertEqual(0, emqx_resource_metrics:inflight_get(ResourceId)), - ?assertEqual(0, emqx_resource_metrics:batching_get(ResourceId)), ?assertEqual(0, emqx_resource_metrics:queuing_get(ResourceId)), ?assertEqual(0, emqx_resource_metrics:dropped_other_get(ResourceId)), ?assertEqual(0, emqx_resource_metrics:dropped_queue_full_get(ResourceId)), - ?assertEqual(0, emqx_resource_metrics:dropped_queue_not_enabled_get(ResourceId)), ?assertEqual(0, emqx_resource_metrics:dropped_resource_not_found_get(ResourceId)), ?assertEqual(0, emqx_resource_metrics:dropped_resource_stopped_get(ResourceId)), ?assertEqual(0, emqx_resource_metrics:retried_get(ResourceId)), @@ -314,11 +312,9 @@ kafka_bridge_rest_api_helper(Config) -> ?assertEqual(0, emqx_resource_metrics:dropped_get(ResourceId)), ?assertEqual(0, emqx_resource_metrics:failed_get(ResourceId)), ?assertEqual(0, emqx_resource_metrics:inflight_get(ResourceId)), - ?assertEqual(0, emqx_resource_metrics:batching_get(ResourceId)), ?assertEqual(0, emqx_resource_metrics:queuing_get(ResourceId)), ?assertEqual(0, emqx_resource_metrics:dropped_other_get(ResourceId)), ?assertEqual(0, emqx_resource_metrics:dropped_queue_full_get(ResourceId)), - ?assertEqual(0, emqx_resource_metrics:dropped_queue_not_enabled_get(ResourceId)), ?assertEqual(0, emqx_resource_metrics:dropped_resource_not_found_get(ResourceId)), ?assertEqual(0, emqx_resource_metrics:dropped_resource_stopped_get(ResourceId)), ?assertEqual(0, emqx_resource_metrics:retried_get(ResourceId)), diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_gcp_pubsub_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_gcp_pubsub_SUITE.erl index 70125596c..91fd2f399 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_gcp_pubsub_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_gcp_pubsub_SUITE.erl @@ -38,18 +38,12 @@ groups() -> {group, sync_query}, {group, async_query} ], - QueueGroups = [ - {group, queue_enabled}, - {group, queue_disabled} - ], ResourceGroups = [{group, gcp_pubsub}], [ {with_batch, SynchronyGroups}, {without_batch, SynchronyGroups}, - {sync_query, QueueGroups}, - {async_query, QueueGroups}, - {queue_enabled, ResourceGroups}, - {queue_disabled, ResourceGroups}, + {sync_query, ResourceGroups}, + {async_query, ResourceGroups}, {gcp_pubsub, MatrixTCs} ]. @@ -99,13 +93,9 @@ init_per_group(sync_query, Config) -> init_per_group(async_query, Config) -> [{query_mode, async} | Config]; init_per_group(with_batch, Config) -> - [{enable_batch, true} | Config]; + [{batch_size, 100} | Config]; init_per_group(without_batch, Config) -> - [{enable_batch, false} | Config]; -init_per_group(queue_enabled, Config) -> - [{enable_queue, true} | Config]; -init_per_group(queue_disabled, Config) -> - [{enable_queue, false} | Config]; + [{batch_size, 1} | Config]; init_per_group(_Group, Config) -> Config. @@ -118,16 +108,16 @@ end_per_group(_Group, _Config) -> init_per_testcase(TestCase, Config0) when TestCase =:= t_publish_success_batch -> - case ?config(enable_batch, Config0) of - true -> + case ?config(batch_size, Config0) of + 1 -> + {skip, no_batching}; + _ -> {ok, _} = start_echo_http_server(), delete_all_bridges(), Tid = install_telemetry_handler(TestCase), Config = generate_config(Config0), put(telemetry_table, Tid), - [{telemetry_table, Tid} | Config]; - false -> - {skip, no_batching} + [{telemetry_table, Tid} | Config] end; init_per_testcase(TestCase, Config0) -> {ok, _} = start_echo_http_server(), @@ -271,9 +261,7 @@ certs() -> ]. gcp_pubsub_config(Config) -> - EnableBatch = proplists:get_value(enable_batch, Config, true), QueryMode = proplists:get_value(query_mode, Config, sync), - EnableQueue = proplists:get_value(enable_queue, Config, false), BatchSize = proplists:get_value(batch_size, Config, 100), BatchTime = proplists:get_value(batch_time, Config, <<"20ms">>), PayloadTemplate = proplists:get_value(payload_template, Config, ""), @@ -296,9 +284,7 @@ gcp_pubsub_config(Config) -> " pipelining = ~b\n" " resource_opts = {\n" " worker_pool_size = 1\n" - " enable_batch = ~p\n" " query_mode = ~s\n" - " enable_queue = ~p\n" " batch_size = ~b\n" " batch_time = \"~s\"\n" " }\n" @@ -309,9 +295,7 @@ gcp_pubsub_config(Config) -> PayloadTemplate, PubSubTopic, PipelineSize, - EnableBatch, QueryMode, - EnableQueue, BatchSize, BatchTime ] @@ -358,11 +342,9 @@ service_account_json(PrivateKeyPEM) -> metrics_mapping() -> #{ - batching => fun emqx_resource_metrics:batching_get/1, dropped => fun emqx_resource_metrics:dropped_get/1, dropped_other => fun emqx_resource_metrics:dropped_other_get/1, dropped_queue_full => fun emqx_resource_metrics:dropped_queue_full_get/1, - dropped_queue_not_enabled => fun emqx_resource_metrics:dropped_queue_not_enabled_get/1, dropped_resource_not_found => fun emqx_resource_metrics:dropped_resource_not_found_get/1, dropped_resource_stopped => fun emqx_resource_metrics:dropped_resource_stopped_get/1, failed => fun emqx_resource_metrics:failed_get/1, @@ -625,7 +607,6 @@ t_publish_success(Config) -> ), assert_metrics( #{ - batching => 0, dropped => 0, failed => 0, inflight => 0, @@ -674,7 +655,6 @@ t_publish_success_local_topic(Config) -> ), assert_metrics( #{ - batching => 0, dropped => 0, failed => 0, inflight => 0, @@ -761,7 +741,6 @@ t_publish_templated(Config) -> ), assert_metrics( #{ - batching => 0, dropped => 0, failed => 0, inflight => 0, @@ -830,11 +809,10 @@ t_publish_success_batch(Config) -> wait_until_gauge_is(inflight, 0, _Timeout = 400), assert_metrics( #{ - batching => 0, dropped => 0, failed => 0, inflight => 0, - matched => NumMessages div BatchSize, + matched => NumMessages, queuing => 0, retried => 0, success => NumMessages @@ -1013,7 +991,6 @@ t_publish_timeout(Config) -> do_econnrefused_or_timeout_test(Config, timeout). do_econnrefused_or_timeout_test(Config, Error) -> - EnableQueue = ?config(enable_queue, Config), QueryMode = ?config(query_mode, Config), ResourceId = ?config(resource_id, Config), TelemetryTable = ?config(telemetry_table, Config), @@ -1089,39 +1066,22 @@ do_econnrefused_or_timeout_test(Config, Error) -> end ), - case {Error, QueryMode, EnableQueue} of - {_, sync, false} -> - wait_telemetry_event(TelemetryTable, dropped_queue_not_enabled, ResourceId, #{ - timeout => 10_000, - n_events => 1 - }), - assert_metrics( - #{ - batching => 0, - dropped => 1, - dropped_queue_not_enabled => 1, - failed => 0, - inflight => 0, - matched => 1, - queuing => 0, - retried => 0, - success => 0 - }, - ResourceId - ); + case {Error, QueryMode} of %% apparently, async with disabled queue doesn't mark the %% message as dropped; and since it never considers the %% response expired, this succeeds. - {econnrefused, async, _} -> + {econnrefused, async} -> wait_telemetry_event(TelemetryTable, queuing, ResourceId, #{ timeout => 10_000, n_events => 1 }), + %% even waiting, hard to avoid flakiness... simpler to just sleep + %% a bit until stabilization. + ct:sleep(200), CurrentMetrics = current_metrics(ResourceId), RecordedEvents = ets:tab2list(TelemetryTable), ct:pal("telemetry events: ~p", [RecordedEvents]), ?assertMatch( #{ - batching := 0, dropped := Dropped, failed := 0, inflight := Inflight, @@ -1132,7 +1092,7 @@ do_econnrefused_or_timeout_test(Config, Error) -> } when Matched >= 1 andalso Inflight + Queueing + Dropped =< 2, CurrentMetrics ); - {timeout, async, _} -> + {timeout, async} -> wait_telemetry_event(TelemetryTable, success, ResourceId, #{ timeout => 10_000, n_events => 2 }), @@ -1140,7 +1100,6 @@ do_econnrefused_or_timeout_test(Config, Error) -> wait_until_gauge_is(queuing, 0, _Timeout = 400), assert_metrics( #{ - batching => 0, dropped => 0, failed => 0, inflight => 0, @@ -1151,13 +1110,15 @@ do_econnrefused_or_timeout_test(Config, Error) -> }, ResourceId ); - {_, sync, true} -> + {_, sync} -> wait_telemetry_event(TelemetryTable, queuing, ResourceId, #{ timeout => 10_000, n_events => 2 }), + %% even waiting, hard to avoid flakiness... simpler to just sleep + %% a bit until stabilization. + ct:sleep(200), assert_metrics( #{ - batching => 0, dropped => 0, failed => 0, inflight => 0, @@ -1364,9 +1325,11 @@ t_unrecoverable_error(Config) -> ResourceId, #{n_events => ExpectedInflightEvents, timeout => 5_000} ), + %% even waiting, hard to avoid flakiness... simpler to just sleep + %% a bit until stabilization. + ct:sleep(200), assert_metrics( #{ - batching => 0, dropped => 0, failed => 1, inflight => 0, diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_influxdb_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_influxdb_SUITE.erl index f2037ba14..6331611d0 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_influxdb_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_influxdb_SUITE.erl @@ -204,9 +204,9 @@ init_per_group(sync_query, Config) -> init_per_group(async_query, Config) -> [{query_mode, async} | Config]; init_per_group(with_batch, Config) -> - [{enable_batch, true} | Config]; + [{batch_size, 100} | Config]; init_per_group(without_batch, Config) -> - [{enable_batch, false} | Config]; + [{batch_size, 1} | Config]; init_per_group(_Group, Config) -> Config. @@ -261,7 +261,6 @@ example_write_syntax() -> "${undef_key}=\"hard-coded-value\",", "bool=${payload.bool}">>. influxdb_config(apiv1 = Type, InfluxDBHost, InfluxDBPort, Config) -> - EnableBatch = proplists:get_value(enable_batch, Config, true), BatchSize = proplists:get_value(batch_size, Config, 100), QueryMode = proplists:get_value(query_mode, Config, sync), UseTLS = proplists:get_value(use_tls, Config, false), @@ -278,7 +277,6 @@ influxdb_config(apiv1 = Type, InfluxDBHost, InfluxDBPort, Config) -> " precision = ns\n" " write_syntax = \"~s\"\n" " resource_opts = {\n" - " enable_batch = ~p\n" " query_mode = ~s\n" " batch_size = ~b\n" " }\n" @@ -292,7 +290,6 @@ influxdb_config(apiv1 = Type, InfluxDBHost, InfluxDBPort, Config) -> InfluxDBHost, InfluxDBPort, WriteSyntax, - EnableBatch, QueryMode, BatchSize, UseTLS @@ -300,7 +297,6 @@ influxdb_config(apiv1 = Type, InfluxDBHost, InfluxDBPort, Config) -> ), {Name, ConfigString, parse_and_check(ConfigString, Type, Name)}; influxdb_config(apiv2 = Type, InfluxDBHost, InfluxDBPort, Config) -> - EnableBatch = proplists:get_value(enable_batch, Config, true), BatchSize = proplists:get_value(batch_size, Config, 100), QueryMode = proplists:get_value(query_mode, Config, sync), UseTLS = proplists:get_value(use_tls, Config, false), @@ -317,7 +313,6 @@ influxdb_config(apiv2 = Type, InfluxDBHost, InfluxDBPort, Config) -> " precision = ns\n" " write_syntax = \"~s\"\n" " resource_opts = {\n" - " enable_batch = ~p\n" " query_mode = ~s\n" " batch_size = ~b\n" " }\n" @@ -331,7 +326,6 @@ influxdb_config(apiv2 = Type, InfluxDBHost, InfluxDBPort, Config) -> InfluxDBHost, InfluxDBPort, WriteSyntax, - EnableBatch, QueryMode, BatchSize, UseTLS @@ -723,7 +717,7 @@ t_bad_timestamp(Config) -> InfluxDBType = ?config(influxdb_type, Config), InfluxDBName = ?config(influxdb_name, Config), QueryMode = ?config(query_mode, Config), - EnableBatch = ?config(enable_batch, Config), + BatchSize = ?config(batch_size, Config), InfluxDBConfigString0 = ?config(influxdb_config_string, Config), InfluxDBTypeCfg = case InfluxDBType of @@ -774,7 +768,8 @@ t_bad_timestamp(Config) -> fun(Result, Trace) -> ?assertMatch({_, {ok, _}}, Result), {Return, {ok, _}} = Result, - case {QueryMode, EnableBatch} of + IsBatch = BatchSize > 1, + case {QueryMode, IsBatch} of {async, true} -> ?assertEqual(ok, Return), ?assertMatch( @@ -921,12 +916,13 @@ t_write_failure(Config) -> t_missing_field(Config) -> QueryMode = ?config(query_mode, Config), - EnableBatch = ?config(enable_batch, Config), + BatchSize = ?config(batch_size, Config), + IsBatch = BatchSize > 1, {ok, _} = create_bridge( Config, #{ - <<"resource_opts">> => #{<<"batch_size">> => 1}, + <<"resource_opts">> => #{<<"worker_pool_size">> => 1}, <<"write_syntax">> => <<"${clientid} foo=${foo}i">> } ), @@ -943,9 +939,14 @@ t_missing_field(Config) -> begin emqx:publish(Msg0), emqx:publish(Msg1), + NEvents = + case IsBatch of + true -> 1; + false -> 2 + end, {ok, _} = snabbkaffe:block_until( - ?match_n_events(2, #{ + ?match_n_events(NEvents, #{ ?snk_kind := influxdb_connector_send_query_error, mode := QueryMode }), @@ -956,10 +957,10 @@ t_missing_field(Config) -> fun(Trace) -> PersistedData0 = query_by_clientid(ClientId0, Config), PersistedData1 = query_by_clientid(ClientId1, Config), - case EnableBatch of + case IsBatch of true -> ?assertMatch( - [#{error := points_trans_failed}, #{error := points_trans_failed} | _], + [#{error := points_trans_failed} | _], ?of_kind(influxdb_connector_send_query_error, Trace) ); false -> diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_mysql_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_mysql_SUITE.erl index caace762a..812c4ee85 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_mysql_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_mysql_SUITE.erl @@ -78,10 +78,10 @@ init_per_group(tls, Config) -> | Config ]; init_per_group(with_batch, Config0) -> - Config = [{enable_batch, true} | Config0], + Config = [{batch_size, 100} | Config0], common_init(Config); init_per_group(without_batch, Config0) -> - Config = [{enable_batch, false} | Config0], + Config = [{batch_size, 1} | Config0], common_init(Config); init_per_group(_Group, Config) -> Config. @@ -157,7 +157,7 @@ mysql_config(BridgeType, Config) -> MysqlPort = integer_to_list(?config(mysql_port, Config)), Server = ?config(mysql_host, Config) ++ ":" ++ MysqlPort, Name = atom_to_binary(?MODULE), - EnableBatch = ?config(enable_batch, Config), + BatchSize = ?config(batch_size, Config), QueryMode = ?config(query_mode, Config), TlsEnabled = ?config(enable_tls, Config), ConfigString = @@ -170,7 +170,7 @@ mysql_config(BridgeType, Config) -> " password = ~p\n" " sql = ~p\n" " resource_opts = {\n" - " enable_batch = ~p\n" + " batch_size = ~b\n" " query_mode = ~s\n" " }\n" " ssl = {\n" @@ -185,7 +185,7 @@ mysql_config(BridgeType, Config) -> ?MYSQL_USERNAME, ?MYSQL_PASSWORD, ?SQL_BRIDGE, - EnableBatch, + BatchSize, QueryMode, TlsEnabled ] @@ -440,7 +440,9 @@ t_simple_sql_query(Config) -> ), Request = {sql, <<"SELECT count(1) AS T">>}, Result = query_resource(Config, Request), - case ?config(enable_batch, Config) of + BatchSize = ?config(batch_size, Config), + IsBatch = BatchSize > 1, + case IsBatch of true -> ?assertEqual({error, batch_select_not_implemented}, Result); false -> ?assertEqual({ok, [<<"T">>], [[1]]}, Result) end, @@ -452,7 +454,9 @@ t_missing_data(Config) -> create_bridge(Config) ), Result = send_message(Config, #{}), - case ?config(enable_batch, Config) of + BatchSize = ?config(batch_size, Config), + IsBatch = BatchSize > 1, + case IsBatch of true -> ?assertMatch( {error, {1292, _, <<"Truncated incorrect DOUBLE value: 'undefined'">>}}, Result @@ -469,7 +473,9 @@ t_bad_sql_parameter(Config) -> ), Request = {sql, <<"">>, [bad_parameter]}, Result = query_resource(Config, Request), - case ?config(enable_batch, Config) of + BatchSize = ?config(batch_size, Config), + IsBatch = BatchSize > 1, + case IsBatch of true -> ?assertEqual({error, invalid_request}, Result); false -> ?assertEqual({error, {invalid_params, [bad_parameter]}}, Result) end, @@ -482,7 +488,9 @@ t_unprepared_statement_query(Config) -> ), Request = {prepared_query, unprepared_query, []}, Result = query_resource(Config, Request), - case ?config(enable_batch, Config) of + BatchSize = ?config(batch_size, Config), + IsBatch = BatchSize > 1, + case IsBatch of true -> ?assertEqual({error, invalid_request}, Result); false -> ?assertEqual({error, prepared_statement_invalid}, Result) end, diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_redis_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_redis_SUITE.erl index 6286a3718..f88bc42eb 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_redis_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_redis_SUITE.erl @@ -451,8 +451,6 @@ toxiproxy_redis_bridge_config() -> Conf0 = ?REDIS_TOXYPROXY_CONNECT_CONFIG#{ <<"resource_opts">> => #{ <<"query_mode">> => <<"async">>, - <<"enable_batch">> => <<"true">>, - <<"enable_queue">> => <<"true">>, <<"worker_pool_size">> => <<"1">>, <<"batch_size">> => integer_to_binary(?BATCH_SIZE), <<"health_check_interval">> => <<"1s">> @@ -465,8 +463,7 @@ invalid_command_bridge_config() -> Conf1 = maps:merge(Conf0, ?COMMON_REDIS_OPTS), Conf1#{ <<"resource_opts">> => #{ - <<"enable_batch">> => <<"false">>, - <<"enable_queue">> => <<"false">>, + <<"batch_size">> => <<"1">>, <<"worker_pool_size">> => <<"1">> }, <<"command_template">> => [<<"BAD">>, <<"COMMAND">>, <<"${payload}">>] @@ -476,13 +473,10 @@ resource_configs() -> #{ batch_off => #{ <<"query_mode">> => <<"sync">>, - <<"enable_batch">> => <<"false">>, - <<"enable_queue">> => <<"false">> + <<"batch_size">> => <<"1">> }, batch_on => #{ <<"query_mode">> => <<"async">>, - <<"enable_batch">> => <<"true">>, - <<"enable_queue">> => <<"true">>, <<"worker_pool_size">> => <<"1">>, <<"batch_size">> => integer_to_binary(?BATCH_SIZE) } diff --git a/scripts/ct/run.sh b/scripts/ct/run.sh index fc28c3a90..0a64794b6 100755 --- a/scripts/ct/run.sh +++ b/scripts/ct/run.sh @@ -69,6 +69,7 @@ done if [ "${WHICH_APP}" = 'novalue' ]; then echo "must provide --app arg" + help exit 1 fi From af31ed4264c08414bb09d026419536bfd0287d82 Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Mon, 2 Jan 2023 17:55:57 -0300 Subject: [PATCH 0028/1802] docs: improve descriptions Co-authored-by: Zaiming (Stone) Shi --- changes/v5.0.14/feat-9642.zh.md | 2 +- .../src/kafka/emqx_bridge_impl_kafka_producer.erl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/changes/v5.0.14/feat-9642.zh.md b/changes/v5.0.14/feat-9642.zh.md index 9db3acc47..e394abdbb 100644 --- a/changes/v5.0.14/feat-9642.zh.md +++ b/changes/v5.0.14/feat-9642.zh.md @@ -1 +1 @@ -废弃了桥梁/资源的`enable_batch`和`enable_queue`选项 。 在这一改变之后,队列总是被启用,而批处理由`batch_size`选项控制:`batch_size > 1`意味着批处理将被启用。 +废弃了桥接的 `enable_batch` 和 `enable_queue` 配置项 。在这一改变之后,桥接的工作进程总是启用缓存队列,而批处理由 `batch_size` 选项控制:`batch_size > 1` 则意味着启用批处理。 diff --git a/lib-ee/emqx_ee_bridge/src/kafka/emqx_bridge_impl_kafka_producer.erl b/lib-ee/emqx_ee_bridge/src/kafka/emqx_bridge_impl_kafka_producer.erl index a012a7ab0..a7de100d6 100644 --- a/lib-ee/emqx_ee_bridge/src/kafka/emqx_bridge_impl_kafka_producer.erl +++ b/lib-ee/emqx_ee_bridge/src/kafka/emqx_bridge_impl_kafka_producer.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved. +%% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved. %%-------------------------------------------------------------------- -module(emqx_bridge_impl_kafka_producer). From 56437228dcaafaf3d353076d769f0c4022619028 Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Tue, 3 Jan 2023 11:52:59 -0300 Subject: [PATCH 0029/1802] docs: improve descriptions Thanks to @qzhuyan for the corrections. --- apps/emqx_resource/i18n/emqx_resource_schema_i18n.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx_resource/i18n/emqx_resource_schema_i18n.conf b/apps/emqx_resource/i18n/emqx_resource_schema_i18n.conf index ae8b15ad7..52756f70d 100644 --- a/apps/emqx_resource/i18n/emqx_resource_schema_i18n.conf +++ b/apps/emqx_resource/i18n/emqx_resource_schema_i18n.conf @@ -127,7 +127,7 @@ When disabled the messages are buffered in RAM only.""" batch_size { desc { en: """Maximum batch count. If equal to 1, there's effectively no batching.""" - zh: """批量请求大小。如果等于1,实际上就没有批处理。""" + zh: """批量请求大小。如果设为1,则无批处理。""" } label { en: """Batch size""" From 70eb5ffb58ef8c3d3360528175b00a12a84d4c90 Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Wed, 4 Jan 2023 09:24:09 -0300 Subject: [PATCH 0030/1802] refactor: remove unused function --- .../src/emqx_resource_worker.erl | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/apps/emqx_resource/src/emqx_resource_worker.erl b/apps/emqx_resource/src/emqx_resource_worker.erl index 15b279f71..93bb22551 100644 --- a/apps/emqx_resource/src/emqx_resource_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_worker.erl @@ -32,7 +32,6 @@ sync_query/3, async_query/3, block/1, - block/2, resume/1 ]). @@ -69,7 +68,6 @@ -type id() :: binary(). -type index() :: pos_integer(). --type query() :: {query, request(), query_opts()}. -type queue_query() :: ?QUERY(from(), request(), HasBeenSent :: boolean()). -type request() :: term(). -type from() :: pid() | reply_fun() | request_from(). @@ -139,10 +137,6 @@ simple_async_query(Id, Request, ReplyFun) -> block(ServerRef) -> gen_statem:cast(ServerRef, block). --spec block(pid() | atom(), [query()]) -> ok. -block(ServerRef, Query) -> - gen_statem:cast(ServerRef, {block, Query}). - -spec resume(pid() | atom()) -> ok. resume(ServerRef) -> gen_statem:cast(ServerRef, resume). @@ -192,13 +186,6 @@ running(cast, resume, _St) -> keep_state_and_data; running(cast, block, St) -> {next_state, blocked, St}; -running( - cast, {block, [?QUERY(_, _, _) | _] = Batch}, #{id := Id, index := Index, queue := Q} = St -) when - is_list(Batch) --> - Q1 = append_queue(Id, Index, Q, Batch), - {next_state, blocked, St#{queue := Q1}}; running(info, ?SEND_REQ(_From, _Req) = Request0, Data) -> handle_query_requests(Request0, Data); running(info, {flush, Ref}, St = #{tref := {_TRef, Ref}}) -> @@ -216,13 +203,6 @@ blocked(enter, _, #{resume_interval := ResumeT} = _St) -> {keep_state_and_data, {state_timeout, ResumeT, resume}}; blocked(cast, block, _St) -> keep_state_and_data; -blocked( - cast, {block, [?QUERY(_, _, _) | _] = Batch}, #{id := Id, index := Index, queue := Q} = St -) when - is_list(Batch) --> - Q1 = append_queue(Id, Index, Q, Batch), - {keep_state, St#{queue := Q1}}; blocked(cast, resume, St) -> do_resume(St); blocked(state_timeout, resume, St) -> From cf6996cbea6f2067cf18b8bcb34446b7da7cb986 Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Thu, 5 Jan 2023 11:19:43 -0300 Subject: [PATCH 0031/1802] test(flaky): fix flaky jwt worker test --- .../emqx_connector/test/emqx_connector_jwt_worker_SUITE.erl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/emqx_connector/test/emqx_connector_jwt_worker_SUITE.erl b/apps/emqx_connector/test/emqx_connector_jwt_worker_SUITE.erl index e8355f746..eb104801c 100644 --- a/apps/emqx_connector/test/emqx_connector_jwt_worker_SUITE.erl +++ b/apps/emqx_connector/test/emqx_connector_jwt_worker_SUITE.erl @@ -127,8 +127,12 @@ t_unknown_error(_Config) -> 1_000 ), fun(Trace) -> + %% there seems to be some occasions when empty_key is + %% returned instead. ?assertMatch( - [#{error := {invalid_private_key, some_strange_error}}], + [#{error := Error}] when + Error =:= {invalid_private_key, some_strange_error} orelse + Error =:= empty_key, ?of_kind(connector_jwt_worker_startup_error, Trace) ), ok From c6b8e614df45b3ef259fa1e0e95298b1b20343cc Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Thu, 5 Jan 2023 11:02:04 -0300 Subject: [PATCH 0032/1802] fix(authz_http): handle `ignore` results (request failures) Related issue: https://github.com/emqx/emqx/issues/9683 When the HTTP request for authz fails (e.g.: resource is down or server is down), then the HTTP authorizer returns `ignore`, which was not handled correctly by the authorization callback. --- apps/emqx_authz/src/emqx_authz.app.src | 2 +- apps/emqx_authz/src/emqx_authz.erl | 8 ++++ apps/emqx_authz/src/emqx_authz_http.erl | 2 + .../emqx_authz/test/emqx_authz_http_SUITE.erl | 44 ++++++++++++++++++- changes/v5.0.14/fix-9689.en.md | 1 + changes/v5.0.14/fix-9689.zh.md | 1 + 6 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 changes/v5.0.14/fix-9689.en.md create mode 100644 changes/v5.0.14/fix-9689.zh.md diff --git a/apps/emqx_authz/src/emqx_authz.app.src b/apps/emqx_authz/src/emqx_authz.app.src index c876fbf16..f5b9f9da6 100644 --- a/apps/emqx_authz/src/emqx_authz.app.src +++ b/apps/emqx_authz/src/emqx_authz.app.src @@ -1,7 +1,7 @@ %% -*- mode: erlang -*- {application, emqx_authz, [ {description, "An OTP application"}, - {vsn, "0.1.10"}, + {vsn, "0.1.11"}, {registered, []}, {mod, {emqx_authz_app, []}}, {applications, [ diff --git a/apps/emqx_authz/src/emqx_authz.erl b/apps/emqx_authz/src/emqx_authz.erl index bf07f3083..682ad7f2e 100644 --- a/apps/emqx_authz/src/emqx_authz.erl +++ b/apps/emqx_authz/src/emqx_authz.erl @@ -20,6 +20,7 @@ -include("emqx_authz.hrl"). -include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/emqx_hooks.hrl"). +-include_lib("snabbkaffe/include/snabbkaffe.hrl"). -ifdef(TEST). -compile(export_all). @@ -358,6 +359,7 @@ authorize_non_superuser( emqx_metrics:inc(?METRIC_DENY), {stop, #{result => deny, from => AuthzSource}}; nomatch -> + ?tp(authz_non_superuser, #{result => nomatch}), ?SLOG(info, #{ msg => "authorization_failed_nomatch", username => Username, @@ -388,6 +390,12 @@ do_authorize( nomatch -> emqx_metrics_worker:inc(authz_metrics, Type, nomatch), do_authorize(Client, PubSub, Topic, Tail); + %% {matched, allow | deny | ignore} + {matched, ignore} -> + do_authorize(Client, PubSub, Topic, Tail); + ignore -> + do_authorize(Client, PubSub, Topic, Tail); + %% {matched, allow | deny} Matched -> {Matched, Type} catch diff --git a/apps/emqx_authz/src/emqx_authz_http.erl b/apps/emqx_authz/src/emqx_authz_http.erl index ea12214ec..4479d1483 100644 --- a/apps/emqx_authz/src/emqx_authz_http.erl +++ b/apps/emqx_authz/src/emqx_authz_http.erl @@ -20,6 +20,7 @@ -include_lib("emqx/include/emqx.hrl"). -include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/emqx_placeholder.hrl"). +-include_lib("snabbkaffe/include/snabbkaffe.hrl"). -behaviour(emqx_authz). @@ -104,6 +105,7 @@ authorize( log_nomtach_msg(Status, Headers, Body), nomatch; {error, Reason} -> + ?tp(authz_http_request_failure, #{error => Reason}), ?SLOG(error, #{ msg => "http_server_query_failed", resource => ResourceID, diff --git a/apps/emqx_authz/test/emqx_authz_http_SUITE.erl b/apps/emqx_authz/test/emqx_authz_http_SUITE.erl index b95192cb7..e91da9829 100644 --- a/apps/emqx_authz/test/emqx_authz_http_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_http_SUITE.erl @@ -23,6 +23,7 @@ -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). -include_lib("emqx/include/emqx_placeholder.hrl"). +-include_lib("snabbkaffe/include/snabbkaffe.hrl"). -define(HTTP_PORT, 33333). -define(HTTP_PATH, "/authz/[...]"). @@ -64,7 +65,14 @@ init_per_testcase(_Case, Config) -> Config. end_per_testcase(_Case, _Config) -> - ok = emqx_authz_http_test_server:stop(). + try + ok = emqx_authz_http_test_server:stop() + catch + exit:noproc -> + ok + end, + snabbkaffe:stop(), + ok. %%------------------------------------------------------------------------------ %% Tests @@ -148,7 +156,39 @@ t_response_handling(_Config) -> ?assertEqual( deny, emqx_access_control:authorize(ClientInfo, publish, <<"t">>) - ). + ), + + %% the server cannot be reached; should skip to the next + %% authorizer in the chain. + ok = emqx_authz_http_test_server:stop(), + + ?check_trace( + ?assertEqual( + deny, + emqx_access_control:authorize(ClientInfo, publish, <<"t">>) + ), + fun(Trace) -> + ?assertMatch( + [ + #{ + ?snk_kind := authz_http_request_failure, + error := {recoverable_error, econnrefused} + } + ], + ?of_kind(authz_http_request_failure, Trace) + ), + ?assert( + ?strict_causality( + #{?snk_kind := authz_http_request_failure}, + #{?snk_kind := authz_non_superuser, result := nomatch}, + Trace + ) + ), + ok + end + ), + + ok. t_query_params(_Config) -> ok = setup_handler_and_config( diff --git a/changes/v5.0.14/fix-9689.en.md b/changes/v5.0.14/fix-9689.en.md new file mode 100644 index 000000000..7582c8bc5 --- /dev/null +++ b/changes/v5.0.14/fix-9689.en.md @@ -0,0 +1 @@ +Fix handling of HTTP authorization result when a request failure (e.g.: HTTP resource is down) would cause a `function_clause` error. diff --git a/changes/v5.0.14/fix-9689.zh.md b/changes/v5.0.14/fix-9689.zh.md new file mode 100644 index 000000000..62f4a90fb --- /dev/null +++ b/changes/v5.0.14/fix-9689.zh.md @@ -0,0 +1 @@ +修正当请求失败(如:HTTP资源关闭)会导致`function_clause`错误时对HTTP授权结果的处理。 From b59c4c34c5cc5ca240152237b48a555f5db4e7dd Mon Sep 17 00:00:00 2001 From: Kjell Winblad Date: Wed, 4 Jan 2023 16:00:42 +0100 Subject: [PATCH 0033/1802] fix(Bridge REST API): no feedback when deleting bridge This fixes https://emqx.atlassian.net/browse/EMQX-8648. The issue described in `EMQX-8648` is that when deleting a non-existing bridge the server gives a success response. See below: ``` curl --head -u admin:public2 -X 'DELETE' 'http://localhost:18083/api/v5/bridges/webhook:i_do_not_exist' HTTP/1.1 204 No Content date: Tue, 03 Jan 2023 16:59:01 GMT server: Cowboy ``` After the fix, deleting a non existing bridge will give the following response: ``` HTTP/1.1 404 Not Found content-length: 49 content-type: application/json date: Thu, 05 Jan 2023 12:40:35 GMT server: Cowboy ``` Closes: EMQX-8648 --- apps/emqx_bridge/src/emqx_bridge_api.erl | 30 +++++++++++-------- .../test/emqx_bridge_api_SUITE.erl | 9 ++++++ changes/v5.0.14/fix-8648.en.md | 1 + 3 files changed, 28 insertions(+), 12 deletions(-) create mode 100644 changes/v5.0.14/fix-8648.en.md diff --git a/apps/emqx_bridge/src/emqx_bridge_api.erl b/apps/emqx_bridge/src/emqx_bridge_api.erl index 0d0d2e9ad..cf39ebf14 100644 --- a/apps/emqx_bridge/src/emqx_bridge_api.erl +++ b/apps/emqx_bridge/src/emqx_bridge_api.erl @@ -328,6 +328,7 @@ schema("/bridges/:id") -> responses => #{ 204 => <<"Bridge deleted">>, 400 => error_schema(['INVALID_ID'], "Update bridge failed"), + 404 => error_schema('NOT_FOUND', "Bridge not found"), 403 => error_schema('FORBIDDEN_REQUEST', "Forbidden operation"), 503 => error_schema('SERVICE_UNAVAILABLE', "Service unavailable") } @@ -433,19 +434,24 @@ schema("/nodes/:node/bridges/:id/operation/:operation") -> end, ?TRY_PARSE_ID( Id, - case emqx_bridge:check_deps_and_remove(BridgeType, BridgeName, AlsoDeleteActs) of + case emqx_bridge:lookup(BridgeType, BridgeName) of {ok, _} -> - 204; - {error, {rules_deps_on_this_bridge, RuleIds}} -> - {403, - error_msg( - 'FORBIDDEN_REQUEST', - {<<"There're some rules dependent on this bridge">>, RuleIds} - )}; - {error, timeout} -> - {503, error_msg('SERVICE_UNAVAILABLE', <<"request timeout">>)}; - {error, Reason} -> - {500, error_msg('INTERNAL_ERROR', Reason)} + case emqx_bridge:check_deps_and_remove(BridgeType, BridgeName, AlsoDeleteActs) of + {ok, _} -> + 204; + {error, {rules_deps_on_this_bridge, RuleIds}} -> + {403, + error_msg( + 'FORBIDDEN_REQUEST', + {<<"There're some rules dependent on this bridge">>, RuleIds} + )}; + {error, timeout} -> + {503, error_msg('SERVICE_UNAVAILABLE', <<"request timeout">>)}; + {error, Reason} -> + {500, error_msg('INTERNAL_ERROR', Reason)} + end; + {error, not_found} -> + {404, error_msg('NOT_FOUND', <<"Bridge not found">>)} end ). diff --git a/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl b/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl index 60f770df8..8f7978f40 100644 --- a/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl +++ b/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl @@ -303,6 +303,15 @@ t_http_crud_apis(Config) -> }, jsx:decode(ErrMsg2) ), + %% Deleting a non-existing bridge should result in an error + {ok, 404, ErrMsg3} = request(delete, uri(["bridges", BridgeID]), []), + ?assertMatch( + #{ + <<"code">> := _, + <<"message">> := <<"Bridge not found">> + }, + jsx:decode(ErrMsg3) + ), ok. t_check_dependent_actions_on_delete(Config) -> diff --git a/changes/v5.0.14/fix-8648.en.md b/changes/v5.0.14/fix-8648.en.md new file mode 100644 index 000000000..ac608a2e1 --- /dev/null +++ b/changes/v5.0.14/fix-8648.en.md @@ -0,0 +1 @@ +When deleting a non-existing bridge the server gave a success response. This has been fixed so that the server instead gives an error response when the user attempts to delete a non-existing bridge. From 5266722e512b51708095a112397af6c8d40edb84 Mon Sep 17 00:00:00 2001 From: Kjell Winblad Date: Thu, 5 Jan 2023 16:40:45 +0100 Subject: [PATCH 0034/1802] feat: upgrade MongoDB library to support MongoDB 5.1+ --- apps/emqx_connector/rebar.config | 2 +- changes/v5.0.14/feat-8329.en.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changes/v5.0.14/feat-8329.en.md diff --git a/apps/emqx_connector/rebar.config b/apps/emqx_connector/rebar.config index 98490a91c..af5edb9cd 100644 --- a/apps/emqx_connector/rebar.config +++ b/apps/emqx_connector/rebar.config @@ -12,7 +12,7 @@ {mysql, {git, "https://github.com/emqx/mysql-otp", {tag, "1.7.1"}}}, {epgsql, {git, "https://github.com/emqx/epgsql", {tag, "4.7-emqx.2"}}}, %% NOTE: mind poolboy version when updating mongodb-erlang version - {mongodb, {git, "https://github.com/emqx/mongodb-erlang", {tag, "v3.0.13"}}}, + {mongodb, {git, "https://github.com/emqx/mongodb-erlang", {tag, "v3.0.18"}}}, %% NOTE: mind poolboy version when updating eredis_cluster version {eredis_cluster, {git, "https://github.com/emqx/eredis_cluster", {tag, "0.7.1"}}}, %% mongodb-erlang uses a special fork https://github.com/comtihon/poolboy.git diff --git a/changes/v5.0.14/feat-8329.en.md b/changes/v5.0.14/feat-8329.en.md new file mode 100644 index 000000000..2876e1754 --- /dev/null +++ b/changes/v5.0.14/feat-8329.en.md @@ -0,0 +1 @@ +The MongoDB library has been upgraded to support MongoDB 5.1+ From fc12a8c4c8a1894d1532d13d50468852681a28b4 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Thu, 5 Jan 2023 07:22:21 +0100 Subject: [PATCH 0035/1802] test: do not use tc_user_skip for test cases --- .../test/emqx_ee_bridge_gcp_pubsub_SUITE.erl | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_gcp_pubsub_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_gcp_pubsub_SUITE.erl index 91fd2f399..0eadf46ad 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_gcp_pubsub_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_gcp_pubsub_SUITE.erl @@ -110,7 +110,7 @@ init_per_testcase(TestCase, Config0) when -> case ?config(batch_size, Config0) of 1 -> - {skip, no_batching}; + [{skip_due_to_no_batching, true}]; _ -> {ok, _} = start_echo_http_server(), delete_all_bridges(), @@ -754,6 +754,15 @@ t_publish_templated(Config) -> ok. t_publish_success_batch(Config) -> + case proplists:get_bool(skip_due_to_no_batching, Config) of + true -> + ct:pal("this test case is skipped due to non-applicable config"), + ok; + false -> + test_publish_success_batch(Config) + end. + +test_publish_success_batch(Config) -> ResourceId = ?config(resource_id, Config), ServiceAccountJSON = ?config(service_account_json, Config), TelemetryTable = ?config(telemetry_table, Config), From 5f12cdff6c3e35cad220ceabd427b3eec41a6e32 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Tue, 3 Jan 2023 10:53:10 +0100 Subject: [PATCH 0036/1802] ci: should not skip tests in github actions --- .github/workflows/run_test_cases.yaml | 2 +- .../test/emqx_connector_redis_SUITE.erl | 9 +++++++- .../test/emqx_ee_bridge_redis_SUITE.erl | 8 +++++++ scripts/ct/run.sh | 21 +++++++++++++++---- 4 files changed, 34 insertions(+), 6 deletions(-) diff --git a/.github/workflows/run_test_cases.yaml b/.github/workflows/run_test_cases.yaml index e8534ef68..91a087c95 100644 --- a/.github/workflows/run_test_cases.yaml +++ b/.github/workflows/run_test_cases.yaml @@ -162,7 +162,7 @@ jobs: INFLUXDB_TAG: 2.5.0 PROFILE: ${{ matrix.profile }} CT_COVER_EXPORT_PREFIX: ${{ matrix.profile }}-${{ matrix.otp }} - run: ./scripts/ct/run.sh --app ${{ matrix.app }} + run: ./scripts/ct/run.sh --ci --app ${{ matrix.app }} - uses: actions/upload-artifact@v3 with: name: coverdata diff --git a/apps/emqx_connector/test/emqx_connector_redis_SUITE.erl b/apps/emqx_connector/test/emqx_connector_redis_SUITE.erl index fa742da0b..5584af763 100644 --- a/apps/emqx_connector/test/emqx_connector_redis_SUITE.erl +++ b/apps/emqx_connector/test/emqx_connector_redis_SUITE.erl @@ -50,7 +50,7 @@ init_per_suite(Config) -> {ok, _} = application:ensure_all_started(emqx_connector), Config; false -> - {skip, no_redis} + assert_ci() end. end_per_suite(_Config) -> @@ -63,6 +63,13 @@ init_per_testcase(_, Config) -> end_per_testcase(_, _Config) -> ok. +assert_ci() -> + case os:getenv("IS_CI") of + "yes" -> + throw(no_redis); + _ -> + {skip, no_redis} + end. % %%------------------------------------------------------------------------------ % %% Testcases % %%------------------------------------------------------------------------------ diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_redis_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_redis_SUITE.erl index f88bc42eb..1f4b52ddc 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_redis_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_redis_SUITE.erl @@ -96,6 +96,14 @@ init_per_suite(Config) -> | Config ]; false -> + assert_ci() + end. + +assert_ci() -> + case os:getenv("IS_CI") of + "yes" -> + throw(no_redis); + _ -> {skip, no_redis} end. diff --git a/scripts/ct/run.sh b/scripts/ct/run.sh index 0a64794b6..fbe3b7f64 100755 --- a/scripts/ct/run.sh +++ b/scripts/ct/run.sh @@ -17,6 +17,9 @@ help() { echo "--stop: Stop running containers for the given app" echo "--only-up: Only start the testbed but do not run CT" echo "--keep-up: Keep the testbed running after CT" + echo "--ci: Set this flag in GitHub action to enforce no tests are skipped" + echo "--" If any, all args after '--' are passed to rebar3 ct + echo " otherwise it runs the entire app's CT" } WHICH_APP='novalue' @@ -26,6 +29,7 @@ ONLY_UP='no' SUITES='' ATTACH='no' STOP='no' +IS_CI='no' while [ "$#" -gt 0 ]; do case $1 in -h|--help) @@ -56,9 +60,14 @@ while [ "$#" -gt 0 ]; do CONSOLE='yes' shift 1 ;; - --suites) - SUITES="$2" - shift 2 + --ci) + IS_CI='yes' + shift 1 + ;; + --) + shift 1 + REBAR3CT="$*" + shift $# ;; *) echo "unknown option $1" @@ -202,7 +211,11 @@ elif [ "$CONSOLE" = 'yes' ]; then docker exec -e PROFILE="$PROFILE" -i $TTY "$ERLANG_CONTAINER" bash -c "make run" restore_ownership else - docker exec -e PROFILE="$PROFILE" -i $TTY -e EMQX_CT_SUITES="$SUITES" "$ERLANG_CONTAINER" bash -c "BUILD_WITHOUT_QUIC=1 make ${WHICH_APP}-ct" + if [ -z "${REBAR3CT:-}" ]; then + docker exec -e IS_CI="$IS_CI" -e PROFILE="$PROFILE" -i $TTY "$ERLANG_CONTAINER" bash -c "BUILD_WITHOUT_QUIC=1 make ${WHICH_APP}-ct" + else + docker exec -e IS_CI="$IS_CI" -e PROFILE="$PROFILE" -i $TTY "$ERLANG_CONTAINER" bash -c "./rebar3 ct $REBAR3CT" + fi RESULT=$? restore_ownership if [ $RESULT -ne 0 ]; then From 7a4e7a6de9c274e10e942fe7ab83ed46f4b92684 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Thu, 5 Jan 2023 10:48:37 +0100 Subject: [PATCH 0037/1802] test: wait for redis --- .../test/emqx_connector_redis_SUITE.erl | 49 ++++++++++--------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/apps/emqx_connector/test/emqx_connector_redis_SUITE.erl b/apps/emqx_connector/test/emqx_connector_redis_SUITE.erl index 5584af763..060340fed 100644 --- a/apps/emqx_connector/test/emqx_connector_redis_SUITE.erl +++ b/apps/emqx_connector/test/emqx_connector_redis_SUITE.erl @@ -36,22 +36,16 @@ groups() -> []. init_per_suite(Config) -> - case - emqx_common_test_helpers:is_all_tcp_servers_available( - [ - {?REDIS_SINGLE_HOST, ?REDIS_SINGLE_PORT}, - {?REDIS_SENTINEL_HOST, ?REDIS_SENTINEL_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), - Config; - false -> - assert_ci() - end. + Checks = + case os:getenv("IS_CI") of + "yes" -> 10; + _ -> 1 + end, + ok = wait_for_redis(Checks), + 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), + Config. end_per_suite(_Config) -> ok = emqx_common_test_helpers:stop_apps([emqx_resource]), @@ -63,13 +57,24 @@ init_per_testcase(_, Config) -> end_per_testcase(_, _Config) -> ok. -assert_ci() -> - case os:getenv("IS_CI") of - "yes" -> - throw(no_redis); - _ -> - {skip, no_redis} +wait_for_redis(0) -> + throw(timeout); +wait_for_redis(Checks) -> + case + emqx_common_test_helpers:is_all_tcp_servers_available( + [ + {?REDIS_SINGLE_HOST, ?REDIS_SINGLE_PORT}, + {?REDIS_SENTINEL_HOST, ?REDIS_SENTINEL_PORT} + ] + ) + of + true -> + ok; + false -> + timer:sleep(1000), + wait_for_redis(Checks - 1) end. + % %%------------------------------------------------------------------------------ % %% Testcases % %%------------------------------------------------------------------------------ From a23fdcab6f19f9528b6a9477243566656cc2bc42 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Thu, 5 Jan 2023 22:51:11 +0100 Subject: [PATCH 0038/1802] chore: delete unused var in ct/run.sh --- scripts/ct/run.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/ct/run.sh b/scripts/ct/run.sh index fbe3b7f64..7fb9e00fd 100755 --- a/scripts/ct/run.sh +++ b/scripts/ct/run.sh @@ -26,7 +26,6 @@ WHICH_APP='novalue' CONSOLE='no' KEEP_UP='no' ONLY_UP='no' -SUITES='' ATTACH='no' STOP='no' IS_CI='no' From 13b2f45405a311426fdc7806ae0afc04858053d4 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Thu, 5 Jan 2023 18:06:01 +0800 Subject: [PATCH 0039/1802] fix: function_clause when sending messages to bridges --- apps/emqx_bridge/src/emqx_bridge.erl | 11 +++-- .../test/emqx_bridge_api_SUITE.erl | 49 +++++++++++++++++++ 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/apps/emqx_bridge/src/emqx_bridge.erl b/apps/emqx_bridge/src/emqx_bridge.erl index 20614a344..c86087014 100644 --- a/apps/emqx_bridge/src/emqx_bridge.erl +++ b/apps/emqx_bridge/src/emqx_bridge.erl @@ -363,10 +363,13 @@ get_matched_egress_bridges(Topic) -> get_matched_bridge_id(_BType, #{enable := false}, _Topic, _BName, Acc) -> Acc; -get_matched_bridge_id(BType, #{local_topic := Filter}, Topic, BName, Acc) when - ?EGRESS_DIR_BRIDGES(BType) --> - do_get_matched_bridge_id(Topic, Filter, BType, BName, Acc); +get_matched_bridge_id(BType, Conf, Topic, BName, Acc) when ?EGRESS_DIR_BRIDGES(BType) -> + case maps:get(local_topic, Conf, undefined) of + undefined -> + Acc; + Filter -> + do_get_matched_bridge_id(Topic, Filter, BType, BName, Acc) + end; get_matched_bridge_id(mqtt, #{egress := #{local := #{topic := Filter}}}, Topic, BName, Acc) -> do_get_matched_bridge_id(Topic, Filter, mqtt, BName, Acc); get_matched_bridge_id(kafka, #{producer := #{mqtt := #{topic := Filter}}}, Topic, BName, Acc) -> diff --git a/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl b/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl index 60f770df8..e533c78b0 100644 --- a/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl +++ b/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl @@ -305,6 +305,55 @@ t_http_crud_apis(Config) -> ), ok. +t_http_bridges_local_topic(Config) -> + Port = ?config(port, Config), + %% assert we there's no bridges at first + {ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []), + + %% then we add a webhook bridge, using POST + %% POST /bridges/ will create a bridge + URL1 = ?URL(Port, "path1"), + Name1 = <<"t_http_bridges_with_local_topic1">>, + Name2 = <<"t_http_bridges_without_local_topic1">>, + %% create one http bridge with local_topic + {ok, 201, _} = request( + post, + uri(["bridges"]), + ?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, Name1) + ), + %% and we create another one without local_topic + {ok, 201, _} = request( + post, + uri(["bridges"]), + maps:remove(<<"local_topic">>, ?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, Name2)) + ), + BridgeID1 = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE, Name1), + BridgeID2 = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE, Name2), + %% Send an message to emqx and the message should be forwarded to the HTTP server. + %% This is to verify we can have 2 bridges with and without local_topic fields + %% at the same time. + Body = <<"my msg">>, + emqx:publish(emqx_message:make(<<"emqx_webhook/1">>, Body)), + ?assert( + receive + {http_server, received, #{ + method := <<"POST">>, + path := <<"/path1">>, + body := Body + }} -> + true; + Msg -> + ct:pal("error: http got unexpected request: ~p", [Msg]), + false + after 100 -> + false + end + ), + %% delete the bridge + {ok, 204, <<>>} = request(delete, uri(["bridges", BridgeID1]), []), + {ok, 204, <<>>} = request(delete, uri(["bridges", BridgeID2]), []), + ok. + t_check_dependent_actions_on_delete(Config) -> Port = ?config(port, Config), %% assert we there's no bridges at first From 65729cd640c28ded666594b16284a61c8341067a Mon Sep 17 00:00:00 2001 From: JianBo He Date: Thu, 29 Dec 2022 17:02:29 +0800 Subject: [PATCH 0040/1802] fix(clients): fix expiry_interval unit error It should be second not millisecond --- .../src/emqx_mgmt_api_clients.erl | 17 +++++------------ .../test/emqx_mgmt_api_clients_SUITE.erl | 6 +++++- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/apps/emqx_management/src/emqx_mgmt_api_clients.erl b/apps/emqx_management/src/emqx_mgmt_api_clients.erl index bf025bfc7..8eb4b26e9 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_clients.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_clients.erl @@ -65,7 +65,6 @@ {<<"ip_address">>, ip}, {<<"conn_state">>, atom}, {<<"clean_start">>, atom}, - {<<"proto_name">>, binary}, {<<"proto_ver">>, integer}, {<<"like_clientid">>, binary}, {<<"like_username">>, binary}, @@ -145,14 +144,6 @@ schema("/clients") -> required => false, description => <<"Whether the client uses a new session">> })}, - {proto_name, - hoconsc:mk(hoconsc:enum(['MQTT', 'CoAP', 'LwM2M', 'MQTT-SN']), #{ - in => query, - required => false, - description => - <<"Client protocol name, ", - "the possible values are MQTT,CoAP,LwM2M,MQTT-SN">> - })}, {proto_ver, hoconsc:mk(binary(), #{ in => query, @@ -830,8 +821,6 @@ ms(ip_address, X) -> #{conninfo => #{peername => {X, '_'}}}; ms(clean_start, X) -> #{conninfo => #{clean_start => X}}; -ms(proto_name, X) -> - #{conninfo => #{proto_name => X}}; ms(proto_ver, X) -> #{conninfo => #{proto_ver => X}}; ms(connected_at, X) -> @@ -879,7 +868,8 @@ format_channel_info(WhichNode, {_, ClientInfo0, ClientStats}) -> ClientInfoMap2 = maps:put(node, Node, ClientInfoMap1), ClientInfoMap3 = maps:put(ip_address, IpAddress, ClientInfoMap2), ClientInfoMap4 = maps:put(port, Port, ClientInfoMap3), - ClientInfoMap = maps:put(connected, Connected, ClientInfoMap4), + ClientInfoMap5 = convert_expiry_interval_unit(ClientInfoMap4), + ClientInfoMap = maps:put(connected, Connected, ClientInfoMap5), RemoveList = [ @@ -949,6 +939,9 @@ peername_dispart({Addr, Port}) -> %% PortBinary = integer_to_binary(Port), {AddrBinary, Port}. +convert_expiry_interval_unit(ClientInfoMap = #{expiry_interval := Interval}) -> + ClientInfoMap#{expiry_interval := Interval div 1000}. + format_authz_cache({{PubSub, Topic}, {AuthzResult, Timestamp}}) -> #{ access => PubSub, diff --git a/apps/emqx_management/test/emqx_mgmt_api_clients_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_api_clients_SUITE.erl index 8dd1dabfc..16ba99ad6 100644 --- a/apps/emqx_management/test/emqx_mgmt_api_clients_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_api_clients_SUITE.erl @@ -44,7 +44,10 @@ t_clients(_) -> AuthHeader = emqx_mgmt_api_test_util:auth_header_(), {ok, C1} = emqtt:start_link(#{ - username => Username1, clientid => ClientId1, proto_ver => v5 + username => Username1, + clientid => ClientId1, + proto_ver => v5, + properties => #{'Session-Expiry-Interval' => 120} }), {ok, _} = emqtt:connect(C1), {ok, C2} = emqtt:start_link(#{username => Username2, clientid => ClientId2}), @@ -70,6 +73,7 @@ t_clients(_) -> Client1Response = emqx_json:decode(Client1, [return_maps]), ?assertEqual(Username1, maps:get(<<"username">>, Client1Response)), ?assertEqual(ClientId1, maps:get(<<"clientid">>, Client1Response)), + ?assertEqual(120, maps:get(<<"expiry_interval">>, Client1Response)), %% delete /clients/:clientid kickout Client2Path = emqx_mgmt_api_test_util:api_path(["clients", binary_to_list(ClientId2)]), From ec51fcc834f9b352a9faebf543209234a3bcc3c2 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Thu, 29 Dec 2022 17:23:05 +0800 Subject: [PATCH 0041/1802] chore: update changes --- changes/v5.0.14-en.md | 9 +++++++++ changes/v5.0.14-zh.md | 8 ++++++++ 2 files changed, 17 insertions(+) create mode 100644 changes/v5.0.14-en.md create mode 100644 changes/v5.0.14-zh.md diff --git a/changes/v5.0.14-en.md b/changes/v5.0.14-en.md new file mode 100644 index 000000000..9e428f757 --- /dev/null +++ b/changes/v5.0.14-en.md @@ -0,0 +1,9 @@ +# v5.0.13 + +## Enhancements + + +## Bug fixes + +- Fix the expiry_interval fields of the clients HTTP API to measure in seconds [#9637](https://github.com/emqx/emqx/pull/9637). + diff --git a/changes/v5.0.14-zh.md b/changes/v5.0.14-zh.md new file mode 100644 index 000000000..08459d357 --- /dev/null +++ b/changes/v5.0.14-zh.md @@ -0,0 +1,8 @@ +# v5.0.14 + +## 增强 + +## 修复 + +- 修复 clients HTTP API 下的 expiry_interval 字段的时间单位为秒 [#9637](https://github.com/emqx/emqx/pull/9637)。 + From 5d55e3a7b3e95acc99ac61028afc272bd2ecdfc2 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Thu, 29 Dec 2022 17:23:45 +0800 Subject: [PATCH 0042/1802] chore: updage mgmt app vsn --- apps/emqx_management/src/emqx_management.app.src | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx_management/src/emqx_management.app.src b/apps/emqx_management/src/emqx_management.app.src index 5df8fe4df..c0cb05401 100644 --- a/apps/emqx_management/src/emqx_management.app.src +++ b/apps/emqx_management/src/emqx_management.app.src @@ -2,7 +2,7 @@ {application, emqx_management, [ {description, "EMQX Management API and CLI"}, % strict semver, bump manually! - {vsn, "5.0.10"}, + {vsn, "5.0.11"}, {modules, []}, {registered, [emqx_management_sup]}, {applications, [kernel, stdlib, emqx_plugins, minirest, emqx]}, From e2e73e7c5937d5e69726585ffdf1893fd439007f Mon Sep 17 00:00:00 2001 From: JianBo He Date: Wed, 4 Jan 2023 11:15:36 +0800 Subject: [PATCH 0043/1802] chore: Update changes/v5.0.14-en.md Co-authored-by: Ivan Dyachkov --- changes/v5.0.14-en.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changes/v5.0.14-en.md b/changes/v5.0.14-en.md index 9e428f757..7fef4c104 100644 --- a/changes/v5.0.14-en.md +++ b/changes/v5.0.14-en.md @@ -1,4 +1,4 @@ -# v5.0.13 +# v5.0.14 ## Enhancements From 9046913f9c83b97c67e97555b44810acf0bb5897 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Fri, 6 Jan 2023 10:37:21 +0800 Subject: [PATCH 0044/1802] chore: upgrade changes format --- changes/v5.0.14-en.md | 9 --------- changes/v5.0.14-zh.md | 8 -------- changes/v5.0.14/fix-9637.en.md | 1 + changes/v5.0.14/fix-9637.zh.md | 1 + 4 files changed, 2 insertions(+), 17 deletions(-) delete mode 100644 changes/v5.0.14-en.md delete mode 100644 changes/v5.0.14-zh.md create mode 100644 changes/v5.0.14/fix-9637.en.md create mode 100644 changes/v5.0.14/fix-9637.zh.md diff --git a/changes/v5.0.14-en.md b/changes/v5.0.14-en.md deleted file mode 100644 index 7fef4c104..000000000 --- a/changes/v5.0.14-en.md +++ /dev/null @@ -1,9 +0,0 @@ -# v5.0.14 - -## Enhancements - - -## Bug fixes - -- Fix the expiry_interval fields of the clients HTTP API to measure in seconds [#9637](https://github.com/emqx/emqx/pull/9637). - diff --git a/changes/v5.0.14-zh.md b/changes/v5.0.14-zh.md deleted file mode 100644 index 08459d357..000000000 --- a/changes/v5.0.14-zh.md +++ /dev/null @@ -1,8 +0,0 @@ -# v5.0.14 - -## 增强 - -## 修复 - -- 修复 clients HTTP API 下的 expiry_interval 字段的时间单位为秒 [#9637](https://github.com/emqx/emqx/pull/9637)。 - diff --git a/changes/v5.0.14/fix-9637.en.md b/changes/v5.0.14/fix-9637.en.md new file mode 100644 index 000000000..d93ed493c --- /dev/null +++ b/changes/v5.0.14/fix-9637.en.md @@ -0,0 +1 @@ +Fix the expiry_interval fields of the clients HTTP API to measure in seconds. diff --git a/changes/v5.0.14/fix-9637.zh.md b/changes/v5.0.14/fix-9637.zh.md new file mode 100644 index 000000000..8164a0bbf --- /dev/null +++ b/changes/v5.0.14/fix-9637.zh.md @@ -0,0 +1 @@ +修复 clients HTTP API 下的 expiry_interval 字段的时间单位为秒。 From a7f612eeb39d8fe4e52033a546ffc740a7f76bdb Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Thu, 5 Jan 2023 18:17:43 +0800 Subject: [PATCH 0045/1802] chore: update change logs --- changes/v5.0.14/fix-9687.en.md | 2 ++ changes/v5.0.14/fix-9687.zh.md | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 changes/v5.0.14/fix-9687.en.md create mode 100644 changes/v5.0.14/fix-9687.zh.md diff --git a/changes/v5.0.14/fix-9687.en.md b/changes/v5.0.14/fix-9687.en.md new file mode 100644 index 000000000..6c2f53bb3 --- /dev/null +++ b/changes/v5.0.14/fix-9687.en.md @@ -0,0 +1,2 @@ +Fix the problem that sending messages to data-bridges failed because of incorrect handling of some data-bridges without `local_topic` field configured. +Before this change, if some bridges have configured the `local_topic` field but others have not, a `function_clause` error will occur when forwarding messages to the data-bridges. diff --git a/changes/v5.0.14/fix-9687.zh.md b/changes/v5.0.14/fix-9687.zh.md new file mode 100644 index 000000000..089db5986 --- /dev/null +++ b/changes/v5.0.14/fix-9687.zh.md @@ -0,0 +1,2 @@ +修复由于某些数据桥接未配置 `local_topic` 字段,导致的所有数据桥接无法发送消息。 +在此改动之前,如果有些桥接设置了 `local_topic` 字段而有些没有设置,数据桥接转发消息时会出现 `function_clause` 的错误。 From f7b50c56da09d5350c6d02d9972ece587ecfcbf9 Mon Sep 17 00:00:00 2001 From: firest Date: Tue, 3 Jan 2023 14:34:39 +0800 Subject: [PATCH 0046/1802] feat(bridges): integrate PostgreSQL into bridges support both simple and batch query --- .../src/emqx_connector_pgsql.erl | 265 ++++++++++++++++-- .../i18n/emqx_ee_bridge_pgsql.conf | 74 +++++ lib-ee/emqx_ee_bridge/src/emqx_ee_bridge.erl | 15 +- .../src/emqx_ee_bridge_pgsql.erl | 130 +++++++++ 4 files changed, 454 insertions(+), 30 deletions(-) create mode 100644 lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_pgsql.conf create mode 100644 lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_pgsql.erl diff --git a/apps/emqx_connector/src/emqx_connector_pgsql.erl b/apps/emqx_connector/src/emqx_connector_pgsql.erl index cde8bbe3b..4b565a614 100644 --- a/apps/emqx_connector/src/emqx_connector_pgsql.erl +++ b/apps/emqx_connector/src/emqx_connector_pgsql.erl @@ -20,6 +20,7 @@ -include_lib("emqx/include/logger.hrl"). -include_lib("hocon/include/hoconsc.hrl"). -include_lib("epgsql/include/epgsql.hrl"). +-include_lib("snabbkaffe/include/snabbkaffe.hrl"). -export([roots/0, fields/1]). @@ -31,6 +32,7 @@ on_start/2, on_stop/2, on_query/3, + on_batch_query/3, on_get_status/2 ]). @@ -38,7 +40,8 @@ -export([ query/3, - prepared_query/3 + prepared_query/3, + execute_batch/3 ]). -export([do_get_status/1]). @@ -47,6 +50,18 @@ default_port => ?PGSQL_DEFAULT_PORT }). +-type prepares() :: #{atom() => binary()}. +-type params_tokens() :: #{atom() => list()}. + +-type state() :: + #{ + poolname := atom(), + auto_reconnect := boolean(), + prepare_sql := prepares(), + params_tokens := params_tokens(), + prepare_statement := epgsql:statement() + }. + %%===================================================================== roots() -> @@ -65,6 +80,7 @@ server() -> %% =================================================================== callback_mode() -> always_sync. +-spec on_start(binary(), hoconsc:config()) -> {ok, state()} | {error, _}. on_start( InstId, #{ @@ -87,7 +103,7 @@ on_start( case maps:get(enable, SSL) of true -> [ - {ssl, true}, + {ssl, required}, {ssl_opts, emqx_tls_lib:to_client_opts(SSL)} ]; false -> @@ -100,13 +116,21 @@ on_start( {password, emqx_secret:wrap(Password)}, {database, DB}, {auto_reconnect, reconn_interval(AutoReconn)}, - {pool_size, PoolSize}, - {prepare_statement, maps:to_list(maps:get(prepare_statement, Config, #{}))} + {pool_size, PoolSize} ], PoolName = emqx_plugin_libs_pool:pool_name(InstId), + Prepares = parse_prepare_sql(Config), + InitState = #{poolname => PoolName, auto_reconnect => AutoReconn, prepare_statement => #{}}, + State = maps:merge(InitState, Prepares), case emqx_plugin_libs_pool:start_pool(PoolName, ?MODULE, Options ++ SslOpts) of - ok -> {ok, #{poolname => PoolName, auto_reconnect => AutoReconn}}; - {error, Reason} -> {error, Reason} + ok -> + {ok, init_prepare(State)}; + {error, Reason} -> + ?tp( + pgsql_connector_start_failed, + #{error => Reason} + ), + {error, Reason} end. on_stop(InstId, #{poolname := PoolName}) -> @@ -116,37 +140,145 @@ on_stop(InstId, #{poolname := PoolName}) -> }), emqx_plugin_libs_pool:stop_pool(PoolName). -on_query(InstId, {Type, NameOrSQL}, #{poolname := _PoolName} = State) -> - on_query(InstId, {Type, NameOrSQL, []}, State); -on_query(InstId, {Type, NameOrSQL, Params}, #{poolname := PoolName} = State) -> +on_query(InstId, {TypeOrKey, NameOrSQL}, #{poolname := _PoolName} = State) -> + on_query(InstId, {TypeOrKey, NameOrSQL, []}, State); +on_query( + InstId, + {TypeOrKey, NameOrSQL, Params}, + #{poolname := PoolName} = State +) -> ?SLOG(debug, #{ msg => "postgresql connector received sql query", connector => InstId, + type => TypeOrKey, sql => NameOrSQL, state => State }), - case Result = ecpool:pick_and_do(PoolName, {?MODULE, Type, [NameOrSQL, Params]}, no_handover) of + Type = pgsql_query_type(TypeOrKey), + {NameOrSQL2, Data} = proc_sql_params(TypeOrKey, NameOrSQL, Params, State), + on_sql_query(InstId, PoolName, Type, NameOrSQL2, Data). + +pgsql_query_type(sql) -> + query; +pgsql_query_type(query) -> + query; +pgsql_query_type(prepared_query) -> + prepared_query; +%% for bridge +pgsql_query_type(_) -> + pgsql_query_type(prepared_query). + +on_batch_query( + InstId, + BatchReq, + #{poolname := PoolName, params_tokens := Tokens, prepare_statement := Sts} = State +) -> + case BatchReq of + [{Key, _} = Request | _] -> + BinKey = to_bin(Key), + case maps:get(BinKey, Tokens, undefined) of + undefined -> + Log = #{ + connector => InstId, + first_request => Request, + state => State, + msg => "batch prepare not implemented" + }, + ?SLOG(error, Log), + {error, batch_prepare_not_implemented}; + TokenList -> + {_, Datas} = lists:unzip(BatchReq), + Datas2 = [emqx_plugin_libs_rule:proc_sql(TokenList, Data) || Data <- Datas], + St = maps:get(BinKey, Sts), + {_Column, Results} = on_sql_query(InstId, PoolName, execute_batch, St, Datas2), + %% this local function only suits for the result of batch insert + TransResult = fun + Trans([{ok, Count} | T], Acc) -> + Trans(T, Acc + Count); + Trans([{error, _} = Error | _], _Acc) -> + Error; + Trans([], Acc) -> + {ok, Acc} + end, + + TransResult(Results, 0) + end; + _ -> + Log = #{ + connector => InstId, + request => BatchReq, + state => State, + msg => "invalid request" + }, + ?SLOG(error, Log), + {error, invalid_request} + end. + +proc_sql_params(query, SQLOrKey, Params, _State) -> + {SQLOrKey, Params}; +proc_sql_params(prepared_query, SQLOrKey, Params, _State) -> + {SQLOrKey, Params}; +proc_sql_params(TypeOrKey, SQLOrData, Params, #{params_tokens := ParamsTokens}) -> + Key = to_bin(TypeOrKey), + case maps:get(Key, ParamsTokens, undefined) of + undefined -> + {SQLOrData, Params}; + Tokens -> + {Key, emqx_plugin_libs_rule:proc_sql(Tokens, SQLOrData)} + end. + +on_sql_query(InstId, PoolName, Type, NameOrSQL, Data) -> + Result = ecpool:pick_and_do(PoolName, {?MODULE, Type, [NameOrSQL, Data]}, no_handover), + case Result of {error, Reason} -> ?SLOG(error, #{ msg => "postgresql connector do sql query failed", connector => InstId, + type => Type, sql => NameOrSQL, reason => Reason }); _ -> + ?tp( + pgsql_connector_query_return, + #{result => Result} + ), ok end, Result. -on_get_status(_InstId, #{poolname := Pool, auto_reconnect := AutoReconn}) -> +on_get_status(_InstId, #{poolname := Pool, auto_reconnect := AutoReconn} = State) -> case emqx_plugin_libs_pool:health_check_ecpool_workers(Pool, fun ?MODULE:do_get_status/1) of - true -> connected; - false -> conn_status(AutoReconn) + true -> + case do_check_prepares(State) of + ok -> + connected; + {ok, NState} -> + %% return new state with prepared statements + {connected, NState}; + false -> + %% do not log error, it is logged in prepare_sql_to_conn + conn_status(AutoReconn) + end; + false -> + conn_status(AutoReconn) end. do_get_status(Conn) -> ok == element(1, epgsql:squery(Conn, "SELECT count(1) AS T")). +do_check_prepares(#{prepare_sql := Prepares}) when is_map(Prepares) -> + ok; +do_check_prepares(State = #{poolname := PoolName, prepare_sql := {error, Prepares}}) -> + %% retry to prepare + case prepare_sql(Prepares, PoolName) of + {ok, Sts} -> + %% remove the error + {ok, State#{prepare_sql => Prepares, prepare_statement := Sts}}; + _Error -> + false + end. + %% =================================================================== conn_status(_AutoReconn = true) -> connecting; conn_status(_AutoReconn = false) -> disconnected. @@ -158,13 +290,9 @@ connect(Opts) -> Host = proplists:get_value(host, Opts), Username = proplists:get_value(username, Opts), Password = emqx_secret:unwrap(proplists:get_value(password, Opts)), - PrepareStatement = proplists:get_value(prepare_statement, Opts), case epgsql:connect(Host, Username, Password, conn_opts(Opts)) of - {ok, Conn} -> - case parse(Conn, PrepareStatement) of - ok -> {ok, Conn}; - {error, Reason} -> {error, Reason} - end; + {ok, _Conn} = Ok -> + Ok; {error, Reason} -> {error, Reason} end. @@ -175,15 +303,8 @@ query(Conn, SQL, Params) -> prepared_query(Conn, Name, Params) -> epgsql:prepared_query2(Conn, Name, Params). -parse(_Conn, []) -> - ok; -parse(Conn, [{Name, Query} | More]) -> - case epgsql:parse2(Conn, Name, Query, []) of - {ok, _Statement} -> - parse(Conn, More); - Other -> - Other - end. +execute_batch(Conn, Statement, Params) -> + epgsql:execute_batch(Conn, Statement, Params). conn_opts(Opts) -> conn_opts(Opts, []). @@ -206,3 +327,91 @@ conn_opts([Opt = {ssl_opts, _} | Opts], Acc) -> conn_opts(Opts, [Opt | Acc]); conn_opts([_Opt | Opts], Acc) -> conn_opts(Opts, Acc). + +parse_prepare_sql(Config) -> + SQL = + case maps:get(prepare_statement, Config, undefined) of + undefined -> + case maps:get(sql, Config, undefined) of + undefined -> #{}; + Template -> #{<<"send_message">> => Template} + end; + Any -> + Any + end, + parse_prepare_sql(maps:to_list(SQL), #{}, #{}). + +parse_prepare_sql([{Key, H} | T], Prepares, Tokens) -> + {PrepareSQL, ParamsTokens} = emqx_plugin_libs_rule:preproc_sql(H, '$n'), + parse_prepare_sql( + T, Prepares#{Key => PrepareSQL}, Tokens#{Key => ParamsTokens} + ); +parse_prepare_sql([], Prepares, Tokens) -> + #{ + prepare_sql => Prepares, + params_tokens => Tokens + }. + +init_prepare(State = #{prepare_sql := Prepares, poolname := PoolName}) -> + case maps:size(Prepares) of + 0 -> + State; + _ -> + case prepare_sql(Prepares, PoolName) of + {ok, Sts} -> + State#{prepare_statement := Sts}; + Error -> + LogMeta = #{ + msg => <<"PostgreSQL init prepare statement failed">>, error => Error + }, + ?SLOG(error, LogMeta), + %% mark the prepare_sqlas failed + State#{prepare_sql => {error, Prepares}} + end + end. + +prepare_sql(Prepares, PoolName) when is_map(Prepares) -> + prepare_sql(maps:to_list(Prepares), PoolName); +prepare_sql(Prepares, PoolName) -> + case do_prepare_sql(Prepares, PoolName) of + {ok, _Sts} = Ok -> + %% prepare for reconnect + ecpool:add_reconnect_callback(PoolName, {?MODULE, prepare_sql_to_conn, [Prepares]}), + Ok; + Error -> + Error + end. + +do_prepare_sql(Prepares, PoolName) -> + do_prepare_sql(ecpool:workers(PoolName), Prepares, PoolName, #{}). + +do_prepare_sql([{_Name, Worker} | T], Prepares, PoolName, _LastSts) -> + {ok, Conn} = ecpool_worker:client(Worker), + case prepare_sql_to_conn(Conn, Prepares) of + {ok, Sts} -> + do_prepare_sql(T, Prepares, PoolName, Sts); + Error -> + Error + end; +do_prepare_sql([], _Prepares, _PoolName, LastSts) -> + {ok, LastSts}. + +prepare_sql_to_conn(Conn, Prepares) -> + prepare_sql_to_conn(Conn, Prepares, #{}). + +prepare_sql_to_conn(Conn, [], Statements) when is_pid(Conn) -> {ok, Statements}; +prepare_sql_to_conn(Conn, [{Key, SQL} | PrepareList], Statements) when is_pid(Conn) -> + LogMeta = #{msg => "PostgreSQL Prepare Statement", name => Key, prepare_sql => SQL}, + ?SLOG(info, LogMeta), + case epgsql:parse2(Conn, Key, SQL, []) of + {ok, Statement} -> + prepare_sql_to_conn(Conn, PrepareList, Statements#{Key => Statement}); + {error, Error} = Other -> + ?SLOG(error, LogMeta#{msg => "PostgreSQL parse failed", error => Error}), + Other + end. + +to_bin(Bin) when is_binary(Bin) -> + Bin; +to_bin(Atom) when is_atom(Atom) -> + erlang:atom_to_binary(Atom). diff --git a/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_pgsql.conf b/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_pgsql.conf new file mode 100644 index 000000000..0f80e1a1b --- /dev/null +++ b/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_pgsql.conf @@ -0,0 +1,74 @@ +emqx_ee_bridge_pgsql { + + local_topic { + desc { + en: """The MQTT topic filter to be forwarded to PostgreSQL. All MQTT 'PUBLISH' messages with the topic +matching the local_topic will be forwarded.
+NOTE: if this bridge is used as the action of a rule (EMQX rule engine), and also local_topic is +configured, then both the data got from the rule and the MQTT messages that match local_topic +will be forwarded. +""" + zh: """发送到 'local_topic' 的消息都会转发到 PostgreSQL。
+注意:如果这个 Bridge 被用作规则(EMQX 规则引擎)的输出,同时也配置了 'local_topic' ,那么这两部分的消息都会被转发。 +""" + } + label { + en: "Local Topic" + zh: "本地 Topic" + } + } + + sql_template { + desc { + en: """SQL Template""" + zh: """SQL 模板""" + } + label { + en: "SQL Template" + zh: "SQL 模板" + } + } + config_enable { + desc { + en: """Enable or disable this bridge""" + zh: """启用/禁用桥接""" + } + label { + en: "Enable Or Disable Bridge" + zh: "启用/禁用桥接" + } + } + + desc_config { + desc { + en: """Configuration for an PostgreSQL bridge.""" + zh: """PostgreSQL 桥接配置""" + } + label: { + en: "PostgreSQL Bridge Configuration" + zh: "PostgreSQL 桥接配置" + } + } + + desc_type { + desc { + en: """The Bridge Type""" + zh: """Bridge 类型""" + } + label { + en: "Bridge Type" + zh: "桥接类型" + } + } + + desc_name { + desc { + en: """Bridge name.""" + zh: """桥接名字""" + } + label { + en: "Bridge Name" + zh: "桥接名字" + } + } +} diff --git a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge.erl b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge.erl index 2e93a313c..d0099db1c 100644 --- a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge.erl +++ b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge.erl @@ -17,6 +17,7 @@ api_schemas(Method) -> ref(emqx_ee_bridge_gcp_pubsub, Method), ref(emqx_ee_bridge_kafka, Method), ref(emqx_ee_bridge_mysql, Method), + ref(emqx_ee_bridge_pgsql, Method), ref(emqx_ee_bridge_mongodb, Method ++ "_rs"), ref(emqx_ee_bridge_mongodb, Method ++ "_sharded"), ref(emqx_ee_bridge_mongodb, Method ++ "_single"), @@ -36,7 +37,8 @@ schema_modules() -> emqx_ee_bridge_influxdb, emqx_ee_bridge_mongodb, emqx_ee_bridge_mysql, - emqx_ee_bridge_redis + emqx_ee_bridge_redis, + emqx_ee_bridge_pgsql ]. examples(Method) -> @@ -63,7 +65,8 @@ resource_type(influxdb_api_v1) -> emqx_ee_connector_influxdb; resource_type(influxdb_api_v2) -> emqx_ee_connector_influxdb; resource_type(redis_single) -> emqx_ee_connector_redis; resource_type(redis_sentinel) -> emqx_ee_connector_redis; -resource_type(redis_cluster) -> emqx_ee_connector_redis. +resource_type(redis_cluster) -> emqx_ee_connector_redis; +resource_type(pgsql) -> emqx_connector_pgsql. fields(bridges) -> [ @@ -98,6 +101,14 @@ fields(bridges) -> desc => <<"MySQL Bridge Config">>, required => false } + )}, + {pgsql, + mk( + hoconsc:map(name, ref(emqx_ee_bridge_pgsql, "config")), + #{ + desc => <<"PostgreSQL Bridge Config">>, + required => false + } )} ] ++ mongodb_structs() ++ influxdb_structs() ++ redis_structs(). diff --git a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_pgsql.erl b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_pgsql.erl new file mode 100644 index 000000000..1f9a005c9 --- /dev/null +++ b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_pgsql.erl @@ -0,0 +1,130 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +-module(emqx_ee_bridge_pgsql). + +-include_lib("typerefl/include/types.hrl"). +-include_lib("hocon/include/hoconsc.hrl"). +-include_lib("emqx_bridge/include/emqx_bridge.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 +]). + +-define(DEFAULT_SQL, << + "insert into t_mqtt_msg(msgid, topic, qos, payload, arrived) " + "values (${id}, ${topic}, ${qos}, ${payload}, TO_TIMESTAMP((${timestamp} :: bigint)/1000))" +>>). + +%% ------------------------------------------------------------------------------------------------- +%% api + +conn_bridge_examples(Method) -> + [ + #{ + <<"pgsql">> => #{ + summary => <<"PostgreSQL Bridge">>, + value => values(Method) + } + } + ]. + +values(get) -> + maps:merge(values(post), ?METRICS_EXAMPLE); +values(post) -> + #{ + enable => true, + type => pgsql, + name => <<"foo">>, + server => <<"127.0.0.1:5432">>, + database => <<"mqtt">>, + pool_size => 8, + username => <<"root">>, + password => <<"public">>, + auto_reconnect => true, + sql => ?DEFAULT_SQL, + local_topic => <<"local/topic/#">>, + 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_queue_bytes => ?DEFAULT_QUEUE_SIZE + } + }; +values(put) -> + values(post). + +%% ------------------------------------------------------------------------------------------------- +%% Hocon Schema Definitions +namespace() -> "bridge_pgsql". + +roots() -> []. + +fields("config") -> + [ + {enable, mk(boolean(), #{desc => ?DESC("config_enable"), default => true})}, + {sql, + mk( + binary(), + #{desc => ?DESC("sql_template"), default => ?DEFAULT_SQL, format => <<"sql">>} + )}, + {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_connector_mysql:fields(config) -- emqx_connector_schema_lib:prepare_statement_fields(); +fields("creation_opts") -> + Opts = emqx_resource_schema:fields("creation_opts"), + [O || {Field, _} = O <- Opts, not is_hidden_opts(Field)]; +fields("post") -> + [type_field(), name_field() | fields("config")]; +fields("put") -> + fields("config"); +fields("get") -> + emqx_bridge_schema:metrics_status_fields() ++ fields("post"). + +desc("config") -> + ?DESC("desc_config"); +desc(Method) when Method =:= "get"; Method =:= "put"; Method =:= "post" -> + ["Configuration for PostgreSQL using `", string:to_upper(Method), "` method."]; +desc("creation_opts" = Name) -> + emqx_resource_schema:desc(Name); +desc(_) -> + undefined. + +%% ------------------------------------------------------------------------------------------------- +%% internal +is_hidden_opts(Field) -> + lists:member(Field, [ + async_inflight_window + ]). + +type_field() -> + {type, mk(enum([mysql]), #{required => true, desc => ?DESC("desc_type")})}. + +name_field() -> + {name, mk(binary(), #{required => true, desc => ?DESC("desc_name")})}. From ea405fe55d8530d06ff006804c5a8277afd2bc0d Mon Sep 17 00:00:00 2001 From: firest Date: Thu, 5 Jan 2023 15:05:49 +0800 Subject: [PATCH 0047/1802] test(bridges): add test case for the PostgreSQL backend --- .../docker-compose-toxiproxy.yaml | 2 + .ci/docker-compose-file/toxiproxy.json | 12 + lib-ee/emqx_ee_bridge/docker-ct | 1 + .../test/emqx_ee_bridge_pgsql_SUITE.erl | 481 ++++++++++++++++++ 4 files changed, 496 insertions(+) create mode 100644 lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_pgsql_SUITE.erl diff --git a/.ci/docker-compose-file/docker-compose-toxiproxy.yaml b/.ci/docker-compose-file/docker-compose-toxiproxy.yaml index 66e7ec308..ce4f28ba7 100644 --- a/.ci/docker-compose-file/docker-compose-toxiproxy.yaml +++ b/.ci/docker-compose-file/docker-compose-toxiproxy.yaml @@ -15,6 +15,8 @@ services: - 8087:8087 - 13306:3306 - 13307:3307 + - 15432:5432 + - 15433:5433 command: - "-host=0.0.0.0" - "-config=/config/toxiproxy.json" diff --git a/.ci/docker-compose-file/toxiproxy.json b/.ci/docker-compose-file/toxiproxy.json index 34bc5b1db..f4b11116b 100644 --- a/.ci/docker-compose-file/toxiproxy.json +++ b/.ci/docker-compose-file/toxiproxy.json @@ -29,5 +29,17 @@ "listen": "0.0.0.0:6379", "upstream": "redis:6379", "enabled": true + }, + { + "name": "pgsql_tcp", + "listen": "0.0.0.0:5432", + "upstream": "pgsql:5432", + "enabled": true + }, + { + "name": "pgsql_tls", + "listen": "0.0.0.0:5433", + "upstream": "pgsql-tls:5432", + "enabled": true } ] diff --git a/lib-ee/emqx_ee_bridge/docker-ct b/lib-ee/emqx_ee_bridge/docker-ct index fba33559e..bf990bd7c 100644 --- a/lib-ee/emqx_ee_bridge/docker-ct +++ b/lib-ee/emqx_ee_bridge/docker-ct @@ -6,3 +6,4 @@ mongo_rs_sharded mysql redis redis_cluster +pgsql diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_pgsql_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_pgsql_SUITE.erl new file mode 100644 index 000000000..c5292a892 --- /dev/null +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_pgsql_SUITE.erl @@ -0,0 +1,481 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- + +-module(emqx_ee_bridge_pgsql_SUITE). + +-compile(nowarn_export_all). +-compile(export_all). + +-include_lib("eunit/include/eunit.hrl"). +-include_lib("common_test/include/ct.hrl"). +-include_lib("snabbkaffe/include/snabbkaffe.hrl"). + +% SQL definitions +-define(SQL_BRIDGE, + "INSERT INTO mqtt_test(payload, arrived) " + "VALUES (${payload}, TO_TIMESTAMP((${timestamp} :: bigint)/1000))" +). +-define(SQL_CREATE_TABLE, + "CREATE TABLE IF NOT EXISTS mqtt_test (payload text, arrived timestamp NOT NULL) " +). +-define(SQL_DROP_TABLE, "DROP TABLE mqtt_test"). +-define(SQL_DELETE, "DELETE from mqtt_test"). +-define(SQL_SELECT, "SELECT payload FROM mqtt_test"). + +% DB defaults +-define(PGSQL_DATABASE, "mqtt"). +-define(PGSQL_USERNAME, "root"). +-define(PGSQL_PASSWORD, "public"). +-define(BATCH_SIZE, 10). + +%%------------------------------------------------------------------------------ +%% CT boilerplate +%%------------------------------------------------------------------------------ + +all() -> + [ + {group, tcp}, + {group, tls} + ]. + +groups() -> + TCs = emqx_common_test_helpers:all(?MODULE), + NonBatchCases = [t_write_timeout], + [ + {tcp, [ + {group, with_batch}, + {group, without_batch} + ]}, + {tls, [ + {group, with_batch}, + {group, without_batch} + ]}, + {with_batch, TCs -- NonBatchCases}, + {without_batch, TCs} + ]. + +init_per_group(tcp, Config) -> + Host = os:getenv("PGSQL_TCP_HOST", "toxiproxy"), + Port = list_to_integer(os:getenv("PGSQL_TCP_PORT", "5432")), + [ + {pgsql_host, Host}, + {pgsql_port, Port}, + {enable_tls, false}, + {query_mode, sync}, + {proxy_name, "pgsql_tcp"} + | Config + ]; +init_per_group(tls, Config) -> + Host = os:getenv("PGSQL_TLS_HOST", "toxiproxy"), + Port = list_to_integer(os:getenv("PGSQL_TLS_PORT", "5433")), + [ + {pgsql_host, Host}, + {pgsql_port, Port}, + {enable_tls, true}, + {query_mode, sync}, + {proxy_name, "pgsql_tls"} + | Config + ]; +init_per_group(with_batch, Config0) -> + Config = [{enable_batch, true} | Config0], + common_init(Config); +init_per_group(without_batch, Config0) -> + Config = [{enable_batch, false} | Config0], + common_init(Config); +init_per_group(_Group, Config) -> + Config. + +end_per_group(Group, Config) when Group =:= with_batch; Group =:= without_batch -> + connect_and_drop_table(Config), + ProxyHost = ?config(proxy_host, Config), + ProxyPort = ?config(proxy_port, Config), + emqx_common_test_helpers:reset_proxy(ProxyHost, ProxyPort), + ok; +end_per_group(_Group, _Config) -> + ok. + +init_per_suite(Config) -> + Config. + +end_per_suite(_Config) -> + emqx_mgmt_api_test_util:end_suite(), + ok = emqx_common_test_helpers:stop_apps([emqx_bridge, emqx_conf]), + ok. + +init_per_testcase(_Testcase, Config) -> + connect_and_clear_table(Config), + delete_bridge(Config), + Config. + +end_per_testcase(_Testcase, Config) -> + ProxyHost = ?config(proxy_host, Config), + ProxyPort = ?config(proxy_port, Config), + emqx_common_test_helpers:reset_proxy(ProxyHost, ProxyPort), + connect_and_clear_table(Config), + ok = snabbkaffe:stop(), + delete_bridge(Config), + ok. + +%%------------------------------------------------------------------------------ +%% Helper fns +%%------------------------------------------------------------------------------ + +common_init(Config0) -> + BridgeType = <<"pgsql">>, + Host = ?config(pgsql_host, Config0), + Port = ?config(pgsql_port, Config0), + case emqx_common_test_helpers:is_tcp_server_available(Host, Port) of + true -> + % Setup toxiproxy + ProxyHost = os:getenv("PROXY_HOST", "toxiproxy"), + ProxyPort = list_to_integer(os:getenv("PROXY_PORT", "8474")), + emqx_common_test_helpers:reset_proxy(ProxyHost, ProxyPort), + % Ensure EE bridge module is loaded + _ = application:load(emqx_ee_bridge), + _ = emqx_ee_bridge:module_info(), + ok = emqx_common_test_helpers:start_apps([emqx_conf, emqx_bridge]), + emqx_mgmt_api_test_util:init_suite(), + % Connect to pgsql directly and create the table + connect_and_create_table(Config0), + {Name, PGConf} = pgsql_config(BridgeType, Config0), + Config = + [ + {pgsql_config, PGConf}, + {pgsql_bridge_type, BridgeType}, + {pgsql_name, Name}, + {proxy_host, ProxyHost}, + {proxy_port, ProxyPort} + | Config0 + ], + Config; + false -> + case os:getenv("IS_CI") of + "yes" -> + throw(no_pgsql); + _ -> + {skip, no_pgsql} + end + end. + +pgsql_config(BridgeType, Config) -> + Port = integer_to_list(?config(pgsql_port, Config)), + Server = ?config(pgsql_host, Config) ++ ":" ++ Port, + Name = atom_to_binary(?MODULE), + BatchSize = + case ?config(enable_batch, Config) of + true -> ?BATCH_SIZE; + false -> 1 + end, + QueryMode = ?config(query_mode, Config), + TlsEnabled = ?config(enable_tls, Config), + ConfigString = + io_lib:format( + "bridges.~s.~s {\n" + " enable = true\n" + " server = ~p\n" + " database = ~p\n" + " username = ~p\n" + " password = ~p\n" + " sql = ~p\n" + " resource_opts = {\n" + " batch_size = ~b\n" + " query_mode = ~s\n" + " }\n" + " ssl = {\n" + " enable = ~w\n" + " }\n" + "}", + [ + BridgeType, + Name, + Server, + ?PGSQL_DATABASE, + ?PGSQL_USERNAME, + ?PGSQL_PASSWORD, + ?SQL_BRIDGE, + BatchSize, + QueryMode, + TlsEnabled + ] + ), + {Name, parse_and_check(ConfigString, BridgeType, 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 := Config}}} = RawConf, + Config. + +create_bridge(Config) -> + BridgeType = ?config(pgsql_bridge_type, Config), + Name = ?config(pgsql_name, Config), + PGConfig = ?config(pgsql_config, Config), + emqx_bridge:create(BridgeType, Name, PGConfig). + +delete_bridge(Config) -> + BridgeType = ?config(pgsql_bridge_type, Config), + Name = ?config(pgsql_name, Config), + emqx_bridge:remove(BridgeType, Name). + +create_bridge_http(Params) -> + Path = emqx_mgmt_api_test_util:api_path(["bridges"]), + AuthHeader = emqx_mgmt_api_test_util:auth_header_(), + case emqx_mgmt_api_test_util:request_api(post, Path, "", AuthHeader, Params) of + {ok, Res} -> {ok, emqx_json:decode(Res, [return_maps])}; + Error -> Error + end. + +send_message(Config, Payload) -> + Name = ?config(pgsql_name, Config), + BridgeType = ?config(pgsql_bridge_type, Config), + BridgeID = emqx_bridge_resource:bridge_id(BridgeType, Name), + emqx_bridge:send_message(BridgeID, Payload). + +query_resource(Config, Request) -> + Name = ?config(pgsql_name, Config), + BridgeType = ?config(pgsql_bridge_type, Config), + ResourceID = emqx_bridge_resource:resource_id(BridgeType, Name), + emqx_resource:query(ResourceID, Request). + +connect_direct_pgsql(Config) -> + Opts = #{ + host => ?config(pgsql_host, Config), + port => ?config(pgsql_port, Config), + username => ?PGSQL_USERNAME, + password => ?PGSQL_PASSWORD, + database => ?PGSQL_DATABASE + }, + + SslOpts = + case ?config(enable_tls, Config) of + true -> + Opts#{ + ssl => true, + ssl_opts => emqx_tls_lib:to_client_opts(#{enable => true}) + }; + false -> + Opts + end, + {ok, Con} = epgsql:connect(SslOpts), + Con. + +% These funs connect and then stop the pgsql connection +connect_and_create_table(Config) -> + Con = connect_direct_pgsql(Config), + {ok, _, _} = epgsql:squery(Con, ?SQL_CREATE_TABLE), + ok = epgsql:close(Con). + +connect_and_drop_table(Config) -> + Con = connect_direct_pgsql(Config), + {ok, _, _} = epgsql:squery(Con, ?SQL_DROP_TABLE), + ok = epgsql:close(Con). + +connect_and_clear_table(Config) -> + Con = connect_direct_pgsql(Config), + {ok, _} = epgsql:squery(Con, ?SQL_DELETE), + ok = epgsql:close(Con). + +connect_and_get_payload(Config) -> + Con = connect_direct_pgsql(Config), + {ok, _, [{Result}]} = epgsql:squery(Con, ?SQL_SELECT), + ok = epgsql:close(Con), + Result. + +%%------------------------------------------------------------------------------ +%% Testcases +%%------------------------------------------------------------------------------ + +t_setup_via_config_and_publish(Config) -> + ?assertMatch( + {ok, _}, + create_bridge(Config) + ), + Val = integer_to_binary(erlang:unique_integer()), + SentData = #{payload => Val, timestamp => 1668602148000}, + ?check_trace( + begin + ?wait_async_action( + ?assertEqual({ok, 1}, send_message(Config, SentData)), + #{?snk_kind := pgsql_connector_query_return}, + 10_000 + ), + ?assertMatch( + Val, + connect_and_get_payload(Config) + ), + ok + end, + fun(Trace0) -> + Trace = ?of_kind(pgsql_connector_query_return, Trace0), + case ?config(enable_batch, Config) of + true -> + ?assertMatch([#{result := {_, [{ok, 1}]}}], Trace); + false -> + ?assertMatch([#{result := {ok, 1}}], Trace) + end, + ok + end + ), + ok. + +t_setup_via_http_api_and_publish(Config) -> + BridgeType = ?config(pgsql_bridge_type, Config), + Name = ?config(pgsql_name, Config), + PgsqlConfig0 = ?config(pgsql_config, Config), + PgsqlConfig = PgsqlConfig0#{ + <<"name">> => Name, + <<"type">> => BridgeType + }, + ?assertMatch( + {ok, _}, + create_bridge_http(PgsqlConfig) + ), + Val = integer_to_binary(erlang:unique_integer()), + SentData = #{payload => Val, timestamp => 1668602148000}, + ?check_trace( + begin + ?wait_async_action( + ?assertEqual({ok, 1}, send_message(Config, SentData)), + #{?snk_kind := pgsql_connector_query_return}, + 10_000 + ), + ?assertMatch( + Val, + connect_and_get_payload(Config) + ), + ok + end, + fun(Trace0) -> + Trace = ?of_kind(pgsql_connector_query_return, Trace0), + case ?config(enable_batch, Config) of + true -> + ?assertMatch([#{result := {_, [{ok, 1}]}}], Trace); + false -> + ?assertMatch([#{result := {ok, 1}}], Trace) + end, + ok + end + ), + ok. + +t_get_status(Config) -> + ?assertMatch( + {ok, _}, + create_bridge(Config) + ), + ProxyPort = ?config(proxy_port, Config), + ProxyHost = ?config(proxy_host, Config), + ProxyName = ?config(proxy_name, Config), + + Name = ?config(pgsql_name, Config), + BridgeType = ?config(pgsql_bridge_type, Config), + ResourceID = emqx_bridge_resource:resource_id(BridgeType, Name), + + ?assertEqual({ok, connected}, emqx_resource_manager:health_check(ResourceID)), + emqx_common_test_helpers:with_failure(down, ProxyName, ProxyHost, ProxyPort, fun() -> + ?assertMatch( + {ok, Status} when Status =:= disconnected orelse Status =:= connecting, + emqx_resource_manager:health_check(ResourceID) + ) + end), + ok. + +t_create_disconnected(Config) -> + ProxyPort = ?config(proxy_port, Config), + ProxyHost = ?config(proxy_host, Config), + ProxyName = ?config(proxy_name, Config), + ?check_trace( + emqx_common_test_helpers:with_failure(down, ProxyName, ProxyHost, ProxyPort, fun() -> + ?assertMatch({ok, _}, create_bridge(Config)) + end), + fun(Trace) -> + ?assertMatch( + [#{error := {start_pool_failed, _, _}}], + ?of_kind(pgsql_connector_start_failed, Trace) + ), + ok + end + ), + ok. + +t_write_failure(Config) -> + ProxyName = ?config(proxy_name, Config), + ProxyPort = ?config(proxy_port, Config), + ProxyHost = ?config(proxy_host, Config), + {ok, _} = create_bridge(Config), + Val = integer_to_binary(erlang:unique_integer()), + SentData = #{payload => Val, timestamp => 1668602148000}, + ?check_trace( + emqx_common_test_helpers:with_failure(down, ProxyName, ProxyHost, ProxyPort, fun() -> + send_message(Config, SentData) + end), + fun + ({error, {resource_error, _}}, _Trace) -> + ok; + ({error, {recoverable_error, disconnected}}, _Trace) -> + ok; + (_, _Trace) -> + ?assert(false) + end + ), + ok. + +% This test doesn't work with batch enabled since it is not possible +% to set the timeout directly for batch queries +t_write_timeout(Config) -> + ProxyName = ?config(proxy_name, Config), + ProxyPort = ?config(proxy_port, Config), + ProxyHost = ?config(proxy_host, Config), + {ok, _} = create_bridge(Config), + Val = integer_to_binary(erlang:unique_integer()), + SentData = #{payload => Val, timestamp => 1668602148000}, + Timeout = 10, + emqx_common_test_helpers:with_failure(timeout, ProxyName, ProxyHost, ProxyPort, fun() -> + ?assertMatch( + {error, {resource_error, _}}, + query_resource(Config, {send_message, SentData, [], Timeout}) + ) + end), + ok. + +t_simple_sql_query(Config) -> + ?assertMatch( + {ok, _}, + create_bridge(Config) + ), + Request = {sql, <<"SELECT count(1) AS T">>}, + Result = query_resource(Config, Request), + case ?config(enable_batch, Config) of + true -> ?assertEqual({error, batch_prepare_not_implemented}, Result); + false -> ?assertMatch({ok, _, [{1}]}, Result) + end, + ok. + +t_missing_data(Config) -> + ?assertMatch( + {ok, _}, + create_bridge(Config) + ), + Result = send_message(Config, #{}), + ?assertMatch( + {error, {error, error, <<"23502">>, not_null_violation, _, _}}, Result + ), + ok. + +t_bad_sql_parameter(Config) -> + ?assertMatch( + {ok, _}, + create_bridge(Config) + ), + Request = {sql, <<"">>, [bad_parameter]}, + Result = query_resource(Config, Request), + case ?config(enable_batch, Config) of + true -> + ?assertEqual({error, invalid_request}, Result); + false -> + ?assertMatch( + {error, {resource_error, _}}, Result + ) + end, + ok. From d2456f9fbee01546c435590af77a926698953a30 Mon Sep 17 00:00:00 2001 From: firest Date: Fri, 6 Jan 2023 17:28:06 +0800 Subject: [PATCH 0048/1802] chore: add i18n file for ldap connector --- .../i18n/emqx_connector_ldap.conf | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 apps/emqx_connector/i18n/emqx_connector_ldap.conf diff --git a/apps/emqx_connector/i18n/emqx_connector_ldap.conf b/apps/emqx_connector/i18n/emqx_connector_ldap.conf new file mode 100644 index 000000000..0bcb4869e --- /dev/null +++ b/apps/emqx_connector/i18n/emqx_connector_ldap.conf @@ -0,0 +1,37 @@ +emqx_connector_ldap { + + bind_dn { + desc { + en: """LDAP's Binding Distinguished Name (DN)""" + zh: """LDAP 绑定的 DN 的值""" + } + label: { + en: "Bind DN" + zh: "Bind DN" + } + } + + port { + desc { + en: """LDAP Port""" + zh: """LDAP 端口""" + } + label: { + en: "Port" + zh: "端口" + } + } + + + timeout { + desc { + en: """LDAP's query timeout""" + zh: """LDAP 查询超时时间""" + } + label: { + en: "timeout" + zh: "超时时间" + } + } + +} From 91f182de348f431fb2c38ff11d1a2fe7e63bd303 Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Wed, 4 Jan 2023 13:49:24 +0300 Subject: [PATCH 0049/1802] chore: bump to emqx/ehttpc 0.4.3 Which sports full OTP-25 compatibility. --- mix.exs | 2 +- rebar.config | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mix.exs b/mix.exs index 9c8d6ff59..265448d72 100644 --- a/mix.exs +++ b/mix.exs @@ -47,7 +47,7 @@ defmodule EMQXUmbrella.MixProject do {:lc, github: "emqx/lc", tag: "0.3.2", override: true}, {:redbug, "2.0.8"}, {:typerefl, github: "ieQu1/typerefl", tag: "0.9.1", override: true}, - {:ehttpc, github: "emqx/ehttpc", tag: "0.4.2", override: true}, + {:ehttpc, github: "emqx/ehttpc", tag: "0.4.3", override: true}, {:gproc, github: "uwiger/gproc", tag: "0.8.0", override: true}, {:jiffy, github: "emqx/jiffy", tag: "1.0.5", override: true}, {:cowboy, github: "emqx/cowboy", tag: "2.9.0", override: true}, diff --git a/rebar.config b/rebar.config index 3d19d2181..4476a128c 100644 --- a/rebar.config +++ b/rebar.config @@ -49,7 +49,7 @@ , {gpb, "4.19.5"} %% gpb only used to build, but not for release, pin it here to avoid fetching a wrong version due to rebar plugins scattered in all the deps , {typerefl, {git, "https://github.com/ieQu1/typerefl", {tag, "0.9.1"}}} , {gun, {git, "https://github.com/emqx/gun", {tag, "1.3.9"}}} - , {ehttpc, {git, "https://github.com/emqx/ehttpc", {tag, "0.4.2"}}} + , {ehttpc, {git, "https://github.com/emqx/ehttpc", {tag, "0.4.3"}}} , {gproc, {git, "https://github.com/uwiger/gproc", {tag, "0.8.0"}}} , {jiffy, {git, "https://github.com/emqx/jiffy", {tag, "1.0.5"}}} , {cowboy, {git, "https://github.com/emqx/cowboy", {tag, "2.9.0"}}} From 1336a57b3bcc10d13c75ecb72d45ca117d7bc88b Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Wed, 4 Jan 2023 13:50:53 +0300 Subject: [PATCH 0050/1802] chore: bump to emqx/eredis_cluster 0.7.5 * Full OTP-25 compat. * Password censoring in status and crash reports. * No more crashes on query timeout. --- apps/emqx_connector/rebar.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx_connector/rebar.config b/apps/emqx_connector/rebar.config index 98490a91c..afe09df7b 100644 --- a/apps/emqx_connector/rebar.config +++ b/apps/emqx_connector/rebar.config @@ -14,7 +14,7 @@ %% NOTE: mind poolboy version when updating mongodb-erlang version {mongodb, {git, "https://github.com/emqx/mongodb-erlang", {tag, "v3.0.13"}}}, %% NOTE: mind poolboy version when updating eredis_cluster version - {eredis_cluster, {git, "https://github.com/emqx/eredis_cluster", {tag, "0.7.1"}}}, + {eredis_cluster, {git, "https://github.com/emqx/eredis_cluster", {tag, "0.7.5"}}}, %% mongodb-erlang uses a special fork https://github.com/comtihon/poolboy.git %% (which has overflow_ttl feature added). %% However, it references `{branch, "master}` (commit 9c06a9a on 2021-04-07). From 7ed13846232dbd80f1a7f76f722ee06b0a63cd31 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Mon, 9 Jan 2023 16:02:10 +0800 Subject: [PATCH 0051/1802] fix: missing default value for qos field of API /clients/:clientid/subscribe --- apps/emqx_management/src/emqx_mgmt_api_clients.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx_management/src/emqx_mgmt_api_clients.erl b/apps/emqx_management/src/emqx_mgmt_api_clients.erl index 8eb4b26e9..539f38c6c 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_clients.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_clients.erl @@ -550,7 +550,7 @@ fields(keepalive) -> fields(subscribe) -> [ {topic, hoconsc:mk(binary(), #{desc => <<"Topic">>})}, - {qos, hoconsc:mk(emqx_schema:qos(), #{desc => <<"QoS">>})}, + {qos, hoconsc:mk(emqx_schema:qos(), #{default => 0, desc => <<"QoS">>})}, {nl, hoconsc:mk(integer(), #{default => 0, desc => <<"No Local">>})}, {rap, hoconsc:mk(integer(), #{default => 0, desc => <<"Retain as Published">>})}, {rh, hoconsc:mk(integer(), #{default => 0, desc => <<"Retain Handling">>})} From e498010f2379bdbfeb51dafef3e2c41a59e5255e Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Mon, 9 Jan 2023 08:27:55 +0100 Subject: [PATCH 0052/1802] chore: update default license (100 conns) --- lib-ee/emqx_license/i18n/emqx_license_schema_i18n.conf | 6 +++--- lib-ee/emqx_license/src/emqx_license_schema.erl | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib-ee/emqx_license/i18n/emqx_license_schema_i18n.conf b/lib-ee/emqx_license/i18n/emqx_license_schema_i18n.conf index d4a91159d..379cb3358 100644 --- a/lib-ee/emqx_license/i18n/emqx_license_schema_i18n.conf +++ b/lib-ee/emqx_license/i18n/emqx_license_schema_i18n.conf @@ -3,14 +3,14 @@ emqx_license_schema { desc { en: "Defines the EMQX Enterprise license. \n\n" "\n" - "The default license has 1000 connections limit, it is " - "issued on 2023-01-02 and valid for 5 years (1825 days).\n" + "The default license has 100 connections limit, it is " + "issued on 2023-01-09 and valid for 5 years (1825 days).\n" "\n" "EMQX comes with a default trial license. For production use, please \n" "visit https://www.emqx.com/apply-licenses/emqx to apply." zh: "EMQX企业许可证。\n" "EMQX 自带一个默认的试用许可证," - "默认试用许可允许最多接入 1000 个连接,签发时间是 2023年1月2日,有效期是 5 年(1825 天)。" + "默认试用许可允许最多接入 100 个连接,签发时间是 2023年1月9日,有效期是 5 年(1825 天)。" "若需要在生产环境部署,\n" "请访问 https://www.emqx.com/apply-licenses/emqx 来申请。\n" } diff --git a/lib-ee/emqx_license/src/emqx_license_schema.erl b/lib-ee/emqx_license/src/emqx_license_schema.erl index dbac851fe..bde4ed076 100644 --- a/lib-ee/emqx_license/src/emqx_license_schema.erl +++ b/lib-ee/emqx_license/src/emqx_license_schema.erl @@ -78,11 +78,11 @@ check_license_watermark(Conf) -> %% @doc The default license key. %% This default license has 1000 connections limit. -%% It is issued on 2023-01-02 and valid for 5 years (1825 days) +%% It is issued on 2023-01-09 and valid for 5 years (1825 days) %% NOTE: when updating a new key, the schema doc in emqx_license_schema_i18n.conf %% should be updated accordingly default_license() -> "MjIwMTExCjAKMTAKRXZhbHVhdGlvbgpjb250YWN0QGVtcXguaW8KZ" - "GVmYXVsdAoyMDIzMDEwMgoxODI1CjEwMDAK.MEQCIGEuYO8KxSh5d" - "1WanqHG41OOjHEVkU8ChnyoOTry2FFUAiA+vPBAH8yhcGuzMUX1ER" - "kf6nY+xrVSKxnsx0GivANEXA==". + "GVmYXVsdAoyMDIzMDEwOQoxODI1CjEwMAo=.MEUCIG62t8W15g05f" + "1cKx3tA3YgJoR0dmyHOPCdbUxBGxgKKAiEAhHKh8dUwhU+OxNEaOn" + "8mgRDtiT3R8RZooqy6dEsOmDI=". From e88529d55b92d80b14d888fc339083702c43663a Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Mon, 9 Jan 2023 16:12:45 +0800 Subject: [PATCH 0053/1802] chore: update change logs for #9703 --- changes/v5.0.14/fix-9703.en.md | 3 +++ changes/v5.0.14/fix-9703.zh.md | 2 ++ 2 files changed, 5 insertions(+) create mode 100644 changes/v5.0.14/fix-9703.en.md create mode 100644 changes/v5.0.14/fix-9703.zh.md diff --git a/changes/v5.0.14/fix-9703.en.md b/changes/v5.0.14/fix-9703.en.md new file mode 100644 index 000000000..4eb91c7d0 --- /dev/null +++ b/changes/v5.0.14/fix-9703.en.md @@ -0,0 +1,3 @@ +Set the default value of the `qos` field of the HTTP API `/clients/:clientid/subscribe` to 0. +Before this fix, the `qos` field have no default value, which leads to a `function_clause` error +when querying this API. diff --git a/changes/v5.0.14/fix-9703.zh.md b/changes/v5.0.14/fix-9703.zh.md new file mode 100644 index 000000000..863304a66 --- /dev/null +++ b/changes/v5.0.14/fix-9703.zh.md @@ -0,0 +1,2 @@ +将 HTTP 接口 `/clients/:clientid/subscribe` 的 `qos` 字段的默认值设置为 0。 +在此修复之前,`qos` 字段没有默认值,调用订阅接口的时候将导致 `function_clause` 错误。 From 8d53d154d94ee9003f220421705d0f9e6779b720 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Mon, 9 Jan 2023 09:16:19 +0100 Subject: [PATCH 0054/1802] refactor: remove license from configs API license has its own config APIs --- apps/emqx_management/src/emqx_mgmt_api_configs.erl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/emqx_management/src/emqx_mgmt_api_configs.erl b/apps/emqx_management/src/emqx_mgmt_api_configs.erl index eec5793d0..976dd29f2 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_configs.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_configs.erl @@ -62,7 +62,8 @@ <<"event_message">>, <<"prometheus">>, <<"telemetry">>, - <<"listeners">> + <<"listeners">>, + <<"license">> ] ++ global_zone_roots() ). From 3319a8d28ef578e8825e6430b07686671d02721f Mon Sep 17 00:00:00 2001 From: Erik Timan Date: Tue, 3 Jan 2023 14:32:40 +0100 Subject: [PATCH 0055/1802] fix(mgmt_api): remove possibility to set clientid in /publish API To avoid security confusion, we remove the possibility to specify the client ID in the request body for /publish and /publish/bulk. --- apps/emqx_management/src/emqx_mgmt_api_publish.erl | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/apps/emqx_management/src/emqx_mgmt_api_publish.erl b/apps/emqx_management/src/emqx_mgmt_api_publish.erl index 672de661b..b7c48a26a 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_publish.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_publish.erl @@ -102,12 +102,6 @@ fields(message) -> required => false, default => 0 })}, - {clientid, - hoconsc:mk(binary(), #{ - desc => ?DESC(clientid), - required => false, - example => <<"api_example_client">> - })}, {payload, hoconsc:mk(binary(), #{ desc => ?DESC(payload), @@ -254,7 +248,6 @@ is_ok_deliver({_NodeOrShare, _MatchedTopic, {error, _}}) -> false. %% %%%%%% Below error codes are not implemented so far %%%% %% %% If HTTP request passes HTTP authentication, it is considered trusted. -%% In the future, we may choose to check ACL for the provided MQTT Client ID %% 135 Not authorized 401 %% %% %%%%%% Below error codes are not applicable %%%%%%% @@ -326,7 +319,6 @@ make_message(Map) -> Encoding = maps:get(<<"payload_encoding">>, Map, plain), case decode_payload(Encoding, maps:get(<<"payload">>, Map)) of {ok, Payload} -> - From = maps:get(<<"clientid">>, Map, http_api), QoS = maps:get(<<"qos">>, Map, 0), Topic = maps:get(<<"topic">>, Map), Retain = maps:get(<<"retain">>, Map, false), @@ -346,7 +338,9 @@ make_message(Map) -> error:_Reason -> throw(invalid_topic_name) end, - Message = emqx_message:make(From, QoS, Topic, Payload, #{retain => Retain}, Headers), + Message = emqx_message:make( + http_api, QoS, Topic, Payload, #{retain => Retain}, Headers + ), Size = emqx_message:estimate_size(Message), (Size > size_limit()) andalso throw(packet_too_large), {ok, Message}; From 19033c812ab671438bcd97fec6b4caa7537f683b Mon Sep 17 00:00:00 2001 From: Erik Timan Date: Tue, 3 Jan 2023 15:41:01 +0100 Subject: [PATCH 0056/1802] chore: update changes --- changes/v5.0.14/fix-9667.en.md | 1 + changes/v5.0.14/fix-9667.zh.md | 1 + 2 files changed, 2 insertions(+) create mode 100644 changes/v5.0.14/fix-9667.en.md create mode 100644 changes/v5.0.14/fix-9667.zh.md diff --git a/changes/v5.0.14/fix-9667.en.md b/changes/v5.0.14/fix-9667.en.md new file mode 100644 index 000000000..4b0fe7aef --- /dev/null +++ b/changes/v5.0.14/fix-9667.en.md @@ -0,0 +1 @@ +Remove possibility to set `clientid` for `/publish` and `/publish/bulk` HTTP APIs. This is to reduce the risk for security confusion. diff --git a/changes/v5.0.14/fix-9667.zh.md b/changes/v5.0.14/fix-9667.zh.md new file mode 100644 index 000000000..f3952ca14 --- /dev/null +++ b/changes/v5.0.14/fix-9667.zh.md @@ -0,0 +1 @@ +从 HTTP API /publish 和 /publish/bulk 中移除 clientid, 降低安全风险 From a30c2471f62340d82d8c278cdb2a56ac2a2013f9 Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Mon, 9 Jan 2023 11:52:42 +0300 Subject: [PATCH 0057/1802] chore: update the changelog --- changes/v5.0.14/feat-9675.en.md | 2 ++ changes/v5.0.14/feat-9675.zh.md | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 changes/v5.0.14/feat-9675.en.md create mode 100644 changes/v5.0.14/feat-9675.zh.md diff --git a/changes/v5.0.14/feat-9675.en.md b/changes/v5.0.14/feat-9675.en.md new file mode 100644 index 000000000..5249e9826 --- /dev/null +++ b/changes/v5.0.14/feat-9675.en.md @@ -0,0 +1,2 @@ +HTTP client library `ehttpc` upgraded from `0.4.2` to `0.4.3`. +Library `eredis_cluster` which manages clients to redis clusters upgraded from `0.7.1` to `0.7.5`. diff --git a/changes/v5.0.14/feat-9675.zh.md b/changes/v5.0.14/feat-9675.zh.md new file mode 100644 index 000000000..d14f260ae --- /dev/null +++ b/changes/v5.0.14/feat-9675.zh.md @@ -0,0 +1,2 @@ +HTTP 客户端库 `ehttpc` 从 `0.4.2` 升级到 `0.4.3` +Redis cluster 客户端库 `eredis_cluster` 从 `0.7.1` 升级到 `0.7.5`. From 903ae9a6446e2643f9b8ced9de72220af5bba525 Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Wed, 21 Dec 2022 10:07:18 +0100 Subject: [PATCH 0058/1802] style: fix typo in fun name --- apps/emqx/src/emqx_map_lib.erl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/emqx/src/emqx_map_lib.erl b/apps/emqx/src/emqx_map_lib.erl index 5455fe9e7..631c3914d 100644 --- a/apps/emqx/src/emqx_map_lib.erl +++ b/apps/emqx/src/emqx_map_lib.erl @@ -152,7 +152,7 @@ deep_convert(Val, _, _Args) -> -spec unsafe_atom_key_map(#{binary() | atom() => any()}) -> #{atom() => any()}. unsafe_atom_key_map(Map) -> - covert_keys_to_atom(Map, fun(K) -> binary_to_atom(K, utf8) end). + convert_keys_to_atom(Map, fun(K) -> binary_to_atom(K, utf8) end). -spec binary_key_map(map()) -> map(). binary_key_map(Map) -> @@ -167,7 +167,7 @@ binary_key_map(Map) -> -spec safe_atom_key_map(#{binary() | atom() => any()}) -> #{atom() => any()}. safe_atom_key_map(Map) -> - covert_keys_to_atom(Map, fun(K) -> binary_to_existing_atom(K, utf8) end). + convert_keys_to_atom(Map, fun(K) -> binary_to_existing_atom(K, utf8) end). -spec jsonable_map(map() | list()) -> map() | list(). jsonable_map(Map) -> @@ -221,7 +221,7 @@ binary_string(Val) -> Val. %%--------------------------------------------------------------------------- -covert_keys_to_atom(BinKeyMap, Conv) -> +convert_keys_to_atom(BinKeyMap, Conv) -> deep_convert( BinKeyMap, fun From 96ca0d9f499b591152241921e69feb1dd524dade Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Tue, 20 Dec 2022 16:12:13 +0100 Subject: [PATCH 0059/1802] feat(emqx_bridge): add /bridges_probe API endpoint --- apps/emqx_bridge/i18n/emqx_bridge_api.conf | 16 ++++ apps/emqx_bridge/src/emqx_bridge_api.erl | 76 ++++++++++++++++++- apps/emqx_bridge/src/emqx_bridge_resource.erl | 6 +- .../test/emqx_bridge_api_SUITE.erl | 46 +++++++++++ 4 files changed, 140 insertions(+), 4 deletions(-) diff --git a/apps/emqx_bridge/i18n/emqx_bridge_api.conf b/apps/emqx_bridge/i18n/emqx_bridge_api.conf index e8bb2403a..28f3db324 100644 --- a/apps/emqx_bridge/i18n/emqx_bridge_api.conf +++ b/apps/emqx_bridge/i18n/emqx_bridge_api.conf @@ -134,4 +134,20 @@ NOTE:不允许在单节点上启用/禁用 Bridge""" } } + desc_api9 { + desc { + en: """ +Test creating a new bridge by given ID
+The ID must be of format '{type}:{name}' +""" + zh: """ +通过给定的 ID 测试创建一个新的桥接。
+ID 的格式必须为 ’{type}:{name}” +""" + } + label: { + en: "Test Bridge Creation" + zh: "测试桥接创建" + } + } } diff --git a/apps/emqx_bridge/src/emqx_bridge_api.erl b/apps/emqx_bridge/src/emqx_bridge_api.erl index 0d0d2e9ad..aa11ebc5d 100644 --- a/apps/emqx_bridge/src/emqx_bridge_api.erl +++ b/apps/emqx_bridge/src/emqx_bridge_api.erl @@ -38,7 +38,8 @@ '/bridges/:id'/2, '/bridges/:id/operation/:operation'/2, '/nodes/:node/bridges/:id/operation/:operation'/2, - '/bridges/:id/reset_metrics'/2 + '/bridges/:id/reset_metrics'/2, + '/bridges_probe'/2 ]). -export([lookup_from_local_node/2]). @@ -68,7 +69,8 @@ paths() -> "/bridges/:id", "/bridges/:id/operation/:operation", "/nodes/:node/bridges/:id/operation/:operation", - "/bridges/:id/reset_metrics" + "/bridges/:id/reset_metrics", + "/bridges_probe" ]. error_schema(Code, Message) when is_atom(Code) -> @@ -384,6 +386,23 @@ schema("/nodes/:node/bridges/:id/operation/:operation") -> 503 => error_schema('SERVICE_UNAVAILABLE', "Service unavailable") } } + }; +schema("/bridges_probe") -> + #{ + 'operationId' => '/bridges_probe', + post => #{ + tags => [<<"bridges">>], + desc => ?DESC("desc_api9"), + summary => <<"Test creating bridge">>, + 'requestBody' => emqx_dashboard_swagger:schema_with_examples( + emqx_bridge_schema:post_request(), + bridge_info_examples(post) + ), + responses => #{ + 204 => <<"Test bridge OK">>, + 400 => error_schema(['TEST_FAILED'], "bridge test failed") + } + } }. '/bridges'(post, #{body := #{<<"type">> := BridgeType, <<"name">> := BridgeName} = Conf0}) -> @@ -462,6 +481,59 @@ schema("/nodes/:node/bridges/:id/operation/:operation") -> end ). +'/bridges_probe'(post, Request) -> + RequestMeta = #{module => ?MODULE, method => post, path => "/bridges_probe"}, + case emqx_dashboard_swagger:filter_check_request_and_translate_body(Request, RequestMeta) of + {ok, #{body := #{<<"type">> := ConnType} = Params}} -> + case do_probe(ConnType, maps:remove(<<"type">>, Params)) of + ok -> + {204}; + {error, Error} -> + {400, error_msg('TEST_FAILED', Error)} + end; + BadRequest -> + BadRequest + end. + +do_probe(ConnType, Params) -> + case test_connection(host_and_port(ConnType, Params)) of + ok -> + emqx_bridge_resource:create_dry_run(ConnType, Params); + Error -> + Error + end. + +host_and_port(mqtt, #{<<"server">> := Server}) -> + Server; +host_and_port(webhook, #{<<"url">> := Url}) -> + {BaseUrl, _Path} = parse_url(Url), + {ok, #{host := Host, port := Port}} = emqx_http_lib:uri_parse(BaseUrl), + {Host, Port}; +host_and_port(_Unknown, _) -> + undefined. + +test_connection(undefined) -> + %% be friendly, it might fail later on with a 'timeout' error. + ok; +test_connection({Host, Port}) -> + case gen_tcp:connect(Host, Port, []) of + {ok, TestSocket} -> gen_tcp:close(TestSocket); + Error -> Error + end. + +parse_url(Url) -> + case string:split(Url, "//", leading) of + [Scheme, UrlRem] -> + case string:split(UrlRem, "/", leading) of + [HostPort, Path] -> + {iolist_to_binary([Scheme, "//", HostPort]), Path}; + [HostPort] -> + {iolist_to_binary([Scheme, "//", HostPort]), <<>>} + end; + [Url] -> + error({invalid_url, Url}) + end. + lookup_from_all_nodes(BridgeType, BridgeName, SuccCode) -> Nodes = mria_mnesia:running_nodes(), case is_ok(emqx_bridge_proto_v1:lookup_from_all_nodes(Nodes, BridgeType, BridgeName)) of diff --git a/apps/emqx_bridge/src/emqx_bridge_resource.erl b/apps/emqx_bridge/src/emqx_bridge_resource.erl index d1ce260c9..dce7b9f1a 100644 --- a/apps/emqx_bridge/src/emqx_bridge_resource.erl +++ b/apps/emqx_bridge/src/emqx_bridge_resource.erl @@ -213,14 +213,16 @@ recreate(Type, Name, Conf, Opts) -> Opts ). -create_dry_run(Type, Conf) -> +create_dry_run(Type, Conf0) -> TmpPath = iolist_to_binary(["bridges-create-dry-run:", emqx_misc:gen_id(8)]), + Conf = emqx_map_lib:safe_atom_key_map(Conf0), case emqx_connector_ssl:convert_certs(TmpPath, Conf) of {error, Reason} -> {error, Reason}; {ok, ConfNew} -> + ParseConf = parse_confs(bin(Type), TmpPath, ConfNew), Res = emqx_resource:create_dry_run_local( - bridge_to_resource_type(Type), ConfNew + bridge_to_resource_type(Type), ParseConf ), _ = maybe_clear_certs(TmpPath, ConfNew), Res diff --git a/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl b/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl index e533c78b0..a32019e41 100644 --- a/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl +++ b/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl @@ -596,6 +596,52 @@ t_with_redact_update(_Config) -> ?assertEqual(Password, Value), ok. +-define(MQTT_BRIDGE(Server), #{ + <<"server">> => Server, + <<"username">> => <<"user1">>, + <<"password">> => <<"">>, + <<"proto_ver">> => <<"v5">>, + <<"ssl">> => #{<<"enable">> => false}, + <<"type">> => <<"mqtt">>, + <<"name">> => <<"mqtt_egress_test_bridge">> +}). + +t_bridges_probe(Config) -> + Port = ?config(port, Config), + URL = ?URL(Port, "some_path"), + + {ok, 204, <<>>} = request( + post, + uri(["bridges_probe"]), + ?HTTP_BRIDGE(URL, ?BRIDGE_TYPE, ?BRIDGE_NAME) + ), + + %% second time with same name is ok since no real bridge created + {ok, 204, <<>>} = request( + post, + uri(["bridges_probe"]), + ?HTTP_BRIDGE(URL, ?BRIDGE_TYPE, ?BRIDGE_NAME) + ), + + {ok, 400, _} = request( + post, + uri(["bridges_probe"]), + ?HTTP_BRIDGE(<<"http://203.0.113.3:1234/foo">>, ?BRIDGE_TYPE, ?BRIDGE_NAME) + ), + + {ok, 204, _} = request( + post, + uri(["bridges_probe"]), + ?MQTT_BRIDGE(<<"127.0.0.1:1883">>) + ), + + {ok, 400, _} = request( + post, + uri(["bridges_probe"]), + ?MQTT_BRIDGE(<<"127.0.0.1:2883">>) + ), + ok. + request(Method, Url, Body) -> request(<<"bridge_admin">>, Method, Url, Body). From c42c99f94f53785ae424709a99cac7e347f8b621 Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Wed, 21 Dec 2022 15:20:53 +0100 Subject: [PATCH 0060/1802] fix: set a timeout for tcp connect --- apps/emqx_bridge/src/emqx_bridge_api.erl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/emqx_bridge/src/emqx_bridge_api.erl b/apps/emqx_bridge/src/emqx_bridge_api.erl index aa11ebc5d..8696aadaa 100644 --- a/apps/emqx_bridge/src/emqx_bridge_api.erl +++ b/apps/emqx_bridge/src/emqx_bridge_api.erl @@ -512,11 +512,12 @@ host_and_port(webhook, #{<<"url">> := Url}) -> host_and_port(_Unknown, _) -> undefined. +%% [TODO] remove in EMQX-8588 when resource manager handles things more elegantly test_connection(undefined) -> %% be friendly, it might fail later on with a 'timeout' error. ok; test_connection({Host, Port}) -> - case gen_tcp:connect(Host, Port, []) of + case gen_tcp:connect(Host, Port, [], 5000) of {ok, TestSocket} -> gen_tcp:close(TestSocket); Error -> Error end. From d55404cc99d9383a6b86ada419fe5280a0ddc67c Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Wed, 21 Dec 2022 15:23:46 +0100 Subject: [PATCH 0061/1802] chore: add changelog --- changes/v5.0.13-en.md | 2 ++ changes/v5.0.13-zh.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/changes/v5.0.13-en.md b/changes/v5.0.13-en.md index 1ed3c798b..669275958 100644 --- a/changes/v5.0.13-en.md +++ b/changes/v5.0.13-en.md @@ -18,6 +18,8 @@ - Ensure the default expiration time of `banned` is large enough [#9599](https://github.com/emqx/emqx/pull/9599/). +- `/bridges_probe` API endpoint to test params for creating a new data bridge [#9585](https://github.com/emqx/emqx/pull/9585). + ## Bug fixes - Trigger `message.dropped` hook when QoS2 message is resend by client with a same packet id, or 'awaiting_rel' queue is full [#9487](https://github.com/emqx/emqx/pull/9487). diff --git a/changes/v5.0.13-zh.md b/changes/v5.0.13-zh.md index 41a1228f4..aae629a4a 100644 --- a/changes/v5.0.13-zh.md +++ b/changes/v5.0.13-zh.md @@ -18,6 +18,8 @@ - 确保黑名单的默认超期时间足够长 [#9599](https://github.com/emqx/emqx/pull/9599/)。 +- [FIXME] `/bridges_probe` API 端点用于测试创建新数据桥的参数 [#9585](https://github.com/emqx/emqx/pull/9585)。 + ## 修复 - 当 QoS2 消息被重发(使用相同 Packet ID),或当 'awaiting_rel' 队列已满时,触发消息丢弃钩子(`message.dropped`)及计数器 [#9487](https://github.com/emqx/emqx/pull/9487)。 From bd0b767ef9b2f4868043f1c9221b867264271c39 Mon Sep 17 00:00:00 2001 From: Kjell Winblad Date: Mon, 9 Jan 2023 10:34:57 +0100 Subject: [PATCH 0062/1802] docs: add Chinese version of changelog text for EMQX-8648 --- changes/v5.0.14/fix-8648.zh.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/v5.0.14/fix-8648.zh.md diff --git a/changes/v5.0.14/fix-8648.zh.md b/changes/v5.0.14/fix-8648.zh.md new file mode 100644 index 000000000..6512c5aa5 --- /dev/null +++ b/changes/v5.0.14/fix-8648.zh.md @@ -0,0 +1 @@ +修复了当通过 API 删除一个不存在的桥接时,服务器会返回操作成功的问题,现在将会返回操作失败的信息。 From 50717a556717fc20c3e57e91947421a402411159 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Mon, 9 Jan 2023 17:57:39 +0800 Subject: [PATCH 0063/1802] fix: set the topic field of API /clients/:clientid/subscribe as requried --- apps/emqx_management/src/emqx_mgmt_api_clients.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx_management/src/emqx_mgmt_api_clients.erl b/apps/emqx_management/src/emqx_mgmt_api_clients.erl index 539f38c6c..f078f4d2a 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_clients.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_clients.erl @@ -549,7 +549,7 @@ fields(keepalive) -> ]; fields(subscribe) -> [ - {topic, hoconsc:mk(binary(), #{desc => <<"Topic">>})}, + {topic, hoconsc:mk(binary(), #{required => true, desc => <<"Topic">>})}, {qos, hoconsc:mk(emqx_schema:qos(), #{default => 0, desc => <<"QoS">>})}, {nl, hoconsc:mk(integer(), #{default => 0, desc => <<"No Local">>})}, {rap, hoconsc:mk(integer(), #{default => 0, desc => <<"Retain as Published">>})}, From 569df42d8d2d777351fd819ef008cec3f67330e9 Mon Sep 17 00:00:00 2001 From: Kjell Winblad Date: Mon, 9 Jan 2023 11:40:17 +0100 Subject: [PATCH 0064/1802] docs: add Chinese translation for change-log entry for EMQX-8329 --- changes/v5.0.14/feat-8329.zh.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/v5.0.14/feat-8329.zh.md diff --git a/changes/v5.0.14/feat-8329.zh.md b/changes/v5.0.14/feat-8329.zh.md new file mode 100644 index 000000000..31e73086d --- /dev/null +++ b/changes/v5.0.14/feat-8329.zh.md @@ -0,0 +1 @@ +MongoDB 的驱动现在已经升级到 MongoDB 5.1+ 了。 From c85a988a4332b5fb1b1c0ad9684c8706901b6960 Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Mon, 9 Jan 2023 12:02:38 +0100 Subject: [PATCH 0065/1802] fix: split 'server' param into host and port for mqtt bridge --- apps/emqx_bridge/src/emqx_bridge_api.erl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/emqx_bridge/src/emqx_bridge_api.erl b/apps/emqx_bridge/src/emqx_bridge_api.erl index 8696aadaa..851089acb 100644 --- a/apps/emqx_bridge/src/emqx_bridge_api.erl +++ b/apps/emqx_bridge/src/emqx_bridge_api.erl @@ -504,7 +504,10 @@ do_probe(ConnType, Params) -> end. host_and_port(mqtt, #{<<"server">> := Server}) -> - Server; + case string:split(Server, ":") of + [Host, Port] -> {Host, list_to_integer(Port)}; + _Other -> error(invalid_server, Server) + end; host_and_port(webhook, #{<<"url">> := Url}) -> {BaseUrl, _Path} = parse_url(Url), {ok, #{host := Host, port := Port}} = emqx_http_lib:uri_parse(BaseUrl), From b80325f98808f47add468b15e1722d5893b7d507 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Mon, 2 Jan 2023 08:47:52 +0100 Subject: [PATCH 0066/1802] build: delete rebar.lock before making rebar release --- build | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build b/build index 05a3a33d3..87c719a53 100755 --- a/build +++ b/build @@ -135,6 +135,9 @@ assert_no_compile_time_only_deps() { make_rel() { ./scripts/pre-compile.sh "$PROFILE" + # make_elixir_rel always create rebar.lock + # delete it to make git clone + checkout work because we use shallow close for rebar deps + rm -f rebar.lock # compile all beams ./rebar3 as "$PROFILE" compile # generate docs (require beam compiled), generated to etc and priv dirs From e52f9d5920c1ff4f3ed6a85496081c12534ce5cf Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Sat, 31 Dec 2022 19:52:38 +0100 Subject: [PATCH 0067/1802] refactor: use union member type selector for authz sources --- apps/emqx/rebar.config | 2 +- apps/emqx/src/emqx_config.erl | 5 +- apps/emqx/test/emqx_schema_tests.erl | 2 +- apps/emqx_authz/src/emqx_authz.app.src | 2 +- apps/emqx_authz/src/emqx_authz_schema.erl | 98 +++++++++++++-- .../test/emqx_authz_schema_tests.erl | 116 ++++++++++++++++++ apps/emqx_conf/src/emqx_conf.erl | 2 +- .../src/emqx_connector_mongo.erl | 3 - .../src/emqx_connector_redis.erl | 3 - .../src/emqx_dashboard_swagger.erl | 2 +- changes/refactor-9653.en.md | 1 + changes/refactor-9653.zh.md | 1 + lib-ee/emqx_ee_bridge/rebar.config | 3 +- .../src/emqx_ee_bridge_redis.erl | 5 +- .../test/emqx_ee_bridge_mongodb_SUITE.erl | 3 + .../test/emqx_ee_bridge_redis_SUITE.erl | 9 +- mix.exs | 2 +- rebar.config | 2 +- 18 files changed, 229 insertions(+), 32 deletions(-) create mode 100644 apps/emqx_authz/test/emqx_authz_schema_tests.erl create mode 100644 changes/refactor-9653.en.md create mode 100644 changes/refactor-9653.zh.md diff --git a/apps/emqx/rebar.config b/apps/emqx/rebar.config index 8ff4fea58..8d363aeed 100644 --- a/apps/emqx/rebar.config +++ b/apps/emqx/rebar.config @@ -29,7 +29,7 @@ {esockd, {git, "https://github.com/emqx/esockd", {tag, "5.9.4"}}}, {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.13.7"}}}, {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.8.1"}}}, - {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.33.0"}}}, + {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.34.0"}}}, {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}}, {recon, {git, "https://github.com/ferd/recon", {tag, "2.5.1"}}}, {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "1.0.0"}}} diff --git a/apps/emqx/src/emqx_config.erl b/apps/emqx/src/emqx_config.erl index 49962e490..2fa39d094 100644 --- a/apps/emqx/src/emqx_config.erl +++ b/apps/emqx/src/emqx_config.erl @@ -362,8 +362,8 @@ schema_default(Schema) -> []; ?LAZY(?ARRAY(_)) -> []; - ?LAZY(?UNION(Unions)) -> - case [A || ?ARRAY(A) <- Unions] of + ?LAZY(?UNION(Members)) -> + case [A || ?ARRAY(A) <- hoconsc:union_members(Members)] of [_ | _] -> []; _ -> #{} end; @@ -402,7 +402,6 @@ merge_envs(SchemaMod, RawConf) -> required => false, format => map, apply_override_envs => true, - remove_env_meta => true, check_lazy => true }, hocon_tconf:merge_env_overrides(SchemaMod, RawConf, all, Opts). diff --git a/apps/emqx/test/emqx_schema_tests.erl b/apps/emqx/test/emqx_schema_tests.erl index e4fadb192..b249dea92 100644 --- a/apps/emqx/test/emqx_schema_tests.erl +++ b/apps/emqx/test/emqx_schema_tests.erl @@ -153,7 +153,7 @@ ssl_opts_gc_after_handshake_test_rancher_listener_test() -> #{ kind := validation_error, reason := unknown_fields, - unknown := <<"gc_after_handshake">> + unknown := "gc_after_handshake" } ]}, validate(Sc, #{<<"gc_after_handshake">> => true}) diff --git a/apps/emqx_authz/src/emqx_authz.app.src b/apps/emqx_authz/src/emqx_authz.app.src index c876fbf16..f5b9f9da6 100644 --- a/apps/emqx_authz/src/emqx_authz.app.src +++ b/apps/emqx_authz/src/emqx_authz.app.src @@ -1,7 +1,7 @@ %% -*- mode: erlang -*- {application, emqx_authz, [ {description, "An OTP application"}, - {vsn, "0.1.10"}, + {vsn, "0.1.11"}, {registered, []}, {mod, {emqx_authz_app, []}}, {applications, [ diff --git a/apps/emqx_authz/src/emqx_authz_schema.erl b/apps/emqx_authz/src/emqx_authz_schema.erl index d03747b84..a684ae6ba 100644 --- a/apps/emqx_authz/src/emqx_authz_schema.erl +++ b/apps/emqx_authz/src/emqx_authz_schema.erl @@ -47,14 +47,8 @@ %% Hocon Schema %%-------------------------------------------------------------------- -namespace() -> authz. - -%% @doc authorization schema is not exported -%% but directly used by emqx_schema -roots() -> []. - -fields("authorization") -> - Types = [ +type_names() -> + [ file, http_get, http_post, @@ -67,12 +61,26 @@ fields("authorization") -> redis_single, redis_sentinel, redis_cluster - ], - Unions = [?R_REF(Type) || Type <- Types], + ]. + +namespace() -> authz. + +%% @doc authorization schema is not exported +%% but directly used by emqx_schema +roots() -> []. + +fields("authorization") -> + Types = [?R_REF(Type) || Type <- type_names()], + UnionMemberSelector = + fun + (all_union_members) -> Types; + %% must return list + ({value, Value}) -> [select_union_member(Value)] + end, [ {sources, ?HOCON( - ?ARRAY(?UNION(Unions)), + ?ARRAY(?UNION(UnionMemberSelector)), #{ default => [], desc => ?DESC(sources) @@ -408,9 +416,75 @@ common_rate_field() -> ]. method(Method) -> - ?HOCON(Method, #{default => Method, required => true, desc => ?DESC(method)}). + ?HOCON(Method, #{required => true, desc => ?DESC(method)}). array(Ref) -> array(Ref, Ref). array(Ref, DescId) -> ?HOCON(?ARRAY(?R_REF(Ref)), #{desc => ?DESC(DescId)}). + +select_union_member(#{<<"type">> := <<"mongodb">>} = Value) -> + MongoType = maps:get(<<"mongo_type">>, Value, undefined), + case MongoType of + <<"single">> -> + ?R_REF(mongo_single); + <<"rs">> -> + ?R_REF(mongo_rs); + <<"sharded">> -> + ?R_REF(mongo_sharded); + Else -> + throw(#{ + reason => "unknown_mongo_type", + expected => "single | rs | sharded", + got => Else + }) + end; +select_union_member(#{<<"type">> := <<"redis">>} = Value) -> + RedisType = maps:get(<<"redis_type">>, Value, undefined), + case RedisType of + <<"single">> -> + ?R_REF(redis_single); + <<"cluster">> -> + ?R_REF(redis_cluster); + <<"sentinel">> -> + ?R_REF(redis_sentinel); + Else -> + throw(#{ + reason => "unknown_redis_type", + expected => "single | cluster | sentinel", + got => Else + }) + end; +select_union_member(#{<<"type">> := <<"http">>} = Value) -> + RedisType = maps:get(<<"method">>, Value, undefined), + case RedisType of + <<"get">> -> + ?R_REF(http_get); + <<"post">> -> + ?R_REF(http_post); + Else -> + throw(#{ + reason => "unknown_http_method", + expected => "get | post", + got => Else + }) + end; +select_union_member(#{<<"type">> := <<"built_in_database">>}) -> + ?R_REF(mnesia); +select_union_member(#{<<"type">> := Type}) -> + select_union_member_loop(Type, type_names()); +select_union_member(_) -> + throw("missing_type_field"). + +select_union_member_loop(TypeValue, []) -> + throw(#{ + reason => "unknown_authz_type", + got => TypeValue + }); +select_union_member_loop(TypeValue, [Type | Types]) -> + case TypeValue =:= atom_to_binary(Type) of + true -> + ?R_REF(Type); + false -> + select_union_member_loop(TypeValue, Types) + end. diff --git a/apps/emqx_authz/test/emqx_authz_schema_tests.erl b/apps/emqx_authz/test/emqx_authz_schema_tests.erl new file mode 100644 index 000000000..f7b2e3c10 --- /dev/null +++ b/apps/emqx_authz/test/emqx_authz_schema_tests.erl @@ -0,0 +1,116 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2023-2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_authz_schema_tests). + +-include_lib("eunit/include/eunit.hrl"). + +bad_authz_type_test() -> + Txt = "[{type: foobar}]", + ?assertThrow( + [ + #{ + reason := "unknown_authz_type", + got := <<"foobar">> + } + ], + check(Txt) + ). + +bad_mongodb_type_test() -> + Txt = "[{type: mongodb, mongo_type: foobar}]", + ?assertThrow( + [ + #{ + reason := "unknown_mongo_type", + got := <<"foobar">> + } + ], + check(Txt) + ). + +missing_mongodb_type_test() -> + Txt = "[{type: mongodb}]", + ?assertThrow( + [ + #{ + reason := "unknown_mongo_type", + got := undefined + } + ], + check(Txt) + ). + +unknown_redis_type_test() -> + Txt = "[{type: redis, redis_type: foobar}]", + ?assertThrow( + [ + #{ + reason := "unknown_redis_type", + got := <<"foobar">> + } + ], + check(Txt) + ). + +missing_redis_type_test() -> + Txt = "[{type: redis}]", + ?assertThrow( + [ + #{ + reason := "unknown_redis_type", + got := undefined + } + ], + check(Txt) + ). + +unknown_http_method_test() -> + Txt = "[{type: http, method: getx}]", + ?assertThrow( + [ + #{ + reason := "unknown_http_method", + got := <<"getx">> + } + ], + check(Txt) + ). + +missing_http_method_test() -> + Txt = "[{type: http, methodx: get}]", + ?assertThrow( + [ + #{ + reason := "unknown_http_method", + got := undefined + } + ], + check(Txt) + ). + +check(Txt0) -> + Txt = ["sources: ", Txt0], + {ok, RawConf} = hocon:binary(Txt), + try + hocon_tconf:check_plain(schema(), RawConf, #{}) + catch + throw:{_Schema, Errors} -> + throw(Errors) + end. + +schema() -> + #{roots => emqx_authz_schema:fields("authorization")}. diff --git a/apps/emqx_conf/src/emqx_conf.erl b/apps/emqx_conf/src/emqx_conf.erl index 6fd9ac009..8b471a137 100644 --- a/apps/emqx_conf/src/emqx_conf.erl +++ b/apps/emqx_conf/src/emqx_conf.erl @@ -316,7 +316,7 @@ hocon_schema_to_spec(?UNION(Types), LocalModule) -> {[Schema | Acc], SubRefs ++ RefsAcc} end, {[], []}, - Types + hoconsc:union_members(Types) ), {#{<<"oneOf">> => OneOf}, Refs}; hocon_schema_to_spec(Atom, _LocalModule) when is_atom(Atom) -> diff --git a/apps/emqx_connector/src/emqx_connector_mongo.erl b/apps/emqx_connector/src/emqx_connector_mongo.erl index f9e703d20..a735d8c31 100644 --- a/apps/emqx_connector/src/emqx_connector_mongo.erl +++ b/apps/emqx_connector/src/emqx_connector_mongo.erl @@ -67,7 +67,6 @@ fields(single) -> [ {mongo_type, #{ type => single, - default => single, required => true, desc => ?DESC("single_mongo_type") }}, @@ -78,7 +77,6 @@ fields(rs) -> [ {mongo_type, #{ type => rs, - default => rs, required => true, desc => ?DESC("rs_mongo_type") }}, @@ -91,7 +89,6 @@ fields(sharded) -> [ {mongo_type, #{ type => sharded, - default => sharded, required => true, desc => ?DESC("sharded_mongo_type") }}, diff --git a/apps/emqx_connector/src/emqx_connector_redis.erl b/apps/emqx_connector/src/emqx_connector_redis.erl index 350d49f01..11d71f1df 100644 --- a/apps/emqx_connector/src/emqx_connector_redis.erl +++ b/apps/emqx_connector/src/emqx_connector_redis.erl @@ -63,7 +63,6 @@ fields(single) -> {server, server()}, {redis_type, #{ type => single, - default => single, required => true, desc => ?DESC("single") }} @@ -75,7 +74,6 @@ fields(cluster) -> {servers, servers()}, {redis_type, #{ type => cluster, - default => cluster, required => true, desc => ?DESC("cluster") }} @@ -87,7 +85,6 @@ fields(sentinel) -> {servers, servers()}, {redis_type, #{ type => sentinel, - default => sentinel, required => true, desc => ?DESC("sentinel") }}, diff --git a/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl b/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl index 4b7a672bd..85b928dda 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl @@ -623,7 +623,7 @@ hocon_schema_to_spec(?UNION(Types), LocalModule) -> {[Schema | Acc], SubRefs ++ RefsAcc} end, {[], []}, - Types + hoconsc:union_members(Types) ), {#{<<"oneOf">> => OneOf}, Refs}; hocon_schema_to_spec(Atom, _LocalModule) when is_atom(Atom) -> diff --git a/changes/refactor-9653.en.md b/changes/refactor-9653.en.md new file mode 100644 index 000000000..2807f81d5 --- /dev/null +++ b/changes/refactor-9653.en.md @@ -0,0 +1 @@ +Make authorization config validation error message more readable. diff --git a/changes/refactor-9653.zh.md b/changes/refactor-9653.zh.md new file mode 100644 index 000000000..755fd1683 --- /dev/null +++ b/changes/refactor-9653.zh.md @@ -0,0 +1 @@ +改进授权配置检查错误日志的可读性。 diff --git a/lib-ee/emqx_ee_bridge/rebar.config b/lib-ee/emqx_ee_bridge/rebar.config index 2bd4036e0..6ca554c72 100644 --- a/lib-ee/emqx_ee_bridge/rebar.config +++ b/lib-ee/emqx_ee_bridge/rebar.config @@ -1,6 +1,5 @@ {erl_opts, [debug_info]}. -{deps, [ {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.33.0"}}} - , {wolff, {git, "https://github.com/kafka4beam/wolff.git", {tag, "1.7.4"}}} +{deps, [ {wolff, {git, "https://github.com/kafka4beam/wolff.git", {tag, "1.7.4"}}} , {kafka_protocol, {git, "https://github.com/kafka4beam/kafka_protocol.git", {tag, "4.1.2"}}} , {brod_gssapi, {git, "https://github.com/kafka4beam/brod_gssapi.git", {tag, "v0.1.0-rc1"}}} , {brod, {git, "https://github.com/kafka4beam/brod.git", {tag, "3.16.7"}}} diff --git a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_redis.erl b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_redis.erl index 727a6df4b..a0c6ba834 100644 --- a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_redis.erl +++ b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_redis.erl @@ -50,19 +50,22 @@ values(Protocol, get) -> values("single", post) -> SpecificOpts = #{ server => <<"127.0.0.1:6379">>, + redis_type => single, database => 1 }, values(common, "single", SpecificOpts); values("sentinel", post) -> SpecificOpts = #{ servers => [<<"127.0.0.1:26379">>], + redis_type => sentinel, sentinel => <<"mymaster">>, database => 1 }, values(common, "sentinel", SpecificOpts); values("cluster", post) -> SpecificOpts = #{ - servers => [<<"127.0.0.1:6379">>] + servers => [<<"127.0.0.1:6379">>], + redis_type => cluster }, values(common, "cluster", SpecificOpts); values(Protocol, put) -> diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_mongodb_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_mongodb_SUITE.erl index fb8f1fcc3..05c513eb1 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_mongodb_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_mongodb_SUITE.erl @@ -151,6 +151,7 @@ mongo_config(MongoHost, MongoPort0, rs = Type) -> " servers = [~p]\n" " w_mode = safe\n" " database = mqtt\n" + " mongo_type = rs\n" "}", [Name, Servers] ), @@ -167,6 +168,7 @@ mongo_config(MongoHost, MongoPort0, sharded = Type) -> " servers = [~p]\n" " w_mode = safe\n" " database = mqtt\n" + " mongo_type = sharded\n" "}", [Name, Servers] ), @@ -183,6 +185,7 @@ mongo_config(MongoHost, MongoPort0, single = Type) -> " server = ~p\n" " w_mode = safe\n" " database = mqtt\n" + " mongo_type = single\n" "}", [Name, Server] ), diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_redis_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_redis_SUITE.erl index 1f4b52ddc..fb5f688a6 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_redis_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_redis_SUITE.erl @@ -17,7 +17,8 @@ %%------------------------------------------------------------------------------ -define(REDIS_TOXYPROXY_CONNECT_CONFIG, #{ - <<"server">> => <<"toxiproxy:6379">> + <<"server">> => <<"toxiproxy:6379">>, + <<"redis_type">> => <<"single">> }). -define(COMMON_REDIS_OPTS, #{ @@ -425,19 +426,23 @@ redis_connect_configs() -> #{ redis_single => #{ tcp => #{ + <<"redis_type">> => <<"single">>, <<"server">> => <<"redis:6379">> }, tls => #{ + <<"redis_type">> => <<"single">>, <<"server">> => <<"redis-tls:6380">>, <<"ssl">> => redis_connect_ssl_opts(redis_single) } }, redis_sentinel => #{ tcp => #{ + <<"redis_type">> => <<"sentinel">>, <<"servers">> => <<"redis-sentinel:26379">>, <<"sentinel">> => <<"mymaster">> }, tls => #{ + <<"redis_type">> => <<"sentinel">>, <<"servers">> => <<"redis-sentinel-tls:26380">>, <<"sentinel">> => <<"mymaster">>, <<"ssl">> => redis_connect_ssl_opts(redis_sentinel) @@ -445,9 +450,11 @@ redis_connect_configs() -> }, redis_cluster => #{ tcp => #{ + <<"redis_type">> => <<"cluster">>, <<"servers">> => <<"redis-cluster:7000,redis-cluster:7001,redis-cluster:7002">> }, tls => #{ + <<"redis_type">> => <<"cluster">>, <<"servers">> => <<"redis-cluster-tls:8000,redis-cluster-tls:8001,redis-cluster-tls:8002">>, <<"ssl">> => redis_connect_ssl_opts(redis_cluster) diff --git a/mix.exs b/mix.exs index 265448d72..b7fc7dac8 100644 --- a/mix.exs +++ b/mix.exs @@ -68,7 +68,7 @@ defmodule EMQXUmbrella.MixProject do # in conflict by emqtt and hocon {:getopt, "1.0.2", override: true}, {:snabbkaffe, github: "kafka4beam/snabbkaffe", tag: "1.0.0", override: true}, - {:hocon, github: "emqx/hocon", tag: "0.33.0", override: true}, + {:hocon, github: "emqx/hocon", tag: "0.34.0", override: true}, {:emqx_http_lib, github: "emqx/emqx_http_lib", tag: "0.5.1", override: true}, {:esasl, github: "emqx/esasl", tag: "0.2.0"}, {:jose, github: "potatosalad/erlang-jose", tag: "1.11.2"}, diff --git a/rebar.config b/rebar.config index 4476a128c..878c911e5 100644 --- a/rebar.config +++ b/rebar.config @@ -68,7 +68,7 @@ , {system_monitor, {git, "https://github.com/ieQu1/system_monitor", {tag, "3.0.3"}}} , {getopt, "1.0.2"} , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "1.0.0"}}} - , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.33.0"}}} + , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.34.0"}}} , {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.5.1"}}} , {esasl, {git, "https://github.com/emqx/esasl", {tag, "0.2.0"}}} , {jose, {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.11.2"}}} From ffb09f0c4dca410e074e805ef6dfa905ced4dbd2 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Tue, 3 Jan 2023 10:36:39 +0100 Subject: [PATCH 0068/1802] test: rename a test option name to avoid clashing with prod config name --- .../test/emqx_ee_bridge_redis_SUITE.erl | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_redis_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_redis_SUITE.erl index fb5f688a6..05f066d1a 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_redis_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_redis_SUITE.erl @@ -32,7 +32,7 @@ -define(PROXY_HOST, "toxiproxy"). -define(PROXY_PORT, "8474"). -all() -> [{group, redis_types}, {group, rest}]. +all() -> [{group, transport_types}, {group, rest}]. groups() -> ResourceSpecificTCs = [t_create_delete_bridge], @@ -48,7 +48,7 @@ groups() -> ], [ {rest, TCs}, - {redis_types, [ + {transport_types, [ {group, tcp}, {group, tls} ]}, @@ -64,7 +64,7 @@ groups() -> init_per_group(Group, Config) when Group =:= redis_single; Group =:= redis_sentinel; Group =:= redis_cluster -> - [{redis_type, Group} | Config]; + [{transport_type, Group} | Config]; init_per_group(Group, Config) when Group =:= tcp; Group =:= tls -> @@ -117,7 +117,7 @@ end_per_suite(_Config) -> init_per_testcase(_Testcase, Config) -> ok = delete_all_bridges(), - case ?config(redis_type, Config) of + case ?config(transport_type, Config) of undefined -> Config; RedisType -> @@ -140,7 +140,7 @@ end_per_testcase(_Testcase, Config) -> t_create_delete_bridge(Config) -> Name = <<"mybridge">>, - Type = ?config(redis_type, Config), + Type = ?config(transport_type, Config), BridgeConfig = ?config(bridge_config, Config), IsBatch = ?config(is_batch, Config), ?assertMatch( @@ -426,37 +426,37 @@ redis_connect_configs() -> #{ redis_single => #{ tcp => #{ - <<"redis_type">> => <<"single">>, - <<"server">> => <<"redis:6379">> + <<"server">> => <<"redis:6379">>, + <<"redis_type">> => <<"single">> }, tls => #{ - <<"redis_type">> => <<"single">>, <<"server">> => <<"redis-tls:6380">>, - <<"ssl">> => redis_connect_ssl_opts(redis_single) + <<"ssl">> => redis_connect_ssl_opts(redis_single), + <<"redis_type">> => <<"single">> } }, redis_sentinel => #{ tcp => #{ - <<"redis_type">> => <<"sentinel">>, <<"servers">> => <<"redis-sentinel:26379">>, + <<"redis_type">> => <<"sentinel">>, <<"sentinel">> => <<"mymaster">> }, tls => #{ - <<"redis_type">> => <<"sentinel">>, <<"servers">> => <<"redis-sentinel-tls:26380">>, + <<"redis_type">> => <<"sentinel">>, <<"sentinel">> => <<"mymaster">>, <<"ssl">> => redis_connect_ssl_opts(redis_sentinel) } }, redis_cluster => #{ tcp => #{ - <<"redis_type">> => <<"cluster">>, - <<"servers">> => <<"redis-cluster:7000,redis-cluster:7001,redis-cluster:7002">> + <<"servers">> => <<"redis-cluster:7000,redis-cluster:7001,redis-cluster:7002">>, + <<"redis_type">> => <<"cluster">> }, tls => #{ - <<"redis_type">> => <<"cluster">>, <<"servers">> => <<"redis-cluster-tls:8000,redis-cluster-tls:8001,redis-cluster-tls:8002">>, + <<"redis_type">> => <<"cluster">>, <<"ssl">> => redis_connect_ssl_opts(redis_cluster) } } From 0697c692ed2992920ed9b9988aa85d955af8defb Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Sun, 8 Jan 2023 09:51:26 +0100 Subject: [PATCH 0069/1802] refactor: mongo_type and redis_type are not mandatory the connector schemas are shared between authn, auth and bridges, the difference is that authn and authz configs are unions like: [ {mongo_type = rs, ... } {another backend config} ] however the brdige types are maps like, for example: mongodb_rs.$name { mongo_type = rs ... } in which case, the mongo_type is not required. in order to keep the schema static as much as possible, this field is chanegd to 'required => false' with a default value. However, for authn and authz, the union selector will still raise exception if the there is no type provided. --- .../i18n/emqx_connector_mongo.conf | 18 +++++++++--------- .../i18n/emqx_connector_redis.conf | 12 ++++++------ .../src/emqx_connector_mongo.erl | 6 +++--- .../src/emqx_connector_redis.erl | 9 ++++++--- .../test/emqx_ee_bridge_mongodb_SUITE.erl | 3 --- 5 files changed, 24 insertions(+), 24 deletions(-) diff --git a/apps/emqx_connector/i18n/emqx_connector_mongo.conf b/apps/emqx_connector/i18n/emqx_connector_mongo.conf index a598c084d..619a8e3b4 100644 --- a/apps/emqx_connector/i18n/emqx_connector_mongo.conf +++ b/apps/emqx_connector/i18n/emqx_connector_mongo.conf @@ -2,34 +2,34 @@ emqx_connector_mongo { single_mongo_type { desc { - en: "Standalone instance." - zh: "Standalone模式。" + en: "Standalone instance. Must be set to 'single' when MongoDB server is running in standalone mode." + zh: "Standalone 模式。当 MongoDB 服务运行在 standalone 模式下,该配置必须设置为 'single'。 " } label: { en: "Standalone instance" - zh: "Standalone模式" + zh: "Standalone 模式" } } rs_mongo_type { desc { - en: "Replica set." - zh: "Replica set模式。" + en: "Replica set. Must be set to 'rs' when MongoDB server is running in 'replica set' mode." + zh: "Replica set模式。当 MongoDB 服务运行在 replica-set 模式下,该配置必须设置为 'rs'。" } label: { en: "Replica set" - zh: "Replica set模式" + zh: "Replica set 模式" } } sharded_mongo_type { desc { - en: "Sharded cluster." - zh: "Sharded cluster模式。" + en: "Sharded cluster. Must be set to 'sharded' when MongoDB server is running in 'sharded' mode." + zh: "Sharded cluster模式。当 MongoDB 服务运行在 sharded 模式下,该配置必须设置为 'sharded'。" } label: { en: "Sharded cluster" - zh: "Sharded cluster模式" + zh: "Sharded cluster 模式" } } diff --git a/apps/emqx_connector/i18n/emqx_connector_redis.conf b/apps/emqx_connector/i18n/emqx_connector_redis.conf index 228d0805a..f42f38f30 100644 --- a/apps/emqx_connector/i18n/emqx_connector_redis.conf +++ b/apps/emqx_connector/i18n/emqx_connector_redis.conf @@ -2,8 +2,8 @@ emqx_connector_redis { single { desc { - en: "Single mode" - zh: "单机模式。" + en: "Single mode. Must be set to 'single' when Redis server is running in single mode." + zh: "单机模式。当 Redis 服务运行在单机模式下,该配置必须设置为 'single'。" } label: { en: "Single Mode" @@ -13,8 +13,8 @@ emqx_connector_redis { cluster { desc { - en: "Cluster mode" - zh: "集群模式。" + en: "Cluster mode. Must be set to 'cluster' when Redis server is running in clustered mode." + zh: "集群模式。当 Redis 服务运行在集群模式下,该配置必须设置为 'cluster'。" } label: { en: "Cluster Mode" @@ -24,8 +24,8 @@ emqx_connector_redis { sentinel { desc { - en: "Sentinel mode" - zh: "哨兵模式。" + en: "Sentinel mode. Must be set to 'sentinel' when Redis server is running in sentinel mode." + zh: "哨兵模式。当 Redis 服务运行在哨兵模式下,该配置必须设置为 'sentinel'。" } label: { en: "Sentinel Mode" diff --git a/apps/emqx_connector/src/emqx_connector_mongo.erl b/apps/emqx_connector/src/emqx_connector_mongo.erl index a735d8c31..0bcc39208 100644 --- a/apps/emqx_connector/src/emqx_connector_mongo.erl +++ b/apps/emqx_connector/src/emqx_connector_mongo.erl @@ -67,7 +67,7 @@ fields(single) -> [ {mongo_type, #{ type => single, - required => true, + default => single, desc => ?DESC("single_mongo_type") }}, {server, server()}, @@ -77,7 +77,7 @@ fields(rs) -> [ {mongo_type, #{ type => rs, - required => true, + default => rs, desc => ?DESC("rs_mongo_type") }}, {servers, servers()}, @@ -89,7 +89,7 @@ fields(sharded) -> [ {mongo_type, #{ type => sharded, - required => true, + default => sharded, desc => ?DESC("sharded_mongo_type") }}, {servers, servers()}, diff --git a/apps/emqx_connector/src/emqx_connector_redis.erl b/apps/emqx_connector/src/emqx_connector_redis.erl index 11d71f1df..726af2d9b 100644 --- a/apps/emqx_connector/src/emqx_connector_redis.erl +++ b/apps/emqx_connector/src/emqx_connector_redis.erl @@ -63,7 +63,8 @@ fields(single) -> {server, server()}, {redis_type, #{ type => single, - required => true, + default => single, + required => false, desc => ?DESC("single") }} ] ++ @@ -74,7 +75,8 @@ fields(cluster) -> {servers, servers()}, {redis_type, #{ type => cluster, - required => true, + default => cluster, + required => false, desc => ?DESC("cluster") }} ] ++ @@ -85,7 +87,8 @@ fields(sentinel) -> {servers, servers()}, {redis_type, #{ type => sentinel, - required => true, + default => sentinel, + required => false, desc => ?DESC("sentinel") }}, {sentinel, #{ diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_mongodb_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_mongodb_SUITE.erl index 05c513eb1..fb8f1fcc3 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_mongodb_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_mongodb_SUITE.erl @@ -151,7 +151,6 @@ mongo_config(MongoHost, MongoPort0, rs = Type) -> " servers = [~p]\n" " w_mode = safe\n" " database = mqtt\n" - " mongo_type = rs\n" "}", [Name, Servers] ), @@ -168,7 +167,6 @@ mongo_config(MongoHost, MongoPort0, sharded = Type) -> " servers = [~p]\n" " w_mode = safe\n" " database = mqtt\n" - " mongo_type = sharded\n" "}", [Name, Servers] ), @@ -185,7 +183,6 @@ mongo_config(MongoHost, MongoPort0, single = Type) -> " server = ~p\n" " w_mode = safe\n" " database = mqtt\n" - " mongo_type = single\n" "}", [Name, Server] ), From c3635f537a7dbc7da45248646caa45f7ca4206af Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Mon, 2 Jan 2023 20:37:38 +0100 Subject: [PATCH 0070/1802] ci: wait for redis in emqx_ee_bridge_redis_SUITE --- .../test/emqx_connector_redis_SUITE.erl | 6 +++--- .../test/emqx_ee_bridge_redis_SUITE.erl | 14 ++++++++++---- scripts/ct/run.sh | 1 - 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/apps/emqx_connector/test/emqx_connector_redis_SUITE.erl b/apps/emqx_connector/test/emqx_connector_redis_SUITE.erl index 060340fed..a1d8fe9d5 100644 --- a/apps/emqx_connector/test/emqx_connector_redis_SUITE.erl +++ b/apps/emqx_connector/test/emqx_connector_redis_SUITE.erl @@ -75,9 +75,9 @@ wait_for_redis(Checks) -> wait_for_redis(Checks - 1) end. -% %%------------------------------------------------------------------------------ -% %% Testcases -% %%------------------------------------------------------------------------------ +%%------------------------------------------------------------------------------ +%% Testcases +%%------------------------------------------------------------------------------ t_single_lifecycle(_Config) -> perform_lifecycle_check( diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_redis_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_redis_SUITE.erl index 05f066d1a..c3529ddeb 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_redis_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_redis_SUITE.erl @@ -80,6 +80,12 @@ end_per_group(_Group, _Config) -> ok. init_per_suite(Config) -> + wait_for_ci_redis(redis_checks(), Config). + +wait_for_ci_redis(0, _Config) -> + throw(no_redis); +wait_for_ci_redis(Checks, Config) -> + timer:sleep(1000), TestHosts = all_test_hosts(), case emqx_common_test_helpers:is_all_tcp_servers_available(TestHosts) of true -> @@ -97,15 +103,15 @@ init_per_suite(Config) -> | Config ]; false -> - assert_ci() + wait_for_ci_redis(Checks - 1, Config) end. -assert_ci() -> +redis_checks() -> case os:getenv("IS_CI") of "yes" -> - throw(no_redis); + 10; _ -> - {skip, no_redis} + 1 end. end_per_suite(_Config) -> diff --git a/scripts/ct/run.sh b/scripts/ct/run.sh index 7fb9e00fd..3a7b40317 100755 --- a/scripts/ct/run.sh +++ b/scripts/ct/run.sh @@ -11,7 +11,6 @@ help() { echo echo "-h|--help: To display this usage info" echo "--app lib_dir/app_name: For which app to run start docker-compose, and run common tests" - echo "--suites SUITE1,SUITE2: Comma separated SUITE names to run. e.g. apps/emqx/test/emqx_SUITE.erl" echo "--console: Start EMQX in console mode but do not run test cases" echo "--attach: Attach to the Erlang docker container without running any test case" echo "--stop: Stop running containers for the given app" From e08d6dbc9b582f0ce9d366c6c6c46e89a1550815 Mon Sep 17 00:00:00 2001 From: firest Date: Mon, 9 Jan 2023 14:38:24 +0800 Subject: [PATCH 0071/1802] feat(bridges): add timescale && matrix bridges --- lib-ee/emqx_ee_bridge/src/emqx_ee_bridge.erl | 39 +++++++++++------ .../src/emqx_ee_bridge_matrix.erl | 42 +++++++++++++++++++ .../src/emqx_ee_bridge_pgsql.erl | 29 +++++++------ .../src/emqx_ee_bridge_timescale.erl | 42 +++++++++++++++++++ 4 files changed, 128 insertions(+), 24 deletions(-) create mode 100644 lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_matrix.erl create mode 100644 lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_timescale.erl diff --git a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge.erl b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge.erl index d0099db1c..a709601bb 100644 --- a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge.erl +++ b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge.erl @@ -26,7 +26,9 @@ api_schemas(Method) -> ref(emqx_ee_bridge_influxdb, Method ++ "_api_v2"), ref(emqx_ee_bridge_redis, Method ++ "_single"), ref(emqx_ee_bridge_redis, Method ++ "_sentinel"), - ref(emqx_ee_bridge_redis, Method ++ "_cluster") + ref(emqx_ee_bridge_redis, Method ++ "_cluster"), + ref(emqx_ee_bridge_timescale, Method), + ref(emqx_ee_bridge_matrix, Method) ]. schema_modules() -> @@ -38,7 +40,9 @@ schema_modules() -> emqx_ee_bridge_mongodb, emqx_ee_bridge_mysql, emqx_ee_bridge_redis, - emqx_ee_bridge_pgsql + emqx_ee_bridge_pgsql, + emqx_ee_bridge_timescale, + emqx_ee_bridge_matrix ]. examples(Method) -> @@ -66,7 +70,9 @@ resource_type(influxdb_api_v2) -> emqx_ee_connector_influxdb; resource_type(redis_single) -> emqx_ee_connector_redis; resource_type(redis_sentinel) -> emqx_ee_connector_redis; resource_type(redis_cluster) -> emqx_ee_connector_redis; -resource_type(pgsql) -> emqx_connector_pgsql. +resource_type(pgsql) -> emqx_connector_pgsql; +resource_type(timescale) -> emqx_connector_pgsql; +resource_type(matrix) -> emqx_connector_pgsql. fields(bridges) -> [ @@ -101,16 +107,8 @@ fields(bridges) -> desc => <<"MySQL Bridge Config">>, required => false } - )}, - {pgsql, - mk( - hoconsc:map(name, ref(emqx_ee_bridge_pgsql, "config")), - #{ - desc => <<"PostgreSQL Bridge Config">>, - required => false - } )} - ] ++ mongodb_structs() ++ influxdb_structs() ++ redis_structs(). + ] ++ mongodb_structs() ++ influxdb_structs() ++ redis_structs() ++ pgsql_structs(). mongodb_structs() -> [ @@ -157,3 +155,20 @@ redis_structs() -> redis_cluster ] ]. + +pgsql_structs() -> + [ + {Type, + mk( + hoconsc:map(name, ref(emqx_ee_bridge_pgsql, "config")), + #{ + desc => <>, + required => false + } + )} + || {Type, Name} <- [ + {pgsql, <<"PostgreSQL">>}, + {timescale, <<"Timescale">>}, + {matrix, <<"Matrix">>} + ] + ]. diff --git a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_matrix.erl b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_matrix.erl new file mode 100644 index 000000000..106fac48a --- /dev/null +++ b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_matrix.erl @@ -0,0 +1,42 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +-module(emqx_ee_bridge_matrix). + +-export([ + conn_bridge_examples/1 +]). + +-export([ + namespace/0, + roots/0, + fields/1, + desc/1 +]). + +%% ------------------------------------------------------------------------------------------------- +%% api + +conn_bridge_examples(Method) -> + [ + #{ + <<"matrix">> => #{ + summary => <<"Matrix Bridge">>, + value => emqx_ee_bridge_pgsql:values(Method, matrix) + } + } + ]. + +%% ------------------------------------------------------------------------------------------------- +%% Hocon Schema Definitions +namespace() -> "bridge_matrix". + +roots() -> []. + +fields("post") -> + emqx_ee_bridge_pgsql:fields("post", matrix); +fields(Method) -> + emqx_ee_bridge_pgsql:fields(Method). + +desc(_) -> + undefined. diff --git a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_pgsql.erl b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_pgsql.erl index 1f9a005c9..dc8697e37 100644 --- a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_pgsql.erl +++ b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_pgsql.erl @@ -11,7 +11,9 @@ -import(hoconsc, [mk/2, enum/1, ref/2]). -export([ - conn_bridge_examples/1 + conn_bridge_examples/1, + values/2, + fields/2 ]). -export([ @@ -34,17 +36,17 @@ conn_bridge_examples(Method) -> #{ <<"pgsql">> => #{ summary => <<"PostgreSQL Bridge">>, - value => values(Method) + value => values(Method, pgsql) } } ]. -values(get) -> - maps:merge(values(post), ?METRICS_EXAMPLE); -values(post) -> +values(get, Type) -> + maps:merge(values(post, Type), ?METRICS_EXAMPLE); +values(post, Type) -> #{ enable => true, - type => pgsql, + type => Type, name => <<"foo">>, server => <<"127.0.0.1:5432">>, database => <<"mqtt">>, @@ -64,8 +66,8 @@ values(post) -> max_queue_bytes => ?DEFAULT_QUEUE_SIZE } }; -values(put) -> - values(post). +values(put, Type) -> + values(post, Type). %% ------------------------------------------------------------------------------------------------- %% Hocon Schema Definitions @@ -96,17 +98,20 @@ fields("config") -> } )} ] ++ - emqx_connector_mysql:fields(config) -- emqx_connector_schema_lib:prepare_statement_fields(); + emqx_connector_pgsql:fields(config) -- emqx_connector_schema_lib:prepare_statement_fields(); fields("creation_opts") -> Opts = emqx_resource_schema:fields("creation_opts"), [O || {Field, _} = O <- Opts, not is_hidden_opts(Field)]; fields("post") -> - [type_field(), name_field() | fields("config")]; + fields("post", pgsql); fields("put") -> fields("config"); fields("get") -> emqx_bridge_schema:metrics_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" -> @@ -123,8 +128,8 @@ is_hidden_opts(Field) -> async_inflight_window ]). -type_field() -> - {type, mk(enum([mysql]), #{required => true, desc => ?DESC("desc_type")})}. +type_field(Type) -> + {type, mk(enum([Type]), #{required => true, desc => ?DESC("desc_type")})}. name_field() -> {name, mk(binary(), #{required => true, desc => ?DESC("desc_name")})}. diff --git a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_timescale.erl b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_timescale.erl new file mode 100644 index 000000000..20d940462 --- /dev/null +++ b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_timescale.erl @@ -0,0 +1,42 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +-module(emqx_ee_bridge_timescale). + +-export([ + conn_bridge_examples/1 +]). + +-export([ + namespace/0, + roots/0, + fields/1, + desc/1 +]). + +%% ------------------------------------------------------------------------------------------------- +%% api + +conn_bridge_examples(Method) -> + [ + #{ + <<"timescale">> => #{ + summary => <<"Timescale Bridge">>, + value => emqx_ee_bridge_pgsql:values(Method, timescale) + } + } + ]. + +%% ------------------------------------------------------------------------------------------------- +%% Hocon Schema Definitions +namespace() -> "bridge_timescale". + +roots() -> []. + +fields("post") -> + emqx_ee_bridge_pgsql:fields("post", timescale); +fields(Method) -> + emqx_ee_bridge_pgsql:fields(Method). + +desc(_) -> + undefined. From 062f14bd6506cfa1d258db86c89dc5b595873d77 Mon Sep 17 00:00:00 2001 From: firest Date: Mon, 9 Jan 2023 15:37:01 +0800 Subject: [PATCH 0072/1802] test(bridges): add timescale && matrix test cases --- .../test/emqx_ee_bridge_pgsql_SUITE.erl | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_pgsql_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_pgsql_SUITE.erl index c5292a892..c2ff6fa8f 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_pgsql_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_pgsql_SUITE.erl @@ -45,14 +45,20 @@ groups() -> [ {tcp, [ {group, with_batch}, - {group, without_batch} + {group, without_batch}, + {group, matrix}, + {group, timescale} ]}, {tls, [ {group, with_batch}, - {group, without_batch} + {group, without_batch}, + {group, matrix}, + {group, timescale} ]}, {with_batch, TCs -- NonBatchCases}, - {without_batch, TCs} + {without_batch, TCs}, + {matrix, [t_setup_via_config_and_publish, t_setup_via_http_api_and_publish]}, + {timescale, [t_setup_via_config_and_publish, t_setup_via_http_api_and_publish]} ]. init_per_group(tcp, Config) -> @@ -83,6 +89,12 @@ init_per_group(with_batch, Config0) -> init_per_group(without_batch, Config0) -> Config = [{enable_batch, false} | Config0], common_init(Config); +init_per_group(matrix, Config0) -> + Config = [{bridge_type, <<"matrix">>}, {enable_batch, true} | Config0], + common_init(Config); +init_per_group(timescale, Config0) -> + Config = [{bridge_type, <<"timescale">>}, {enable_batch, true} | Config0], + common_init(Config); init_per_group(_Group, Config) -> Config. @@ -122,7 +134,7 @@ end_per_testcase(_Testcase, Config) -> %%------------------------------------------------------------------------------ common_init(Config0) -> - BridgeType = <<"pgsql">>, + BridgeType = proplists:get_value(bridge_type, Config0, <<"pgsql">>), Host = ?config(pgsql_host, Config0), Port = ?config(pgsql_port, Config0), case emqx_common_test_helpers:is_tcp_server_available(Host, Port) of From b9f258b73708ecf6c5937d3e2ac570fab1a93475 Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Mon, 9 Jan 2023 11:39:42 -0300 Subject: [PATCH 0073/1802] feat(mongodb): add `payload_template` field for bridge (e5.0) https://emqx.atlassian.net/browse/EMQX-8705 Adds a `payload_template` fields that allows users to customize the payload to publish to MongoDB. --- .../i18n/emqx_ee_bridge_mongodb.conf | 11 +++ lib-ee/emqx_ee_bridge/src/emqx_ee_bridge.erl | 6 +- .../src/emqx_ee_bridge_mongodb.erl | 3 +- .../test/emqx_ee_bridge_mongodb_SUITE.erl | 24 +++++- .../src/emqx_ee_connector_mongodb.erl | 78 +++++++++++++++++++ 5 files changed, 115 insertions(+), 7 deletions(-) create mode 100644 lib-ee/emqx_ee_connector/src/emqx_ee_connector_mongodb.erl diff --git a/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_mongodb.conf b/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_mongodb.conf index f8009f0a4..4880148f9 100644 --- a/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_mongodb.conf +++ b/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_mongodb.conf @@ -86,4 +86,15 @@ emqx_ee_bridge_mongodb { zh: "桥接名称" } } + + payload_template { + desc { + en: "The template for formatting the outgoing messages. If undefined, will send all the available context in JSON format." + zh: "用于格式化外发信息的模板。 如果未定义,将以JSON格式发送所有可用的上下文。" + } + label: { + en: "Payload template" + zh: "有效载荷模板" + } + } } diff --git a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge.erl b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge.erl index d0099db1c..f47829870 100644 --- a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge.erl +++ b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge.erl @@ -57,9 +57,9 @@ resource_type(Type) when is_binary(Type) -> resource_type(binary_to_atom(Type, u resource_type(kafka) -> emqx_bridge_impl_kafka; resource_type(hstreamdb) -> emqx_ee_connector_hstreamdb; resource_type(gcp_pubsub) -> emqx_ee_connector_gcp_pubsub; -resource_type(mongodb_rs) -> emqx_connector_mongo; -resource_type(mongodb_sharded) -> emqx_connector_mongo; -resource_type(mongodb_single) -> emqx_connector_mongo; +resource_type(mongodb_rs) -> emqx_ee_connector_mongodb; +resource_type(mongodb_sharded) -> emqx_ee_connector_mongodb; +resource_type(mongodb_single) -> emqx_ee_connector_mongodb; resource_type(mysql) -> emqx_connector_mysql; resource_type(influxdb_api_v1) -> emqx_ee_connector_influxdb; resource_type(influxdb_api_v2) -> emqx_ee_connector_influxdb; diff --git a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_mongodb.erl b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_mongodb.erl index 516c75f65..bb4082681 100644 --- a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_mongodb.erl +++ b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_mongodb.erl @@ -37,7 +37,8 @@ roots() -> fields("config") -> [ {enable, mk(boolean(), #{desc => ?DESC("enable"), default => true})}, - {collection, mk(binary(), #{desc => ?DESC("collection"), default => <<"mqtt">>})} + {collection, mk(binary(), #{desc => ?DESC("collection"), default => <<"mqtt">>})}, + {payload_template, mk(binary(), #{required => false, desc => ?DESC("payload_template")})} ]; fields(mongodb_rs) -> emqx_connector_mongo:fields(rs) ++ fields("config"); diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_mongodb_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_mongodb_SUITE.erl index fb8f1fcc3..7e44347f3 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_mongodb_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_mongodb_SUITE.erl @@ -25,7 +25,8 @@ all() -> group_tests() -> [ t_setup_via_config_and_publish, - t_setup_via_http_api_and_publish + t_setup_via_http_api_and_publish, + t_payload_template ]. groups() -> @@ -196,9 +197,14 @@ parse_and_check(ConfigString, Type, Name) -> Config. create_bridge(Config) -> + create_bridge(Config, _Overrides = #{}). + +create_bridge(Config, Overrides) -> Type = mongo_type_bin(?config(mongo_type, Config)), Name = ?config(mongo_name, Config), - MongoConfig = ?config(mongo_config, Config), + MongoConfig0 = ?config(mongo_config, Config), + MongoConfig = emqx_map_lib:deep_merge(MongoConfig0, Overrides), + ct:pal("creating ~p bridge with config:\n ~p", [Type, MongoConfig]), emqx_bridge:create(Type, Name, MongoConfig). delete_bridge(Config) -> @@ -219,7 +225,8 @@ clear_db(Config) -> Name = ?config(mongo_name, Config), #{<<"collection">> := Collection} = ?config(mongo_config, Config), ResourceID = emqx_bridge_resource:resource_id(Type, Name), - {ok, _, #{state := #{poolname := PoolName}}} = emqx_resource:get_instance(ResourceID), + {ok, _, #{state := #{connector_state := #{poolname := PoolName}}}} = + emqx_resource:get_instance(ResourceID), Selector = #{}, {true, _} = ecpool:pick_and_do( PoolName, {mongo_api, delete, [Collection, Selector]}, no_handover @@ -275,3 +282,14 @@ t_setup_via_http_api_and_publish(Config) -> find_all(Config) ), ok. + +t_payload_template(Config) -> + {ok, _} = create_bridge(Config, #{<<"payload_template">> => <<"{\"foo\": \"${clientid}\"}">>}), + Val = erlang:unique_integer(), + ClientId = emqx_guid:to_hexstr(emqx_guid:gen()), + ok = send_message(Config, #{key => Val, clientid => ClientId}), + ?assertMatch( + {ok, [#{<<"foo">> := ClientId}]}, + find_all(Config) + ), + ok. diff --git a/lib-ee/emqx_ee_connector/src/emqx_ee_connector_mongodb.erl b/lib-ee/emqx_ee_connector/src/emqx_ee_connector_mongodb.erl new file mode 100644 index 000000000..b1327fef6 --- /dev/null +++ b/lib-ee/emqx_ee_connector/src/emqx_ee_connector_mongodb.erl @@ -0,0 +1,78 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- + +-module(emqx_ee_connector_mongodb). + +-behaviour(emqx_resource). + +-include_lib("emqx_connector/include/emqx_connector_tables.hrl"). +-include_lib("emqx_resource/include/emqx_resource.hrl"). +-include_lib("typerefl/include/types.hrl"). +-include_lib("emqx/include/logger.hrl"). +-include_lib("snabbkaffe/include/snabbkaffe.hrl"). + +%% `emqx_resource' API +-export([ + callback_mode/0, + is_buffer_supported/0, + on_start/2, + on_stop/2, + on_query/3, + on_get_status/2 +]). + +%%======================================================================================== +%% `emqx_resource' API +%%======================================================================================== + +callback_mode() -> emqx_connector_mongo:callback_mode(). + +is_buffer_supported() -> false. + +on_start(InstanceId, Config) -> + case emqx_connector_mongo:on_start(InstanceId, Config) of + {ok, ConnectorState} -> + PayloadTemplate0 = maps:get(payload_template, Config, undefined), + PayloadTemplate = preprocess_template(PayloadTemplate0), + State = #{ + payload_template => PayloadTemplate, + connector_state => ConnectorState + }, + {ok, State}; + Error -> + Error + end. + +on_stop(InstanceId, _State = #{connector_state := ConnectorState}) -> + emqx_connector_mongo:on_stop(InstanceId, ConnectorState). + +on_query(InstanceId, {send_message, Message0}, State) -> + #{ + payload_template := PayloadTemplate, + connector_state := ConnectorState + } = State, + Message = render_message(PayloadTemplate, Message0), + emqx_connector_mongo:on_query(InstanceId, {send_message, Message}, ConnectorState); +on_query(InstanceId, Request, _State = #{connector_state := ConnectorState}) -> + emqx_connector_mongo:on_query(InstanceId, Request, ConnectorState). + +on_get_status(InstanceId, _State = #{connector_state := ConnectorState}) -> + emqx_connector_mongo:on_get_status(InstanceId, ConnectorState). + +%%======================================================================================== +%% Helper fns +%%======================================================================================== + +preprocess_template(undefined = _PayloadTemplate) -> + undefined; +preprocess_template(PayloadTemplate) -> + emqx_plugin_libs_rule:preproc_tmpl(PayloadTemplate). + +render_message(undefined = _PayloadTemplate, Message) -> + Message; +render_message(PayloadTemplate, Message) -> + %% Note: mongo expects a map as a document, so the rendered result + %% must be JSON-serializable + Rendered = emqx_plugin_libs_rule:proc_tmpl(PayloadTemplate, Message), + emqx_json:decode(Rendered, [return_maps]). From 0bd5bec982f4ea75592df7e65f1f391604bf1c77 Mon Sep 17 00:00:00 2001 From: Kjell Winblad Date: Mon, 9 Jan 2023 18:14:11 +0100 Subject: [PATCH 0074/1802] chore: upgrade mongodb-erlang to v3.0.19 This upgrades mongodb-erlang from v3.0.18 to v3.0.19. This fixes a bug that occurred when one issued a command specified as a map*. This fix should not affect present EMQX as this functionality is currently not used by EMQX. However, I think it is good to do the upgrade anyway in case we will use the fixed functionality in the future. There is no need to update the changelog. * https://github.com/emqx/mongodb-erlang/pull/36 --- apps/emqx_connector/rebar.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx_connector/rebar.config b/apps/emqx_connector/rebar.config index b84d87e38..ed0bc827d 100644 --- a/apps/emqx_connector/rebar.config +++ b/apps/emqx_connector/rebar.config @@ -12,7 +12,7 @@ {mysql, {git, "https://github.com/emqx/mysql-otp", {tag, "1.7.1"}}}, {epgsql, {git, "https://github.com/emqx/epgsql", {tag, "4.7-emqx.2"}}}, %% NOTE: mind poolboy version when updating mongodb-erlang version - {mongodb, {git, "https://github.com/emqx/mongodb-erlang", {tag, "v3.0.18"}}}, + {mongodb, {git, "https://github.com/emqx/mongodb-erlang", {tag, "v3.0.19"}}}, %% NOTE: mind poolboy version when updating eredis_cluster version {eredis_cluster, {git, "https://github.com/emqx/eredis_cluster", {tag, "0.7.5"}}}, %% mongodb-erlang uses a special fork https://github.com/comtihon/poolboy.git From 724015af47a3f96d04ec129725186068be58c691 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Mon, 9 Jan 2023 18:22:00 +0800 Subject: [PATCH 0075/1802] fix: remove the default value of webhook body field --- apps/emqx_bridge/i18n/emqx_bridge_webhook_schema.conf | 11 ++++++++++- apps/emqx_bridge/src/emqx_bridge_resource.erl | 3 +-- .../src/schema/emqx_bridge_webhook_schema.erl | 2 +- apps/emqx_connector/src/emqx_connector_http.erl | 11 ++++++++--- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/apps/emqx_bridge/i18n/emqx_bridge_webhook_schema.conf b/apps/emqx_bridge/i18n/emqx_bridge_webhook_schema.conf index d9d2d0c40..f58b59aad 100644 --- a/apps/emqx_bridge/i18n/emqx_bridge_webhook_schema.conf +++ b/apps/emqx_bridge/i18n/emqx_bridge_webhook_schema.conf @@ -93,11 +93,20 @@ HTTP 请求的标头。
desc { en: """ The body of the HTTP request.
+If not provided, the body will be a JSON object of all the available fields.
+There, 'all the available fields' means the context of a MQTT message when +this webhook is triggered by receiving a MQTT message (the `local_topic` is set), +or the context of the event when this webhook is triggered by a rule (i.e. this +webhook is used as an action of a rule).
Template with variables is allowed. """ zh: """ HTTP 请求的正文。
-允许使用带有变量的模板。""" +如果没有设置该字段,请求正文将是包含所有可用字段的 JSON object。
+如果该 webhook 是由于收到 MQTT 消息触发的,'所有可用字段' 将是 MQTT 消息的 +上下文信息;如果该 webhook 是由于规则触发的,'所有可用字段' 则为触发事件的上下文信息。
+允许使用带有变量的模板。 +""" } label: { en: "HTTP Body" diff --git a/apps/emqx_bridge/src/emqx_bridge_resource.erl b/apps/emqx_bridge/src/emqx_bridge_resource.erl index d1ce260c9..b28b891a8 100644 --- a/apps/emqx_bridge/src/emqx_bridge_resource.erl +++ b/apps/emqx_bridge/src/emqx_bridge_resource.erl @@ -274,7 +274,6 @@ parse_confs( #{ url := Url, method := Method, - body := Body, headers := Headers, request_timeout := ReqTimeout, max_retries := Retry @@ -288,7 +287,7 @@ parse_confs( #{ path => Path, method => Method, - body => Body, + body => maps:get(body, Conf, undefined), headers => Headers, request_timeout => ReqTimeout, max_retries => Retry diff --git a/apps/emqx_bridge/src/schema/emqx_bridge_webhook_schema.erl b/apps/emqx_bridge/src/schema/emqx_bridge_webhook_schema.erl index a41fc35f5..0495911e7 100644 --- a/apps/emqx_bridge/src/schema/emqx_bridge_webhook_schema.erl +++ b/apps/emqx_bridge/src/schema/emqx_bridge_webhook_schema.erl @@ -115,7 +115,7 @@ request_config() -> mk( binary(), #{ - default => <<"${payload}">>, + default => undefined, desc => ?DESC("config_body") } )}, diff --git a/apps/emqx_connector/src/emqx_connector_http.erl b/apps/emqx_connector/src/emqx_connector_http.erl index 18a246edb..a04850746 100644 --- a/apps/emqx_connector/src/emqx_connector_http.erl +++ b/apps/emqx_connector/src/emqx_connector_http.erl @@ -431,14 +431,13 @@ preprocess_request( #{ method := Method, path := Path, - body := Body, headers := Headers } = Req ) -> #{ method => emqx_plugin_libs_rule:preproc_tmpl(bin(Method)), path => emqx_plugin_libs_rule:preproc_tmpl(Path), - body => emqx_plugin_libs_rule:preproc_tmpl(Body), + body => maybe_preproc_tmpl(body, Req), headers => preproc_headers(Headers), request_timeout => maps:get(request_timeout, Req, 30000), max_retries => maps:get(max_retries, Req, 2) @@ -469,6 +468,12 @@ preproc_headers(Headers) when is_list(Headers) -> Headers ). +maybe_preproc_tmpl(Key, Conf) -> + case maps:get(Key, Conf, undefined) of + undefined -> undefined; + Val -> emqx_plugin_libs_rule:preproc_tmpl(Val) + end. + process_request( #{ method := MethodTks, @@ -487,7 +492,7 @@ process_request( request_timeout => ReqTimeout }. -process_request_body([], Msg) -> +process_request_body(undefined, Msg) -> emqx_json:encode(Msg); process_request_body(BodyTks, Msg) -> emqx_plugin_libs_rule:proc_tmpl(BodyTks, Msg). From cfabbbe14ba3c0eadc23ea667d61185527c975bf Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Tue, 10 Jan 2023 09:53:35 +0800 Subject: [PATCH 0076/1802] chore: add the change logs for #9705 --- changes/v5.0.14/fix-9705.en.md | 8 ++++++++ changes/v5.0.14/fix-9705.zh.md | 5 +++++ 2 files changed, 13 insertions(+) create mode 100644 changes/v5.0.14/fix-9705.en.md create mode 100644 changes/v5.0.14/fix-9705.zh.md diff --git a/changes/v5.0.14/fix-9705.en.md b/changes/v5.0.14/fix-9705.en.md new file mode 100644 index 000000000..479d3d4ea --- /dev/null +++ b/changes/v5.0.14/fix-9705.en.md @@ -0,0 +1,8 @@ +Remove the default value of Webhook. +Before this repair, the default value of the `body` field of Webhook is `${payload}`, +but there is no `payload` field in the available fields of other events except message +publishing in the rule, so in this case, the webhook will send a string with the +message body as "undefined" to the HTTP service. +This fix removes the default value of the `body` field. When the `body` field is +not configured, Webhook will send all available fields of the current event in +the format of JSON object. diff --git a/changes/v5.0.14/fix-9705.zh.md b/changes/v5.0.14/fix-9705.zh.md new file mode 100644 index 000000000..6a57eba05 --- /dev/null +++ b/changes/v5.0.14/fix-9705.zh.md @@ -0,0 +1,5 @@ +删除 Webhook 的默认值。 +在此修复之前,Webhook 的 `body` 字段的默认值为 `${payload}`,但规则中除了消息发布之外的其他事件的可用字段中 +都没有 `payload` 字段,所以这种情况下 Webhook 将发送消息正文为 "undefined" 的字符串到 HTTP 服务。 +此修复移除了 `body` 字段的默认值,当未配置 `body` 字段的时候,Webhook 将以 JSON object 的格式发送 +当前事件的全部可用字段。 From 73643e932fdf1e2f5db23d73feb7a11460b1da8c Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Tue, 10 Jan 2023 16:43:24 +0800 Subject: [PATCH 0077/1802] fix: bulk subscribe topics failed in the client.connected hook --- apps/emqx_management/src/emqx_mgmt_api_clients.erl | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/apps/emqx_management/src/emqx_mgmt_api_clients.erl b/apps/emqx_management/src/emqx_mgmt_api_clients.erl index f078f4d2a..7c45206fd 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_clients.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_clients.erl @@ -718,15 +718,18 @@ subscribe(#{clientid := ClientID, topic := Topic} = Sub) -> end. subscribe_batch(#{clientid := ClientID, topics := Topics}) -> - case lookup(#{clientid => ClientID}) of - {200, _} -> + %% We use emqx_channel instead of emqx_channel_info (used by the emqx_mgmt:lookup_client/2), + %% as the emqx_channel_info table will only be populated after the hook `client.connected` + %% has returned. So if one want to subscribe topics in this hook, it will fail. + case ets:lookup(emqx_channel, ClientID) of + [] -> + {404, ?CLIENT_ID_NOT_FOUND}; + _ -> ArgList = [ [ClientID, Topic, maps:with([qos, nl, rap, rh], Sub)] || #{topic := Topic} = Sub <- Topics ], - {200, emqx_mgmt_util:batch_operation(?MODULE, do_subscribe, ArgList)}; - {404, ?CLIENT_ID_NOT_FOUND} -> - {404, ?CLIENT_ID_NOT_FOUND} + {200, emqx_mgmt_util:batch_operation(?MODULE, do_subscribe, ArgList)} end. unsubscribe(#{clientid := ClientID, topic := Topic}) -> From 3ced422a7a35b2b7407867172c6f739d4ebba5fa Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Tue, 10 Jan 2023 17:09:53 +0800 Subject: [PATCH 0078/1802] chore: update the change logs for #9712 --- changes/v5.0.14/fix-9712.en.md | 2 ++ changes/v5.0.14/fix-9712.zh.md | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 changes/v5.0.14/fix-9712.en.md create mode 100644 changes/v5.0.14/fix-9712.zh.md diff --git a/changes/v5.0.14/fix-9712.en.md b/changes/v5.0.14/fix-9712.en.md new file mode 100644 index 000000000..e110b03e2 --- /dev/null +++ b/changes/v5.0.14/fix-9712.en.md @@ -0,0 +1,2 @@ +Fixed the problem of '404 Not Found' when calling the HTTP API '/clients/:clientid/subscribe/bulk' +from the plug-ins and data-bridges on handling the 'client.connected' event. diff --git a/changes/v5.0.14/fix-9712.zh.md b/changes/v5.0.14/fix-9712.zh.md new file mode 100644 index 000000000..053f6b08c --- /dev/null +++ b/changes/v5.0.14/fix-9712.zh.md @@ -0,0 +1,2 @@ +修复了监听 `client.connected` 事件的插件和数据桥接在调用 `/clients/:clientid/subscribe/bulk` +HTTP 接口时报 `404 Not Found` 的问题。 From f27f573109284d690082ed3fad062c52182b3a0a Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Fri, 16 Dec 2022 14:46:13 +0100 Subject: [PATCH 0079/1802] refactor: move to /authorization/sources/built_in_database/rules --- .github/workflows/run_jmeter_tests.yaml | 10 +-- .../i18n/emqx_authz_api_mnesia_i18n.conf | 50 +++++++-------- apps/emqx_authz/src/emqx_authz.app.src | 2 +- apps/emqx_authz/src/emqx_authz_api_mnesia.erl | 54 ++++++++++------ .../test/emqx_authz_api_mnesia_SUITE.erl | 64 +++++++++---------- changes/v5.0.13-en.md | 2 + changes/v5.0.13-zh.md | 4 +- 7 files changed, 101 insertions(+), 85 deletions(-) diff --git a/.github/workflows/run_jmeter_tests.yaml b/.github/workflows/run_jmeter_tests.yaml index 6eaf4aa75..ba64b6d94 100644 --- a/.github/workflows/run_jmeter_tests.yaml +++ b/.github/workflows/run_jmeter_tests.yaml @@ -92,7 +92,7 @@ jobs: - uses: actions/checkout@v3 with: repository: emqx/emqx-fvt - ref: broker-autotest-v2 + ref: broker-autotest-v3 path: scripts - uses: actions/setup-java@v3 with: @@ -191,7 +191,7 @@ jobs: - uses: actions/checkout@v3 with: repository: emqx/emqx-fvt - ref: broker-autotest-v2 + ref: broker-autotest-v3 path: scripts - uses: actions/setup-java@v3 with: @@ -297,7 +297,7 @@ jobs: - uses: actions/checkout@v3 with: repository: emqx/emqx-fvt - ref: broker-autotest-v2 + ref: broker-autotest-v3 path: scripts - uses: actions/setup-java@v3 with: @@ -396,7 +396,7 @@ jobs: - uses: actions/checkout@v3 with: repository: emqx/emqx-fvt - ref: broker-autotest-v2 + ref: broker-autotest-v3 path: scripts - name: run jwks_server timeout-minutes: 10 @@ -496,7 +496,7 @@ jobs: - uses: actions/checkout@v3 with: repository: emqx/emqx-fvt - ref: broker-autotest-v2 + ref: broker-autotest-v3 path: scripts - uses: actions/setup-java@v3 with: diff --git a/apps/emqx_authz/i18n/emqx_authz_api_mnesia_i18n.conf b/apps/emqx_authz/i18n/emqx_authz_api_mnesia_i18n.conf index 50f644097..6d318d02b 100644 --- a/apps/emqx_authz/i18n/emqx_authz_api_mnesia_i18n.conf +++ b/apps/emqx_authz/i18n/emqx_authz_api_mnesia_i18n.conf @@ -1,28 +1,28 @@ emqx_authz_api_mnesia { users_username_get { desc { - en: """Show the list of record for username""" + en: """Show the list of rules for users""" zh: """获取内置数据库中所有用户名类型的规则记录""" } } users_username_post { desc { - en: """Add new records for username""" + en: """Add new rule for 'username'""" zh: """添加内置数据库中用户名类型的规则记录""" } } users_clientid_get { desc { - en: """Show the list of record for clientid""" + en: """Show the list of rules for clients""" zh: """获取内置数据库中所有客户端标识符类型的规则记录""" } } users_clientid_post { desc { - en: """Add new records for clientid""" + en: """Add new rule for 'clientid'""" zh: """添加内置数据库中客户端标识符类型的规则记录""" } } @@ -30,71 +30,71 @@ emqx_authz_api_mnesia { user_username_get { desc { - en: """Get record info for username""" + en: """Get rule for 'username'""" zh: """获取内置数据库中指定用户名类型的规则记录""" } } user_username_put { desc { - en: """Set record for username""" + en: """Set rule for 'username'""" zh: """更新内置数据库中指定用户名类型的规则记录""" } } user_username_delete { desc { - en: """Delete one record for username""" + en: """Delete rule for 'username'""" zh: """删除内置数据库中指定用户名类型的规则记录""" } } user_clientid_get { desc { - en: """Get record info for clientid""" + en: """Get rule for 'clientid'""" zh: """获取内置数据库中指定客户端标识符类型的规则记录""" } } user_clientid_put { desc { - en: """Set record for clientid""" + en: """Set rule for 'clientid'""" zh: """更新内置数据库中指定客户端标识符类型的规则记录""" } } user_clientid_delete { desc { - en: """Delete one record for clientid""" + en: """Delete rule for 'clientid'""" zh: """删除内置数据库中指定客户端标识符类型的规则记录""" } } - - rules_for_all_get { + rules_all_get { desc { - en: """Show the list of rules for all""" + en: """Show the list of rules for 'all'""" zh: """列出为所有客户端启用的规则列表""" } } - rules_for_all_post { + rules_all_post { desc { - en: """ -Create/Update the list of rules for all. -Set a empty list to clean up rules -""" - zh: """ -创建/更新 为所有客户端启用的规则列表。 -设为空列表以清楚所有规则 -""" + en: """Create/Update the list of rules for 'all'.""" + zh: """创建/更新 为所有客户端启用的规则列表。""" } } - purge_all_delete { + rules_all_delete { desc { - en: """Purge all records for username/clientid/all""" - zh: """清除所有内置数据库中的规则, 用户名/客户端标识符/所有""" + en: """Delete rules for 'all'""" + zh: """删除 `all` 规则""" + } + } + + rules_delete { + desc { + en: """Delete all rules for all 'users', 'clients' and 'all'""" + zh: """清除内置数据库中的所有类型('users' 、'clients' 、'all')的所有规则""" } } diff --git a/apps/emqx_authz/src/emqx_authz.app.src b/apps/emqx_authz/src/emqx_authz.app.src index c876fbf16..f5b9f9da6 100644 --- a/apps/emqx_authz/src/emqx_authz.app.src +++ b/apps/emqx_authz/src/emqx_authz.app.src @@ -1,7 +1,7 @@ %% -*- mode: erlang -*- {application, emqx_authz, [ {description, "An OTP application"}, - {vsn, "0.1.10"}, + {vsn, "0.1.11"}, {registered, []}, {mod, {emqx_authz_app, []}}, {applications, [ diff --git a/apps/emqx_authz/src/emqx_authz_api_mnesia.erl b/apps/emqx_authz/src/emqx_authz_api_mnesia.erl index 6a747496c..b39379b43 100644 --- a/apps/emqx_authz/src/emqx_authz_api_mnesia.erl +++ b/apps/emqx_authz/src/emqx_authz_api_mnesia.erl @@ -44,7 +44,7 @@ user/2, client/2, all/2, - purge/2 + rules/2 ]). %% query funs @@ -70,19 +70,19 @@ api_spec() -> paths() -> [ - "/authorization/sources/built_in_database/username", - "/authorization/sources/built_in_database/clientid", - "/authorization/sources/built_in_database/username/:username", - "/authorization/sources/built_in_database/clientid/:clientid", - "/authorization/sources/built_in_database/all", - "/authorization/sources/built_in_database/purge-all" + "/authorization/sources/built_in_database/rules/users", + "/authorization/sources/built_in_database/rules/clients", + "/authorization/sources/built_in_database/rules/users/:username", + "/authorization/sources/built_in_database/rules/clients/:clientid", + "/authorization/sources/built_in_database/rules/all", + "/authorization/sources/built_in_database/rules" ]. %%-------------------------------------------------------------------- %% Schema for each URI %%-------------------------------------------------------------------- -schema("/authorization/sources/built_in_database/username") -> +schema("/authorization/sources/built_in_database/rules/users") -> #{ 'operationId' => users, get => @@ -128,7 +128,7 @@ schema("/authorization/sources/built_in_database/username") -> } } }; -schema("/authorization/sources/built_in_database/clientid") -> +schema("/authorization/sources/built_in_database/rules/clients") -> #{ 'operationId' => clients, get => @@ -174,7 +174,7 @@ schema("/authorization/sources/built_in_database/clientid") -> } } }; -schema("/authorization/sources/built_in_database/username/:username") -> +schema("/authorization/sources/built_in_database/rules/users/:username") -> #{ 'operationId' => user, get => @@ -227,7 +227,7 @@ schema("/authorization/sources/built_in_database/username/:username") -> } } }; -schema("/authorization/sources/built_in_database/clientid/:clientid") -> +schema("/authorization/sources/built_in_database/rules/clients/:clientid") -> #{ 'operationId' => client, get => @@ -280,20 +280,20 @@ schema("/authorization/sources/built_in_database/clientid/:clientid") -> } } }; -schema("/authorization/sources/built_in_database/all") -> +schema("/authorization/sources/built_in_database/rules/all") -> #{ 'operationId' => all, get => #{ tags => [<<"authorization">>], - description => ?DESC(rules_for_all_get), + description => ?DESC(rules_all_get), responses => #{200 => swagger_with_example({rules, ?TYPE_REF}, {all, ?PUT_MAP_EXAMPLE})} }, post => #{ tags => [<<"authorization">>], - description => ?DESC(rules_for_all_post), + description => ?DESC(rules_all_post), 'requestBody' => swagger_with_example({rules, ?TYPE_REF}, {all, ?PUT_MAP_EXAMPLE}), responses => @@ -303,15 +303,24 @@ schema("/authorization/sources/built_in_database/all") -> [?BAD_REQUEST], <<"Bad rule schema">> ) } - } - }; -schema("/authorization/sources/built_in_database/purge-all") -> - #{ - 'operationId' => purge, + }, delete => #{ tags => [<<"authorization">>], - description => ?DESC(purge_all_delete), + description => ?DESC(rules_all_delete), + responses => + #{ + 204 => <<"Deleted">> + } + } + }; +schema("/authorization/sources/built_in_database/rules") -> + #{ + 'operationId' => rules, + delete => + #{ + tags => [<<"authorization">>], + description => ?DESC(rules_delete), responses => #{ 204 => <<"Deleted">>, @@ -555,9 +564,12 @@ all(get, _) -> end; all(post, #{body := #{<<"rules">> := Rules}}) -> emqx_authz_mnesia:store_rules(all, format_rules(Rules)), + {204}; +all(delete, _) -> + emqx_authz_mnesia:store_rules(all, []), {204}. -purge(delete, _) -> +rules(delete, _) -> case emqx_authz_api_sources:get_raw_source(<<"built_in_database">>) of [#{<<"enable">> := false}] -> ok = emqx_authz_mnesia:purge_rules(), diff --git a/apps/emqx_authz/test/emqx_authz_api_mnesia_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_mnesia_SUITE.erl index 62c747433..7b91f9b1c 100644 --- a/apps/emqx_authz/test/emqx_authz_api_mnesia_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_api_mnesia_SUITE.erl @@ -70,21 +70,21 @@ t_api(_) -> {ok, 204, _} = request( post, - uri(["authorization", "sources", "built_in_database", "username"]), + uri(["authorization", "sources", "built_in_database", "rules", "users"]), [?USERNAME_RULES_EXAMPLE] ), {ok, 409, _} = request( post, - uri(["authorization", "sources", "built_in_database", "username"]), + uri(["authorization", "sources", "built_in_database", "rules", "users"]), [?USERNAME_RULES_EXAMPLE] ), {ok, 200, Request1} = request( get, - uri(["authorization", "sources", "built_in_database", "username"]), + uri(["authorization", "sources", "built_in_database", "rules", "users"]), [] ), #{ @@ -104,7 +104,8 @@ t_api(_) -> "authorization", "sources", "built_in_database", - "username?page=1&limit=20&like_username=noexist" + "rules", + "users?page=1&limit=20&like_username=noexist" ]), [] ), @@ -120,7 +121,7 @@ t_api(_) -> {ok, 200, Request2} = request( get, - uri(["authorization", "sources", "built_in_database", "username", "user1"]), + uri(["authorization", "sources", "built_in_database", "rules", "users", "user1"]), [] ), #{<<"username">> := <<"user1">>, <<"rules">> := Rules1} = jsx:decode(Request2), @@ -128,13 +129,13 @@ t_api(_) -> {ok, 204, _} = request( put, - uri(["authorization", "sources", "built_in_database", "username", "user1"]), + uri(["authorization", "sources", "built_in_database", "rules", "users", "user1"]), ?USERNAME_RULES_EXAMPLE#{rules => []} ), {ok, 200, Request3} = request( get, - uri(["authorization", "sources", "built_in_database", "username", "user1"]), + uri(["authorization", "sources", "built_in_database", "rules", "users", "user1"]), [] ), #{<<"username">> := <<"user1">>, <<"rules">> := Rules2} = jsx:decode(Request3), @@ -143,46 +144,46 @@ t_api(_) -> {ok, 204, _} = request( delete, - uri(["authorization", "sources", "built_in_database", "username", "user1"]), + uri(["authorization", "sources", "built_in_database", "rules", "users", "user1"]), [] ), {ok, 404, _} = request( get, - uri(["authorization", "sources", "built_in_database", "username", "user1"]), + uri(["authorization", "sources", "built_in_database", "rules", "users", "user1"]), [] ), {ok, 404, _} = request( delete, - uri(["authorization", "sources", "built_in_database", "username", "user1"]), + uri(["authorization", "sources", "built_in_database", "rules", "users", "user1"]), [] ), {ok, 204, _} = request( post, - uri(["authorization", "sources", "built_in_database", "clientid"]), + uri(["authorization", "sources", "built_in_database", "rules", "clients"]), [?CLIENTID_RULES_EXAMPLE] ), {ok, 409, _} = request( post, - uri(["authorization", "sources", "built_in_database", "clientid"]), + uri(["authorization", "sources", "built_in_database", "rules", "clients"]), [?CLIENTID_RULES_EXAMPLE] ), {ok, 200, Request4} = request( get, - uri(["authorization", "sources", "built_in_database", "clientid"]), + uri(["authorization", "sources", "built_in_database", "rules", "clients"]), [] ), {ok, 200, Request5} = request( get, - uri(["authorization", "sources", "built_in_database", "clientid", "client1"]), + uri(["authorization", "sources", "built_in_database", "rules", "clients", "client1"]), [] ), #{ @@ -196,13 +197,13 @@ t_api(_) -> {ok, 204, _} = request( put, - uri(["authorization", "sources", "built_in_database", "clientid", "client1"]), + uri(["authorization", "sources", "built_in_database", "rules", "clients", "client1"]), ?CLIENTID_RULES_EXAMPLE#{rules => []} ), {ok, 200, Request6} = request( get, - uri(["authorization", "sources", "built_in_database", "clientid", "client1"]), + uri(["authorization", "sources", "built_in_database", "rules", "clients", "client1"]), [] ), #{<<"clientid">> := <<"client1">>, <<"rules">> := Rules4} = jsx:decode(Request6), @@ -211,32 +212,32 @@ t_api(_) -> {ok, 204, _} = request( delete, - uri(["authorization", "sources", "built_in_database", "clientid", "client1"]), + uri(["authorization", "sources", "built_in_database", "rules", "clients", "client1"]), [] ), {ok, 404, _} = request( get, - uri(["authorization", "sources", "built_in_database", "clientid", "client1"]), + uri(["authorization", "sources", "built_in_database", "rules", "clients", "client1"]), [] ), {ok, 404, _} = request( delete, - uri(["authorization", "sources", "built_in_database", "clientid", "client1"]), + uri(["authorization", "sources", "built_in_database", "rules", "clients", "client1"]), [] ), {ok, 204, _} = request( post, - uri(["authorization", "sources", "built_in_database", "all"]), + uri(["authorization", "sources", "built_in_database", "rules", "all"]), ?ALL_RULES_EXAMPLE ), {ok, 200, Request7} = request( get, - uri(["authorization", "sources", "built_in_database", "all"]), + uri(["authorization", "sources", "built_in_database", "rules", "all"]), [] ), #{<<"rules">> := Rules5} = jsx:decode(Request7), @@ -244,15 +245,14 @@ t_api(_) -> {ok, 204, _} = request( - post, - uri(["authorization", "sources", "built_in_database", "all"]), - - ?ALL_RULES_EXAMPLE#{rules => []} + delete, + uri(["authorization", "sources", "built_in_database", "rules", "all"]), + [] ), {ok, 200, Request8} = request( get, - uri(["authorization", "sources", "built_in_database", "all"]), + uri(["authorization", "sources", "built_in_database", "rules", "all"]), [] ), #{<<"rules">> := Rules6} = jsx:decode(Request8), @@ -261,7 +261,7 @@ t_api(_) -> {ok, 204, _} = request( post, - uri(["authorization", "sources", "built_in_database", "username"]), + uri(["authorization", "sources", "built_in_database", "rules", "users"]), [ #{username => erlang:integer_to_binary(N), rules => []} || N <- lists:seq(1, 20) @@ -270,7 +270,7 @@ t_api(_) -> {ok, 200, Request9} = request( get, - uri(["authorization", "sources", "built_in_database", "username?page=2&limit=5"]), + uri(["authorization", "sources", "built_in_database", "rules", "users?page=2&limit=5"]), [] ), #{<<"data">> := Data1} = jsx:decode(Request9), @@ -279,7 +279,7 @@ t_api(_) -> {ok, 204, _} = request( post, - uri(["authorization", "sources", "built_in_database", "clientid"]), + uri(["authorization", "sources", "built_in_database", "rules", "clients"]), [ #{clientid => erlang:integer_to_binary(N), rules => []} || N <- lists:seq(1, 20) @@ -288,7 +288,7 @@ t_api(_) -> {ok, 200, Request10} = request( get, - uri(["authorization", "sources", "built_in_database", "clientid?limit=5"]), + uri(["authorization", "sources", "built_in_database", "rules", "clients?limit=5"]), [] ), #{<<"data">> := Data2} = jsx:decode(Request10), @@ -297,7 +297,7 @@ t_api(_) -> {ok, 400, Msg1} = request( delete, - uri(["authorization", "sources", "built_in_database", "purge-all"]), + uri(["authorization", "sources", "built_in_database", "rules"]), [] ), ?assertMatch({match, _}, re:run(Msg1, "must\sbe\sdisabled\sbefore")), @@ -323,7 +323,7 @@ t_api(_) -> {ok, 204, _} = request( delete, - uri(["authorization", "sources", "built_in_database", "purge-all"]), + uri(["authorization", "sources", "built_in_database", "rules"]), [] ), ?assertEqual(0, emqx_authz_mnesia:record_count()), diff --git a/changes/v5.0.13-en.md b/changes/v5.0.13-en.md index 669275958..11c7705a4 100644 --- a/changes/v5.0.13-en.md +++ b/changes/v5.0.13-en.md @@ -20,6 +20,8 @@ - `/bridges_probe` API endpoint to test params for creating a new data bridge [#9585](https://github.com/emqx/emqx/pull/9585). +- Refactor `/authorization/sources/built_in_database/` by adding `rules/` to the path [#9569](https://github.com/emqx/emqx/pull/9569). + ## Bug fixes - Trigger `message.dropped` hook when QoS2 message is resend by client with a same packet id, or 'awaiting_rel' queue is full [#9487](https://github.com/emqx/emqx/pull/9487). diff --git a/changes/v5.0.13-zh.md b/changes/v5.0.13-zh.md index aae629a4a..8b556a6d8 100644 --- a/changes/v5.0.13-zh.md +++ b/changes/v5.0.13-zh.md @@ -18,7 +18,9 @@ - 确保黑名单的默认超期时间足够长 [#9599](https://github.com/emqx/emqx/pull/9599/)。 -- [FIXME] `/bridges_probe` API 端点用于测试创建新数据桥的参数 [#9585](https://github.com/emqx/emqx/pull/9585)。 +- 添加新 API 接口 `/bridges_probe` 用于测试创建桥接的参数是否可用 [#9585](https://github.com/emqx/emqx/pull/9585)。 + +- 重构 `/authorization/sources/built_in_database/` 接口,将 `rules/` 添加到了其路径中 [#9569](https://github.com/emqx/emqx/pull/9569)。 ## 修复 From 142826c4c39ec3d02c952a7fcf570683e3e01689 Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Mon, 9 Jan 2023 19:15:50 +0800 Subject: [PATCH 0080/1802] chore: limit the maximum number of api keys to 100 --- apps/emqx_management/src/emqx_mgmt_auth.erl | 2 +- ...gmt_api_app_SUITE.erl => emqx_mgmt_api_api_keys_SUITE.erl} | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename apps/emqx_management/test/{emqx_mgmt_api_app_SUITE.erl => emqx_mgmt_api_api_keys_SUITE.erl} (99%) diff --git a/apps/emqx_management/src/emqx_mgmt_auth.erl b/apps/emqx_management/src/emqx_mgmt_auth.erl index 3d97e53bc..e92a9310d 100644 --- a/apps/emqx_management/src/emqx_mgmt_auth.erl +++ b/apps/emqx_management/src/emqx_mgmt_auth.erl @@ -59,7 +59,7 @@ mnesia(boot) -> ]). create(Name, Enable, ExpiredAt, Desc) -> - case mnesia:table_info(?APP, size) < 30 of + case mnesia:table_info(?APP, size) < 1024 of true -> create_app(Name, Enable, ExpiredAt, Desc); false -> {error, "Maximum ApiKey"} end. diff --git a/apps/emqx_management/test/emqx_mgmt_api_app_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_api_api_keys_SUITE.erl similarity index 99% rename from apps/emqx_management/test/emqx_mgmt_api_app_SUITE.erl rename to apps/emqx_management/test/emqx_mgmt_api_api_keys_SUITE.erl index a3aaf8f58..3203a4a7c 100644 --- a/apps/emqx_management/test/emqx_mgmt_api_app_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_api_api_keys_SUITE.erl @@ -13,7 +13,7 @@ %% See the License for the specific language governing permissions and %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_mgmt_api_app_SUITE). +-module(emqx_mgmt_api_api_keys_SUITE). -compile(export_all). -compile(nowarn_export_all). @@ -69,7 +69,7 @@ t_create_failed(_Config) -> ?assertEqual(BadRequest, create_app(LongName)), {ok, List} = list_app(), - CreateNum = 30 - erlang:length(List), + CreateNum = 1024 - erlang:length(List), Names = lists:map( fun(Seq) -> <<"EMQX-API-FAILED-KEY-", (integer_to_binary(Seq))/binary>> From f6a47e5cf61270580bbee7c0c6c7f369bc99a0f8 Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Tue, 10 Jan 2023 09:43:04 +0800 Subject: [PATCH 0081/1802] chore: support api_key.bootstrap_file config --- apps/emqx/src/emqx_packet.erl | 2 +- apps/emqx_conf/src/emqx_conf_schema.erl | 3 +- .../i18n/emqx_dashboard_i18n.conf | 19 +- .../src/emqx_dashboard_admin.erl | 59 +---- .../emqx_dashboard/src/emqx_dashboard_app.erl | 9 +- .../src/emqx_dashboard_schema.erl | 10 +- .../i18n/emqx_mgmt_api_key.conf | 221 ++++++++++++++++++ .../i18n/emqx_mgmt_api_key_i18n.conf | 33 +++ ...api_app.erl => emqx_mgmt_api_api_keys.erl} | 2 +- .../src/emqx_mgmt_api_configs.erl | 3 +- .../src/emqx_mgmt_api_key_schema.erl | 44 ++++ apps/emqx_management/src/emqx_mgmt_app.erl | 11 +- apps/emqx_management/src/emqx_mgmt_auth.erl | 112 ++++++++- 13 files changed, 438 insertions(+), 90 deletions(-) create mode 100644 apps/emqx_management/i18n/emqx_mgmt_api_key.conf create mode 100644 apps/emqx_management/i18n/emqx_mgmt_api_key_i18n.conf rename apps/emqx_management/src/{emqx_mgmt_api_app.erl => emqx_mgmt_api_api_keys.erl} (99%) create mode 100644 apps/emqx_management/src/emqx_mgmt_api_key_schema.erl diff --git a/apps/emqx/src/emqx_packet.erl b/apps/emqx/src/emqx_packet.erl index 8f539563e..d0de1f018 100644 --- a/apps/emqx/src/emqx_packet.erl +++ b/apps/emqx/src/emqx_packet.erl @@ -492,7 +492,7 @@ format_variable(undefined, _, _) -> format_variable(Variable, undefined, PayloadEncode) -> format_variable(Variable, PayloadEncode); format_variable(Variable, Payload, PayloadEncode) -> - [format_variable(Variable, PayloadEncode), format_payload(Payload, PayloadEncode)]. + [format_variable(Variable, PayloadEncode), ",", format_payload(Payload, PayloadEncode)]. format_variable( #mqtt_packet_connect{ diff --git a/apps/emqx_conf/src/emqx_conf_schema.erl b/apps/emqx_conf/src/emqx_conf_schema.erl index 2a46e95a5..a7b388964 100644 --- a/apps/emqx_conf/src/emqx_conf_schema.erl +++ b/apps/emqx_conf/src/emqx_conf_schema.erl @@ -60,7 +60,8 @@ emqx_exhook_schema, emqx_psk_schema, emqx_limiter_schema, - emqx_slow_subs_schema + emqx_slow_subs_schema, + emqx_mgmt_api_key_schema ]). %% root config should not have a namespace diff --git a/apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf b/apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf index e6758d0de..872cfdf26 100644 --- a/apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf +++ b/apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf @@ -199,23 +199,12 @@ its own from which a browser should permit loading resources.""" } bootstrap_users_file { desc { - en: "Initialize users file." - zh: "初始化用户文件" + en: "Deprecated, use api_key.bootstrap_file" + zh: "已废弃,请使用 api_key.bootstrap_file" } label { - en: """Is used to add an administrative user to Dashboard when emqx is first launched, - the format is: - ``` - username1:password1 - username2:password2 - ``` -""" - zh: """用于在首次启动 emqx 时,为 Dashboard 添加管理用户,其格式为: - ``` - username1:password1 - username2:password2 - ``` -""" + en: """Deprecated""" + zh: """已废弃""" } } } diff --git a/apps/emqx_dashboard/src/emqx_dashboard_admin.erl b/apps/emqx_dashboard/src/emqx_dashboard_admin.erl index 77c77d5b9..e36c2628b 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_admin.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_admin.erl @@ -51,8 +51,7 @@ -export([ add_default_user/0, - default_username/0, - add_bootstrap_users/0 + default_username/0 ]). -type emqx_admin() :: #?ADMIN{}. @@ -85,21 +84,6 @@ mnesia(boot) -> add_default_user() -> add_default_user(binenv(default_username), binenv(default_password)). --spec add_bootstrap_users() -> ok | {error, _}. -add_bootstrap_users() -> - case emqx:get_config([dashboard, bootstrap_users_file], undefined) of - undefined -> - ok; - File -> - case mnesia:table_info(?ADMIN, size) of - 0 -> - ?SLOG(debug, #{msg => "Add dashboard bootstrap users", file => File}), - add_bootstrap_users(File); - _ -> - ok - end - end. - %%-------------------------------------------------------------------- %% API %%-------------------------------------------------------------------- @@ -311,44 +295,3 @@ add_default_user(Username, Password) -> [] -> add_user(Username, Password, <<"administrator">>); _ -> {ok, default_user_exists} end. - -add_bootstrap_users(File) -> - case file:open(File, [read]) of - {ok, Dev} -> - {ok, MP} = re:compile(<<"(\.+):(\.+$)">>, [ungreedy]), - try - load_bootstrap_user(Dev, MP) - catch - Type:Reason -> - {error, {Type, Reason}} - after - file:close(Dev) - end; - {error, Reason} = Error -> - ?SLOG(error, #{ - msg => "failed to open the dashboard bootstrap users file", - file => File, - reason => Reason - }), - Error - end. - -load_bootstrap_user(Dev, MP) -> - case file:read_line(Dev) of - {ok, Line} -> - case re:run(Line, MP, [global, {capture, all_but_first, binary}]) of - {match, [[Username, Password]]} -> - case add_user(Username, Password, ?BOOTSTRAP_USER_TAG) of - {ok, _} -> - load_bootstrap_user(Dev, MP); - Error -> - Error - end; - _ -> - load_bootstrap_user(Dev, MP) - end; - eof -> - ok; - Error -> - Error - end. diff --git a/apps/emqx_dashboard/src/emqx_dashboard_app.erl b/apps/emqx_dashboard/src/emqx_dashboard_app.erl index 6956f3fc8..2c3f9b8bc 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_app.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_app.erl @@ -31,13 +31,8 @@ start(_StartType, _StartArgs) -> case emqx_dashboard:start_listeners() of ok -> emqx_dashboard_cli:load(), - case emqx_dashboard_admin:add_bootstrap_users() of - ok -> - {ok, _} = emqx_dashboard_admin:add_default_user(), - {ok, Sup}; - Error -> - Error - end; + {ok, _} = emqx_dashboard_admin:add_default_user(), + {ok, Sup}; {error, Reason} -> {error, Reason} end. diff --git a/apps/emqx_dashboard/src/emqx_dashboard_schema.erl b/apps/emqx_dashboard/src/emqx_dashboard_schema.erl index 4605d911d..6742032d5 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_schema.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_schema.erl @@ -56,7 +56,15 @@ fields("dashboard") -> {cors, fun cors/1}, {i18n_lang, fun i18n_lang/1}, {bootstrap_users_file, - ?HOCON(binary(), #{desc => ?DESC(bootstrap_users_file), required => false})} + ?HOCON( + binary(), + #{ + desc => ?DESC(bootstrap_users_file), + required => false, + default => <<>> + %% deprecated => {since, "5.1.0"} + } + )} ]; fields("listeners") -> [ diff --git a/apps/emqx_management/i18n/emqx_mgmt_api_key.conf b/apps/emqx_management/i18n/emqx_mgmt_api_key.conf new file mode 100644 index 000000000..e6758d0de --- /dev/null +++ b/apps/emqx_management/i18n/emqx_mgmt_api_key.conf @@ -0,0 +1,221 @@ +emqx_dashboard_schema { + listeners { + desc { + en: """HTTP(s) listeners are identified by their protocol type and are +used to serve dashboard UI and restful HTTP API. +Listeners must have a unique combination of port number and IP address. +For example, an HTTP listener can listen on all configured IP addresses +on a given port for a machine by specifying the IP address 0.0.0.0. +Alternatively, the HTTP listener can specify a unique IP address for each listener, +but use the same port.""" + zh: """仪表盘监听器设置。""" + } + label { + en: "Listeners" + zh: "监听器" + } + } + sample_interval { + desc { + en: """How often to update metrics displayed in the dashboard. +Note: `sample_interval` should be a divisor of 60.""" + zh: """更新仪表板中显示的指标的时间间隔。必须小于60,且被60的整除。""" + } + } + token_expired_time { + desc { + en: "JWT token expiration time." + zh: "JWT token 过期时间" + } + label { + en: "Token expired time" + zh: "JWT 过期时间" + } + } + num_acceptors { + desc { + en: "Socket acceptor pool size for TCP protocols." + zh: "TCP协议的Socket acceptor池大小" + } + label { + en: "Number of acceptors" + zh: "Acceptor 数量" + } + } + max_connections { + desc { + en: "Maximum number of simultaneous connections." + zh: "同时处理的最大连接数" + } + label { + en: "Maximum connections" + zh: "最大连接数" + } + } + backlog { + desc { + en: "Defines the maximum length that the queue of pending connections can grow to." + zh: "排队等待连接的队列的最大长度" + } + label { + en: "Backlog" + zh: "排队长度" + } + } + send_timeout { + desc { + en: "Send timeout for the socket." + zh: "Socket发送超时时间" + } + label { + en: "Send timeout" + zh: "发送超时时间" + } + } + inet6 { + desc { + en: "Enable IPv6 support, default is false, which means IPv4 only." + zh: "启用IPv6, 如果机器不支持IPv6,请关闭此选项,否则会导致仪表盘无法使用。" + } + label { + en: "IPv6" + zh: "IPv6" + } + } + ipv6_v6only { + desc { + en: "Disable IPv4-to-IPv6 mapping for the listener." + zh: "当开启 inet6 功能的同时禁用 IPv4-to-IPv6 映射。该配置仅在 inet6 功能开启时有效。" + } + label { + en: "IPv6 only" + zh: "IPv6 only" + } + } + desc_dashboard { + desc { + en: "Configuration for EMQX dashboard." + zh: "EMQX仪表板配置" + } + label { + en: "Dashboard" + zh: "仪表板" + } + } + desc_listeners { + desc { + en: "Configuration for the dashboard listener." + zh: "仪表板监听器配置" + } + label { + en: "Listeners" + zh: "监听器" + } + } + desc_http { + desc { + en: "Configuration for the dashboard listener (plaintext)." + zh: "仪表板监听器(HTTP)配置" + } + label { + en: "HTTP" + zh: "HTTP" + } + } + desc_https { + desc { + en: "Configuration for the dashboard listener (TLS)." + zh: "仪表板监听器(HTTPS)配置" + } + label { + en: "HTTPS" + zh: "HTTPS" + } + } + listener_enable { + desc { + en: "Ignore or enable this listener" + zh: "忽略或启用该监听器配置" + } + label { + en: "Enable" + zh: "启用" + } + } + bind { + desc { + en: "Port without IP(18083) or port with specified IP(127.0.0.1:18083)." + zh: "监听的地址与端口,在dashboard更新此配置时,会重启dashboard服务。" + } + label { + en: "Bind" + zh: "绑定端口" + } + } + default_username { + desc { + en: "The default username of the automatically created dashboard user." + zh: "默认的仪表板用户名" + } + label { + en: "Default username" + zh: "默认用户名" + } + } + default_password { + desc { + en: """The initial default password for dashboard 'admin' user. +For safety, it should be changed as soon as possible.""" + zh: """默认的仪表板用户密码 +为了安全,应该尽快修改密码。""" + } + label { + en: "Default password" + zh: "默认密码" + } + } + cors { + desc { + en: """Support Cross-Origin Resource Sharing (CORS). +Allows a server to indicate any origins (domain, scheme, or port) other than +its own from which a browser should permit loading resources.""" + zh: """支持跨域资源共享(CORS) +允许服务器指示任何来源(域名、协议或端口),除了本服务器之外的任何浏览器应允许加载资源。""" + } + label { + en: "CORS" + zh: "跨域资源共享" + } + } + i18n_lang { + desc { + en: "Internationalization language support." + zh: "swagger多语言支持" + } + label { + en: "I18n language" + zh: "多语言支持" + } + } + bootstrap_users_file { + desc { + en: "Initialize users file." + zh: "初始化用户文件" + } + label { + en: """Is used to add an administrative user to Dashboard when emqx is first launched, + the format is: + ``` + username1:password1 + username2:password2 + ``` +""" + zh: """用于在首次启动 emqx 时,为 Dashboard 添加管理用户,其格式为: + ``` + username1:password1 + username2:password2 + ``` +""" + } + } +} diff --git a/apps/emqx_management/i18n/emqx_mgmt_api_key_i18n.conf b/apps/emqx_management/i18n/emqx_mgmt_api_key_i18n.conf new file mode 100644 index 000000000..1651f85ea --- /dev/null +++ b/apps/emqx_management/i18n/emqx_mgmt_api_key_i18n.conf @@ -0,0 +1,33 @@ +emqx_mgmt_api_key_schema { + api_key { + desc { + en: """API Key, can be used to request API other than the management API key and the Dashboard user management API""" + zh: """API 密钥, 可用于请求除管理 API 密钥及 Dashboard 用户管理 API 的其它接口""" + } + label { + en: "API Key" + zh: "API 密钥" + } + } + bootstrap_file { + desc { + en: """Is used to add an api_key when emqx is launched, + the format is: + ``` + 7e729ae70d23144b:2QILI9AcQ9BYlVqLDHQNWN2saIjBV4egr1CZneTNKr9CpK + ec3907f865805db0:Ee3taYltUKtoBVD9C3XjQl9C6NXheip8Z9B69BpUv5JxVHL + ``` +""" + zh: """用于在启动 emqx 时,添加 API 密钥,其格式为: + ``` + 7e729ae70d23144b:2QILI9AcQ9BYlVqLDHQNWN2saIjBV4egr1CZneTNKr9CpK + ec3907f865805db0:Ee3taYltUKtoBVD9C3XjQl9C6NXheip8Z9B69BpUv5JxVHL + ``` +""" + } + label { + en: "Initialize api_key file." + zh: "API 密钥初始化文件" + } + } +} diff --git a/apps/emqx_management/src/emqx_mgmt_api_app.erl b/apps/emqx_management/src/emqx_mgmt_api_api_keys.erl similarity index 99% rename from apps/emqx_management/src/emqx_mgmt_api_app.erl rename to apps/emqx_management/src/emqx_mgmt_api_api_keys.erl index d317bea70..c39b11273 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_app.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_api_keys.erl @@ -13,7 +13,7 @@ %% See the License for the specific language governing permissions and %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_mgmt_api_app). +-module(emqx_mgmt_api_api_keys). -behaviour(minirest_api). diff --git a/apps/emqx_management/src/emqx_mgmt_api_configs.erl b/apps/emqx_management/src/emqx_mgmt_api_configs.erl index 976dd29f2..d9cdf6477 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_configs.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_configs.erl @@ -63,7 +63,8 @@ <<"prometheus">>, <<"telemetry">>, <<"listeners">>, - <<"license">> + <<"license">>, + <<"api_key">> ] ++ global_zone_roots() ). diff --git a/apps/emqx_management/src/emqx_mgmt_api_key_schema.erl b/apps/emqx_management/src/emqx_mgmt_api_key_schema.erl new file mode 100644 index 000000000..556e4308f --- /dev/null +++ b/apps/emqx_management/src/emqx_mgmt_api_key_schema.erl @@ -0,0 +1,44 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- +-module(emqx_mgmt_api_key_schema). + +-include_lib("hocon/include/hoconsc.hrl"). + +-export([ + roots/0, + fields/1, + namespace/0, + desc/1 +]). + +namespace() -> api_key. +roots() -> ["api_key"]. + +fields("api_key") -> + [ + {bootstrap_file, + ?HOCON( + binary(), + #{ + desc => ?DESC(bootstrap_file), + required => false, + default => <<>> + } + )} + ]. + +desc("api_key") -> + ?DESC(api_key). diff --git a/apps/emqx_management/src/emqx_mgmt_app.erl b/apps/emqx_management/src/emqx_mgmt_app.erl index 164ac1b36..137f4502c 100644 --- a/apps/emqx_management/src/emqx_mgmt_app.erl +++ b/apps/emqx_management/src/emqx_mgmt_app.erl @@ -28,10 +28,15 @@ -include("emqx_mgmt.hrl"). start(_Type, _Args) -> - {ok, Sup} = emqx_mgmt_sup:start_link(), ok = mria_rlog:wait_for_shards([?MANAGEMENT_SHARD], infinity), - emqx_mgmt_cli:load(), - {ok, Sup}. + case emqx_mgmt_auth:init_bootstrap_file() of + ok -> + {ok, Sup} = emqx_mgmt_sup:start_link(), + ok = emqx_mgmt_cli:load(), + {ok, Sup}; + {error, Reason} -> + {error, Reason} + end. stop(_State) -> ok. diff --git a/apps/emqx_management/src/emqx_mgmt_auth.erl b/apps/emqx_management/src/emqx_mgmt_auth.erl index e92a9310d..19317da42 100644 --- a/apps/emqx_management/src/emqx_mgmt_auth.erl +++ b/apps/emqx_management/src/emqx_mgmt_auth.erl @@ -15,6 +15,7 @@ %%-------------------------------------------------------------------- -module(emqx_mgmt_auth). -include_lib("emqx/include/emqx.hrl"). +-include_lib("emqx/include/logger.hrl"). %% API -export([mnesia/1]). @@ -25,7 +26,8 @@ read/1, update/4, delete/1, - list/0 + list/0, + init_bootstrap_file/0 ]). -export([authorize/3]). @@ -34,7 +36,8 @@ -export([ do_update/4, do_delete/1, - do_create_app/3 + do_create_app/3, + do_force_create_app/3 ]). -define(APP, emqx_app). @@ -58,6 +61,12 @@ mnesia(boot) -> {attributes, record_info(fields, ?APP)} ]). +-spec init_bootstrap_file() -> ok | {error, _}. +init_bootstrap_file() -> + File = bootstrap_file(), + ?SLOG(debug, #{msg => "init_bootstrap_api_keys_from_file", file => File}), + init_bootstrap_file(File). + create(Name, Enable, ExpiredAt, Desc) -> case mnesia:table_info(?APP, size) < 1024 of true -> create_app(Name, Enable, ExpiredAt, Desc); @@ -169,6 +178,9 @@ create_app(Name, Enable, ExpiredAt, Desc) -> create_app(App = #?APP{api_key = ApiKey, name = Name}) -> trans(fun ?MODULE:do_create_app/3, [App, ApiKey, Name]). +force_create_app(NamePrefix, App = #?APP{api_key = ApiKey}) -> + trans(fun ?MODULE:do_force_create_app/3, [App, ApiKey, NamePrefix]). + do_create_app(App, ApiKey, Name) -> case mnesia:read(?APP, Name) of [_] -> @@ -183,6 +195,22 @@ do_create_app(App, ApiKey, Name) -> end end. +do_force_create_app(App, ApiKey, NamePrefix) -> + case mnesia:match_object(?APP, #?APP{api_key = ApiKey, _ = '_'}, read) of + [] -> + NewName = generate_unique_name(NamePrefix), + ok = mnesia:write(App#?APP{name = NewName}); + [#?APP{name = Name}] -> + ok = mnesia:write(App#?APP{name = Name}) + end. + +generate_unique_name(NamePrefix) -> + New = list_to_binary(NamePrefix ++ emqx_misc:gen_id(16)), + case mnesia:read(?APP, New) of + [] -> New; + _ -> generate_unique_name(NamePrefix) + end. + trans(Fun, Args) -> case mria:transaction(?COMMON_SHARD, Fun, Args) of {atomic, Res} -> {ok, Res}; @@ -192,3 +220,83 @@ trans(Fun, Args) -> generate_api_secret() -> Random = crypto:strong_rand_bytes(32), emqx_base62:encode(Random). + +bootstrap_file() -> + case emqx:get_config([api_key, bootstrap_file], <<>>) of + %% For compatible remove until 5.1.0 + <<>> -> + emqx:get_config([dashboard, bootstrap_users_file], <<>>); + File -> + File + end. + +init_bootstrap_file(<<>>) -> + ok; +init_bootstrap_file(File) -> + case file:open(File, [read, binary]) of + {ok, Dev} -> + {ok, MP} = re:compile(<<"(\.+):(\.+$)">>, [ungreedy]), + init_bootstrap_file(File, Dev, MP); + {error, Reason} = Error -> + ?SLOG( + error, + #{ + msg => "failed_to_open_the_bootstrap_file", + file => File, + reason => emqx_misc:explain_posix(Reason) + } + ), + Error + end. + +init_bootstrap_file(File, Dev, MP) -> + try + add_bootstrap_file(File, Dev, MP, 1) + catch + throw:Error -> {error, Error}; + Type:Reason:Stacktrace -> {error, {Type, Reason, Stacktrace}} + after + file:close(Dev) + end. + +-define(BOOTSTRAP_TAG, <<"Bootstrapped From File">>). + +add_bootstrap_file(File, Dev, MP, Line) -> + case file:read_line(Dev) of + {ok, Bin} -> + case re:run(Bin, MP, [global, {capture, all_but_first, binary}]) of + {match, [[AppKey, ApiSecret]]} -> + App = + #?APP{ + enable = true, + expired_at = infinity, + desc = ?BOOTSTRAP_TAG, + created_at = erlang:system_time(second), + api_secret_hash = emqx_dashboard_admin:hash(ApiSecret), + api_key = AppKey + }, + case force_create_app("from_bootstrap_file_", App) of + {ok, ok} -> + add_bootstrap_file(File, Dev, MP, Line + 1); + {error, Reason} -> + throw(#{file => File, line => Line, content => Bin, reason => Reason}) + end; + _ -> + Reason = "invalid_format", + ?SLOG( + error, + #{ + msg => "failed_to_load_bootstrap_file", + file => File, + line => Line, + content => Bin, + reason => Reason + } + ), + throw(#{file => File, line => Line, content => Bin, reason => Reason}) + end; + eof -> + ok; + {error, Reason} -> + throw(#{file => File, line => Line, reason => Reason}) + end. From 5d4a1933a20983b61d88412b3afcc121a1b1ee13 Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Tue, 10 Jan 2023 18:39:05 +0800 Subject: [PATCH 0082/1802] chore: add more test for api_key --- .../i18n/emqx_mgmt_api_key.conf | 221 ------------------ apps/emqx_management/src/emqx_mgmt_auth.erl | 11 +- .../test/emqx_mgmt_api_api_keys_SUITE.erl | 55 ++++- 3 files changed, 57 insertions(+), 230 deletions(-) delete mode 100644 apps/emqx_management/i18n/emqx_mgmt_api_key.conf diff --git a/apps/emqx_management/i18n/emqx_mgmt_api_key.conf b/apps/emqx_management/i18n/emqx_mgmt_api_key.conf deleted file mode 100644 index e6758d0de..000000000 --- a/apps/emqx_management/i18n/emqx_mgmt_api_key.conf +++ /dev/null @@ -1,221 +0,0 @@ -emqx_dashboard_schema { - listeners { - desc { - en: """HTTP(s) listeners are identified by their protocol type and are -used to serve dashboard UI and restful HTTP API. -Listeners must have a unique combination of port number and IP address. -For example, an HTTP listener can listen on all configured IP addresses -on a given port for a machine by specifying the IP address 0.0.0.0. -Alternatively, the HTTP listener can specify a unique IP address for each listener, -but use the same port.""" - zh: """仪表盘监听器设置。""" - } - label { - en: "Listeners" - zh: "监听器" - } - } - sample_interval { - desc { - en: """How often to update metrics displayed in the dashboard. -Note: `sample_interval` should be a divisor of 60.""" - zh: """更新仪表板中显示的指标的时间间隔。必须小于60,且被60的整除。""" - } - } - token_expired_time { - desc { - en: "JWT token expiration time." - zh: "JWT token 过期时间" - } - label { - en: "Token expired time" - zh: "JWT 过期时间" - } - } - num_acceptors { - desc { - en: "Socket acceptor pool size for TCP protocols." - zh: "TCP协议的Socket acceptor池大小" - } - label { - en: "Number of acceptors" - zh: "Acceptor 数量" - } - } - max_connections { - desc { - en: "Maximum number of simultaneous connections." - zh: "同时处理的最大连接数" - } - label { - en: "Maximum connections" - zh: "最大连接数" - } - } - backlog { - desc { - en: "Defines the maximum length that the queue of pending connections can grow to." - zh: "排队等待连接的队列的最大长度" - } - label { - en: "Backlog" - zh: "排队长度" - } - } - send_timeout { - desc { - en: "Send timeout for the socket." - zh: "Socket发送超时时间" - } - label { - en: "Send timeout" - zh: "发送超时时间" - } - } - inet6 { - desc { - en: "Enable IPv6 support, default is false, which means IPv4 only." - zh: "启用IPv6, 如果机器不支持IPv6,请关闭此选项,否则会导致仪表盘无法使用。" - } - label { - en: "IPv6" - zh: "IPv6" - } - } - ipv6_v6only { - desc { - en: "Disable IPv4-to-IPv6 mapping for the listener." - zh: "当开启 inet6 功能的同时禁用 IPv4-to-IPv6 映射。该配置仅在 inet6 功能开启时有效。" - } - label { - en: "IPv6 only" - zh: "IPv6 only" - } - } - desc_dashboard { - desc { - en: "Configuration for EMQX dashboard." - zh: "EMQX仪表板配置" - } - label { - en: "Dashboard" - zh: "仪表板" - } - } - desc_listeners { - desc { - en: "Configuration for the dashboard listener." - zh: "仪表板监听器配置" - } - label { - en: "Listeners" - zh: "监听器" - } - } - desc_http { - desc { - en: "Configuration for the dashboard listener (plaintext)." - zh: "仪表板监听器(HTTP)配置" - } - label { - en: "HTTP" - zh: "HTTP" - } - } - desc_https { - desc { - en: "Configuration for the dashboard listener (TLS)." - zh: "仪表板监听器(HTTPS)配置" - } - label { - en: "HTTPS" - zh: "HTTPS" - } - } - listener_enable { - desc { - en: "Ignore or enable this listener" - zh: "忽略或启用该监听器配置" - } - label { - en: "Enable" - zh: "启用" - } - } - bind { - desc { - en: "Port without IP(18083) or port with specified IP(127.0.0.1:18083)." - zh: "监听的地址与端口,在dashboard更新此配置时,会重启dashboard服务。" - } - label { - en: "Bind" - zh: "绑定端口" - } - } - default_username { - desc { - en: "The default username of the automatically created dashboard user." - zh: "默认的仪表板用户名" - } - label { - en: "Default username" - zh: "默认用户名" - } - } - default_password { - desc { - en: """The initial default password for dashboard 'admin' user. -For safety, it should be changed as soon as possible.""" - zh: """默认的仪表板用户密码 -为了安全,应该尽快修改密码。""" - } - label { - en: "Default password" - zh: "默认密码" - } - } - cors { - desc { - en: """Support Cross-Origin Resource Sharing (CORS). -Allows a server to indicate any origins (domain, scheme, or port) other than -its own from which a browser should permit loading resources.""" - zh: """支持跨域资源共享(CORS) -允许服务器指示任何来源(域名、协议或端口),除了本服务器之外的任何浏览器应允许加载资源。""" - } - label { - en: "CORS" - zh: "跨域资源共享" - } - } - i18n_lang { - desc { - en: "Internationalization language support." - zh: "swagger多语言支持" - } - label { - en: "I18n language" - zh: "多语言支持" - } - } - bootstrap_users_file { - desc { - en: "Initialize users file." - zh: "初始化用户文件" - } - label { - en: """Is used to add an administrative user to Dashboard when emqx is first launched, - the format is: - ``` - username1:password1 - username2:password2 - ``` -""" - zh: """用于在首次启动 emqx 时,为 Dashboard 添加管理用户,其格式为: - ``` - username1:password1 - username2:password2 - ``` -""" - } - } -} diff --git a/apps/emqx_management/src/emqx_mgmt_auth.erl b/apps/emqx_management/src/emqx_mgmt_auth.erl index 19317da42..0bf849d3c 100644 --- a/apps/emqx_management/src/emqx_mgmt_auth.erl +++ b/apps/emqx_management/src/emqx_mgmt_auth.erl @@ -48,7 +48,7 @@ api_secret_hash = <<>> :: binary() | '_', enable = true :: boolean() | '_', desc = <<>> :: binary() | '_', - expired_at = 0 :: integer() | undefined | '_', + expired_at = 0 :: integer() | undefined | infinity | '_', created_at = 0 :: integer() | '_' }). @@ -68,7 +68,7 @@ init_bootstrap_file() -> init_bootstrap_file(File). create(Name, Enable, ExpiredAt, Desc) -> - case mnesia:table_info(?APP, size) < 1024 of + case mnesia:table_info(?APP, size) < 100 of true -> create_app(Name, Enable, ExpiredAt, Desc); false -> {error, "Maximum ApiKey"} end. @@ -237,16 +237,17 @@ init_bootstrap_file(File) -> {ok, Dev} -> {ok, MP} = re:compile(<<"(\.+):(\.+$)">>, [ungreedy]), init_bootstrap_file(File, Dev, MP); - {error, Reason} = Error -> + {error, Reason0} -> + Reason = emqx_misc:explain_posix(Reason0), ?SLOG( error, #{ msg => "failed_to_open_the_bootstrap_file", file => File, - reason => emqx_misc:explain_posix(Reason) + reason => Reason } ), - Error + {error, Reason} end. init_bootstrap_file(File, Dev, MP) -> diff --git a/apps/emqx_management/test/emqx_mgmt_api_api_keys_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_api_api_keys_SUITE.erl index 3203a4a7c..079351538 100644 --- a/apps/emqx_management/test/emqx_mgmt_api_api_keys_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_api_api_keys_SUITE.erl @@ -25,15 +25,62 @@ suite() -> [{timetrap, {minutes, 1}}]. groups() -> [ {parallel, [parallel], [t_create, t_update, t_delete, t_authorize, t_create_unexpired_app]}, - {sequence, [], [t_create_failed]} + {sequence, [], [t_bootstrap_file, t_create_failed]} ]. init_per_suite(Config) -> - emqx_mgmt_api_test_util:init_suite(), + emqx_mgmt_api_test_util:init_suite([emqx_conf]), Config. end_per_suite(_) -> - emqx_mgmt_api_test_util:end_suite(). + emqx_mgmt_api_test_util:end_suite([emqx_conf]). + +t_bootstrap_file(_) -> + TestPath = <<"/api/v5/status">>, + Bin = <<"test-1:secret-1\ntest-2:secret-2">>, + File = "./bootstrap_api_keys.txt", + ok = file:write_file(File, Bin), + emqx:update_config([api_key, bootstrap_file], File), + ok = emqx_mgmt_auth:init_bootstrap_file(), + ?assertEqual(ok, emqx_mgmt_auth:authorize(TestPath, <<"test-1">>, <<"secret-1">>)), + ?assertEqual(ok, emqx_mgmt_auth:authorize(TestPath, <<"test-2">>, <<"secret-2">>)), + ?assertMatch({error, _}, emqx_mgmt_auth:authorize(TestPath, <<"test-2">>, <<"secret-1">>)), + + %% relaunch to check if the table is changed. + Bin1 = <<"test-1:new-secret-1\ntest-2:new-secret-2">>, + ok = file:write_file(File, Bin1), + ok = emqx_mgmt_auth:init_bootstrap_file(), + ?assertMatch({error, _}, emqx_mgmt_auth:authorize(TestPath, <<"test-1">>, <<"secret-1">>)), + ?assertMatch({error, _}, emqx_mgmt_auth:authorize(TestPath, <<"test-2">>, <<"secret-2">>)), + ?assertEqual(ok, emqx_mgmt_auth:authorize(TestPath, <<"test-1">>, <<"new-secret-1">>)), + ?assertEqual(ok, emqx_mgmt_auth:authorize(TestPath, <<"test-2">>, <<"new-secret-2">>)), + + %% Compatibility + Bin2 = <<"test-3:new-secret-3\ntest-4:new-secret-4">>, + ok = file:write_file(File, Bin2), + emqx:update_config([api_key, bootstrap_file], <<>>), + emqx:update_config([dashboard, bootstrap_users_file], File), + ok = emqx_mgmt_auth:init_bootstrap_file(), + ?assertMatch(ok, emqx_mgmt_auth:authorize(TestPath, <<"test-1">>, <<"new-secret-1">>)), + ?assertMatch(ok, emqx_mgmt_auth:authorize(TestPath, <<"test-2">>, <<"new-secret-2">>)), + ?assertEqual(ok, emqx_mgmt_auth:authorize(TestPath, <<"test-3">>, <<"new-secret-3">>)), + ?assertEqual(ok, emqx_mgmt_auth:authorize(TestPath, <<"test-4">>, <<"new-secret-4">>)), + + %% not found + NotFoundFile = "./bootstrap_apps_not_exist.txt", + emqx:update_config([api_key, bootstrap_file], NotFoundFile), + ?assertMatch({error, "No such file or directory"}, emqx_mgmt_auth:init_bootstrap_file()), + + %% bad format + BadBin = <<"test-1:secret-11\ntest-2 secret-12">>, + ok = file:write_file(File, BadBin), + emqx:update_config([api_key, bootstrap_file], File), + ?assertMatch({error, #{reason := "invalid_format"}}, emqx_mgmt_auth:init_bootstrap_file()), + ?assertEqual(ok, emqx_mgmt_auth:authorize(TestPath, <<"test-1">>, <<"secret-11">>)), + ?assertMatch({error, _}, emqx_mgmt_auth:authorize(TestPath, <<"test-2">>, <<"secret-12">>)), + emqx:update_config([api_key, bootstrap_file], <<>>), + emqx:update_config([dashboard, bootstrap_users_file], <<>>), + ok. t_create(_Config) -> Name = <<"EMQX-API-KEY-1">>, @@ -69,7 +116,7 @@ t_create_failed(_Config) -> ?assertEqual(BadRequest, create_app(LongName)), {ok, List} = list_app(), - CreateNum = 1024 - erlang:length(List), + CreateNum = 100 - erlang:length(List), Names = lists:map( fun(Seq) -> <<"EMQX-API-FAILED-KEY-", (integer_to_binary(Seq))/binary>> From 21dbc21dc8302ce6a08c4ff578a4fbde3a514f23 Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Tue, 10 Jan 2023 19:15:01 +0800 Subject: [PATCH 0083/1802] chore: add changelog for api_key.bootstrap_file --- apps/emqx/src/emqx_packet.erl | 2 ++ changes/v5.0.14/feat-9713.en.md | 3 +++ changes/v5.0.14/feat-9713.zh.md | 3 +++ 3 files changed, 8 insertions(+) create mode 100644 changes/v5.0.14/feat-9713.en.md create mode 100644 changes/v5.0.14/feat-9713.zh.md diff --git a/apps/emqx/src/emqx_packet.erl b/apps/emqx/src/emqx_packet.erl index d0de1f018..d82810d15 100644 --- a/apps/emqx/src/emqx_packet.erl +++ b/apps/emqx/src/emqx_packet.erl @@ -16,6 +16,8 @@ -module(emqx_packet). +-elvis([{elvis_style, no_spec_with_records, disable}]). + -include("emqx.hrl"). -include("emqx_mqtt.hrl"). diff --git a/changes/v5.0.14/feat-9713.en.md b/changes/v5.0.14/feat-9713.en.md new file mode 100644 index 000000000..7d68eae12 --- /dev/null +++ b/changes/v5.0.14/feat-9713.en.md @@ -0,0 +1,3 @@ +Introduce `api_key.bootstrap_file` to initialize the api key at boot time. +Deprecate `dashboard.bootstrap_users_file`. +limit the maximum number of api keys to 100 instead of 30. diff --git a/changes/v5.0.14/feat-9713.zh.md b/changes/v5.0.14/feat-9713.zh.md new file mode 100644 index 000000000..7535b8bd5 --- /dev/null +++ b/changes/v5.0.14/feat-9713.zh.md @@ -0,0 +1,3 @@ +引入 `api_key.bootstrap_file`,用于启动时初始化api密钥。 +废弃 `dashboard.boostrap_users_file`。 +将 API 密钥的最大数量限制提升为 100(原来为30)。 From f3cc722172ff2af873a25e57cb425d92f4baa059 Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Tue, 10 Jan 2023 09:06:12 -0300 Subject: [PATCH 0084/1802] docs: improve descriptions Co-authored-by: Zaiming (Stone) Shi --- lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_mongodb.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_mongodb.conf b/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_mongodb.conf index 4880148f9..81ebc1e31 100644 --- a/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_mongodb.conf +++ b/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_mongodb.conf @@ -89,8 +89,8 @@ emqx_ee_bridge_mongodb { payload_template { desc { - en: "The template for formatting the outgoing messages. If undefined, will send all the available context in JSON format." - zh: "用于格式化外发信息的模板。 如果未定义,将以JSON格式发送所有可用的上下文。" + en: "The template for formatting the outgoing messages. If undefined, rule engine will use JSON format to serialize all visible inputs, such as clientid, topic, payload etc." + zh: "用于格式化写入 MongoDB 的消息模板。 如果未定义,规则引擎会使用 JSON 格式序列化所有的可见输入,例如 clientid, topic, payload 等。" } label: { en: "Payload template" From 918ba0dffd8e06af80deb386773be9b437be367a Mon Sep 17 00:00:00 2001 From: zhongwencool Date: Tue, 10 Jan 2023 20:39:31 +0800 Subject: [PATCH 0085/1802] chore: apply suggestions from code review Co-authored-by: ieQu1 <99872536+ieQu1@users.noreply.github.com> --- apps/emqx_management/i18n/emqx_mgmt_api_key_i18n.conf | 2 +- changes/v5.0.14/feat-9713.en.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/emqx_management/i18n/emqx_mgmt_api_key_i18n.conf b/apps/emqx_management/i18n/emqx_mgmt_api_key_i18n.conf index 1651f85ea..eae559660 100644 --- a/apps/emqx_management/i18n/emqx_mgmt_api_key_i18n.conf +++ b/apps/emqx_management/i18n/emqx_mgmt_api_key_i18n.conf @@ -11,7 +11,7 @@ emqx_mgmt_api_key_schema { } bootstrap_file { desc { - en: """Is used to add an api_key when emqx is launched, + en: """Bootstrap file is used to add an api_key when emqx is launched, the format is: ``` 7e729ae70d23144b:2QILI9AcQ9BYlVqLDHQNWN2saIjBV4egr1CZneTNKr9CpK diff --git a/changes/v5.0.14/feat-9713.en.md b/changes/v5.0.14/feat-9713.en.md index 7d68eae12..e8dbe4c6c 100644 --- a/changes/v5.0.14/feat-9713.en.md +++ b/changes/v5.0.14/feat-9713.en.md @@ -1,3 +1,3 @@ Introduce `api_key.bootstrap_file` to initialize the api key at boot time. Deprecate `dashboard.bootstrap_users_file`. -limit the maximum number of api keys to 100 instead of 30. +Limit the maximum number of api keys to 100 instead of 30. From 5415e341fb1e506645d1ee2b02c9f99778b4fc0c Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Tue, 10 Jan 2023 21:53:54 +0800 Subject: [PATCH 0086/1802] fix: bad auto_subscribe api schema --- .../src/emqx_auto_subscribe.erl | 33 +++++++++---------- .../src/emqx_auto_subscribe_api.erl | 19 ++++++----- .../test/emqx_auto_subscribe_SUITE.erl | 26 +++++++++++++++ .../src/emqx_dashboard_swagger.erl | 2 +- 4 files changed, 53 insertions(+), 27 deletions(-) diff --git a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe.erl b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe.erl index 878fc2ad7..7453eabdb 100644 --- a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe.erl +++ b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe.erl @@ -51,8 +51,21 @@ max_limit() -> list() -> format(emqx_conf:get([auto_subscribe, topics], [])). -update(Topics) -> - update_(Topics). +update(Topics) when length(Topics) =< ?MAX_AUTO_SUBSCRIBE -> + case + emqx_conf:update( + [auto_subscribe, topics], + Topics, + #{rawconf_with_defaults => true, override_to => cluster} + ) + of + {ok, #{raw_config := NewTopics}} -> + {ok, NewTopics}; + {error, Reason} -> + {error, Reason} + end; +update(_Topics) -> + {error, quota_exceeded}. post_config_update(_KeyPath, _Req, NewTopics, _OldConf, _AppEnvs) -> Config = emqx_conf:get([auto_subscribe], #{}), @@ -95,22 +108,6 @@ format(Rule = #{topic := Topic}) when is_map(Rule) -> nl => maps:get(nl, Rule, 0) }. -update_(Topics) when length(Topics) =< ?MAX_AUTO_SUBSCRIBE -> - case - emqx_conf:update( - [auto_subscribe, topics], - Topics, - #{rawconf_with_defaults => true, override_to => cluster} - ) - of - {ok, #{raw_config := NewTopics}} -> - {ok, NewTopics}; - {error, Reason} -> - {error, Reason} - end; -update_(_Topics) -> - {error, quota_exceeded}. - update_hook() -> update_hook(emqx_conf:get([auto_subscribe], #{})). diff --git a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_api.erl b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_api.erl index f30482d4c..678c8e9b7 100644 --- a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_api.erl +++ b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_api.erl @@ -34,7 +34,7 @@ -include_lib("emqx/include/emqx_placeholder.hrl"). api_spec() -> - emqx_dashboard_swagger:spec(?MODULE). + emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true}). paths() -> ["/mqtt/auto_subscribe"]. @@ -46,15 +46,15 @@ schema("/mqtt/auto_subscribe") -> description => ?DESC(list_auto_subscribe_api), tags => [<<"Auto Subscribe">>], responses => #{ - 200 => hoconsc:ref(emqx_auto_subscribe_schema, "auto_subscribe") + 200 => topics() } }, put => #{ description => ?DESC(update_auto_subscribe_api), tags => [<<"Auto Subscribe">>], - 'requestBody' => hoconsc:ref(emqx_auto_subscribe_schema, "auto_subscribe"), + 'requestBody' => topics(), responses => #{ - 200 => hoconsc:ref(emqx_auto_subscribe_schema, "auto_subscribe"), + 200 => topics(), 409 => emqx_dashboard_swagger:error_codes( [?EXCEED_LIMIT], ?DESC(update_auto_subscribe_api_response409) @@ -63,14 +63,17 @@ schema("/mqtt/auto_subscribe") -> } }. +topics() -> + Fields = emqx_auto_subscribe_schema:fields("auto_subscribe"), + {topics, Topics} = lists:keyfind(topics, 1, Fields), + Topics. + %%%============================================================================================== %% api apply auto_subscribe(get, _) -> {200, emqx_auto_subscribe:list()}; -auto_subscribe(put, #{body := #{}}) -> - {400, #{code => ?BAD_REQUEST, message => <<"Request body required">>}}; -auto_subscribe(put, #{body := Params}) -> - case emqx_auto_subscribe:update(Params) of +auto_subscribe(put, #{body := Topics}) when is_list(Topics) -> + case emqx_auto_subscribe:update(Topics) of {error, quota_exceeded} -> Message = list_to_binary( io_lib:format( diff --git a/apps/emqx_auto_subscribe/test/emqx_auto_subscribe_SUITE.erl b/apps/emqx_auto_subscribe/test/emqx_auto_subscribe_SUITE.erl index 36c4e708e..959f8ec1b 100644 --- a/apps/emqx_auto_subscribe/test/emqx_auto_subscribe_SUITE.erl +++ b/apps/emqx_auto_subscribe/test/emqx_auto_subscribe_SUITE.erl @@ -151,6 +151,32 @@ t_update(_) -> ResponseMap = emqx_json:decode(Response, [return_maps]), ?assertEqual(1, erlang:length(ResponseMap)), + BadBody1 = #{topic => ?TOPIC_S}, + ?assertMatch( + {error, {"HTTP/1.1", 400, "Bad Request"}}, + emqx_mgmt_api_test_util:request_api(put, Path, "", Auth, BadBody1) + ), + BadBody2 = [#{topic => ?TOPIC_S, qos => 3}], + ?assertMatch( + {error, {"HTTP/1.1", 400, "Bad Request"}}, + emqx_mgmt_api_test_util:request_api(put, Path, "", Auth, BadBody2) + ), + BadBody3 = [#{topic => ?TOPIC_S, rh => 10}], + ?assertMatch( + {error, {"HTTP/1.1", 400, "Bad Request"}}, + emqx_mgmt_api_test_util:request_api(put, Path, "", Auth, BadBody3) + ), + BadBody4 = [#{topic => ?TOPIC_S, rap => -1}], + ?assertMatch( + {error, {"HTTP/1.1", 400, "Bad Request"}}, + emqx_mgmt_api_test_util:request_api(put, Path, "", Auth, BadBody4) + ), + BadBody5 = [#{topic => ?TOPIC_S, nl => -1}], + ?assertMatch( + {error, {"HTTP/1.1", 400, "Bad Request"}}, + emqx_mgmt_api_test_util:request_api(put, Path, "", Auth, BadBody5) + ), + {ok, Client} = emqtt:start_link(#{username => ?CLIENT_USERNAME, clientid => ?CLIENT_ID}), {ok, _} = emqtt:connect(Client), timer:sleep(100), diff --git a/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl b/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl index 85b928dda..102b95f4e 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl @@ -705,7 +705,7 @@ typename_to_spec("service_account_json()", _Mod) -> typename_to_spec("#{" ++ _, Mod) -> typename_to_spec("map()", Mod); typename_to_spec("qos()", _Mod) -> - #{type => string, enum => [0, 1, 2]}; + #{type => integer, minimum => 0, maximum => 2, example => 0}; typename_to_spec("{binary(), binary()}", _Mod) -> #{type => object, example => #{}}; typename_to_spec("comma_separated_list()", _Mod) -> From 558a6697e9216fa9a6f28eaff60bab3fc087a700 Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Tue, 10 Jan 2023 22:10:11 +0800 Subject: [PATCH 0087/1802] chore: add 9714's changelog --- changes/v5.0.14/fix-9714.en.md | 1 + changes/v5.0.14/fix-9714.zh.md | 1 + 2 files changed, 2 insertions(+) create mode 100644 changes/v5.0.14/fix-9714.en.md create mode 100644 changes/v5.0.14/fix-9714.zh.md diff --git a/changes/v5.0.14/fix-9714.en.md b/changes/v5.0.14/fix-9714.en.md new file mode 100644 index 000000000..e1a606744 --- /dev/null +++ b/changes/v5.0.14/fix-9714.en.md @@ -0,0 +1 @@ +Fix `/mqtt/auto_subscribe` API's bad swagger schema, and make sure swagger always checks if the schema is correct. diff --git a/changes/v5.0.14/fix-9714.zh.md b/changes/v5.0.14/fix-9714.zh.md new file mode 100644 index 000000000..cbf38f041 --- /dev/null +++ b/changes/v5.0.14/fix-9714.zh.md @@ -0,0 +1 @@ +修复 `/mqtt/auto_subscribe` API 错误的 swagger 格式,并且保证 swagger 总是检查格式是否正确。 From a2be9947f5e81d13202f7d8907be136a5fedd0f0 Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Tue, 10 Jan 2023 22:12:23 +0800 Subject: [PATCH 0088/1802] chore: update auto_subscribe app version --- apps/emqx_auto_subscribe/src/emqx_auto_subscribe.app.src | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe.app.src b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe.app.src index 1c5627a1f..a273face1 100644 --- a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe.app.src +++ b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe.app.src @@ -1,7 +1,7 @@ %% -*- mode: erlang -*- {application, emqx_auto_subscribe, [ - {description, "An OTP application"}, - {vsn, "0.1.2"}, + {description, "Auto subscribe Application"}, + {vsn, "0.1.3"}, {registered, []}, {mod, {emqx_auto_subscribe_app, []}}, {applications, [ From fb97096405a37ac3faf530fc3939f8f20f03dc36 Mon Sep 17 00:00:00 2001 From: Erik Timan Date: Tue, 10 Jan 2023 16:48:04 +0100 Subject: [PATCH 0089/1802] fix(mgmt_api): deprecate clientid field instead of removing it --- apps/emqx_management/i18n/emqx_mgmt_api_publish_i18n.conf | 6 ------ apps/emqx_management/src/emqx_mgmt_api_publish.erl | 4 ++++ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/apps/emqx_management/i18n/emqx_mgmt_api_publish_i18n.conf b/apps/emqx_management/i18n/emqx_mgmt_api_publish_i18n.conf index d845bff4b..4ba7e8dba 100644 --- a/apps/emqx_management/i18n/emqx_mgmt_api_publish_i18n.conf +++ b/apps/emqx_management/i18n/emqx_mgmt_api_publish_i18n.conf @@ -63,12 +63,6 @@ result of each individual message in the batch. zh: "MQTT 消息的 QoS" } } - clientid { - desc { - en: "Each message can be published as if it is done on behalf of an MQTT client whos ID can be specified in this field." - zh: "每个消息都可以带上一个 MQTT 客户端 ID,用于模拟 MQTT 客户端的发布行为。" - } - } payload { desc { en: "The MQTT message payload." diff --git a/apps/emqx_management/src/emqx_mgmt_api_publish.erl b/apps/emqx_management/src/emqx_mgmt_api_publish.erl index b7c48a26a..245b56c1d 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_publish.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_publish.erl @@ -102,6 +102,10 @@ fields(message) -> required => false, default => 0 })}, + {clientid, + hoconsc:mk(binary(), #{ + deprecated => {since, "v5.0.14"} + })}, {payload, hoconsc:mk(binary(), #{ desc => ?DESC(payload), From ee89de816cd9aca0b3106fcbfd9fd9c5b80c7685 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Tue, 10 Jan 2023 20:27:42 +0100 Subject: [PATCH 0090/1802] fix: upgrade bridge config converter --- apps/emqx_bridge/src/schema/emqx_bridge_schema.erl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/emqx_bridge/src/schema/emqx_bridge_schema.erl b/apps/emqx_bridge/src/schema/emqx_bridge_schema.erl index b4159e0a0..1ad5d7aba 100644 --- a/apps/emqx_bridge/src/schema/emqx_bridge_schema.erl +++ b/apps/emqx_bridge/src/schema/emqx_bridge_schema.erl @@ -122,7 +122,9 @@ fields(bridges) -> #{ desc => ?DESC("bridges_mqtt"), required => false, - converter => fun emqx_bridge_mqtt_config:upgrade_pre_ee/1 + converter => fun(X, _HoconOpts) -> + emqx_bridge_mqtt_config:upgrade_pre_ee(X) + end } )} ] ++ ee_fields_bridges(); From c5fba85b83c1d85295aa1f143f749b830bb62adb Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Tue, 10 Jan 2023 22:03:38 +0100 Subject: [PATCH 0091/1802] docs: add changelog --- changes/v5.0.14/fix-9716.en.md | 1 + changes/v5.0.14/fix-9716.zh.md | 1 + 2 files changed, 2 insertions(+) create mode 100644 changes/v5.0.14/fix-9716.en.md create mode 100644 changes/v5.0.14/fix-9716.zh.md diff --git a/changes/v5.0.14/fix-9716.en.md b/changes/v5.0.14/fix-9716.en.md new file mode 100644 index 000000000..93d4f1823 --- /dev/null +++ b/changes/v5.0.14/fix-9716.en.md @@ -0,0 +1 @@ +MQTT bridge config compatibility fix. The config created from before v5.0.12 may encounter a compatibility issue after upgraded to v5.0.13. diff --git a/changes/v5.0.14/fix-9716.zh.md b/changes/v5.0.14/fix-9716.zh.md new file mode 100644 index 000000000..f368fe325 --- /dev/null +++ b/changes/v5.0.14/fix-9716.zh.md @@ -0,0 +1 @@ +修复 v5.0.12 之前的 MQTT 桥接配置在 升级到 v5.0.13 后 HTTP API 查询 桥接配置时的一个兼容性问题。 From 52ccd0762e346416608ddd2c6c076fc76550f1ce Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Wed, 11 Jan 2023 10:34:09 +0800 Subject: [PATCH 0092/1802] test: QOS ct failed --- apps/emqx_dashboard/test/emqx_swagger_parameter_SUITE.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx_dashboard/test/emqx_swagger_parameter_SUITE.erl b/apps/emqx_dashboard/test/emqx_swagger_parameter_SUITE.erl index 8a5fe68e7..5d89fb273 100644 --- a/apps/emqx_dashboard/test/emqx_swagger_parameter_SUITE.erl +++ b/apps/emqx_dashboard/test/emqx_swagger_parameter_SUITE.erl @@ -112,7 +112,7 @@ t_in_query(_Config) -> description => <<"QOS">>, in => query, name => qos, - schema => #{enum => [0, 1, 2], type => string} + schema => #{minimum => 0, maximum => 2, type => integer, example => 0} } ], validate("/test/in/query", Expect), From 3692033fb5c08f39176e215fac563d582287cfb0 Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Wed, 11 Jan 2023 11:29:41 +0800 Subject: [PATCH 0093/1802] chore: bump dashboard to v1.1.5 --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 79107cba9..180028899 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ export EMQX_DEFAULT_BUILDER = ghcr.io/emqx/emqx-builder/5.0-26:1.13.4-24.3.4.2-1 export EMQX_DEFAULT_RUNNER = debian:11-slim export OTP_VSN ?= $(shell $(CURDIR)/scripts/get-otp-vsn.sh) export ELIXIR_VSN ?= $(shell $(CURDIR)/scripts/get-elixir-vsn.sh) -export EMQX_DASHBOARD_VERSION ?= v1.1.4 +export EMQX_DASHBOARD_VERSION ?= v1.1.5 export EMQX_EE_DASHBOARD_VERSION ?= e1.0.1-beta.9 export EMQX_REL_FORM ?= tgz export QUICER_DOWNLOAD_FROM_RELEASE = 1 From ba738d7707b4194e6143a3233b38e1f6578690e1 Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Wed, 11 Jan 2023 11:37:06 +0800 Subject: [PATCH 0094/1802] chore: bump to 5.0.14 --- apps/emqx/include/emqx_release.hrl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx/include/emqx_release.hrl b/apps/emqx/include/emqx_release.hrl index 98b17eae7..e749ebc2e 100644 --- a/apps/emqx/include/emqx_release.hrl +++ b/apps/emqx/include/emqx_release.hrl @@ -32,7 +32,7 @@ %% `apps/emqx/src/bpapi/README.md' %% Community edition --define(EMQX_RELEASE_CE, "5.0.13"). +-define(EMQX_RELEASE_CE, "5.0.14"). %% Enterprise edition -define(EMQX_RELEASE_EE, "5.0.0-beta.6"). From d9429bba00d59e3b6f286c4631b69b73002a66f6 Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Wed, 11 Jan 2023 14:44:34 +0800 Subject: [PATCH 0095/1802] fix: bump hocon to 0.35.0(don't log overriding structs) --- apps/emqx/rebar.config | 2 +- mix.exs | 2 +- rebar.config | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/emqx/rebar.config b/apps/emqx/rebar.config index 8d363aeed..82be1cb9a 100644 --- a/apps/emqx/rebar.config +++ b/apps/emqx/rebar.config @@ -29,7 +29,7 @@ {esockd, {git, "https://github.com/emqx/esockd", {tag, "5.9.4"}}}, {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.13.7"}}}, {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.8.1"}}}, - {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.34.0"}}}, + {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.35.0"}}}, {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}}, {recon, {git, "https://github.com/ferd/recon", {tag, "2.5.1"}}}, {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "1.0.0"}}} diff --git a/mix.exs b/mix.exs index b7fc7dac8..45bc820e0 100644 --- a/mix.exs +++ b/mix.exs @@ -68,7 +68,7 @@ defmodule EMQXUmbrella.MixProject do # in conflict by emqtt and hocon {:getopt, "1.0.2", override: true}, {:snabbkaffe, github: "kafka4beam/snabbkaffe", tag: "1.0.0", override: true}, - {:hocon, github: "emqx/hocon", tag: "0.34.0", override: true}, + {:hocon, github: "emqx/hocon", tag: "0.35.0", override: true}, {:emqx_http_lib, github: "emqx/emqx_http_lib", tag: "0.5.1", override: true}, {:esasl, github: "emqx/esasl", tag: "0.2.0"}, {:jose, github: "potatosalad/erlang-jose", tag: "1.11.2"}, diff --git a/rebar.config b/rebar.config index 878c911e5..a61bfe16a 100644 --- a/rebar.config +++ b/rebar.config @@ -68,7 +68,7 @@ , {system_monitor, {git, "https://github.com/ieQu1/system_monitor", {tag, "3.0.3"}}} , {getopt, "1.0.2"} , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "1.0.0"}}} - , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.34.0"}}} + , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.35.0"}}} , {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.5.1"}}} , {esasl, {git, "https://github.com/emqx/esasl", {tag, "0.2.0"}}} , {jose, {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.11.2"}}} From 85a8eff90bf89ac3c8bd4f99fba2c7dacd132acc Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Tue, 10 Jan 2023 21:58:48 +0100 Subject: [PATCH 0096/1802] fix(emqx_resource_manager): do not start when disabled --- apps/emqx_bridge/src/emqx_bridge_resource.erl | 23 ++++++++++--------- .../src/emqx_resource_manager.erl | 4 ++-- changes/v5.0.14/fix-9717.en.md | 1 + changes/v5.0.14/fix-9717.zh.md | 1 + 4 files changed, 16 insertions(+), 13 deletions(-) create mode 100644 changes/v5.0.14/fix-9717.en.md create mode 100644 changes/v5.0.14/fix-9717.zh.md diff --git a/apps/emqx_bridge/src/emqx_bridge_resource.erl b/apps/emqx_bridge/src/emqx_bridge_resource.erl index b28b891a8..18ce354f1 100644 --- a/apps/emqx_bridge/src/emqx_bridge_resource.erl +++ b/apps/emqx_bridge/src/emqx_bridge_resource.erl @@ -132,13 +132,14 @@ create(BridgeId, Conf) -> create(Type, Name, Conf) -> create(Type, Name, Conf, #{}). -create(Type, Name, Conf, Opts) -> +create(Type, Name, Conf, Opts0) -> ?SLOG(info, #{ msg => "create bridge", type => Type, name => Name, config => Conf }), + Opts = override_start_after_created(Conf, Opts0), {ok, _Data} = emqx_resource:create_local( resource_id(Type, Name), <<"emqx_bridge">>, @@ -146,7 +147,7 @@ create(Type, Name, Conf, Opts) -> parse_confs(bin(Type), Name, Conf), Opts ), - maybe_disable_bridge(Type, Name, Conf). + ok. update(BridgeId, {OldConf, Conf}) -> {BridgeType, BridgeName} = parse_bridge_id(BridgeId), @@ -155,7 +156,7 @@ update(BridgeId, {OldConf, Conf}) -> update(Type, Name, {OldConf, Conf}) -> update(Type, Name, {OldConf, Conf}, #{}). -update(Type, Name, {OldConf, Conf}, Opts) -> +update(Type, Name, {OldConf, Conf}, Opts0) -> %% TODO: sometimes its not necessary to restart the bridge connection. %% %% - if the connection related configs like `servers` is updated, we should restart/start @@ -164,6 +165,7 @@ update(Type, Name, {OldConf, Conf}, Opts) -> %% the `method` or `headers` of a WebHook is changed, then the bridge can be updated %% without restarting the bridge. %% + Opts = override_start_after_created(Conf, Opts0), case emqx_map_lib:if_only_to_toggle_enable(OldConf, Conf) of false -> ?SLOG(info, #{ @@ -174,10 +176,10 @@ update(Type, Name, {OldConf, Conf}, Opts) -> }), case recreate(Type, Name, Conf, Opts) of {ok, _} -> - maybe_disable_bridge(Type, Name, Conf); + ok; {error, not_found} -> ?SLOG(warning, #{ - msg => "updating_a_non-exist_bridge_need_create_a_new_one", + msg => "updating_a_non_existing_bridge", type => Type, name => Name, config => Conf @@ -242,12 +244,6 @@ remove(Type, Name, _Conf, _Opts) -> {error, Reason} -> {error, Reason} end. -maybe_disable_bridge(Type, Name, Conf) -> - case maps:get(enable, Conf, true) of - false -> stop(Type, Name); - true -> ok - end. - maybe_clear_certs(TmpPath, #{ssl := SslConf} = Conf) -> %% don't remove the cert files if they are in use case is_tmp_path_conf(TmpPath, SslConf) of @@ -321,3 +317,8 @@ str(Str) when is_list(Str) -> Str. bin(Bin) when is_binary(Bin) -> Bin; bin(Str) when is_list(Str) -> list_to_binary(Str); bin(Atom) when is_atom(Atom) -> atom_to_binary(Atom, utf8). + +override_start_after_created(Config, Opts) -> + Enabled = maps:get(enable, Config, true), + StartAfterCreated = Enabled andalso maps:get(start_after_created, Opts, Enabled), + Opts#{start_after_created => StartAfterCreated}. diff --git a/apps/emqx_resource/src/emqx_resource_manager.erl b/apps/emqx_resource/src/emqx_resource_manager.erl index 8ad3fdd80..8531f1641 100644 --- a/apps/emqx_resource/src/emqx_resource_manager.erl +++ b/apps/emqx_resource/src/emqx_resource_manager.erl @@ -112,7 +112,7 @@ recreate(ResId, ResourceType, NewConfig, Opts) -> end. create_and_return_data(MgrId, ResId, Group, ResourceType, Config, Opts) -> - create(MgrId, ResId, Group, ResourceType, Config, Opts), + _ = create(MgrId, ResId, Group, ResourceType, Config, Opts), {ok, _Group, Data} = lookup(ResId), {ok, Data}. @@ -304,7 +304,7 @@ init({Data, Opts}) -> process_flag(trap_exit, true), %% init the cache so that lookup/1 will always return something insert_cache(Data#data.id, Data#data.group, Data), - case maps:get(start_after_created, Opts, true) of + case maps:get(start_after_created, Opts, ?START_AFTER_CREATED) of true -> {ok, connecting, Data, {next_event, internal, start_resource}}; false -> {ok, stopped, Data} end. diff --git a/changes/v5.0.14/fix-9717.en.md b/changes/v5.0.14/fix-9717.en.md new file mode 100644 index 000000000..9a3b29157 --- /dev/null +++ b/changes/v5.0.14/fix-9717.en.md @@ -0,0 +1 @@ +Prior to this fix, if it always times out when trying to connect a bridge server, it's not possible to change other configs even when the bridge is disabled. diff --git a/changes/v5.0.14/fix-9717.zh.md b/changes/v5.0.14/fix-9717.zh.md new file mode 100644 index 000000000..859d7806f --- /dev/null +++ b/changes/v5.0.14/fix-9717.zh.md @@ -0,0 +1 @@ +修复已禁用的桥接资源服务器连接超时的情况下不能修改其他配置参数的问题。 From fdc0682c3fcd0dbf8f3d9309f84ba2be618ca7be Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Wed, 11 Jan 2023 17:30:45 +0800 Subject: [PATCH 0097/1802] fix: elvis warning --- apps/emqx/src/emqx_session_router.erl | 21 +++++---------- apps/emqx_rule_engine/src/emqx_rule_date.erl | 28 ++++++++++---------- deploy/charts/emqx/Chart.yaml | 4 +-- 3 files changed, 22 insertions(+), 31 deletions(-) diff --git a/apps/emqx/src/emqx_session_router.erl b/apps/emqx/src/emqx_session_router.erl index 94f7fb64d..0d4972e8c 100644 --- a/apps/emqx/src/emqx_session_router.erl +++ b/apps/emqx/src/emqx_session_router.erl @@ -71,24 +71,15 @@ %%-------------------------------------------------------------------- create_router_tab(disc) -> - ok = mria:create_table(?ROUTE_DISC_TAB, [ - {type, bag}, - {rlog_shard, ?ROUTE_SHARD}, - {storage, disc_copies}, - {record_name, route}, - {attributes, record_info(fields, route)}, - {storage_properties, [ - {ets, [ - {read_concurrency, true}, - {write_concurrency, true} - ]} - ]} - ]); + create_table(?ROUTE_DISC_TAB, disc_copies); create_router_tab(ram) -> - ok = mria:create_table(?ROUTE_RAM_TAB, [ + create_table(?ROUTE_RAM_TAB, ram_copies). + +create_table(Tab, Storage) -> + ok = mria:create_table(Tab, [ {type, bag}, {rlog_shard, ?ROUTE_SHARD}, - {storage, ram_copies}, + {storage, Storage}, {record_name, route}, {attributes, record_info(fields, route)}, {storage_properties, [ diff --git a/apps/emqx_rule_engine/src/emqx_rule_date.erl b/apps/emqx_rule_engine/src/emqx_rule_date.erl index a41beb20d..aeb5d7a1b 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_date.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_date.erl @@ -88,20 +88,20 @@ parse_date(TimeUnit, Offset, FormatString, InputString) -> calendar:rfc3339_to_system_time(Str, [{unit, TimeUnit}]). mlist(R) -> - %% %H Shows hour in 24-hour format [15] + %% %H Shows hour in 24-hour format [15] [ {$H, R#result.hour}, - %% %M Displays minutes [00-59] + %% %M Displays minutes [00-59] {$M, R#result.minute}, - %% %S Displays seconds [00-59] + %% %S Displays seconds [00-59] {$S, R#result.second}, - %% %y Displays year YYYY [2021] + %% %y Displays year YYYY [2021] {$y, R#result.year}, - %% %m Displays the number of the month [01-12] + %% %m Displays the number of the month [01-12] {$m, R#result.month}, - %% %d Displays the number of the month [01-12] + %% %d Displays the number of the month [01-12] {$d, R#result.day}, - %% %Z Displays Time zone + %% %Z Displays Time zone {$Z, R#result.zone} ]. @@ -223,20 +223,20 @@ parse_zone(Input) -> mlist1() -> maps:from_list( - %% %H Shows hour in 24-hour format [15] + %% %H Shows hour in 24-hour format [15] [ {$H, fun(Input) -> parse_int_times(2, Input) end}, - %% %M Displays minutes [00-59] + %% %M Displays minutes [00-59] {$M, fun(Input) -> parse_int_times(2, Input) end}, - %% %S Displays seconds [00-59] + %% %S Displays seconds [00-59] {$S, fun(Input) -> parse_second(Input) end}, - %% %y Displays year YYYY [2021] + %% %y Displays year YYYY [2021] {$y, fun(Input) -> parse_int_times(4, Input) end}, - %% %m Displays the number of the month [01-12] + %% %m Displays the number of the month [01-12] {$m, fun(Input) -> parse_int_times(2, Input) end}, - %% %d Displays the number of the month [01-12] + %% %d Displays the number of the month [01-12] {$d, fun(Input) -> parse_int_times(2, Input) end}, - %% %Z Displays Time zone + %% %Z Displays Time zone {$Z, fun(Input) -> parse_zone(Input) end} ] ). diff --git a/deploy/charts/emqx/Chart.yaml b/deploy/charts/emqx/Chart.yaml index 2be2a6324..ae48f9de2 100644 --- a/deploy/charts/emqx/Chart.yaml +++ b/deploy/charts/emqx/Chart.yaml @@ -14,8 +14,8 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. -version: 5.0.13 +version: 5.0.14 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. -appVersion: 5.0.13 +appVersion: 5.0.14 From 50b7ac6a22799f16511d21c2e3a385dedb1abb67 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Tue, 3 Jan 2023 12:55:07 +0800 Subject: [PATCH 0098/1802] feat(prom): support headers for pushing --- .../i18n/emqx_prometheus_schema_i18n.conf | 10 +++++++++ apps/emqx_prometheus/src/emqx_prometheus.erl | 21 ++++++++++++++----- .../src/emqx_prometheus_api.erl | 1 + .../src/emqx_prometheus_schema.erl | 9 ++++++++ .../test/emqx_prometheus_SUITE.erl | 20 ++++++++++++++++++ 5 files changed, 56 insertions(+), 5 deletions(-) diff --git a/apps/emqx_prometheus/i18n/emqx_prometheus_schema_i18n.conf b/apps/emqx_prometheus/i18n/emqx_prometheus_schema_i18n.conf index 7f251ff4b..391637570 100644 --- a/apps/emqx_prometheus/i18n/emqx_prometheus_schema_i18n.conf +++ b/apps/emqx_prometheus/i18n/emqx_prometheus_schema_i18n.conf @@ -24,6 +24,16 @@ emqx_prometheus_schema { zh: """数据推送间隔""" } } + + headers { + desc { + en: """A list of HTTP Headers when pushing to Push Gateway.
+For example, { Authorization = "some-authz-tokens"}""" + zh: """推送到 Push Gateway 的 HTTP Headers 列表。
+例如, { Authorization = "some-authz-tokens"}""" + } + } + enable { desc { en: """Turn Prometheus data pushing on or off""" diff --git a/apps/emqx_prometheus/src/emqx_prometheus.erl b/apps/emqx_prometheus/src/emqx_prometheus.erl index 5424c4e24..623ae43f0 100644 --- a/apps/emqx_prometheus/src/emqx_prometheus.erl +++ b/apps/emqx_prometheus/src/emqx_prometheus.erl @@ -98,8 +98,12 @@ handle_cast(_Msg, State) -> {noreply, State}. handle_info({timeout, Timer, ?TIMER_MSG}, State = #{timer := Timer}) -> - #{interval := Interval, push_gateway_server := Server} = opts(), - PushRes = push_to_push_gateway(Server), + #{ + interval := Interval, + headers := Headers, + push_gateway_server := Server + } = opts(), + PushRes = push_to_push_gateway(Server, Headers), NewTimer = ensure_timer(Interval), NewState = maps:update_with(PushRes, fun(C) -> C + 1 end, 1, State#{timer => NewTimer}), %% Data is too big, hibernate for saving memory and stop system monitor warning. @@ -107,12 +111,19 @@ handle_info({timeout, Timer, ?TIMER_MSG}, State = #{timer := Timer}) -> handle_info(_Msg, State) -> {noreply, State}. -push_to_push_gateway(Uri) -> +push_to_push_gateway(Uri, Headers0) when is_map(Headers0) -> [Name, Ip] = string:tokens(atom_to_list(node()), "@"), Url = lists:concat([Uri, "/metrics/job/", Name, "/instance/", Name, "~", Ip]), Data = prometheus_text_format:format(), - case httpc:request(post, {Url, [], "text/plain", Data}, ?HTTP_OPTIONS, []) of - {ok, {{"HTTP/1.1", 200, "OK"}, _Headers, _Body}} -> + Headers = maps:fold( + fun(K, V, Acc) -> + [{atom_to_list(K), binary_to_list(V)} | Acc] + end, + [], + Headers0 + ), + case httpc:request(post, {Url, Headers, "text/plain", Data}, ?HTTP_OPTIONS, []) of + {ok, {{"HTTP/1.1", 200, _}, _Headers, _Body}} -> ok; Error -> ?SLOG(error, #{ diff --git a/apps/emqx_prometheus/src/emqx_prometheus_api.erl b/apps/emqx_prometheus/src/emqx_prometheus_api.erl index 7466a1fd1..1acdf99a7 100644 --- a/apps/emqx_prometheus/src/emqx_prometheus_api.erl +++ b/apps/emqx_prometheus/src/emqx_prometheus_api.erl @@ -121,6 +121,7 @@ prometheus_config_example() -> enable => true, interval => "15s", push_gateway_server => <<"http://127.0.0.1:9091">>, + headers => #{'header-name' => 'header-value'}, vm_dist_collector => enabled, mnesia_collector => enabled, vm_statistics_collector => enabled, diff --git a/apps/emqx_prometheus/src/emqx_prometheus_schema.erl b/apps/emqx_prometheus/src/emqx_prometheus_schema.erl index 688c9be58..40b9ed6a1 100644 --- a/apps/emqx_prometheus/src/emqx_prometheus_schema.erl +++ b/apps/emqx_prometheus/src/emqx_prometheus_schema.erl @@ -52,6 +52,15 @@ fields("prometheus") -> desc => ?DESC(interval) } )}, + {headers, + ?HOCON( + map(), + #{ + default => #{}, + required => false, + desc => ?DESC(headers) + } + )}, {enable, ?HOCON( boolean(), diff --git a/apps/emqx_prometheus/test/emqx_prometheus_SUITE.erl b/apps/emqx_prometheus/test/emqx_prometheus_SUITE.erl index b9df1103b..effdfc914 100644 --- a/apps/emqx_prometheus/test/emqx_prometheus_SUITE.erl +++ b/apps/emqx_prometheus/test/emqx_prometheus_SUITE.erl @@ -27,6 +27,7 @@ "prometheus {\n" " push_gateway_server = \"http://127.0.0.1:9091\"\n" " interval = \"1s\"\n" + " headers = { Authorization = \"some-authz-tokens\"}\n" " enable = true\n" " vm_dist_collector = enabled\n" " mnesia_collector = enabled\n" @@ -85,6 +86,25 @@ t_collector_no_crash_test(_) -> prometheus_text_format:format(), ok. +t_assert_push(_) -> + meck:new(httpc, [passthrough]), + Self = self(), + AssertPush = fun(Method, Req = {Url, Headers, ContentType, _Data}, HttpOpts, Opts) -> + ?assertEqual(post, Method), + ?assertMatch("http://127.0.0.1:9091/metrics/job/" ++ _, Url), + ?assertEqual([{"Authorization", "some-authz-tokens"}], Headers), + ?assertEqual("text/plain", ContentType), + Self ! pass, + meck:passthrough([Method, Req, HttpOpts, Opts]) + end, + meck:expect(httpc, request, AssertPush), + ?assertMatch(ok, emqx_prometheus_sup:start_child(emqx_prometheus)), + receive + pass -> ok + after 2000 -> + ct:fail(assert_push_request_failed) + end. + t_only_for_coverage(_) -> ?assertEqual("5.0.0", emqx_prometheus_proto_v1:introduced_in()), ok. From b1292d6d4f610e6a455c1665fc157a1381a5768c Mon Sep 17 00:00:00 2001 From: JianBo He Date: Tue, 3 Jan 2023 18:27:41 +0800 Subject: [PATCH 0099/1802] feat(prom): support configuring job_name for pushing mode --- .../i18n/emqx_prometheus_schema_i18n.conf | 19 +++++++++++++++++++ apps/emqx_prometheus/src/emqx_prometheus.erl | 15 ++++++++++++--- .../src/emqx_prometheus_api.erl | 1 + .../src/emqx_prometheus_schema.erl | 10 ++++++++++ .../test/emqx_prometheus_SUITE.erl | 3 ++- 5 files changed, 44 insertions(+), 4 deletions(-) diff --git a/apps/emqx_prometheus/i18n/emqx_prometheus_schema_i18n.conf b/apps/emqx_prometheus/i18n/emqx_prometheus_schema_i18n.conf index 391637570..f25e35219 100644 --- a/apps/emqx_prometheus/i18n/emqx_prometheus_schema_i18n.conf +++ b/apps/emqx_prometheus/i18n/emqx_prometheus_schema_i18n.conf @@ -34,6 +34,25 @@ For example, { Authorization = "some-authz-tokens"}""" } } + job_name { + desc { + en: """Job Name that is pushed to the Push Gateway. Available variables:
+- ${name}: Name of EMQX node.
+- ${host}: Host name of EMQX node.
+For example, when the EMQX node name is emqx@127.0.0.1 then the name variable takes value emqx and the host variable takes value 127.0.0.1.
+ +Default value is: ${name}/instance/${name}~${host} +""" + zh: """推送到 Push Gateway 的 Job 名称。可用变量为:
+- ${name}: EMQX 节点的名称。 +- ${host}: EMQX 节点主机名。 + +例如,当 EMQX 节点名为 emqx@127.0.0.1 则 name 变量的值为 emqx,host 变量的值为 127.0.0.1
+ +默认值为: ${name}/instance/${name}~${host}""" + } + } + enable { desc { en: """Turn Prometheus data pushing on or off""" diff --git a/apps/emqx_prometheus/src/emqx_prometheus.erl b/apps/emqx_prometheus/src/emqx_prometheus.erl index 623ae43f0..91c1f22fc 100644 --- a/apps/emqx_prometheus/src/emqx_prometheus.erl +++ b/apps/emqx_prometheus/src/emqx_prometheus.erl @@ -101,9 +101,10 @@ handle_info({timeout, Timer, ?TIMER_MSG}, State = #{timer := Timer}) -> #{ interval := Interval, headers := Headers, + job_name := JobName, push_gateway_server := Server } = opts(), - PushRes = push_to_push_gateway(Server, Headers), + PushRes = push_to_push_gateway(Server, Headers, JobName), NewTimer = ensure_timer(Interval), NewState = maps:update_with(PushRes, fun(C) -> C + 1 end, 1, State#{timer => NewTimer}), %% Data is too big, hibernate for saving memory and stop system monitor warning. @@ -111,9 +112,17 @@ handle_info({timeout, Timer, ?TIMER_MSG}, State = #{timer := Timer}) -> handle_info(_Msg, State) -> {noreply, State}. -push_to_push_gateway(Uri, Headers0) when is_map(Headers0) -> +push_to_push_gateway(Uri, Headers0, JobName) when is_map(Headers0) -> [Name, Ip] = string:tokens(atom_to_list(node()), "@"), - Url = lists:concat([Uri, "/metrics/job/", Name, "/instance/", Name, "~", Ip]), + JobName1 = emqx_placeholder:preproc_tmpl(JobName), + JobName2 = binary_to_list( + emqx_placeholder:proc_tmpl( + JobName1, + #{<<"name">> => Name, <<"host">> => Ip} + ) + ), + + Url = lists:concat([Uri, "/metrics/job/", JobName2]), Data = prometheus_text_format:format(), Headers = maps:fold( fun(K, V, Acc) -> diff --git a/apps/emqx_prometheus/src/emqx_prometheus_api.erl b/apps/emqx_prometheus/src/emqx_prometheus_api.erl index 1acdf99a7..945c6eba9 100644 --- a/apps/emqx_prometheus/src/emqx_prometheus_api.erl +++ b/apps/emqx_prometheus/src/emqx_prometheus_api.erl @@ -122,6 +122,7 @@ prometheus_config_example() -> interval => "15s", push_gateway_server => <<"http://127.0.0.1:9091">>, headers => #{'header-name' => 'header-value'}, + job_name => <<"${name}/instance/${name}~${host}">>, vm_dist_collector => enabled, mnesia_collector => enabled, vm_statistics_collector => enabled, diff --git a/apps/emqx_prometheus/src/emqx_prometheus_schema.erl b/apps/emqx_prometheus/src/emqx_prometheus_schema.erl index 40b9ed6a1..da65e02c4 100644 --- a/apps/emqx_prometheus/src/emqx_prometheus_schema.erl +++ b/apps/emqx_prometheus/src/emqx_prometheus_schema.erl @@ -61,6 +61,16 @@ fields("prometheus") -> desc => ?DESC(headers) } )}, + {job_name, + ?HOCON( + binary(), + #{ + default => <<"${name}/instance/${name}~${host}">>, + required => true, + desc => ?DESC(job_name) + } + )}, + {enable, ?HOCON( boolean(), diff --git a/apps/emqx_prometheus/test/emqx_prometheus_SUITE.erl b/apps/emqx_prometheus/test/emqx_prometheus_SUITE.erl index effdfc914..77d9902a2 100644 --- a/apps/emqx_prometheus/test/emqx_prometheus_SUITE.erl +++ b/apps/emqx_prometheus/test/emqx_prometheus_SUITE.erl @@ -28,6 +28,7 @@ " push_gateway_server = \"http://127.0.0.1:9091\"\n" " interval = \"1s\"\n" " headers = { Authorization = \"some-authz-tokens\"}\n" + " job_name = \"${name}~${host}\"\n" " enable = true\n" " vm_dist_collector = enabled\n" " mnesia_collector = enabled\n" @@ -91,7 +92,7 @@ t_assert_push(_) -> Self = self(), AssertPush = fun(Method, Req = {Url, Headers, ContentType, _Data}, HttpOpts, Opts) -> ?assertEqual(post, Method), - ?assertMatch("http://127.0.0.1:9091/metrics/job/" ++ _, Url), + ?assertMatch("http://127.0.0.1:9091/metrics/job/test~127.0.0.1", Url), ?assertEqual([{"Authorization", "some-authz-tokens"}], Headers), ?assertEqual("text/plain", ContentType), Self ! pass, From 4493cfafae951c523be9e249961190bcd641bafe Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Wed, 11 Jan 2023 17:34:49 +0800 Subject: [PATCH 0100/1802] chore: Generate changelog for v5.0.14 --- changes/v5.0.14-en.md | 68 +++++++++++++++++++++++++++++++++++++++++++ changes/v5.0.14-zh.md | 64 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 changes/v5.0.14-en.md create mode 100644 changes/v5.0.14-zh.md diff --git a/changes/v5.0.14-en.md b/changes/v5.0.14-en.md new file mode 100644 index 000000000..17ae121cb --- /dev/null +++ b/changes/v5.0.14-en.md @@ -0,0 +1,68 @@ +# v5.0.14 + +## Enhancements + +- [#8329](https://github.com/emqx/emqx/pull/8329) The MongoDB library has been upgraded to support MongoDB 5.1+ + +- [#9593](https://github.com/emqx/emqx/pull/9593) Obfuscated sensitive data in the response when querying `bridges` information by API. + +- [#9614](https://github.com/emqx/emqx/pull/9614) Make possible to configure `host:port` from environment variables without quotes. + Prior to this change, when overriding a `host:port` config value from environment variable, one has to quote it as: + `env EMQX_BRIDGES__MQTT__XYZ__SERVER='"localhost:1883"'`. + Now it's possible to set it without quote as `env EMQX_BRIDGES__MQTT__XYZ__SERVER='localhost:1883'`. + +- [#9642](https://github.com/emqx/emqx/pull/9642) Deprecates `enable_batch` and `enable_queue` options for bridges/resources. After this change, queuing is always enabled for bridges, and batching is controlled by the `batch_size` option: `batch_size > 1` means batching will be enabled. + +- [#9671](https://github.com/emqx/emqx/pull/9671) Implement sliding window average metrics. + +- [#9674](https://github.com/emqx/emqx/pull/9674) Made rule engine behavior more consistent with bridge behavior regarding metrics: if a rule engine is disabled, its metrics are now reset + +- [#9675](https://github.com/emqx/emqx/pull/9675) HTTP client library `ehttpc` upgraded from `0.4.2` to `0.4.3`. + Library `eredis_cluster` which manages clients to redis clusters upgraded from `0.7.1` to `0.7.5`. + +- [#9713](https://github.com/emqx/emqx/pull/9713) Introduce `api_key.bootstrap_file` to initialize the api key at boot time. + Deprecate `dashboard.bootstrap_users_file`. + Limit the maximum number of api keys to 100 instead of 30. + +## Bug fixes + +- [#8648](https://github.com/emqx/emqx/pull/8648) When deleting a non-existing bridge the server gave a success response. This has been fixed so that the server instead gives an error response when the user attempts to delete a non-existing bridge. + +- [#9637](https://github.com/emqx/emqx/pull/9637) Fix the expiry_interval fields of the clients HTTP API to measure in seconds. + +- [#9638](https://github.com/emqx/emqx/pull/9638) Fix the problem of data loss and bad match when the MySQL driver is disconnected. + +- [#9641](https://github.com/emqx/emqx/pull/9641) Fix an issue where testing the GCP PubSub could leak memory, and an issue where its JWT token would fail to refresh a second time. + +- [#9642](https://github.com/emqx/emqx/pull/9642) Fix some issues that could lead to wrong bridge metrics. + Fix and issue that could lead to message loss and wrong metrics with Kafka Producer bridge when Kafka or the connection to it is down. + Fix some issues that could lead to the same message being delivered more than once when using batching for bridges and when the batch was retried. + +- [#9667](https://github.com/emqx/emqx/pull/9667) Remove possibility to set `clientid` for `/publish` and `/publish/bulk` HTTP APIs. This is to reduce the risk for security confusion. + +- [#9687](https://github.com/emqx/emqx/pull/9687) Fix the problem that sending messages to data-bridges failed because of incorrect handling of some data-bridges without `local_topic` field configured. + Before this change, if some bridges have configured the `local_topic` field but others have not, a `function_clause` error will occur when forwarding messages to the data-bridges. + +- [#9689](https://github.com/emqx/emqx/pull/9689) Fix handling of HTTP authorization result when a request failure (e.g.: HTTP resource is down) would cause a `function_clause` error. + +- [#9703](https://github.com/emqx/emqx/pull/9703) Set the default value of the `qos` field of the HTTP API `/clients/:clientid/subscribe` to 0. + Before this fix, the `qos` field have no default value, which leads to a `function_clause` error + when querying this API. + +- [#9705](https://github.com/emqx/emqx/pull/9705) Remove the default value of Webhook. + Before this repair, the default value of the `body` field of Webhook is `${payload}`, + but there is no `payload` field in the available fields of other events except message + publishing in the rule, so in this case, the webhook will send a string with the + message body as "undefined" to the HTTP service. + This fix removes the default value of the `body` field. When the `body` field is + not configured, Webhook will send all available fields of the current event in + the format of JSON object. + +- [#9712](https://github.com/emqx/emqx/pull/9712) Fixed the problem of '404 Not Found' when calling the HTTP API '/clients/:clientid/subscribe/bulk' + from the plug-ins and data-bridges on handling the 'client.connected' event. + +- [#9714](https://github.com/emqx/emqx/pull/9714) Fix `/mqtt/auto_subscribe` API's bad swagger schema, and make sure swagger always checks if the schema is correct. + +- [#9716](https://github.com/emqx/emqx/pull/9716) MQTT bridge config compatibility fix. The config created from before v5.0.12 may encounter a compatibility issue after upgraded to v5.0.13. + +- [#9717](https://github.com/emqx/emqx/pull/9717) Prior to this fix, if it always times out when trying to connect a bridge server, it's not possible to change other configs even when the bridge is disabled. diff --git a/changes/v5.0.14-zh.md b/changes/v5.0.14-zh.md new file mode 100644 index 000000000..4eb510a43 --- /dev/null +++ b/changes/v5.0.14-zh.md @@ -0,0 +1,64 @@ +# v5.0.14 + +## 增强 + +- [#8329](https://github.com/emqx/emqx/pull/8329) MongoDB 的驱动现在已经升级到 MongoDB 5.1+ 了。 + +- [#9593](https://github.com/emqx/emqx/pull/9593) 通过 API 查询 `bridges` 信息时将混淆响应中的敏感数据。 + +- [#9614](https://github.com/emqx/emqx/pull/9614) 允许环境变量重载 `host:port` 值时不使用引号。 + 在此修复前,环境变量中使用 `host:port` 这种配置时,用户必须使用引号,例如: + `env EMQX_BRIDGES__MQTT__XYZ__SERVER='"localhost:1883"'`。 + 此修复后,可以不使用引号,例如 `env EMQX_BRIDGES__MQTT__XYZ__SERVER='localhost:1883'`。 + +- [#9642](https://github.com/emqx/emqx/pull/9642) 废弃了桥接的 `enable_batch` 和 `enable_queue` 配置项 。在这一改变之后,桥接的工作进程总是启用缓存队列,而批处理由 `batch_size` 选项控制:`batch_size > 1` 则意味着启用批处理。 + +- [#9671](https://github.com/emqx/emqx/pull/9671) 实施滑动窗口平均度量。 + +- [#9674](https://github.com/emqx/emqx/pull/9674) 使得规则引擎的行为与桥梁的指标行为更加一致:如果一个规则引擎被禁用,其指标现在会被重置。 + +- [#9675](https://github.com/emqx/emqx/pull/9675) HTTP 客户端库 `ehttpc` 从 `0.4.2` 升级到 `0.4.3` + Redis cluster 客户端库 `eredis_cluster` 从 `0.7.1` 升级到 `0.7.5`. + +- [#9713](https://github.com/emqx/emqx/pull/9713) 引入 `api_key.bootstrap_file`,用于启动时初始化api密钥。 + 废弃 `dashboard.boostrap_users_file`。 + 将 API 密钥的最大数量限制提升为 100(原来为30)。 + +## 修复 + +- [#8648](https://github.com/emqx/emqx/pull/8648) 修复了当通过 API 删除一个不存在的桥接时,服务器会返回操作成功的问题,现在将会返回操作失败的信息。 + +- [#9637](https://github.com/emqx/emqx/pull/9637) 修复 clients HTTP API 下的 expiry_interval 字段的时间单位为秒。 + +- [#9638](https://github.com/emqx/emqx/pull/9638) 修复 MySQL 驱动断开连接时出现的数据丢失和匹配错误的问题。 + +- [#9641](https://github.com/emqx/emqx/pull/9641) 修复了测试GCP PubSub可能泄露内存的问题,以及其JWT令牌第二次刷新失败的问题。 + +- [#9642](https://github.com/emqx/emqx/pull/9642) 修复一些可能导致错误桥接指标的问题。 + 修复当Kafka或其连接中断时,可能导致Kafka Producer桥的消息丢失和错误指标的问题。 + 修复一些问题,这些问题可能导致在为桥接使用批处理时,同一消息被多次传递,以及批处理被重试时。 + +- [#9667](https://github.com/emqx/emqx/pull/9667) 从 HTTP API /publish 和 /publish/bulk 中移除 clientid, 降低安全风险 + +- [#9687](https://github.com/emqx/emqx/pull/9687) 修复由于某些数据桥接未配置 `local_topic` 字段,导致的所有数据桥接无法发送消息。 + 在此改动之前,如果有些桥接设置了 `local_topic` 字段而有些没有设置,数据桥接转发消息时会出现 `function_clause` 的错误。 + +- [#9689](https://github.com/emqx/emqx/pull/9689) 修正当请求失败(如:HTTP资源关闭)会导致`function_clause`错误时对HTTP授权结果的处理。 + +- [#9703](https://github.com/emqx/emqx/pull/9703) 将 HTTP 接口 `/clients/:clientid/subscribe` 的 `qos` 字段的默认值设置为 0。 + 在此修复之前,`qos` 字段没有默认值,调用订阅接口的时候将导致 `function_clause` 错误。 + +- [#9705](https://github.com/emqx/emqx/pull/9705) 删除 Webhook 的默认值。 + 在此修复之前,Webhook 的 `body` 字段的默认值为 `${payload}`,但规则中除了消息发布之外的其他事件的可用字段中 + 都没有 `payload` 字段,所以这种情况下 Webhook 将发送消息正文为 "undefined" 的字符串到 HTTP 服务。 + 此修复移除了 `body` 字段的默认值,当未配置 `body` 字段的时候,Webhook 将以 JSON object 的格式发送 + 当前事件的全部可用字段。 + +- [#9712](https://github.com/emqx/emqx/pull/9712) 修复了监听 `client.connected` 事件的插件和数据桥接在调用 `/clients/:clientid/subscribe/bulk` + HTTP 接口时报 `404 Not Found` 的问题。 + +- [#9714](https://github.com/emqx/emqx/pull/9714) 修复 `/mqtt/auto_subscribe` API 错误的 swagger 格式,并且保证 swagger 总是检查格式是否正确。 + +- [#9716](https://github.com/emqx/emqx/pull/9716) 修复 v5.0.12 之前的 MQTT 桥接配置在 升级到 v5.0.13 后 HTTP API 查询 桥接配置时的一个兼容性问题。 + +- [#9717](https://github.com/emqx/emqx/pull/9717) 修复已禁用的桥接资源服务器连接超时的情况下不能修改其他配置参数的问题。 From 371b42eef295ebcb08a9c9fd88e9430ecc187fbb Mon Sep 17 00:00:00 2001 From: JianBo He Date: Wed, 11 Jan 2023 17:46:00 +0800 Subject: [PATCH 0101/1802] chore: update changes --- changes/v5.0.15/feat-9722.en.md | 3 +++ changes/v5.0.15/feat-9722.zh.md | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 changes/v5.0.15/feat-9722.en.md create mode 100644 changes/v5.0.15/feat-9722.zh.md diff --git a/changes/v5.0.15/feat-9722.en.md b/changes/v5.0.15/feat-9722.en.md new file mode 100644 index 000000000..b86f37b83 --- /dev/null +++ b/changes/v5.0.15/feat-9722.en.md @@ -0,0 +1,3 @@ +Add the following configuration options for Pushing metrics to Prometheus Push Gateway: +- `headers`: Allows custom HTTP request headers. +- `job_name`: allows to customize the name of the Job pushed to Push Gateway. diff --git a/changes/v5.0.15/feat-9722.zh.md b/changes/v5.0.15/feat-9722.zh.md new file mode 100644 index 000000000..a806cb1de --- /dev/null +++ b/changes/v5.0.15/feat-9722.zh.md @@ -0,0 +1,3 @@ +为 Prometheus 推送到 Push Gateway 新增以下配置项: +- `headers`:允许自定义 HTTP 请求头。 +- `job_name`:允许自定义推送到 Push Gateway 的 Job 名称。 From 9f6df2775960ac096a00d573962a17427938a537 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Wed, 11 Jan 2023 17:46:57 +0800 Subject: [PATCH 0102/1802] chore: bump app.src vsn --- apps/emqx_prometheus/src/emqx_prometheus.app.src | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx_prometheus/src/emqx_prometheus.app.src b/apps/emqx_prometheus/src/emqx_prometheus.app.src index d95c89c3b..31f8cbfaf 100644 --- a/apps/emqx_prometheus/src/emqx_prometheus.app.src +++ b/apps/emqx_prometheus/src/emqx_prometheus.app.src @@ -2,7 +2,7 @@ {application, emqx_prometheus, [ {description, "Prometheus for EMQX"}, % strict semver, bump manually! - {vsn, "5.0.3"}, + {vsn, "5.0.4"}, {modules, []}, {registered, [emqx_prometheus_sup]}, {applications, [kernel, stdlib, prometheus, emqx]}, From 940d9a135ad92c7053dac48deef75138cdabee66 Mon Sep 17 00:00:00 2001 From: Rory Z Date: Wed, 11 Jan 2023 19:25:13 +0800 Subject: [PATCH 0103/1802] chore(CI): fix error docker hub token --- .github/workflows/build_and_push_docker_images.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_and_push_docker_images.yaml b/.github/workflows/build_and_push_docker_images.yaml index 76238c75f..6e6ef1e0c 100644 --- a/.github/workflows/build_and_push_docker_images.yaml +++ b/.github/workflows/build_and_push_docker_images.yaml @@ -186,8 +186,8 @@ jobs: if: matrix.registry == 'docker.io' uses: peter-evans/dockerhub-description@v3 with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_PASSWORD }} + username: ${{ secrets.DOCKER_HUB_USER }} + password: ${{ secrets.DOCKER_HUB_TOKEN }} repository: "emqx/${{ needs.prepare.outputs.BUILD_PROFILE }}" readme-filepath: ./source/deploy/docker/README.md short-description: "The most scalable open-source MQTT broker for IoT, IIoT, connected vehicles, and more." From 48e1ba48322c818c67786de1951bb0a721b202ee Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Tue, 6 Dec 2022 16:47:55 -0300 Subject: [PATCH 0104/1802] feat(docs): add tags to schemas This'll allow us to split the generated `schema.json` file into subsections for better documentation navigation. --- apps/emqx/src/emqx_schema.erl | 2 +- apps/emqx_authn/src/emqx_authn_schema.erl | 4 ++++ .../enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl | 4 ++++ apps/emqx_authn/src/simple_authn/emqx_authn_http.erl | 4 ++++ apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl | 4 ++++ apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl | 4 ++++ apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl | 4 ++++ apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl | 4 ++++ apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl | 4 ++++ apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl | 4 ++++ apps/emqx_authz/src/emqx_authz_schema.erl | 4 ++++ apps/emqx_bridge/src/schema/emqx_bridge_schema.erl | 5 ++++- apps/emqx_conf/src/emqx_conf_schema.erl | 7 ++++++- apps/emqx_gateway/src/emqx_gateway_schema.erl | 5 ++++- apps/emqx_rule_engine/src/emqx_rule_engine_schema.erl | 4 ++++ lib-ee/emqx_license/src/emqx_license_schema.erl | 5 ++++- 16 files changed, 63 insertions(+), 5 deletions(-) diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 4cd78b575..f113089dd 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -2342,7 +2342,7 @@ authentication(Which) -> undefined -> hoconsc:array(typerefl:map()); Module -> Module:root_type() end, - %% It is a lazy type because when handing runtime update requests + %% It is a lazy type because when handling runtime update requests %% the config is not checked by emqx_schema, but by the injected schema Type = hoconsc:lazy(Type0), #{ diff --git a/apps/emqx_authn/src/emqx_authn_schema.erl b/apps/emqx_authn/src/emqx_authn_schema.erl index 88d8955c5..f40e759f0 100644 --- a/apps/emqx_authn/src/emqx_authn_schema.erl +++ b/apps/emqx_authn/src/emqx_authn_schema.erl @@ -22,6 +22,7 @@ -export([ common_fields/0, roots/0, + tags/0, fields/1, authenticator_type/0, authenticator_type_without_scram/0, @@ -32,6 +33,9 @@ roots() -> []. +tags() -> + [<<"Authentication">>]. + common_fields() -> [{enable, fun enable/1}]. diff --git a/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl b/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl index ba13bd069..ac39e2cda 100644 --- a/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl +++ b/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl @@ -25,6 +25,7 @@ -export([ namespace/0, + tags/0, roots/0, fields/1, desc/1 @@ -105,6 +106,9 @@ mnesia(boot) -> namespace() -> "authn-scram-builtin_db". +tags() -> + [<<"Authentication">>]. + roots() -> [?CONF_NS]. fields(?CONF_NS) -> diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl index 0a9aaa825..faa06b71a 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl @@ -26,6 +26,7 @@ -export([ namespace/0, + tags/0, roots/0, fields/1, desc/1, @@ -51,6 +52,9 @@ namespace() -> "authn-http". +tags() -> + [<<"Authentication">>]. + roots() -> [ {?CONF_NS, diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl index 5709a1fe7..1c44b4d1f 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl @@ -25,6 +25,7 @@ -export([ namespace/0, + tags/0, roots/0, fields/1, desc/1 @@ -44,6 +45,9 @@ namespace() -> "authn-jwt". +tags() -> + [<<"Authentication">>]. + roots() -> [ {?CONF_NS, diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl index e915744e1..7c51644b7 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl @@ -26,6 +26,7 @@ -export([ namespace/0, + tags/0, roots/0, fields/1, desc/1 @@ -107,6 +108,9 @@ mnesia(boot) -> namespace() -> "authn-builtin_db". +tags() -> + [<<"Authentication">>]. + roots() -> [?CONF_NS]. fields(?CONF_NS) -> diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl index 3fac0ed7d..3f140a8eb 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl @@ -25,6 +25,7 @@ -export([ namespace/0, + tags/0, roots/0, fields/1, desc/1 @@ -44,6 +45,9 @@ namespace() -> "authn-mongodb". +tags() -> + [<<"Authentication">>]. + roots() -> [ {?CONF_NS, diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl index 68913443f..ffce42bb3 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl @@ -27,6 +27,7 @@ -export([ namespace/0, + tags/0, roots/0, fields/1, desc/1 @@ -46,6 +47,9 @@ namespace() -> "authn-mysql". +tags() -> + [<<"Authentication">>]. + roots() -> [?CONF_NS]. fields(?CONF_NS) -> diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl index 1cadf9c56..2d7974301 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl @@ -26,6 +26,7 @@ -export([ namespace/0, + tags/0, roots/0, fields/1, desc/1 @@ -50,6 +51,9 @@ namespace() -> "authn-postgresql". +tags() -> + [<<"Authentication">>]. + roots() -> [?CONF_NS]. fields(?CONF_NS) -> diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl index 0c8fedfb5..12b7422b5 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl @@ -25,6 +25,7 @@ -export([ namespace/0, + tags/0, roots/0, fields/1, desc/1 @@ -44,6 +45,9 @@ namespace() -> "authn-redis". +tags() -> + [<<"Authentication">>]. + roots() -> [ {?CONF_NS, diff --git a/apps/emqx_authz/src/emqx_authz_schema.erl b/apps/emqx_authz/src/emqx_authz_schema.erl index a684ae6ba..e2da9b41e 100644 --- a/apps/emqx_authz/src/emqx_authz_schema.erl +++ b/apps/emqx_authz/src/emqx_authz_schema.erl @@ -33,6 +33,7 @@ -export([ namespace/0, roots/0, + tags/0, fields/1, validations/0, desc/1 @@ -65,6 +66,9 @@ type_names() -> namespace() -> authz. +tags() -> + [<<"Authorization">>]. + %% @doc authorization schema is not exported %% but directly used by emqx_schema roots() -> []. diff --git a/apps/emqx_bridge/src/schema/emqx_bridge_schema.erl b/apps/emqx_bridge/src/schema/emqx_bridge_schema.erl index 1ad5d7aba..845c1ef90 100644 --- a/apps/emqx_bridge/src/schema/emqx_bridge_schema.erl +++ b/apps/emqx_bridge/src/schema/emqx_bridge_schema.erl @@ -20,7 +20,7 @@ -import(hoconsc, [mk/2, ref/2]). --export([roots/0, fields/1, desc/1, namespace/0]). +-export([roots/0, fields/1, desc/1, namespace/0, tags/0]). -export([ get_response/0, @@ -104,6 +104,9 @@ metrics_status_fields() -> namespace() -> "bridge". +tags() -> + [<<"Bridge">>]. + roots() -> [bridges]. fields(bridges) -> diff --git a/apps/emqx_conf/src/emqx_conf_schema.erl b/apps/emqx_conf/src/emqx_conf_schema.erl index a7b388964..71d2ab3fd 100644 --- a/apps/emqx_conf/src/emqx_conf_schema.erl +++ b/apps/emqx_conf/src/emqx_conf_schema.erl @@ -38,7 +38,9 @@ cipher/0 ]). --export([namespace/0, roots/0, fields/1, translations/0, translation/1, validations/0, desc/1]). +-export([ + namespace/0, roots/0, fields/1, translations/0, translation/1, validations/0, desc/1, tags/0 +]). -export([conf_get/2, conf_get/3, keys/2, filter/1]). %% Static apps which merge their configs into the merged emqx.conf @@ -67,6 +69,9 @@ %% root config should not have a namespace namespace() -> undefined. +tags() -> + [<<"EMQX">>]. + roots() -> PtKey = ?EMQX_AUTHENTICATION_SCHEMA_MODULE_PT_KEY, case persistent_term:get(PtKey, undefined) of diff --git a/apps/emqx_gateway/src/emqx_gateway_schema.erl b/apps/emqx_gateway/src/emqx_gateway_schema.erl index e89280f14..804e1f862 100644 --- a/apps/emqx_gateway/src/emqx_gateway_schema.erl +++ b/apps/emqx_gateway/src/emqx_gateway_schema.erl @@ -49,12 +49,15 @@ ]). -elvis([{elvis_style, dont_repeat_yourself, disable}]). --export([namespace/0, roots/0, fields/1, desc/1]). +-export([namespace/0, roots/0, fields/1, desc/1, tags/0]). -export([proxy_protocol_opts/0]). namespace() -> gateway. +tags() -> + [<<"Gateway">>]. + roots() -> [gateway]. fields(gateway) -> diff --git a/apps/emqx_rule_engine/src/emqx_rule_engine_schema.erl b/apps/emqx_rule_engine/src/emqx_rule_engine_schema.erl index cbe7dae82..d6913cbc6 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_engine_schema.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_engine_schema.erl @@ -23,6 +23,7 @@ -export([ namespace/0, + tags/0, roots/0, fields/1, desc/1, @@ -33,6 +34,9 @@ namespace() -> rule_engine. +tags() -> + [<<"Rule Engine">>]. + roots() -> ["rule_engine"]. fields("rule_engine") -> diff --git a/lib-ee/emqx_license/src/emqx_license_schema.erl b/lib-ee/emqx_license/src/emqx_license_schema.erl index bde4ed076..00c253ec8 100644 --- a/lib-ee/emqx_license/src/emqx_license_schema.erl +++ b/lib-ee/emqx_license/src/emqx_license_schema.erl @@ -13,7 +13,7 @@ -behaviour(hocon_schema). --export([roots/0, fields/1, validations/0, desc/1]). +-export([roots/0, fields/1, validations/0, desc/1, tags/0]). -export([ default_license/0, @@ -31,6 +31,9 @@ roots() -> )} ]. +tags() -> + [<<"License">>]. + fields(key_license) -> [ {key, #{ From a9d0143d5fe0dd94629b9fc1b16e112c2dd7ed6d Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Wed, 7 Dec 2022 09:15:42 -0300 Subject: [PATCH 0105/1802] chore(license): change `key` type to `binary()` So that the `raw_default` field in the schema JSON renders properly. --- bin/nodetool | 11 +++++++++-- lib-ee/emqx_license/src/emqx_license_schema.erl | 12 +++++++----- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/bin/nodetool b/bin/nodetool index b4f0a0183..9a5d5e069 100755 --- a/bin/nodetool +++ b/bin/nodetool @@ -24,12 +24,19 @@ main(Args) -> ["hocon" | Rest] -> %% forward the call to hocon_cli hocon_cli:main(Rest); - ["check_license_key", Key] -> - check_license(#{key => list_to_binary(Key)}); + ["check_license_key", Key0] -> + Key = cleanup_key(Key0), + check_license(#{key => Key}); _ -> do(Args) end. +%% the key is a string (list) representation of a binary, so we need +%% to remove the leading and trailing angle brackets. +cleanup_key(Str0) -> + Str1 = iolist_to_binary(string:replace(Str0, "<<", "", leading)), + iolist_to_binary(string:replace(Str1, ">>", "", trailing)). + do(Args) -> ok = do_with_halt(Args, "mnesia_dir", fun create_mnesia_dir/2), ok = do_with_halt(Args, "chkconfig", fun("-config", X) -> chkconfig(X) end), diff --git a/lib-ee/emqx_license/src/emqx_license_schema.erl b/lib-ee/emqx_license/src/emqx_license_schema.erl index 00c253ec8..9d16f697c 100644 --- a/lib-ee/emqx_license/src/emqx_license_schema.erl +++ b/lib-ee/emqx_license/src/emqx_license_schema.erl @@ -37,7 +37,7 @@ tags() -> fields(key_license) -> [ {key, #{ - type => string(), + type => binary(), default => default_license(), %% so it's not logged sensitive => true, @@ -85,7 +85,9 @@ check_license_watermark(Conf) -> %% NOTE: when updating a new key, the schema doc in emqx_license_schema_i18n.conf %% should be updated accordingly default_license() -> - "MjIwMTExCjAKMTAKRXZhbHVhdGlvbgpjb250YWN0QGVtcXguaW8KZ" - "GVmYXVsdAoyMDIzMDEwOQoxODI1CjEwMAo=.MEUCIG62t8W15g05f" - "1cKx3tA3YgJoR0dmyHOPCdbUxBGxgKKAiEAhHKh8dUwhU+OxNEaOn" - "8mgRDtiT3R8RZooqy6dEsOmDI=". + << + "MjIwMTExCjAKMTAKRXZhbHVhdGlvbgpjb250YWN0QGVtcXguaW8KZ" + "GVmYXVsdAoyMDIzMDEwOQoxODI1CjEwMAo=.MEUCIG62t8W15g05f" + "1cKx3tA3YgJoR0dmyHOPCdbUxBGxgKKAiEAhHKh8dUwhU+OxNEaOn" + "8mgRDtiT3R8RZooqy6dEsOmDI=" + >>. From e6a8be1ff612c770bf39f13109c78f93b6450fd7 Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Fri, 16 Dec 2022 10:02:40 -0300 Subject: [PATCH 0106/1802] docs: fix closing tag --- apps/emqx/i18n/emqx_schema_i18n.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx/i18n/emqx_schema_i18n.conf b/apps/emqx/i18n/emqx_schema_i18n.conf index 750c0c2cd..045b06da0 100644 --- a/apps/emqx/i18n/emqx_schema_i18n.conf +++ b/apps/emqx/i18n/emqx_schema_i18n.conf @@ -2050,7 +2050,7 @@ base_listener_enable_authn { Set true (default) to enable client authentication on this listener, the authentication process goes through the configured authentication chain. When set to false to allow any clients with or without authentication information such as username or password to log in. -When set to quick_deny_anonymous, it behaves like when set to true but clients will be +When set to quick_deny_anonymous, it behaves like when set to true but clients will be denied immediately without going through any authenticators if username is not provided. This is useful to fence off anonymous clients early. """ From d5a462486eb2936eba10b55085b654fcb92ff331 Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Fri, 16 Dec 2022 14:27:00 -0300 Subject: [PATCH 0107/1802] feat: tag another emqx schema as `emqx` --- apps/emqx/src/emqx_schema.erl | 5 ++++- apps/emqx_authn/src/emqx_authn.app.src | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index f113089dd..ed7e0a016 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -111,7 +111,7 @@ comma_separated_atoms/0 ]). --export([namespace/0, roots/0, roots/1, fields/1, desc/1]). +-export([namespace/0, roots/0, roots/1, fields/1, desc/1, tags/0]). -export([conf_get/2, conf_get/3, keys/2, filter/1]). -export([server_ssl_opts_schema/2, client_ssl_opts_schema/1, ciphers_schema/1]). -export([sc/2, map/2]). @@ -120,6 +120,9 @@ namespace() -> broker. +tags() -> + [<<"EMQX">>]. + roots() -> %% TODO change config importance to a field metadata roots(high) ++ roots(medium) ++ roots(low). diff --git a/apps/emqx_authn/src/emqx_authn.app.src b/apps/emqx_authn/src/emqx_authn.app.src index ea21e5bdc..7f01d94c0 100644 --- a/apps/emqx_authn/src/emqx_authn.app.src +++ b/apps/emqx_authn/src/emqx_authn.app.src @@ -1,7 +1,7 @@ %% -*- mode: erlang -*- {application, emqx_authn, [ {description, "EMQX Authentication"}, - {vsn, "0.1.11"}, + {vsn, "0.1.12"}, {modules, []}, {registered, [emqx_authn_sup, emqx_authn_registry]}, {applications, [kernel, stdlib, emqx_resource, emqx_connector, ehttpc, epgsql, mysql, jose]}, From 8ad82881959c97623ff152206645ed44339ea2c7 Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Mon, 9 Jan 2023 17:11:06 +0100 Subject: [PATCH 0108/1802] feat: report error in create_dry_run --- apps/emqx_bridge/src/emqx_bridge_api.erl | 45 +---------------- .../test/emqx_bridge_api_SUITE.erl | 49 +++++++++++++------ .../src/emqx_resource_manager.erl | 11 +++-- 3 files changed, 43 insertions(+), 62 deletions(-) diff --git a/apps/emqx_bridge/src/emqx_bridge_api.erl b/apps/emqx_bridge/src/emqx_bridge_api.erl index 851089acb..6de7d8695 100644 --- a/apps/emqx_bridge/src/emqx_bridge_api.erl +++ b/apps/emqx_bridge/src/emqx_bridge_api.erl @@ -485,7 +485,7 @@ schema("/bridges_probe") -> RequestMeta = #{module => ?MODULE, method => post, path => "/bridges_probe"}, case emqx_dashboard_swagger:filter_check_request_and_translate_body(Request, RequestMeta) of {ok, #{body := #{<<"type">> := ConnType} = Params}} -> - case do_probe(ConnType, maps:remove(<<"type">>, Params)) of + case emqx_bridge_resource:create_dry_run(ConnType, maps:remove(<<"type">>, Params)) of ok -> {204}; {error, Error} -> @@ -495,49 +495,6 @@ schema("/bridges_probe") -> BadRequest end. -do_probe(ConnType, Params) -> - case test_connection(host_and_port(ConnType, Params)) of - ok -> - emqx_bridge_resource:create_dry_run(ConnType, Params); - Error -> - Error - end. - -host_and_port(mqtt, #{<<"server">> := Server}) -> - case string:split(Server, ":") of - [Host, Port] -> {Host, list_to_integer(Port)}; - _Other -> error(invalid_server, Server) - end; -host_and_port(webhook, #{<<"url">> := Url}) -> - {BaseUrl, _Path} = parse_url(Url), - {ok, #{host := Host, port := Port}} = emqx_http_lib:uri_parse(BaseUrl), - {Host, Port}; -host_and_port(_Unknown, _) -> - undefined. - -%% [TODO] remove in EMQX-8588 when resource manager handles things more elegantly -test_connection(undefined) -> - %% be friendly, it might fail later on with a 'timeout' error. - ok; -test_connection({Host, Port}) -> - case gen_tcp:connect(Host, Port, [], 5000) of - {ok, TestSocket} -> gen_tcp:close(TestSocket); - Error -> Error - end. - -parse_url(Url) -> - case string:split(Url, "//", leading) of - [Scheme, UrlRem] -> - case string:split(UrlRem, "/", leading) of - [HostPort, Path] -> - {iolist_to_binary([Scheme, "//", HostPort]), Path}; - [HostPort] -> - {iolist_to_binary([Scheme, "//", HostPort]), <<>>} - end; - [Url] -> - error({invalid_url, Url}) - end. - lookup_from_all_nodes(BridgeType, BridgeName, SuccCode) -> Nodes = mria_mnesia:running_nodes(), case is_ok(emqx_bridge_proto_v1:lookup_from_all_nodes(Nodes, BridgeType, BridgeName)) of diff --git a/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl b/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl index a32019e41..a77da7544 100644 --- a/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl +++ b/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl @@ -33,13 +33,21 @@ ) ) ). --define(HTTP_BRIDGE(URL, TYPE, NAME), #{ +-define(BRIDGE(NAME, TYPE), #{ + <<"ssl">> => #{<<"enable">> => false}, <<"type">> => TYPE, - <<"name">> => NAME, + <<"name">> => NAME +}). +-define(MQTT_BRIDGE(SERVER), ?BRIDGE(<<"mqtt_egress_test_bridge">>, <<"mqtt">>)#{ + <<"server">> => SERVER, + <<"username">> => <<"user1">>, + <<"password">> => <<"">>, + <<"proto_ver">> => <<"v5">> +}). +-define(HTTP_BRIDGE(URL, TYPE, NAME), ?BRIDGE(NAME, TYPE)#{ <<"url">> => URL, <<"local_topic">> => <<"emqx_webhook/#">>, <<"method">> => <<"post">>, - <<"ssl">> => #{<<"enable">> => false}, <<"body">> => <<"${payload}">>, <<"headers">> => #{ <<"content-type">> => <<"application/json">> @@ -596,16 +604,6 @@ t_with_redact_update(_Config) -> ?assertEqual(Password, Value), ok. --define(MQTT_BRIDGE(Server), #{ - <<"server">> => Server, - <<"username">> => <<"user1">>, - <<"password">> => <<"">>, - <<"proto_ver">> => <<"v5">>, - <<"ssl">> => #{<<"enable">> => false}, - <<"type">> => <<"mqtt">>, - <<"name">> => <<"mqtt_egress_test_bridge">> -}). - t_bridges_probe(Config) -> Port = ?config(port, Config), URL = ?URL(Port, "some_path"), @@ -623,11 +621,18 @@ t_bridges_probe(Config) -> ?HTTP_BRIDGE(URL, ?BRIDGE_TYPE, ?BRIDGE_NAME) ), - {ok, 400, _} = request( + {ok, 400, NxDomain} = request( post, uri(["bridges_probe"]), ?HTTP_BRIDGE(<<"http://203.0.113.3:1234/foo">>, ?BRIDGE_TYPE, ?BRIDGE_NAME) ), + ?assertMatch( + #{ + <<"code">> := <<"TEST_FAILED">>, + <<"message">> := _ + }, + jsx:decode(NxDomain) + ), {ok, 204, _} = request( post, @@ -635,11 +640,25 @@ t_bridges_probe(Config) -> ?MQTT_BRIDGE(<<"127.0.0.1:1883">>) ), - {ok, 400, _} = request( + {ok, 400, ConnRefused} = request( post, uri(["bridges_probe"]), ?MQTT_BRIDGE(<<"127.0.0.1:2883">>) ), + ?assertMatch( + #{ + <<"code">> := <<"TEST_FAILED">>, + <<"message">> := <<"#{reason => econnrefused", _/binary>> + }, + jsx:decode(ConnRefused) + ), + + {ok, 400, BadReq} = request( + post, + uri(["bridges_probe"]), + ?BRIDGE(<<"bad_bridge">>, <<"unknown_type">>) + ), + ?assertMatch(#{<<"code">> := <<"BAD_REQUEST">>}, jsx:decode(BadReq)), ok. request(Method, Url, Body) -> diff --git a/apps/emqx_resource/src/emqx_resource_manager.erl b/apps/emqx_resource/src/emqx_resource_manager.erl index 8ad3fdd80..821bcbc5c 100644 --- a/apps/emqx_resource/src/emqx_resource_manager.erl +++ b/apps/emqx_resource/src/emqx_resource_manager.erl @@ -174,6 +174,9 @@ create_dry_run(ResourceType, Config) -> case wait_for_ready(ResId, 15000) of ok -> remove(ResId); + {error, Reason} -> + _ = remove(ResId), + {error, Reason}; timeout -> _ = remove(ResId), {error, timeout} @@ -632,16 +635,18 @@ data_record_to_external_map_with_metrics(Data) -> metrics => get_metrics(Data#data.id) }. --spec wait_for_ready(resource_id(), integer()) -> ok | timeout. +-spec wait_for_ready(resource_id(), integer()) -> ok | timeout | {error, term()}. wait_for_ready(ResId, WaitTime) -> do_wait_for_ready(ResId, WaitTime div ?WAIT_FOR_RESOURCE_DELAY). do_wait_for_ready(_ResId, 0) -> timeout; do_wait_for_ready(ResId, Retry) -> - case ets_lookup(ResId) of - {ok, _Group, #{status := connected}} -> + case read_cache(ResId) of + {_Group, #data{status = connected}} -> ok; + {_Group, #data{status = disconnected, error = Reason}} -> + {error, Reason}; _ -> timer:sleep(?WAIT_FOR_RESOURCE_DELAY), do_wait_for_ready(ResId, Retry - 1) From 254b92a3c17789cba73a2ed72e87c048b9c9d7a3 Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Wed, 11 Jan 2023 10:31:23 -0300 Subject: [PATCH 0109/1802] docs: add comma to please spellcheck --- apps/emqx/i18n/emqx_schema_i18n.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx/i18n/emqx_schema_i18n.conf b/apps/emqx/i18n/emqx_schema_i18n.conf index 045b06da0..0665cfb09 100644 --- a/apps/emqx/i18n/emqx_schema_i18n.conf +++ b/apps/emqx/i18n/emqx_schema_i18n.conf @@ -2050,7 +2050,7 @@ base_listener_enable_authn { Set true (default) to enable client authentication on this listener, the authentication process goes through the configured authentication chain. When set to false to allow any clients with or without authentication information such as username or password to log in. -When set to quick_deny_anonymous, it behaves like when set to true but clients will be +When set to quick_deny_anonymous, it behaves like when set to true, but clients will be denied immediately without going through any authenticators if username is not provided. This is useful to fence off anonymous clients early. """ From bf259e360ab3d82a1c7ed6ced014b2c31f2fa1e9 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Wed, 11 Jan 2023 21:30:20 +0800 Subject: [PATCH 0110/1802] chore: update the change log for #9725 --- changes/v5.0.15/feat-9725.en.md | 11 +++++++++++ changes/v5.0.15/feat-9725.zh.md | 8 ++++++++ 2 files changed, 19 insertions(+) create mode 100644 changes/v5.0.15/feat-9725.en.md create mode 100644 changes/v5.0.15/feat-9725.zh.md diff --git a/changes/v5.0.15/feat-9725.en.md b/changes/v5.0.15/feat-9725.en.md new file mode 100644 index 000000000..832aa6bf9 --- /dev/null +++ b/changes/v5.0.15/feat-9725.en.md @@ -0,0 +1,11 @@ +Remove the config `auto_reconnect` from the emqx_authz, emqx_authn and data-bridge componets. +This is because we have another config with similar functions: `resource_opts.auto_restart_interval`。 + +The functions of these two config are difficult to distinguish, which will lead to confusion. +After this change, `auto_reconnect` will not be configurable (always be true), and the underlying +drivers that support this config will automatically reconnect the abnormally disconnected +connection every `2s`. + +And the config `resource_opts.auto_restart_interval` is still available for user. +It is the time interval that emqx restarts the resource when the connection cannot be +established for some reason. diff --git a/changes/v5.0.15/feat-9725.zh.md b/changes/v5.0.15/feat-9725.zh.md new file mode 100644 index 000000000..e7a2412d4 --- /dev/null +++ b/changes/v5.0.15/feat-9725.zh.md @@ -0,0 +1,8 @@ +从认证、鉴权和数据桥接功能中,删除 `auto_reconnect` 配置项,因为我们还有另一个功能类似的配置项: +`resource_opts.auto_restart_interval`。 + +这两个配置项的功能难以区分,会导致困惑。此修改之后,`auto_reconnect` 将不可配置(永远为 true), +支持此配置的底层驱动将以 `2s` 为周期自动重连异常断开的连接。 + +而 `resource_opts.auto_restart_interval` 配置项仍然开放给用户配置,它是资源因为某些原因 +无法建立连接的时候,emqx 重新启动该资源的时间间隔。 From 3e9c4f444f03f3ebc31ce67db556025fecbeef92 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Wed, 11 Jan 2023 19:24:51 +0800 Subject: [PATCH 0111/1802] refactor: remove the auto_reconnect field --- apps/emqx_authz/README.md | 3 --- apps/emqx_connector/README.md | 2 +- .../i18n/emqx_connector_schema_lib.conf | 10 ++++----- .../emqx_connector/include/emqx_connector.hrl | 2 ++ .../src/emqx_connector_ldap.erl | 8 ++----- .../src/emqx_connector_mqtt.erl | 11 +++++----- .../src/emqx_connector_mysql.erl | 20 ++++++------------ .../src/emqx_connector_pgsql.erl | 20 ++++++------------ .../src/emqx_connector_redis.erl | 21 +++++++------------ .../src/emqx_connector_schema_lib.erl | 1 + .../src/emqx_ee_bridge_mysql.erl | 1 - .../src/emqx_ee_bridge_pgsql.erl | 1 - .../src/emqx_ee_bridge_redis.erl | 1 - mix.exs | 2 +- rebar.config | 2 +- 15 files changed, 38 insertions(+), 67 deletions(-) diff --git a/apps/emqx_authz/README.md b/apps/emqx_authz/README.md index 8c05f21be..af543e478 100644 --- a/apps/emqx_authz/README.md +++ b/apps/emqx_authz/README.md @@ -15,7 +15,6 @@ authz:{ pool_size: 1 username: root password: public - auto_reconnect: true ssl: { enable: true cacertfile: "etc/certs/cacert.pem" @@ -33,7 +32,6 @@ authz:{ pool_size: 1 username: root password: public - auto_reconnect: true ssl: {enable: false} } sql: "select ipaddress, username, clientid, action, permission, topic from mqtt_authz where ipaddr = ${peerhost} or username = ${username} or username = '$all' or clientid = ${clientid}" @@ -45,7 +43,6 @@ authz:{ database: 0 pool_size: 1 password: public - auto_reconnect: true ssl: {enable: false} } cmd: "HGETALL mqtt_authz:${username}" diff --git a/apps/emqx_connector/README.md b/apps/emqx_connector/README.md index 7ef3a8c4a..6baba29de 100644 --- a/apps/emqx_connector/README.md +++ b/apps/emqx_connector/README.md @@ -14,7 +14,7 @@ An MySQL connector can be used as following: ``` (emqx@127.0.0.1)5> emqx_resource:list_instances_verbose(). [#{config => - #{auto_reconnect => true,cacertfile => [],certfile => [], + #{cacertfile => [],certfile => [], database => "mqtt",keyfile => [],password => "public", pool_size => 1, server => {{127,0,0,1},3306}, diff --git a/apps/emqx_connector/i18n/emqx_connector_schema_lib.conf b/apps/emqx_connector/i18n/emqx_connector_schema_lib.conf index f5caf29c4..8f25e0352 100644 --- a/apps/emqx_connector/i18n/emqx_connector_schema_lib.conf +++ b/apps/emqx_connector/i18n/emqx_connector_schema_lib.conf @@ -68,13 +68,13 @@ emqx_connector_schema_lib { auto_reconnect { desc { - en: "Enable automatic reconnect to the database." - zh: "自动重连数据库。" + en: "Deprecated. Enable automatic reconnect to the database." + zh: "已弃用。自动重连数据库。" } label: { - en: "Auto Reconnect Database" - zh: "自动重连数据库" - } + en: "Deprecated. Auto Reconnect Database" + zh: "已弃用。自动重连数据库" + } } } diff --git a/apps/emqx_connector/include/emqx_connector.hrl b/apps/emqx_connector/include/emqx_connector.hrl index 96b6ba4d6..82c946cfc 100644 --- a/apps/emqx_connector/include/emqx_connector.hrl +++ b/apps/emqx_connector/include/emqx_connector.hrl @@ -24,6 +24,8 @@ -define(REDIS_DEFAULT_PORT, 6379). -define(PGSQL_DEFAULT_PORT, 5432). +-define(AUTO_RECONNECT_INTERVAL, 2). + -define(SERVERS_DESC, "A Node list for Cluster to connect to. The nodes should be separated with commas, such as: `Node[,Node].`
" "For each Node should be: " diff --git a/apps/emqx_connector/src/emqx_connector_ldap.erl b/apps/emqx_connector/src/emqx_connector_ldap.erl index 1cb65034d..1930d9e68 100644 --- a/apps/emqx_connector/src/emqx_connector_ldap.erl +++ b/apps/emqx_connector/src/emqx_connector_ldap.erl @@ -59,7 +59,6 @@ on_start( bind_password := BindPassword, timeout := Timeout, pool_size := PoolSize, - auto_reconnect := AutoReconn, ssl := SSL } = Config ) -> @@ -86,11 +85,11 @@ on_start( {bind_password, BindPassword}, {timeout, Timeout}, {pool_size, PoolSize}, - {auto_reconnect, reconn_interval(AutoReconn)} + {auto_reconnect, ?AUTO_RECONNECT_INTERVAL} ], PoolName = emqx_plugin_libs_pool:pool_name(InstId), case emqx_plugin_libs_pool:start_pool(PoolName, ?MODULE, Opts ++ SslOpts) of - ok -> {ok, #{poolname => PoolName, auto_reconnect => AutoReconn}}; + ok -> {ok, #{poolname => PoolName}}; {error, Reason} -> {error, Reason} end. @@ -129,9 +128,6 @@ on_query(InstId, {search, Base, Filter, Attributes}, #{poolname := PoolName} = S on_get_status(_InstId, _State) -> connected. -reconn_interval(true) -> 15; -reconn_interval(false) -> false. - search(Conn, Base, Filter, Attributes) -> eldap2:search(Conn, [ {base, Base}, diff --git a/apps/emqx_connector/src/emqx_connector_mqtt.erl b/apps/emqx_connector/src/emqx_connector_mqtt.erl index 522f15ccf..c28aa2514 100644 --- a/apps/emqx_connector/src/emqx_connector_mqtt.erl +++ b/apps/emqx_connector/src/emqx_connector_mqtt.erl @@ -15,6 +15,8 @@ %%-------------------------------------------------------------------- -module(emqx_connector_mqtt). +-include("emqx_connector.hrl"). + -include_lib("typerefl/include/types.hrl"). -include_lib("hocon/include/hoconsc.hrl"). -include_lib("emqx/include/logger.hrl"). @@ -198,12 +200,10 @@ on_query_async( ?TRACE("QUERY", "async_send_msg_to_remote_node", #{message => Msg, connector => InstanceId}), emqx_connector_mqtt_worker:send_to_remote_async(InstanceId, Msg, {ReplyFun, Args}). -on_get_status(_InstId, #{name := InstanceId, bridge_conf := Conf}) -> - AutoReconn = maps:get(auto_reconnect, Conf, true), +on_get_status(_InstId, #{name := InstanceId}) -> case emqx_connector_mqtt_worker:status(InstanceId) of connected -> connected; - _ when AutoReconn == true -> connecting; - _ when AutoReconn == false -> disconnected + _ -> connecting end. ensure_mqtt_worker_started(InstanceId, BridgeConf) -> @@ -236,7 +236,6 @@ make_forward_confs(FrowardConf) -> basic_config( #{ server := Server, - reconnect_interval := ReconnIntv, proto_ver := ProtoVer, bridge_mode := BridgeMode, clean_start := CleanStart, @@ -252,7 +251,7 @@ basic_config( %% 30s connect_timeout => 30, auto_reconnect => true, - reconnect_interval => ReconnIntv, + reconnect_interval => ?AUTO_RECONNECT_INTERVAL, proto_ver => ProtoVer, %% Opening bridge_mode will form a non-standard mqtt connection message. %% A load balancing server (such as haproxy) is often set up before the emqx broker server. diff --git a/apps/emqx_connector/src/emqx_connector_mysql.erl b/apps/emqx_connector/src/emqx_connector_mysql.erl index 6c0ff7210..d119a01e9 100644 --- a/apps/emqx_connector/src/emqx_connector_mysql.erl +++ b/apps/emqx_connector/src/emqx_connector_mysql.erl @@ -52,7 +52,6 @@ -type state() :: #{ poolname := atom(), - auto_reconnect := boolean(), prepare_statement := prepares(), params_tokens := params_tokens(), batch_inserts := sqls(), @@ -84,8 +83,6 @@ on_start( server := Server, database := DB, username := User, - password := Password, - auto_reconnect := AutoReconn, pool_size := PoolSize, ssl := SSL } = Config @@ -107,14 +104,14 @@ on_start( {host, Host}, {port, Port}, {user, User}, - {password, Password}, + {password, maps:get(password, Config, <<>>)}, {database, DB}, - {auto_reconnect, reconn_interval(AutoReconn)}, + {auto_reconnect, ?AUTO_RECONNECT_INTERVAL}, {pool_size, PoolSize} ], PoolName = emqx_plugin_libs_pool:pool_name(InstId), Prepares = parse_prepare_sql(Config), - State = maps:merge(#{poolname => PoolName, auto_reconnect => AutoReconn}, Prepares), + State = maps:merge(#{poolname => PoolName}, Prepares), case emqx_plugin_libs_pool:start_pool(PoolName, ?MODULE, Options ++ SslOpts) of ok -> {ok, init_prepare(State)}; @@ -194,7 +191,7 @@ mysql_function(prepared_query) -> mysql_function(_) -> mysql_function(prepared_query). -on_get_status(_InstId, #{poolname := Pool, auto_reconnect := AutoReconn} = State) -> +on_get_status(_InstId, #{poolname := Pool} = State) -> case emqx_plugin_libs_pool:health_check_ecpool_workers(Pool, fun ?MODULE:do_get_status/1) of true -> case do_check_prepares(State) of @@ -205,10 +202,10 @@ on_get_status(_InstId, #{poolname := Pool, auto_reconnect := AutoReconn} = State {connected, NState}; {error, _Reason} -> %% do not log error, it is logged in prepare_sql_to_conn - conn_status(AutoReconn) + connecting end; false -> - conn_status(AutoReconn) + connecting end. do_get_status(Conn) -> @@ -227,11 +224,6 @@ do_check_prepares(State = #{poolname := PoolName, prepare_statement := {error, P end. %% =================================================================== -conn_status(_AutoReconn = true) -> connecting; -conn_status(_AutoReconn = false) -> disconnected. - -reconn_interval(true) -> 15; -reconn_interval(false) -> false. connect(Options) -> mysql:start_link(Options). diff --git a/apps/emqx_connector/src/emqx_connector_pgsql.erl b/apps/emqx_connector/src/emqx_connector_pgsql.erl index 4b565a614..25a22455d 100644 --- a/apps/emqx_connector/src/emqx_connector_pgsql.erl +++ b/apps/emqx_connector/src/emqx_connector_pgsql.erl @@ -56,7 +56,6 @@ -type state() :: #{ poolname := atom(), - auto_reconnect := boolean(), prepare_sql := prepares(), params_tokens := params_tokens(), prepare_statement := epgsql:statement() @@ -87,8 +86,6 @@ on_start( server := Server, database := DB, username := User, - password := Password, - auto_reconnect := AutoReconn, pool_size := PoolSize, ssl := SSL } = Config @@ -113,14 +110,14 @@ on_start( {host, Host}, {port, Port}, {username, User}, - {password, emqx_secret:wrap(Password)}, + {password, emqx_secret:wrap(maps:get(password, Config, ""))}, {database, DB}, - {auto_reconnect, reconn_interval(AutoReconn)}, + {auto_reconnect, ?AUTO_RECONNECT_INTERVAL}, {pool_size, PoolSize} ], PoolName = emqx_plugin_libs_pool:pool_name(InstId), Prepares = parse_prepare_sql(Config), - InitState = #{poolname => PoolName, auto_reconnect => AutoReconn, prepare_statement => #{}}, + InitState = #{poolname => PoolName, prepare_statement => #{}}, State = maps:merge(InitState, Prepares), case emqx_plugin_libs_pool:start_pool(PoolName, ?MODULE, Options ++ SslOpts) of ok -> @@ -247,7 +244,7 @@ on_sql_query(InstId, PoolName, Type, NameOrSQL, Data) -> end, Result. -on_get_status(_InstId, #{poolname := Pool, auto_reconnect := AutoReconn} = State) -> +on_get_status(_InstId, #{poolname := Pool} = State) -> case emqx_plugin_libs_pool:health_check_ecpool_workers(Pool, fun ?MODULE:do_get_status/1) of true -> case do_check_prepares(State) of @@ -258,10 +255,10 @@ on_get_status(_InstId, #{poolname := Pool, auto_reconnect := AutoReconn} = State {connected, NState}; false -> %% do not log error, it is logged in prepare_sql_to_conn - conn_status(AutoReconn) + connecting end; false -> - conn_status(AutoReconn) + connecting end. do_get_status(Conn) -> @@ -280,11 +277,6 @@ do_check_prepares(State = #{poolname := PoolName, prepare_sql := {error, Prepare end. %% =================================================================== -conn_status(_AutoReconn = true) -> connecting; -conn_status(_AutoReconn = false) -> disconnected. - -reconn_interval(true) -> 15; -reconn_interval(false) -> false. connect(Opts) -> Host = proplists:get_value(host, Opts), diff --git a/apps/emqx_connector/src/emqx_connector_redis.erl b/apps/emqx_connector/src/emqx_connector_redis.erl index 726af2d9b..6143dbf0c 100644 --- a/apps/emqx_connector/src/emqx_connector_redis.erl +++ b/apps/emqx_connector/src/emqx_connector_redis.erl @@ -117,7 +117,6 @@ on_start( #{ redis_type := Type, pool_size := PoolSize, - auto_reconnect := AutoReconn, ssl := SSL } = Config ) -> @@ -142,7 +141,7 @@ on_start( [ {pool_size, PoolSize}, {password, maps:get(password, Config, "")}, - {auto_reconnect, reconn_interval(AutoReconn)} + {auto_reconnect, ?AUTO_RECONNECT_INTERVAL} ] ++ Database ++ Servers, Options = case maps:get(enable, SSL) of @@ -155,7 +154,7 @@ on_start( [{ssl, false}] end ++ [{sentinel, maps:get(sentinel, Config, undefined)}], PoolName = emqx_plugin_libs_pool:pool_name(InstId), - State = #{poolname => PoolName, type => Type, auto_reconnect => AutoReconn}, + State = #{poolname => PoolName, type => Type}, case Type of cluster -> case eredis_cluster:start_pool(PoolName, Opts ++ [{options, Options}]) of @@ -229,18 +228,18 @@ eredis_cluster_workers_exist_and_are_connected(Workers) -> Workers ). -on_get_status(_InstId, #{type := cluster, poolname := PoolName, auto_reconnect := AutoReconn}) -> +on_get_status(_InstId, #{type := cluster, poolname := PoolName}) -> case eredis_cluster:pool_exists(PoolName) of true -> Workers = extract_eredis_cluster_workers(PoolName), Health = eredis_cluster_workers_exist_and_are_connected(Workers), - status_result(Health, AutoReconn); + status_result(Health); false -> disconnected end; -on_get_status(_InstId, #{poolname := Pool, auto_reconnect := AutoReconn}) -> +on_get_status(_InstId, #{poolname := Pool}) -> Health = emqx_plugin_libs_pool:health_check_ecpool_workers(Pool, fun ?MODULE:do_get_status/1), - status_result(Health, AutoReconn). + status_result(Health). do_get_status(Conn) -> case eredis:q(Conn, ["PING"]) of @@ -248,12 +247,8 @@ do_get_status(Conn) -> _ -> false end. -status_result(_Status = true, _AutoReconn) -> connected; -status_result(_Status = false, _AutoReconn = true) -> connecting; -status_result(_Status = false, _AutoReconn = false) -> disconnected. - -reconn_interval(true) -> 15; -reconn_interval(false) -> false. +status_result(_Status = true) -> connected; +status_result(_Status = false) -> connecting. do_cmd(PoolName, cluster, {cmd, Command}) -> eredis_cluster:q(PoolName, Command); diff --git a/apps/emqx_connector/src/emqx_connector_schema_lib.erl b/apps/emqx_connector/src/emqx_connector_schema_lib.erl index 2364aeeaa..5d8f6941c 100644 --- a/apps/emqx_connector/src/emqx_connector_schema_lib.erl +++ b/apps/emqx_connector/src/emqx_connector_schema_lib.erl @@ -106,4 +106,5 @@ password(_) -> undefined. auto_reconnect(type) -> boolean(); auto_reconnect(desc) -> ?DESC("auto_reconnect"); auto_reconnect(default) -> true; +auto_reconnect(deprecated) -> {since, "v5.0.15"}; auto_reconnect(_) -> undefined. diff --git a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_mysql.erl b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_mysql.erl index bf5d2e140..114459149 100644 --- a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_mysql.erl +++ b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_mysql.erl @@ -51,7 +51,6 @@ values(post) -> pool_size => 8, username => <<"root">>, password => <<"">>, - auto_reconnect => true, sql => ?DEFAULT_SQL, local_topic => <<"local/topic/#">>, resource_opts => #{ diff --git a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_pgsql.erl b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_pgsql.erl index dc8697e37..be9fc9dc8 100644 --- a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_pgsql.erl +++ b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_pgsql.erl @@ -53,7 +53,6 @@ values(post, Type) -> pool_size => 8, username => <<"root">>, password => <<"public">>, - auto_reconnect => true, sql => ?DEFAULT_SQL, local_topic => <<"local/topic/#">>, resource_opts => #{ diff --git a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_redis.erl b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_redis.erl index a0c6ba834..5c273e050 100644 --- a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_redis.erl +++ b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_redis.erl @@ -79,7 +79,6 @@ values(common, RedisType, SpecificOpts) -> local_topic => <<"local/topic/#">>, pool_size => 8, password => <<"secret">>, - auto_reconnect => true, command_template => [<<"LPUSH">>, <<"MSGS">>, <<"${payload}">>], resource_opts => #{ batch_size => 1, diff --git a/mix.exs b/mix.exs index 45bc820e0..832f6e7ae 100644 --- a/mix.exs +++ b/mix.exs @@ -69,7 +69,7 @@ defmodule EMQXUmbrella.MixProject do {:getopt, "1.0.2", override: true}, {:snabbkaffe, github: "kafka4beam/snabbkaffe", tag: "1.0.0", override: true}, {:hocon, github: "emqx/hocon", tag: "0.35.0", override: true}, - {:emqx_http_lib, github: "emqx/emqx_http_lib", tag: "0.5.1", override: true}, + {:emqx_http_lib, github: "emqx/emqx_http_lib", tag: "0.5.2", override: true}, {:esasl, github: "emqx/esasl", tag: "0.2.0"}, {:jose, github: "potatosalad/erlang-jose", tag: "1.11.2"}, # in conflict by ehttpc and emqtt diff --git a/rebar.config b/rebar.config index a61bfe16a..f05624477 100644 --- a/rebar.config +++ b/rebar.config @@ -69,7 +69,7 @@ , {getopt, "1.0.2"} , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "1.0.0"}}} , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.35.0"}}} - , {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.5.1"}}} + , {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.5.2"}}} , {esasl, {git, "https://github.com/emqx/esasl", {tag, "0.2.0"}}} , {jose, {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.11.2"}}} , {telemetry, "1.1.0"} From 8c482e03d143733552a1196f2a90042e0612220b Mon Sep 17 00:00:00 2001 From: Kjell Winblad Date: Wed, 11 Jan 2023 16:48:29 +0100 Subject: [PATCH 0112/1802] fix: remove atom leaks Both emqx_resource_managers and emqx_resource_workers leaked atoms as they created an unique atoms to use as registered names. This is fixed by removing the need to register the names. Fixes: https://emqx.atlassian.net/browse/EMQX-8583 --- .../src/emqx_resource_manager.erl | 27 ++-- .../src/emqx_resource_worker.erl | 141 +++++++++--------- changes/v5.0.14/fix-8583.en.md | 1 + changes/v5.0.14/fix-8583.zh.md | 1 + 4 files changed, 86 insertions(+), 84 deletions(-) create mode 100644 changes/v5.0.14/fix-8583.en.md create mode 100644 changes/v5.0.14/fix-8583.zh.md diff --git a/apps/emqx_resource/src/emqx_resource_manager.erl b/apps/emqx_resource/src/emqx_resource_manager.erl index 8531f1641..06e6288e3 100644 --- a/apps/emqx_resource/src/emqx_resource_manager.erl +++ b/apps/emqx_resource/src/emqx_resource_manager.erl @@ -54,7 +54,7 @@ % State record -record(data, { - id, manager_id, group, mod, callback_mode, query_mode, config, opts, status, state, error + id, manager_id, group, mod, callback_mode, query_mode, config, opts, status, state, error, pid }). -type data() :: #data{}. @@ -296,17 +296,16 @@ start_link(MgrId, ResId, Group, ResourceType, Config, Opts) -> state = undefined, error = undefined }, - Module = atom_to_binary(?MODULE), - ProcName = binary_to_atom(<>, utf8), - gen_statem:start_link({local, ProcName}, ?MODULE, {Data, Opts}, []). + gen_statem:start_link(?MODULE, {Data, Opts}, []). init({Data, Opts}) -> process_flag(trap_exit, true), %% init the cache so that lookup/1 will always return something - insert_cache(Data#data.id, Data#data.group, Data), + DataWithPid = Data#data{pid = self()}, + insert_cache(DataWithPid#data.id, DataWithPid#data.group, DataWithPid), case maps:get(start_after_created, Opts, ?START_AFTER_CREATED) of - true -> {ok, connecting, Data, {next_event, internal, start_resource}}; - false -> {ok, stopped, Data} + true -> {ok, connecting, DataWithPid, {next_event, internal, start_resource}}; + false -> {ok, stopped, DataWithPid} end. terminate(_Reason, _State, Data) -> @@ -429,6 +428,14 @@ read_cache(ResId) -> [] -> not_found end. +read_manager_pid_from_cache(ResId) -> + case read_cache(ResId) of + not_found -> + erlang:error(badarg); + {_, #data{pid = ManagerPid}} -> + ManagerPid + end. + delete_cache(ResId, MgrId) -> case get_owner(ResId) of MgrIdNow when MgrIdNow == not_found; MgrIdNow == MgrId -> @@ -649,10 +656,8 @@ do_wait_for_ready(ResId, Retry) -> safe_call(ResId, Message, Timeout) -> try - Module = atom_to_binary(?MODULE), - MgrId = get_owner(ResId), - ProcName = binary_to_existing_atom(<>, utf8), - gen_statem:call(ProcName, Message, {clean_timeout, Timeout}) + ManagerPid = read_manager_pid_from_cache(ResId), + gen_statem:call(ManagerPid, Message, {clean_timeout, Timeout}) catch error:badarg -> {error, not_found}; diff --git a/apps/emqx_resource/src/emqx_resource_worker.erl b/apps/emqx_resource/src/emqx_resource_worker.erl index 93bb22551..264556bb5 100644 --- a/apps/emqx_resource/src/emqx_resource_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_worker.erl @@ -76,7 +76,7 @@ -type data() :: #{ id => id(), index => index(), - name => atom(), + inflight_tid => ets:tid(), batch_size => pos_integer(), batch_time => timer:time(), queue => replayq:q(), @@ -87,7 +87,7 @@ callback_mode() -> [state_functions, state_enter]. start_link(Id, Index, Opts) -> - gen_statem:start_link({local, name(Id, Index)}, ?MODULE, {Id, Index, Opts}, []). + gen_statem:start_link(?MODULE, {Id, Index, Opts}, []). -spec sync_query(id(), request(), query_opts()) -> Result :: term(). sync_query(Id, Request, Opts) -> @@ -133,11 +133,11 @@ simple_async_query(Id, Request, ReplyFun) -> _ = handle_query_result(Id, Result, false, false), Result. --spec block(pid() | atom()) -> ok. +-spec block(pid()) -> ok. block(ServerRef) -> gen_statem:cast(ServerRef, block). --spec resume(pid() | atom()) -> ok. +-spec resume(pid()) -> ok. resume(ServerRef) -> gen_statem:cast(ServerRef, resume). @@ -145,7 +145,6 @@ resume(ServerRef) -> init({Id, Index, Opts}) -> process_flag(trap_exit, true), true = gproc_pool:connect_worker(Id, {Id, Index}), - Name = name(Id, Index), BatchSize = maps:get(batch_size, Opts, ?DEFAULT_BATCH_SIZE), SegBytes0 = maps:get(queue_seg_bytes, Opts, ?DEFAULT_QUEUE_SEG_SIZE), TotalBytes = maps:get(max_queue_bytes, Opts, ?DEFAULT_QUEUE_SIZE), @@ -165,12 +164,12 @@ init({Id, Index, Opts}) -> emqx_resource_metrics:queuing_set(Id, Index, queue_count(Queue)), emqx_resource_metrics:inflight_set(Id, Index, 0), InfltWinSZ = maps:get(async_inflight_window, Opts, ?DEFAULT_INFLIGHT), - ok = inflight_new(Name, InfltWinSZ, Id, Index), + {ok, InflightTID} = inflight_new(InfltWinSZ, Id, Index), HCItvl = maps:get(health_check_interval, Opts, ?HEALTHCHECK_INTERVAL), St = #{ id => Id, index => Index, - name => Name, + inflight_tid => InflightTID, batch_size => BatchSize, batch_time => maps:get(batch_time, Opts, ?DEFAULT_BATCH_TIME), queue => Queue, @@ -283,14 +282,14 @@ pick_cast(Id, Key, Query) -> ok end). -do_resume(#{id := Id, name := Name} = Data) -> - case inflight_get_first(Name) of +do_resume(#{id := Id, inflight_tid := InflightTID} = Data) -> + case inflight_get_first(InflightTID) of empty -> retry_queue(Data); {Ref, FirstQuery} -> %% We retry msgs in inflight window sync, as if we send them %% async, they will be appended to the end of inflight window again. - retry_inflight_sync(Id, Ref, FirstQuery, Name, Data) + retry_inflight_sync(Id, Ref, FirstQuery, InflightTID, Data) end. retry_queue( @@ -299,7 +298,7 @@ retry_queue( id := Id, index := Index, batch_size := 1, - name := Name, + inflight_tid := InflightTID, resume_interval := ResumeT } = Data0 ) -> @@ -308,7 +307,7 @@ retry_queue( empty -> {next_state, running, Data0}; {Q1, QAckRef, [?QUERY(_, Request, HasBeenSent) = Query]} -> - QueryOpts = #{inflight_name => Name}, + QueryOpts = #{inflight_name => InflightTID}, Result = call_query(configured, Id, Index, Query, QueryOpts), Reply = ?REPLY(undefined, Request, HasBeenSent, Result), case reply_caller(Id, Reply) of @@ -327,7 +326,7 @@ retry_queue( id := Id, index := Index, batch_size := BatchSize, - name := Name, + inflight_tid := InflightTID, resume_interval := ResumeT } = Data0 ) -> @@ -336,7 +335,7 @@ retry_queue( empty -> {next_state, running, Data0}; {Q1, QAckRef, Batch0} -> - QueryOpts = #{inflight_name => Name}, + QueryOpts = #{inflight_name => InflightTID}, Result = call_query(configured, Id, Index, Batch0, QueryOpts), %% The caller has been replied with ?RESOURCE_ERROR(blocked, _) before saving into the queue, %% we now change the 'from' field to 'undefined' so it will not reply the caller again. @@ -361,7 +360,7 @@ retry_inflight_sync( Id, Ref, QueryOrBatch, - Name, + InflightTID, #{index := Index, resume_interval := ResumeT} = Data0 ) -> QueryOpts = #{}, @@ -375,7 +374,7 @@ retry_inflight_sync( {keep_state, Data0, {state_timeout, ResumeT, resume}}; %% Send ok or failed but the resource is working false -> - inflight_drop(Name, Ref, Id, Index), + inflight_drop(InflightTID, Ref, Id, Index), do_resume(Data0) end. @@ -451,11 +450,11 @@ do_flush(Data0, #{is_batch := false, batch := Batch, ack_ref := QAckRef, new_que #{ id := Id, index := Index, - name := Name + inflight_tid := InflightTID } = Data0, %% unwrap when not batching (i.e., batch size == 1) [?QUERY(From, CoreReq, HasBeenSent) = Request] = Batch, - QueryOpts = #{inflight_name => Name}, + QueryOpts = #{inflight_name => InflightTID}, Result = call_query(configured, Id, Index, Request, QueryOpts), IsAsync = is_async(Id), Data1 = cancel_flush_timer(Data0), @@ -489,9 +488,9 @@ do_flush(Data0, #{is_batch := true, batch := Batch, ack_ref := QAckRef, new_queu id := Id, index := Index, batch_size := BatchSize, - name := Name + inflight_tid := InflightTID } = Data0, - QueryOpts = #{inflight_name => Name}, + QueryOpts = #{inflight_name => InflightTID}, Result = call_query(configured, Id, Index, Batch, QueryOpts), IsAsync = is_async(Id), Data1 = cancel_flush_timer(Data0), @@ -639,17 +638,17 @@ apply_query_fun(sync, Mod, Id, _Index, ?QUERY(_, Request, _) = _Query, ResSt, _Q ?APPLY_RESOURCE(call_query, Mod:on_query(Id, Request, ResSt), Request); apply_query_fun(async, Mod, Id, Index, ?QUERY(_, Request, _) = Query, ResSt, QueryOpts) -> ?tp(call_query_async, #{id => Id, mod => Mod, query => Query, res_st => ResSt}), - Name = maps:get(inflight_name, QueryOpts, undefined), + InflightTID = maps:get(inflight_name, QueryOpts, undefined), ?APPLY_RESOURCE( call_query_async, - case is_inflight_full(Name) of + case is_inflight_full(InflightTID) of true -> {async_return, inflight_full}; false -> ReplyFun = fun ?MODULE:reply_after_query/7, Ref = make_message_ref(), - Args = [self(), Id, Index, Name, Ref, Query], - ok = inflight_append(Name, Ref, Query, Id, Index), + Args = [self(), Id, Index, InflightTID, Ref, Query], + ok = inflight_append(InflightTID, Ref, Query, Id, Index), Result = Mod:on_query_async(Id, Request, {ReplyFun, Args}, ResSt), {async_return, Result} end, @@ -661,25 +660,25 @@ apply_query_fun(sync, Mod, Id, _Index, [?QUERY(_, _, _) | _] = Batch, ResSt, _Qu ?APPLY_RESOURCE(call_batch_query, Mod:on_batch_query(Id, Requests, ResSt), Batch); apply_query_fun(async, Mod, Id, Index, [?QUERY(_, _, _) | _] = Batch, ResSt, QueryOpts) -> ?tp(call_batch_query_async, #{id => Id, mod => Mod, batch => Batch, res_st => ResSt}), - Name = maps:get(inflight_name, QueryOpts, undefined), + InflightTID = maps:get(inflight_name, QueryOpts, undefined), ?APPLY_RESOURCE( call_batch_query_async, - case is_inflight_full(Name) of + case is_inflight_full(InflightTID) of true -> {async_return, inflight_full}; false -> ReplyFun = fun ?MODULE:batch_reply_after_query/7, Ref = make_message_ref(), - ReplyFunAndArgs = {ReplyFun, [self(), Id, Index, Name, Ref, Batch]}, + ReplyFunAndArgs = {ReplyFun, [self(), Id, Index, InflightTID, Ref, Batch]}, Requests = [Request || ?QUERY(_From, Request, _) <- Batch], - ok = inflight_append(Name, Ref, Batch, Id, Index), + ok = inflight_append(InflightTID, Ref, Batch, Id, Index), Result = Mod:on_batch_query_async(Id, Requests, ReplyFunAndArgs, ResSt), {async_return, Result} end, Batch ). -reply_after_query(Pid, Id, Index, Name, Ref, ?QUERY(From, Request, HasBeenSent), Result) -> +reply_after_query(Pid, Id, Index, InflightTID, Ref, ?QUERY(From, Request, HasBeenSent), Result) -> %% NOTE: 'inflight' is the count of messages that were sent async %% but received no ACK, NOT the number of messages queued in the %% inflight window. @@ -687,10 +686,10 @@ reply_after_query(Pid, Id, Index, Name, Ref, ?QUERY(From, Request, HasBeenSent), true -> ?MODULE:block(Pid); false -> - drop_inflight_and_resume(Pid, Name, Ref, Id, Index) + drop_inflight_and_resume(Pid, InflightTID, Ref, Id, Index) end. -batch_reply_after_query(Pid, Id, Index, Name, Ref, Batch, Result) -> +batch_reply_after_query(Pid, Id, Index, InflightTID, Ref, Batch, Result) -> %% NOTE: 'inflight' is the count of messages that were sent async %% but received no ACK, NOT the number of messages queued in the %% inflight window. @@ -698,16 +697,16 @@ batch_reply_after_query(Pid, Id, Index, Name, Ref, Batch, Result) -> true -> ?MODULE:block(Pid); false -> - drop_inflight_and_resume(Pid, Name, Ref, Id, Index) + drop_inflight_and_resume(Pid, InflightTID, Ref, Id, Index) end. -drop_inflight_and_resume(Pid, Name, Ref, Id, Index) -> - case is_inflight_full(Name) of +drop_inflight_and_resume(Pid, InflightTID, Ref, Id, Index) -> + case is_inflight_full(InflightTID) of true -> - inflight_drop(Name, Ref, Id, Index), + inflight_drop(InflightTID, Ref, Id, Index), ?MODULE:resume(Pid); false -> - inflight_drop(Name, Ref, Id, Index) + inflight_drop(InflightTID, Ref, Id, Index) end. %%============================================================================== @@ -757,82 +756,85 @@ get_first_n_from_queue(Q, N) -> %% the inflight queue for async query -define(MAX_SIZE_REF, -1). -define(SIZE_REF, -2). -inflight_new(Name, InfltWinSZ, Id, Index) -> - _ = ets:new(Name, [named_table, ordered_set, public, {write_concurrency, true}]), - inflight_append(Name, ?MAX_SIZE_REF, {max_size, InfltWinSZ}, Id, Index), +inflight_new(InfltWinSZ, Id, Index) -> + TableId = ets:new( + emqx_resource_worker_inflight_tab, + [ordered_set, public, {write_concurrency, true}] + ), + inflight_append(TableId, ?MAX_SIZE_REF, {max_size, InfltWinSZ}, Id, Index), %% we use this counter because we might deal with batches as %% elements. - inflight_append(Name, ?SIZE_REF, 0, Id, Index), - ok. + inflight_append(TableId, ?SIZE_REF, 0, Id, Index), + {ok, TableId}. -inflight_get_first(Name) -> - case ets:next(Name, ?MAX_SIZE_REF) of +inflight_get_first(InflightTID) -> + case ets:next(InflightTID, ?MAX_SIZE_REF) of '$end_of_table' -> empty; Ref -> - case ets:lookup(Name, Ref) of + case ets:lookup(InflightTID, Ref) of [Object] -> Object; [] -> %% it might have been dropped - inflight_get_first(Name) + inflight_get_first(InflightTID) end end. is_inflight_full(undefined) -> false; -is_inflight_full(Name) -> - [{_, {max_size, MaxSize}}] = ets:lookup(Name, ?MAX_SIZE_REF), +is_inflight_full(InflightTID) -> + [{_, {max_size, MaxSize}}] = ets:lookup(InflightTID, ?MAX_SIZE_REF), %% we consider number of batches rather than number of messages %% because one batch request may hold several messages. - Size = inflight_num_batches(Name), + Size = inflight_num_batches(InflightTID), Size >= MaxSize. -inflight_num_batches(Name) -> +inflight_num_batches(InflightTID) -> %% Note: we subtract 2 because there're 2 metadata rows that hold %% the maximum size value and the number of messages. MetadataRowCount = 2, - case ets:info(Name, size) of + case ets:info(InflightTID, size) of undefined -> 0; Size -> max(0, Size - MetadataRowCount) end. -inflight_num_msgs(Name) -> - [{_, Size}] = ets:lookup(Name, ?SIZE_REF), +inflight_num_msgs(InflightTID) -> + [{_, Size}] = ets:lookup(InflightTID, ?SIZE_REF), Size. inflight_append(undefined, _Ref, _Query, _Id, _Index) -> ok; -inflight_append(Name, Ref, [?QUERY(_, _, _) | _] = Batch0, Id, Index) -> +inflight_append(InflightTID, Ref, [?QUERY(_, _, _) | _] = Batch0, Id, Index) -> Batch = mark_as_sent(Batch0), - ets:insert(Name, {Ref, Batch}), + ets:insert(InflightTID, {Ref, Batch}), BatchSize = length(Batch), - ets:update_counter(Name, ?SIZE_REF, {2, BatchSize}), - emqx_resource_metrics:inflight_set(Id, Index, inflight_num_msgs(Name)), + ets:update_counter(InflightTID, ?SIZE_REF, {2, BatchSize}), + emqx_resource_metrics:inflight_set(Id, Index, inflight_num_msgs(InflightTID)), ok; -inflight_append(Name, Ref, ?QUERY(_From, _Req, _HasBeenSent) = Query0, Id, Index) -> +inflight_append(InflightTID, Ref, ?QUERY(_From, _Req, _HasBeenSent) = Query0, Id, Index) -> Query = mark_as_sent(Query0), - ets:insert(Name, {Ref, Query}), - ets:update_counter(Name, ?SIZE_REF, {2, 1}), - emqx_resource_metrics:inflight_set(Id, Index, inflight_num_msgs(Name)), + ets:insert(InflightTID, {Ref, Query}), + ets:update_counter(InflightTID, ?SIZE_REF, {2, 1}), + emqx_resource_metrics:inflight_set(Id, Index, inflight_num_msgs(InflightTID)), ok; -inflight_append(Name, Ref, Data, _Id, _Index) -> - ets:insert(Name, {Ref, Data}), +inflight_append(InflightTID, Ref, Data, _Id, _Index) -> + ets:insert(InflightTID, {Ref, Data}), %% this is a metadata row being inserted; therefore, we don't bump %% the inflight metric. ok. inflight_drop(undefined, _, _Id, _Index) -> ok; -inflight_drop(Name, Ref, Id, Index) -> +inflight_drop(InflightTID, Ref, Id, Index) -> Count = - case ets:take(Name, Ref) of + case ets:take(InflightTID, Ref) of [{Ref, ?QUERY(_, _, _)}] -> 1; [{Ref, [?QUERY(_, _, _) | _] = Batch}] -> length(Batch); _ -> 0 end, - Count > 0 andalso ets:update_counter(Name, ?SIZE_REF, {2, -Count, 0, 0}), - emqx_resource_metrics:inflight_set(Id, Index, inflight_num_msgs(Name)), + Count > 0 andalso ets:update_counter(InflightTID, ?SIZE_REF, {2, -Count, 0, 0}), + emqx_resource_metrics:inflight_set(Id, Index, inflight_num_msgs(InflightTID)), ok. %%============================================================================== @@ -868,13 +870,6 @@ assert_ok_result(R) -> queue_count(Q) -> replayq:count(Q). --spec name(id(), integer()) -> atom(). -name(Id, Index) -> - Mod = atom_to_list(?MODULE), - Id1 = binary_to_list(Id), - Index1 = integer_to_list(Index), - list_to_atom(lists:concat([Mod, ":", Id1, ":", Index1])). - disk_queue_dir(Id, Index) -> QDir = binary_to_list(Id) ++ ":" ++ integer_to_list(Index), filename:join([emqx:data_dir(), "resource_worker", node(), QDir]). diff --git a/changes/v5.0.14/fix-8583.en.md b/changes/v5.0.14/fix-8583.en.md new file mode 100644 index 000000000..f926d6e51 --- /dev/null +++ b/changes/v5.0.14/fix-8583.en.md @@ -0,0 +1 @@ +Potential leaks of atoms that could lead to a crash if a lot of resources were created have been removed. diff --git a/changes/v5.0.14/fix-8583.zh.md b/changes/v5.0.14/fix-8583.zh.md new file mode 100644 index 000000000..e2138d654 --- /dev/null +++ b/changes/v5.0.14/fix-8583.zh.md @@ -0,0 +1 @@ +如果创建了大量的资源,可能会导致崩溃的潜在的原子泄漏已经被删除。 From 1194d07a07598d602a4597c61623231051ceb470 Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Thu, 12 Jan 2023 12:48:26 +0800 Subject: [PATCH 0113/1802] refactor: update emqx_prometheus's headers from map() to list() --- .../src/emqx_dashboard_swagger.erl | 2 ++ apps/emqx_prometheus/src/emqx_prometheus.erl | 14 ++++---------- .../src/emqx_prometheus_schema.erl | 17 +++++++++++++++-- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl b/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl index 102b95f4e..1b5b6ca9c 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl @@ -708,6 +708,8 @@ typename_to_spec("qos()", _Mod) -> #{type => integer, minimum => 0, maximum => 2, example => 0}; typename_to_spec("{binary(), binary()}", _Mod) -> #{type => object, example => #{}}; +typename_to_spec("{string(), string()}", _Mod) -> + #{type => object, example => #{}}; typename_to_spec("comma_separated_list()", _Mod) -> #{type => string, example => <<"item1,item2">>}; typename_to_spec("comma_separated_binary()", _Mod) -> diff --git a/apps/emqx_prometheus/src/emqx_prometheus.erl b/apps/emqx_prometheus/src/emqx_prometheus.erl index 91c1f22fc..a66f275f8 100644 --- a/apps/emqx_prometheus/src/emqx_prometheus.erl +++ b/apps/emqx_prometheus/src/emqx_prometheus.erl @@ -112,7 +112,7 @@ handle_info({timeout, Timer, ?TIMER_MSG}, State = #{timer := Timer}) -> handle_info(_Msg, State) -> {noreply, State}. -push_to_push_gateway(Uri, Headers0, JobName) when is_map(Headers0) -> +push_to_push_gateway(Uri, Headers, JobName) when is_list(Headers) -> [Name, Ip] = string:tokens(atom_to_list(node()), "@"), JobName1 = emqx_placeholder:preproc_tmpl(JobName), JobName2 = binary_to_list( @@ -124,21 +124,15 @@ push_to_push_gateway(Uri, Headers0, JobName) when is_map(Headers0) -> Url = lists:concat([Uri, "/metrics/job/", JobName2]), Data = prometheus_text_format:format(), - Headers = maps:fold( - fun(K, V, Acc) -> - [{atom_to_list(K), binary_to_list(V)} | Acc] - end, - [], - Headers0 - ), case httpc:request(post, {Url, Headers, "text/plain", Data}, ?HTTP_OPTIONS, []) of - {ok, {{"HTTP/1.1", 200, _}, _Headers, _Body}} -> + {ok, {{"HTTP/1.1", 200, _}, _RespHeaders, _RespBody}} -> ok; Error -> ?SLOG(error, #{ msg => "post_to_push_gateway_failed", error => Error, - url => Url + url => Url, + headers => Headers }), failed end. diff --git a/apps/emqx_prometheus/src/emqx_prometheus_schema.erl b/apps/emqx_prometheus/src/emqx_prometheus_schema.erl index da65e02c4..c13d198a2 100644 --- a/apps/emqx_prometheus/src/emqx_prometheus_schema.erl +++ b/apps/emqx_prometheus/src/emqx_prometheus_schema.erl @@ -25,7 +25,8 @@ roots/0, fields/1, desc/1, - translation/1 + translation/1, + convert_headers/1 ]). namespace() -> "prometheus". @@ -54,10 +55,11 @@ fields("prometheus") -> )}, {headers, ?HOCON( - map(), + list({string(), string()}), #{ default => #{}, required => false, + converter => fun ?MODULE:convert_headers/1, desc => ?DESC(headers) } )}, @@ -145,6 +147,17 @@ fields("prometheus") -> desc("prometheus") -> ?DESC(prometheus); desc(_) -> undefined. +convert_headers(Headers) when is_map(Headers) -> + maps:fold( + fun(K, V, Acc) -> + [{binary_to_list(K), binary_to_list(V)} | Acc] + end, + [], + Headers + ); +convert_headers(Headers) when is_list(Headers) -> + Headers. + %% for CI test, CI don't load the whole emqx_conf_schema. translation(Name) -> emqx_conf_schema:translation(Name). From e07aa2086bdd9e0c428a5da3436024ff55eb0ca7 Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Wed, 11 Jan 2023 16:53:47 +0300 Subject: [PATCH 0114/1802] fix(api): augment paged search responses with `hasnext` flag This flag indicates whether there are more results available on the next pages. It is needed in cases when the total number of search results is not known in advance. Also, in such cases there's no `count` field in responses anymore because responding with `0` was confusing for clients: it's not possible to differentiate between "there are no results" and "we don't know how much". Co-authored-by: Thales Macedo Garitezi --- .../src/emqx_dashboard_swagger.erl | 16 +- apps/emqx_management/src/emqx_mgmt_api.erl | 187 ++++++++++-------- .../test/emqx_mgmt_api_SUITE.erl | 7 +- .../test/emqx_mgmt_api_subscription_SUITE.erl | 79 ++++++-- 4 files changed, 180 insertions(+), 109 deletions(-) diff --git a/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl b/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl index 102b95f4e..42fdcd46d 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl @@ -139,14 +139,20 @@ fields(limit) -> [{limit, hoconsc:mk(range(1, ?MAX_ROW_LIMIT), Meta)}]; fields(count) -> Desc = << - "Total number of records counted.
" - "Note: this field is 0 when the queryed table is empty, " - "or if the query can not be optimized and requires a full table scan." + "Total number of records matching the query.
" + "Note: this field is present only if the query can be optimized and does " + "not require a full table scan." + >>, + Meta = #{desc => Desc, required => false}, + [{count, hoconsc:mk(non_neg_integer(), Meta)}]; +fields(hasnext) -> + Desc = << + "Flag indicating whether there are more results available on next pages." >>, Meta = #{desc => Desc, required => true}, - [{count, hoconsc:mk(non_neg_integer(), Meta)}]; + [{hasnext, hoconsc:mk(boolean(), Meta)}]; fields(meta) -> - fields(page) ++ fields(limit) ++ fields(count). + fields(page) ++ fields(limit) ++ fields(count) ++ fields(hasnext). -spec schema_with_example(hocon_schema:type(), term()) -> hocon_schema:field_schema_map(). schema_with_example(Type, Example) -> diff --git a/apps/emqx_management/src/emqx_mgmt_api.erl b/apps/emqx_management/src/emqx_mgmt_api.erl index 893007ebf..614949f16 100644 --- a/apps/emqx_management/src/emqx_mgmt_api.erl +++ b/apps/emqx_management/src/emqx_mgmt_api.erl @@ -20,7 +20,6 @@ -elvis([{elvis_style, dont_repeat_yourself, #{min_complexity => 100}}]). --define(FRESH_SELECT, fresh_select). -define(LONG_QUERY_TIMEOUT, 50000). -export([ @@ -174,13 +173,12 @@ do_node_query( case do_query(Node, QueryState) of {error, {badrpc, R}} -> {error, Node, {badrpc, R}}; - {Rows, NQueryState = #{continuation := ?FRESH_SELECT}} -> - {_, NResultAcc} = accumulate_query_rows(Node, Rows, NQueryState, ResultAcc), - NResultAcc; - {Rows, NQueryState} -> + {Rows, NQueryState = #{complete := Complete}} -> case accumulate_query_rows(Node, Rows, NQueryState, ResultAcc) of {enough, NResultAcc} -> - NResultAcc; + finalize_query(NResultAcc, NQueryState); + {_, NResultAcc} when Complete -> + finalize_query(NResultAcc, NQueryState); {more, NResultAcc} -> do_node_query(Node, NQueryState, NResultAcc) end @@ -212,8 +210,6 @@ cluster_query(Tab, QString, QSchema, MsFun, FmtFun) -> end. %% @private -do_cluster_query([], _QueryState, ResultAcc) -> - ResultAcc; do_cluster_query( [Node | Tail] = Nodes, QueryState, @@ -222,31 +218,29 @@ do_cluster_query( case do_query(Node, QueryState) of {error, {badrpc, R}} -> {error, Node, {badrpc, R}}; - {Rows, NQueryState} -> + {Rows, NQueryState = #{complete := Complete}} -> case accumulate_query_rows(Node, Rows, NQueryState, ResultAcc) of {enough, NResultAcc} -> - maybe_collect_total_from_tail_nodes(Tail, NQueryState, NResultAcc); + FQueryState = maybe_collect_total_from_tail_nodes(Tail, NQueryState), + FComplete = Complete andalso Tail =:= [], + finalize_query(NResultAcc, mark_complete(FQueryState, FComplete)); + {more, NResultAcc} when not Complete -> + do_cluster_query(Nodes, NQueryState, NResultAcc); + {more, NResultAcc} when Tail =/= [] -> + do_cluster_query(Tail, reset_query_state(NQueryState), NResultAcc); {more, NResultAcc} -> - NextNodes = - case NQueryState of - #{continuation := ?FRESH_SELECT} -> Tail; - _ -> Nodes - end, - do_cluster_query(NextNodes, NQueryState, NResultAcc) + finalize_query(NResultAcc, NQueryState) end end. -maybe_collect_total_from_tail_nodes([], _QueryState, ResultAcc) -> - ResultAcc; -maybe_collect_total_from_tail_nodes(Nodes, QueryState, ResultAcc) -> - case counting_total_fun(QueryState) of - false -> - ResultAcc; - _Fun -> - collect_total_from_tail_nodes(Nodes, QueryState, ResultAcc) - end. +maybe_collect_total_from_tail_nodes([], QueryState) -> + QueryState; +maybe_collect_total_from_tail_nodes(Nodes, QueryState = #{total := _}) -> + collect_total_from_tail_nodes(Nodes, QueryState); +maybe_collect_total_from_tail_nodes(_Nodes, QueryState) -> + QueryState. -collect_total_from_tail_nodes(Nodes, QueryState, ResultAcc = #{total := TotalAcc}) -> +collect_total_from_tail_nodes(Nodes, QueryState = #{total := TotalAcc}) -> %% XXX: badfun risk? if the FuzzyFun is an anonumous func in local node case rpc:multicall(Nodes, ?MODULE, apply_total_query, [QueryState], ?LONG_QUERY_TIMEOUT) of {_, [Node | _]} -> @@ -257,7 +251,8 @@ collect_total_from_tail_nodes(Nodes, QueryState, ResultAcc = #{total := TotalAcc [{Node, {badrpc, Reason}} | _] -> {error, Node, {badrpc, Reason}}; [] -> - ResultAcc#{total => ResL ++ TotalAcc} + NTotalAcc = maps:merge(TotalAcc, maps:from_list(ResL)), + QueryState#{total := NTotalAcc} end end. @@ -266,13 +261,14 @@ collect_total_from_tail_nodes(Nodes, QueryState, ResultAcc = #{total := TotalAcc %%-------------------------------------------------------------------- %% QueryState :: -%% #{continuation := ets:continuation(), +%% #{continuation => ets:continuation(), %% page := pos_integer(), %% limit := pos_integer(), -%% total := [{node(), non_neg_integer()}], +%% total => #{node() => non_neg_integer()}, %% table := atom(), -%% qs := {Qs, Fuzzy} %% parsed query params -%% msfun := query_to_match_spec_fun() +%% qs := {Qs, Fuzzy}, %% parsed query params +%% msfun := query_to_match_spec_fun(), +%% complete := boolean() %% } init_query_state(Tab, QString, MsFun, _Meta = #{page := Page, limit := Limit}) -> #{match_spec := Ms, fuzzy_fun := FuzzyFun} = erlang:apply(MsFun, [Tab, QString]), @@ -285,17 +281,31 @@ init_query_state(Tab, QString, MsFun, _Meta = #{page := Page, limit := Limit}) - true = is_list(Args), {type, external} = erlang:fun_info(NamedFun, type) end, - #{ + QueryState = #{ page => Page, limit => Limit, table => Tab, qs => QString, msfun => MsFun, - mactch_spec => Ms, + match_spec => Ms, fuzzy_fun => FuzzyFun, - total => [], - continuation => ?FRESH_SELECT - }. + complete => false + }, + case counting_total_fun(QueryState) of + false -> + QueryState; + Fun when is_function(Fun) -> + QueryState#{total => #{}} + end. + +reset_query_state(QueryState) -> + maps:remove(continuation, mark_complete(QueryState, false)). + +mark_complete(QueryState) -> + mark_complete(QueryState, true). + +mark_complete(QueryState, Complete) -> + QueryState#{complete => Complete}. %% @private This function is exempt from BPAPI do_query(Node, QueryState) when Node =:= node() -> @@ -318,47 +328,50 @@ do_select( Node, QueryState0 = #{ table := Tab, - mactch_spec := Ms, - fuzzy_fun := FuzzyFun, - continuation := Continuation, - limit := Limit + match_spec := Ms, + limit := Limit, + complete := false } ) -> QueryState = maybe_apply_total_query(Node, QueryState0), Result = - case Continuation of - ?FRESH_SELECT -> + case maps:get(continuation, QueryState, undefined) of + undefined -> ets:select(Tab, Ms, Limit); - _ -> + Continuation -> %% XXX: Repair is necessary because we pass Continuation back %% and forth through the nodes in the `do_cluster_query` ets:select(ets:repair_continuation(Continuation, Ms)) end, case Result of - '$end_of_table' -> - {[], QueryState#{continuation => ?FRESH_SELECT}}; + {Rows, '$end_of_table'} -> + NRows = maybe_apply_fuzzy_filter(Rows, QueryState), + {NRows, mark_complete(QueryState)}; {Rows, NContinuation} -> - NRows = - case FuzzyFun of - undefined -> - Rows; - {FilterFun, Args0} when is_function(FilterFun), is_list(Args0) -> - lists:filter( - fun(E) -> erlang:apply(FilterFun, [E | Args0]) end, - Rows - ) - end, - {NRows, QueryState#{continuation => NContinuation}} + NRows = maybe_apply_fuzzy_filter(Rows, QueryState), + {NRows, QueryState#{continuation => NContinuation}}; + '$end_of_table' -> + {[], mark_complete(QueryState)} end. -maybe_apply_total_query(Node, QueryState = #{total := TotalAcc}) -> - case proplists:get_value(Node, TotalAcc, undefined) of - undefined -> - Total = apply_total_query(QueryState), - QueryState#{total := [{Node, Total} | TotalAcc]}; - _ -> - QueryState - end. +maybe_apply_fuzzy_filter(Rows, #{fuzzy_fun := undefined}) -> + Rows; +maybe_apply_fuzzy_filter(Rows, #{fuzzy_fun := {FilterFun, Args}}) -> + lists:filter( + fun(E) -> erlang:apply(FilterFun, [E | Args]) end, + Rows + ). + +maybe_apply_total_query(Node, QueryState = #{total := Acc}) -> + case Acc of + #{Node := _} -> + QueryState; + #{} -> + NodeTotal = apply_total_query(QueryState), + QueryState#{total := Acc#{Node => NodeTotal}} + end; +maybe_apply_total_query(_Node, QueryState = #{}) -> + QueryState. apply_total_query(QueryState = #{table := Tab}) -> case counting_total_fun(QueryState) of @@ -371,7 +384,7 @@ apply_total_query(QueryState = #{table := Tab}) -> counting_total_fun(_QueryState = #{qs := {[], []}}) -> fun(Tab) -> ets:info(Tab, size) end; -counting_total_fun(_QueryState = #{mactch_spec := Ms, fuzzy_fun := undefined}) -> +counting_total_fun(_QueryState = #{match_spec := Ms, fuzzy_fun := undefined}) -> %% XXX: Calculating the total number of data that match a certain %% condition under a large table is very expensive because the %% entire ETS table needs to be scanned. @@ -390,15 +403,16 @@ counting_total_fun(_QueryState = #{fuzzy_fun := FuzzyFun}) when FuzzyFun =/= und %% ResultAcc :: #{count := integer(), %% cursor := integer(), %% rows := [{node(), Rows :: list()}], -%% total := [{node() => integer()}] +%% partial := boolean(), +%% hasnext => boolean() %% } init_query_result() -> - #{cursor => 0, count => 0, rows => [], total => []}. + #{cursor => 0, count => 0, rows => [], partial => false}. accumulate_query_rows( Node, Rows, - _QueryState = #{page := Page, limit := Limit, total := TotalAcc}, + _QueryState = #{page := Page, limit := Limit}, ResultAcc = #{cursor := Cursor, count := Count, rows := RowsAcc} ) -> PageStart = (Page - 1) * Limit + 1, @@ -406,12 +420,11 @@ accumulate_query_rows( Len = length(Rows), case Cursor + Len of NCursor when NCursor < PageStart -> - {more, ResultAcc#{cursor => NCursor, total => TotalAcc}}; + {more, ResultAcc#{cursor => NCursor}}; NCursor when NCursor < PageEnd -> {more, ResultAcc#{ cursor => NCursor, count => Count + length(Rows), - total => TotalAcc, rows => [{Node, Rows} | RowsAcc] }}; NCursor when NCursor >= PageEnd -> @@ -419,11 +432,21 @@ accumulate_query_rows( {enough, ResultAcc#{ cursor => NCursor, count => Count + length(SubRows), - total => TotalAcc, - rows => [{Node, SubRows} | RowsAcc] + rows => [{Node, SubRows} | RowsAcc], + partial => (Limit - Count) < Len }} end. +finalize_query(Result = #{partial := Partial}, QueryState = #{complete := Complete}) -> + HasNext = Partial orelse not Complete, + maybe_accumulate_totals(Result#{hasnext => HasNext}, QueryState). + +maybe_accumulate_totals(Result, #{total := TotalAcc}) -> + QueryTotal = maps:fold(fun(_Node, T, N) -> N + T end, 0, TotalAcc), + Result#{total => QueryTotal}; +maybe_accumulate_totals(Result, _QueryState) -> + Result. + %%-------------------------------------------------------------------- %% Internal Functions %%-------------------------------------------------------------------- @@ -520,16 +543,22 @@ is_fuzzy_key(<<"match_", _/binary>>) -> is_fuzzy_key(_) -> false. -format_query_result(_FmtFun, _Meta, Error = {error, _Node, _Reason}) -> +format_query_result(_FmtFun, _MetaIn, Error = {error, _Node, _Reason}) -> Error; format_query_result( - FmtFun, Meta, _ResultAcc = #{total := TotalAcc, rows := RowsAcc} + FmtFun, MetaIn, ResultAcc = #{hasnext := HasNext, rows := RowsAcc} ) -> - Total = lists:foldr(fun({_Node, T}, N) -> N + T end, 0, TotalAcc), + Meta = + case ResultAcc of + #{total := QueryTotal} -> + %% The `count` is used in HTTP API to indicate the total number of + %% queries that can be read + MetaIn#{hasnext => HasNext, count => QueryTotal}; + #{} -> + MetaIn#{hasnext => HasNext} + end, #{ - %% The `count` is used in HTTP API to indicate the total number of - %% queries that can be read - meta => Meta#{count => Total}, + meta => Meta, data => lists:flatten( lists:foldl( fun({Node, Rows}, Acc) -> @@ -552,7 +581,7 @@ parse_pager_params(Params) -> Limit = b2i(limit(Params)), case Page > 0 andalso Limit > 0 of true -> - #{page => Page, limit => Limit, count => 0}; + #{page => Page, limit => Limit}; false -> false end. diff --git a/apps/emqx_management/test/emqx_mgmt_api_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_api_SUITE.erl index a14305d8b..a8bbfa6d9 100644 --- a/apps/emqx_management/test/emqx_mgmt_api_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_api_SUITE.erl @@ -88,10 +88,9 @@ t_cluster_query(_Config) -> %% fuzzy searching can't return total {200, ClientsNode2} = query_clients(Node2, #{<<"like_username">> => <<"corenode2">>}), - ?assertMatch( - #{count := 0}, - maps:get(meta, ClientsNode2) - ), + MetaNode2 = maps:get(meta, ClientsNode2), + ?assertNotMatch(#{count := _}, MetaNode2), + ?assertMatch(#{hasnext := false}, MetaNode2), ?assertMatch(10, length(maps:get(data, ClientsNode2))), _ = lists:foreach(fun(C) -> emqtt:disconnect(C) end, ClientLs1), diff --git a/apps/emqx_management/test/emqx_mgmt_api_subscription_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_api_subscription_SUITE.erl index 965ed0997..2ab213e30 100644 --- a/apps/emqx_management/test/emqx_mgmt_api_subscription_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_api_subscription_SUITE.erl @@ -44,9 +44,8 @@ init_per_suite(Config) -> end_per_suite(_) -> emqx_mgmt_api_test_util:end_suite(). -t_subscription_api(_) -> - {ok, Client} = emqtt:start_link(#{username => ?USERNAME, clientid => ?CLIENTID, proto_ver => v5}), - {ok, _} = emqtt:connect(Client), +t_subscription_api(Config) -> + Client = proplists:get_value(client, Config), {ok, _, _} = emqtt:subscribe( Client, [ {?TOPIC1, [{rh, ?TOPIC1RH}, {rap, ?TOPIC1RAP}, {nl, ?TOPIC1NL}, {qos, ?TOPIC1QOS}]} @@ -84,40 +83,78 @@ t_subscription_api(_) -> ?assertEqual(maps:get(<<"topic">>, Subscriptions2), ?TOPIC2), ?assertEqual(maps:get(<<"clientid">>, Subscriptions2), ?CLIENTID), - QS = uri_string:compose_query([ + QS = [ {"clientid", ?CLIENTID}, {"topic", ?TOPIC2_TOPIC_ONLY}, {"node", atom_to_list(node())}, {"qos", "0"}, {"share_group", "test_group"}, {"match_topic", "t/#"} - ]), + ], Headers = emqx_mgmt_api_test_util:auth_header_(), - {ok, ResponseTopic2} = emqx_mgmt_api_test_util:request_api(get, Path, QS, Headers), - DataTopic2 = emqx_json:decode(ResponseTopic2, [return_maps]), - Meta2 = maps:get(<<"meta">>, DataTopic2), + DataTopic2 = #{<<"meta">> := Meta2} = request_json(get, QS, Headers), ?assertEqual(1, maps:get(<<"page">>, Meta2)), ?assertEqual(emqx_mgmt:max_row_limit(), maps:get(<<"limit">>, Meta2)), ?assertEqual(1, maps:get(<<"count">>, Meta2)), SubscriptionsList2 = maps:get(<<"data">>, DataTopic2), - ?assertEqual(length(SubscriptionsList2), 1), + ?assertEqual(length(SubscriptionsList2), 1). - MatchQs = uri_string:compose_query([ +t_subscription_fuzzy_search(Config) -> + Client = proplists:get_value(client, Config), + Topics = [ + <<"t/foo">>, + <<"t/foo/bar">>, + <<"t/foo/baz">>, + <<"topic/foo/bar">>, + <<"topic/foo/baz">> + ], + _ = [{ok, _, _} = emqtt:subscribe(Client, T) || T <- Topics], + + Headers = emqx_mgmt_api_test_util:auth_header_(), + MatchQs = [ {"clientid", ?CLIENTID}, {"node", atom_to_list(node())}, - {"qos", "0"}, {"match_topic", "t/#"} - ]), + ], - {ok, MatchRes} = emqx_mgmt_api_test_util:request_api(get, Path, MatchQs, Headers), - MatchData = emqx_json:decode(MatchRes, [return_maps]), - MatchMeta = maps:get(<<"meta">>, MatchData), - ?assertEqual(1, maps:get(<<"page">>, MatchMeta)), - ?assertEqual(emqx_mgmt:max_row_limit(), maps:get(<<"limit">>, MatchMeta)), - %% count equals 0 in fuzzy searching - ?assertEqual(0, maps:get(<<"count">>, MatchMeta)), - MatchSubs = maps:get(<<"data">>, MatchData), - ?assertEqual(1, length(MatchSubs)), + MatchData1 = #{<<"meta">> := MatchMeta1} = request_json(get, MatchQs, Headers), + ?assertEqual(1, maps:get(<<"page">>, MatchMeta1)), + ?assertEqual(emqx_mgmt:max_row_limit(), maps:get(<<"limit">>, MatchMeta1)), + %% count is undefined in fuzzy searching + ?assertNot(maps:is_key(<<"count">>, MatchMeta1)), + ?assertMatch(3, length(maps:get(<<"data">>, MatchData1))), + ?assertEqual(false, maps:get(<<"hasnext">>, MatchMeta1)), + LimitMatchQuery = [ + {"clientid", ?CLIENTID}, + {"match_topic", "+/+/+"}, + {"limit", "3"} + ], + + MatchData2 = #{<<"meta">> := MatchMeta2} = request_json(get, LimitMatchQuery, Headers), + ?assertEqual(#{<<"page">> => 1, <<"limit">> => 3, <<"hasnext">> => true}, MatchMeta2), + ?assertEqual(3, length(maps:get(<<"data">>, MatchData2))), + + MatchData2P2 = + #{<<"meta">> := MatchMeta2P2} = + request_json(get, [{"page", "2"} | LimitMatchQuery], Headers), + ?assertEqual(#{<<"page">> => 2, <<"limit">> => 3, <<"hasnext">> => false}, MatchMeta2P2), + ?assertEqual(1, length(maps:get(<<"data">>, MatchData2P2))). + +request_json(Method, Query, Headers) when is_list(Query) -> + Qs = uri_string:compose_query(Query), + {ok, MatchRes} = emqx_mgmt_api_test_util:request_api(Method, path(), Qs, Headers), + emqx_json:decode(MatchRes, [return_maps]). + +path() -> + emqx_mgmt_api_test_util:api_path(["subscriptions"]). + +init_per_testcase(_TC, Config) -> + {ok, Client} = emqtt:start_link(#{username => ?USERNAME, clientid => ?CLIENTID, proto_ver => v5}), + {ok, _} = emqtt:connect(Client), + [{client, Client} | Config]. + +end_per_testcase(_TC, Config) -> + Client = proplists:get_value(client, Config), emqtt:disconnect(Client). From abf6f143e5e4b66a4fe42b286d2d7dabceaeca99 Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Wed, 11 Jan 2023 16:59:16 +0300 Subject: [PATCH 0115/1802] fix(paging): return subset of rows if page start is in the middle This change should cover this example scenario where select ended returning 4 rows. Row 3 Row 4 Row 5 <- page start Row 6 Here, only rows 5 and 6 should end up in the response. Before this change some rows could be duplicated across adjacent search pages. --- apps/emqx_management/src/emqx_mgmt_api.erl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/emqx_management/src/emqx_mgmt_api.erl b/apps/emqx_management/src/emqx_mgmt_api.erl index 614949f16..5f4d8f78a 100644 --- a/apps/emqx_management/src/emqx_mgmt_api.erl +++ b/apps/emqx_management/src/emqx_mgmt_api.erl @@ -422,10 +422,11 @@ accumulate_query_rows( NCursor when NCursor < PageStart -> {more, ResultAcc#{cursor => NCursor}}; NCursor when NCursor < PageEnd -> + SubRows = lists:nthtail(max(0, PageStart - Cursor - 1), Rows), {more, ResultAcc#{ cursor => NCursor, - count => Count + length(Rows), - rows => [{Node, Rows} | RowsAcc] + count => Count + length(SubRows), + rows => [{Node, SubRows} | RowsAcc] }}; NCursor when NCursor >= PageEnd -> SubRows = lists:sublist(Rows, Limit - Count), From c89b2276872d0d9f9b86bb80e45f8e2c51eb4e79 Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Wed, 11 Jan 2023 17:38:45 +0300 Subject: [PATCH 0116/1802] fix(test): adapt affected testcases --- apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl | 6 +++--- .../test/emqx_enhanced_authn_scram_mnesia_SUITE.erl | 6 +++--- apps/emqx_authz/test/emqx_authz_api_mnesia_SUITE.erl | 7 ++++--- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl b/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl index 8191fe2e9..cd97a15d9 100644 --- a/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl @@ -197,7 +197,7 @@ t_list_users(_) -> #{is_superuser := false, user_id := _}, #{is_superuser := false, user_id := _} ], - meta := #{page := 1, limit := 2, count := 3} + meta := #{page := 1, limit := 2, count := 3, hasnext := true} } = emqx_authn_mnesia:list_users( #{<<"page">> => 1, <<"limit">> => 2}, State @@ -205,7 +205,7 @@ t_list_users(_) -> #{ data := [#{is_superuser := false, user_id := _}], - meta := #{page := 2, limit := 2, count := 3} + meta := #{page := 2, limit := 2, count := 3, hasnext := false} } = emqx_authn_mnesia:list_users( #{<<"page">> => 2, <<"limit">> => 2}, State @@ -213,7 +213,7 @@ t_list_users(_) -> #{ data := [#{is_superuser := false, user_id := <<"u3">>}], - meta := #{page := 1, limit := 20, count := 0} + meta := #{page := 1, limit := 20, hasnext := false} } = emqx_authn_mnesia:list_users( #{ <<"page">> => 1, diff --git a/apps/emqx_authn/test/emqx_enhanced_authn_scram_mnesia_SUITE.erl b/apps/emqx_authn/test/emqx_enhanced_authn_scram_mnesia_SUITE.erl index e1a2586cd..b143903b5 100644 --- a/apps/emqx_authn/test/emqx_enhanced_authn_scram_mnesia_SUITE.erl +++ b/apps/emqx_authn/test/emqx_enhanced_authn_scram_mnesia_SUITE.erl @@ -300,14 +300,14 @@ t_list_users(_) -> #{ data := [?USER_MAP, ?USER_MAP], - meta := #{page := 1, limit := 2, count := 3} + meta := #{page := 1, limit := 2, count := 3, hasnext := true} } = emqx_enhanced_authn_scram_mnesia:list_users( #{<<"page">> => 1, <<"limit">> => 2}, State ), #{ data := [?USER_MAP], - meta := #{page := 2, limit := 2, count := 3} + meta := #{page := 2, limit := 2, count := 3, hasnext := false} } = emqx_enhanced_authn_scram_mnesia:list_users( #{<<"page">> => 2, <<"limit">> => 2}, State @@ -319,7 +319,7 @@ t_list_users(_) -> is_superuser := _ } ], - meta := #{page := 1, limit := 3, count := 0} + meta := #{page := 1, limit := 3, hasnext := false} } = emqx_enhanced_authn_scram_mnesia:list_users( #{ <<"page">> => 1, diff --git a/apps/emqx_authz/test/emqx_authz_api_mnesia_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_mnesia_SUITE.erl index 62c747433..1819cf66f 100644 --- a/apps/emqx_authz/test/emqx_authz_api_mnesia_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_api_mnesia_SUITE.erl @@ -92,7 +92,8 @@ t_api(_) -> <<"meta">> := #{ <<"count">> := 1, <<"limit">> := 100, - <<"page">> := 1 + <<"page">> := 1, + <<"hasnext">> := false } } = jsx:decode(Request1), ?assertEqual(3, length(Rules1)), @@ -111,9 +112,9 @@ t_api(_) -> #{ <<"data">> := [], <<"meta">> := #{ - <<"count">> := 0, <<"limit">> := 20, - <<"page">> := 1 + <<"page">> := 1, + <<"hasnext">> := false } } = jsx:decode(Request1_1), From 50c9321b9ed969a618691d809db590b28e37619b Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Wed, 11 Jan 2023 22:47:28 +0300 Subject: [PATCH 0117/1802] fix(api): anticipate empty set of nodes handling cluster query --- apps/emqx_management/src/emqx_mgmt_api.erl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/emqx_management/src/emqx_mgmt_api.erl b/apps/emqx_management/src/emqx_mgmt_api.erl index 5f4d8f78a..9a1d0ce4e 100644 --- a/apps/emqx_management/src/emqx_mgmt_api.erl +++ b/apps/emqx_management/src/emqx_mgmt_api.erl @@ -210,6 +210,8 @@ cluster_query(Tab, QString, QSchema, MsFun, FmtFun) -> end. %% @private +do_cluster_query([], QueryState, ResultAcc) -> + finalize_query(ResultAcc, mark_complete(QueryState)); do_cluster_query( [Node | Tail] = Nodes, QueryState, From 82e80e56104e83c533fbccaae2649d11866a9900 Mon Sep 17 00:00:00 2001 From: Kjell Winblad Date: Thu, 12 Jan 2023 09:01:11 +0100 Subject: [PATCH 0118/1802] fix: remove password from info logs Information log messages for several connectors contained sensitive password information. This commit fixes this by hiding the sensitive information from the log messages. Fixes: https://emqx.atlassian.net/browse/EMQX-8718 --- apps/emqx_connector/src/emqx_connector_http.erl | 2 +- apps/emqx_connector/src/emqx_connector_ldap.erl | 2 +- apps/emqx_connector/src/emqx_connector_mongo.erl | 2 +- apps/emqx_connector/src/emqx_connector_mqtt.erl | 2 +- apps/emqx_connector/src/emqx_connector_mysql.erl | 2 +- apps/emqx_connector/src/emqx_connector_pgsql.erl | 2 +- apps/emqx_connector/src/emqx_connector_redis.erl | 2 +- changes/v5.0.15/fix-8718-en.md | 1 + changes/v5.0.15/fix-8718-zh.md | 1 + 9 files changed, 9 insertions(+), 7 deletions(-) create mode 100644 changes/v5.0.15/fix-8718-en.md create mode 100644 changes/v5.0.15/fix-8718-zh.md diff --git a/apps/emqx_connector/src/emqx_connector_http.erl b/apps/emqx_connector/src/emqx_connector_http.erl index a04850746..817cd44f6 100644 --- a/apps/emqx_connector/src/emqx_connector_http.erl +++ b/apps/emqx_connector/src/emqx_connector_http.erl @@ -209,7 +209,7 @@ on_start( ?SLOG(info, #{ msg => "starting_http_connector", connector => InstId, - config => Config + config => emqx_misc:redact(Config) }), {Transport, TransportOpts} = case Scheme of diff --git a/apps/emqx_connector/src/emqx_connector_ldap.erl b/apps/emqx_connector/src/emqx_connector_ldap.erl index 1930d9e68..82d622e09 100644 --- a/apps/emqx_connector/src/emqx_connector_ldap.erl +++ b/apps/emqx_connector/src/emqx_connector_ldap.erl @@ -65,7 +65,7 @@ on_start( ?SLOG(info, #{ msg => "starting_ldap_connector", connector => InstId, - config => Config + config => emqx_misc:redact(Config) }), Servers = emqx_schema:parse_servers(Servers0, ?LDAP_HOST_OPTIONS), SslOpts = diff --git a/apps/emqx_connector/src/emqx_connector_mongo.erl b/apps/emqx_connector/src/emqx_connector_mongo.erl index 0bcc39208..1b0bcf94d 100644 --- a/apps/emqx_connector/src/emqx_connector_mongo.erl +++ b/apps/emqx_connector/src/emqx_connector_mongo.erl @@ -155,7 +155,7 @@ on_start( rs -> "starting_mongodb_replica_set_connector"; sharded -> "starting_mongodb_sharded_connector" end, - ?SLOG(info, #{msg => Msg, connector => InstId, config => Config}), + ?SLOG(info, #{msg => Msg, connector => InstId, config => emqx_misc:redact(Config)}), NConfig = #{hosts := Hosts} = maybe_resolve_srv_and_txt_records(Config), SslOpts = case maps:get(enable, SSL) of diff --git a/apps/emqx_connector/src/emqx_connector_mqtt.erl b/apps/emqx_connector/src/emqx_connector_mqtt.erl index c28aa2514..585122539 100644 --- a/apps/emqx_connector/src/emqx_connector_mqtt.erl +++ b/apps/emqx_connector/src/emqx_connector_mqtt.erl @@ -149,7 +149,7 @@ on_start(InstId, Conf) -> ?SLOG(info, #{ msg => "starting_mqtt_connector", connector => InstanceId, - config => Conf + config => emqx_misc:redact(Conf) }), BasicConf = basic_config(Conf), BridgeConf = BasicConf#{ diff --git a/apps/emqx_connector/src/emqx_connector_mysql.erl b/apps/emqx_connector/src/emqx_connector_mysql.erl index d119a01e9..ea6a84240 100644 --- a/apps/emqx_connector/src/emqx_connector_mysql.erl +++ b/apps/emqx_connector/src/emqx_connector_mysql.erl @@ -91,7 +91,7 @@ on_start( ?SLOG(info, #{ msg => "starting_mysql_connector", connector => InstId, - config => Config + config => emqx_misc:redact(Config) }), SslOpts = case maps:get(enable, SSL) of diff --git a/apps/emqx_connector/src/emqx_connector_pgsql.erl b/apps/emqx_connector/src/emqx_connector_pgsql.erl index 25a22455d..9965ff3b4 100644 --- a/apps/emqx_connector/src/emqx_connector_pgsql.erl +++ b/apps/emqx_connector/src/emqx_connector_pgsql.erl @@ -94,7 +94,7 @@ on_start( ?SLOG(info, #{ msg => "starting_postgresql_connector", connector => InstId, - config => Config + config => emqx_misc:redact(Config) }), SslOpts = case maps:get(enable, SSL) of diff --git a/apps/emqx_connector/src/emqx_connector_redis.erl b/apps/emqx_connector/src/emqx_connector_redis.erl index 6143dbf0c..7fdf9d28d 100644 --- a/apps/emqx_connector/src/emqx_connector_redis.erl +++ b/apps/emqx_connector/src/emqx_connector_redis.erl @@ -123,7 +123,7 @@ on_start( ?SLOG(info, #{ msg => "starting_redis_connector", connector => InstId, - config => Config + config => emqx_misc:redact(Config) }), ConfKey = case Type of diff --git a/changes/v5.0.15/fix-8718-en.md b/changes/v5.0.15/fix-8718-en.md new file mode 100644 index 000000000..6085adecd --- /dev/null +++ b/changes/v5.0.15/fix-8718-en.md @@ -0,0 +1 @@ +Password information has been removed from information log messages for http, ldap, mongo, mqtt, mysql, pgsql and redis. diff --git a/changes/v5.0.15/fix-8718-zh.md b/changes/v5.0.15/fix-8718-zh.md new file mode 100644 index 000000000..d8aa81fd1 --- /dev/null +++ b/changes/v5.0.15/fix-8718-zh.md @@ -0,0 +1 @@ +密码信息已从http、ldap、mongo、mqtt、mysql、pgsql和redis的信息日志消息中删除。 From 49e1f5176527069dc79805da569d0919406b0c10 Mon Sep 17 00:00:00 2001 From: Ivan Dyachkov Date: Thu, 29 Dec 2022 20:30:32 +0100 Subject: [PATCH 0119/1802] ci: simplify build_and_push_docker_images workflow --- .github/actions/docker-meta/action.yaml | 81 ----- .../build_and_push_docker_images.yaml | 314 ++++-------------- deploy/docker/Dockerfile | 3 +- deploy/docker/Dockerfile.alpine | 3 +- scripts/docker-create-push-manifests.sh | 27 -- 5 files changed, 76 insertions(+), 352 deletions(-) delete mode 100644 .github/actions/docker-meta/action.yaml delete mode 100755 scripts/docker-create-push-manifests.sh diff --git a/.github/actions/docker-meta/action.yaml b/.github/actions/docker-meta/action.yaml deleted file mode 100644 index 13ab21da6..000000000 --- a/.github/actions/docker-meta/action.yaml +++ /dev/null @@ -1,81 +0,0 @@ -name: 'Docker meta' -inputs: - profile: - required: true - type: string - registry: - required: true - type: string - arch: - required: true - type: string - otp: - required: true - type: string - elixir: - required: false - type: string - default: '' - builder_base: - required: true - type: string - owner: - required: true - type: string - docker_tags: - required: true - type: string - -outputs: - emqx_name: - description: "EMQX name" - value: ${{ steps.pre-meta.outputs.emqx_name }} - version: - description: "docker image version" - value: ${{ steps.meta.outputs.version }} - tags: - description: "docker image tags" - value: ${{ steps.meta.outputs.tags }} - labels: - description: "docker image labels" - value: ${{ steps.meta.outputs.labels }} - -runs: - using: composite - steps: - - name: prepare for docker/metadata-action - id: pre-meta - shell: bash - run: | - emqx_name=${{ inputs.profile }} - img_suffix=${{ inputs.arch }} - img_labels="org.opencontainers.image.otp.version=${{ inputs.otp }}" - if [ -n "${{ inputs.elixir }}" ]; then - emqx_name="emqx-elixir" - img_suffix="elixir-${{ inputs.arch }}" - img_labels="org.opencontainers.image.elixir.version=${{ inputs.elixir }}\n${img_labels}" - fi - if [ "${{ inputs.profile }}" = "emqx" ]; then - img_labels="org.opencontainers.image.edition=Opensource\n${img_labels}" - fi - if [ "${{ inputs.profile }}" = "emqx-enterprise" ]; then - img_labels="org.opencontainers.image.edition=Enterprise\n${img_labels}" - fi - if [[ "${{ inputs.builder_base }}" =~ "alpine" ]]; then - img_suffix="${img_suffix}-alpine" - fi - echo "emqx_name=${emqx_name}" >> $GITHUB_OUTPUT - echo "img_suffix=${img_suffix}" >> $GITHUB_OUTPUT - echo "img_labels=${img_labels}" >> $GITHUB_OUTPUT - echo "img_name=${{ inputs.registry }}/${{ inputs.owner }}/${{ inputs.profile }}" >> $GITHUB_OUTPUT - - uses: docker/metadata-action@v4 - id: meta - with: - images: - ${{ steps.pre-meta.outputs.img_name }} - flavor: | - suffix=-${{ steps.pre-meta.outputs.img_suffix }} - tags: | - type=raw,value=${{ inputs.docker_tags }} - labels: - ${{ steps.pre-meta.outputs.img_labels }} diff --git a/.github/workflows/build_and_push_docker_images.yaml b/.github/workflows/build_and_push_docker_images.yaml index 76238c75f..71515f699 100644 --- a/.github/workflows/build_and_push_docker_images.yaml +++ b/.github/workflows/build_and_push_docker_images.yaml @@ -9,15 +9,17 @@ on: tags: - v* - e* - release: - types: - - published + - docker-latest-* workflow_dispatch: inputs: branch_or_tag: required: false profile: required: false + default: 'emqx' + is_latest: + required: false + default: false jobs: prepare: @@ -26,10 +28,11 @@ jobs: container: "ghcr.io/emqx/emqx-builder/5.0-26:1.13.4-24.3.4.2-1-ubuntu20.04" outputs: - BUILD_PROFILE: ${{ steps.get_profile.outputs.BUILD_PROFILE }} - IS_DOCKER_LATEST: ${{ steps.get_profile.outputs.IS_DOCKER_LATEST }} + PROFILE: ${{ steps.get_profile.outputs.PROFILE }} + EDITION: ${{ steps.get_profile.outputs.EDITION }} + IS_LATEST: ${{ steps.get_profile.outputs.IS_LATEST }} IS_EXACT_TAG: ${{ steps.get_profile.outputs.IS_EXACT_TAG }} - DOCKER_TAG_VERSION: ${{ steps.get_profile.outputs.DOCKER_TAG_VERSION }} + VERSION: ${{ steps.get_profile.outputs.VERSION }} steps: - uses: actions/checkout@v3 @@ -45,14 +48,14 @@ jobs: tag=${{ github.ref }} # tag docker-latest-ce or docker-latest-ee if git describe --tags --exact --match 'docker-latest-*' 2>/dev/null; then - echo 'docker_latest=true due to docker-latest-* tag' - docker_latest=true - elif [ "${{ github.event_name }}" = "release" ]; then - echo 'docker_latest=true due to release' - docker_latest=true + echo 'is_latest=true due to docker-latest-* tag' + is_latest=true + elif [ "${{ inputs.is_latest }}" = "true" ]; then + echo 'is_latest=true due to manual input from workflow_dispatch' + is_latest=true else - echo 'docker_latest=false' - docker_latest=false + echo 'is_latest=false' + is_latest=false fi if git describe --tags --match "[v|e]*" --exact; then echo "This is an exact git tag, will publish images" @@ -64,18 +67,20 @@ jobs: case $tag in refs/tags/v*) PROFILE='emqx' + EDITION='Opensource' ;; refs/tags/e*) PROFILE=emqx-enterprise + EDITION='Enterprise' ;; *) PROFILE=${{ github.event.inputs.profile }} case "$PROFILE" in emqx) - true + EDITION='Opensource' ;; emqx-enterprise) - true + EDITION='Enterprise' ;; *) echo "ERROR: Failed to resolve build profile" @@ -85,14 +90,18 @@ jobs: ;; esac VSN="$(./pkg-vsn.sh "$PROFILE")" - echo "Building $PROFILE image with tag $VSN (latest=$docker_latest)" - echo "IS_DOCKER_LATEST=$docker_latest" >> $GITHUB_OUTPUT + echo "Building emqx/$PROFILE:$VSN image (latest=$is_latest)" + echo "Push = $is_exact" + echo "IS_LATEST=$is_latest" >> $GITHUB_OUTPUT echo "IS_EXACT_TAG=$is_exact" >> $GITHUB_OUTPUT - echo "BUILD_PROFILE=$PROFILE" >> $GITHUB_OUTPUT - echo "DOCKER_TAG_VERSION=$VSN" >> $GITHUB_OUTPUT + echo "PROFILE=$PROFILE" >> $GITHUB_OUTPUT + echo "EDITION=$EDITION" >> $GITHUB_OUTPUT + echo "VERSION=$VSN" >> $GITHUB_OUTPUT - name: get_all_deps + env: + PROFILE: ${{ steps.get_profile.outputs.PROFILE }} run: | - make -C source deps-all + PROFILE=$PROFILE make -C source deps-$PROFILE zip -ryq source.zip source/* source/.[^.]* - uses: actions/upload-artifact@v3 with: @@ -100,17 +109,17 @@ jobs: path: source.zip docker: - runs-on: ${{ matrix.arch[1] }} + runs-on: ubuntu-20.04 needs: prepare strategy: fail-fast: false matrix: - arch: - - [amd64, ubuntu-20.04] - - [arm64, aws-arm64] profile: - - ${{ needs.prepare.outputs.BUILD_PROFILE }} + - "${{ needs.prepare.outputs.PROFILE }}" + flavor: + - '' + - '-elixir' registry: - 'docker.io' - 'public.ecr.aws' @@ -128,9 +137,10 @@ jobs: exclude: # TODO: publish enterprise to ecr too? - registry: 'public.ecr.aws' profile: emqx-enterprise + - flavor: '-elixir' + os: [alpine3.15.1, "alpine:3.15.1", "deploy/docker/Dockerfile.alpine"] + steps: - - uses: AutoModality/action-clean@v1 - if: matrix.arch[1] == 'aws-arm64' - uses: actions/download-artifact@v3 with: name: source @@ -138,16 +148,17 @@ jobs: - name: unzip source code run: unzip -q source.zip + - uses: docker/setup-qemu-action@v2 - uses: docker/setup-buildx-action@v2 - - name: Login for docker. + - name: Login to hub.docker.com uses: docker/login-action@v2 if: matrix.registry == 'docker.io' with: username: ${{ secrets.DOCKER_HUB_USER }} password: ${{ secrets.DOCKER_HUB_TOKEN }} - - name: Login for AWS ECR + - name: Login to AWS ECR uses: docker/login-action@v2 if: matrix.registry == 'public.ecr.aws' with: @@ -156,229 +167,48 @@ jobs: password: ${{ secrets.AWS_SECRET_ACCESS_KEY }} ecr: true - - uses: ./source/.github/actions/docker-meta + - name: prepare for docker/metadata-action + id: pre-meta + shell: bash + run: | + extra_labels= + img_suffix= + flavor="${{ matrix.flavor }}" + if [ "${{ matrix.flavor }}" = '-elixir' ]; then + img_suffix="-elixir" + extra_labels="org.opencontainers.image.elixir.version=${{ matrix.elixir }}" + fi + if [[ "${{ matrix.os[0] }}" =~ "alpine" ]]; then + img_suffix="${img_suffix}-alpine" + fi + + echo "img_suffix=$img_suffix" >> $GITHUB_OUTPUT + echo "extra_labels=$extra_labels" >> $GITHUB_OUTPUT + + - uses: docker/metadata-action@v4 id: meta with: - profile: ${{ matrix.profile }} - registry: ${{ matrix.registry }} - arch: ${{ matrix.arch[0] }} - otp: ${{ matrix.otp }} - builder_base: ${{ matrix.os[0] }} - owner: ${{ github.repository_owner }} - docker_tags: ${{ needs.prepare.outputs.DOCKER_TAG_VERSION }} + images: | + ${{ matrix.registry }}/${{ github.repository_owner }}/${{ matrix.profile }} + flavor: | + suffix=${{ steps.pre-meta.outputs.img_suffix }} + tags: | + type=raw,value=${{ needs.prepare.outputs.VERSION }} + type=raw,value=latest,enable=${{ needs.prepare.outputs.IS_LATEST }} + labels: | + org.opencontainers.image.otp.version=${{ matrix.otp }} + org.opencontainers.image.edition=${{ needs.prepare.outputs.EDITION }} + ${{ steps.pre-meta.outputs.extra_labels }} - uses: docker/build-push-action@v3 with: push: ${{ needs.prepare.outputs.IS_EXACT_TAG == 'true' || github.repository_owner != 'emqx' }} pull: true no-cache: true - platforms: linux/${{ matrix.arch[0] }} + platforms: linux/amd64,linux/arm64 tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} build-args: | - BUILD_FROM=ghcr.io/emqx/emqx-builder/${{ matrix.builder }}:${{ matrix.elixir }}-${{ matrix.otp }}-${{ matrix.os[0] }} - RUN_FROM=${{ matrix.os[1] }} - EMQX_NAME=${{ steps.meta.outputs.emqx_name }} + EMQX_NAME=${{ matrix.profile }}${{ matrix.flavor }} file: source/${{ matrix.os[2] }} context: source - - - name: Docker Hub Description - if: matrix.registry == 'docker.io' - uses: peter-evans/dockerhub-description@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_PASSWORD }} - repository: "emqx/${{ needs.prepare.outputs.BUILD_PROFILE }}" - readme-filepath: ./source/deploy/docker/README.md - short-description: "The most scalable open-source MQTT broker for IoT, IIoT, connected vehicles, and more." - - docker-elixir: - runs-on: ${{ matrix.arch[1] }} - needs: prepare - # do not build elixir images for ee for now - if: needs.prepare.outputs.BUILD_PROFILE == 'emqx' - - strategy: - fail-fast: false - matrix: - arch: - - [amd64, ubuntu-20.04] - - [arm64, aws-arm64] - profile: - - ${{ needs.prepare.outputs.BUILD_PROFILE }} - registry: - - 'docker.io' - os: - - [debian11, "debian:11-slim", "deploy/docker/Dockerfile"] - builder: - - 5.0-26 # update to latest - otp: - - 25.1.2-2 # update to latest - elixir: - - 1.13.4 # update to latest - - steps: - - uses: AutoModality/action-clean@v1 - if: matrix.arch[1] == 'aws-arm64' - - uses: actions/download-artifact@v3 - with: - name: source - path: . - - name: unzip source code - run: unzip -q source.zip - - - uses: docker/setup-buildx-action@v2 - - - name: Login for docker. - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKER_HUB_USER }} - password: ${{ secrets.DOCKER_HUB_TOKEN }} - - - uses: ./source/.github/actions/docker-meta - id: meta - with: - profile: ${{ matrix.profile }} - registry: ${{ matrix.registry }} - arch: ${{ matrix.arch[0] }} - otp: ${{ matrix.otp }} - elixir: ${{ matrix.elixir }} - builder_base: ${{ matrix.os[0] }} - owner: ${{ github.repository_owner }} - docker_tags: ${{ needs.prepare.outputs.DOCKER_TAG_VERSION }} - - - uses: docker/build-push-action@v3 - with: - push: ${{ needs.prepare.outputs.IS_EXACT_TAG == 'true' || github.repository_owner != 'emqx' }} - pull: true - no-cache: true - platforms: linux/${{ matrix.arch[0] }} - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - build-args: | - BUILD_FROM=ghcr.io/emqx/emqx-builder/${{ matrix.builder }}:${{ matrix.elixir }}-${{ matrix.otp }}-${{ matrix.os[0] }} - RUN_FROM=${{ matrix.os[1] }} - EMQX_NAME=${{ steps.meta.outputs.emqx_name }} - file: source/${{ matrix.os[2] }} - context: source - - docker-push-multi-arch-manifest: - # note, we only run on amd64 - if: needs.prepare.outputs.IS_EXACT_TAG - needs: - - prepare - - docker - runs-on: ${{ matrix.arch[1] }} - strategy: - fail-fast: false - matrix: - arch: - - [amd64, ubuntu-20.04] - profile: - - ${{ needs.prepare.outputs.BUILD_PROFILE }} - os: - - [alpine3.15.1, "alpine:3.15.1", "deploy/docker/Dockerfile.alpine"] - - [debian11, "debian:11-slim", "deploy/docker/Dockerfile"] - # NOTE: only support latest otp version, not a matrix - otp: - - 24.3.4.2-1 # switch to 25 once ready to release 5.1 - registry: - - 'docker.io' - - 'public.ecr.aws' - exclude: - - registry: 'public.ecr.aws' - profile: emqx-enterprise - - steps: - - uses: actions/download-artifact@v3 - with: - name: source - path: . - - - name: unzip source code - run: unzip -q source.zip - - - uses: docker/login-action@v2 - if: matrix.registry == 'docker.io' - with: - username: ${{ secrets.DOCKER_HUB_USER }} - password: ${{ secrets.DOCKER_HUB_TOKEN }} - - - uses: docker/login-action@v2 - if: matrix.registry == 'public.ecr.aws' - with: - registry: public.ecr.aws - username: ${{ secrets.AWS_ACCESS_KEY_ID }} - password: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - ecr: true - - - uses: ./source/.github/actions/docker-meta - id: meta - with: - profile: ${{ matrix.profile }} - registry: ${{ matrix.registry }} - arch: ${{ matrix.arch[0] }} - otp: ${{ matrix.otp }} - builder_base: ${{ matrix.os[0] }} - owner: ${{ github.repository_owner }} - docker_tags: ${{ needs.prepare.outputs.DOCKER_TAG_VERSION }} - - - name: update manifest for multiarch image - working-directory: source - run: | - is_latest="${{ needs.prepare.outputs.IS_DOCKER_LATEST }}" - scripts/docker-create-push-manifests.sh "${{ steps.meta.outputs.tags }}" "$is_latest" - - docker-elixir-push-multi-arch-manifest: - # note, we only run on amd64 - # do not build enterprise elixir images for now - if: needs.prepare.outputs.IS_EXACT_TAG == 'true' && needs.prepare.outputs.BUILD_PROFILE == 'emqx' - needs: - - prepare - - docker-elixir - runs-on: ${{ matrix.arch[1] }} - strategy: - fail-fast: false - matrix: - arch: - - [amd64, ubuntu-20.04] - profile: - - ${{ needs.prepare.outputs.BUILD_PROFILE }} - # NOTE: for docker, only support latest otp version, not a matrix - otp: - - 25.1.2-2 # update to latest - elixir: - - 1.13.4 # update to latest - registry: - - 'docker.io' - - steps: - - uses: actions/download-artifact@v3 - with: - name: source - path: . - - - name: unzip source code - run: unzip -q source.zip - - - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKER_HUB_USER }} - password: ${{ secrets.DOCKER_HUB_TOKEN }} - - - uses: ./source/.github/actions/docker-meta - id: meta - with: - profile: ${{ matrix.profile }} - registry: ${{ matrix.registry }} - arch: ${{ matrix.arch[0] }} - otp: ${{ matrix.otp }} - elixir: ${{ matrix.elixir }} - builder_base: ${{ matrix.os[0] }} - owner: ${{ github.repository_owner }} - docker_tags: ${{ needs.prepare.outputs.DOCKER_TAG_VERSION }} - - - name: update manifest for multiarch image - working-directory: source - run: | - scripts/docker-create-push-manifests.sh "${{ steps.meta.outputs.tags }}" false diff --git a/deploy/docker/Dockerfile b/deploy/docker/Dockerfile index 4a00c68fb..03533eec4 100644 --- a/deploy/docker/Dockerfile +++ b/deploy/docker/Dockerfile @@ -1,6 +1,7 @@ ARG BUILD_FROM=ghcr.io/emqx/emqx-builder/5.0-26:1.13.4-24.3.4.2-1-debian11 ARG RUN_FROM=debian:11-slim -FROM ${BUILD_FROM} AS builder +ARG BUILDPLATFORM=linux/amd64 +FROM --platform=$BUILDPLATFORM ${BUILD_FROM} AS builder COPY . /emqx diff --git a/deploy/docker/Dockerfile.alpine b/deploy/docker/Dockerfile.alpine index 0f72be9ab..ebce2f539 100644 --- a/deploy/docker/Dockerfile.alpine +++ b/deploy/docker/Dockerfile.alpine @@ -1,6 +1,7 @@ ARG BUILD_FROM=ghcr.io/emqx/emqx-builder/5.0-26:1.13.4-24.3.4.2-1-alpine3.15.1 ARG RUN_FROM=alpine:3.15.1 -FROM ${BUILD_FROM} AS builder +ARG BUILDPLATFORM=linux/amd64 +FROM --platform=$BUILDPLATFORM ${BUILD_FROM} AS builder RUN apk add --no-cache \ autoconf \ diff --git a/scripts/docker-create-push-manifests.sh b/scripts/docker-create-push-manifests.sh deleted file mode 100755 index db9c01bfb..000000000 --- a/scripts/docker-create-push-manifests.sh +++ /dev/null @@ -1,27 +0,0 @@ -##!/usr/bin/env bash -set -exuo pipefail - -img_amd64=$1 -push_latest=${2:-false} - -img_arm64=$(echo ${img_amd64} | sed 's/-amd64$/-arm64/g') -img_name=${img_amd64%-amd64} -docker pull "$img_amd64" -docker pull --platform linux/arm64 "$img_arm64" -img_amd64_digest=$(docker inspect --format='{{index .RepoDigests 0}}' "$img_amd64") -img_arm64_digest=$(docker inspect --format='{{index .RepoDigests 0}}' "$img_arm64") -echo "sha256 of amd64 is $img_amd64_digest" -echo "sha256 of arm64 is $img_arm64_digest" -docker manifest create "${img_name}" \ - --amend "$img_amd64_digest" \ - --amend "$img_arm64_digest" -docker manifest push "${img_name}" - -# PUSH latest if it is a release build -if [ "$push_latest" = "true" ]; then - img_latest=$(echo "$img_arm64" | cut -d: -f 1):latest - docker manifest create "${img_latest}" \ - --amend "$img_amd64_digest" \ - --amend "$img_arm64_digest" - docker manifest push "${img_latest}" -fi From 4d3b6405f60095eaf851c091aa4995a43651db3d Mon Sep 17 00:00:00 2001 From: JimMoen Date: Thu, 12 Jan 2023 18:13:14 +0800 Subject: [PATCH 0120/1802] chore: bump dsahboard tag to `e1.0.1-beta.12` --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 180028899..faa866753 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ export EMQX_DEFAULT_RUNNER = debian:11-slim export OTP_VSN ?= $(shell $(CURDIR)/scripts/get-otp-vsn.sh) export ELIXIR_VSN ?= $(shell $(CURDIR)/scripts/get-elixir-vsn.sh) export EMQX_DASHBOARD_VERSION ?= v1.1.5 -export EMQX_EE_DASHBOARD_VERSION ?= e1.0.1-beta.9 +export EMQX_EE_DASHBOARD_VERSION ?= e1.0.1-beta.12 export EMQX_REL_FORM ?= tgz export QUICER_DOWNLOAD_FROM_RELEASE = 1 ifeq ($(OS),Windows_NT) From 1f57e7b5383033041d003e5ecb41b7bf87fa52d5 Mon Sep 17 00:00:00 2001 From: Ivan Dyachkov Date: Wed, 28 Dec 2022 11:11:26 +0100 Subject: [PATCH 0121/1802] fix: enable authorization cache by default --- apps/emqx_authz/etc/emqx_authz.conf | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/emqx_authz/etc/emqx_authz.conf b/apps/emqx_authz/etc/emqx_authz.conf index e7fd73498..3bdc180c5 100644 --- a/apps/emqx_authz/etc/emqx_authz.conf +++ b/apps/emqx_authz/etc/emqx_authz.conf @@ -1,6 +1,7 @@ authorization { deny_action = ignore no_match = allow + cache = { enable = true } sources = [ { type = file From 32f75197f2982381aa87f637144671e35d52d146 Mon Sep 17 00:00:00 2001 From: Ivan Dyachkov Date: Thu, 29 Dec 2022 10:16:40 +0100 Subject: [PATCH 0122/1802] docs: add changelog --- changes/v5.0.14-en.md | 8 ++++++++ changes/v5.0.14-zh.md | 8 ++++++++ 2 files changed, 16 insertions(+) create mode 100644 changes/v5.0.14-en.md create mode 100644 changes/v5.0.14-zh.md diff --git a/changes/v5.0.14-en.md b/changes/v5.0.14-en.md new file mode 100644 index 000000000..060e86652 --- /dev/null +++ b/changes/v5.0.14-en.md @@ -0,0 +1,8 @@ +# v5.0.14 + +## Enhancements + +- Enable authorization cache by default [#9626](https://github.com/emqx/emqx/pull/9626). + +## Bug fixes + diff --git a/changes/v5.0.14-zh.md b/changes/v5.0.14-zh.md new file mode 100644 index 000000000..62c40e638 --- /dev/null +++ b/changes/v5.0.14-zh.md @@ -0,0 +1,8 @@ +# v5.0.14 + +## 增强 + +- 默认启用授权缓存 [#9626](https://github.com/emqx/emqx/pull/9626)。 + +## 修复 + From f90c41f769174fa1ac312ed73a2342c7565f9b18 Mon Sep 17 00:00:00 2001 From: Ivan Dyachkov Date: Thu, 12 Jan 2023 13:47:50 +0100 Subject: [PATCH 0123/1802] fix: set default value in schema --- apps/emqx/src/emqx_hocon.erl | 6 ++- apps/emqx/src/emqx_schema.erl | 54 ++++++++++--------- .../src/emqx_authz_api_settings.erl | 6 ++- .../emqx_authz/src/emqx_authz_api_sources.erl | 2 +- apps/emqx_authz/src/emqx_authz_schema.erl | 40 +++++++------- apps/emqx_conf/src/emqx_conf_schema.erl | 4 +- changes/v5.0.14-en.md | 8 --- changes/v5.0.14-zh.md | 8 --- changes/v5.0.15/fix-9626.en.md | 2 + changes/v5.0.15/fix-9626.zh.md | 3 ++ 10 files changed, 68 insertions(+), 65 deletions(-) delete mode 100644 changes/v5.0.14-en.md delete mode 100644 changes/v5.0.14-zh.md create mode 100644 changes/v5.0.15/fix-9626.en.md create mode 100644 changes/v5.0.15/fix-9626.zh.md diff --git a/apps/emqx/src/emqx_hocon.erl b/apps/emqx/src/emqx_hocon.erl index 7e9dbca77..4391a9a0b 100644 --- a/apps/emqx/src/emqx_hocon.erl +++ b/apps/emqx/src/emqx_hocon.erl @@ -21,7 +21,8 @@ format_path/1, check/2, format_error/1, - format_error/2 + format_error/2, + make_schema/1 ]). %% @doc Format hocon config field path to dot-separated string in iolist format. @@ -79,6 +80,9 @@ format_error({_Schema, [#{kind := K} = First | Rest] = All}, Opts) when format_error(_Other, _) -> false. +make_schema(Fields) -> + #{roots => Fields, fields => #{}}. + %% Ensure iolist() iol(B) when is_binary(B) -> B; iol(A) when is_atom(A) -> atom_to_binary(A, utf8); diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index ed7e0a016..043b57b99 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -114,6 +114,7 @@ -export([namespace/0, roots/0, roots/1, fields/1, desc/1, tags/0]). -export([conf_get/2, conf_get/3, keys/2, filter/1]). -export([server_ssl_opts_schema/2, client_ssl_opts_schema/1, ciphers_schema/1]). +-export([authz_fields/0]). -export([sc/2, map/2]). -elvis([{elvis_style, god_modules, disable}]). @@ -326,31 +327,7 @@ fields("stats") -> )} ]; fields("authorization") -> - [ - {"no_match", - sc( - hoconsc:enum([allow, deny]), - #{ - default => allow, - required => true, - desc => ?DESC(fields_authorization_no_match) - } - )}, - {"deny_action", - sc( - hoconsc:enum([ignore, disconnect]), - #{ - default => ignore, - required => true, - desc => ?DESC(fields_authorization_deny_action) - } - )}, - {"cache", - sc( - ref(?MODULE, "cache"), - #{} - )} - ]; + authz_fields(); fields("cache") -> [ {"enable", @@ -2091,6 +2068,33 @@ do_default_ciphers(_) -> %% otherwise resolve default ciphers list at runtime []. +authz_fields() -> + [ + {"no_match", + sc( + hoconsc:enum([allow, deny]), + #{ + default => allow, + required => true, + desc => ?DESC(fields_authorization_no_match) + } + )}, + {"deny_action", + sc( + hoconsc:enum([ignore, disconnect]), + #{ + default => ignore, + required => true, + desc => ?DESC(fields_authorization_deny_action) + } + )}, + {"cache", + sc( + ref(?MODULE, "cache"), + #{} + )} + ]. + %% @private return a list of keys in a parent field -spec keys(string(), hocon:config()) -> [string()]. keys(Parent, Conf) -> diff --git a/apps/emqx_authz/src/emqx_authz_api_settings.erl b/apps/emqx_authz/src/emqx_authz_api_settings.erl index 72a2db35c..db915a795 100644 --- a/apps/emqx_authz/src/emqx_authz_api_settings.erl +++ b/apps/emqx_authz/src/emqx_authz_api_settings.erl @@ -64,7 +64,7 @@ schema("/authorization/settings") -> }. ref_authz_schema() -> - proplists:delete(sources, emqx_conf_schema:fields("authorization")). + emqx_schema:authz_fields(). settings(get, _Params) -> {200, authorization_settings()}; @@ -83,4 +83,6 @@ settings(put, #{ {200, authorization_settings()}. authorization_settings() -> - maps:remove(<<"sources">>, emqx:get_raw_config([authorization], #{})). + C = maps:remove(<<"sources">>, emqx:get_raw_config([authorization], #{})), + Schema = emqx_hocon:make_schema(emqx_schema:authz_fields()), + hocon_tconf:make_serializable(Schema, C, #{}). diff --git a/apps/emqx_authz/src/emqx_authz_api_sources.erl b/apps/emqx_authz/src/emqx_authz_api_sources.erl index f5570f1f1..c692154b1 100644 --- a/apps/emqx_authz/src/emqx_authz_api_sources.erl +++ b/apps/emqx_authz/src/emqx_authz_api_sources.erl @@ -449,7 +449,7 @@ is_ok(ResL) -> get_raw_sources() -> RawSources = emqx:get_raw_config([authorization, sources], []), - Schema = #{roots => emqx_authz_schema:fields("authorization"), fields => #{}}, + Schema = emqx_hocon:make_schema(emqx_authz_schema:authz_fields()), Conf = #{<<"sources">> => RawSources}, #{<<"sources">> := Sources} = hocon_tconf:make_serializable(Schema, Conf, #{}), merge_default_headers(Sources). diff --git a/apps/emqx_authz/src/emqx_authz_schema.erl b/apps/emqx_authz/src/emqx_authz_schema.erl index e2da9b41e..5527c26d6 100644 --- a/apps/emqx_authz/src/emqx_authz_schema.erl +++ b/apps/emqx_authz/src/emqx_authz_schema.erl @@ -36,7 +36,8 @@ tags/0, fields/1, validations/0, - desc/1 + desc/1, + authz_fields/0 ]). -export([ @@ -74,23 +75,7 @@ tags() -> roots() -> []. fields("authorization") -> - Types = [?R_REF(Type) || Type <- type_names()], - UnionMemberSelector = - fun - (all_union_members) -> Types; - %% must return list - ({value, Value}) -> [select_union_member(Value)] - end, - [ - {sources, - ?HOCON( - ?ARRAY(?UNION(UnionMemberSelector)), - #{ - default => [], - desc => ?DESC(sources) - } - )} - ]; + authz_fields(); fields(file) -> authz_common_fields(file) ++ [{path, ?HOCON(string(), #{required => true, desc => ?DESC(path)})}]; @@ -492,3 +477,22 @@ select_union_member_loop(TypeValue, [Type | Types]) -> false -> select_union_member_loop(TypeValue, Types) end. + +authz_fields() -> + Types = [?R_REF(Type) || Type <- type_names()], + UnionMemberSelector = + fun + (all_union_members) -> Types; + %% must return list + ({value, Value}) -> [select_union_member(Value)] + end, + [ + {sources, + ?HOCON( + ?ARRAY(?UNION(UnionMemberSelector)), + #{ + default => [], + desc => ?DESC(sources) + } + )} + ]. diff --git a/apps/emqx_conf/src/emqx_conf_schema.erl b/apps/emqx_conf/src/emqx_conf_schema.erl index 71d2ab3fd..90af47aca 100644 --- a/apps/emqx_conf/src/emqx_conf_schema.erl +++ b/apps/emqx_conf/src/emqx_conf_schema.erl @@ -947,8 +947,8 @@ fields("log_burst_limit") -> )} ]; fields("authorization") -> - emqx_schema:fields("authorization") ++ - emqx_authz_schema:fields("authorization"). + emqx_schema:authz_fields() ++ + emqx_authz_schema:authz_fields(). desc("cluster") -> ?DESC("desc_cluster"); diff --git a/changes/v5.0.14-en.md b/changes/v5.0.14-en.md deleted file mode 100644 index 060e86652..000000000 --- a/changes/v5.0.14-en.md +++ /dev/null @@ -1,8 +0,0 @@ -# v5.0.14 - -## Enhancements - -- Enable authorization cache by default [#9626](https://github.com/emqx/emqx/pull/9626). - -## Bug fixes - diff --git a/changes/v5.0.14-zh.md b/changes/v5.0.14-zh.md deleted file mode 100644 index 62c40e638..000000000 --- a/changes/v5.0.14-zh.md +++ /dev/null @@ -1,8 +0,0 @@ -# v5.0.14 - -## 增强 - -- 默认启用授权缓存 [#9626](https://github.com/emqx/emqx/pull/9626)。 - -## 修复 - diff --git a/changes/v5.0.15/fix-9626.en.md b/changes/v5.0.15/fix-9626.en.md new file mode 100644 index 000000000..cc1c86d3e --- /dev/null +++ b/changes/v5.0.15/fix-9626.en.md @@ -0,0 +1,2 @@ +Return authorization settings with default values. +The authorization cache is enabled by default, but due to the missing default value in `GET` response of `/authorization/settings`, it seemed to be disabled from the dashboard. diff --git a/changes/v5.0.15/fix-9626.zh.md b/changes/v5.0.15/fix-9626.zh.md new file mode 100644 index 000000000..bc2391f48 --- /dev/null +++ b/changes/v5.0.15/fix-9626.zh.md @@ -0,0 +1,3 @@ +为授权设置 API 返回默认值。 +授权缓存默认为开启,但是在此修复前,因为默认值在 `/authorization/settings` 这个 API 的返回值中缺失, +使得在仪表盘配置页面中看起来是关闭了。 From b5d3e9d8b89a1a7f2071cb4629581e57448f3949 Mon Sep 17 00:00:00 2001 From: Ivan Dyachkov Date: Thu, 12 Jan 2023 14:14:21 +0100 Subject: [PATCH 0124/1802] fix: remove time unit from duration fields description --- apps/emqx_resource/i18n/emqx_resource_schema_i18n.conf | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/emqx_resource/i18n/emqx_resource_schema_i18n.conf b/apps/emqx_resource/i18n/emqx_resource_schema_i18n.conf index 52756f70d..0b6cbd0a2 100644 --- a/apps/emqx_resource/i18n/emqx_resource_schema_i18n.conf +++ b/apps/emqx_resource/i18n/emqx_resource_schema_i18n.conf @@ -36,8 +36,8 @@ For bridges only have ingress direction data flow, it can be set to 0 otherwise health_check_interval { desc { - en: """Health check interval, in milliseconds.""" - zh: """健康检查间隔,单位毫秒。""" + en: """Health check interval.""" + zh: """健康检查间隔。""" } label { en: """Health Check Interval""" @@ -69,8 +69,8 @@ For bridges only have ingress direction data flow, it can be set to 0 otherwise auto_restart_interval { desc { - en: """The auto restart interval after the resource is disconnected, in milliseconds.""" - zh: """资源断开以后,自动重连的时间间隔,单位毫秒。""" + en: """The auto restart interval after the resource is disconnected.""" + zh: """资源断开以后,自动重连的时间间隔。""" } label { en: """Auto Restart Interval""" From 77f043dedff31017cbf9ef46e98776b1f9228f58 Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Wed, 11 Jan 2023 14:43:52 +0100 Subject: [PATCH 0125/1802] fix: don't require username and password not enforced by schema so we shouldn't either --- .../src/emqx_connector_mysql.erl | 37 ++++++++++++++----- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/apps/emqx_connector/src/emqx_connector_mysql.erl b/apps/emqx_connector/src/emqx_connector_mysql.erl index 6c0ff7210..693917a27 100644 --- a/apps/emqx_connector/src/emqx_connector_mysql.erl +++ b/apps/emqx_connector/src/emqx_connector_mysql.erl @@ -66,10 +66,21 @@ roots() -> fields(config) -> [{server, server()}] ++ - emqx_connector_schema_lib:relational_db_fields() ++ + add_default_username(emqx_connector_schema_lib:relational_db_fields(), []) ++ emqx_connector_schema_lib:ssl_fields() ++ emqx_connector_schema_lib:prepare_statement_fields(). +add_default_username([{username, OrigUsernameFn} | Tail], Head) -> + Head ++ [{username, add_default_fn(OrigUsernameFn, <<"root">>)} | Tail]; +add_default_username([Field | Tail], Head) -> + add_default_username(Tail, Head ++ [Field]). + +add_default_fn(OrigFn, Default) -> + fun + (default) -> Default; + (Field) -> OrigFn(Field) + end. + server() -> Meta = #{desc => ?DESC("server")}, emqx_schema:servers_sc(Meta, ?MYSQL_HOST_OPTIONS). @@ -83,8 +94,7 @@ on_start( #{ server := Server, database := DB, - username := User, - password := Password, + username := Username, auto_reconnect := AutoReconn, pool_size := PoolSize, ssl := SSL @@ -104,13 +114,15 @@ on_start( [] end, Options = [ - {host, Host}, - {port, Port}, - {user, User}, - {password, Password}, - {database, DB}, - {auto_reconnect, reconn_interval(AutoReconn)}, - {pool_size, PoolSize} + maybe_password_opt(maps:get(password, Config, undefined)) + | [ + {host, Host}, + {port, Port}, + {user, Username}, + {database, DB}, + {auto_reconnect, reconn_interval(AutoReconn)}, + {pool_size, PoolSize} + ] ], PoolName = emqx_plugin_libs_pool:pool_name(InstId), Prepares = parse_prepare_sql(Config), @@ -126,6 +138,11 @@ on_start( {error, Reason} end. +maybe_password_opt(undefined) -> + []; +maybe_password_opt(Password) -> + {password, Password}. + on_stop(InstId, #{poolname := PoolName}) -> ?SLOG(info, #{ msg => "stopping_mysql_connector", From 04f46f5227836bb451852d36d54f7bf832904a47 Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Wed, 11 Jan 2023 15:11:41 +0100 Subject: [PATCH 0126/1802] feat: make http connector report errors --- .../src/emqx_connector_http.erl | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/apps/emqx_connector/src/emqx_connector_http.erl b/apps/emqx_connector/src/emqx_connector_http.erl index 18a246edb..55e6d6f8e 100644 --- a/apps/emqx_connector/src/emqx_connector_http.erl +++ b/apps/emqx_connector/src/emqx_connector_http.erl @@ -384,14 +384,15 @@ on_query_async( on_get_status(_InstId, #{pool_name := PoolName, connect_timeout := Timeout} = State) -> case do_get_status(PoolName, Timeout) of - true -> - connected; - false -> + ok -> + {connected, State}; + {error, Reason} -> ?SLOG(error, #{ msg => "http_connector_get_status_failed", + reason => Reason, state => State }), - disconnected + {disconnected, State, Reason} end. do_get_status(PoolName, Timeout) -> @@ -400,24 +401,28 @@ do_get_status(PoolName, Timeout) -> fun(Worker) -> case ehttpc:health_check(Worker, Timeout) of ok -> - true; - {error, Reason} -> + ok; + {error, Reason} = Error -> ?SLOG(error, #{ msg => "ehttpc_health_check_failed", reason => Reason, worker => Worker }), - false + Error end end, try emqx_misc:pmap(DoPerWorker, Workers, Timeout) of - [_ | _] = Status -> - lists:all(fun(St) -> St =:= true end, Status); - [] -> - false + % we crash in case of non-empty lists since we don't know what to do in that case + [_ | _] = Results -> + case [E || {error, _} = E <- Results] of + [] -> + ok; + Errors -> + hd(Errors) + end catch exit:timeout -> - false + {error, timeout} end. %%-------------------------------------------------------------------- From 2a81fa152263381cffaa52d7bbe93b3212e8b22a Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Thu, 12 Jan 2023 14:29:11 +0100 Subject: [PATCH 0127/1802] fix: remove redundant log message --- apps/emqx_connector/src/emqx_connector_http.erl | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/apps/emqx_connector/src/emqx_connector_http.erl b/apps/emqx_connector/src/emqx_connector_http.erl index 55e6d6f8e..286e0e4e6 100644 --- a/apps/emqx_connector/src/emqx_connector_http.erl +++ b/apps/emqx_connector/src/emqx_connector_http.erl @@ -387,11 +387,6 @@ on_get_status(_InstId, #{pool_name := PoolName, connect_timeout := Timeout} = St ok -> {connected, State}; {error, Reason} -> - ?SLOG(error, #{ - msg => "http_connector_get_status_failed", - reason => Reason, - state => State - }), {disconnected, State, Reason} end. @@ -404,7 +399,7 @@ do_get_status(PoolName, Timeout) -> ok; {error, Reason} = Error -> ?SLOG(error, #{ - msg => "ehttpc_health_check_failed", + msg => "http_connector_get_status_failed", reason => Reason, worker => Worker }), From c5f557e315cad4c0ccded1051458e2a1a6c72dbe Mon Sep 17 00:00:00 2001 From: firest Date: Wed, 11 Jan 2023 13:35:42 +0800 Subject: [PATCH 0128/1802] fix: disable basic auth for HTTP API --- .github/workflows/run_fvt_tests.yaml | 17 ++++++- .github/workflows/run_jmeter_tests.yaml | 10 ++-- apps/emqx/include/http_api.hrl | 10 ++-- apps/emqx/test/emqx_common_test_http.erl | 19 ++++++-- apps/emqx_authn/test/emqx_authn_api_SUITE.erl | 15 ++---- .../test/emqx_authz_api_cache_SUITE.erl | 12 ++--- .../test/emqx_authz_api_mnesia_SUITE.erl | 8 ++-- .../test/emqx_authz_api_settings_SUITE.erl | 6 +-- .../test/emqx_authz_api_sources_SUITE.erl | 8 ++-- .../test/emqx_auto_subscribe_SUITE.erl | 13 ++--- .../test/emqx_bridge_api_SUITE.erl | 17 ++----- apps/emqx_dashboard/src/emqx_dashboard.erl | 48 +++++++++---------- .../emqx_dashboard/src/emqx_dashboard_api.erl | 6 +-- .../test/emqx_dashboard_SUITE.erl | 4 +- .../test/emqx_dashboard_bad_api_SUITE.erl | 29 ++--------- .../test/emqx_dashboard_monitor_SUITE.erl | 6 +-- .../test/emqx_exhook_api_SUITE.erl | 8 +--- .../test/emqx_gateway_test_utils.erl | 5 +- apps/emqx_management/src/emqx_mgmt_auth.erl | 20 +++++--- .../test/emqx_mgmt_api_api_keys_SUITE.erl | 15 +++--- .../test/emqx_mgmt_api_test_util.erl | 41 +++++++++++++--- .../test/emqx_mgmt_api_trace_SUITE.erl | 9 +--- .../test/emqx_delayed_api_SUITE.erl | 14 ++---- .../test/emqx_rewrite_api_SUITE.erl | 16 ++----- .../test/emqx_telemetry_api_SUITE.erl | 10 ++-- .../test/emqx_topic_metrics_api_SUITE.erl | 17 ++----- .../test/emqx_slow_subs_api_SUITE.erl | 8 +--- 27 files changed, 183 insertions(+), 208 deletions(-) diff --git a/.github/workflows/run_fvt_tests.yaml b/.github/workflows/run_fvt_tests.yaml index 9e44034fb..a54bb68dd 100644 --- a/.github/workflows/run_fvt_tests.yaml +++ b/.github/workflows/run_fvt_tests.yaml @@ -201,12 +201,25 @@ jobs: echo "waiting emqx started"; sleep 10; done + - name: Get Token + timeout-minutes: 1 + run: | + kubectl port-forward service/${{ matrix.profile }} 18083:18083 > /dev/null & + + while + [ "$(curl --silent -X 'GET' 'http://127.0.0.1:18083/api/v5/status' | tail -n1)" != "emqx is running" ] + do + echo "waiting emqx" + sleep 1 + done + + echo "TOKEN=$(curl --silent -X 'POST' 'http://127.0.0.1:18083/api/v5/login' -H 'accept: application/json' -H 'Content-Type: application/json' -d '{"username": "admin","password": "public"}' | jq -r ".token")" >> $GITHUB_ENV + - name: Check cluster timeout-minutes: 10 run: | - kubectl port-forward service/${{ matrix.profile }} 18083:18083 > /dev/null & while - [ "$(curl --silent --basic -u admin:public -X GET http://127.0.0.1:18083/api/v5/cluster| jq '.nodes|length')" != "3" ]; + [ "$(curl --silent -H "Authorization: Bearer $TOKEN" -X GET http://127.0.0.1:18083/api/v5/cluster| jq '.nodes|length')" != "3" ]; do echo "waiting ${{ matrix.profile }} cluster scale" sleep 1 diff --git a/.github/workflows/run_jmeter_tests.yaml b/.github/workflows/run_jmeter_tests.yaml index 6eaf4aa75..4ba246d8c 100644 --- a/.github/workflows/run_jmeter_tests.yaml +++ b/.github/workflows/run_jmeter_tests.yaml @@ -92,7 +92,7 @@ jobs: - uses: actions/checkout@v3 with: repository: emqx/emqx-fvt - ref: broker-autotest-v2 + ref: broker-autotest-v4 path: scripts - uses: actions/setup-java@v3 with: @@ -191,7 +191,7 @@ jobs: - uses: actions/checkout@v3 with: repository: emqx/emqx-fvt - ref: broker-autotest-v2 + ref: broker-autotest-v4 path: scripts - uses: actions/setup-java@v3 with: @@ -297,7 +297,7 @@ jobs: - uses: actions/checkout@v3 with: repository: emqx/emqx-fvt - ref: broker-autotest-v2 + ref: broker-autotest-v4 path: scripts - uses: actions/setup-java@v3 with: @@ -396,7 +396,7 @@ jobs: - uses: actions/checkout@v3 with: repository: emqx/emqx-fvt - ref: broker-autotest-v2 + ref: broker-autotest-v4 path: scripts - name: run jwks_server timeout-minutes: 10 @@ -496,7 +496,7 @@ jobs: - uses: actions/checkout@v3 with: repository: emqx/emqx-fvt - ref: broker-autotest-v2 + ref: broker-autotest-v4 path: scripts - uses: actions/setup-java@v3 with: diff --git a/apps/emqx/include/http_api.hrl b/apps/emqx/include/http_api.hrl index 858ce96ce..08dd08362 100644 --- a/apps/emqx/include/http_api.hrl +++ b/apps/emqx/include/http_api.hrl @@ -15,10 +15,8 @@ %%-------------------------------------------------------------------- %% HTTP API Auth --define(WRONG_USERNAME_OR_PWD, 'WRONG_USERNAME_OR_PWD'). --define(WRONG_USERNAME_OR_PWD_OR_API_KEY_OR_API_SECRET, - 'WRONG_USERNAME_OR_PWD_OR_API_KEY_OR_API_SECRET' -). +-define(BAD_USERNAME_OR_PWD, 'BAD_USERNAME_OR_PWD'). +-define(BAD_API_KEY_OR_SECRET, 'BAD_API_KEY_OR_SECRET'). %% Bad Request -define(BAD_REQUEST, 'BAD_REQUEST'). @@ -57,8 +55,8 @@ %% All codes -define(ERROR_CODES, [ - {'WRONG_USERNAME_OR_PWD', <<"Wrong username or pwd">>}, - {'WRONG_USERNAME_OR_PWD_OR_API_KEY_OR_API_SECRET', <<"Wrong username & pwd or key & secret">>}, + {?BAD_USERNAME_OR_PWD, <<"Bad username or password">>}, + {?BAD_API_KEY_OR_SECRET, <<"Bad API key or secret">>}, {'BAD_REQUEST', <<"Request parameters are not legal">>}, {'NOT_MATCH', <<"Conditions are not matched">>}, {'ALREADY_EXISTS', <<"Resource already existed">>}, diff --git a/apps/emqx/test/emqx_common_test_http.erl b/apps/emqx/test/emqx_common_test_http.erl index 87a35a1e2..575bed5c3 100644 --- a/apps/emqx/test/emqx_common_test_http.erl +++ b/apps/emqx/test/emqx_common_test_http.erl @@ -29,6 +29,9 @@ auth_header/2 ]). +-define(DEFAULT_APP_ID, <<"default_appid">>). +-define(DEFAULT_APP_SECRET, <<"default_app_secret">>). + request_api(Method, Url, Auth) -> request_api(Method, Url, [], Auth, []). @@ -74,12 +77,18 @@ auth_header(User, Pass) -> {"Authorization", "Basic " ++ Encoded}. default_auth_header() -> - AppId = <<"myappid">>, - AppSecret = emqx_mgmt_auth:get_appsecret(AppId), - auth_header(erlang:binary_to_list(AppId), erlang:binary_to_list(AppSecret)). + {ok, #{api_key := APIKey}} = emqx_mgmt_auth:read(?DEFAULT_APP_ID), + auth_header( + erlang:binary_to_list(APIKey), erlang:binary_to_list(?DEFAULT_APP_SECRET) + ). create_default_app() -> - emqx_mgmt_auth:add_app(<<"myappid">>, <<"test">>). + Now = erlang:system_time(second), + ExpiredAt = Now + timer:minutes(10), + emqx_mgmt_auth:create( + ?DEFAULT_APP_ID, ?DEFAULT_APP_SECRET, true, ExpiredAt, <<"default app key for test">> + ), + ok. delete_default_app() -> - emqx_mgmt_auth:del_app(<<"myappid">>). + emqx_mgmt_auth:delete(?DEFAULT_APP_ID). diff --git a/apps/emqx_authn/test/emqx_authn_api_SUITE.erl b/apps/emqx_authn/test/emqx_authn_api_SUITE.erl index 1a867b0be..11e2c6773 100644 --- a/apps/emqx_authn/test/emqx_authn_api_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_api_SUITE.erl @@ -18,7 +18,8 @@ -compile(nowarn_export_all). -compile(export_all). --import(emqx_dashboard_api_test_helpers, [request/3, uri/1, multipart_formdata_request/3]). +-import(emqx_dashboard_api_test_helpers, [multipart_formdata_request/3]). +-import(emqx_mgmt_api_test_util, [request/3, uri/1]). -include("emqx_authn.hrl"). -include_lib("eunit/include/eunit.hrl"). @@ -65,9 +66,8 @@ end_per_testcase(_, Config) -> init_per_suite(Config) -> emqx_config:erase(?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_BINARY), _ = application:load(emqx_conf), - ok = emqx_common_test_helpers:start_apps( - [emqx_authn, emqx_dashboard], - fun set_special_configs/1 + ok = emqx_mgmt_api_test_util:init_suite( + [emqx_authn] ), ?AUTHN:delete_chain(?GLOBAL), @@ -76,12 +76,7 @@ init_per_suite(Config) -> Config. end_per_suite(_Config) -> - emqx_common_test_helpers:stop_apps([emqx_dashboard, emqx_authn]), - ok. - -set_special_configs(emqx_dashboard) -> - emqx_dashboard_api_test_helpers:set_default_config(); -set_special_configs(_App) -> + emqx_mgmt_api_test_util:end_suite([emqx_authn]), ok. %%------------------------------------------------------------------------------ diff --git a/apps/emqx_authz/test/emqx_authz_api_cache_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_cache_SUITE.erl index 24b8fe25e..45e6d7287 100644 --- a/apps/emqx_authz/test/emqx_authz_api_cache_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_api_cache_SUITE.erl @@ -18,7 +18,7 @@ -compile(nowarn_export_all). -compile(export_all). --import(emqx_dashboard_api_test_helpers, [request/2, uri/1]). +-import(emqx_mgmt_api_test_util, [request/2, uri/1]). -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). @@ -32,8 +32,8 @@ groups() -> []. init_per_suite(Config) -> - ok = emqx_common_test_helpers:start_apps( - [emqx_conf, emqx_authz, emqx_dashboard, emqx_management], + ok = emqx_mgmt_api_test_util:init_suite( + [emqx_conf, emqx_authz], fun set_special_configs/1 ), Config. @@ -47,7 +47,7 @@ end_per_suite(_Config) -> <<"sources">> => [] } ), - emqx_common_test_helpers:stop_apps([emqx_dashboard, emqx_authz, emqx_conf, emqx_management]), + emqx_mgmt_api_test_util:end_suite([emqx_authz, emqx_conf]), ok. set_special_configs(emqx_dashboard) -> @@ -67,12 +67,12 @@ t_clean_cahce(_) -> ok = emqtt:publish(C, <<"a/b/c">>, <<"{\"x\":1,\"y\":1}">>, 0), {ok, 200, Result3} = request(get, uri(["clients", "emqx0", "authorization", "cache"])), - ?assertEqual(2, length(jsx:decode(Result3))), + ?assertEqual(2, length(emqx_json:decode(Result3))), request(delete, uri(["authorization", "cache"])), {ok, 200, Result4} = request(get, uri(["clients", "emqx0", "authorization", "cache"])), - ?assertEqual(0, length(jsx:decode(Result4))), + ?assertEqual(0, length(emqx_json:decode(Result4))), ok. diff --git a/apps/emqx_authz/test/emqx_authz_api_mnesia_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_mnesia_SUITE.erl index 1819cf66f..62bce770e 100644 --- a/apps/emqx_authz/test/emqx_authz_api_mnesia_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_api_mnesia_SUITE.erl @@ -22,7 +22,7 @@ -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). --import(emqx_dashboard_api_test_helpers, [request/3, uri/1]). +-import(emqx_mgmt_api_test_util, [request/3, uri/1]). all() -> emqx_common_test_helpers:all(?MODULE). @@ -31,8 +31,8 @@ groups() -> []. init_per_suite(Config) -> - ok = emqx_common_test_helpers:start_apps( - [emqx_conf, emqx_authz, emqx_dashboard], + ok = emqx_mgmt_api_test_util:init_suite( + [emqx_conf, emqx_authz], fun set_special_configs/1 ), Config. @@ -46,7 +46,7 @@ end_per_suite(_Config) -> <<"sources">> => [] } ), - emqx_common_test_helpers:stop_apps([emqx_dashboard, emqx_authz, emqx_conf]), + emqx_mgmt_api_test_util:end_suite([emqx_authz, emqx_conf]), ok. set_special_configs(emqx_dashboard) -> diff --git a/apps/emqx_authz/test/emqx_authz_api_settings_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_settings_SUITE.erl index 275b04e40..41eba109e 100644 --- a/apps/emqx_authz/test/emqx_authz_api_settings_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_api_settings_SUITE.erl @@ -18,7 +18,7 @@ -compile(nowarn_export_all). -compile(export_all). --import(emqx_dashboard_api_test_helpers, [request/3, uri/1]). +-import(emqx_mgmt_api_test_util, [request/3, uri/1]). -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). @@ -30,7 +30,7 @@ groups() -> []. init_per_suite(Config) -> - ok = emqx_common_test_helpers:start_apps( + ok = emqx_mgmt_api_test_util:init_suite( [emqx_conf, emqx_authz, emqx_dashboard], fun set_special_configs/1 ), @@ -46,7 +46,7 @@ end_per_suite(_Config) -> } ), ok = stop_apps([emqx_resource]), - emqx_common_test_helpers:stop_apps([emqx_dashboard, emqx_authz, emqx_conf]), + emqx_mgmt_api_test_util:end_suite([emqx_authz, emqx_conf]), ok. set_special_configs(emqx_dashboard) -> diff --git a/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl index 34638d0aa..76b025716 100644 --- a/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl @@ -18,7 +18,7 @@ -compile(nowarn_export_all). -compile(export_all). --import(emqx_dashboard_api_test_helpers, [request/3, uri/1]). +-import(emqx_mgmt_api_test_util, [request/3, uri/1]). -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). @@ -115,8 +115,8 @@ init_per_suite(Config) -> end ), - ok = emqx_common_test_helpers:start_apps( - [emqx_conf, emqx_authz, emqx_dashboard], + ok = emqx_mgmt_api_test_util:init_suite( + [emqx_conf, emqx_authz], fun set_special_configs/1 ), ok = start_apps([emqx_resource]), @@ -134,7 +134,7 @@ end_per_suite(_Config) -> %% resource and connector should be stop first, %% or authz_[mysql|pgsql|redis..]_SUITE would be failed ok = stop_apps([emqx_resource]), - emqx_common_test_helpers:stop_apps([emqx_dashboard, emqx_authz, emqx_conf]), + emqx_mgmt_api_test_util:end_suite([emqx_authz, emqx_conf]), meck:unload(emqx_resource), ok. diff --git a/apps/emqx_auto_subscribe/test/emqx_auto_subscribe_SUITE.erl b/apps/emqx_auto_subscribe/test/emqx_auto_subscribe_SUITE.erl index 959f8ec1b..900f39ebb 100644 --- a/apps/emqx_auto_subscribe/test/emqx_auto_subscribe_SUITE.erl +++ b/apps/emqx_auto_subscribe/test/emqx_auto_subscribe_SUITE.erl @@ -93,9 +93,8 @@ init_per_suite(Config) -> " }" >> ), - emqx_common_test_helpers:start_apps( - [emqx_conf, emqx_dashboard, ?APP], - fun set_special_configs/1 + emqx_mgmt_api_test_util:init_suite( + [emqx_conf, ?APP] ), Config. @@ -111,12 +110,6 @@ end_per_testcase(t_get_basic_usage_info, _Config) -> end_per_testcase(_TestCase, _Config) -> ok. -set_special_configs(emqx_dashboard) -> - emqx_dashboard_api_test_helpers:set_default_config(), - ok; -set_special_configs(_) -> - ok. - topic_config(T) -> #{ topic => T, @@ -132,7 +125,7 @@ end_per_suite(_) -> application:unload(?APP), meck:unload(emqx_resource), meck:unload(emqx_schema), - emqx_common_test_helpers:stop_apps([emqx_dashboard, emqx_conf, ?APP]). + emqx_mgmt_api_test_util:end_suite([emqx_conf, ?APP]). t_auto_subscribe(_) -> emqx_auto_subscribe:update([#{<<"topic">> => Topic} || Topic <- ?TOPICS]), diff --git a/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl b/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl index 557eced13..4d16f1692 100644 --- a/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl +++ b/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl @@ -18,7 +18,7 @@ -compile(nowarn_export_all). -compile(export_all). --import(emqx_dashboard_api_test_helpers, [request/4, uri/1]). +-import(emqx_mgmt_api_test_util, [request/3, uri/1]). -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). @@ -60,9 +60,8 @@ init_per_suite(Config) -> %% some testcases (may from other app) already get emqx_connector started _ = application:stop(emqx_resource), _ = application:stop(emqx_connector), - ok = emqx_common_test_helpers:start_apps( - [emqx_rule_engine, emqx_bridge, emqx_dashboard], - fun set_special_configs/1 + ok = emqx_mgmt_api_test_util:init_suite( + [emqx_rule_engine, emqx_bridge] ), ok = emqx_common_test_helpers:load_config( emqx_rule_engine_schema, @@ -72,12 +71,7 @@ init_per_suite(Config) -> Config. end_per_suite(_Config) -> - emqx_common_test_helpers:stop_apps([emqx_rule_engine, emqx_bridge, emqx_dashboard]), - ok. - -set_special_configs(emqx_dashboard) -> - emqx_dashboard_api_test_helpers:set_default_config(<<"bridge_admin">>); -set_special_configs(_) -> + emqx_mgmt_api_test_util:end_suite([emqx_rule_engine, emqx_bridge]), ok. init_per_testcase(_, Config) -> @@ -605,9 +599,6 @@ t_with_redact_update(_Config) -> ?assertEqual(Password, Value), ok. -request(Method, Url, Body) -> - request(<<"bridge_admin">>, Method, Url, Body). - operation_path(node, Oper, BridgeID) -> uri(["nodes", node(), "bridges", BridgeID, "operation", Oper]); operation_path(cluster, Oper, BridgeID) -> diff --git a/apps/emqx_dashboard/src/emqx_dashboard.erl b/apps/emqx_dashboard/src/emqx_dashboard.erl index f15467658..36c7660cc 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard.erl @@ -65,8 +65,12 @@ start_listeners(Listeners) -> components => #{ schemas => #{}, 'securitySchemes' => #{ - 'basicAuth' => #{type => http, scheme => basic}, - 'bearerAuth' => #{type => http, scheme => bearer} + 'basicAuth' => #{ + type => http, + scheme => basic, + description => + <<"Authorize with [API Keys](https://www.emqx.io/docs/en/v5.0/admin/api.html#api-keys)">> + } } } }, @@ -215,28 +219,7 @@ listener_name(Protocol) -> authorize(Req) -> case cowboy_req:parse_header(<<"authorization">>, Req) of {basic, Username, Password} -> - case emqx_dashboard_admin:check(Username, Password) of - ok -> - ok; - {error, <<"username_not_found">>} -> - Path = cowboy_req:path(Req), - case emqx_mgmt_auth:authorize(Path, Username, Password) of - ok -> - ok; - {error, <<"not_allowed">>} -> - return_unauthorized( - ?WRONG_USERNAME_OR_PWD, - <<"Check username/password">> - ); - {error, _} -> - return_unauthorized( - ?WRONG_USERNAME_OR_PWD_OR_API_KEY_OR_API_SECRET, - <<"Check username/password or api_key/api_secret">> - ) - end; - {error, _} -> - return_unauthorized(?WRONG_USERNAME_OR_PWD, <<"Check username/password">>) - end; + api_key_authorize(Req, Username, Password); {bearer, Token} -> case emqx_dashboard_admin:verify_token(Token) of ok -> @@ -269,3 +252,20 @@ i18n_file() -> listeners() -> emqx_conf:get([dashboard, listeners], []). + +api_key_authorize(Req, Key, Secret) -> + Path = cowboy_req:path(Req), + case emqx_mgmt_auth:authorize(Path, Key, Secret) of + ok -> + ok; + {error, <<"not_allowed">>} -> + return_unauthorized( + ?BAD_API_KEY_OR_SECRET, + <<"Not allowed, Check api_key/api_secret">> + ); + {error, _} -> + return_unauthorized( + ?BAD_API_KEY_OR_SECRET, + <<"Check api_key/api_secret">> + ) + end. diff --git a/apps/emqx_dashboard/src/emqx_dashboard_api.erl b/apps/emqx_dashboard/src/emqx_dashboard_api.erl index 9facac59c..a4322c696 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_api.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_api.erl @@ -47,7 +47,7 @@ -define(EMPTY(V), (V == undefined orelse V == <<>>)). --define(WRONG_USERNAME_OR_PWD, 'WRONG_USERNAME_OR_PWD'). +-define(BAD_USERNAME_OR_PWD, 'BAD_USERNAME_OR_PWD'). -define(WRONG_TOKEN_OR_USERNAME, 'WRONG_TOKEN_OR_USERNAME'). -define(USER_NOT_FOUND, 'USER_NOT_FOUND'). -define(ERROR_PWD_NOT_MATCH, 'ERROR_PWD_NOT_MATCH'). @@ -164,7 +164,7 @@ schema("/users/:username/change_pwd") -> }. response_schema(401) -> - emqx_dashboard_swagger:error_codes([?WRONG_USERNAME_OR_PWD], ?DESC(login_failed401)); + emqx_dashboard_swagger:error_codes([?BAD_USERNAME_OR_PWD], ?DESC(login_failed401)); response_schema(404) -> emqx_dashboard_swagger:error_codes([?USER_NOT_FOUND], ?DESC(users_api404)). @@ -223,7 +223,7 @@ login(post, #{body := Params}) -> }}; {error, R} -> ?SLOG(info, #{msg => "Dashboard login failed", username => Username, reason => R}), - {401, ?WRONG_USERNAME_OR_PWD, <<"Auth failed">>} + {401, ?BAD_USERNAME_OR_PWD, <<"Auth failed">>} end. logout(_, #{ diff --git a/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl b/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl index 934d6055d..23d1b40c1 100644 --- a/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl +++ b/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl @@ -114,9 +114,9 @@ t_admin_delete_self_failed(_) -> ?assertEqual(1, length(Admins)), Header = auth_header_(<<"username1">>, <<"password">>), {error, {_, 400, _}} = request_dashboard(delete, api_path(["users", "username1"]), Header), - Token = erlang:iolist_to_binary(["Basic ", base64:encode("username1:password")]), + Token = ["Basic ", base64:encode("username1:password")], Header2 = {"Authorization", Token}, - {error, {_, 400, _}} = request_dashboard(delete, api_path(["users", "username1"]), Header2), + {error, {_, 401, _}} = request_dashboard(delete, api_path(["users", "username1"]), Header2), mnesia:clear_table(?ADMIN). t_rest_api(_Config) -> diff --git a/apps/emqx_dashboard/test/emqx_dashboard_bad_api_SUITE.erl b/apps/emqx_dashboard/test/emqx_dashboard_bad_api_SUITE.erl index b7fbf889e..a9b448662 100644 --- a/apps/emqx_dashboard/test/emqx_dashboard_bad_api_SUITE.erl +++ b/apps/emqx_dashboard/test/emqx_dashboard_bad_api_SUITE.erl @@ -25,43 +25,24 @@ -define(SERVER, "http://127.0.0.1:18083/api/v5"). +-import(emqx_mgmt_api_test_util, [request/2]). + all() -> emqx_common_test_helpers:all(?MODULE). init_per_suite(Config) -> mria:start(), - application:load(emqx_dashboard), - emqx_common_test_helpers:start_apps([emqx_conf, emqx_dashboard], fun set_special_configs/1), + emqx_mgmt_api_test_util:init_suite([emqx_conf]), Config. -set_special_configs(emqx_dashboard) -> - emqx_dashboard_api_test_helpers:set_default_config(), - ok; -set_special_configs(_) -> - ok. - end_per_suite(Config) -> end_suite(), Config. end_suite() -> - application:unload(emqx_management), - emqx_common_test_helpers:stop_apps([emqx_dashboard]). + emqx_mgmt_api_test_util:end_suite([emqx_conf]). t_bad_api_path(_) -> Url = ?SERVER ++ "/for/test/some/path/not/exist", - {error, {"HTTP/1.1", 404, "Not Found"}} = request(Url), + {ok, 404, _} = request(get, Url), ok. - -request(Url) -> - Request = {Url, []}, - case httpc:request(get, Request, [], []) of - {error, Reason} -> - {error, Reason}; - {ok, {{"HTTP/1.1", Code, _}, _, Return}} when - Code >= 200 andalso Code =< 299 - -> - {ok, emqx_json:decode(Return, [return_maps])}; - {ok, {Reason, _, _}} -> - {error, Reason} - end. diff --git a/apps/emqx_dashboard/test/emqx_dashboard_monitor_SUITE.erl b/apps/emqx_dashboard/test/emqx_dashboard_monitor_SUITE.erl index 6f4a0e0fd..7d4980320 100644 --- a/apps/emqx_dashboard/test/emqx_dashboard_monitor_SUITE.erl +++ b/apps/emqx_dashboard/test/emqx_dashboard_monitor_SUITE.erl @@ -19,6 +19,8 @@ -compile(nowarn_export_all). -compile(export_all). +-import(emqx_dashboard_SUITE, [auth_header_/0]). + -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). -include_lib("emqx/include/emqx.hrl"). @@ -153,10 +155,6 @@ do_request_api(Method, Request) -> {error, Reason} end. -auth_header_() -> - Basic = binary_to_list(base64:encode(<<"admin:public">>)), - {"Authorization", "Basic " ++ Basic}. - restart_monitor() -> OldMonitor = erlang:whereis(emqx_dashboard_monitor), erlang:exit(OldMonitor, kill), diff --git a/apps/emqx_exhook/test/emqx_exhook_api_SUITE.erl b/apps/emqx_exhook/test/emqx_exhook_api_SUITE.erl index 7be940a53..8a4fb7a44 100644 --- a/apps/emqx_exhook/test/emqx_exhook_api_SUITE.erl +++ b/apps/emqx_exhook/test/emqx_exhook_api_SUITE.erl @@ -347,13 +347,7 @@ do_request_api(Method, Request) -> end. auth_header_() -> - AppId = <<"admin">>, - AppSecret = <<"public">>, - auth_header_(binary_to_list(AppId), binary_to_list(AppSecret)). - -auth_header_(User, Pass) -> - Encoded = base64:encode_to_string(lists:append([User, ":", Pass])), - {"Authorization", "Basic " ++ Encoded}. + emqx_mgmt_api_test_util:auth_header_(). api_path(Parts) -> ?HOST ++ filename:join([?BASE_PATH, ?API_VERSION] ++ Parts). diff --git a/apps/emqx_gateway/test/emqx_gateway_test_utils.erl b/apps/emqx_gateway/test/emqx_gateway_test_utils.erl index a6791a36b..deb602bc7 100644 --- a/apps/emqx_gateway/test/emqx_gateway_test_utils.erl +++ b/apps/emqx_gateway/test/emqx_gateway_test_utils.erl @@ -106,8 +106,6 @@ assert_fields_exist(Ks, Map) -> %% http -define(http_api_host, "http://127.0.0.1:18083/api/v5"). --define(default_user, "admin"). --define(default_pass, "public"). request(delete = Mth, Path) -> do_request(Mth, req(Path, [])); @@ -176,5 +174,4 @@ url(Path, Qs) -> lists:concat([?http_api_host, Path, "?", binary_to_list(cow_qs:qs(Qs))]). auth(Headers) -> - Token = base64:encode(?default_user ++ ":" ++ ?default_pass), - [{"Authorization", "Basic " ++ binary_to_list(Token)}] ++ Headers. + [emqx_mgmt_api_test_util:auth_header_() | Headers]. diff --git a/apps/emqx_management/src/emqx_mgmt_auth.erl b/apps/emqx_management/src/emqx_mgmt_auth.erl index 0bf849d3c..6f2a27414 100644 --- a/apps/emqx_management/src/emqx_mgmt_auth.erl +++ b/apps/emqx_management/src/emqx_mgmt_auth.erl @@ -40,6 +40,10 @@ do_force_create_app/3 ]). +-ifdef(TEST). +-export([create/5]). +-endif. + -define(APP, emqx_app). -record(?APP, { @@ -68,8 +72,12 @@ init_bootstrap_file() -> init_bootstrap_file(File). create(Name, Enable, ExpiredAt, Desc) -> + ApiSecret = generate_api_secret(), + create(Name, ApiSecret, Enable, ExpiredAt, Desc). + +create(Name, ApiSecret, Enable, ExpiredAt, Desc) -> case mnesia:table_info(?APP, size) < 100 of - true -> create_app(Name, Enable, ExpiredAt, Desc); + true -> create_app(Name, ApiSecret, Enable, ExpiredAt, Desc); false -> {error, "Maximum ApiKey"} end. @@ -157,8 +165,7 @@ to_map(#?APP{name = N, api_key = K, enable = E, expired_at = ET, created_at = CT is_expired(undefined) -> false; is_expired(ExpiredTime) -> ExpiredTime < erlang:system_time(second). -create_app(Name, Enable, ExpiredAt, Desc) -> - ApiSecret = generate_api_secret(), +create_app(Name, ApiSecret, Enable, ExpiredAt, Desc) -> App = #?APP{ name = Name, @@ -170,9 +177,10 @@ create_app(Name, Enable, ExpiredAt, Desc) -> api_key = list_to_binary(emqx_misc:gen_id(16)) }, case create_app(App) of - {error, api_key_already_existed} -> create_app(Name, Enable, ExpiredAt, Desc); - {ok, Res} -> {ok, Res#{api_secret => ApiSecret}}; - Error -> Error + {ok, Res} -> + {ok, Res#{api_secret => ApiSecret}}; + Error -> + Error end. create_app(App = #?APP{api_key = ApiKey, name = Name}) -> diff --git a/apps/emqx_management/test/emqx_mgmt_api_api_keys_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_api_api_keys_SUITE.erl index 079351538..241a73dc4 100644 --- a/apps/emqx_management/test/emqx_mgmt_api_api_keys_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_api_api_keys_SUITE.erl @@ -225,21 +225,23 @@ t_create_unexpired_app(_Config) -> ok. list_app() -> + AuthHeader = emqx_dashboard_SUITE:auth_header_(), Path = emqx_mgmt_api_test_util:api_path(["api_key"]), - case emqx_mgmt_api_test_util:request_api(get, Path) of + case emqx_mgmt_api_test_util:request_api(get, Path, AuthHeader) of {ok, Apps} -> {ok, emqx_json:decode(Apps, [return_maps])}; Error -> Error end. read_app(Name) -> + AuthHeader = emqx_dashboard_SUITE:auth_header_(), Path = emqx_mgmt_api_test_util:api_path(["api_key", Name]), - case emqx_mgmt_api_test_util:request_api(get, Path) of + case emqx_mgmt_api_test_util:request_api(get, Path, AuthHeader) of {ok, Res} -> {ok, emqx_json:decode(Res, [return_maps])}; Error -> Error end. create_app(Name) -> - AuthHeader = emqx_mgmt_api_test_util:auth_header_(), + AuthHeader = emqx_dashboard_SUITE:auth_header_(), Path = emqx_mgmt_api_test_util:api_path(["api_key"]), ExpiredAt = to_rfc3339(erlang:system_time(second) + 1000), App = #{ @@ -254,7 +256,7 @@ create_app(Name) -> end. create_unexpired_app(Name, Params) -> - AuthHeader = emqx_mgmt_api_test_util:auth_header_(), + AuthHeader = emqx_dashboard_SUITE:auth_header_(), Path = emqx_mgmt_api_test_util:api_path(["api_key"]), App = maps:merge(#{name => Name, desc => <<"Note"/utf8>>, enable => true}, Params), case emqx_mgmt_api_test_util:request_api(post, Path, "", AuthHeader, App) of @@ -263,11 +265,12 @@ create_unexpired_app(Name, Params) -> end. delete_app(Name) -> + AuthHeader = emqx_dashboard_SUITE:auth_header_(), DeletePath = emqx_mgmt_api_test_util:api_path(["api_key", Name]), - emqx_mgmt_api_test_util:request_api(delete, DeletePath). + emqx_mgmt_api_test_util:request_api(delete, DeletePath, AuthHeader). update_app(Name, Change) -> - AuthHeader = emqx_mgmt_api_test_util:auth_header_(), + AuthHeader = emqx_dashboard_SUITE:auth_header_(), UpdatePath = emqx_mgmt_api_test_util:api_path(["api_key", Name]), case emqx_mgmt_api_test_util:request_api(put, UpdatePath, "", AuthHeader, Change) of {ok, Update} -> {ok, emqx_json:decode(Update, [return_maps])}; diff --git a/apps/emqx_management/test/emqx_mgmt_api_test_util.erl b/apps/emqx_management/test/emqx_mgmt_api_test_util.erl index 5bb0ba818..82d55bb6a 100644 --- a/apps/emqx_management/test/emqx_mgmt_api_test_util.erl +++ b/apps/emqx_management/test/emqx_mgmt_api_test_util.erl @@ -24,14 +24,19 @@ init_suite() -> init_suite([]). init_suite(Apps) -> + init_suite(Apps, fun set_special_configs/1). + +init_suite(Apps, SetConfigs) -> mria:start(), application:load(emqx_management), - emqx_common_test_helpers:start_apps(Apps ++ [emqx_dashboard], fun set_special_configs/1). + emqx_common_test_helpers:start_apps(Apps ++ [emqx_dashboard], SetConfigs), + emqx_common_test_http:create_default_app(). end_suite() -> end_suite([]). end_suite(Apps) -> + emqx_common_test_http:delete_default_app(), application:unload(emqx_management), emqx_common_test_helpers:stop_apps(Apps ++ [emqx_dashboard]), emqx_config:delete_override_conf_files(), @@ -43,8 +48,23 @@ set_special_configs(emqx_dashboard) -> set_special_configs(_App) -> ok. +%% there is no difference between the 'request' and 'request_api' +%% the 'request' is only to be compatible with the 'emqx_dashboard_api_test_helpers:request' +request(Method, Url) -> + request(Method, Url, []). + +request(Method, Url, Body) -> + request_api_with_body(Method, Url, Body). + +uri(Parts) -> + emqx_dashboard_api_test_helpers:uri(Parts). + +%% compatible_mode will return as same as 'emqx_dashboard_api_test_helpers:request' +request_api_with_body(Method, Url, Body) -> + request_api(Method, Url, [], auth_header_(), Body, #{compatible_mode => true}). + request_api(Method, Url) -> - request_api(Method, Url, [], [], [], #{}). + request_api(Method, Url, auth_header_()). request_api(Method, Url, AuthOrHeaders) -> request_api(Method, Url, [], AuthOrHeaders, [], #{}). @@ -90,10 +110,20 @@ request_api(Method, Url, QueryParams, AuthOrHeaders, Body, Opts) when do_request_api(Method, Request, Opts) -> ReturnAll = maps:get(return_all, Opts, false), + CompatibleMode = maps:get(compatible_mode, Opts, false), + ReqOpts = + case CompatibleMode of + true -> + [{body_format, binary}]; + _ -> + [] + end, ct:pal("Method: ~p, Request: ~p", [Method, Request]), - case httpc:request(Method, Request, [], []) of + case httpc:request(Method, Request, [], ReqOpts) of {error, socket_closed_remotely} -> {error, socket_closed_remotely}; + {ok, {{_, Code, _}, _Headers, Body}} when CompatibleMode -> + {ok, Code, Body}; {ok, {{"HTTP/1.1", Code, _} = Reason, Headers, Body}} when Code >= 200 andalso Code =< 299 andalso ReturnAll -> @@ -109,10 +139,7 @@ do_request_api(Method, Request, Opts) -> end. auth_header_() -> - Username = <<"admin">>, - Password = <<"public">>, - {ok, Token} = emqx_dashboard_admin:sign_token(Username, Password), - {"Authorization", "Bearer " ++ binary_to_list(Token)}. + emqx_common_test_http:default_auth_header(). build_http_header(X) when is_list(X) -> X; diff --git a/apps/emqx_management/test/emqx_mgmt_api_trace_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_api_trace_SUITE.erl index 8e8c5b06f..0ba05b280 100644 --- a/apps/emqx_management/test/emqx_mgmt_api_trace_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_api_trace_SUITE.erl @@ -30,6 +30,8 @@ -define(API_VERSION, "v5"). -define(BASE_PATH, "api"). +-import(emqx_dashboard_SUITE, [auth_header_/0]). + %%-------------------------------------------------------------------- %% Setups %%-------------------------------------------------------------------- @@ -330,13 +332,6 @@ t_stream_log(_Config) -> to_rfc3339(Second) -> list_to_binary(calendar:system_time_to_rfc3339(Second)). -auth_header_() -> - auth_header_("admin", "public"). - -auth_header_(User, Pass) -> - Encoded = base64:encode_to_string(lists:append([User, ":", Pass])), - {"Authorization", "Basic " ++ Encoded}. - request_api(Method, Url, Auth) -> do_request_api(Method, {Url, [Auth]}). request_api(Method, Url, Auth, Body) -> diff --git a/apps/emqx_modules/test/emqx_delayed_api_SUITE.erl b/apps/emqx_modules/test/emqx_delayed_api_SUITE.erl index 96cdf7840..ed3cd9292 100644 --- a/apps/emqx_modules/test/emqx_delayed_api_SUITE.erl +++ b/apps/emqx_modules/test/emqx_delayed_api_SUITE.erl @@ -26,7 +26,7 @@ <<"max_delayed_messages">> => <<"0">> }). --import(emqx_dashboard_api_test_helpers, [request/2, request/3, uri/1]). +-import(emqx_mgmt_api_test_util, [request/2, request/3, uri/1]). all() -> emqx_common_test_helpers:all(?MODULE). @@ -36,27 +36,21 @@ init_per_suite(Config) -> raw_with_default => true }), - ok = emqx_common_test_helpers:start_apps( - [emqx_conf, emqx_modules, emqx_dashboard], - fun set_special_configs/1 + ok = emqx_mgmt_api_test_util:init_suite( + [emqx_conf, emqx_modules] ), emqx_delayed:load(), Config. end_per_suite(Config) -> ok = emqx_delayed:unload(), - emqx_common_test_helpers:stop_apps([emqx_conf, emqx_dashboard, emqx_modules]), + emqx_mgmt_api_test_util:end_suite([emqx_conf, emqx_modules]), Config. init_per_testcase(_, Config) -> {ok, _} = emqx_cluster_rpc:start_link(), Config. -set_special_configs(emqx_dashboard) -> - emqx_dashboard_api_test_helpers:set_default_config(); -set_special_configs(_App) -> - ok. - %%------------------------------------------------------------------------------ %% Test Cases %%------------------------------------------------------------------------------ diff --git a/apps/emqx_modules/test/emqx_rewrite_api_SUITE.erl b/apps/emqx_modules/test/emqx_rewrite_api_SUITE.erl index 90e90d788..ddb136f1e 100644 --- a/apps/emqx_modules/test/emqx_rewrite_api_SUITE.erl +++ b/apps/emqx_modules/test/emqx_rewrite_api_SUITE.erl @@ -18,7 +18,7 @@ -compile(nowarn_export_all). -compile(export_all). --import(emqx_dashboard_api_test_helpers, [request/3, uri/1]). +-import(emqx_mgmt_api_test_util, [request/3, uri/1]). -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). @@ -37,20 +37,14 @@ init_per_suite(Config) -> raw_with_default => true }), - ok = emqx_common_test_helpers:start_apps( - [emqx_conf, emqx_modules, emqx_dashboard], - fun set_special_configs/1 + ok = emqx_mgmt_api_test_util:init_suite( + [emqx_conf, emqx_modules] ), Config. end_per_suite(_Config) -> - emqx_common_test_helpers:stop_apps([emqx_conf, emqx_dashboard, emqx_modules]), - ok. - -set_special_configs(emqx_dashboard) -> - emqx_dashboard_api_test_helpers:set_default_config(); -set_special_configs(_App) -> + emqx_mgmt_api_test_util:end_suite([emqx_conf, emqx_modules]), ok. %%------------------------------------------------------------------------------ @@ -81,7 +75,7 @@ t_mqtt_topic_rewrite(_) -> ?assertEqual( Rules, - jsx:decode(Result) + emqx_json:decode(Result, [return_maps]) ). t_mqtt_topic_rewrite_limit(_) -> diff --git a/apps/emqx_modules/test/emqx_telemetry_api_SUITE.erl b/apps/emqx_modules/test/emqx_telemetry_api_SUITE.erl index 288a155d9..16f942bc0 100644 --- a/apps/emqx_modules/test/emqx_telemetry_api_SUITE.erl +++ b/apps/emqx_modules/test/emqx_telemetry_api_SUITE.erl @@ -18,7 +18,7 @@ -compile(nowarn_export_all). -compile(export_all). --import(emqx_dashboard_api_test_helpers, [request/2, request/3, uri/1]). +-import(emqx_mgmt_api_test_util, [request/2, request/3, uri/1]). -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). @@ -33,8 +33,8 @@ init_per_suite(Config) -> raw_with_default => true }), - ok = emqx_common_test_helpers:start_apps( - [emqx_conf, emqx_authn, emqx_authz, emqx_modules, emqx_dashboard], + ok = emqx_mgmt_api_test_util:init_suite( + [emqx_conf, emqx_authn, emqx_authz, emqx_modules], fun set_special_configs/1 ), @@ -49,8 +49,8 @@ end_per_suite(_Config) -> <<"sources">> => [] } ), - emqx_common_test_helpers:stop_apps([ - emqx_dashboard, emqx_conf, emqx_authn, emqx_authz, emqx_modules + emqx_mgmt_api_test_util:end_suite([ + emqx_conf, emqx_authn, emqx_authz, emqx_modules ]), ok. diff --git a/apps/emqx_modules/test/emqx_topic_metrics_api_SUITE.erl b/apps/emqx_modules/test/emqx_topic_metrics_api_SUITE.erl index 8c23d042c..ea85d1fe9 100644 --- a/apps/emqx_modules/test/emqx_topic_metrics_api_SUITE.erl +++ b/apps/emqx_modules/test/emqx_topic_metrics_api_SUITE.erl @@ -18,7 +18,7 @@ -compile(nowarn_export_all). -compile(export_all). --import(emqx_dashboard_api_test_helpers, [request/3, uri/1]). +-import(emqx_mgmt_api_test_util, [request/2, request/3, uri/1]). -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). @@ -44,9 +44,8 @@ init_per_suite(Config) -> raw_with_default => true }), - ok = emqx_common_test_helpers:start_apps( - [emqx_conf, emqx_modules, emqx_dashboard], - fun set_special_configs/1 + ok = emqx_mgmt_api_test_util:init_suite( + [emqx_conf, emqx_modules] ), %% When many tests run in an obscure order, it may occur that @@ -59,15 +58,10 @@ init_per_suite(Config) -> Config. end_per_suite(_Config) -> - emqx_common_test_helpers:stop_apps([emqx_conf, emqx_dashboard, emqx_modules]), + emqx_mgmt_api_test_util:end_suite([emqx_conf, emqx_modules]), application:stop(gen_rpc), ok. -set_special_configs(emqx_dashboard) -> - emqx_dashboard_api_test_helpers:set_default_config(); -set_special_configs(_App) -> - ok. - %%------------------------------------------------------------------------------ %% Tests %%------------------------------------------------------------------------------ @@ -315,6 +309,3 @@ t_badrpc(_) -> %%------------------------------------------------------------------------------ %% Helpers %%------------------------------------------------------------------------------ - -request(Method, Url) -> - request(Method, Url, []). diff --git a/apps/emqx_slow_subs/test/emqx_slow_subs_api_SUITE.erl b/apps/emqx_slow_subs/test/emqx_slow_subs_api_SUITE.erl index 5b5ed063f..6b0721e3d 100644 --- a/apps/emqx_slow_subs/test/emqx_slow_subs_api_SUITE.erl +++ b/apps/emqx_slow_subs/test/emqx_slow_subs_api_SUITE.erl @@ -203,13 +203,7 @@ do_request_api(Method, Request) -> end. auth_header_() -> - AppId = <<"admin">>, - AppSecret = <<"public">>, - auth_header_(binary_to_list(AppId), binary_to_list(AppSecret)). - -auth_header_(User, Pass) -> - Encoded = base64:encode_to_string(lists:append([User, ":", Pass])), - {"Authorization", "Basic " ++ Encoded}. + emqx_mgmt_api_test_util:auth_header_(). api_path(Parts) -> ?HOST ++ filename:join([?BASE_PATH, ?API_VERSION] ++ Parts). From 45c3b372dd4adaa12137bbbd16b9a777b87760dc Mon Sep 17 00:00:00 2001 From: firest Date: Thu, 12 Jan 2023 11:56:09 +0800 Subject: [PATCH 0129/1802] chore: update changes --- changes/v5.0.15/feat-9586.en.md | 1 + changes/v5.0.15/feat-9586.zh.md | 1 + 2 files changed, 2 insertions(+) create mode 100644 changes/v5.0.15/feat-9586.en.md create mode 100644 changes/v5.0.15/feat-9586.zh.md diff --git a/changes/v5.0.15/feat-9586.en.md b/changes/v5.0.15/feat-9586.en.md new file mode 100644 index 000000000..777fb81df --- /dev/null +++ b/changes/v5.0.15/feat-9586.en.md @@ -0,0 +1 @@ +Basic auth is no longer allowed for API calls, must use API key instead. diff --git a/changes/v5.0.15/feat-9586.zh.md b/changes/v5.0.15/feat-9586.zh.md new file mode 100644 index 000000000..102266a46 --- /dev/null +++ b/changes/v5.0.15/feat-9586.zh.md @@ -0,0 +1 @@ +API 调用不再支持基于 `username:password` 的 `baisc` 认证, 现在 API 必须通过 API Key 才能进行调用。 From 1711823487859d1cf572a1e42febb14efb57f8b9 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Thu, 12 Jan 2023 14:37:07 +0800 Subject: [PATCH 0130/1802] refactor: remove default value of timestamp field of influxdb --- .../src/emqx_plugin_libs.app.src | 2 +- .../src/emqx_plugin_libs_rule.erl | 2 + .../src/emqx_ee_bridge_influxdb.erl | 36 ++--- .../test/emqx_ee_bridge_influxdb_SUITE.erl | 1 - lib-ee/emqx_ee_connector/rebar.config | 2 +- .../src/emqx_ee_connector_influxdb.erl | 135 ++++++++++-------- mix.exs | 2 +- 7 files changed, 92 insertions(+), 88 deletions(-) diff --git a/apps/emqx_plugin_libs/src/emqx_plugin_libs.app.src b/apps/emqx_plugin_libs/src/emqx_plugin_libs.app.src index bcdcfe420..3120b8503 100644 --- a/apps/emqx_plugin_libs/src/emqx_plugin_libs.app.src +++ b/apps/emqx_plugin_libs/src/emqx_plugin_libs.app.src @@ -1,7 +1,7 @@ %% -*- mode: erlang -*- {application, emqx_plugin_libs, [ {description, "EMQX Plugin utility libs"}, - {vsn, "4.3.4"}, + {vsn, "4.3.5"}, {modules, []}, {applications, [kernel, stdlib]}, {env, []} diff --git a/apps/emqx_plugin_libs/src/emqx_plugin_libs_rule.erl b/apps/emqx_plugin_libs/src/emqx_plugin_libs_rule.erl index 57bdd16e5..969374309 100644 --- a/apps/emqx_plugin_libs/src/emqx_plugin_libs_rule.erl +++ b/apps/emqx_plugin_libs/src/emqx_plugin_libs_rule.erl @@ -63,6 +63,8 @@ can_topic_match_oneof/2 ]). +-export_type([tmpl_token/0]). + -compile({no_auto_import, [float/1]}). -define(EX_PLACE_HOLDER, "(\\$\\{[a-zA-Z0-9\\._]+\\})"). diff --git a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_influxdb.erl b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_influxdb.erl index 62c8b6ab7..b42c27832 100644 --- a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_influxdb.erl +++ b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_influxdb.erl @@ -196,33 +196,25 @@ to_influx_lines(RawLines) -> converter_influx_line(Line, AccIn) -> case string:tokens(str(Line), " ") of [MeasurementAndTags, Fields, Timestamp] -> - {Measurement, Tags} = split_measurement_and_tags(MeasurementAndTags), - [ - #{ - measurement => Measurement, - tags => kv_pairs(Tags), - fields => kv_pairs(string:tokens(Fields, ",")), - timestamp => Timestamp - } - | AccIn - ]; + append_influx_item(MeasurementAndTags, Fields, Timestamp, AccIn); [MeasurementAndTags, Fields] -> - {Measurement, Tags} = split_measurement_and_tags(MeasurementAndTags), - %% TODO: fix here both here and influxdb driver. - %% Default value should evaluated by InfluxDB. - [ - #{ - measurement => Measurement, - tags => kv_pairs(Tags), - fields => kv_pairs(string:tokens(Fields, ",")), - timestamp => "${timestamp}" - } - | AccIn - ]; + append_influx_item(MeasurementAndTags, Fields, undefined, AccIn); _ -> throw("Bad InfluxDB Line Protocol schema") end. +append_influx_item(MeasurementAndTags, Fields, Timestamp, Acc) -> + {Measurement, Tags} = split_measurement_and_tags(MeasurementAndTags), + [ + #{ + measurement => Measurement, + tags => kv_pairs(Tags), + fields => kv_pairs(string:tokens(Fields, ",")), + timestamp => Timestamp + } + | Acc + ]. + split_measurement_and_tags(Subject) -> case string:tokens(Subject, ",") of [] -> diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_influxdb_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_influxdb_SUITE.erl index 6331611d0..bb87a9f37 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_influxdb_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_influxdb_SUITE.erl @@ -525,7 +525,6 @@ t_start_ok(Config) -> SentData = #{ <<"clientid">> => ClientId, <<"topic">> => atom_to_binary(?FUNCTION_NAME), - <<"timestamp">> => erlang:system_time(nanosecond), <<"payload">> => Payload }, ?check_trace( diff --git a/lib-ee/emqx_ee_connector/rebar.config b/lib-ee/emqx_ee_connector/rebar.config index ab4c88396..262641d44 100644 --- a/lib-ee/emqx_ee_connector/rebar.config +++ b/lib-ee/emqx_ee_connector/rebar.config @@ -1,7 +1,7 @@ {erl_opts, [debug_info]}. {deps, [ {hstreamdb_erl, {git, "https://github.com/hstreamdb/hstreamdb_erl.git", {tag, "0.2.5"}}}, - {influxdb, {git, "https://github.com/emqx/influxdb-client-erl", {tag, "1.1.5"}}}, + {influxdb, {git, "https://github.com/emqx/influxdb-client-erl", {tag, "1.1.6"}}}, {emqx, {path, "../../apps/emqx"}} ]}. diff --git a/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl b/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl index 7974bf028..db99c4475 100644 --- a/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl +++ b/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl @@ -142,7 +142,11 @@ fields(common) -> [ {server, server()}, {precision, - mk(enum([ns, us, ms, s, m, h]), #{ + %% The influxdb only supports these 4 precision: + %% See "https://github.com/influxdata/influxdb/blob/ + %% 6b607288439a991261307518913eb6d4e280e0a7/models/points.go#L487" for + %% more information. + mk(enum([ns, us, ms, s]), #{ required => false, default => ms, desc => ?DESC("precision") })} ]; @@ -210,9 +214,7 @@ start_client(InstId, Config) -> do_start_client( InstId, ClientConfig, - Config = #{ - write_syntax := Lines - } + Config = #{write_syntax := Lines} ) -> case influxdb:start_client(ClientConfig) of {ok, Client} -> @@ -220,7 +222,9 @@ do_start_client( true -> State = #{ client => Client, - write_syntax => to_config(Lines) + write_syntax => to_config( + Lines, proplists:get_value(precision, ClientConfig) + ) }, ?SLOG(info, #{ msg => "starting influxdb connector success", @@ -348,30 +352,33 @@ do_async_query(InstId, Client, Points, ReplyFunAndArgs) -> %% ------------------------------------------------------------------------------------------------- %% Tags & Fields Config Trans -to_config(Lines) -> - to_config(Lines, []). +to_config(Lines, Precision) -> + to_config(Lines, [], Precision). -to_config([], Acc) -> +to_config([], Acc, _Precision) -> lists:reverse(Acc); -to_config( - [ - #{ - measurement := Measurement, - timestamp := Timestamp, - tags := Tags, - fields := Fields - } - | Rest - ], - Acc -) -> - Res = #{ - measurement => emqx_plugin_libs_rule:preproc_tmpl(Measurement), - timestamp => emqx_plugin_libs_rule:preproc_tmpl(Timestamp), - tags => to_kv_config(Tags), - fields => to_kv_config(Fields) +to_config([Item0 | Rest], Acc, Precision) -> + Ts = maps:get(timestamp, Item0, undefined), + Item = #{ + measurement => emqx_plugin_libs_rule:preproc_tmpl(maps:get(measurement, Item0)), + timestamp => preproc_tmpl_timestamp(Ts, Precision), + tags => to_kv_config(maps:get(tags, Item0)), + fields => to_kv_config(maps:get(fields, Item0)) }, - to_config(Rest, [Res | Acc]). + to_config(Rest, [Item | Acc], Precision). + +preproc_tmpl_timestamp(undefined, <<"ns">>) -> + erlang:system_time(nanosecond); +preproc_tmpl_timestamp(undefined, <<"us">>) -> + erlang:system_time(microsecond); +preproc_tmpl_timestamp(undefined, <<"ms">>) -> + erlang:system_time(millisecond); +preproc_tmpl_timestamp(undefined, <<"s">>) -> + erlang:system_time(second); +preproc_tmpl_timestamp(Ts, _) when is_integer(Ts) -> + Ts; +preproc_tmpl_timestamp(Ts, _) when is_binary(Ts); is_list(Ts) -> + emqx_plugin_libs_rule:preproc_tmpl(Ts). to_kv_config(KVfields) -> maps:fold(fun to_maps_config/3, #{}, proplists:to_map(KVfields)). @@ -414,7 +421,7 @@ parse_batch_data(InstId, BatchData, SyntaxLines) -> fields := [{binary(), binary()}], measurement := binary(), tags := [{binary(), binary()}], - timestamp := binary() + timestamp := emqx_plugin_libs_rule:tmpl_token() | integer() } ]) -> {ok, [map()]} | {error, term()}. data_to_points(Data, SyntaxLines) -> @@ -430,46 +437,50 @@ lines_to_points(_, [], Points, ErrorPoints) -> %% ignore trans succeeded points {error, ErrorPoints} end; -lines_to_points( - Data, - [ - #{ - measurement := Measurement, - timestamp := Timestamp, - tags := Tags, - fields := Fields - } - | Rest - ], - ResultPointsAcc, - ErrorPointsAcc -) -> +lines_to_points(Data, [#{timestamp := Ts} = Item | Rest], ResultPointsAcc, ErrorPointsAcc) when + is_list(Ts) +-> TransOptions = #{return => rawlist, var_trans => fun data_filter/1}, - case emqx_plugin_libs_rule:proc_tmpl(Timestamp, Data, TransOptions) of - [TimestampInt] when is_integer(TimestampInt) -> - {_, EncodedTags} = maps:fold(fun maps_config_to_data/3, {Data, #{}}, Tags), - {_, EncodedFields} = maps:fold(fun maps_config_to_data/3, {Data, #{}}, Fields), - Point = #{ - measurement => emqx_plugin_libs_rule:proc_tmpl(Measurement, Data), - timestamp => TimestampInt, - tags => EncodedTags, - fields => EncodedFields - }, - case map_size(EncodedFields) =:= 0 of - true -> - %% influxdb client doesn't like empty field maps... - lines_to_points(Data, Rest, ResultPointsAcc, [ - {error, no_fields} | ErrorPointsAcc - ]); - false -> - lines_to_points(Data, Rest, [Point | ResultPointsAcc], ErrorPointsAcc) - end; - BadTimestamp -> + case emqx_plugin_libs_rule:proc_tmpl(Ts, Data, TransOptions) of + [TsInt] when is_integer(TsInt) -> + Item1 = Item#{timestamp => TsInt}, + continue_lines_to_points(Data, Item1, Rest, ResultPointsAcc, ErrorPointsAcc); + BadTs -> lines_to_points(Data, Rest, ResultPointsAcc, [ - {error, {bad_timestamp, BadTimestamp}} | ErrorPointsAcc + {error, {bad_timestamp, BadTs}} | ErrorPointsAcc ]) + end; +lines_to_points(Data, [#{timestamp := Ts} = Item | Rest], ResultPointsAcc, ErrorPointsAcc) when + is_integer(Ts) +-> + continue_lines_to_points(Data, Item, Rest, ResultPointsAcc, ErrorPointsAcc). + +continue_lines_to_points(Data, Item, Rest, ResultPointsAcc, ErrorPointsAcc) -> + case line_to_point(Data, Item) of + #{fields := Fields} when map_size(Fields) =:= 0 -> + %% influxdb client doesn't like empty field maps... + ErrorPointsAcc1 = [{error, no_fields} | ErrorPointsAcc], + lines_to_points(Data, Rest, ResultPointsAcc, ErrorPointsAcc1); + Point -> + lines_to_points(Data, Rest, [Point | ResultPointsAcc], ErrorPointsAcc) end. +line_to_point( + Data, + #{ + measurement := Measurement, + tags := Tags, + fields := Fields + } = Item +) -> + {_, EncodedTags} = maps:fold(fun maps_config_to_data/3, {Data, #{}}, Tags), + {_, EncodedFields} = maps:fold(fun maps_config_to_data/3, {Data, #{}}, Fields), + Item#{ + measurement => emqx_plugin_libs_rule:proc_tmpl(Measurement, Data), + tags => EncodedTags, + fields => EncodedFields + }. + maps_config_to_data(K, V, {Data, Res}) -> KTransOptions = #{return => rawlist, var_trans => fun key_filter/1}, VTransOptions = #{return => rawlist, var_trans => fun data_filter/1}, diff --git a/mix.exs b/mix.exs index 832f6e7ae..27d9b43ce 100644 --- a/mix.exs +++ b/mix.exs @@ -131,7 +131,7 @@ defmodule EMQXUmbrella.MixProject do defp enterprise_deps(_profile_info = %{edition_type: :enterprise}) do [ {:hstreamdb_erl, github: "hstreamdb/hstreamdb_erl", tag: "0.2.5"}, - {:influxdb, github: "emqx/influxdb-client-erl", tag: "1.1.4", override: true}, + {:influxdb, github: "emqx/influxdb-client-erl", tag: "1.1.6", override: true}, {:wolff, github: "kafka4beam/wolff", tag: "1.7.4"}, {:kafka_protocol, github: "kafka4beam/kafka_protocol", tag: "4.1.2", override: true}, {:brod_gssapi, github: "kafka4beam/brod_gssapi", tag: "v0.1.0-rc1"}, From 9adc6017545a4586c053517a096a53ce5d738dd7 Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Thu, 12 Jan 2023 18:29:46 +0300 Subject: [PATCH 0131/1802] chore: add changelog for the fuzzy search API fix Co-authored-by: Zaiming (Stone) Shi --- changes/v5.0.15/fix-9726-en.md | 1 + changes/v5.0.15/fix-9726-zh.md | 1 + 2 files changed, 2 insertions(+) create mode 100644 changes/v5.0.15/fix-9726-en.md create mode 100644 changes/v5.0.15/fix-9726-zh.md diff --git a/changes/v5.0.15/fix-9726-en.md b/changes/v5.0.15/fix-9726-en.md new file mode 100644 index 000000000..9aa522690 --- /dev/null +++ b/changes/v5.0.15/fix-9726-en.md @@ -0,0 +1 @@ +Client fuzzy search API results were missing information which could tell if more results are available in the next pages, this is now fixed by providing `hasnext` flag in the response. diff --git a/changes/v5.0.15/fix-9726-zh.md b/changes/v5.0.15/fix-9726-zh.md new file mode 100644 index 000000000..3554d2db7 --- /dev/null +++ b/changes/v5.0.15/fix-9726-zh.md @@ -0,0 +1 @@ +在此修复前,客户端模糊搜索 API 缺少一些可以用于判断是否可以继续翻页的信息,现在通过在响应中提供 `hasnext` 标志来解决这个问题。 From 90e837783fc1a8d2c1d237cf57d3f43ca995e31d Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Thu, 12 Jan 2023 19:25:40 +0300 Subject: [PATCH 0132/1802] fix(paging): rename `partial` to `overflow` for clarity --- apps/emqx_management/src/emqx_mgmt_api.erl | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/apps/emqx_management/src/emqx_mgmt_api.erl b/apps/emqx_management/src/emqx_mgmt_api.erl index 9a1d0ce4e..ba232daf3 100644 --- a/apps/emqx_management/src/emqx_mgmt_api.erl +++ b/apps/emqx_management/src/emqx_mgmt_api.erl @@ -405,11 +405,11 @@ counting_total_fun(_QueryState = #{fuzzy_fun := FuzzyFun}) when FuzzyFun =/= und %% ResultAcc :: #{count := integer(), %% cursor := integer(), %% rows := [{node(), Rows :: list()}], -%% partial := boolean(), +%% overflow := boolean(), %% hasnext => boolean() %% } init_query_result() -> - #{cursor => 0, count => 0, rows => [], partial => false}. + #{cursor => 0, count => 0, rows => [], overflow => false}. accumulate_query_rows( Node, @@ -436,12 +436,13 @@ accumulate_query_rows( cursor => NCursor, count => Count + length(SubRows), rows => [{Node, SubRows} | RowsAcc], - partial => (Limit - Count) < Len + % there are more rows than can fit in the page + overflow => (Limit - Count) < Len }} end. -finalize_query(Result = #{partial := Partial}, QueryState = #{complete := Complete}) -> - HasNext = Partial orelse not Complete, +finalize_query(Result = #{overflow := Overflow}, QueryState = #{complete := Complete}) -> + HasNext = Overflow orelse not Complete, maybe_accumulate_totals(Result#{hasnext => HasNext}, QueryState). maybe_accumulate_totals(Result, #{total := TotalAcc}) -> From ed2be4d086b410003afc241423ee497c4f7456f1 Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Thu, 12 Jan 2023 10:21:50 -0300 Subject: [PATCH 0133/1802] feat(mongo): expose buffer worker opts to the bridge frontend (5.0) --- .../src/emqx_ee_bridge_mongodb.erl | 31 +++++++++++++++++-- .../test/emqx_ee_bridge_mongodb_SUITE.erl | 9 ++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_mongodb.erl b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_mongodb.erl index bb4082681..84db0c214 100644 --- a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_mongodb.erl +++ b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_mongodb.erl @@ -39,7 +39,7 @@ fields("config") -> {enable, mk(boolean(), #{desc => ?DESC("enable"), default => true})}, {collection, mk(binary(), #{desc => ?DESC("collection"), default => <<"mqtt">>})}, {payload_template, mk(binary(), #{required => false, desc => ?DESC("payload_template")})} - ]; + ] ++ fields("resource_opts"); fields(mongodb_rs) -> emqx_connector_mongo:fields(rs) ++ fields("config"); fields(mongodb_sharded) -> @@ -69,7 +69,32 @@ fields("get_sharded") -> fields("get_single") -> emqx_bridge_schema:metrics_status_fields() ++ fields(mongodb_single) ++ - type_and_name_fields(mongodb_single). + type_and_name_fields(mongodb_single); +fields("creation_opts") -> + lists:map( + fun + ({query_mode, _FieldSchema}) -> + {query_mode, + mk( + enum([sync, async]), + #{ + desc => ?DESC(emqx_resource_schema, "query_mode"), + default => sync + } + )}; + (Field) -> + Field + end, + emqx_resource_schema:fields("creation_opts") + ); +fields("resource_opts") -> + [ + {resource_opts, + mk( + ref(?MODULE, "creation_opts"), + #{default => #{}, desc => ?DESC(emqx_resource_schema, "resource_opts")} + )} + ]. conn_bridge_examples(Method) -> [ @@ -95,6 +120,8 @@ conn_bridge_examples(Method) -> desc("config") -> ?DESC("desc_config"); +desc("creation_opts") -> + ?DESC(emqx_resource_schema, "creation_opts"); desc(mongodb_rs) -> ?DESC(mongodb_rs_conf); desc(mongodb_sharded) -> diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_mongodb_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_mongodb_SUITE.erl index 7e44347f3..f81571223 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_mongodb_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_mongodb_SUITE.erl @@ -152,6 +152,9 @@ mongo_config(MongoHost, MongoPort0, rs = Type) -> " servers = [~p]\n" " w_mode = safe\n" " database = mqtt\n" + " resource_opts = {\n" + " worker_pool_size = 1\n" + " }\n" "}", [Name, Servers] ), @@ -168,6 +171,9 @@ mongo_config(MongoHost, MongoPort0, sharded = Type) -> " servers = [~p]\n" " w_mode = safe\n" " database = mqtt\n" + " resource_opts = {\n" + " worker_pool_size = 1\n" + " }\n" "}", [Name, Servers] ), @@ -184,6 +190,9 @@ mongo_config(MongoHost, MongoPort0, single = Type) -> " server = ~p\n" " w_mode = safe\n" " database = mqtt\n" + " resource_opts = {\n" + " worker_pool_size = 1\n" + " }\n" "}", [Name, Server] ), From c2fd1a4482a30307fa986384d3d611bfcdeb03d8 Mon Sep 17 00:00:00 2001 From: Erik Timan Date: Thu, 12 Jan 2023 10:10:52 +0100 Subject: [PATCH 0134/1802] feat(emqx_bridge): shorten operation endpoint URLs This shortens and simplifies URLs for performing bridge operations so that the API looks more congruent. --- apps/emqx_bridge/src/emqx_bridge_api.erl | 20 +++++++++---------- .../test/emqx_bridge_api_SUITE.erl | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/apps/emqx_bridge/src/emqx_bridge_api.erl b/apps/emqx_bridge/src/emqx_bridge_api.erl index cf39ebf14..a71142bc5 100644 --- a/apps/emqx_bridge/src/emqx_bridge_api.erl +++ b/apps/emqx_bridge/src/emqx_bridge_api.erl @@ -36,8 +36,8 @@ -export([ '/bridges'/2, '/bridges/:id'/2, - '/bridges/:id/operation/:operation'/2, - '/nodes/:node/bridges/:id/operation/:operation'/2, + '/bridges/:id/:operation'/2, + '/nodes/:node/bridges/:id/:operation'/2, '/bridges/:id/reset_metrics'/2 ]). @@ -66,8 +66,8 @@ paths() -> [ "/bridges", "/bridges/:id", - "/bridges/:id/operation/:operation", - "/nodes/:node/bridges/:id/operation/:operation", + "/bridges/:id/:operation", + "/nodes/:node/bridges/:id/:operation", "/bridges/:id/reset_metrics" ]. @@ -348,9 +348,9 @@ schema("/bridges/:id/reset_metrics") -> } } }; -schema("/bridges/:id/operation/:operation") -> +schema("/bridges/:id/:operation") -> #{ - 'operationId' => '/bridges/:id/operation/:operation', + 'operationId' => '/bridges/:id/:operation', post => #{ tags => [<<"bridges">>], summary => <<"Enable/Disable/Stop/Restart Bridge">>, @@ -366,9 +366,9 @@ schema("/bridges/:id/operation/:operation") -> } } }; -schema("/nodes/:node/bridges/:id/operation/:operation") -> +schema("/nodes/:node/bridges/:id/:operation") -> #{ - 'operationId' => '/nodes/:node/bridges/:id/operation/:operation', + 'operationId' => '/nodes/:node/bridges/:id/:operation', post => #{ tags => [<<"bridges">>], summary => <<"Stop/Restart Bridge">>, @@ -485,7 +485,7 @@ lookup_from_local_node(BridgeType, BridgeName) -> Error -> Error end. -'/bridges/:id/operation/:operation'(post, #{ +'/bridges/:id/:operation'(post, #{ bindings := #{id := Id, operation := Op} }) -> @@ -513,7 +513,7 @@ lookup_from_local_node(BridgeType, BridgeName) -> end ). -'/nodes/:node/bridges/:id/operation/:operation'(post, #{ +'/nodes/:node/bridges/:id/:operation'(post, #{ bindings := #{id := Id, operation := Op, node := Node} }) -> diff --git a/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl b/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl index 4d16f1692..a6b5ece89 100644 --- a/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl +++ b/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl @@ -600,9 +600,9 @@ t_with_redact_update(_Config) -> ok. operation_path(node, Oper, BridgeID) -> - uri(["nodes", node(), "bridges", BridgeID, "operation", Oper]); + uri(["nodes", node(), "bridges", BridgeID, Oper]); operation_path(cluster, Oper, BridgeID) -> - uri(["bridges", BridgeID, "operation", Oper]). + uri(["bridges", BridgeID, Oper]). str(S) when is_list(S) -> S; str(S) when is_binary(S) -> binary_to_list(S). From 860e21d40f1b32eb4e3ac03d93679fbac6dd9e2b Mon Sep 17 00:00:00 2001 From: Erik Timan Date: Thu, 12 Jan 2023 10:39:19 +0100 Subject: [PATCH 0135/1802] feat(emqx_bridge): move metrics to own endpoint, rename reset_metrics In order for the /bridges APIs to be consistent with other APIs, we move out metrics from GET /bridges/{id} to its own endpoint, /bridges/{id}/metrics. We also rename /bridges/reset_metrics to /bridges/metrics/reset. --- apps/emqx_bridge/i18n/emqx_bridge_api.conf | 10 ++ apps/emqx_bridge/src/emqx_bridge_api.erl | 78 +++++++++++---- .../src/schema/emqx_bridge_mqtt_schema.erl | 2 +- .../src/schema/emqx_bridge_schema.erl | 21 ++-- .../src/schema/emqx_bridge_webhook_schema.erl | 2 +- .../test/emqx_bridge_api_SUITE.erl | 96 ++++++++++++++++--- .../src/emqx_ee_bridge_gcp_pubsub.erl | 2 +- .../src/emqx_ee_bridge_hstreamdb.erl | 2 +- .../src/emqx_ee_bridge_influxdb.erl | 2 +- .../src/emqx_ee_bridge_kafka.erl | 2 +- .../src/emqx_ee_bridge_mongodb.erl | 6 +- .../src/emqx_ee_bridge_mysql.erl | 2 +- .../src/emqx_ee_bridge_pgsql.erl | 2 +- .../src/emqx_ee_bridge_redis.erl | 2 +- 14 files changed, 176 insertions(+), 53 deletions(-) diff --git a/apps/emqx_bridge/i18n/emqx_bridge_api.conf b/apps/emqx_bridge/i18n/emqx_bridge_api.conf index e8bb2403a..796cbb91c 100644 --- a/apps/emqx_bridge/i18n/emqx_bridge_api.conf +++ b/apps/emqx_bridge/i18n/emqx_bridge_api.conf @@ -134,4 +134,14 @@ NOTE:不允许在单节点上启用/禁用 Bridge""" } } + desc_bridge_metrics { + desc { + en: """Get bridge metrics by Id""" + zh: """""" + } + label: { + en: "Get Bridge Metrics" + zh: "" + } + } } diff --git a/apps/emqx_bridge/src/emqx_bridge_api.erl b/apps/emqx_bridge/src/emqx_bridge_api.erl index a71142bc5..b05e31b11 100644 --- a/apps/emqx_bridge/src/emqx_bridge_api.erl +++ b/apps/emqx_bridge/src/emqx_bridge_api.erl @@ -38,7 +38,8 @@ '/bridges/:id'/2, '/bridges/:id/:operation'/2, '/nodes/:node/bridges/:id/:operation'/2, - '/bridges/:id/reset_metrics'/2 + '/bridges/:id/metrics'/2, + '/bridges/:id/metrics/reset'/2 ]). -export([lookup_from_local_node/2]). @@ -68,7 +69,8 @@ paths() -> "/bridges/:id", "/bridges/:id/:operation", "/nodes/:node/bridges/:id/:operation", - "/bridges/:id/reset_metrics" + "/bridges/:id/metrics", + "/bridges/:id/metrics/reset" ]. error_schema(Code, Message) when is_atom(Code) -> @@ -132,19 +134,22 @@ param_path_id() -> } )}. -bridge_info_array_example(Method) -> - [Config || #{value := Config} <- maps:values(bridge_info_examples(Method))]. +bridge_info_array_example(Method, WithMetrics) -> + [Config || #{value := Config} <- maps:values(bridge_info_examples(Method, WithMetrics))]. bridge_info_examples(Method) -> + bridge_info_examples(Method, false). + +bridge_info_examples(Method, WithMetrics) -> maps:merge( #{ <<"webhook_example">> => #{ summary => <<"WebHook">>, - value => info_example(webhook, Method) + value => info_example(webhook, Method, WithMetrics) }, <<"mqtt_example">> => #{ summary => <<"MQTT Bridge">>, - value => info_example(mqtt, Method) + value => info_example(mqtt, Method, WithMetrics) } }, ee_bridge_examples(Method) @@ -157,24 +162,24 @@ ee_bridge_examples(Method) -> _:_ -> #{} end. -info_example(Type, Method) -> +info_example(Type, Method, WithMetrics) -> maps:merge( info_example_basic(Type), - method_example(Type, Method) + method_example(Type, Method, WithMetrics) ). -method_example(Type, Method) when Method == get; Method == post -> +method_example(Type, Method, WithMetrics) when Method == get; Method == post -> SType = atom_to_list(Type), SName = SType ++ "_example", TypeNameExam = #{ type => bin(SType), name => bin(SName) }, - maybe_with_metrics_example(TypeNameExam, Method); -method_example(_Type, put) -> + maybe_with_metrics_example(TypeNameExam, Method, WithMetrics); +method_example(_Type, put, _WithMetrics) -> #{}. -maybe_with_metrics_example(TypeNameExam, get) -> +maybe_with_metrics_example(TypeNameExam, get, true) -> TypeNameExam#{ metrics => ?EMPTY_METRICS, node_metrics => [ @@ -184,7 +189,7 @@ maybe_with_metrics_example(TypeNameExam, get) -> } ] }; -maybe_with_metrics_example(TypeNameExam, _) -> +maybe_with_metrics_example(TypeNameExam, _, _) -> TypeNameExam. info_example_basic(webhook) -> @@ -274,7 +279,7 @@ schema("/bridges") -> responses => #{ 200 => emqx_dashboard_swagger:schema_with_example( array(emqx_bridge_schema:get_response()), - bridge_info_array_example(get) + bridge_info_array_example(get, true) ) } }, @@ -334,9 +339,23 @@ schema("/bridges/:id") -> } } }; -schema("/bridges/:id/reset_metrics") -> +schema("/bridges/:id/metrics") -> #{ - 'operationId' => '/bridges/:id/reset_metrics', + 'operationId' => '/bridges/:id/metrics', + get => #{ + tags => [<<"bridges">>], + summary => <<"Get Bridge Metrics">>, + description => ?DESC("desc_bridge_metrics"), + parameters => [param_path_id()], + responses => #{ + 200 => emqx_bridge_schema:metrics_fields(), + 404 => error_schema('NOT_FOUND', "Bridge not found") + } + } + }; +schema("/bridges/:id/metrics/reset") -> + #{ + 'operationId' => '/bridges/:id/metrics/reset', put => #{ tags => [<<"bridges">>], summary => <<"Reset Bridge Metrics">>, @@ -455,7 +474,10 @@ schema("/nodes/:node/bridges/:id/:operation") -> end ). -'/bridges/:id/reset_metrics'(put, #{bindings := #{id := Id}}) -> +'/bridges/:id/metrics'(get, #{bindings := #{id := Id}}) -> + ?TRY_PARSE_ID(Id, lookup_from_all_nodes_metrics(BridgeType, BridgeName, 200)). + +'/bridges/:id/metrics/reset'(put, #{bindings := #{id := Id}}) -> ?TRY_PARSE_ID( Id, case @@ -469,10 +491,18 @@ schema("/nodes/:node/bridges/:id/:operation") -> ). lookup_from_all_nodes(BridgeType, BridgeName, SuccCode) -> + FormatFun = fun format_bridge_info_without_metrics/1, + do_lookup_from_all_nodes(BridgeType, BridgeName, SuccCode, FormatFun). + +lookup_from_all_nodes_metrics(BridgeType, BridgeName, SuccCode) -> + FormatFun = fun format_bridge_metrics/1, + do_lookup_from_all_nodes(BridgeType, BridgeName, SuccCode, FormatFun). + +do_lookup_from_all_nodes(BridgeType, BridgeName, SuccCode, FormatFun) -> Nodes = mria_mnesia:running_nodes(), case is_ok(emqx_bridge_proto_v1:lookup_from_all_nodes(Nodes, BridgeType, BridgeName)) of {ok, [{ok, _} | _] = Results} -> - {SuccCode, format_bridge_info([R || {ok, R} <- Results])}; + {SuccCode, FormatFun([R || {ok, R} <- Results])}; {ok, [{error, not_found} | _]} -> {404, error_msg('NOT_FOUND', <<"not_found">>)}; {error, ErrL} -> @@ -572,7 +602,7 @@ zip_bridges([BridgesFirstNode | _] = BridgesAllNodes) -> lists:foldl( fun(#{type := Type, name := Name}, Acc) -> Bridges = pick_bridges_by_id(Type, Name, BridgesAllNodes), - [format_bridge_info(Bridges) | Acc] + [format_bridge_info_with_metrics(Bridges) | Acc] end, [], BridgesFirstNode @@ -606,7 +636,7 @@ pick_bridges_by_id(Type, Name, BridgesAllNodes) -> BridgesAllNodes ). -format_bridge_info([FirstBridge | _] = Bridges) -> +format_bridge_info_with_metrics([FirstBridge | _] = Bridges) -> Res = maps:remove(node, FirstBridge), NodeStatus = collect_status(Bridges), NodeMetrics = collect_metrics(Bridges), @@ -617,6 +647,14 @@ format_bridge_info([FirstBridge | _] = Bridges) -> node_metrics => NodeMetrics }). +format_bridge_info_without_metrics(Bridges) -> + Res = format_bridge_info_with_metrics(Bridges), + maps:without([metrics, node_metrics], Res). + +format_bridge_metrics(Bridges) -> + Res = format_bridge_info_with_metrics(Bridges), + maps:with([metrics, node_metrics], Res). + collect_status(Bridges) -> [maps:with([node, status], B) || B <- Bridges]. diff --git a/apps/emqx_bridge/src/schema/emqx_bridge_mqtt_schema.erl b/apps/emqx_bridge/src/schema/emqx_bridge_mqtt_schema.erl index 4665a3bc5..5cd1693c7 100644 --- a/apps/emqx_bridge/src/schema/emqx_bridge_mqtt_schema.erl +++ b/apps/emqx_bridge/src/schema/emqx_bridge_mqtt_schema.erl @@ -51,7 +51,7 @@ fields("post") -> fields("put") -> fields("config"); fields("get") -> - emqx_bridge_schema:metrics_status_fields() ++ fields("config"). + emqx_bridge_schema:status_fields() ++ fields("config"). desc("config") -> ?DESC("config"); diff --git a/apps/emqx_bridge/src/schema/emqx_bridge_schema.erl b/apps/emqx_bridge/src/schema/emqx_bridge_schema.erl index 845c1ef90..09a99488e 100644 --- a/apps/emqx_bridge/src/schema/emqx_bridge_schema.erl +++ b/apps/emqx_bridge/src/schema/emqx_bridge_schema.erl @@ -30,7 +30,8 @@ -export([ common_bridge_fields/0, - metrics_status_fields/0 + status_fields/0, + metrics_fields/0 ]). %%====================================================================================== @@ -83,19 +84,23 @@ common_bridge_fields() -> )} ]. -metrics_status_fields() -> +status_fields() -> + [ + {"status", mk(status(), #{desc => ?DESC("desc_status")})}, + {"node_status", + mk( + hoconsc:array(ref(?MODULE, "node_status")), + #{desc => ?DESC("desc_node_status")} + )} + ]. + +metrics_fields() -> [ {"metrics", mk(ref(?MODULE, "metrics"), #{desc => ?DESC("desc_metrics")})}, {"node_metrics", mk( hoconsc:array(ref(?MODULE, "node_metrics")), #{desc => ?DESC("desc_node_metrics")} - )}, - {"status", mk(status(), #{desc => ?DESC("desc_status")})}, - {"node_status", - mk( - hoconsc:array(ref(?MODULE, "node_status")), - #{desc => ?DESC("desc_node_status")} )} ]. diff --git a/apps/emqx_bridge/src/schema/emqx_bridge_webhook_schema.erl b/apps/emqx_bridge/src/schema/emqx_bridge_webhook_schema.erl index 0495911e7..b495436a4 100644 --- a/apps/emqx_bridge/src/schema/emqx_bridge_webhook_schema.erl +++ b/apps/emqx_bridge/src/schema/emqx_bridge_webhook_schema.erl @@ -38,7 +38,7 @@ fields("post") -> fields("put") -> fields("config"); fields("get") -> - emqx_bridge_schema:metrics_status_fields() ++ fields("post"); + emqx_bridge_schema:status_fields() ++ fields("post"); fields("creation_opts") -> lists:filter( fun({K, _V}) -> diff --git a/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl b/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl index a6b5ece89..eb86b923f 100644 --- a/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl +++ b/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl @@ -187,8 +187,6 @@ t_http_crud_apis(Config) -> <<"enable">> := true, <<"status">> := _, <<"node_status">> := [_ | _], - <<"metrics">> := _, - <<"node_metrics">> := [_ | _], <<"url">> := URL1 } = jsx:decode(Bridge), @@ -225,8 +223,6 @@ t_http_crud_apis(Config) -> <<"enable">> := true, <<"status">> := _, <<"node_status">> := [_ | _], - <<"metrics">> := _, - <<"node_metrics">> := [_ | _], <<"url">> := URL2 }, jsx:decode(Bridge2) @@ -259,8 +255,6 @@ t_http_crud_apis(Config) -> <<"enable">> := true, <<"status">> := _, <<"node_status">> := [_ | _], - <<"metrics">> := _, - <<"node_metrics">> := [_ | _], <<"url">> := URL2 }, jsx:decode(Bridge3Str) @@ -456,8 +450,6 @@ do_start_stop_bridges(Type, Config) -> <<"enable">> := true, <<"status">> := <<"connected">>, <<"node_status">> := [_ | _], - <<"metrics">> := _, - <<"node_metrics">> := [_ | _], <<"url">> := URL1 } = jsx:decode(Bridge), BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE, Name), @@ -502,8 +494,6 @@ t_enable_disable_bridges(Config) -> <<"enable">> := true, <<"status">> := <<"connected">>, <<"node_status">> := [_ | _], - <<"metrics">> := _, - <<"node_metrics">> := [_ | _], <<"url">> := URL1 } = jsx:decode(Bridge), BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE, Name), @@ -555,12 +545,10 @@ t_reset_bridges(Config) -> <<"enable">> := true, <<"status">> := <<"connected">>, <<"node_status">> := [_ | _], - <<"metrics">> := _, - <<"node_metrics">> := [_ | _], <<"url">> := URL1 } = jsx:decode(Bridge), BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE, Name), - {ok, 200, <<"Reset success">>} = request(put, uri(["bridges", BridgeID, "reset_metrics"]), []), + {ok, 200, <<"Reset success">>} = request(put, uri(["bridges", BridgeID, "metrics/reset"]), []), %% delete the bridge {ok, 204, <<>>} = request(delete, uri(["bridges", BridgeID]), []), @@ -599,6 +587,88 @@ t_with_redact_update(_Config) -> ?assertEqual(Password, Value), ok. +t_metrics(Config) -> + Port = ?config(port, Config), + %% assert we there's no bridges at first + {ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []), + + %% then we add a webhook bridge, using POST + %% POST /bridges/ will create a bridge + URL1 = ?URL(Port, "path1"), + Name = ?BRIDGE_NAME, + {ok, 201, Bridge} = request( + post, + uri(["bridges"]), + ?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, Name) + ), + + %ct:pal("---bridge: ~p", [Bridge]), + #{ + <<"type">> := ?BRIDGE_TYPE, + <<"name">> := Name, + <<"enable">> := true, + <<"status">> := _, + <<"node_status">> := [_ | _], + <<"url">> := URL1 + } = jsx:decode(Bridge), + + BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE, Name), + + %% check for empty bridge metrics + {ok, 200, Bridge1Str} = request(get, uri(["bridges", BridgeID, "metrics"]), []), + ct:pal("HERE ~p", [jsx:decode(Bridge1Str)]), + ?assertMatch( + #{ + <<"metrics">> := #{<<"success">> := 0}, + <<"node_metrics">> := [_ | _] + }, + jsx:decode(Bridge1Str) + ), + + %% send an message to emqx and the message should be forwarded to the HTTP server + Body = <<"my msg">>, + emqx:publish(emqx_message:make(<<"emqx_webhook/1">>, Body)), + ?assert( + receive + {http_server, received, #{ + method := <<"POST">>, + path := <<"/path1">>, + body := Body + }} -> + true; + Msg -> + ct:pal("error: http got unexpected request: ~p", [Msg]), + false + after 100 -> + false + end + ), + + %% check for non-empty bridge metrics + {ok, 200, Bridge2Str} = request(get, uri(["bridges", BridgeID, "metrics"]), []), + ct:pal("HERE ~p", [jsx:decode(Bridge2Str)]), + ?assertMatch( + #{ + <<"metrics">> := #{<<"success">> := 1}, + <<"node_metrics">> := [_ | _] + }, + jsx:decode(Bridge2Str) + ), + + %% check for non-empty metrics when listing all bridges + {ok, 200, BridgesStr} = request(get, uri(["bridges"]), []), + ct:pal("HERE ~p", [jsx:decode(BridgesStr)]), + ?assertMatch( + [ + #{ + <<"metrics">> := #{<<"success">> := 1}, + <<"node_metrics">> := [_ | _] + } + ], + jsx:decode(BridgesStr) + ), + ok. + operation_path(node, Oper, BridgeID) -> uri(["nodes", node(), "bridges", BridgeID, Oper]); operation_path(cluster, Oper, BridgeID) -> diff --git a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_gcp_pubsub.erl b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_gcp_pubsub.erl index 760aba9e1..83fe31b49 100644 --- a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_gcp_pubsub.erl +++ b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_gcp_pubsub.erl @@ -124,7 +124,7 @@ fields(bridge_config) -> )} ]; fields("get") -> - emqx_bridge_schema:metrics_status_fields() ++ fields("post"); + emqx_bridge_schema:status_fields() ++ fields("post"); fields("post") -> [type_field(), name_field() | fields("config")]; fields("put") -> diff --git a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_hstreamdb.erl b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_hstreamdb.erl index 135087929..6e0c711b2 100644 --- a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_hstreamdb.erl +++ b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_hstreamdb.erl @@ -67,7 +67,7 @@ fields("post") -> fields("put") -> fields("config"); fields("get") -> - emqx_bridge_schema:metrics_status_fields() ++ fields("post"). + emqx_bridge_schema:status_fields() ++ fields("post"). field(connector) -> mk( diff --git a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_influxdb.erl b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_influxdb.erl index 62c8b6ab7..fece72e82 100644 --- a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_influxdb.erl +++ b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_influxdb.erl @@ -139,7 +139,7 @@ method_fileds(get, ConnectorType) -> influxdb_bridge_common_fields() ++ connector_fields(ConnectorType) ++ type_name_fields(ConnectorType) ++ - emqx_bridge_schema:metrics_status_fields(); + emqx_bridge_schema:status_fields(); method_fileds(put, ConnectorType) -> influxdb_bridge_common_fields() ++ connector_fields(ConnectorType). diff --git a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_kafka.erl b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_kafka.erl index 3f2f6a85f..9fae4f30a 100644 --- a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_kafka.erl +++ b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_kafka.erl @@ -67,7 +67,7 @@ fields("post") -> fields("put") -> fields("config"); fields("get") -> - emqx_bridge_schema:metrics_status_fields() ++ fields("post"); + emqx_bridge_schema:status_fields() ++ fields("post"); fields("config") -> [ {enable, mk(boolean(), #{desc => ?DESC("config_enable"), default => true})}, diff --git a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_mongodb.erl b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_mongodb.erl index bb4082681..eaf2b7da1 100644 --- a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_mongodb.erl +++ b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_mongodb.erl @@ -59,15 +59,15 @@ fields("put_sharded") -> fields("put_single") -> fields(mongodb_single); fields("get_rs") -> - emqx_bridge_schema:metrics_status_fields() ++ + emqx_bridge_schema:status_fields() ++ fields(mongodb_rs) ++ type_and_name_fields(mongodb_rs); fields("get_sharded") -> - emqx_bridge_schema:metrics_status_fields() ++ + emqx_bridge_schema:status_fields() ++ fields(mongodb_sharded) ++ type_and_name_fields(mongodb_sharded); fields("get_single") -> - emqx_bridge_schema:metrics_status_fields() ++ + emqx_bridge_schema:status_fields() ++ fields(mongodb_single) ++ type_and_name_fields(mongodb_single). diff --git a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_mysql.erl b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_mysql.erl index 114459149..71f8a8399 100644 --- a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_mysql.erl +++ b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_mysql.erl @@ -104,7 +104,7 @@ fields("post") -> fields("put") -> fields("config"); fields("get") -> - emqx_bridge_schema:metrics_status_fields() ++ fields("post"). + emqx_bridge_schema:status_fields() ++ fields("post"). desc("config") -> ?DESC("desc_config"); diff --git a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_pgsql.erl b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_pgsql.erl index be9fc9dc8..7e21d4dd7 100644 --- a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_pgsql.erl +++ b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_pgsql.erl @@ -106,7 +106,7 @@ fields("post") -> fields("put") -> fields("config"); fields("get") -> - emqx_bridge_schema:metrics_status_fields() ++ fields("post"). + emqx_bridge_schema:status_fields() ++ fields("post"). fields("post", Type) -> [type_field(Type), name_field() | fields("config")]. diff --git a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_redis.erl b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_redis.erl index 5c273e050..3a3963786 100644 --- a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_redis.erl +++ b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_redis.erl @@ -126,7 +126,7 @@ method_fileds(get, ConnectorType) -> redis_bridge_common_fields() ++ connector_fields(ConnectorType) ++ type_name_fields(ConnectorType) ++ - emqx_bridge_schema:metrics_status_fields(); + emqx_bridge_schema:status_fields(); method_fileds(put, ConnectorType) -> redis_bridge_common_fields() ++ connector_fields(ConnectorType). From 42f42de4d9fad1666d1b76d758623d3f4dbc2edf Mon Sep 17 00:00:00 2001 From: Erik Timan Date: Thu, 12 Jan 2023 19:32:38 +0100 Subject: [PATCH 0136/1802] feat(emqx_bridge): add separate endpoint for enable/disable of bridge In order to improve the consistency with other API endpoints, we move the enable/disable operations to a separate endpoint /bridges/{id}/enable/[true,false]. --- apps/emqx_bridge/i18n/emqx_bridge_api.conf | 35 +++++++-- apps/emqx_bridge/src/emqx_bridge_api.erl | 75 ++++++++++++++----- .../test/emqx_bridge_api_SUITE.erl | 13 ++-- 3 files changed, 93 insertions(+), 30 deletions(-) diff --git a/apps/emqx_bridge/i18n/emqx_bridge_api.conf b/apps/emqx_bridge/i18n/emqx_bridge_api.conf index 796cbb91c..8adda9355 100644 --- a/apps/emqx_bridge/i18n/emqx_bridge_api.conf +++ b/apps/emqx_bridge/i18n/emqx_bridge_api.conf @@ -2,8 +2,8 @@ emqx_bridge_api { desc_param_path_operation_cluster { desc { - en: """Operations can be one of: enable, disable, start, stop, restart""" - zh: """集群可用操作:启用、禁用、启动、停止、重新启动""" + en: """Operations can be one of: start, stop, restart""" + zh: """""" } label: { en: "Cluster Operation" @@ -44,6 +44,16 @@ emqx_bridge_api { } } + desc_param_path_enable { + desc { + en: """Whether or not the bridge is enabled""" + zh: """""" + } + label: { + en: "Enable bridge" + zh: "" + } + } desc_api1 { desc { en: """List all created bridges""" @@ -112,8 +122,8 @@ emqx_bridge_api { desc_api7 { desc { - en: """Enable/Disable/Stop/Restart bridges on all nodes in the cluster.""" - zh: """在集群中的所有节点上启用/禁用/停止/重新启动 Bridge。""" + en: """Stop/Restart bridges on all nodes in the cluster.""" + zh: """""" } label: { en: "Cluster Bridge Operate" @@ -123,10 +133,8 @@ emqx_bridge_api { desc_api8 { desc { - en: """Stop/Restart bridges on a specific node. - NOTE: It's not allowed to disable/enable bridges on a single node.""" - zh: """在某个节点上停止/重新启动 Bridge。 -NOTE:不允许在单节点上启用/禁用 Bridge""" + en: """Stop/Restart bridges on a specific node.""" + zh: """在某个节点上停止/重新启动 Bridge。""" } label: { en: "Node Bridge Operate" @@ -144,4 +152,15 @@ NOTE:不允许在单节点上启用/禁用 Bridge""" zh: "" } } + + desc_enable_bridge { + desc { + en: """Enable or Disable bridges on all nodes in the cluster.""" + zh: """""" + } + label: { + en: "Cluster Bridge Enable" + zh: "" + } + } } diff --git a/apps/emqx_bridge/src/emqx_bridge_api.erl b/apps/emqx_bridge/src/emqx_bridge_api.erl index b05e31b11..f3247206e 100644 --- a/apps/emqx_bridge/src/emqx_bridge_api.erl +++ b/apps/emqx_bridge/src/emqx_bridge_api.erl @@ -36,6 +36,7 @@ -export([ '/bridges'/2, '/bridges/:id'/2, + '/bridges/:id/enable/:enable'/2, '/bridges/:id/:operation'/2, '/nodes/:node/bridges/:id/:operation'/2, '/bridges/:id/metrics'/2, @@ -67,6 +68,7 @@ paths() -> [ "/bridges", "/bridges/:id", + "/bridges/:id/enable/:enable", "/bridges/:id/:operation", "/nodes/:node/bridges/:id/:operation", "/bridges/:id/metrics", @@ -89,7 +91,7 @@ get_response_body_schema() -> param_path_operation_cluster() -> {operation, mk( - enum([enable, disable, stop, restart]), + enum([stop, restart]), #{ in => path, required => true, @@ -134,6 +136,17 @@ param_path_id() -> } )}. +param_path_enable() -> + {enable, + mk( + boolean(), + #{ + in => path, + desc => ?DESC("desc_param_path_enable"), + example => true + } + )}. + bridge_info_array_example(Method, WithMetrics) -> [Config || #{value := Config} <- maps:values(bridge_info_examples(Method, WithMetrics))]. @@ -367,12 +380,29 @@ schema("/bridges/:id/metrics/reset") -> } } }; +schema("/bridges/:id/enable/:enable") -> + #{ + 'operationId' => '/bridges/:id/enable/:enable', + put => + #{ + tags => [<<"bridges">>], + summary => <<"Enable or Disable Bridge">>, + desc => ?DESC("desc_enable_bridge"), + parameters => [param_path_id(), param_path_enable()], + responses => + #{ + 204 => <<"Success">>, + 400 => error_schema('INVALID_ID', "Bad bridge ID"), + 503 => error_schema('SERVICE_UNAVAILABLE', "Service unavailable") + } + } + }; schema("/bridges/:id/:operation") -> #{ 'operationId' => '/bridges/:id/:operation', post => #{ tags => [<<"bridges">>], - summary => <<"Enable/Disable/Stop/Restart Bridge">>, + summary => <<"Stop or Restart Bridge">>, description => ?DESC("desc_api7"), parameters => [ param_path_id(), @@ -515,6 +545,28 @@ lookup_from_local_node(BridgeType, BridgeName) -> Error -> Error end. +'/bridges/:id/enable/:enable'(put, #{bindings := #{id := Id, enable := Enable}}) -> + ?TRY_PARSE_ID( + Id, + case enable_func(Enable) of + invalid -> + {400, error_msg('BAD_REQUEST', <<"invalid operation">>)}; + OperFunc -> + case emqx_bridge:disable_enable(OperFunc, BridgeType, BridgeName) of + {ok, _} -> + {204}; + {error, {pre_config_update, _, bridge_not_found}} -> + {404, error_msg('NOT_FOUND', <<"bridge not found">>)}; + {error, {_, _, timeout}} -> + {503, error_msg('SERVICE_UNAVAILABLE', <<"request timeout">>)}; + {error, timeout} -> + {503, error_msg('SERVICE_UNAVAILABLE', <<"request timeout">>)}; + {error, Reason} -> + {500, error_msg('INTERNAL_ERROR', Reason)} + end + end + ). + '/bridges/:id/:operation'(post, #{ bindings := #{id := Id, operation := Op} @@ -524,19 +576,6 @@ lookup_from_local_node(BridgeType, BridgeName) -> case operation_func(Op) of invalid -> {400, error_msg('BAD_REQUEST', <<"invalid operation">>)}; - OperFunc when OperFunc == enable; OperFunc == disable -> - case emqx_bridge:disable_enable(OperFunc, BridgeType, BridgeName) of - {ok, _} -> - {200}; - {error, {pre_config_update, _, bridge_not_found}} -> - {404, error_msg('NOT_FOUND', <<"bridge not found">>)}; - {error, {_, _, timeout}} -> - {503, error_msg('SERVICE_UNAVAILABLE', <<"request timeout">>)}; - {error, timeout} -> - {503, error_msg('SERVICE_UNAVAILABLE', <<"request timeout">>)}; - {error, Reason} -> - {500, error_msg('INTERNAL_ERROR', Reason)} - end; OperFunc -> Nodes = mria_mnesia:running_nodes(), operation_to_all_nodes(Nodes, OperFunc, BridgeType, BridgeName) @@ -573,10 +612,12 @@ node_operation_func(_) -> invalid. operation_func(<<"stop">>) -> stop; operation_func(<<"restart">>) -> restart; -operation_func(<<"enable">>) -> enable; -operation_func(<<"disable">>) -> disable; operation_func(_) -> invalid. +enable_func(<<"true">>) -> enable; +enable_func(<<"false">>) -> disable; +enable_func(_) -> invalid. + operation_to_all_nodes(Nodes, OperFunc, BridgeType, BridgeName) -> RpcFunc = case OperFunc of diff --git a/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl b/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl index eb86b923f..4650ea1ad 100644 --- a/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl +++ b/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl @@ -498,19 +498,19 @@ t_enable_disable_bridges(Config) -> } = jsx:decode(Bridge), BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE, Name), %% disable it - {ok, 200, <<>>} = request(post, operation_path(cluster, disable, BridgeID), <<"">>), + {ok, 204, <<>>} = request(put, enable_path(false, BridgeID), <<"">>), {ok, 200, Bridge2} = request(get, uri(["bridges", BridgeID]), []), ?assertMatch(#{<<"status">> := <<"stopped">>}, jsx:decode(Bridge2)), %% enable again - {ok, 200, <<>>} = request(post, operation_path(cluster, enable, BridgeID), <<"">>), + {ok, 204, <<>>} = request(put, enable_path(true, BridgeID), <<"">>), {ok, 200, Bridge3} = request(get, uri(["bridges", BridgeID]), []), ?assertMatch(#{<<"status">> := <<"connected">>}, jsx:decode(Bridge3)), %% enable an already started bridge - {ok, 200, <<>>} = request(post, operation_path(cluster, enable, BridgeID), <<"">>), + {ok, 204, <<>>} = request(put, enable_path(true, BridgeID), <<"">>), {ok, 200, Bridge3} = request(get, uri(["bridges", BridgeID]), []), ?assertMatch(#{<<"status">> := <<"connected">>}, jsx:decode(Bridge3)), %% disable it again - {ok, 200, <<>>} = request(post, operation_path(cluster, disable, BridgeID), <<"">>), + {ok, 204, <<>>} = request(put, enable_path(false, BridgeID), <<"">>), {ok, 403, Res} = request(post, operation_path(node, restart, BridgeID), <<"">>), ?assertEqual( @@ -519,7 +519,7 @@ t_enable_disable_bridges(Config) -> ), %% enable a stopped bridge - {ok, 200, <<>>} = request(post, operation_path(cluster, enable, BridgeID), <<"">>), + {ok, 204, <<>>} = request(put, enable_path(true, BridgeID), <<"">>), {ok, 200, Bridge4} = request(get, uri(["bridges", BridgeID]), []), ?assertMatch(#{<<"status">> := <<"connected">>}, jsx:decode(Bridge4)), %% delete the bridge @@ -674,5 +674,8 @@ operation_path(node, Oper, BridgeID) -> operation_path(cluster, Oper, BridgeID) -> uri(["bridges", BridgeID, Oper]). +enable_path(Enable, BridgeID) -> + uri(["bridges", BridgeID, "enable", Enable]). + str(S) when is_list(S) -> S; str(S) when is_binary(S) -> binary_to_list(S). From 522e091dd2f3a6b95dd7afb85f1cfda29c07b6e8 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Thu, 12 Jan 2023 14:52:27 +0100 Subject: [PATCH 0137/1802] docs: chane cache to buffer for Kafka bridge description --- lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_kafka.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_kafka.conf b/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_kafka.conf index 7ccf20d0b..02f979449 100644 --- a/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_kafka.conf +++ b/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_kafka.conf @@ -437,7 +437,7 @@ emqx_ee_bridge_kafka { buffer_memory_overload_protection { desc { en: "Applicable when buffer mode is set to memory or hybrid.\n" - "EMQX will drop old cached messages under high memory pressure. " + "EMQX will drop old buffered messages under high memory pressure. " "The high memory threshold is defined in config sysmon.os.sysmem_high_watermark. " "NOTE: This config only works on Linux." zh: "缓存模式是 memoryhybrid 时适用。" From c7ac55520d70265ac83ea854d77d5302c3d1f959 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Thu, 12 Jan 2023 14:53:01 +0100 Subject: [PATCH 0138/1802] docs: update pool_size doc --- apps/emqx_connector/i18n/emqx_connector_schema_lib.conf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/emqx_connector/i18n/emqx_connector_schema_lib.conf b/apps/emqx_connector/i18n/emqx_connector_schema_lib.conf index 8f25e0352..1bc45c36d 100644 --- a/apps/emqx_connector/i18n/emqx_connector_schema_lib.conf +++ b/apps/emqx_connector/i18n/emqx_connector_schema_lib.conf @@ -35,11 +35,11 @@ emqx_connector_schema_lib { pool_size { desc { - en: "Size of the connection pool." - zh: "连接池大小。" + en: "Size of the connection pool towards the bridge target service." + zh: "桥接远端服务时使用的连接池大小。" } label: { - en: "Pool Size" + en: "Connection Pool Size" zh: "连接池大小" } } From 153d64068a2c23d76ea3a97ee7a253b533f32d02 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Thu, 12 Jan 2023 14:53:29 +0100 Subject: [PATCH 0139/1802] refactor: rename root level cache type to authz_cache --- apps/emqx/src/emqx_schema.erl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 043b57b99..628edb858 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -328,7 +328,7 @@ fields("stats") -> ]; fields("authorization") -> authz_fields(); -fields("cache") -> +fields("authz_cache") -> [ {"enable", sc( @@ -1684,7 +1684,7 @@ desc("mqtt") -> "Global MQTT configuration.
" "The configs here work as default values which can be overridden\n" "in zone configs"; -desc("cache") -> +desc("authz_cache") -> "Settings for the authorization cache."; desc("zone") -> "A `Zone` defines a set of configuration items (such as the maximum number of connections)" @@ -2090,7 +2090,7 @@ authz_fields() -> )}, {"cache", sc( - ref(?MODULE, "cache"), + ref(?MODULE, "authz_cache"), #{} )} ]. From 2bc19c3a622db0470f93c57eb7fbc69ea65085dd Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Fri, 13 Jan 2023 08:21:09 +0100 Subject: [PATCH 0140/1802] fix(emqx_packet): log empty string if password value is missing to keep it a consistent behaviour comparing when logging username --- apps/emqx/src/emqx_packet.erl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/emqx/src/emqx_packet.erl b/apps/emqx/src/emqx_packet.erl index d82810d15..c247419f0 100644 --- a/apps/emqx/src/emqx_packet.erl +++ b/apps/emqx/src/emqx_packet.erl @@ -601,7 +601,8 @@ format_variable(#mqtt_packet_auth{reason_code = ReasonCode}, _) -> format_variable(PacketId, _) when is_integer(PacketId) -> io_lib:format("PacketId=~p", [PacketId]). -format_password(undefined) -> "undefined"; +format_password(undefined) -> ""; +format_password(<<>>) -> ""; format_password(_Password) -> "******". format_payload(Payload, text) -> ["Payload=", io_lib:format("~ts", [Payload])]; From eacdaffe3c06a6d23ab5e48db0ed8574095e9dd3 Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Fri, 13 Jan 2023 15:30:01 +0800 Subject: [PATCH 0141/1802] fix: default max_connections in raw config should be binary not atom --- apps/emqx/src/emqx_schema.erl | 2 +- .../test/emqx_mgmt_api_listeners_SUITE.erl | 23 ++++++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index ed7e0a016..9a380e4bc 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -1647,7 +1647,7 @@ base_listener(Bind) -> sc( hoconsc:union([infinity, pos_integer()]), #{ - default => infinity, + default => <<"infinity">>, desc => ?DESC(base_listener_max_connections) } )}, diff --git a/apps/emqx_management/test/emqx_mgmt_api_listeners_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_api_listeners_SUITE.erl index e5c47ac4d..b47e66189 100644 --- a/apps/emqx_management/test/emqx_mgmt_api_listeners_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_api_listeners_SUITE.erl @@ -32,6 +32,25 @@ end_per_suite(_) -> emqx_conf:remove([listeners, tcp, new1], #{override_to => local}), emqx_mgmt_api_test_util:end_suite([emqx_conf]). +t_max_connection_default(_Config) -> + emqx_mgmt_api_test_util:end_suite([emqx_conf]), + Etc = filename:join(["etc", "emqx.conf.all"]), + ConfFile = emqx_common_test_helpers:app_path(emqx_conf, Etc), + Bin = <<"listeners.tcp.max_connection_test {bind = \"0.0.0.0:3883\"}">>, + ok = file:write_file(ConfFile, Bin, [append]), + emqx_mgmt_api_test_util:init_suite([emqx_conf]), + %% Check infinity is binary not atom. + #{<<"listeners">> := Listeners} = emqx_mgmt_api_listeners:do_list_listeners(), + Target = lists:filter( + fun(#{<<"id">> := Id}) -> Id =:= 'tcp:max_connection_test' end, + Listeners + ), + ?assertMatch([#{<<"max_connections">> := <<"infinity">>}], Target), + NewPath = emqx_mgmt_api_test_util:api_path(["listeners", "tcp:max_connection_test"]), + ?assertMatch(#{<<"max_connections">> := <<"infinity">>}, request(get, NewPath, [], [])), + emqx_conf:remove([listeners, tcp, max_connection_test], #{override_to => cluster}), + ok. + t_list_listeners(_) -> Path = emqx_mgmt_api_test_util:api_path(["listeners"]), Res = request(get, Path, [], []), @@ -54,12 +73,14 @@ t_list_listeners(_) -> OriginListener2 = maps:remove(<<"id">>, OriginListener), NewConf = OriginListener2#{ <<"name">> => <<"new">>, - <<"bind">> => <<"0.0.0.0:2883">> + <<"bind">> => <<"0.0.0.0:2883">>, + <<"max_connections">> := <<"infinity">> }, Create = request(post, Path, [], NewConf), ?assertEqual(lists:sort(maps:keys(OriginListener)), lists:sort(maps:keys(Create))), Get1 = request(get, NewPath, [], []), ?assertMatch(Create, Get1), + ?assertMatch(#{<<"max_connections">> := <<"infinity">>}, Create), ?assert(is_running(NewListenerId)), %% delete From cbebfdbb13ef81c651074e58546dda4a72405c1f Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Fri, 13 Jan 2023 16:07:49 +0800 Subject: [PATCH 0142/1802] chore: changelog for #9748 --- changes/v5.0.15/fix-9748-en.md | 1 + changes/v5.0.15/fix-9748-zh.md | 1 + 2 files changed, 2 insertions(+) create mode 100644 changes/v5.0.15/fix-9748-en.md create mode 100644 changes/v5.0.15/fix-9748-zh.md diff --git a/changes/v5.0.15/fix-9748-en.md b/changes/v5.0.15/fix-9748-en.md new file mode 100644 index 000000000..85f5896b2 --- /dev/null +++ b/changes/v5.0.15/fix-9748-en.md @@ -0,0 +1 @@ +Listeners not configured with `max_connections` will cause the cluster `/listeners` API to return 500 error. diff --git a/changes/v5.0.15/fix-9748-zh.md b/changes/v5.0.15/fix-9748-zh.md new file mode 100644 index 000000000..cab352e79 --- /dev/null +++ b/changes/v5.0.15/fix-9748-zh.md @@ -0,0 +1 @@ +监听器不配置 `max_connections` 时会导致集群 `/listeners` 接口返回 500 错误。 From a9844b33031abf65108f79c7ff11d22bb198e59f Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Fri, 13 Jan 2023 10:04:29 +0100 Subject: [PATCH 0143/1802] chore: move changelog entries to v5.0.15 --- changes/v5.0.13-en.md | 4 ---- changes/v5.0.13-zh.md | 4 ---- changes/v5.0.15/feat-9569.en.md | 1 + changes/v5.0.15/feat-9569.zh.md | 1 + changes/v5.0.15/feat-9585.en.md | 1 + changes/v5.0.15/feat-9585.zh.md | 1 + 6 files changed, 4 insertions(+), 8 deletions(-) create mode 100644 changes/v5.0.15/feat-9569.en.md create mode 100644 changes/v5.0.15/feat-9569.zh.md create mode 100644 changes/v5.0.15/feat-9585.en.md create mode 100644 changes/v5.0.15/feat-9585.zh.md diff --git a/changes/v5.0.13-en.md b/changes/v5.0.13-en.md index 11c7705a4..1ed3c798b 100644 --- a/changes/v5.0.13-en.md +++ b/changes/v5.0.13-en.md @@ -18,10 +18,6 @@ - Ensure the default expiration time of `banned` is large enough [#9599](https://github.com/emqx/emqx/pull/9599/). -- `/bridges_probe` API endpoint to test params for creating a new data bridge [#9585](https://github.com/emqx/emqx/pull/9585). - -- Refactor `/authorization/sources/built_in_database/` by adding `rules/` to the path [#9569](https://github.com/emqx/emqx/pull/9569). - ## Bug fixes - Trigger `message.dropped` hook when QoS2 message is resend by client with a same packet id, or 'awaiting_rel' queue is full [#9487](https://github.com/emqx/emqx/pull/9487). diff --git a/changes/v5.0.13-zh.md b/changes/v5.0.13-zh.md index 8b556a6d8..41a1228f4 100644 --- a/changes/v5.0.13-zh.md +++ b/changes/v5.0.13-zh.md @@ -18,10 +18,6 @@ - 确保黑名单的默认超期时间足够长 [#9599](https://github.com/emqx/emqx/pull/9599/)。 -- 添加新 API 接口 `/bridges_probe` 用于测试创建桥接的参数是否可用 [#9585](https://github.com/emqx/emqx/pull/9585)。 - -- 重构 `/authorization/sources/built_in_database/` 接口,将 `rules/` 添加到了其路径中 [#9569](https://github.com/emqx/emqx/pull/9569)。 - ## 修复 - 当 QoS2 消息被重发(使用相同 Packet ID),或当 'awaiting_rel' 队列已满时,触发消息丢弃钩子(`message.dropped`)及计数器 [#9487](https://github.com/emqx/emqx/pull/9487)。 diff --git a/changes/v5.0.15/feat-9569.en.md b/changes/v5.0.15/feat-9569.en.md new file mode 100644 index 000000000..f3b70ec41 --- /dev/null +++ b/changes/v5.0.15/feat-9569.en.md @@ -0,0 +1 @@ +Refactor `/authorization/sources/built_in_database/` by adding `rules/` to the path. diff --git a/changes/v5.0.15/feat-9569.zh.md b/changes/v5.0.15/feat-9569.zh.md new file mode 100644 index 000000000..dd2e19c11 --- /dev/null +++ b/changes/v5.0.15/feat-9569.zh.md @@ -0,0 +1 @@ +重构 `/authorization/sources/built_in_database/` 接口,将 `rules/` 添加到了其路径中。 diff --git a/changes/v5.0.15/feat-9585.en.md b/changes/v5.0.15/feat-9585.en.md new file mode 100644 index 000000000..986cbb0c3 --- /dev/null +++ b/changes/v5.0.15/feat-9585.en.md @@ -0,0 +1 @@ +`/bridges_probe` API endpoint to test params for creating a new data bridge. diff --git a/changes/v5.0.15/feat-9585.zh.md b/changes/v5.0.15/feat-9585.zh.md new file mode 100644 index 000000000..82dd307ae --- /dev/null +++ b/changes/v5.0.15/feat-9585.zh.md @@ -0,0 +1 @@ +添加新 API 接口 `/bridges_probe` 用于测试创建桥接的参数是否可用。 From 59ac0b14247723fb4ffc0a99a2f5d70624b5f8b6 Mon Sep 17 00:00:00 2001 From: JimMoen Date: Tue, 3 Jan 2023 17:54:07 +0800 Subject: [PATCH 0144/1802] fix(mqtt-bridge): transmit raw msg payload with empty template --- .../src/mqtt/emqx_connector_mqtt_msg.erl | 17 +++++++++-------- changes/v5.0.14/fix-9672.en.md | 1 + changes/v5.0.14/fix-9672.zh.md | 1 + 3 files changed, 11 insertions(+), 8 deletions(-) create mode 100644 changes/v5.0.14/fix-9672.en.md create mode 100644 changes/v5.0.14/fix-9672.zh.md diff --git a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_msg.erl b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_msg.erl index bdd516db6..defbbaea2 100644 --- a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_msg.erl +++ b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_msg.erl @@ -71,14 +71,13 @@ to_remote_msg(#message{flags = Flags0} = Msg, Vars) -> to_remote_msg(MapMsg, #{ remote := #{ topic := TopicToken, - payload := PayloadToken, qos := QoSToken, retain := RetainToken - }, + } = Remote, mountpoint := Mountpoint }) when is_map(MapMsg) -> Topic = replace_vars_in_str(TopicToken, MapMsg), - Payload = process_payload(PayloadToken, MapMsg), + Payload = process_payload(Remote, MapMsg), QoS = replace_simple_var(QoSToken, MapMsg), Retain = replace_simple_var(RetainToken, MapMsg), PubProps = maps:get(pub_props, MapMsg, #{}), @@ -100,16 +99,15 @@ to_broker_msg( #{ local := #{ topic := TopicToken, - payload := PayloadToken, qos := QoSToken, retain := RetainToken - }, + } = Local, mountpoint := Mountpoint }, Props ) -> Topic = replace_vars_in_str(TopicToken, MapMsg), - Payload = process_payload(PayloadToken, MapMsg), + Payload = process_payload(Local, MapMsg), QoS = replace_simple_var(QoSToken, MapMsg), Retain = replace_simple_var(RetainToken, MapMsg), PubProps = maps:get(pub_props, MapMsg, #{}), @@ -121,9 +119,12 @@ to_broker_msg( ) ). -process_payload([], Msg) -> +process_payload(From, MapMsg) -> + do_process_payload(maps:get(payload, From, undefined), MapMsg). + +do_process_payload(undefined, Msg) -> emqx_json:encode(Msg); -process_payload(Tks, Msg) -> +do_process_payload(Tks, Msg) -> replace_vars_in_str(Tks, Msg). %% Replace a string contains vars to another string in which the placeholders are replace by the diff --git a/changes/v5.0.14/fix-9672.en.md b/changes/v5.0.14/fix-9672.en.md new file mode 100644 index 000000000..01724801e --- /dev/null +++ b/changes/v5.0.14/fix-9672.en.md @@ -0,0 +1 @@ +Fix the problem that the bridge is not available when the Payload template is empty in the MQTT bridge. diff --git a/changes/v5.0.14/fix-9672.zh.md b/changes/v5.0.14/fix-9672.zh.md new file mode 100644 index 000000000..86106cc1d --- /dev/null +++ b/changes/v5.0.14/fix-9672.zh.md @@ -0,0 +1 @@ +修复 MQTT 桥接中 Payload 模板为空时桥接不可用的问题。 From b3e62bd8f8c16c57754fa91bfffcc52f4ed32783 Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Fri, 13 Jan 2023 12:32:35 +0300 Subject: [PATCH 0145/1802] fix(paging): respect matchspec even if qs is empty when counting --- .../test/emqx_authz_api_mnesia_SUITE.erl | 27 +++++++++++++------ apps/emqx_management/src/emqx_mgmt_api.erl | 6 ++--- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/apps/emqx_authz/test/emqx_authz_api_mnesia_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_mnesia_SUITE.erl index 62bce770e..0d4d974f2 100644 --- a/apps/emqx_authz/test/emqx_authz_api_mnesia_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_api_mnesia_SUITE.erl @@ -109,14 +109,17 @@ t_api(_) -> ]), [] ), - #{ - <<"data">> := [], - <<"meta">> := #{ - <<"limit">> := 20, - <<"page">> := 1, - <<"hasnext">> := false - } - } = jsx:decode(Request1_1), + ?assertEqual( + #{ + <<"data">> => [], + <<"meta">> => #{ + <<"limit">> => 20, + <<"page">> => 1, + <<"hasnext">> => false + } + }, + jsx:decode(Request1_1) + ), {ok, 200, Request2} = request( @@ -160,6 +163,14 @@ t_api(_) -> [] ), + % ensure that db contain a mix of records + {ok, 204, _} = + request( + post, + uri(["authorization", "sources", "built_in_database", "username"]), + [?USERNAME_RULES_EXAMPLE] + ), + {ok, 204, _} = request( post, diff --git a/apps/emqx_management/src/emqx_mgmt_api.erl b/apps/emqx_management/src/emqx_mgmt_api.erl index ba232daf3..e46047521 100644 --- a/apps/emqx_management/src/emqx_mgmt_api.erl +++ b/apps/emqx_management/src/emqx_mgmt_api.erl @@ -384,8 +384,6 @@ apply_total_query(QueryState = #{table := Tab}) -> Fun(Tab) end. -counting_total_fun(_QueryState = #{qs := {[], []}}) -> - fun(Tab) -> ets:info(Tab, size) end; counting_total_fun(_QueryState = #{match_spec := Ms, fuzzy_fun := undefined}) -> %% XXX: Calculating the total number of data that match a certain %% condition under a large table is very expensive because the @@ -400,7 +398,9 @@ counting_total_fun(_QueryState = #{match_spec := Ms, fuzzy_fun := undefined}) -> counting_total_fun(_QueryState = #{fuzzy_fun := FuzzyFun}) when FuzzyFun =/= undefined -> %% XXX: Calculating the total number for a fuzzy searching is very very expensive %% so it is not supported now - false. + false; +counting_total_fun(_QueryState = #{qs := {[], []}}) -> + fun(Tab) -> ets:info(Tab, size) end. %% ResultAcc :: #{count := integer(), %% cursor := integer(), From 734e6b9c96c1366ff7bbbf898ec65571baec98fb Mon Sep 17 00:00:00 2001 From: Kjell Winblad Date: Fri, 13 Jan 2023 10:20:26 +0100 Subject: [PATCH 0146/1802] chore: fix flaky test cases, log labels and review comments Co-authored-by: Thales Macedo Garitezi --- apps/emqx_resource/src/emqx_resource_manager.erl | 16 ++++++---------- apps/emqx_resource/src/emqx_resource_worker.erl | 4 ++-- apps/emqx_resource/test/emqx_resource_SUITE.erl | 10 +++++++++- .../v5.0.14/{fix-8583.en.md => fix-9730.en.md} | 0 .../v5.0.14/{fix-8583.zh.md => fix-9730.zh.md} | 0 5 files changed, 17 insertions(+), 13 deletions(-) rename changes/v5.0.14/{fix-8583.en.md => fix-9730.en.md} (100%) rename changes/v5.0.14/{fix-8583.zh.md => fix-9730.zh.md} (100%) diff --git a/apps/emqx_resource/src/emqx_resource_manager.erl b/apps/emqx_resource/src/emqx_resource_manager.erl index 06e6288e3..553802b78 100644 --- a/apps/emqx_resource/src/emqx_resource_manager.erl +++ b/apps/emqx_resource/src/emqx_resource_manager.erl @@ -428,14 +428,6 @@ read_cache(ResId) -> [] -> not_found end. -read_manager_pid_from_cache(ResId) -> - case read_cache(ResId) of - not_found -> - erlang:error(badarg); - {_, #data{pid = ManagerPid}} -> - ManagerPid - end. - delete_cache(ResId, MgrId) -> case get_owner(ResId) of MgrIdNow when MgrIdNow == not_found; MgrIdNow == MgrId -> @@ -656,8 +648,12 @@ do_wait_for_ready(ResId, Retry) -> safe_call(ResId, Message, Timeout) -> try - ManagerPid = read_manager_pid_from_cache(ResId), - gen_statem:call(ManagerPid, Message, {clean_timeout, Timeout}) + case read_cache(ResId) of + not_found -> + {error, not_found}; + {_, #data{pid = ManagerPid}} -> + gen_statem:call(ManagerPid, Message, {clean_timeout, Timeout}) + end catch error:badarg -> {error, not_found}; diff --git a/apps/emqx_resource/src/emqx_resource_worker.erl b/apps/emqx_resource/src/emqx_resource_worker.erl index 264556bb5..7840fd474 100644 --- a/apps/emqx_resource/src/emqx_resource_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_worker.erl @@ -164,7 +164,7 @@ init({Id, Index, Opts}) -> emqx_resource_metrics:queuing_set(Id, Index, queue_count(Queue)), emqx_resource_metrics:inflight_set(Id, Index, 0), InfltWinSZ = maps:get(async_inflight_window, Opts, ?DEFAULT_INFLIGHT), - {ok, InflightTID} = inflight_new(InfltWinSZ, Id, Index), + InflightTID = inflight_new(InfltWinSZ, Id, Index), HCItvl = maps:get(health_check_interval, Opts, ?HEALTHCHECK_INTERVAL), St = #{ id => Id, @@ -765,7 +765,7 @@ inflight_new(InfltWinSZ, Id, Index) -> %% we use this counter because we might deal with batches as %% elements. inflight_append(TableId, ?SIZE_REF, 0, Id, Index), - {ok, TableId}. + TableId. inflight_get_first(InflightTID) -> case ets:next(InflightTID, ?MAX_SIZE_REF) of diff --git a/apps/emqx_resource/test/emqx_resource_SUITE.erl b/apps/emqx_resource/test/emqx_resource_SUITE.erl index 4bea0a1ee..cdec414c9 100644 --- a/apps/emqx_resource/test/emqx_resource_SUITE.erl +++ b/apps/emqx_resource/test/emqx_resource_SUITE.erl @@ -944,7 +944,15 @@ t_create_dry_run_local(_) -> end, lists:seq(1, 10) ), - [] = ets:match(emqx_resource_manager, {{owner, '$1'}, '_'}). + case [] =:= ets:match(emqx_resource_manager, {{owner, '$1'}, '_'}) of + false -> + %% Sleep to remove flakyness in test case. It take some time for + %% the ETS table to be cleared. + timer:sleep(2000), + [] = ets:match(emqx_resource_manager, {{owner, '$1'}, '_'}); + true -> + ok + end. create_dry_run_local_succ() -> case whereis(test_resource) of diff --git a/changes/v5.0.14/fix-8583.en.md b/changes/v5.0.14/fix-9730.en.md similarity index 100% rename from changes/v5.0.14/fix-8583.en.md rename to changes/v5.0.14/fix-9730.en.md diff --git a/changes/v5.0.14/fix-8583.zh.md b/changes/v5.0.14/fix-9730.zh.md similarity index 100% rename from changes/v5.0.14/fix-8583.zh.md rename to changes/v5.0.14/fix-9730.zh.md From b7259d9a20d3083e77cbd7633860fe8fea5ea281 Mon Sep 17 00:00:00 2001 From: JimMoen Date: Tue, 3 Jan 2023 18:11:29 +0800 Subject: [PATCH 0147/1802] test(mqtt-bridge): use empty payload template for ingress/egress mqtt bridge --- .../test/emqx_bridge_mqtt_SUITE.erl | 167 ++++++++++++++++++ 1 file changed, 167 insertions(+) diff --git a/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl b/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl index ae4fc4692..cc5e8a97a 100644 --- a/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl +++ b/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl @@ -75,6 +75,29 @@ } }). +-define(INGRESS_CONF_NO_PAYLOAD_TEMPLATE, #{ + <<"remote">> => #{ + <<"topic">> => <>, + <<"qos">> => 2 + }, + <<"local">> => #{ + <<"topic">> => <>, + <<"qos">> => <<"${qos}">>, + <<"retain">> => <<"${retain}">> + } +}). + +-define(EGRESS_CONF_NO_PAYLOAD_TEMPLATE, #{ + <<"local">> => #{ + <<"topic">> => <> + }, + <<"remote">> => #{ + <<"topic">> => <>, + <<"qos">> => <<"${qos}">>, + <<"retain">> => <<"${retain}">> + } +}). + inspect(Selected, _Envs, _Args) -> persistent_term:put(?MODULE, #{inspect => Selected}). @@ -209,6 +232,76 @@ t_mqtt_conn_bridge_ingress(_) -> ok. +t_mqtt_conn_bridge_ingress_no_payload_template(_) -> + User1 = <<"user1">>, + %% create an MQTT bridge, using POST + {ok, 201, Bridge} = request( + post, + uri(["bridges"]), + ?SERVER_CONF(User1)#{ + <<"type">> => ?TYPE_MQTT, + <<"name">> => ?BRIDGE_NAME_INGRESS, + <<"ingress">> => ?INGRESS_CONF_NO_PAYLOAD_TEMPLATE + } + ), + #{ + <<"type">> := ?TYPE_MQTT, + <<"name">> := ?BRIDGE_NAME_INGRESS + } = jsx:decode(Bridge), + BridgeIDIngress = emqx_bridge_resource:bridge_id(?TYPE_MQTT, ?BRIDGE_NAME_INGRESS), + + %% we now test if the bridge works as expected + RemoteTopic = <>, + LocalTopic = <>, + Payload = <<"hello">>, + emqx:subscribe(LocalTopic), + timer:sleep(100), + %% PUBLISH a message to the 'remote' broker, as we have only one broker, + %% the remote broker is also the local one. + emqx:publish(emqx_message:make(RemoteTopic, Payload)), + %% we should receive a message on the local broker, with specified topic + ?assert( + receive + {deliver, LocalTopic, #message{payload = MapMsg}} -> + ct:pal("local broker got message: ~p on topic ~p", [MapMsg, LocalTopic]), + %% the MapMsg is all fields outputed by Rule-Engine. it's a binary coded json here. + case jsx:decode(MapMsg) of + #{<<"payload">> := Payload} -> + true; + _ -> + false + end; + Msg -> + ct:pal("Msg: ~p", [Msg]), + false + after 100 -> + false + end + ), + + %% verify the metrics of the bridge + {ok, 200, BridgeStr} = request(get, uri(["bridges", BridgeIDIngress]), []), + ?assertMatch( + #{ + <<"metrics">> := #{<<"matched">> := 0, <<"received">> := 1}, + <<"node_metrics">> := + [ + #{ + <<"node">> := _, + <<"metrics">> := + #{<<"matched">> := 0, <<"received">> := 1} + } + ] + }, + jsx:decode(BridgeStr) + ), + + %% delete the bridge + {ok, 204, <<>>} = request(delete, uri(["bridges", BridgeIDIngress]), []), + {ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []), + + ok. + t_mqtt_conn_bridge_egress(_) -> %% then we add a mqtt connector, using POST User1 = <<"user1">>, @@ -276,6 +369,80 @@ t_mqtt_conn_bridge_egress(_) -> {ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []), ok. +t_mqtt_conn_bridge_egress_no_payload_template(_) -> + %% then we add a mqtt connector, using POST + User1 = <<"user1">>, + + {ok, 201, Bridge} = request( + post, + uri(["bridges"]), + ?SERVER_CONF(User1)#{ + <<"type">> => ?TYPE_MQTT, + <<"name">> => ?BRIDGE_NAME_EGRESS, + <<"egress">> => ?EGRESS_CONF_NO_PAYLOAD_TEMPLATE + } + ), + #{ + <<"type">> := ?TYPE_MQTT, + <<"name">> := ?BRIDGE_NAME_EGRESS + } = jsx:decode(Bridge), + BridgeIDEgress = emqx_bridge_resource:bridge_id(?TYPE_MQTT, ?BRIDGE_NAME_EGRESS), + ResourceID = emqx_bridge_resource:resource_id(?TYPE_MQTT, ?BRIDGE_NAME_EGRESS), + %% we now test if the bridge works as expected + LocalTopic = <>, + RemoteTopic = <>, + Payload = <<"hello">>, + emqx:subscribe(RemoteTopic), + timer:sleep(100), + %% PUBLISH a message to the 'local' broker, as we have only one broker, + %% the remote broker is also the local one. + emqx:publish(emqx_message:make(LocalTopic, Payload)), + + %% we should receive a message on the "remote" broker, with specified topic + ?assert( + receive + {deliver, RemoteTopic, #message{payload = MapMsg, from = From}} -> + ct:pal("local broker got message: ~p on topic ~p", [MapMsg, RemoteTopic]), + %% the MapMsg is all fields outputed by Rule-Engine. it's a binary coded json here. + Size = byte_size(ResourceID), + ?assertMatch(<>, From), + case jsx:decode(MapMsg) of + #{<<"payload">> := Payload} -> + true; + _ -> + false + end; + Msg -> + ct:pal("Msg: ~p", [Msg]), + false + after 100 -> + false + end + ), + + %% verify the metrics of the bridge + {ok, 200, BridgeStr} = request(get, uri(["bridges", BridgeIDEgress]), []), + ?assertMatch( + #{ + <<"metrics">> := #{<<"matched">> := 1, <<"success">> := 1, <<"failed">> := 0}, + <<"node_metrics">> := + [ + #{ + <<"node">> := _, + <<"metrics">> := + #{<<"matched">> := 1, <<"success">> := 1, <<"failed">> := 0} + } + ] + }, + jsx:decode(BridgeStr) + ), + + %% delete the bridge + {ok, 204, <<>>} = request(delete, uri(["bridges", BridgeIDEgress]), []), + {ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []), + + ok. + t_egress_custom_clientid_prefix(_Config) -> User1 = <<"user1">>, {ok, 201, Bridge} = request( From f15b29b1efb5edfaf0fd104809983a64af53bd38 Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Fri, 13 Jan 2023 18:22:29 +0800 Subject: [PATCH 0148/1802] chore: upgrade app version --- apps/emqx/src/emqx.app.src | 2 +- apps/emqx_authz/src/emqx_authz.app.src | 2 +- apps/emqx_bridge/src/emqx_bridge.app.src | 2 +- apps/emqx_conf/src/emqx_conf.app.src | 2 +- apps/emqx_connector/src/emqx_connector.app.src | 2 +- apps/emqx_dashboard/src/emqx_dashboard.app.src | 2 +- apps/emqx_gateway/src/emqx_gateway.app.src | 2 +- apps/emqx_management/src/emqx_management.app.src | 2 +- apps/emqx_rule_engine/src/emqx_rule_engine.app.src | 2 +- lib-ee/emqx_ee_bridge/src/emqx_ee_bridge.app.src | 2 +- lib-ee/emqx_ee_connector/src/emqx_ee_connector.app.src | 2 +- lib-ee/emqx_license/src/emqx_license.app.src | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/apps/emqx/src/emqx.app.src b/apps/emqx/src/emqx.app.src index bd7617e74..40d2796cd 100644 --- a/apps/emqx/src/emqx.app.src +++ b/apps/emqx/src/emqx.app.src @@ -3,7 +3,7 @@ {id, "emqx"}, {description, "EMQX Core"}, % strict semver, bump manually! - {vsn, "5.0.14"}, + {vsn, "5.0.15"}, {modules, []}, {registered, []}, {applications, [ diff --git a/apps/emqx_authz/src/emqx_authz.app.src b/apps/emqx_authz/src/emqx_authz.app.src index f5b9f9da6..6a4b721e9 100644 --- a/apps/emqx_authz/src/emqx_authz.app.src +++ b/apps/emqx_authz/src/emqx_authz.app.src @@ -1,7 +1,7 @@ %% -*- mode: erlang -*- {application, emqx_authz, [ {description, "An OTP application"}, - {vsn, "0.1.11"}, + {vsn, "0.1.12"}, {registered, []}, {mod, {emqx_authz_app, []}}, {applications, [ diff --git a/apps/emqx_bridge/src/emqx_bridge.app.src b/apps/emqx_bridge/src/emqx_bridge.app.src index 89fb7adaf..39cb1b18b 100644 --- a/apps/emqx_bridge/src/emqx_bridge.app.src +++ b/apps/emqx_bridge/src/emqx_bridge.app.src @@ -1,7 +1,7 @@ %% -*- mode: erlang -*- {application, emqx_bridge, [ {description, "EMQX bridges"}, - {vsn, "0.1.8"}, + {vsn, "0.1.9"}, {registered, []}, {mod, {emqx_bridge_app, []}}, {applications, [ diff --git a/apps/emqx_conf/src/emqx_conf.app.src b/apps/emqx_conf/src/emqx_conf.app.src index 3d18083f8..b13c0d055 100644 --- a/apps/emqx_conf/src/emqx_conf.app.src +++ b/apps/emqx_conf/src/emqx_conf.app.src @@ -1,6 +1,6 @@ {application, emqx_conf, [ {description, "EMQX configuration management"}, - {vsn, "0.1.9"}, + {vsn, "0.1.10"}, {registered, []}, {mod, {emqx_conf_app, []}}, {applications, [kernel, stdlib]}, diff --git a/apps/emqx_connector/src/emqx_connector.app.src b/apps/emqx_connector/src/emqx_connector.app.src index 65ef49c6b..9a82bda27 100644 --- a/apps/emqx_connector/src/emqx_connector.app.src +++ b/apps/emqx_connector/src/emqx_connector.app.src @@ -1,7 +1,7 @@ %% -*- mode: erlang -*- {application, emqx_connector, [ {description, "EMQX Data Integration Connectors"}, - {vsn, "0.1.11"}, + {vsn, "0.1.12"}, {registered, []}, {mod, {emqx_connector_app, []}}, {applications, [ diff --git a/apps/emqx_dashboard/src/emqx_dashboard.app.src b/apps/emqx_dashboard/src/emqx_dashboard.app.src index 2698d5534..57d63247b 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard.app.src +++ b/apps/emqx_dashboard/src/emqx_dashboard.app.src @@ -2,7 +2,7 @@ {application, emqx_dashboard, [ {description, "EMQX Web Dashboard"}, % strict semver, bump manually! - {vsn, "5.0.11"}, + {vsn, "5.0.12"}, {modules, []}, {registered, [emqx_dashboard_sup]}, {applications, [kernel, stdlib, mnesia, minirest, emqx]}, diff --git a/apps/emqx_gateway/src/emqx_gateway.app.src b/apps/emqx_gateway/src/emqx_gateway.app.src index 53403a67a..c5dd76f19 100644 --- a/apps/emqx_gateway/src/emqx_gateway.app.src +++ b/apps/emqx_gateway/src/emqx_gateway.app.src @@ -1,7 +1,7 @@ %% -*- mode: erlang -*- {application, emqx_gateway, [ {description, "The Gateway management application"}, - {vsn, "0.1.10"}, + {vsn, "0.1.11"}, {registered, []}, {mod, {emqx_gateway_app, []}}, {applications, [kernel, stdlib, grpc, emqx, emqx_authn]}, diff --git a/apps/emqx_management/src/emqx_management.app.src b/apps/emqx_management/src/emqx_management.app.src index c0cb05401..ccb53dac4 100644 --- a/apps/emqx_management/src/emqx_management.app.src +++ b/apps/emqx_management/src/emqx_management.app.src @@ -2,7 +2,7 @@ {application, emqx_management, [ {description, "EMQX Management API and CLI"}, % strict semver, bump manually! - {vsn, "5.0.11"}, + {vsn, "5.0.12"}, {modules, []}, {registered, [emqx_management_sup]}, {applications, [kernel, stdlib, emqx_plugins, minirest, emqx]}, diff --git a/apps/emqx_rule_engine/src/emqx_rule_engine.app.src b/apps/emqx_rule_engine/src/emqx_rule_engine.app.src index ea0bf6f9e..ee1544223 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_engine.app.src +++ b/apps/emqx_rule_engine/src/emqx_rule_engine.app.src @@ -2,7 +2,7 @@ {application, emqx_rule_engine, [ {description, "EMQX Rule Engine"}, % strict semver, bump manually! - {vsn, "5.0.7"}, + {vsn, "5.0.8"}, {modules, []}, {registered, [emqx_rule_engine_sup, emqx_rule_engine]}, {applications, [kernel, stdlib, rulesql, getopt]}, diff --git a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge.app.src b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge.app.src index 1563cb8ef..11831ab06 100644 --- a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge.app.src +++ b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge.app.src @@ -1,6 +1,6 @@ {application, emqx_ee_bridge, [ {description, "EMQX Enterprise data bridges"}, - {vsn, "0.1.2"}, + {vsn, "0.1.3"}, {registered, []}, {applications, [ kernel, diff --git a/lib-ee/emqx_ee_connector/src/emqx_ee_connector.app.src b/lib-ee/emqx_ee_connector/src/emqx_ee_connector.app.src index dfebd75f5..15cafa6a4 100644 --- a/lib-ee/emqx_ee_connector/src/emqx_ee_connector.app.src +++ b/lib-ee/emqx_ee_connector/src/emqx_ee_connector.app.src @@ -1,6 +1,6 @@ {application, emqx_ee_connector, [ {description, "EMQX Enterprise connectors"}, - {vsn, "0.1.2"}, + {vsn, "0.1.3"}, {registered, []}, {applications, [ kernel, diff --git a/lib-ee/emqx_license/src/emqx_license.app.src b/lib-ee/emqx_license/src/emqx_license.app.src index 2df1b3797..93ae665d5 100644 --- a/lib-ee/emqx_license/src/emqx_license.app.src +++ b/lib-ee/emqx_license/src/emqx_license.app.src @@ -1,6 +1,6 @@ {application, emqx_license, [ {description, "EMQX License"}, - {vsn, "5.0.4"}, + {vsn, "5.0.5"}, {modules, []}, {registered, [emqx_license_sup]}, {applications, [kernel, stdlib]}, From f8fb40add9c983868f1776a7d201e78fdad8863b Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Fri, 13 Jan 2023 18:28:07 +0800 Subject: [PATCH 0149/1802] chore: add line --- .github/workflows/build_and_push_docker_images.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build_and_push_docker_images.yaml b/.github/workflows/build_and_push_docker_images.yaml index 826a0f2ab..71515f699 100644 --- a/.github/workflows/build_and_push_docker_images.yaml +++ b/.github/workflows/build_and_push_docker_images.yaml @@ -166,6 +166,7 @@ jobs: username: ${{ secrets.AWS_ACCESS_KEY_ID }} password: ${{ secrets.AWS_SECRET_ACCESS_KEY }} ecr: true + - name: prepare for docker/metadata-action id: pre-meta shell: bash From f2b90e4485571667a14e6ab370a0f5e3d3177ae9 Mon Sep 17 00:00:00 2001 From: Kjell Winblad Date: Fri, 13 Jan 2023 11:38:28 +0100 Subject: [PATCH 0150/1802] docs: fix bad label of changelog items --- changes/v5.0.14/{feat-8329.en.md => feat-9691.en.md} | 0 changes/v5.0.14/{feat-8329.zh.md => feat-9691.zh.md} | 0 changes/v5.0.14/{fix-8648.en.md => fix-9678.en.md} | 0 changes/v5.0.14/{fix-8648.zh.md => fix-9678.zh.md} | 0 changes/v5.0.15/{fix-8718-en.md => fix-9735-en.md} | 0 changes/v5.0.15/{fix-8718-zh.md => fix-9735-zh.md} | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename changes/v5.0.14/{feat-8329.en.md => feat-9691.en.md} (100%) rename changes/v5.0.14/{feat-8329.zh.md => feat-9691.zh.md} (100%) rename changes/v5.0.14/{fix-8648.en.md => fix-9678.en.md} (100%) rename changes/v5.0.14/{fix-8648.zh.md => fix-9678.zh.md} (100%) rename changes/v5.0.15/{fix-8718-en.md => fix-9735-en.md} (100%) rename changes/v5.0.15/{fix-8718-zh.md => fix-9735-zh.md} (100%) diff --git a/changes/v5.0.14/feat-8329.en.md b/changes/v5.0.14/feat-9691.en.md similarity index 100% rename from changes/v5.0.14/feat-8329.en.md rename to changes/v5.0.14/feat-9691.en.md diff --git a/changes/v5.0.14/feat-8329.zh.md b/changes/v5.0.14/feat-9691.zh.md similarity index 100% rename from changes/v5.0.14/feat-8329.zh.md rename to changes/v5.0.14/feat-9691.zh.md diff --git a/changes/v5.0.14/fix-8648.en.md b/changes/v5.0.14/fix-9678.en.md similarity index 100% rename from changes/v5.0.14/fix-8648.en.md rename to changes/v5.0.14/fix-9678.en.md diff --git a/changes/v5.0.14/fix-8648.zh.md b/changes/v5.0.14/fix-9678.zh.md similarity index 100% rename from changes/v5.0.14/fix-8648.zh.md rename to changes/v5.0.14/fix-9678.zh.md diff --git a/changes/v5.0.15/fix-8718-en.md b/changes/v5.0.15/fix-9735-en.md similarity index 100% rename from changes/v5.0.15/fix-8718-en.md rename to changes/v5.0.15/fix-9735-en.md diff --git a/changes/v5.0.15/fix-8718-zh.md b/changes/v5.0.15/fix-9735-zh.md similarity index 100% rename from changes/v5.0.15/fix-8718-zh.md rename to changes/v5.0.15/fix-9735-zh.md From 0a965879dd2c12f86c2747d39388d00a10da55c4 Mon Sep 17 00:00:00 2001 From: firest Date: Fri, 13 Jan 2023 18:37:24 +0800 Subject: [PATCH 0151/1802] fix: fix that obsoleted cert file will not be deleted after the listener is updated/deleted --- apps/emqx/src/emqx_listeners.erl | 22 +++++- apps/emqx/test/data/certs/certfile | 24 +++++++ apps/emqx/test/data/certs/keyfile | 27 +++++++ apps/emqx/test/data/certs/keyfile2 | 27 +++++++ .../test/emqx_mgmt_api_listeners_SUITE.erl | 72 +++++++++++++++++++ 5 files changed, 170 insertions(+), 2 deletions(-) create mode 100644 apps/emqx/test/data/certs/certfile create mode 100644 apps/emqx/test/data/certs/keyfile create mode 100644 apps/emqx/test/data/certs/keyfile2 diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index fb6096e80..003c8785e 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -57,6 +57,10 @@ -export([format_bind/1]). +-ifdef(TEST). +-export([certs_dir/2]). +-endif. + -define(CONF_KEY_PATH, [listeners, '?', '?']). -define(TYPES_STRING, ["tcp", "ssl", "ws", "wss", "quic"]). @@ -415,6 +419,7 @@ pre_config_update(_Path, _Request, RawConf) -> post_config_update([listeners, Type, Name], {create, _Request}, NewConf, undefined, _AppEnvs) -> start_listener(Type, Name, NewConf); post_config_update([listeners, Type, Name], {update, _Request}, NewConf, OldConf, _AppEnvs) -> + try_clear_ssl_files(certs_dir(Type, Name), NewConf, OldConf), case NewConf of #{enabled := true} -> restart_listener(Type, Name, {OldConf, NewConf}); _ -> ok @@ -670,7 +675,7 @@ certs_dir(Type, Name) -> iolist_to_binary(filename:join(["listeners", Type, Name])). convert_certs(CertsDir, Conf) -> - case emqx_tls_lib:ensure_ssl_files(CertsDir, maps:get(<<"ssl_options">>, Conf, undefined)) of + case emqx_tls_lib:ensure_ssl_files(CertsDir, get_ssl_options(Conf)) of {ok, undefined} -> Conf; {ok, SSL} -> @@ -681,7 +686,7 @@ convert_certs(CertsDir, Conf) -> end. clear_certs(CertsDir, Conf) -> - OldSSL = maps:get(<<"ssl_options">>, Conf, undefined), + OldSSL = get_ssl_options(Conf), emqx_tls_lib:delete_ssl_files(CertsDir, undefined, OldSSL). filter_stacktrace({Reason, _Stacktrace}) -> Reason; @@ -692,3 +697,16 @@ ensure_override_limiter_conf(Conf, #{<<"limiter">> := Limiter}) -> Conf#{<<"limiter">> => Limiter}; ensure_override_limiter_conf(Conf, _) -> Conf. + +try_clear_ssl_files(CertsDir, NewConf, OldConf) -> + NewSSL = get_ssl_options(NewConf), + OldSSL = get_ssl_options(OldConf), + emqx_tls_lib:delete_ssl_files(CertsDir, NewSSL, OldSSL). + +get_ssl_options(Conf) -> + case maps:find(ssl_options, Conf) of + {ok, SSL} -> + SSL; + error -> + maps:get(<<"ssl_options">>, Conf, undefined) + end. diff --git a/apps/emqx/test/data/certs/certfile b/apps/emqx/test/data/certs/certfile new file mode 100644 index 000000000..a198faf61 --- /dev/null +++ b/apps/emqx/test/data/certs/certfile @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIID/jCCAeagAwIBAgIJAKTICmq1Lg6dMA0GCSqGSIb3DQEBCwUAMDQxEjAQBgNV +BAoMCUVNUVggVGVzdDEeMBwGA1UEAwwVQ2VydGlmaWNhdGUgQXV0aG9yaXR5MB4X +DTIxMTIzMDA4NDExMloXDTQ5MDUxNzA4NDExMlowJTESMBAGA1UECgwJRU1RWCBU +ZXN0MQ8wDQYDVQQDDAZjbGllbnQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQDzrujfx6XZTH0MWqLO6kNAeHndUZ+OGaURXvxKMPMF5dA40lxNG6cEzzlq +0Rm61adlv8tF4kRJrs6EnRjEVoMImrdh07vGFdOTYqP01LjiBhErAzyRtSn2X8FT +Te8ExoCRs3x61SPebGY2hOvFxuO6YDPVOSDvbbxvRgqIlM1ZXC8dOvPSSGZ+P8hV +56EPayRthfu1FVptnkW9CyZCRI0gg95Hv8RC7bGG+tuWpkN9ZrRvohhgGR1+bDUi +BNBpncEsSh+UgWaj8KRN8D16H6m/Im6ty467j0at49FvPx5nACL48/ghtYvzgKLc +uKHtokKUuuzebDK/hQxN3mUSAJStAgMBAAGjIjAgMAsGA1UdDwQEAwIFoDARBglg +hkgBhvhCAQEEBAMCB4AwDQYJKoZIhvcNAQELBQADggIBAIlVyPhOpkz3MNzQmjX7 +xgJ3vGPK5uK11n/wfjRwe2qXwZbrI2sYLVtTpUgvLDuP0gB73Vwfu7xAMdue6TRm +CKr9z0lkQsVBtgoqzZCjd4PYLfHm4EhsOMi98OGKU5uOGD4g3yLwQWXHhbYtiZMO +Jsj0hebYveYJt/BYTd1syGQcIcYCyVExWvSWjidfpAqjT6EF7whdubaFtuF2kaGF +IO9yn9rWtXB5yK99uCguEmKhx3fAQxomzqweTu3WRvy9axsUH3WAUW9a4DIBSz2+ +ZSJNheFn5GktgggygJUGYqpSZHooUJW0UBs/8vX6AP+8MtINmqOGZUawmNwLWLOq +wHyVt2YGD5TXjzzsWNSQ4mqXxM6AXniZVZK0yYNjA4ATikX1AtwunyWBR4IjyE/D +FxYPORdZCOtywRFE1R5KLTUq/C8BNGCkYnoO78DJBO+pT0oagkQGQb0CnmC6C1db +4lWzA9K0i4B0PyooZA+gp+5FFgaLuX1DkyeaY1J204QhHR1z/Vcyl5dpqR9hqnYP +t8raLk9ogMDKqKA9iG0wc3CBNckD4sjVWAEeovXhElG55fD21wwhF+AnDCvX8iVK +cBfKV6z6uxfKjGIxc2I643I5DiIn+V3DnPxYyY74Ln1lWFYmt5JREhAxPu42zq74 +e6+eIMYFszB+5gKgt6pa6ZNI +-----END CERTIFICATE----- diff --git a/apps/emqx/test/data/certs/keyfile b/apps/emqx/test/data/certs/keyfile new file mode 100644 index 000000000..2f0af5d41 --- /dev/null +++ b/apps/emqx/test/data/certs/keyfile @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA867o38el2Ux9DFqizupDQHh53VGfjhmlEV78SjDzBeXQONJc +TRunBM85atEZutWnZb/LReJESa7OhJ0YxFaDCJq3YdO7xhXTk2Kj9NS44gYRKwM8 +kbUp9l/BU03vBMaAkbN8etUj3mxmNoTrxcbjumAz1Tkg7228b0YKiJTNWVwvHTrz +0khmfj/IVeehD2skbYX7tRVabZ5FvQsmQkSNIIPeR7/EQu2xhvrblqZDfWa0b6IY +YBkdfmw1IgTQaZ3BLEoflIFmo/CkTfA9eh+pvyJurcuOu49GrePRbz8eZwAi+PP4 +IbWL84Ci3Lih7aJClLrs3mwyv4UMTd5lEgCUrQIDAQABAoIBAQDwEbBgznrIwn8r +jZt5x/brbAV7Ea/kOcWSgIaCvQifFdJ2OGAwov5/UXwajNgRZe2d4z7qoUhvYuUY +ZwCAZU6ASpRBr2v9cYFYYURvrqZaHmoJew3P6q/lhl6aqFvC06DUagRHqvXEafyk +13zEAvZVpfNKrBaTawPKiDFWb2qDDc9D6hC07EuJ/DNeehiHvzHrSZSDVV5Ut7Bw +YDm33XygheUPAlHfeCnaixzcs3osiVyFEmVjxcIaM0ZS1NgcSaohSpJHMzvEaohX +e+v9vccraSVlw01AlvFwI2vHYUV8jT6HwglTPKKGOCzK/ace3wPdYSU9qLcqfuHn +EFhNc3tNAoGBAPugLMgbReJg2gpbIPUkYyoMMAAU7llFU1WvPWwXzo1a9EBjBACw +WfCZISNtANXR38zIYXzoH547uXi4YPks1Nne3sYuCDpvuX+iz7fIo4zHf1nFmxH7 +eE6GtQr2ubmuuipTc28S0wBMGT1/KybH0e2NKL6GaOkNDmAI0IbEMBrvAoGBAPfr +Y1QYLhPhan6m5g/5s+bQpKtHfNH9TNkk13HuYu72zNuY3qL2GC7oSadR8vTbRXZg +KQqfaO0IGRcdkSFTq/AEhSSqr2Ld5nPadMbKvSGrSCc1s8rFH97jRVQY56yhM7ti +IW4+6cE8ylCMbdYB6wuduK/GIgNpqoF4xs1i2XojAoGACacBUMPLEH4Kny8TupOk +wi4pgTdMVVxVcAoC3yyincWJbRbfRm99Y79cCBHcYFdmsGJXawU0gUtlN/5KqgRQ +PfNQtGV7p1I12XGTakdmDrZwai8sXao52TlNpJgGU9siBRGicfZU5cQFi9he/WPY +57XshDJ/v8DidkigRysrdT0CgYEA5iuO22tblC+KvK1dGOXeZWO+DhrfwuGlcFBp +CaimB2/w/8vsn2VVTG9yujo2E6hj1CQw1mDrfG0xRim4LTXOgpbfugwRqvuTUmo2 +Ur21XEX2RhjwpEfhcACWxB4fMUG0krrniMA2K6axupi1/KNpQi6bYe3UdFCs8Wld +QSAOAvsCgYBk/X5PmD44DvndE5FShM2w70YOoMr3Cgl5sdwAFUFE9yDuC14UhVxk +oxnYxwtVI9uVVirET+LczP9JEvcvxnN/Xg3tH/qm0WlIxmTxyYrFFIK9j0rqeu9z +blPu56OzNI2VMrR1GbOBLxQINLTIpaacjNJAlr8XOlegdUJsW/Jwqw== +-----END RSA PRIVATE KEY----- diff --git a/apps/emqx/test/data/certs/keyfile2 b/apps/emqx/test/data/certs/keyfile2 new file mode 100644 index 000000000..2b3f30cf6 --- /dev/null +++ b/apps/emqx/test/data/certs/keyfile2 @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAzLiGiSwpxkENtjrzS7pNLblTnWe4HUUFwYyUX0H+3TnvA86X +EX85yZvFjkzB6lLjUkMY+C6UTVXt+mxeSJbUtSKZhX+2yoF/KYh7SaVjug5FqEqO +LvMpDZQEhUWF2W9DG6eUgOfDoX2milSDIe10yG2WBkryipHAfE7l1t+i6Rh3on+v +561LmrbqyBWR/cLp23RN3sHbkf2pb5/ugtU9twdgJr6Lve73rvSeulewL5BzszKD +BrYqr+PBT5+3ItCc55bTsO7M7CzOIL99BlqdvFH7xT0U1+2BFwLe4/8kwphSqyJE +C5oOiQBFnFVNXmFQSV+k7rPr80i1IO++HeJ6KQIDAQABAoIBAGWgvPjfuaU3qizq +uti/FY07USz0zkuJdkANH6LiSjlchzDmn8wJ0pApCjuIE0PV/g9aS8z4opp5q/gD +UBLM/a8mC/xf2EhTXOMrY7i9p/I3H5FZ4ZehEqIw9sWKK9YzC6dw26HabB2BGOnW +5nozPSQ6cp2RGzJ7BIkxSZwPzPnVTgy3OAuPOiJytvK+hGLhsNaT+Y9bNDvplVT2 +ZwYTV8GlHZC+4b2wNROILm0O86v96O+Qd8nn3fXjGHbMsAnONBq10bZS16L4fvkH +5G+W/1PeSXmtZFppdRRDxIW+DWcXK0D48WRliuxcV4eOOxI+a9N2ZJZZiNLQZGwg +w3A8+mECgYEA8HuJFrlRvdoBe2U/EwUtG74dcyy30L4yEBnN5QscXmEEikhaQCfX +Wm6EieMcIB/5I5TQmSw0cmBMeZjSXYoFdoI16/X6yMMuATdxpvhOZGdUGXxhAH+x +xoTUavWZnEqW3fkUU71kT5E2f2i+0zoatFESXHeslJyz85aAYpP92H0CgYEA2e5A +Yozt5eaA1Gyhd8SeptkEU4xPirNUnVQHStpMWUb1kzTNXrPmNWccQ7JpfpG6DcYl +zUF6p6mlzY+zkMiyPQjwEJlhiHM2NlL1QS7td0R8ewgsFoyn8WsBI4RejWrEG9td +EDniuIw+pBFkcWthnTLHwECHdzgquToyTMjrBB0CgYEA28tdGbrZXhcyAZEhHAZA +Gzog+pKlkpEzeonLKIuGKzCrEKRecIK5jrqyQsCjhS0T7ZRnL4g6i0s+umiV5M5w +fcc292pEA1h45L3DD6OlKplSQVTv55/OYS4oY3YEJtf5mfm8vWi9lQeY8sxOlQpn +O+VZTdBHmTC8PGeTAgZXHZUCgYA6Tyv88lYowB7SN2qQgBQu8jvdGtqhcs/99GCr +H3N0I69LPsKAR0QeH8OJPXBKhDUywESXAaEOwS5yrLNP1tMRz5Vj65YUCzeDG3kx +gpvY4IMp7ArX0bSRvJ6mYSFnVxy3k174G3TVCfksrtagHioVBGQ7xUg5ltafjrms +n8l55QKBgQDVzU8tQvBVqY8/1lnw11Vj4fkE/drZHJ5UkdC1eenOfSWhlSLfUJ8j +ds7vEWpRPPoVuPZYeR1y78cyxKe1GBx6Wa2lF5c7xjmiu0xbRnrxYeLolce9/ntp +asClqpnHT8/VJYTD7Kqj0fouTTZf0zkig/y+2XERppd8k+pSKjUCPQ== +-----END RSA PRIVATE KEY----- diff --git a/apps/emqx_management/test/emqx_mgmt_api_listeners_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_api_listeners_SUITE.erl index e5c47ac4d..08169206b 100644 --- a/apps/emqx_management/test/emqx_mgmt_api_listeners_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_api_listeners_SUITE.erl @@ -130,6 +130,60 @@ t_api_listeners_list_not_ready(_Config) -> emqx_common_test_helpers:stop_slave(Node2) end. +t_clear_certs(_) -> + ListenerId = <<"ssl:default">>, + NewListenerId = <<"ssl:clear">>, + + OriginPath = emqx_mgmt_api_test_util:api_path(["listeners", ListenerId]), + NewPath = emqx_mgmt_api_test_util:api_path(["listeners", NewListenerId]), + ConfTempT = request(get, OriginPath, [], []), + ConfTemp = ConfTempT#{ + <<"id">> => NewListenerId, + <<"bind">> => <<"0.0.0.0:2883">> + }, + + %% create, make sure the cert files are created + NewConf = emqx_map_lib:deep_put( + [<<"ssl_options">>, <<"certfile">>], ConfTemp, cert_file("certfile") + ), + NewConf2 = emqx_map_lib:deep_put( + [<<"ssl_options">>, <<"keyfile">>], NewConf, cert_file("keyfile") + ), + + _ = request(post, NewPath, [], NewConf2), + ListResult1 = list_pem_dir("ssl", "clear"), + ?assertMatch({ok, [_, _]}, ListResult1), + + %% update + UpdateConf = emqx_map_lib:deep_put( + [<<"ssl_options">>, <<"keyfile">>], NewConf2, cert_file("keyfile2") + ), + _ = request(put, NewPath, [], UpdateConf), + ListResult2 = list_pem_dir("ssl", "clear"), + + %% make sure the old cret file is deleted + ?assertMatch({ok, [_, _]}, ListResult2), + + {ok, ResultList1} = ListResult1, + {ok, ResultList2} = ListResult2, + + FindKeyFile = fun(List) -> + case lists:search(fun(E) -> lists:prefix("key", E) end, List) of + {value, Value} -> + Value; + _ -> + ?assert(false, "Can't find keyfile") + end + end, + + %% check the keyfile has changed + ?assertNotEqual(FindKeyFile(ResultList1), FindKeyFile(ResultList2)), + + %% remove, check all cert files are deleted + _ = delete(NewPath), + ?assertMatch({error, not_dir}, list_pem_dir("ssl", "clear")), + ok. + get_tcp_listeners(Node) -> Query = #{query_string => #{<<"type">> => tcp}}, {200, L} = rpc:call(Node, emqx_mgmt_api_listeners, list_listeners, [get, Query]), @@ -293,3 +347,21 @@ listener_stats(Listener, ExpectedStats) -> is_running(Id) -> emqx_listeners:is_running(binary_to_atom(Id)). + +list_pem_dir(Type, Name) -> + ListenerDir = emqx_listeners:certs_dir(Type, Name), + Dir = filename:join([emqx:mutable_certs_dir(), ListenerDir]), + case filelib:is_dir(Dir) of + true -> + file:list_dir(Dir); + _ -> + {error, not_dir} + end. + +data_file(Name) -> + Dir = code:lib_dir(emqx, test), + {ok, Bin} = file:read_file(filename:join([Dir, "data", Name])), + Bin. + +cert_file(Name) -> + data_file(filename:join(["certs", Name])). From f5ce10f742768f7ae6f483b2f13e2f508333c197 Mon Sep 17 00:00:00 2001 From: firest Date: Fri, 13 Jan 2023 18:45:04 +0800 Subject: [PATCH 0152/1802] chore: update changes --- changes/v5.0.15/fix-9751.en.md | 1 + changes/v5.0.15/fix-9751.zh.md | 1 + 2 files changed, 2 insertions(+) create mode 100644 changes/v5.0.15/fix-9751.en.md create mode 100644 changes/v5.0.15/fix-9751.zh.md diff --git a/changes/v5.0.15/fix-9751.en.md b/changes/v5.0.15/fix-9751.en.md new file mode 100644 index 000000000..f45b99129 --- /dev/null +++ b/changes/v5.0.15/fix-9751.en.md @@ -0,0 +1 @@ +Fix that obsoleted cert file will not be deleted after the listener is updated/deleted diff --git a/changes/v5.0.15/fix-9751.zh.md b/changes/v5.0.15/fix-9751.zh.md new file mode 100644 index 000000000..3908e5c20 --- /dev/null +++ b/changes/v5.0.15/fix-9751.zh.md @@ -0,0 +1 @@ +修复在更新或者删除监听器后,过时的证书文件没有被删除的问题。 From 8c68f8e50e8709bc5b07099d3b3229c37b3166e8 Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Fri, 13 Jan 2023 13:55:24 +0300 Subject: [PATCH 0153/1802] chore: add a changelog entry --- changes/v5.0.15/fix-9749-en.md | 1 + changes/v5.0.15/fix-9749-zh.md | 1 + 2 files changed, 2 insertions(+) create mode 100644 changes/v5.0.15/fix-9749-en.md create mode 100644 changes/v5.0.15/fix-9749-zh.md diff --git a/changes/v5.0.15/fix-9749-en.md b/changes/v5.0.15/fix-9749-en.md new file mode 100644 index 000000000..f079385ce --- /dev/null +++ b/changes/v5.0.15/fix-9749-en.md @@ -0,0 +1 @@ +In some cases search APIs could respond with an incorrect `count` value in the metadata, that is usually much bigger than expected, this is now fixed. diff --git a/changes/v5.0.15/fix-9749-zh.md b/changes/v5.0.15/fix-9749-zh.md new file mode 100644 index 000000000..356cf9475 --- /dev/null +++ b/changes/v5.0.15/fix-9749-zh.md @@ -0,0 +1 @@ +在某些情况下,搜索 API 可能会在元数据中响应不正确的 `count` 值,这通常比预期的要大得多,现在已经修复了。 From 0fd6865c41c8d66d5184c238eebf945b339b2c4f Mon Sep 17 00:00:00 2001 From: Erik Timan Date: Thu, 12 Jan 2023 19:51:25 +0100 Subject: [PATCH 0154/1802] chore: add changes --- changes/v5.0.15/feat-9736.en.md | 5 +++++ changes/v5.0.15/feat-9736.zh.md | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 changes/v5.0.15/feat-9736.en.md create mode 100644 changes/v5.0.15/feat-9736.zh.md diff --git a/changes/v5.0.15/feat-9736.en.md b/changes/v5.0.15/feat-9736.en.md new file mode 100644 index 000000000..59d7bd558 --- /dev/null +++ b/changes/v5.0.15/feat-9736.en.md @@ -0,0 +1,5 @@ +Refactor of /bridges API to make it more consistent with other APIs: +- bridge enable/disable is now done via the endpoint `/bridges/{id}/enable/[true,false]` +- `/bridges/{id}/operation/{operation}` endpoints are now `/bridges/{id}/{operation}` +- metrics are moved out from the GET `/bridges/{id}` response and can now be fetched via `/bridges/{id}/metrics` +- the `bridges/{id}/reset_metrics` endpoint is now `/bridges/{id}/metrics/reset` diff --git a/changes/v5.0.15/feat-9736.zh.md b/changes/v5.0.15/feat-9736.zh.md new file mode 100644 index 000000000..0107c8ab6 --- /dev/null +++ b/changes/v5.0.15/feat-9736.zh.md @@ -0,0 +1,5 @@ +重构部分 /bridges 的API 使得其和其他 API 能够更加一致: +- 桥接的启用和禁用现在是通过 `/bridges/{id}/enable/[true,false]` API 来实现的 +- 使用 `/bridges/{id}/{operation}` 替换了旧的 `/bridges/{id}/operation/{operation}` API +- 指标数据从 `/bridges/{id}` 的响应消息中移除,现在可以使用新的 API `/bridges/{id}/metrics` 进行访问 +- 使用 `/bridges/{id}/metrics/reset` 替换了旧的 `bridges/{id}/reset_metrics` API From f1c58c34ed124f597fdfbcf6d314233ba34a2262 Mon Sep 17 00:00:00 2001 From: Erik Timan Date: Fri, 13 Jan 2023 09:57:33 +0100 Subject: [PATCH 0155/1802] test(emqx_bridge): fix fetching of metrics in emqx_bridge_mqtt_SUITE --- .../test/emqx_bridge_mqtt_SUITE.erl | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl b/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl index ae4fc4692..3040789b3 100644 --- a/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl +++ b/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl @@ -187,7 +187,7 @@ t_mqtt_conn_bridge_ingress(_) -> ), %% verify the metrics of the bridge - {ok, 200, BridgeStr} = request(get, uri(["bridges", BridgeIDIngress]), []), + {ok, 200, BridgeMetricsStr} = request(get, uri(["bridges", BridgeIDIngress, "metrics"]), []), ?assertMatch( #{ <<"metrics">> := #{<<"matched">> := 0, <<"received">> := 1}, @@ -200,7 +200,7 @@ t_mqtt_conn_bridge_ingress(_) -> } ] }, - jsx:decode(BridgeStr) + jsx:decode(BridgeMetricsStr) ), %% delete the bridge @@ -255,7 +255,7 @@ t_mqtt_conn_bridge_egress(_) -> ), %% verify the metrics of the bridge - {ok, 200, BridgeStr} = request(get, uri(["bridges", BridgeIDEgress]), []), + {ok, 200, BridgeMetricsStr} = request(get, uri(["bridges", BridgeIDEgress, "metrics"]), []), ?assertMatch( #{ <<"metrics">> := #{<<"matched">> := 1, <<"success">> := 1, <<"failed">> := 0}, @@ -268,7 +268,7 @@ t_mqtt_conn_bridge_egress(_) -> } ] }, - jsx:decode(BridgeStr) + jsx:decode(BridgeMetricsStr) ), %% delete the bridge @@ -354,7 +354,7 @@ t_mqtt_conn_bridge_ingress_and_egress(_) -> Payload = <<"hello">>, emqx:subscribe(RemoteTopic), - {ok, 200, BridgeStr1} = request(get, uri(["bridges", BridgeIDEgress]), []), + {ok, 200, BridgeMetricsStr1} = request(get, uri(["bridges", BridgeIDEgress, "metrics"]), []), #{ <<"metrics">> := #{ <<"matched">> := CntMatched1, <<"success">> := CntSuccess1, <<"failed">> := 0 @@ -371,7 +371,7 @@ t_mqtt_conn_bridge_ingress_and_egress(_) -> } } ] - } = jsx:decode(BridgeStr1), + } = jsx:decode(BridgeMetricsStr1), timer:sleep(100), %% PUBLISH a message to the 'local' broker, as we have only one broker, %% the remote broker is also the local one. @@ -393,7 +393,7 @@ t_mqtt_conn_bridge_ingress_and_egress(_) -> %% verify the metrics of the bridge timer:sleep(1000), - {ok, 200, BridgeStr2} = request(get, uri(["bridges", BridgeIDEgress]), []), + {ok, 200, BridgeMetricsStr2} = request(get, uri(["bridges", BridgeIDEgress, "metrics"]), []), #{ <<"metrics">> := #{ <<"matched">> := CntMatched2, <<"success">> := CntSuccess2, <<"failed">> := 0 @@ -410,7 +410,7 @@ t_mqtt_conn_bridge_ingress_and_egress(_) -> } } ] - } = jsx:decode(BridgeStr2), + } = jsx:decode(BridgeMetricsStr2), ?assertEqual(CntMatched2, CntMatched1 + 1), ?assertEqual(CntSuccess2, CntSuccess1 + 1), ?assertEqual(NodeCntMatched2, NodeCntMatched1 + 1), @@ -513,7 +513,7 @@ t_ingress_mqtt_bridge_with_rules(_) -> ), %% verify the metrics of the bridge - {ok, 200, BridgeStr} = request(get, uri(["bridges", BridgeIDIngress]), []), + {ok, 200, BridgeMetricsStr} = request(get, uri(["bridges", BridgeIDIngress, "metrics"]), []), ?assertMatch( #{ <<"metrics">> := #{<<"matched">> := 0, <<"received">> := 1}, @@ -526,7 +526,7 @@ t_ingress_mqtt_bridge_with_rules(_) -> } ] }, - jsx:decode(BridgeStr) + jsx:decode(BridgeMetricsStr) ), {ok, 204, <<>>} = request(delete, uri(["rules", RuleId]), []), @@ -627,7 +627,7 @@ t_egress_mqtt_bridge_with_rules(_) -> ), %% verify the metrics of the bridge - {ok, 200, BridgeStr} = request(get, uri(["bridges", BridgeIDEgress]), []), + {ok, 200, BridgeMetricsStr} = request(get, uri(["bridges", BridgeIDEgress, "metrics"]), []), ?assertMatch( #{ <<"metrics">> := #{<<"matched">> := 2, <<"success">> := 2, <<"failed">> := 0}, @@ -641,7 +641,7 @@ t_egress_mqtt_bridge_with_rules(_) -> } ] }, - jsx:decode(BridgeStr) + jsx:decode(BridgeMetricsStr) ), {ok, 204, <<>>} = request(delete, uri(["rules", RuleId]), []), @@ -693,7 +693,7 @@ t_mqtt_conn_bridge_egress_reconnect(_) -> assert_mqtt_msg_received(RemoteTopic, Payload0), %% verify the metrics of the bridge - {ok, 200, BridgeStr} = request(get, uri(["bridges", BridgeIDEgress]), []), + {ok, 200, BridgeMetricsStr} = request(get, uri(["bridges", BridgeIDEgress, "metrics"]), []), ?assertMatch( #{ <<"metrics">> := #{<<"matched">> := 1, <<"success">> := 1, <<"failed">> := 0}, @@ -706,7 +706,7 @@ t_mqtt_conn_bridge_egress_reconnect(_) -> } ] }, - jsx:decode(BridgeStr) + jsx:decode(BridgeMetricsStr) ), %% stop the listener 1883 to make the bridge disconnected @@ -740,7 +740,9 @@ t_mqtt_conn_bridge_egress_reconnect(_) -> %% verify the metrics of the bridge, the message should be queued {ok, 200, BridgeStr1} = request(get, uri(["bridges", BridgeIDEgress]), []), + {ok, 200, BridgeMetricsStr1} = request(get, uri(["bridges", BridgeIDEgress, "metrics"]), []), Decoded1 = jsx:decode(BridgeStr1), + DecodedMetrics1 = jsx:decode(BridgeMetricsStr1), ?assertMatch( Status when (Status == <<"connected">> orelse Status == <<"connecting">>), maps:get(<<"status">>, Decoded1) @@ -753,7 +755,7 @@ t_mqtt_conn_bridge_egress_reconnect(_) -> <<"failed">> := 0, <<"queuing">> := 2 } when Matched >= 3, - maps:get(<<"metrics">>, Decoded1) + maps:get(<<"metrics">>, DecodedMetrics1) ), %% start the listener 1883 to make the bridge reconnected @@ -761,10 +763,12 @@ t_mqtt_conn_bridge_egress_reconnect(_) -> timer:sleep(1500), %% verify the metrics of the bridge, the 2 queued messages should have been sent {ok, 200, BridgeStr2} = request(get, uri(["bridges", BridgeIDEgress]), []), + {ok, 200, BridgeMetricsStr2} = request(get, uri(["bridges", BridgeIDEgress, "metrics"]), []), + Decoded2 = jsx:decode(BridgeStr2), + ?assertEqual(<<"connected">>, maps:get(<<"status">>, Decoded2)), %% matched >= 3 because of possible retries. ?assertMatch( #{ - <<"status">> := <<"connected">>, <<"metrics">> := #{ <<"matched">> := Matched, <<"success">> := 3, @@ -773,7 +777,7 @@ t_mqtt_conn_bridge_egress_reconnect(_) -> <<"retried">> := _ } } when Matched >= 3, - jsx:decode(BridgeStr2) + jsx:decode(BridgeMetricsStr2) ), %% also verify the 2 messages have been sent to the remote broker assert_mqtt_msg_received(RemoteTopic, Payload1), From 8dd52e5a6e92ce46790f45ca713c4b698a414671 Mon Sep 17 00:00:00 2001 From: Erik Timan Date: Fri, 13 Jan 2023 10:16:27 +0100 Subject: [PATCH 0156/1802] chore: add translations to schemas --- apps/emqx_bridge/i18n/emqx_bridge_api.conf | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/emqx_bridge/i18n/emqx_bridge_api.conf b/apps/emqx_bridge/i18n/emqx_bridge_api.conf index 8adda9355..9c9200aeb 100644 --- a/apps/emqx_bridge/i18n/emqx_bridge_api.conf +++ b/apps/emqx_bridge/i18n/emqx_bridge_api.conf @@ -3,7 +3,7 @@ emqx_bridge_api { desc_param_path_operation_cluster { desc { en: """Operations can be one of: start, stop, restart""" - zh: """""" + zh: """集群可用操作:启用、启动、重新启动""" } label: { en: "Cluster Operation" @@ -47,11 +47,11 @@ emqx_bridge_api { desc_param_path_enable { desc { en: """Whether or not the bridge is enabled""" - zh: """""" + zh: """是否启用桥接""" } label: { en: "Enable bridge" - zh: "" + zh: "启用桥接" } } desc_api1 { @@ -123,7 +123,7 @@ emqx_bridge_api { desc_api7 { desc { en: """Stop/Restart bridges on all nodes in the cluster.""" - zh: """""" + zh: """停止或启用所有节点上的桥接""" } label: { en: "Cluster Bridge Operate" @@ -145,22 +145,22 @@ emqx_bridge_api { desc_bridge_metrics { desc { en: """Get bridge metrics by Id""" - zh: """""" + zh: """通过 Id 来获取桥接的指标信息""" } label: { en: "Get Bridge Metrics" - zh: "" + zh: "获取桥接的指标" } } desc_enable_bridge { desc { en: """Enable or Disable bridges on all nodes in the cluster.""" - zh: """""" + zh: """启用或禁用所有节点上的桥接""" } label: { en: "Cluster Bridge Enable" - zh: "" + zh: "是否启用集群内的桥接" } } } From 7a17fb7308d80498af53926f9ee118b9ce89fa23 Mon Sep 17 00:00:00 2001 From: Erik Timan Date: Fri, 13 Jan 2023 13:50:37 +0100 Subject: [PATCH 0157/1802] test(emqx_ee_bridge): fix bridge enable/disable in kafka producer suite --- .../emqx_bridge_impl_kafka_producer_SUITE.erl | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/lib-ee/emqx_ee_bridge/test/emqx_bridge_impl_kafka_producer_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_bridge_impl_kafka_producer_SUITE.erl index bdde21c76..8e5b1fa95 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_bridge_impl_kafka_producer_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_bridge_impl_kafka_producer_SUITE.erl @@ -220,9 +220,10 @@ kafka_bridge_rest_api_helper(Config) -> BridgeIdUrlEnc = BridgeType ++ UrlEscColon ++ BridgeName, BridgesParts = ["bridges"], BridgesPartsIdDeleteAlsoActions = ["bridges", BridgeIdUrlEnc ++ "?also_delete_dep_actions"], - OpUrlFun = fun(OpName) -> ["bridges", BridgeIdUrlEnc, "operation", OpName] end, - BridgesPartsOpDisable = OpUrlFun("disable"), - BridgesPartsOpEnable = OpUrlFun("enable"), + OpUrlFun = fun(OpName) -> ["bridges", BridgeIdUrlEnc, OpName] end, + EnableFun = fun(Enable) -> ["bridges", BridgeIdUrlEnc, "enable", Enable] end, + BridgesPartsOpDisable = EnableFun("false"), + BridgesPartsOpEnable = EnableFun("true"), BridgesPartsOpRestart = OpUrlFun("restart"), BridgesPartsOpStop = OpUrlFun("stop"), %% List bridges @@ -321,10 +322,10 @@ kafka_bridge_rest_api_helper(Config) -> ?assertEqual(0, emqx_resource_metrics:retried_failed_get(ResourceId)), ?assertEqual(0, emqx_resource_metrics:retried_success_get(ResourceId)), %% Perform operations - {ok, 200, _} = show(http_post(show(BridgesPartsOpDisable), #{})), - {ok, 200, _} = show(http_post(show(BridgesPartsOpDisable), #{})), - {ok, 200, _} = show(http_post(show(BridgesPartsOpEnable), #{})), - {ok, 200, _} = show(http_post(show(BridgesPartsOpEnable), #{})), + {ok, 204, _} = show(http_put(show(BridgesPartsOpDisable), #{})), + {ok, 204, _} = show(http_put(show(BridgesPartsOpDisable), #{})), + {ok, 204, _} = show(http_put(show(BridgesPartsOpEnable), #{})), + {ok, 204, _} = show(http_put(show(BridgesPartsOpEnable), #{})), {ok, 200, _} = show(http_post(show(BridgesPartsOpStop), #{})), {ok, 200, _} = show(http_post(show(BridgesPartsOpStop), #{})), {ok, 200, _} = show(http_post(show(BridgesPartsOpRestart), #{})), From c3133fb6a22d50bf569946f7e3790b80af697f37 Mon Sep 17 00:00:00 2001 From: Erik Timan Date: Fri, 13 Jan 2023 14:49:41 +0100 Subject: [PATCH 0158/1802] fix(emqx_bridge): small fixes from review --- apps/emqx_bridge/i18n/emqx_bridge_api.conf | 4 ++-- apps/emqx_bridge/src/emqx_bridge_api.erl | 3 ++- apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl | 13 ++++++++----- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/apps/emqx_bridge/i18n/emqx_bridge_api.conf b/apps/emqx_bridge/i18n/emqx_bridge_api.conf index 9c9200aeb..a5593f1cf 100644 --- a/apps/emqx_bridge/i18n/emqx_bridge_api.conf +++ b/apps/emqx_bridge/i18n/emqx_bridge_api.conf @@ -2,8 +2,8 @@ emqx_bridge_api { desc_param_path_operation_cluster { desc { - en: """Operations can be one of: start, stop, restart""" - zh: """集群可用操作:启用、启动、重新启动""" + en: """Operations can be one of: stop, restart""" + zh: """集群可用操作:停止、重新启动""" } label: { en: "Cluster Operation" diff --git a/apps/emqx_bridge/src/emqx_bridge_api.erl b/apps/emqx_bridge/src/emqx_bridge_api.erl index f3247206e..3af911d3d 100644 --- a/apps/emqx_bridge/src/emqx_bridge_api.erl +++ b/apps/emqx_bridge/src/emqx_bridge_api.erl @@ -107,7 +107,7 @@ param_path_operation_on_node() -> #{ in => path, required => true, - example => <<"start">>, + example => <<"stop">>, desc => ?DESC("desc_param_path_operation_on_node") } )}. @@ -142,6 +142,7 @@ param_path_enable() -> boolean(), #{ in => path, + required => true, desc => ?DESC("desc_param_path_enable"), example => true } diff --git a/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl b/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl index 4650ea1ad..82523a839 100644 --- a/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl +++ b/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl @@ -616,7 +616,6 @@ t_metrics(Config) -> %% check for empty bridge metrics {ok, 200, Bridge1Str} = request(get, uri(["bridges", BridgeID, "metrics"]), []), - ct:pal("HERE ~p", [jsx:decode(Bridge1Str)]), ?assertMatch( #{ <<"metrics">> := #{<<"success">> := 0}, @@ -625,6 +624,12 @@ t_metrics(Config) -> jsx:decode(Bridge1Str) ), + %% check that the bridge doesn't contain metrics anymore + {ok, 200, Bridge2Str} = request(get, uri(["bridges", BridgeID]), []), + Decoded = jsx:decode(Bridge2Str), + ?assertNot(maps:is_key(<<"metrics">>, Decoded)), + ?assertNot(maps:is_key(<<"node_metrics">>, Decoded)), + %% send an message to emqx and the message should be forwarded to the HTTP server Body = <<"my msg">>, emqx:publish(emqx_message:make(<<"emqx_webhook/1">>, Body)), @@ -645,19 +650,17 @@ t_metrics(Config) -> ), %% check for non-empty bridge metrics - {ok, 200, Bridge2Str} = request(get, uri(["bridges", BridgeID, "metrics"]), []), - ct:pal("HERE ~p", [jsx:decode(Bridge2Str)]), + {ok, 200, Bridge3Str} = request(get, uri(["bridges", BridgeID, "metrics"]), []), ?assertMatch( #{ <<"metrics">> := #{<<"success">> := 1}, <<"node_metrics">> := [_ | _] }, - jsx:decode(Bridge2Str) + jsx:decode(Bridge3Str) ), %% check for non-empty metrics when listing all bridges {ok, 200, BridgesStr} = request(get, uri(["bridges"]), []), - ct:pal("HERE ~p", [jsx:decode(BridgesStr)]), ?assertMatch( [ #{ From 61e98900be2841f03152ddc29416271bb77e314a Mon Sep 17 00:00:00 2001 From: Erik Timan Date: Fri, 13 Jan 2023 15:13:35 +0100 Subject: [PATCH 0159/1802] chore: bump app vsn of emqx_resource --- apps/emqx_resource/src/emqx_resource.app.src | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx_resource/src/emqx_resource.app.src b/apps/emqx_resource/src/emqx_resource.app.src index 00389261b..e3a37fd10 100644 --- a/apps/emqx_resource/src/emqx_resource.app.src +++ b/apps/emqx_resource/src/emqx_resource.app.src @@ -1,7 +1,7 @@ %% -*- mode: erlang -*- {application, emqx_resource, [ {description, "Manager for all external resources"}, - {vsn, "0.1.4"}, + {vsn, "0.1.5"}, {registered, []}, {mod, {emqx_resource_app, []}}, {applications, [ From 8f5881d1a53cdcdd1a5de26d62c3075f8d001851 Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Fri, 13 Jan 2023 16:19:35 +0100 Subject: [PATCH 0160/1802] fix: remove stale request/3 from merge error --- apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl | 3 --- 1 file changed, 3 deletions(-) diff --git a/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl b/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl index 981e2f547..4116a4754 100644 --- a/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl +++ b/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl @@ -652,9 +652,6 @@ t_bridges_probe(Config) -> ?assertMatch(#{<<"code">> := <<"BAD_REQUEST">>}, jsx:decode(BadReq)), ok. -request(Method, Url, Body) -> - request(<<"bridge_admin">>, Method, Url, Body). - t_metrics(Config) -> Port = ?config(port, Config), %% assert we there's no bridges at first From 91d38b410ded10246ece940f14646383c31f6d1a Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Wed, 16 Nov 2022 09:55:56 +0800 Subject: [PATCH 0161/1802] fix: refresh logger handler when starting --- apps/emqx/src/config/emqx_config_logger.erl | 12 +++++++++++- apps/emqx/src/emqx_config.erl | 1 + 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/apps/emqx/src/config/emqx_config_logger.erl b/apps/emqx/src/config/emqx_config_logger.erl index babf9c431..3d974d0c6 100644 --- a/apps/emqx/src/config/emqx_config_logger.erl +++ b/apps/emqx/src/config/emqx_config_logger.erl @@ -18,7 +18,7 @@ -behaviour(emqx_config_handler). %% API --export([add_handler/0, remove_handler/0]). +-export([add_handler/0, remove_handler/0, refresh_config/0]). -export([post_config_update/5]). -define(LOG, [log]). @@ -31,6 +31,16 @@ remove_handler() -> ok = emqx_config_handler:remove_handler(?LOG), ok. +%% refresh logger config when booting, the override config may have changed after node start. +%% Kernel's app env is confirmed before the node starts, +%% but we only copy cluster-override.conf from other node after this node starts, +%% so we need to refresh the logger config after this node starts. +%% It will not affect the logger config when cluster-override.conf is unchanged. +refresh_config() -> + Log = emqx:get_raw_config(?LOG), + {ok, _} = emqx:update_config(?LOG, Log), + ok. + post_config_update(?LOG, _Req, _NewConf, _OldConf, AppEnvs) -> Kernel = proplists:get_value(kernel, AppEnvs), NewHandlers = proplists:get_value(logger, Kernel, []), diff --git a/apps/emqx/src/emqx_config.erl b/apps/emqx/src/emqx_config.erl index 2fa39d094..51de1d0d0 100644 --- a/apps/emqx/src/emqx_config.erl +++ b/apps/emqx/src/emqx_config.erl @@ -582,6 +582,7 @@ save_to_override_conf(RawConf, Opts) -> add_handlers() -> ok = emqx_config_logger:add_handler(), emqx_sys_mon:add_handler(), + emqx_config_logger:refresh_config(), ok. remove_handlers() -> From 1085492171eca307353412d8727f0619fb3b6cda Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Tue, 27 Dec 2022 09:38:40 +0800 Subject: [PATCH 0162/1802] chore: don't refresh when CT is running --- apps/emqx/src/config/emqx_config_logger.erl | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/emqx/src/config/emqx_config_logger.erl b/apps/emqx/src/config/emqx_config_logger.erl index 3d974d0c6..4b46e43d6 100644 --- a/apps/emqx/src/config/emqx_config_logger.erl +++ b/apps/emqx/src/config/emqx_config_logger.erl @@ -37,9 +37,14 @@ remove_handler() -> %% so we need to refresh the logger config after this node starts. %% It will not affect the logger config when cluster-override.conf is unchanged. refresh_config() -> - Log = emqx:get_raw_config(?LOG), - {ok, _} = emqx:update_config(?LOG, Log), - ok. + case emqx:get_raw_config(?LOG, undefined) of + %% no logger config when CT is running. + undefined -> + ok; + Log -> + {ok, _} = emqx:update_config(?LOG, Log), + ok + end. post_config_update(?LOG, _Req, _NewConf, _OldConf, AppEnvs) -> Kernel = proplists:get_value(kernel, AppEnvs), From c9f0355f5a13a41ee75e26c5d9411ad225a141c7 Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Fri, 13 Jan 2023 18:36:03 +0800 Subject: [PATCH 0163/1802] fix: save app env when bootstrap and reload --- apps/emqx/src/emqx_config.erl | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/apps/emqx/src/emqx_config.erl b/apps/emqx/src/emqx_config.erl index 51de1d0d0..13d30f3db 100644 --- a/apps/emqx/src/emqx_config.erl +++ b/apps/emqx/src/emqx_config.erl @@ -333,7 +333,8 @@ init_load(SchemaMod, RawConf, Opts) when is_map(RawConf) -> RootNames = get_root_names(), RawConfAll = raw_conf_with_default(SchemaMod, RootNames, RawConfWithOverrides, Opts), %% check configs against the schema - {_AppEnvs, CheckedConf} = check_config(SchemaMod, RawConfAll, #{}), + {AppEnvs, CheckedConf} = check_config(SchemaMod, RawConfAll, #{}), + save_to_app_env(AppEnvs), ok = save_to_config_map(CheckedConf, RawConfAll). %% keep the raw and non-raw conf has the same keys to make update raw conf easier. @@ -534,23 +535,19 @@ get_root_names() -> maps:get(names, persistent_term:get(?PERSIS_SCHEMA_MODS, #{names => []})). -spec save_configs(app_envs(), config(), raw_config(), raw_config(), update_opts()) -> ok. -save_configs(_AppEnvs, Conf, RawConf, OverrideConf, Opts) -> +save_configs(AppEnvs, Conf, RawConf, OverrideConf, Opts) -> %% We first try to save to override.conf, because saving to files is more error prone %% than saving into memory. ok = save_to_override_conf(OverrideConf, Opts), - %% We may need also support hot config update for the apps that use application envs. - %% If that is the case uncomment the following line to update the configs to app env - %save_to_app_env(_AppEnvs), + save_to_app_env(AppEnvs), save_to_config_map(Conf, RawConf). +-define(IGNORE_APPS, [kernel]). + -spec save_to_app_env([tuple()]) -> ok. -save_to_app_env(AppEnvs) -> - lists:foreach( - fun({AppName, Envs}) -> - [application:set_env(AppName, Par, Val) || {Par, Val} <- Envs] - end, - AppEnvs - ). +save_to_app_env(AppEnvs0) -> + AppEnvs = lists:filter(fun({App, _}) -> not lists:member(App, ?IGNORE_APPS) end, AppEnvs0), + application:set_env(AppEnvs). -spec save_to_config_map(config(), raw_config()) -> ok. save_to_config_map(Conf, RawConf) -> From 1fe006112335a07162868214b562b321e4be8624 Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Fri, 13 Jan 2023 23:30:00 +0800 Subject: [PATCH 0164/1802] fix: node.config_files type is hocon:array(string())) --- apps/emqx/test/emqx_common_test_helpers.erl | 4 +++- apps/emqx_conf/src/emqx_conf_schema.erl | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/emqx/test/emqx_common_test_helpers.erl b/apps/emqx/test/emqx_common_test_helpers.erl index f440dfc5a..f53c2702d 100644 --- a/apps/emqx/test/emqx_common_test_helpers.erl +++ b/apps/emqx/test/emqx_common_test_helpers.erl @@ -408,7 +408,9 @@ catch_call(F) -> C:E:S -> {crashed, {C, E, S}} end. -force_set_config_file_paths(emqx_conf, Paths) -> +force_set_config_file_paths(emqx_conf, [Path] = Paths) -> + Bin = iolist_to_binary(io_lib:format("node.config_files = [~p]", [Path])), + ok = file:write_file(Path, Bin, [append]), application:set_env(emqx, config_files, Paths); force_set_config_file_paths(emqx, Paths) -> application:set_env(emqx, config_files, Paths); diff --git a/apps/emqx_conf/src/emqx_conf_schema.erl b/apps/emqx_conf/src/emqx_conf_schema.erl index 90af47aca..7a20a88dc 100644 --- a/apps/emqx_conf/src/emqx_conf_schema.erl +++ b/apps/emqx_conf/src/emqx_conf_schema.erl @@ -463,7 +463,7 @@ fields("node") -> )}, {"config_files", sc( - list(string()), + hoconsc:array(string()), #{ mapping => "emqx.config_files", default => undefined, From 588140b8423d104c530637731cd33abade1ba431 Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Fri, 13 Jan 2023 23:33:22 +0800 Subject: [PATCH 0165/1802] chore: why we should ingore save kernel app env when init --- apps/emqx/src/emqx_config.erl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/emqx/src/emqx_config.erl b/apps/emqx/src/emqx_config.erl index 13d30f3db..ba4095daa 100644 --- a/apps/emqx/src/emqx_config.erl +++ b/apps/emqx/src/emqx_config.erl @@ -542,6 +542,8 @@ save_configs(AppEnvs, Conf, RawConf, OverrideConf, Opts) -> save_to_app_env(AppEnvs), save_to_config_map(Conf, RawConf). +%% we ignore kernel app env, +%% because the old app env will be used in emqx_config_logger:post_config_update/5 -define(IGNORE_APPS, [kernel]). -spec save_to_app_env([tuple()]) -> ok. From 09160383f5fc3b0c3fb73f7ac48a19d283170058 Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Fri, 13 Jan 2023 23:42:41 +0800 Subject: [PATCH 0166/1802] chore: bump emqx_resource to 0.1.5 --- apps/emqx_resource/src/emqx_resource.app.src | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx_resource/src/emqx_resource.app.src b/apps/emqx_resource/src/emqx_resource.app.src index 00389261b..e3a37fd10 100644 --- a/apps/emqx_resource/src/emqx_resource.app.src +++ b/apps/emqx_resource/src/emqx_resource.app.src @@ -1,7 +1,7 @@ %% -*- mode: erlang -*- {application, emqx_resource, [ {description, "Manager for all external resources"}, - {vsn, "0.1.4"}, + {vsn, "0.1.5"}, {registered, []}, {mod, {emqx_resource_app, []}}, {applications, [ From 6fe09447ed45f2d6a7c2d7aa0ca9d51f3c6e4d34 Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Fri, 13 Jan 2023 17:23:25 +0100 Subject: [PATCH 0167/1802] fix: stale test using old resource paths after merge --- apps/emqx_authz/test/emqx_authz_api_mnesia_SUITE.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx_authz/test/emqx_authz_api_mnesia_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_mnesia_SUITE.erl index 3ee3ba4d8..2aa2d9545 100644 --- a/apps/emqx_authz/test/emqx_authz_api_mnesia_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_api_mnesia_SUITE.erl @@ -168,7 +168,7 @@ t_api(_) -> {ok, 204, _} = request( post, - uri(["authorization", "sources", "built_in_database", "username"]), + uri(["authorization", "sources", "built_in_database", "rules", "users"]), [?USERNAME_RULES_EXAMPLE] ), From b07befbf5603d7f123e92e415e147a6c498862b9 Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Sat, 14 Jan 2023 01:03:57 +0800 Subject: [PATCH 0168/1802] test: conf_app ct failed --- .gitignore | 1 + apps/emqx/test/emqx_common_test_helpers.erl | 2 +- apps/emqx_conf/test/emqx_conf_app_SUITE.erl | 8 ++++++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 4780aab38..62e8ddc81 100644 --- a/.gitignore +++ b/.gitignore @@ -67,6 +67,7 @@ mix.lock apps/emqx/test/emqx_static_checks_data/master.bpapi # rendered configurations *.conf.rendered +*.conf.rendered.* lux_logs/ /.prepare bom.json diff --git a/apps/emqx/test/emqx_common_test_helpers.erl b/apps/emqx/test/emqx_common_test_helpers.erl index f53c2702d..e4f50f2a1 100644 --- a/apps/emqx/test/emqx_common_test_helpers.erl +++ b/apps/emqx/test/emqx_common_test_helpers.erl @@ -409,7 +409,7 @@ catch_call(F) -> {crashed, {C, E, S}} end. force_set_config_file_paths(emqx_conf, [Path] = Paths) -> - Bin = iolist_to_binary(io_lib:format("node.config_files = [~p]", [Path])), + Bin = iolist_to_binary(io_lib:format("node.config_files = [~p]~n", [Path])), ok = file:write_file(Path, Bin, [append]), application:set_env(emqx, config_files, Paths); force_set_config_file_paths(emqx, Paths) -> diff --git a/apps/emqx_conf/test/emqx_conf_app_SUITE.erl b/apps/emqx_conf/test/emqx_conf_app_SUITE.erl index dab4c4919..84ced5362 100644 --- a/apps/emqx_conf/test/emqx_conf_app_SUITE.erl +++ b/apps/emqx_conf/test/emqx_conf_app_SUITE.erl @@ -92,6 +92,14 @@ set_data_dir_env() -> Node = atom_to_list(node()), %% will create certs and authz dir ok = filelib:ensure_dir(Node ++ "/configs/"), + {ok, [ConfigFile]} = application:get_env(emqx, config_files), + NewConfigFile = ConfigFile ++ "." ++ Node, + {ok, _} = file:copy(ConfigFile, NewConfigFile), + Bin = iolist_to_binary(io_lib:format("node.config_files = [~p]~n", [NewConfigFile])), + ok = file:write_file(NewConfigFile, Bin, [append]), + DataDir = iolist_to_binary(io_lib:format("node.data_dir = ~p~n", [Node])), + ok = file:write_file(NewConfigFile, DataDir, [append]), + application:set_env(emqx, config_files, [NewConfigFile]), application:set_env(emqx, data_dir, Node), application:set_env(emqx, cluster_override_conf_file, Node ++ "/configs/cluster-override.conf"), ok. From 4b6c95e3d27dda1ba0fd0ed5b43d76ebf68e2b87 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Sat, 14 Jan 2023 09:07:57 +0100 Subject: [PATCH 0169/1802] chore: upgrade enterprise dashboard to e1.0.1-beta.13 --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index faa866753..fc87f1d95 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ export EMQX_DEFAULT_RUNNER = debian:11-slim export OTP_VSN ?= $(shell $(CURDIR)/scripts/get-otp-vsn.sh) export ELIXIR_VSN ?= $(shell $(CURDIR)/scripts/get-elixir-vsn.sh) export EMQX_DASHBOARD_VERSION ?= v1.1.5 -export EMQX_EE_DASHBOARD_VERSION ?= e1.0.1-beta.12 +export EMQX_EE_DASHBOARD_VERSION ?= e1.0.1-beta.13 export EMQX_REL_FORM ?= tgz export QUICER_DOWNLOAD_FROM_RELEASE = 1 ifeq ($(OS),Windows_NT) From 7073c62dd9972314ce9f255a4f02e53085b4b514 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Sat, 14 Jan 2023 10:45:20 +0100 Subject: [PATCH 0170/1802] feat: add a 'this' for rule engine put env now a dot is allowed in front of a var (path) e.g. ${.clientid} is equivalent to ${clientid} ${.} however, means everything. the parsed var is interally represented as $_THIS_ to make it easier to read/search. --- apps/emqx/include/emqx_placeholder.hrl | 2 ++ apps/emqx_plugin_libs/src/emqx_placeholder.erl | 10 ++++++---- apps/emqx_rule_engine/src/emqx_rule_maps.erl | 4 ++++ 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/apps/emqx/include/emqx_placeholder.hrl b/apps/emqx/include/emqx_placeholder.hrl index c4d235caf..d5da3fb18 100644 --- a/apps/emqx/include/emqx_placeholder.hrl +++ b/apps/emqx/include/emqx_placeholder.hrl @@ -17,6 +17,8 @@ -ifndef(EMQX_PLACEHOLDER_HRL). -define(EMQX_PLACEHOLDER_HRL, true). +-define(PH_VAR_THIS, <<"$_THIS_">>). + -define(PH(Type), <<"${", Type/binary, "}">>). %% action: publish/subscribe/all diff --git a/apps/emqx_plugin_libs/src/emqx_placeholder.erl b/apps/emqx_plugin_libs/src/emqx_placeholder.erl index 3b15a389d..b7dea5256 100644 --- a/apps/emqx_plugin_libs/src/emqx_placeholder.erl +++ b/apps/emqx_plugin_libs/src/emqx_placeholder.erl @@ -39,6 +39,8 @@ sql_data/1 ]). +-include_lib("emqx/include/emqx_placeholder.hrl"). + -define(EX_PLACE_HOLDER, "(\\$\\{[a-zA-Z0-9\\._]+\\})"). -define(EX_PLACE_HOLDER_DOUBLE_QUOTE, "(\\$\\{[a-zA-Z0-9\\._]+\\}|\"\\$\\{[a-zA-Z0-9\\._]+\\}\")"). @@ -233,9 +235,6 @@ proc_param_str(Tokens, Data, Quote) -> proc_tmpl(Tokens, Data, #{return => rawlist, var_trans => Quote}) ). -%% backward compatibility for hot upgrading from =< e4.2.1 -get_phld_var(Fun, Data) when is_function(Fun) -> - Fun(Data); get_phld_var(Phld, Data) -> emqx_rule_maps:nested_get(Phld, Data). @@ -298,9 +297,12 @@ replace_with(Tmpl, RE, '$n') -> Parts ), Res. - +parse_nested(<<".", R/binary>>) -> + %% ignroe the root . + parse_nested(R); parse_nested(Attr) -> case string:split(Attr, <<".">>, all) of + [<<>>] -> {var, ?PH_VAR_THIS}; [Attr] -> {var, Attr}; Nested -> {path, [{key, P} || P <- Nested]} end. diff --git a/apps/emqx_rule_engine/src/emqx_rule_maps.erl b/apps/emqx_rule_engine/src/emqx_rule_maps.erl index 3e0ebc72d..13f99c88b 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_maps.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_maps.erl @@ -26,9 +26,13 @@ unsafe_atom_key_map/1 ]). +-include_lib("emqx/include/emqx_placeholder.hrl"). + nested_get(Key, Data) -> nested_get(Key, Data, undefined). +nested_get({var, ?PH_VAR_THIS}, Data, _Default) -> + Data; nested_get({var, Key}, Data, Default) -> general_map_get({key, Key}, Data, Data, Default); nested_get({path, Path}, Data, Default) when is_list(Path) -> From 0f2f5fbbe0f733caa3bf6486af65f8f9339110ee Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Sat, 14 Jan 2023 13:51:26 +0100 Subject: [PATCH 0171/1802] fix(authn): no exception when password is 'undefined' --- apps/emqx/src/emqx_channel.erl | 3 - apps/emqx/src/emqx_passwd.erl | 13 ++-- apps/emqx/test/emqx_frame_SUITE.erl | 36 +++++++++++ apps/emqx_authn/test/emqx_authn_SUITE.erl | 73 ++++++++++++++++++++--- changes/{ => v5.0.14}/refactor-9653.en.md | 0 changes/{ => v5.0.14}/refactor-9653.zh.md | 0 changes/v5.0.15/fix-9763.en.md | 1 + changes/v5.0.15/fix-9763.zh.md | 1 + 8 files changed, 111 insertions(+), 16 deletions(-) rename changes/{ => v5.0.14}/refactor-9653.en.md (100%) rename changes/{ => v5.0.14}/refactor-9653.zh.md (100%) create mode 100644 changes/v5.0.15/fix-9763.en.md create mode 100644 changes/v5.0.15/fix-9763.zh.md diff --git a/apps/emqx/src/emqx_channel.erl b/apps/emqx/src/emqx_channel.erl index b6be52c6e..e82adc786 100644 --- a/apps/emqx/src/emqx_channel.erl +++ b/apps/emqx/src/emqx_channel.erl @@ -1427,7 +1427,6 @@ interval(will_timer, #channel{will_msg = WillMsg}) -> -spec terminate(any(), channel()) -> ok. terminate(_, #channel{conn_state = idle} = _Channel) -> - ?tp(channel_terminated, #{channel => _Channel}), ok; terminate(normal, Channel) -> run_terminate_hook(normal, Channel); @@ -1460,10 +1459,8 @@ persist_if_session(#channel{session = Session} = Channel) -> end. run_terminate_hook(_Reason, #channel{session = undefined} = _Channel) -> - ?tp(channel_terminated, #{channel => _Channel}), ok; run_terminate_hook(Reason, #channel{clientinfo = ClientInfo, session = Session} = _Channel) -> - ?tp(channel_terminated, #{channel => _Channel}), emqx_session:terminate(ClientInfo, Reason, Session). %%-------------------------------------------------------------------- diff --git a/apps/emqx/src/emqx_passwd.erl b/apps/emqx/src/emqx_passwd.erl index 0d264f45f..dc940645b 100644 --- a/apps/emqx/src/emqx_passwd.erl +++ b/apps/emqx/src/emqx_passwd.erl @@ -57,22 +57,27 @@ %% APIs %%-------------------------------------------------------------------- --spec check_pass(hash_params(), password_hash(), password()) -> boolean(). -check_pass({pbkdf2, MacFun, Salt, Iterations, DKLength}, PasswordHash, Password) -> +-spec check_pass(hash_params(), password_hash(), password() | undefined) -> boolean(). +check_pass(_Algo, _Hash, undefined) -> + false; +check_pass(Algo, Hash, Password) -> + do_check_pass(Algo, Hash, Password). + +do_check_pass({pbkdf2, MacFun, Salt, Iterations, DKLength}, PasswordHash, Password) -> case pbkdf2(MacFun, Password, Salt, Iterations, DKLength) of {ok, HashPasswd} -> compare_secure(hex(HashPasswd), PasswordHash); {error, _Reason} -> false end; -check_pass({bcrypt, Salt}, PasswordHash, Password) -> +do_check_pass({bcrypt, Salt}, PasswordHash, Password) -> case bcrypt:hashpw(Password, Salt) of {ok, HashPasswd} -> compare_secure(list_to_binary(HashPasswd), PasswordHash); {error, _Reason} -> false end; -check_pass({_SimpleHash, _Salt, _SaltPosition} = HashParams, PasswordHash, Password) -> +do_check_pass({_SimpleHash, _Salt, _SaltPosition} = HashParams, PasswordHash, Password) -> Hash = hash(HashParams, Password), compare_secure(Hash, PasswordHash). diff --git a/apps/emqx/test/emqx_frame_SUITE.erl b/apps/emqx/test/emqx_frame_SUITE.erl index 2a8d1bc39..23e8972e9 100644 --- a/apps/emqx/test/emqx_frame_SUITE.erl +++ b/apps/emqx/test/emqx_frame_SUITE.erl @@ -670,6 +670,42 @@ t_invalid_clientid(_) -> emqx_frame:parse(<<16, 15, 0, 6, 77, 81, 73, 115, 100, 112, 3, 0, 0, 0, 1, 0, 0>>) ). +%% for regression: `password` must be `undefined` +t_undefined_password(_) -> + Payload = <<16, 19, 0, 4, 77, 81, 84, 84, 4, 130, 0, 60, 0, 2, 97, 49, 0, 3, 97, 97, 97>>, + {ok, Packet, <<>>, {none, _}} = emqx_frame:parse(Payload), + Password = undefined, + ?assertEqual( + #mqtt_packet{ + header = #mqtt_packet_header{ + type = 1, + dup = false, + qos = 0, + retain = false + }, + variable = #mqtt_packet_connect{ + proto_name = <<"MQTT">>, + proto_ver = 4, + is_bridge = false, + clean_start = true, + will_flag = false, + will_qos = 0, + will_retain = false, + keepalive = 60, + properties = #{}, + clientid = <<"a1">>, + will_props = #{}, + will_topic = undefined, + will_payload = undefined, + username = <<"aaa">>, + password = Password + }, + payload = undefined + }, + Packet + ), + ok. + parse_serialize(Packet) -> parse_serialize(Packet, #{strict_mode => true}). diff --git a/apps/emqx_authn/test/emqx_authn_SUITE.erl b/apps/emqx_authn/test/emqx_authn_SUITE.erl index 3017c3346..6b24f7231 100644 --- a/apps/emqx_authn/test/emqx_authn_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_SUITE.erl @@ -97,21 +97,76 @@ t_will_message_connection_denied(Config) when is_list(Config) -> {will_topic, <<"lwt">>}, {will_payload, <<"should not be published">>} ]), - snabbkaffe:start_trace(), - ?wait_async_action( - {error, _} = emqtt:connect(Publisher), - #{?snk_kind := channel_terminated} - ), - snabbkaffe:stop(), - + Ref = monitor(process, Publisher), + {error, _} = emqtt:connect(Publisher), + receive + {'DOWN', Ref, process, Publisher, Reason} -> + ?assertEqual({shutdown, unauthorized_client}, Reason) + after 2000 -> + error(timeout) + end, receive {publish, #{ topic := <<"lwt">>, payload := <<"should not be published">> }} -> ct:fail("should not publish will message") - after 0 -> + after 1000 -> ok end, - ok. + +%% With auth enabled, send CONNECT without password field, +%% expect CONNACK with reason_code=5 and socket close +t_password_undefined({init, Config}) -> + emqx_common_test_helpers:start_apps([emqx_conf, emqx_authn]), + AuthnConfig = #{ + <<"mechanism">> => <<"password_based">>, + <<"backend">> => <<"built_in_database">>, + <<"user_id_type">> => <<"clientid">> + }, + Chain = 'mqtt:global', + emqx:update_config( + [authentication], + {create_authenticator, Chain, AuthnConfig} + ), + Config; +t_password_undefined({'end', _Config}) -> + emqx:update_config( + [authentication], + {delete_authenticator, 'mqtt:global', <<"password_based:built_in_database">>} + ), + emqx_common_test_helpers:stop_apps([emqx_authn, emqx_conf]), + ok; +t_password_undefined(Config) when is_list(Config) -> + Payload = <<16, 19, 0, 4, 77, 81, 84, 84, 4, 130, 0, 60, 0, 2, 97, 49, 0, 3, 97, 97, 97>>, + {ok, Sock} = gen_tcp:connect("localhost", 1883, [binary, {active, true}]), + gen_tcp:send(Sock, Payload), + receive + {tcp, Sock, Bytes} -> + Resp = parse(iolist_to_binary(Bytes)), + ?assertMatch( + #mqtt_packet{ + header = #mqtt_packet_header{type = ?CONNACK}, + variable = #mqtt_packet_connack{ + ack_flags = 0, + reason_code = ?CONNACK_AUTH + }, + payload = undefined + }, + Resp + ) + after 2000 -> + error(timeout) + end, + receive + {tcp_closed, Sock} -> + ok + after 2000 -> + error(timeout) + end, + ok. + +parse(Bytes) -> + {ok, Frame, <<>>, {none, _}} = emqx_frame:parse(Bytes), + Frame. diff --git a/changes/refactor-9653.en.md b/changes/v5.0.14/refactor-9653.en.md similarity index 100% rename from changes/refactor-9653.en.md rename to changes/v5.0.14/refactor-9653.en.md diff --git a/changes/refactor-9653.zh.md b/changes/v5.0.14/refactor-9653.zh.md similarity index 100% rename from changes/refactor-9653.zh.md rename to changes/v5.0.14/refactor-9653.zh.md diff --git a/changes/v5.0.15/fix-9763.en.md b/changes/v5.0.15/fix-9763.en.md new file mode 100644 index 000000000..8c07a3d5d --- /dev/null +++ b/changes/v5.0.15/fix-9763.en.md @@ -0,0 +1 @@ +Fix an authentication exception when password is not provided diff --git a/changes/v5.0.15/fix-9763.zh.md b/changes/v5.0.15/fix-9763.zh.md new file mode 100644 index 000000000..8548a363e --- /dev/null +++ b/changes/v5.0.15/fix-9763.zh.md @@ -0,0 +1 @@ +修复客户端没有提供密码时的一个异常 From 91c5a8998530a1a4cc091896897544aac9bb5996 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Sat, 14 Jan 2023 18:33:55 +0100 Subject: [PATCH 0172/1802] test: wait for redis connected state the case is sometimes flaky because the health check sometimes return connecting --- .../test/emqx_ee_bridge_redis_SUITE.erl | 37 ++++++++++++++++--- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_redis_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_redis_SUITE.erl index c3529ddeb..feebcfade 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_redis_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_redis_SUITE.erl @@ -32,6 +32,21 @@ -define(PROXY_HOST, "toxiproxy"). -define(PROXY_PORT, "8474"). +-define(WAIT(PATTERN, EXPRESSION, TIMEOUT), + wait( + fun() -> + case EXPRESSION of + PATTERN -> + ok; + Other -> + ct:pal("ignored wait result: ~p", [Other]), + error + end + end, + TIMEOUT + ) +). + all() -> [{group, transport_types}, {group, rest}]. groups() -> @@ -156,9 +171,10 @@ t_create_delete_bridge(Config) -> ResourceId = emqx_bridge_resource:resource_id(Type, Name), - ?assertEqual( + ?WAIT( {ok, connected}, - emqx_resource:health_check(ResourceId) + emqx_resource:health_check(ResourceId), + 5 ), RedisType = atom_to_binary(Type), @@ -224,11 +240,11 @@ t_check_replay(Config) -> ), ResourceId = emqx_bridge_resource:resource_id(Type, Name), - Health = emqx_resource:health_check(ResourceId), - ?assertEqual( + ?WAIT( {ok, connected}, - Health + emqx_resource:health_check(ResourceId), + 5 ), ?check_trace( @@ -508,3 +524,14 @@ publish_message(Topic, Payload) -> {ok, _} = emqtt:connect(Client), ok = emqtt:publish(Client, Topic, Payload), ok = emqtt:stop(Client). + +wait(_F, 0) -> + error(timeout); +wait(F, Attempt) -> + case F() of + ok -> + ok; + _ -> + timer:sleep(1000), + wait(F, Attempt - 1) + end. From c228b84db6e26206553d6868ff1c6af8bd1e3b50 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Sat, 14 Jan 2023 18:36:52 +0100 Subject: [PATCH 0173/1802] fix(log): fail fast if emqx.conf is not found in EMQX_ETC_DIR otherwise the call_hocon failure is not quite readable --- bin/emqx | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/emqx b/bin/emqx index c5bf88149..ebf10bada 100755 --- a/bin/emqx +++ b/bin/emqx @@ -456,6 +456,7 @@ if [ "$IS_ENTERPRISE" = 'yes' ]; then fi if [ "$IS_BOOT_COMMAND" = 'yes' ]; then + [ -f "$EMQX_ETC_DIR"/emqx.conf ] || die "emqx.conf is not found in $EMQX_ETC_DIR" 1 if [ "${EMQX_BOOT_CONFIGS:-}" = '' ]; then EMQX_BOOT_CONFIGS="$(call_hocon -s "$SCHEMA_MOD" -c "$EMQX_ETC_DIR"/emqx.conf multi_get "${CONF_KEYS[@]}")" ## export here so the 'console' command recursively called from From 87823e83e8fec45f6ecfcf1ffa857a68dc853a89 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Mon, 16 Jan 2023 12:37:47 +0800 Subject: [PATCH 0174/1802] chore: update ehttpc to 0.4.4 --- mix.exs | 2 +- rebar.config | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mix.exs b/mix.exs index 27d9b43ce..0bbf68769 100644 --- a/mix.exs +++ b/mix.exs @@ -47,7 +47,7 @@ defmodule EMQXUmbrella.MixProject do {:lc, github: "emqx/lc", tag: "0.3.2", override: true}, {:redbug, "2.0.8"}, {:typerefl, github: "ieQu1/typerefl", tag: "0.9.1", override: true}, - {:ehttpc, github: "emqx/ehttpc", tag: "0.4.3", override: true}, + {:ehttpc, github: "emqx/ehttpc", tag: "0.4.4", override: true}, {:gproc, github: "uwiger/gproc", tag: "0.8.0", override: true}, {:jiffy, github: "emqx/jiffy", tag: "1.0.5", override: true}, {:cowboy, github: "emqx/cowboy", tag: "2.9.0", override: true}, diff --git a/rebar.config b/rebar.config index f05624477..bfdc3e4ee 100644 --- a/rebar.config +++ b/rebar.config @@ -49,7 +49,7 @@ , {gpb, "4.19.5"} %% gpb only used to build, but not for release, pin it here to avoid fetching a wrong version due to rebar plugins scattered in all the deps , {typerefl, {git, "https://github.com/ieQu1/typerefl", {tag, "0.9.1"}}} , {gun, {git, "https://github.com/emqx/gun", {tag, "1.3.9"}}} - , {ehttpc, {git, "https://github.com/emqx/ehttpc", {tag, "0.4.3"}}} + , {ehttpc, {git, "https://github.com/emqx/ehttpc", {tag, "0.4.4"}}} , {gproc, {git, "https://github.com/uwiger/gproc", {tag, "0.8.0"}}} , {jiffy, {git, "https://github.com/emqx/jiffy", {tag, "1.0.5"}}} , {cowboy, {git, "https://github.com/emqx/cowboy", {tag, "2.9.0"}}} From 47414e0d537272487c44ded16f2996b27078ffa5 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Fri, 13 Jan 2023 08:31:26 +0100 Subject: [PATCH 0175/1802] docs: improve kafka key and value field description --- .../emqx_plugin_libs/src/emqx_placeholder.erl | 2 +- .../i18n/emqx_ee_bridge_kafka.conf | 15 ++++++----- .../src/emqx_ee_bridge_kafka.erl | 6 ++--- .../kafka/emqx_bridge_impl_kafka_producer.erl | 25 +++++++++++++------ .../emqx_bridge_impl_kafka_producer_SUITE.erl | 7 +++++- 5 files changed, 37 insertions(+), 18 deletions(-) diff --git a/apps/emqx_plugin_libs/src/emqx_placeholder.erl b/apps/emqx_plugin_libs/src/emqx_placeholder.erl index b7dea5256..3e98fa149 100644 --- a/apps/emqx_plugin_libs/src/emqx_placeholder.erl +++ b/apps/emqx_plugin_libs/src/emqx_placeholder.erl @@ -298,7 +298,7 @@ replace_with(Tmpl, RE, '$n') -> ), Res. parse_nested(<<".", R/binary>>) -> - %% ignroe the root . + %% ignore the root . parse_nested(R); parse_nested(Attr) -> case string:split(Attr, <<".">>, all) of diff --git a/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_kafka.conf b/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_kafka.conf index 7ccf20d0b..201eeba3b 100644 --- a/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_kafka.conf +++ b/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_kafka.conf @@ -250,9 +250,10 @@ emqx_ee_bridge_kafka { kafka_message_key { desc { en: "Template to render Kafka message key. " - "If the desired variable for this template is not found in the input data " - "NULL is used." - zh: "生成 Kafka 消息 Key 的模版。当所需要的输入没有时,会使用 NULL。" + "If the template is rendered into a NULL value (i.e. there is no such data field in Rule Engine context) " + "then Kafka's NULL (but not empty string) is used." + zh: "生成 Kafka 消息 Key 的模版。如果模版生成后为空值," + "则会使用 Kafka 的 NULL ,而非空字符串。" } label { en: "Message Key" @@ -262,9 +263,11 @@ emqx_ee_bridge_kafka { kafka_message_value { desc { en: "Template to render Kafka message value. " - "If the desired variable for this template is not found in the input data " - "NULL is used." - zh: "生成 Kafka 消息 Value 的模版。当所需要的输入没有时,会使用 NULL。" + "If the template is rendered " + "into a NULL value (i.e. there is no such data field in Rule Engine context) " + "then Kafka's NULL (but not empty string) is used." + zh: "生成 Kafka 消息 Value 的模版。如果模版生成后为空值," + "则会使用 Kafka 的 NULL,而非空字符串。" } label { en: "Message Value" diff --git a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_kafka.erl b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_kafka.erl index 9fae4f30a..4a6c1411c 100644 --- a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_kafka.erl +++ b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_kafka.erl @@ -207,11 +207,11 @@ fields(producer_kafka_opts) -> ]; fields(kafka_message) -> [ - {key, mk(string(), #{default => "${clientid}", desc => ?DESC(kafka_message_key)})}, - {value, mk(string(), #{default => "${payload}", desc => ?DESC(kafka_message_value)})}, + {key, mk(string(), #{default => "${.clientid}", desc => ?DESC(kafka_message_key)})}, + {value, mk(string(), #{default => "${.}", desc => ?DESC(kafka_message_value)})}, {timestamp, mk(string(), #{ - default => "${timestamp}", desc => ?DESC(kafka_message_timestamp) + default => "${.timestamp}", desc => ?DESC(kafka_message_timestamp) })} ]; fields(producer_buffer) -> diff --git a/lib-ee/emqx_ee_bridge/src/kafka/emqx_bridge_impl_kafka_producer.erl b/lib-ee/emqx_ee_bridge/src/kafka/emqx_bridge_impl_kafka_producer.erl index a7de100d6..d7b4de0c0 100644 --- a/lib-ee/emqx_ee_bridge/src/kafka/emqx_bridge_impl_kafka_producer.erl +++ b/lib-ee/emqx_ee_bridge/src/kafka/emqx_bridge_impl_kafka_producer.erl @@ -145,15 +145,19 @@ on_query(_InstId, {send_message, Message}, #{message_template := Template, produ {_Partition, _Pid} = wolff:send(Producers, [KafkaMessage], {fun ?MODULE:on_kafka_ack/3, [#{}]}), {async_return, ok}. -compile_message_template(#{ - key := KeyTemplate, value := ValueTemplate, timestamp := TimestampTemplate -}) -> +compile_message_template(T) -> + KeyTemplate = maps:get(key, T, <<"${.clientid}">>), + ValueTemplate = maps:get(value, T, <<"${.}">>), + TimestampTemplate = maps:get(value, T, <<"${.timestamp}">>), #{ - key => emqx_plugin_libs_rule:preproc_tmpl(KeyTemplate), - value => emqx_plugin_libs_rule:preproc_tmpl(ValueTemplate), - timestamp => emqx_plugin_libs_rule:preproc_tmpl(TimestampTemplate) + key => preproc_tmpl(KeyTemplate), + value => preproc_tmpl(ValueTemplate), + timestamp => preproc_tmpl(TimestampTemplate) }. +preproc_tmpl(Tmpl) -> + emqx_plugin_libs_rule:preproc_tmpl(Tmpl). + render_message( #{key := KeyTemplate, value := ValueTemplate, timestamp := TimestampTemplate}, Message ) -> @@ -164,7 +168,14 @@ render_message( }. render(Template, Message) -> - emqx_plugin_libs_rule:proc_tmpl(Template, Message). + Opts = #{ + var_trans => fun + (undefined) -> <<"">>; + (X) -> emqx_plugin_libs_rule:bin(X) + end, + return => full_binary + }, + emqx_plugin_libs_rule:proc_tmpl(Template, Message, Opts). render_timestamp(Template, Message) -> try diff --git a/lib-ee/emqx_ee_bridge/test/emqx_bridge_impl_kafka_producer_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_bridge_impl_kafka_producer_SUITE.erl index 8e5b1fa95..2d67a9941 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_bridge_impl_kafka_producer_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_bridge_impl_kafka_producer_SUITE.erl @@ -260,7 +260,11 @@ kafka_bridge_rest_api_helper(Config) -> topic => <<"t/#">> }, <<"kafka">> => #{ - <<"topic">> => erlang:list_to_binary(KafkaTopic) + <<"topic">> => erlang:list_to_binary(KafkaTopic), + <<"message">> => #{ + <<"key">> => <<"${clientid}">>, + <<"value">> => <<"${.payload}">> + } } } }, @@ -501,6 +505,7 @@ producer = { } kafka = { topic = \"{{ kafka_topic }}\" + message = {key = \"${clientid}\", value = \"${.payload}\"} } } """. From edfcea7f4319dcaf348cfaa4e01ea01a560207e3 Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Fri, 13 Jan 2023 14:45:44 +0100 Subject: [PATCH 0176/1802] fix: add log for timeout --- apps/emqx_connector/src/emqx_connector_http.erl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/emqx_connector/src/emqx_connector_http.erl b/apps/emqx_connector/src/emqx_connector_http.erl index 7f684a858..d68f624e4 100644 --- a/apps/emqx_connector/src/emqx_connector_http.erl +++ b/apps/emqx_connector/src/emqx_connector_http.erl @@ -417,6 +417,10 @@ do_get_status(PoolName, Timeout) -> end catch exit:timeout -> + ?SLOG(error, #{ + msg => "http_connector_pmap_failed", + reason => timeout + }), {error, timeout} end. From 9a53410efb0bd9611d6d76cadea2d9b31107305f Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Fri, 13 Jan 2023 14:46:30 +0100 Subject: [PATCH 0177/1802] fix: don't add empty list element to options proplist --- .../src/emqx_connector_mysql.erl | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/apps/emqx_connector/src/emqx_connector_mysql.erl b/apps/emqx_connector/src/emqx_connector_mysql.erl index b162a21b4..c144c48e9 100644 --- a/apps/emqx_connector/src/emqx_connector_mysql.erl +++ b/apps/emqx_connector/src/emqx_connector_mysql.erl @@ -111,17 +111,19 @@ on_start( false -> [] end, - Options = [ - maybe_password_opt(maps:get(password, Config, undefined)) - | [ - {host, Host}, - {port, Port}, - {user, Username}, - {database, DB}, - {auto_reconnect, ?AUTO_RECONNECT_INTERVAL}, - {pool_size, PoolSize} - ] - ], + Options = + maybe_add_password_opt( + maps:get(password, Config, undefined), + [ + {host, Host}, + {port, Port}, + {user, Username}, + {database, DB}, + {auto_reconnect, ?AUTO_RECONNECT_INTERVAL}, + {pool_size, PoolSize} + ] + ), + PoolName = emqx_plugin_libs_pool:pool_name(InstId), Prepares = parse_prepare_sql(Config), State = maps:merge(#{poolname => PoolName}, Prepares), @@ -136,10 +138,10 @@ on_start( {error, Reason} end. -maybe_password_opt(undefined) -> - []; -maybe_password_opt(Password) -> - {password, Password}. +maybe_add_password_opt(undefined, Options) -> + Options; +maybe_add_password_opt(Password, Options) -> + [{password, Password} | Options]. on_stop(InstId, #{poolname := PoolName}) -> ?SLOG(info, #{ From a19226326ccb44c3064777fab04918fbcc73e45b Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Fri, 13 Jan 2023 18:27:00 +0100 Subject: [PATCH 0178/1802] fix: don't test for strict values as that makes test flaky --- apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl b/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl index 4116a4754..a5d7968a2 100644 --- a/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl +++ b/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl @@ -718,7 +718,7 @@ t_metrics(Config) -> {ok, 200, Bridge3Str} = request(get, uri(["bridges", BridgeID, "metrics"]), []), ?assertMatch( #{ - <<"metrics">> := #{<<"success">> := 1}, + <<"metrics">> := #{<<"success">> := _}, <<"node_metrics">> := [_ | _] }, jsx:decode(Bridge3Str) @@ -729,7 +729,7 @@ t_metrics(Config) -> ?assertMatch( [ #{ - <<"metrics">> := #{<<"success">> := 1}, + <<"metrics">> := #{<<"success">> := _}, <<"node_metrics">> := [_ | _] } ], From 67909f0b404db067bf9bf3b71645d360f2389cac Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Mon, 16 Jan 2023 12:07:24 +0100 Subject: [PATCH 0179/1802] fix: testing metrics for emqx_bridge_mqtt_SUITE --- apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl b/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl index 6d0e88254..b3c2b8d85 100644 --- a/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl +++ b/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl @@ -280,7 +280,7 @@ t_mqtt_conn_bridge_ingress_no_payload_template(_) -> ), %% verify the metrics of the bridge - {ok, 200, BridgeStr} = request(get, uri(["bridges", BridgeIDIngress]), []), + {ok, 200, BridgeStr} = request(get, uri(["bridges", BridgeIDIngress, "metrics"]), []), ?assertMatch( #{ <<"metrics">> := #{<<"matched">> := 0, <<"received">> := 1}, @@ -421,7 +421,7 @@ t_mqtt_conn_bridge_egress_no_payload_template(_) -> ), %% verify the metrics of the bridge - {ok, 200, BridgeStr} = request(get, uri(["bridges", BridgeIDEgress]), []), + {ok, 200, BridgeStr} = request(get, uri(["bridges", BridgeIDEgress, "metrics"]), []), ?assertMatch( #{ <<"metrics">> := #{<<"matched">> := 1, <<"success">> := 1, <<"failed">> := 0}, From 54cea3b3f2b2cb6551b6970e44900c1010c3dc0d Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Mon, 16 Jan 2023 12:49:06 +0300 Subject: [PATCH 0180/1802] chore: update epgsql to 4.7.0.1 with full OTP-25 compat --- apps/emqx_connector/rebar.config | 2 +- mix.exs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/emqx_connector/rebar.config b/apps/emqx_connector/rebar.config index ed0bc827d..970ea3b9d 100644 --- a/apps/emqx_connector/rebar.config +++ b/apps/emqx_connector/rebar.config @@ -10,7 +10,7 @@ {emqx_resource, {path, "../emqx_resource"}}, {eldap2, {git, "https://github.com/emqx/eldap2", {tag, "v0.2.2"}}}, {mysql, {git, "https://github.com/emqx/mysql-otp", {tag, "1.7.1"}}}, - {epgsql, {git, "https://github.com/emqx/epgsql", {tag, "4.7-emqx.2"}}}, + {epgsql, {git, "https://github.com/emqx/epgsql", {tag, "4.7.0.1"}}}, %% NOTE: mind poolboy version when updating mongodb-erlang version {mongodb, {git, "https://github.com/emqx/mongodb-erlang", {tag, "v3.0.19"}}}, %% NOTE: mind poolboy version when updating eredis_cluster version diff --git a/mix.exs b/mix.exs index 27d9b43ce..2f3cf70de 100644 --- a/mix.exs +++ b/mix.exs @@ -74,8 +74,8 @@ defmodule EMQXUmbrella.MixProject do {:jose, github: "potatosalad/erlang-jose", tag: "1.11.2"}, # in conflict by ehttpc and emqtt {:gun, github: "emqx/gun", tag: "1.3.9", override: true}, - # in conflict by emqx_connectior and system_monitor - {:epgsql, github: "emqx/epgsql", tag: "4.7-emqx.2", override: true}, + # in conflict by emqx_connector and system_monitor + {:epgsql, github: "emqx/epgsql", tag: "4.7.0.1", override: true}, # in conflict by mongodb and eredis_cluster {:poolboy, github: "emqx/poolboy", tag: "1.5.2", override: true}, # in conflict by emqx and observer_cli From ce2dba15b4b837107be45e048f57a122148a41c6 Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Fri, 13 Jan 2023 17:21:47 +0300 Subject: [PATCH 0181/1802] feat: turn tables queried with search APIs into ordered sets This is needed to ensure more or less consistent client experience for the new planned cursor-based search APIs. --- apps/emqx/src/emqx_alarm.erl | 2 +- apps/emqx/src/emqx_broker.erl | 2 +- .../src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl | 1 + apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl | 1 + apps/emqx_gateway/src/emqx_gateway_cm.erl | 2 +- apps/emqx_rule_engine/src/emqx_rule_engine_app.erl | 2 +- 6 files changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/emqx/src/emqx_alarm.erl b/apps/emqx/src/emqx_alarm.erl index 6122ff596..209715a85 100644 --- a/apps/emqx/src/emqx_alarm.erl +++ b/apps/emqx/src/emqx_alarm.erl @@ -89,7 +89,7 @@ mnesia(boot) -> ok = mria:create_table( ?ACTIVATED_ALARM, [ - {type, set}, + {type, ordered_set}, {storage, disc_copies}, {local_content, true}, {record_name, activated_alarm}, diff --git a/apps/emqx/src/emqx_broker.erl b/apps/emqx/src/emqx_broker.erl index 7b8e3dddd..ba16895ae 100644 --- a/apps/emqx/src/emqx_broker.erl +++ b/apps/emqx/src/emqx_broker.erl @@ -107,7 +107,7 @@ create_tabs() -> TabOpts = [public, {read_concurrency, true}, {write_concurrency, true}], %% SubOption: {SubPid, Topic} -> SubOption - ok = emqx_tables:new(?SUBOPTION, [set | TabOpts]), + ok = emqx_tables:new(?SUBOPTION, [ordered_set | TabOpts]), %% Subscription: SubPid -> Topic1, Topic2, Topic3, ... %% duplicate_bag: o(1) insert diff --git a/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl b/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl index ac39e2cda..6ce59d4f9 100644 --- a/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl +++ b/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl @@ -94,6 +94,7 @@ mnesia(boot) -> ok = mria:create_table(?TAB, [ {rlog_shard, ?AUTH_SHARD}, + {type, ordered_set}, {storage, disc_copies}, {record_name, user_info}, {attributes, record_info(fields, user_info)}, diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl index 7c51644b7..25a3a5976 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl @@ -96,6 +96,7 @@ mnesia(boot) -> ok = mria:create_table(?TAB, [ {rlog_shard, ?AUTH_SHARD}, + {type, ordered_set}, {storage, disc_copies}, {record_name, user_info}, {attributes, record_info(fields, user_info)}, diff --git a/apps/emqx_gateway/src/emqx_gateway_cm.erl b/apps/emqx_gateway/src/emqx_gateway_cm.erl index 5cba1464a..4719b1da8 100644 --- a/apps/emqx_gateway/src/emqx_gateway_cm.erl +++ b/apps/emqx_gateway/src/emqx_gateway_cm.erl @@ -768,7 +768,7 @@ init(Options) -> {ChanTab, ConnTab, InfoTab} = cmtabs(GwName), ok = emqx_tables:new(ChanTab, [bag, {read_concurrency, true} | TabOpts]), ok = emqx_tables:new(ConnTab, [bag | TabOpts]), - ok = emqx_tables:new(InfoTab, [set, compressed | TabOpts]), + ok = emqx_tables:new(InfoTab, [ordered_set, compressed | TabOpts]), %% Start link cm-registry process %% XXX: Should I hang it under a higher level supervisor? diff --git a/apps/emqx_rule_engine/src/emqx_rule_engine_app.erl b/apps/emqx_rule_engine/src/emqx_rule_engine_app.erl index d7ea6c6d1..14d2b1f95 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_engine_app.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_engine_app.erl @@ -25,7 +25,7 @@ -export([stop/1]). start(_Type, _Args) -> - _ = ets:new(?RULE_TAB, [named_table, public, set, {read_concurrency, true}]), + _ = ets:new(?RULE_TAB, [named_table, public, ordered_set, {read_concurrency, true}]), ok = emqx_rule_events:reload(), SupRet = emqx_rule_engine_sup:start_link(), ok = emqx_rule_engine:load_rules(), From 34571c779d7743ab5f89d5c6694be402c06260a4 Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Fri, 13 Jan 2023 18:13:59 +0300 Subject: [PATCH 0182/1802] feat: make `suboption` table ordering more natural --- apps/emqx/src/emqx_broker.erl | 30 +++++++++---------- .../test/emqx_auto_subscribe_SUITE.erl | 2 +- .../src/emqx_mgmt_api_subscriptions.erl | 20 ++++++------- apps/emqx_management/src/emqx_mgmt_cli.erl | 4 +-- 4 files changed, 28 insertions(+), 28 deletions(-) diff --git a/apps/emqx/src/emqx_broker.erl b/apps/emqx/src/emqx_broker.erl index ba16895ae..1c31d86c2 100644 --- a/apps/emqx/src/emqx_broker.erl +++ b/apps/emqx/src/emqx_broker.erl @@ -106,7 +106,7 @@ start_link(Pool, Id) -> create_tabs() -> TabOpts = [public, {read_concurrency, true}, {write_concurrency, true}], - %% SubOption: {SubPid, Topic} -> SubOption + %% SubOption: {Topic, SubPid} -> SubOption ok = emqx_tables:new(?SUBOPTION, [ordered_set | TabOpts]), %% Subscription: SubPid -> Topic1, Topic2, Topic3, ... @@ -136,7 +136,7 @@ subscribe(Topic, SubId, SubOpts0) when is_binary(Topic), ?IS_SUBID(SubId), is_ma SubOpts = maps:merge(?DEFAULT_SUBOPTS, SubOpts0), _ = emqx_trace:subscribe(Topic, SubId, SubOpts), SubPid = self(), - case ets:member(?SUBOPTION, {SubPid, Topic}) of + case subscribed(SubPid, Topic) of %% New false -> ok = emqx_broker_helper:register_sub(SubPid, SubId), @@ -164,16 +164,16 @@ do_subscribe(undefined, Topic, SubPid, SubOpts) -> case emqx_broker_helper:get_sub_shard(SubPid, Topic) of 0 -> true = ets:insert(?SUBSCRIBER, {Topic, SubPid}), - true = ets:insert(?SUBOPTION, {{SubPid, Topic}, SubOpts}), + true = ets:insert(?SUBOPTION, {{Topic, SubPid}, SubOpts}), call(pick(Topic), {subscribe, Topic}); I -> true = ets:insert(?SUBSCRIBER, {{shard, Topic, I}, SubPid}), - true = ets:insert(?SUBOPTION, {{SubPid, Topic}, maps:put(shard, I, SubOpts)}), + true = ets:insert(?SUBOPTION, {{Topic, SubPid}, maps:put(shard, I, SubOpts)}), call(pick({Topic, I}), {subscribe, Topic, I}) end; %% Shared subscription do_subscribe(Group, Topic, SubPid, SubOpts) -> - true = ets:insert(?SUBOPTION, {{SubPid, Topic}, SubOpts}), + true = ets:insert(?SUBOPTION, {{Topic, SubPid}, SubOpts}), emqx_shared_sub:subscribe(Group, Topic, SubPid). %%-------------------------------------------------------------------- @@ -183,7 +183,7 @@ do_subscribe(Group, Topic, SubPid, SubOpts) -> -spec unsubscribe(emqx_types:topic()) -> ok. unsubscribe(Topic) when is_binary(Topic) -> SubPid = self(), - case ets:lookup(?SUBOPTION, {SubPid, Topic}) of + case ets:lookup(?SUBOPTION, {Topic, SubPid}) of [{_, SubOpts}] -> _ = emqx_broker_helper:reclaim_seq(Topic), _ = emqx_trace:unsubscribe(Topic, SubOpts), @@ -193,7 +193,7 @@ unsubscribe(Topic) when is_binary(Topic) -> end. do_unsubscribe(Topic, SubPid, SubOpts) -> - true = ets:delete(?SUBOPTION, {SubPid, Topic}), + true = ets:delete(?SUBOPTION, {Topic, SubPid}), true = ets:delete_object(?SUBSCRIPTION, {SubPid, Topic}), Group = maps:get(share, SubOpts, undefined), do_unsubscribe(Group, Topic, SubPid, SubOpts), @@ -362,10 +362,10 @@ subscribers(Shard = {shard, _Topic, _I}) -> subscriber_down(SubPid) -> lists:foreach( fun(Topic) -> - case lookup_value(?SUBOPTION, {SubPid, Topic}) of + case lookup_value(?SUBOPTION, {Topic, SubPid}) of SubOpts when is_map(SubOpts) -> _ = emqx_broker_helper:reclaim_seq(Topic), - true = ets:delete(?SUBOPTION, {SubPid, Topic}), + true = ets:delete(?SUBOPTION, {Topic, SubPid}), case maps:get(shard, SubOpts, 0) of 0 -> true = ets:delete_object(?SUBSCRIBER, {Topic, SubPid}), @@ -390,7 +390,7 @@ subscriber_down(SubPid) -> [{emqx_types:topic(), emqx_types:subopts()}]. subscriptions(SubPid) when is_pid(SubPid) -> [ - {Topic, lookup_value(?SUBOPTION, {SubPid, Topic}, #{})} + {Topic, lookup_value(?SUBOPTION, {Topic, SubPid}, #{})} || Topic <- lookup_value(?SUBSCRIPTION, SubPid, []) ]; subscriptions(SubId) -> @@ -403,19 +403,19 @@ subscriptions(SubId) -> -spec subscriptions_via_topic(emqx_types:topic()) -> [emqx_types:subopts()]. subscriptions_via_topic(Topic) -> - MatchSpec = [{{{'_', '$1'}, '_'}, [{'=:=', '$1', Topic}], ['$_']}], + MatchSpec = [{{{Topic, '_'}, '_'}, [], ['$_']}], ets:select(?SUBOPTION, MatchSpec). -spec subscribed(pid() | emqx_types:subid(), emqx_types:topic()) -> boolean(). subscribed(SubPid, Topic) when is_pid(SubPid) -> - ets:member(?SUBOPTION, {SubPid, Topic}); + ets:member(?SUBOPTION, {Topic, SubPid}); subscribed(SubId, Topic) when ?IS_SUBID(SubId) -> SubPid = emqx_broker_helper:lookup_subpid(SubId), - ets:member(?SUBOPTION, {SubPid, Topic}). + ets:member(?SUBOPTION, {Topic, SubPid}). -spec get_subopts(pid(), emqx_types:topic()) -> maybe(emqx_types:subopts()). get_subopts(SubPid, Topic) when is_pid(SubPid), is_binary(Topic) -> - lookup_value(?SUBOPTION, {SubPid, Topic}); + lookup_value(?SUBOPTION, {Topic, SubPid}); get_subopts(SubId, Topic) when ?IS_SUBID(SubId) -> case emqx_broker_helper:lookup_subpid(SubId) of SubPid when is_pid(SubPid) -> @@ -430,7 +430,7 @@ set_subopts(Topic, NewOpts) when is_binary(Topic), is_map(NewOpts) -> %% @private set_subopts(SubPid, Topic, NewOpts) -> - Sub = {SubPid, Topic}, + Sub = {Topic, SubPid}, case ets:lookup(?SUBOPTION, Sub) of [{_, OldOpts}] -> ets:insert(?SUBOPTION, {Sub, maps:merge(OldOpts, NewOpts)}); diff --git a/apps/emqx_auto_subscribe/test/emqx_auto_subscribe_SUITE.erl b/apps/emqx_auto_subscribe/test/emqx_auto_subscribe_SUITE.erl index 900f39ebb..5c5a3ee79 100644 --- a/apps/emqx_auto_subscribe/test/emqx_auto_subscribe_SUITE.erl +++ b/apps/emqx_auto_subscribe/test/emqx_auto_subscribe_SUITE.erl @@ -204,7 +204,7 @@ check_subs(Count) -> check_subs([], []) -> ok; -check_subs([{{_, Topic}, #{subid := ?CLIENT_ID}} | Subs], List) -> +check_subs([{{Topic, _}, #{subid := ?CLIENT_ID}} | Subs], List) -> check_subs(Subs, lists:delete(Topic, List)); check_subs([_ | Subs], List) -> check_subs(Subs, List). diff --git a/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl b/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl index b3380f4d1..bf84d03d5 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl @@ -173,7 +173,7 @@ subscriptions(get, #{query_string := QString}) -> {200, Result} end. -format(WhichNode, {{_Subscriber, Topic}, Options}) -> +format(WhichNode, {{Topic, _Subscriber}, Options}) -> maps:merge( #{ topic => get_topic(Topic, Options), @@ -205,14 +205,14 @@ gen_match_spec([], MtchHead) -> gen_match_spec([{Key, '=:=', Value} | More], MtchHead) -> gen_match_spec(More, update_ms(Key, Value, MtchHead)). -update_ms(clientid, X, {{Pid, Topic}, Opts}) -> - {{Pid, Topic}, Opts#{subid => X}}; -update_ms(topic, X, {{Pid, _Topic}, Opts}) -> - {{Pid, X}, Opts}; -update_ms(share_group, X, {{Pid, Topic}, Opts}) -> - {{Pid, Topic}, Opts#{share => X}}; -update_ms(qos, X, {{Pid, Topic}, Opts}) -> - {{Pid, Topic}, Opts#{qos => X}}. +update_ms(clientid, X, {{Topic, Pid}, Opts}) -> + {{Topic, Pid}, Opts#{subid => X}}; +update_ms(topic, X, {{_Topic, Pid}, Opts}) -> + {{X, Pid}, Opts}; +update_ms(share_group, X, {{Topic, Pid}, Opts}) -> + {{Topic, Pid}, Opts#{share => X}}; +update_ms(qos, X, {{Topic, Pid}, Opts}) -> + {{Topic, Pid}, Opts#{qos => X}}. fuzzy_filter_fun([]) -> undefined; @@ -221,5 +221,5 @@ fuzzy_filter_fun(Fuzzy) -> run_fuzzy_filter(_, []) -> true; -run_fuzzy_filter(E = {{_, Topic}, _}, [{topic, match, TopicFilter} | Fuzzy]) -> +run_fuzzy_filter(E = {{Topic, _}, _}, [{topic, match, TopicFilter} | Fuzzy]) -> emqx_topic:match(Topic, TopicFilter) andalso run_fuzzy_filter(E, Fuzzy). diff --git a/apps/emqx_management/src/emqx_mgmt_cli.erl b/apps/emqx_management/src/emqx_mgmt_cli.erl index 8d19716ce..0e7506a0b 100644 --- a/apps/emqx_management/src/emqx_mgmt_cli.erl +++ b/apps/emqx_management/src/emqx_mgmt_cli.erl @@ -213,7 +213,7 @@ subscriptions(["show", ClientId]) -> [] -> emqx_ctl:print("Not Found.~n"); [{_, Pid}] -> - case ets:match_object(emqx_suboption, {{Pid, '_'}, '_'}) of + case ets:match_object(emqx_suboption, {{'_', Pid}, '_'}) of [] -> emqx_ctl:print("Not Found.~n"); Suboption -> [print({emqx_suboption, Sub}) || Sub <- Suboption] end @@ -829,7 +829,7 @@ print({emqx_topic, #route{topic = Topic, dest = {_, Node}}}) -> emqx_ctl:print("~ts -> ~ts~n", [Topic, Node]); print({emqx_topic, #route{topic = Topic, dest = Node}}) -> emqx_ctl:print("~ts -> ~ts~n", [Topic, Node]); -print({emqx_suboption, {{Pid, Topic}, Options}}) when is_pid(Pid) -> +print({emqx_suboption, {{Topic, Pid}, Options}}) when is_pid(Pid) -> SubId = maps:get(subid, Options), QoS = maps:get(qos, Options, 0), NL = maps:get(nl, Options, 0), From 10bbf97e033b38d2712609a59fd0e5767bf92c36 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Mon, 16 Jan 2023 13:33:59 +0100 Subject: [PATCH 0183/1802] docs: merge released change logs (v5.0.14) --- changes/v5.0.14-en.md | 12 ++++++++---- changes/v5.0.14-zh.md | 12 ++++++++---- changes/v5.0.14/feat-9593.en.md | 1 - changes/v5.0.14/feat-9593.zh.md | 1 - changes/v5.0.14/feat-9614.en.md | 4 ---- changes/v5.0.14/feat-9614.zh.md | 4 ---- changes/v5.0.14/feat-9642.en.md | 1 - changes/v5.0.14/feat-9642.zh.md | 1 - changes/v5.0.14/feat-9671.en.md | 1 - changes/v5.0.14/feat-9671.zh.md | 1 - changes/v5.0.14/feat-9674.en.md | 1 - changes/v5.0.14/feat-9674.zh.md | 1 - changes/v5.0.14/feat-9675.en.md | 2 -- changes/v5.0.14/feat-9675.zh.md | 2 -- changes/v5.0.14/feat-9691.en.md | 1 - changes/v5.0.14/feat-9691.zh.md | 1 - changes/v5.0.14/feat-9713.en.md | 3 --- changes/v5.0.14/feat-9713.zh.md | 3 --- changes/v5.0.14/fix-9637.en.md | 1 - changes/v5.0.14/fix-9637.zh.md | 1 - changes/v5.0.14/fix-9638.en.md | 1 - changes/v5.0.14/fix-9638.zh.md | 1 - changes/v5.0.14/fix-9641.en.md | 1 - changes/v5.0.14/fix-9641.zh.md | 1 - changes/v5.0.14/fix-9642.en.md | 3 --- changes/v5.0.14/fix-9642.zh.md | 3 --- changes/v5.0.14/fix-9667.en.md | 1 - changes/v5.0.14/fix-9667.zh.md | 1 - changes/v5.0.14/fix-9672.en.md | 1 - changes/v5.0.14/fix-9672.zh.md | 1 - changes/v5.0.14/fix-9678.en.md | 1 - changes/v5.0.14/fix-9678.zh.md | 1 - changes/v5.0.14/fix-9687.en.md | 2 -- changes/v5.0.14/fix-9687.zh.md | 2 -- changes/v5.0.14/fix-9689.en.md | 1 - changes/v5.0.14/fix-9689.zh.md | 1 - changes/v5.0.14/fix-9703.en.md | 3 --- changes/v5.0.14/fix-9703.zh.md | 2 -- changes/v5.0.14/fix-9705.en.md | 8 -------- changes/v5.0.14/fix-9705.zh.md | 5 ----- changes/v5.0.14/fix-9712.en.md | 2 -- changes/v5.0.14/fix-9712.zh.md | 2 -- changes/v5.0.14/fix-9714.en.md | 1 - changes/v5.0.14/fix-9714.zh.md | 1 - changes/v5.0.14/fix-9716.en.md | 1 - changes/v5.0.14/fix-9716.zh.md | 1 - changes/v5.0.14/fix-9717.en.md | 1 - changes/v5.0.14/fix-9717.zh.md | 1 - changes/v5.0.14/fix-9730.en.md | 1 - changes/v5.0.14/fix-9730.zh.md | 1 - changes/v5.0.14/refactor-9653.en.md | 1 - changes/v5.0.14/refactor-9653.zh.md | 1 - 52 files changed, 16 insertions(+), 92 deletions(-) delete mode 100644 changes/v5.0.14/feat-9593.en.md delete mode 100644 changes/v5.0.14/feat-9593.zh.md delete mode 100644 changes/v5.0.14/feat-9614.en.md delete mode 100644 changes/v5.0.14/feat-9614.zh.md delete mode 100644 changes/v5.0.14/feat-9642.en.md delete mode 100644 changes/v5.0.14/feat-9642.zh.md delete mode 100644 changes/v5.0.14/feat-9671.en.md delete mode 100644 changes/v5.0.14/feat-9671.zh.md delete mode 100644 changes/v5.0.14/feat-9674.en.md delete mode 100644 changes/v5.0.14/feat-9674.zh.md delete mode 100644 changes/v5.0.14/feat-9675.en.md delete mode 100644 changes/v5.0.14/feat-9675.zh.md delete mode 100644 changes/v5.0.14/feat-9691.en.md delete mode 100644 changes/v5.0.14/feat-9691.zh.md delete mode 100644 changes/v5.0.14/feat-9713.en.md delete mode 100644 changes/v5.0.14/feat-9713.zh.md delete mode 100644 changes/v5.0.14/fix-9637.en.md delete mode 100644 changes/v5.0.14/fix-9637.zh.md delete mode 100644 changes/v5.0.14/fix-9638.en.md delete mode 100644 changes/v5.0.14/fix-9638.zh.md delete mode 100644 changes/v5.0.14/fix-9641.en.md delete mode 100644 changes/v5.0.14/fix-9641.zh.md delete mode 100644 changes/v5.0.14/fix-9642.en.md delete mode 100644 changes/v5.0.14/fix-9642.zh.md delete mode 100644 changes/v5.0.14/fix-9667.en.md delete mode 100644 changes/v5.0.14/fix-9667.zh.md delete mode 100644 changes/v5.0.14/fix-9672.en.md delete mode 100644 changes/v5.0.14/fix-9672.zh.md delete mode 100644 changes/v5.0.14/fix-9678.en.md delete mode 100644 changes/v5.0.14/fix-9678.zh.md delete mode 100644 changes/v5.0.14/fix-9687.en.md delete mode 100644 changes/v5.0.14/fix-9687.zh.md delete mode 100644 changes/v5.0.14/fix-9689.en.md delete mode 100644 changes/v5.0.14/fix-9689.zh.md delete mode 100644 changes/v5.0.14/fix-9703.en.md delete mode 100644 changes/v5.0.14/fix-9703.zh.md delete mode 100644 changes/v5.0.14/fix-9705.en.md delete mode 100644 changes/v5.0.14/fix-9705.zh.md delete mode 100644 changes/v5.0.14/fix-9712.en.md delete mode 100644 changes/v5.0.14/fix-9712.zh.md delete mode 100644 changes/v5.0.14/fix-9714.en.md delete mode 100644 changes/v5.0.14/fix-9714.zh.md delete mode 100644 changes/v5.0.14/fix-9716.en.md delete mode 100644 changes/v5.0.14/fix-9716.zh.md delete mode 100644 changes/v5.0.14/fix-9717.en.md delete mode 100644 changes/v5.0.14/fix-9717.zh.md delete mode 100644 changes/v5.0.14/fix-9730.en.md delete mode 100644 changes/v5.0.14/fix-9730.zh.md delete mode 100644 changes/v5.0.14/refactor-9653.en.md delete mode 100644 changes/v5.0.14/refactor-9653.zh.md diff --git a/changes/v5.0.14-en.md b/changes/v5.0.14-en.md index 17ae121cb..b54877c3a 100644 --- a/changes/v5.0.14-en.md +++ b/changes/v5.0.14-en.md @@ -2,8 +2,6 @@ ## Enhancements -- [#8329](https://github.com/emqx/emqx/pull/8329) The MongoDB library has been upgraded to support MongoDB 5.1+ - - [#9593](https://github.com/emqx/emqx/pull/9593) Obfuscated sensitive data in the response when querying `bridges` information by API. - [#9614](https://github.com/emqx/emqx/pull/9614) Make possible to configure `host:port` from environment variables without quotes. @@ -20,14 +18,14 @@ - [#9675](https://github.com/emqx/emqx/pull/9675) HTTP client library `ehttpc` upgraded from `0.4.2` to `0.4.3`. Library `eredis_cluster` which manages clients to redis clusters upgraded from `0.7.1` to `0.7.5`. +- [#9691](https://github.com/emqx/emqx/pull/9691) The MongoDB library has been upgraded to support MongoDB 5.1+ + - [#9713](https://github.com/emqx/emqx/pull/9713) Introduce `api_key.bootstrap_file` to initialize the api key at boot time. Deprecate `dashboard.bootstrap_users_file`. Limit the maximum number of api keys to 100 instead of 30. ## Bug fixes -- [#8648](https://github.com/emqx/emqx/pull/8648) When deleting a non-existing bridge the server gave a success response. This has been fixed so that the server instead gives an error response when the user attempts to delete a non-existing bridge. - - [#9637](https://github.com/emqx/emqx/pull/9637) Fix the expiry_interval fields of the clients HTTP API to measure in seconds. - [#9638](https://github.com/emqx/emqx/pull/9638) Fix the problem of data loss and bad match when the MySQL driver is disconnected. @@ -40,6 +38,10 @@ - [#9667](https://github.com/emqx/emqx/pull/9667) Remove possibility to set `clientid` for `/publish` and `/publish/bulk` HTTP APIs. This is to reduce the risk for security confusion. +- [#9672](https://github.com/emqx/emqx/pull/9672) Fix the problem that the bridge is not available when the Payload template is empty in the MQTT bridge. + +- [#9678](https://github.com/emqx/emqx/pull/9678) When deleting a non-existing bridge the server gave a success response. This has been fixed so that the server instead gives an error response when the user attempts to delete a non-existing bridge. + - [#9687](https://github.com/emqx/emqx/pull/9687) Fix the problem that sending messages to data-bridges failed because of incorrect handling of some data-bridges without `local_topic` field configured. Before this change, if some bridges have configured the `local_topic` field but others have not, a `function_clause` error will occur when forwarding messages to the data-bridges. @@ -66,3 +68,5 @@ - [#9716](https://github.com/emqx/emqx/pull/9716) MQTT bridge config compatibility fix. The config created from before v5.0.12 may encounter a compatibility issue after upgraded to v5.0.13. - [#9717](https://github.com/emqx/emqx/pull/9717) Prior to this fix, if it always times out when trying to connect a bridge server, it's not possible to change other configs even when the bridge is disabled. + +- [#9730](https://github.com/emqx/emqx/pull/9730) Potential leaks of atoms that could lead to a crash if a lot of resources were created have been removed. diff --git a/changes/v5.0.14-zh.md b/changes/v5.0.14-zh.md index 4eb510a43..a6e03efce 100644 --- a/changes/v5.0.14-zh.md +++ b/changes/v5.0.14-zh.md @@ -2,8 +2,6 @@ ## 增强 -- [#8329](https://github.com/emqx/emqx/pull/8329) MongoDB 的驱动现在已经升级到 MongoDB 5.1+ 了。 - - [#9593](https://github.com/emqx/emqx/pull/9593) 通过 API 查询 `bridges` 信息时将混淆响应中的敏感数据。 - [#9614](https://github.com/emqx/emqx/pull/9614) 允许环境变量重载 `host:port` 值时不使用引号。 @@ -20,14 +18,14 @@ - [#9675](https://github.com/emqx/emqx/pull/9675) HTTP 客户端库 `ehttpc` 从 `0.4.2` 升级到 `0.4.3` Redis cluster 客户端库 `eredis_cluster` 从 `0.7.1` 升级到 `0.7.5`. +- [#9691](https://github.com/emqx/emqx/pull/9691) MongoDB 的驱动现在已经升级到 MongoDB 5.1+ 了。 + - [#9713](https://github.com/emqx/emqx/pull/9713) 引入 `api_key.bootstrap_file`,用于启动时初始化api密钥。 废弃 `dashboard.boostrap_users_file`。 将 API 密钥的最大数量限制提升为 100(原来为30)。 ## 修复 -- [#8648](https://github.com/emqx/emqx/pull/8648) 修复了当通过 API 删除一个不存在的桥接时,服务器会返回操作成功的问题,现在将会返回操作失败的信息。 - - [#9637](https://github.com/emqx/emqx/pull/9637) 修复 clients HTTP API 下的 expiry_interval 字段的时间单位为秒。 - [#9638](https://github.com/emqx/emqx/pull/9638) 修复 MySQL 驱动断开连接时出现的数据丢失和匹配错误的问题。 @@ -40,6 +38,10 @@ - [#9667](https://github.com/emqx/emqx/pull/9667) 从 HTTP API /publish 和 /publish/bulk 中移除 clientid, 降低安全风险 +- [#9672](https://github.com/emqx/emqx/pull/9672) 修复 MQTT 桥接中 Payload 模板为空时桥接不可用的问题。 + +- [#9678](https://github.com/emqx/emqx/pull/9678) 修复了当通过 API 删除一个不存在的桥接时,服务器会返回操作成功的问题,现在将会返回操作失败的信息。 + - [#9687](https://github.com/emqx/emqx/pull/9687) 修复由于某些数据桥接未配置 `local_topic` 字段,导致的所有数据桥接无法发送消息。 在此改动之前,如果有些桥接设置了 `local_topic` 字段而有些没有设置,数据桥接转发消息时会出现 `function_clause` 的错误。 @@ -62,3 +64,5 @@ - [#9716](https://github.com/emqx/emqx/pull/9716) 修复 v5.0.12 之前的 MQTT 桥接配置在 升级到 v5.0.13 后 HTTP API 查询 桥接配置时的一个兼容性问题。 - [#9717](https://github.com/emqx/emqx/pull/9717) 修复已禁用的桥接资源服务器连接超时的情况下不能修改其他配置参数的问题。 + +- [#9730](https://github.com/emqx/emqx/pull/9730) 如果创建了大量的资源,可能会导致崩溃的潜在的原子泄漏已经被删除。 diff --git a/changes/v5.0.14/feat-9593.en.md b/changes/v5.0.14/feat-9593.en.md deleted file mode 100644 index d6db43a79..000000000 --- a/changes/v5.0.14/feat-9593.en.md +++ /dev/null @@ -1 +0,0 @@ -Obfuscated sensitive data in the response when querying `bridges` information by API. diff --git a/changes/v5.0.14/feat-9593.zh.md b/changes/v5.0.14/feat-9593.zh.md deleted file mode 100644 index fd3133050..000000000 --- a/changes/v5.0.14/feat-9593.zh.md +++ /dev/null @@ -1 +0,0 @@ -通过 API 查询 `bridges` 信息时将混淆响应中的敏感数据。 diff --git a/changes/v5.0.14/feat-9614.en.md b/changes/v5.0.14/feat-9614.en.md deleted file mode 100644 index 2ccf62c76..000000000 --- a/changes/v5.0.14/feat-9614.en.md +++ /dev/null @@ -1,4 +0,0 @@ -Make possible to configure `host:port` from environment variables without quotes. -Prior to this change, when overriding a `host:port` config value from environment variable, one has to quote it as: -`env EMQX_BRIDGES__MQTT__XYZ__SERVER='"localhost:1883"'`. -Now it's possible to set it without quote as `env EMQX_BRIDGES__MQTT__XYZ__SERVER='localhost:1883'`. diff --git a/changes/v5.0.14/feat-9614.zh.md b/changes/v5.0.14/feat-9614.zh.md deleted file mode 100644 index 7f67d92af..000000000 --- a/changes/v5.0.14/feat-9614.zh.md +++ /dev/null @@ -1,4 +0,0 @@ -允许环境变量重载 `host:port` 值时不使用引号。 -在此修复前,环境变量中使用 `host:port` 这种配置时,用户必须使用引号,例如: -`env EMQX_BRIDGES__MQTT__XYZ__SERVER='"localhost:1883"'`。 -此修复后,可以不使用引号,例如 `env EMQX_BRIDGES__MQTT__XYZ__SERVER='localhost:1883'`。 diff --git a/changes/v5.0.14/feat-9642.en.md b/changes/v5.0.14/feat-9642.en.md deleted file mode 100644 index 19de4d946..000000000 --- a/changes/v5.0.14/feat-9642.en.md +++ /dev/null @@ -1 +0,0 @@ -Deprecates `enable_batch` and `enable_queue` options for bridges/resources. After this change, queuing is always enabled for bridges, and batching is controlled by the `batch_size` option: `batch_size > 1` means batching will be enabled. diff --git a/changes/v5.0.14/feat-9642.zh.md b/changes/v5.0.14/feat-9642.zh.md deleted file mode 100644 index e394abdbb..000000000 --- a/changes/v5.0.14/feat-9642.zh.md +++ /dev/null @@ -1 +0,0 @@ -废弃了桥接的 `enable_batch` 和 `enable_queue` 配置项 。在这一改变之后,桥接的工作进程总是启用缓存队列,而批处理由 `batch_size` 选项控制:`batch_size > 1` 则意味着启用批处理。 diff --git a/changes/v5.0.14/feat-9671.en.md b/changes/v5.0.14/feat-9671.en.md deleted file mode 100644 index dd5ed5e4d..000000000 --- a/changes/v5.0.14/feat-9671.en.md +++ /dev/null @@ -1 +0,0 @@ -Implement sliding window average metrics. diff --git a/changes/v5.0.14/feat-9671.zh.md b/changes/v5.0.14/feat-9671.zh.md deleted file mode 100644 index cbcae01fe..000000000 --- a/changes/v5.0.14/feat-9671.zh.md +++ /dev/null @@ -1 +0,0 @@ -实施滑动窗口平均度量。 diff --git a/changes/v5.0.14/feat-9674.en.md b/changes/v5.0.14/feat-9674.en.md deleted file mode 100644 index a87725b43..000000000 --- a/changes/v5.0.14/feat-9674.en.md +++ /dev/null @@ -1 +0,0 @@ -Made rule engine behavior more consistent with bridge behavior regarding metrics: if a rule engine is disabled, its metrics are now reset diff --git a/changes/v5.0.14/feat-9674.zh.md b/changes/v5.0.14/feat-9674.zh.md deleted file mode 100644 index c9d776525..000000000 --- a/changes/v5.0.14/feat-9674.zh.md +++ /dev/null @@ -1 +0,0 @@ -使得规则引擎的行为与桥梁的指标行为更加一致:如果一个规则引擎被禁用,其指标现在会被重置。 diff --git a/changes/v5.0.14/feat-9675.en.md b/changes/v5.0.14/feat-9675.en.md deleted file mode 100644 index 5249e9826..000000000 --- a/changes/v5.0.14/feat-9675.en.md +++ /dev/null @@ -1,2 +0,0 @@ -HTTP client library `ehttpc` upgraded from `0.4.2` to `0.4.3`. -Library `eredis_cluster` which manages clients to redis clusters upgraded from `0.7.1` to `0.7.5`. diff --git a/changes/v5.0.14/feat-9675.zh.md b/changes/v5.0.14/feat-9675.zh.md deleted file mode 100644 index d14f260ae..000000000 --- a/changes/v5.0.14/feat-9675.zh.md +++ /dev/null @@ -1,2 +0,0 @@ -HTTP 客户端库 `ehttpc` 从 `0.4.2` 升级到 `0.4.3` -Redis cluster 客户端库 `eredis_cluster` 从 `0.7.1` 升级到 `0.7.5`. diff --git a/changes/v5.0.14/feat-9691.en.md b/changes/v5.0.14/feat-9691.en.md deleted file mode 100644 index 2876e1754..000000000 --- a/changes/v5.0.14/feat-9691.en.md +++ /dev/null @@ -1 +0,0 @@ -The MongoDB library has been upgraded to support MongoDB 5.1+ diff --git a/changes/v5.0.14/feat-9691.zh.md b/changes/v5.0.14/feat-9691.zh.md deleted file mode 100644 index 31e73086d..000000000 --- a/changes/v5.0.14/feat-9691.zh.md +++ /dev/null @@ -1 +0,0 @@ -MongoDB 的驱动现在已经升级到 MongoDB 5.1+ 了。 diff --git a/changes/v5.0.14/feat-9713.en.md b/changes/v5.0.14/feat-9713.en.md deleted file mode 100644 index e8dbe4c6c..000000000 --- a/changes/v5.0.14/feat-9713.en.md +++ /dev/null @@ -1,3 +0,0 @@ -Introduce `api_key.bootstrap_file` to initialize the api key at boot time. -Deprecate `dashboard.bootstrap_users_file`. -Limit the maximum number of api keys to 100 instead of 30. diff --git a/changes/v5.0.14/feat-9713.zh.md b/changes/v5.0.14/feat-9713.zh.md deleted file mode 100644 index 7535b8bd5..000000000 --- a/changes/v5.0.14/feat-9713.zh.md +++ /dev/null @@ -1,3 +0,0 @@ -引入 `api_key.bootstrap_file`,用于启动时初始化api密钥。 -废弃 `dashboard.boostrap_users_file`。 -将 API 密钥的最大数量限制提升为 100(原来为30)。 diff --git a/changes/v5.0.14/fix-9637.en.md b/changes/v5.0.14/fix-9637.en.md deleted file mode 100644 index d93ed493c..000000000 --- a/changes/v5.0.14/fix-9637.en.md +++ /dev/null @@ -1 +0,0 @@ -Fix the expiry_interval fields of the clients HTTP API to measure in seconds. diff --git a/changes/v5.0.14/fix-9637.zh.md b/changes/v5.0.14/fix-9637.zh.md deleted file mode 100644 index 8164a0bbf..000000000 --- a/changes/v5.0.14/fix-9637.zh.md +++ /dev/null @@ -1 +0,0 @@ -修复 clients HTTP API 下的 expiry_interval 字段的时间单位为秒。 diff --git a/changes/v5.0.14/fix-9638.en.md b/changes/v5.0.14/fix-9638.en.md deleted file mode 100644 index 7a9265fb4..000000000 --- a/changes/v5.0.14/fix-9638.en.md +++ /dev/null @@ -1 +0,0 @@ -Fix the problem of data loss and bad match when the MySQL driver is disconnected. diff --git a/changes/v5.0.14/fix-9638.zh.md b/changes/v5.0.14/fix-9638.zh.md deleted file mode 100644 index 6ae2a3c1e..000000000 --- a/changes/v5.0.14/fix-9638.zh.md +++ /dev/null @@ -1 +0,0 @@ -修复 MySQL 驱动断开连接时出现的数据丢失和匹配错误的问题。 diff --git a/changes/v5.0.14/fix-9641.en.md b/changes/v5.0.14/fix-9641.en.md deleted file mode 100644 index 29924de62..000000000 --- a/changes/v5.0.14/fix-9641.en.md +++ /dev/null @@ -1 +0,0 @@ -Fix an issue where testing the GCP PubSub could leak memory, and an issue where its JWT token would fail to refresh a second time. diff --git a/changes/v5.0.14/fix-9641.zh.md b/changes/v5.0.14/fix-9641.zh.md deleted file mode 100644 index 7b9e6d55c..000000000 --- a/changes/v5.0.14/fix-9641.zh.md +++ /dev/null @@ -1 +0,0 @@ -修复了测试GCP PubSub可能泄露内存的问题,以及其JWT令牌第二次刷新失败的问题。 diff --git a/changes/v5.0.14/fix-9642.en.md b/changes/v5.0.14/fix-9642.en.md deleted file mode 100644 index 12906299e..000000000 --- a/changes/v5.0.14/fix-9642.en.md +++ /dev/null @@ -1,3 +0,0 @@ -Fix some issues that could lead to wrong bridge metrics. -Fix and issue that could lead to message loss and wrong metrics with Kafka Producer bridge when Kafka or the connection to it is down. -Fix some issues that could lead to the same message being delivered more than once when using batching for bridges and when the batch was retried. diff --git a/changes/v5.0.14/fix-9642.zh.md b/changes/v5.0.14/fix-9642.zh.md deleted file mode 100644 index 2565c422c..000000000 --- a/changes/v5.0.14/fix-9642.zh.md +++ /dev/null @@ -1,3 +0,0 @@ -修复一些可能导致错误桥接指标的问题。 -修复当Kafka或其连接中断时,可能导致Kafka Producer桥的消息丢失和错误指标的问题。 -修复一些问题,这些问题可能导致在为桥接使用批处理时,同一消息被多次传递,以及批处理被重试时。 diff --git a/changes/v5.0.14/fix-9667.en.md b/changes/v5.0.14/fix-9667.en.md deleted file mode 100644 index 4b0fe7aef..000000000 --- a/changes/v5.0.14/fix-9667.en.md +++ /dev/null @@ -1 +0,0 @@ -Remove possibility to set `clientid` for `/publish` and `/publish/bulk` HTTP APIs. This is to reduce the risk for security confusion. diff --git a/changes/v5.0.14/fix-9667.zh.md b/changes/v5.0.14/fix-9667.zh.md deleted file mode 100644 index f3952ca14..000000000 --- a/changes/v5.0.14/fix-9667.zh.md +++ /dev/null @@ -1 +0,0 @@ -从 HTTP API /publish 和 /publish/bulk 中移除 clientid, 降低安全风险 diff --git a/changes/v5.0.14/fix-9672.en.md b/changes/v5.0.14/fix-9672.en.md deleted file mode 100644 index 01724801e..000000000 --- a/changes/v5.0.14/fix-9672.en.md +++ /dev/null @@ -1 +0,0 @@ -Fix the problem that the bridge is not available when the Payload template is empty in the MQTT bridge. diff --git a/changes/v5.0.14/fix-9672.zh.md b/changes/v5.0.14/fix-9672.zh.md deleted file mode 100644 index 86106cc1d..000000000 --- a/changes/v5.0.14/fix-9672.zh.md +++ /dev/null @@ -1 +0,0 @@ -修复 MQTT 桥接中 Payload 模板为空时桥接不可用的问题。 diff --git a/changes/v5.0.14/fix-9678.en.md b/changes/v5.0.14/fix-9678.en.md deleted file mode 100644 index ac608a2e1..000000000 --- a/changes/v5.0.14/fix-9678.en.md +++ /dev/null @@ -1 +0,0 @@ -When deleting a non-existing bridge the server gave a success response. This has been fixed so that the server instead gives an error response when the user attempts to delete a non-existing bridge. diff --git a/changes/v5.0.14/fix-9678.zh.md b/changes/v5.0.14/fix-9678.zh.md deleted file mode 100644 index 6512c5aa5..000000000 --- a/changes/v5.0.14/fix-9678.zh.md +++ /dev/null @@ -1 +0,0 @@ -修复了当通过 API 删除一个不存在的桥接时,服务器会返回操作成功的问题,现在将会返回操作失败的信息。 diff --git a/changes/v5.0.14/fix-9687.en.md b/changes/v5.0.14/fix-9687.en.md deleted file mode 100644 index 6c2f53bb3..000000000 --- a/changes/v5.0.14/fix-9687.en.md +++ /dev/null @@ -1,2 +0,0 @@ -Fix the problem that sending messages to data-bridges failed because of incorrect handling of some data-bridges without `local_topic` field configured. -Before this change, if some bridges have configured the `local_topic` field but others have not, a `function_clause` error will occur when forwarding messages to the data-bridges. diff --git a/changes/v5.0.14/fix-9687.zh.md b/changes/v5.0.14/fix-9687.zh.md deleted file mode 100644 index 089db5986..000000000 --- a/changes/v5.0.14/fix-9687.zh.md +++ /dev/null @@ -1,2 +0,0 @@ -修复由于某些数据桥接未配置 `local_topic` 字段,导致的所有数据桥接无法发送消息。 -在此改动之前,如果有些桥接设置了 `local_topic` 字段而有些没有设置,数据桥接转发消息时会出现 `function_clause` 的错误。 diff --git a/changes/v5.0.14/fix-9689.en.md b/changes/v5.0.14/fix-9689.en.md deleted file mode 100644 index 7582c8bc5..000000000 --- a/changes/v5.0.14/fix-9689.en.md +++ /dev/null @@ -1 +0,0 @@ -Fix handling of HTTP authorization result when a request failure (e.g.: HTTP resource is down) would cause a `function_clause` error. diff --git a/changes/v5.0.14/fix-9689.zh.md b/changes/v5.0.14/fix-9689.zh.md deleted file mode 100644 index 62f4a90fb..000000000 --- a/changes/v5.0.14/fix-9689.zh.md +++ /dev/null @@ -1 +0,0 @@ -修正当请求失败(如:HTTP资源关闭)会导致`function_clause`错误时对HTTP授权结果的处理。 diff --git a/changes/v5.0.14/fix-9703.en.md b/changes/v5.0.14/fix-9703.en.md deleted file mode 100644 index 4eb91c7d0..000000000 --- a/changes/v5.0.14/fix-9703.en.md +++ /dev/null @@ -1,3 +0,0 @@ -Set the default value of the `qos` field of the HTTP API `/clients/:clientid/subscribe` to 0. -Before this fix, the `qos` field have no default value, which leads to a `function_clause` error -when querying this API. diff --git a/changes/v5.0.14/fix-9703.zh.md b/changes/v5.0.14/fix-9703.zh.md deleted file mode 100644 index 863304a66..000000000 --- a/changes/v5.0.14/fix-9703.zh.md +++ /dev/null @@ -1,2 +0,0 @@ -将 HTTP 接口 `/clients/:clientid/subscribe` 的 `qos` 字段的默认值设置为 0。 -在此修复之前,`qos` 字段没有默认值,调用订阅接口的时候将导致 `function_clause` 错误。 diff --git a/changes/v5.0.14/fix-9705.en.md b/changes/v5.0.14/fix-9705.en.md deleted file mode 100644 index 479d3d4ea..000000000 --- a/changes/v5.0.14/fix-9705.en.md +++ /dev/null @@ -1,8 +0,0 @@ -Remove the default value of Webhook. -Before this repair, the default value of the `body` field of Webhook is `${payload}`, -but there is no `payload` field in the available fields of other events except message -publishing in the rule, so in this case, the webhook will send a string with the -message body as "undefined" to the HTTP service. -This fix removes the default value of the `body` field. When the `body` field is -not configured, Webhook will send all available fields of the current event in -the format of JSON object. diff --git a/changes/v5.0.14/fix-9705.zh.md b/changes/v5.0.14/fix-9705.zh.md deleted file mode 100644 index 6a57eba05..000000000 --- a/changes/v5.0.14/fix-9705.zh.md +++ /dev/null @@ -1,5 +0,0 @@ -删除 Webhook 的默认值。 -在此修复之前,Webhook 的 `body` 字段的默认值为 `${payload}`,但规则中除了消息发布之外的其他事件的可用字段中 -都没有 `payload` 字段,所以这种情况下 Webhook 将发送消息正文为 "undefined" 的字符串到 HTTP 服务。 -此修复移除了 `body` 字段的默认值,当未配置 `body` 字段的时候,Webhook 将以 JSON object 的格式发送 -当前事件的全部可用字段。 diff --git a/changes/v5.0.14/fix-9712.en.md b/changes/v5.0.14/fix-9712.en.md deleted file mode 100644 index e110b03e2..000000000 --- a/changes/v5.0.14/fix-9712.en.md +++ /dev/null @@ -1,2 +0,0 @@ -Fixed the problem of '404 Not Found' when calling the HTTP API '/clients/:clientid/subscribe/bulk' -from the plug-ins and data-bridges on handling the 'client.connected' event. diff --git a/changes/v5.0.14/fix-9712.zh.md b/changes/v5.0.14/fix-9712.zh.md deleted file mode 100644 index 053f6b08c..000000000 --- a/changes/v5.0.14/fix-9712.zh.md +++ /dev/null @@ -1,2 +0,0 @@ -修复了监听 `client.connected` 事件的插件和数据桥接在调用 `/clients/:clientid/subscribe/bulk` -HTTP 接口时报 `404 Not Found` 的问题。 diff --git a/changes/v5.0.14/fix-9714.en.md b/changes/v5.0.14/fix-9714.en.md deleted file mode 100644 index e1a606744..000000000 --- a/changes/v5.0.14/fix-9714.en.md +++ /dev/null @@ -1 +0,0 @@ -Fix `/mqtt/auto_subscribe` API's bad swagger schema, and make sure swagger always checks if the schema is correct. diff --git a/changes/v5.0.14/fix-9714.zh.md b/changes/v5.0.14/fix-9714.zh.md deleted file mode 100644 index cbf38f041..000000000 --- a/changes/v5.0.14/fix-9714.zh.md +++ /dev/null @@ -1 +0,0 @@ -修复 `/mqtt/auto_subscribe` API 错误的 swagger 格式,并且保证 swagger 总是检查格式是否正确。 diff --git a/changes/v5.0.14/fix-9716.en.md b/changes/v5.0.14/fix-9716.en.md deleted file mode 100644 index 93d4f1823..000000000 --- a/changes/v5.0.14/fix-9716.en.md +++ /dev/null @@ -1 +0,0 @@ -MQTT bridge config compatibility fix. The config created from before v5.0.12 may encounter a compatibility issue after upgraded to v5.0.13. diff --git a/changes/v5.0.14/fix-9716.zh.md b/changes/v5.0.14/fix-9716.zh.md deleted file mode 100644 index f368fe325..000000000 --- a/changes/v5.0.14/fix-9716.zh.md +++ /dev/null @@ -1 +0,0 @@ -修复 v5.0.12 之前的 MQTT 桥接配置在 升级到 v5.0.13 后 HTTP API 查询 桥接配置时的一个兼容性问题。 diff --git a/changes/v5.0.14/fix-9717.en.md b/changes/v5.0.14/fix-9717.en.md deleted file mode 100644 index 9a3b29157..000000000 --- a/changes/v5.0.14/fix-9717.en.md +++ /dev/null @@ -1 +0,0 @@ -Prior to this fix, if it always times out when trying to connect a bridge server, it's not possible to change other configs even when the bridge is disabled. diff --git a/changes/v5.0.14/fix-9717.zh.md b/changes/v5.0.14/fix-9717.zh.md deleted file mode 100644 index 859d7806f..000000000 --- a/changes/v5.0.14/fix-9717.zh.md +++ /dev/null @@ -1 +0,0 @@ -修复已禁用的桥接资源服务器连接超时的情况下不能修改其他配置参数的问题。 diff --git a/changes/v5.0.14/fix-9730.en.md b/changes/v5.0.14/fix-9730.en.md deleted file mode 100644 index f926d6e51..000000000 --- a/changes/v5.0.14/fix-9730.en.md +++ /dev/null @@ -1 +0,0 @@ -Potential leaks of atoms that could lead to a crash if a lot of resources were created have been removed. diff --git a/changes/v5.0.14/fix-9730.zh.md b/changes/v5.0.14/fix-9730.zh.md deleted file mode 100644 index e2138d654..000000000 --- a/changes/v5.0.14/fix-9730.zh.md +++ /dev/null @@ -1 +0,0 @@ -如果创建了大量的资源,可能会导致崩溃的潜在的原子泄漏已经被删除。 diff --git a/changes/v5.0.14/refactor-9653.en.md b/changes/v5.0.14/refactor-9653.en.md deleted file mode 100644 index 2807f81d5..000000000 --- a/changes/v5.0.14/refactor-9653.en.md +++ /dev/null @@ -1 +0,0 @@ -Make authorization config validation error message more readable. diff --git a/changes/v5.0.14/refactor-9653.zh.md b/changes/v5.0.14/refactor-9653.zh.md deleted file mode 100644 index 755fd1683..000000000 --- a/changes/v5.0.14/refactor-9653.zh.md +++ /dev/null @@ -1 +0,0 @@ -改进授权配置检查错误日志的可读性。 From e668044ebc37bee18584356fc6fe40863fb90a42 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Mon, 16 Jan 2023 08:56:27 +0100 Subject: [PATCH 0184/1802] fix: version number previs in restricted shell --- .github/workflows/run_relup_tests.yaml | 3 +-- apps/emqx/src/emqx_release.erl | 22 ++++++++++++++++++---- rebar.config.erl | 7 ++++++- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/.github/workflows/run_relup_tests.yaml b/.github/workflows/run_relup_tests.yaml index 0cf7e51ee..d8ec38849 100644 --- a/.github/workflows/run_relup_tests.yaml +++ b/.github/workflows/run_relup_tests.yaml @@ -9,7 +9,6 @@ on: branches: - '**' tags: - - v* - e* pull_request: @@ -43,7 +42,7 @@ jobs: run: | set -x cd emqx - make emqx-tgz + export PROFILE='emqx-enterprise' make emqx-enterprise-tgz - uses: actions/upload-artifact@v3 name: Upload built emqx and test scenario diff --git a/apps/emqx/src/emqx_release.erl b/apps/emqx/src/emqx_release.erl index 94a0bd9a1..4ecf8598b 100644 --- a/apps/emqx/src/emqx_release.erl +++ b/apps/emqx/src/emqx_release.erl @@ -31,11 +31,21 @@ ce => "EMQX" }). +-define(EMQX_REL_NAME, #{ + ee => <<"Enterprise">>, + ce => <<"Opensource">> +}). + -define(EMQX_REL_VSNS, #{ ee => ?EMQX_RELEASE_EE, ce => ?EMQX_RELEASE_CE }). +-define(EMQX_REL_VSN_PREFIX, #{ + ee => "e", + ce => "v" +}). + %% @doc Return EMQX description. description() -> maps:get(edition(), ?EMQX_DESCS). @@ -46,14 +56,18 @@ description() -> -spec edition() -> ce | ee. -ifdef(EMQX_RELEASE_EDITION). edition() -> ?EMQX_RELEASE_EDITION. -edition_vsn_prefix() -> "e". -edition_longstr() -> <<"Enterprise">>. -else. edition() -> ce. -edition_vsn_prefix() -> "v". -edition_longstr() -> <<"Opensource">>. -endif. +%% @doc Return EMQX version prefix string. +edition_vsn_prefix() -> + maps:get(edition(), ?EMQX_REL_VSN_PREFIX). + +%% @doc Return EMQX edition name, ee => Enterprise ce => Opensource. +edition_longstr() -> + maps:get(edition(), ?EMQX_REL_NAME). + %% @doc Return the release version. version() -> case lists:keyfind(emqx_vsn, 1, ?MODULE:module_info(compile)) of diff --git a/rebar.config.erl b/rebar.config.erl index e8b1271f2..4ff94bd78 100644 --- a/rebar.config.erl +++ b/rebar.config.erl @@ -205,7 +205,12 @@ prod_overrides() -> [{add, [{erl_opts, [deterministic]}]}]. profiles() -> - profiles_ce() ++ profiles_ee() ++ profiles_dev(). + case get_edition_from_profile_env() of + ee -> + profiles_ee(); + ce -> + profiles_ce() + end ++ profiles_dev(). profiles_ce() -> Vsn = get_vsn(emqx), From f50b660ceabceaf955126dfd35335de65c2e96da Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Mon, 16 Jan 2023 11:57:21 +0100 Subject: [PATCH 0185/1802] ci: ensure PROFILE env is set for elixir apps check --- .github/workflows/elixir_apps_check.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/elixir_apps_check.yaml b/.github/workflows/elixir_apps_check.yaml index 62000f421..e912db10f 100644 --- a/.github/workflows/elixir_apps_check.yaml +++ b/.github/workflows/elixir_apps_check.yaml @@ -35,6 +35,7 @@ jobs: run: ./scripts/check-elixir-applications.exs env: MIX_ENV: ${{ matrix.profile }} + PROFILE: ${{ matrix.profile }} # - name: check applications started with emqx_machine # run: ./scripts/check-elixir-emqx-machine-boot-discrepancies.exs # env: From ebcac056ff7ff5709bd554d4359137c63084f955 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Mon, 16 Jan 2023 12:09:51 +0100 Subject: [PATCH 0186/1802] docs: add changelog --- changes/v5.0.15/fix-9769.en.md | 1 + changes/v5.0.15/fix-9769.zh.md | 1 + 2 files changed, 2 insertions(+) create mode 100644 changes/v5.0.15/fix-9769.en.md create mode 100644 changes/v5.0.15/fix-9769.zh.md diff --git a/changes/v5.0.15/fix-9769.en.md b/changes/v5.0.15/fix-9769.en.md new file mode 100644 index 000000000..e07397363 --- /dev/null +++ b/changes/v5.0.15/fix-9769.en.md @@ -0,0 +1 @@ +Fix Erlang shell prompt version prefix. e5.0.15 -> v5.0.15 diff --git a/changes/v5.0.15/fix-9769.zh.md b/changes/v5.0.15/fix-9769.zh.md new file mode 100644 index 000000000..c7e63b862 --- /dev/null +++ b/changes/v5.0.15/fix-9769.zh.md @@ -0,0 +1 @@ +修复 Eralng 控制台版本号前缀的打印错误 e5.0.15 -> v5.0.15 From 0560145771037f46f40967f66ce2215ca7b86dbc Mon Sep 17 00:00:00 2001 From: Ivan Dyachkov Date: Mon, 16 Jan 2023 09:53:38 +0100 Subject: [PATCH 0187/1802] ci: back to use upstream setup-beam action --- .github/workflows/build_packages.yaml | 2 +- .github/workflows/build_slim_packages.yaml | 2 +- .github/workflows/run_fvt_tests.yaml | 4 ++-- .github/workflows/run_jmeter_tests.yaml | 12 ++++++------ .github/workflows/run_relup_tests.yaml | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build_packages.yaml b/.github/workflows/build_packages.yaml index 36deef717..8764c7097 100644 --- a/.github/workflows/build_packages.yaml +++ b/.github/workflows/build_packages.yaml @@ -101,7 +101,7 @@ jobs: - name: unzip source code run: Expand-Archive -Path source.zip -DestinationPath ./ - uses: ilammy/msvc-dev-cmd@v1.12.0 - - uses: emqx/setup-beam@v1.16.1-emqx + - uses: erlef/setup-beam@v1.15.2 with: otp-version: 24.3.4.6 - name: build diff --git a/.github/workflows/build_slim_packages.yaml b/.github/workflows/build_slim_packages.yaml index 0e72232bb..bfdeb1fb8 100644 --- a/.github/workflows/build_slim_packages.yaml +++ b/.github/workflows/build_slim_packages.yaml @@ -94,7 +94,7 @@ jobs: steps: - uses: actions/checkout@v3 - uses: ilammy/msvc-dev-cmd@v1.12.0 - - uses: emqx/setup-beam@v1.16.1-emqx + - uses: erlef/setup-beam@v1.15.2 with: otp-version: ${{ matrix.otp }} - name: build diff --git a/.github/workflows/run_fvt_tests.yaml b/.github/workflows/run_fvt_tests.yaml index a54bb68dd..1a4568725 100644 --- a/.github/workflows/run_fvt_tests.yaml +++ b/.github/workflows/run_fvt_tests.yaml @@ -57,7 +57,7 @@ jobs: arch: - amd64 steps: - - uses: emqx/setup-beam@v1.16.1-emqx + - uses: erlef/setup-beam@v1.15.2 with: otp-version: 24.3.4.6 - uses: actions/download-artifact@v3 @@ -132,7 +132,7 @@ jobs: # - emqx-enterprise # TODO test enterprise steps: - - uses: emqx/setup-beam@v1.16.1-emqx + - uses: erlef/setup-beam@v1.15.2 with: otp-version: 24.3.4.6 - uses: actions/download-artifact@v3 diff --git a/.github/workflows/run_jmeter_tests.yaml b/.github/workflows/run_jmeter_tests.yaml index 81923dba5..7a575b104 100644 --- a/.github/workflows/run_jmeter_tests.yaml +++ b/.github/workflows/run_jmeter_tests.yaml @@ -14,7 +14,7 @@ jobs: outputs: version: ${{ steps.build_docker.outputs.version}} steps: - - uses: emqx/setup-beam@v1.16.1-emqx + - uses: erlef/setup-beam@v1.15.2 with: otp-version: 24.3.4.6 - name: download jmeter @@ -57,7 +57,7 @@ jobs: needs: build_emqx_for_jmeter_tests steps: - - uses: emqx/setup-beam@v1.16.1-emqx + - uses: erlef/setup-beam@v1.15.2 with: otp-version: 24.3.4.6 - uses: actions/checkout@v3 @@ -153,7 +153,7 @@ jobs: needs: build_emqx_for_jmeter_tests steps: - - uses: emqx/setup-beam@v1.16.1-emqx + - uses: erlef/setup-beam@v1.15.2 with: otp-version: 24.3.4.6 - uses: actions/checkout@v3 @@ -259,7 +259,7 @@ jobs: needs: build_emqx_for_jmeter_tests steps: - - uses: emqx/setup-beam@v1.16.1-emqx + - uses: erlef/setup-beam@v1.15.2 with: otp-version: 24.3.4.6 - uses: actions/checkout@v3 @@ -361,7 +361,7 @@ jobs: needs: build_emqx_for_jmeter_tests steps: - - uses: emqx/setup-beam@v1.16.1-emqx + - uses: erlef/setup-beam@v1.15.2 with: otp-version: 24.3.4.6 - uses: actions/checkout@v3 @@ -460,7 +460,7 @@ jobs: needs: build_emqx_for_jmeter_tests steps: - - uses: emqx/setup-beam@v1.16.1-emqx + - uses: erlef/setup-beam@v1.15.2 with: otp-version: 24.3.4.6 - uses: actions/checkout@v3 diff --git a/.github/workflows/run_relup_tests.yaml b/.github/workflows/run_relup_tests.yaml index 0cf7e51ee..baa55150e 100644 --- a/.github/workflows/run_relup_tests.yaml +++ b/.github/workflows/run_relup_tests.yaml @@ -72,7 +72,7 @@ jobs: shell: bash steps: # setup Erlang to run lux - - uses: emqx/setup-beam@v1.16.1-emqx + - uses: erlef/setup-beam@v1.15.2 with: otp-version: 24.3.4.6 - uses: actions/checkout@v3 From 676f017ec0c0ffa80126ddaf781159c96f719df5 Mon Sep 17 00:00:00 2001 From: Ivan Dyachkov Date: Mon, 16 Jan 2023 14:02:16 +0100 Subject: [PATCH 0188/1802] fix: ensure no colon in filenames --- apps/emqx/src/emqx_authentication_config.erl | 4 ++-- apps/emqx/src/emqx_misc.erl | 11 ++++++++++- apps/emqx_bridge/src/emqx_bridge_resource.erl | 3 ++- apps/emqx_resource/src/emqx_resource_worker.erl | 5 +++-- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/apps/emqx/src/emqx_authentication_config.erl b/apps/emqx/src/emqx_authentication_config.erl index 98471e152..300fb4a66 100644 --- a/apps/emqx/src/emqx_authentication_config.erl +++ b/apps/emqx/src/emqx_authentication_config.erl @@ -327,9 +327,9 @@ atom(Bin) -> binary_to_existing_atom(Bin, utf8). certs_dir(ChainName, ConfigOrID) -> DirName = dir(ChainName, ConfigOrID), SubDir = iolist_to_binary(filename:join(["authn", DirName])), - binary:replace(SubDir, <<":">>, <<"-">>, [global]). + emqx_misc:safe_filename(SubDir). dir(ChainName, ID) when is_binary(ID) -> - binary:replace(iolist_to_binary([to_bin(ChainName), "-", ID]), <<":">>, <<"-">>); + emqx_misc:safe_filename(iolist_to_binary([to_bin(ChainName), "-", ID])); dir(ChainName, Config) when is_map(Config) -> dir(ChainName, authenticator_id(Config)). diff --git a/apps/emqx/src/emqx_misc.erl b/apps/emqx/src/emqx_misc.erl index 483b99587..c20227c07 100644 --- a/apps/emqx/src/emqx_misc.erl +++ b/apps/emqx/src/emqx_misc.erl @@ -55,7 +55,8 @@ readable_error_msg/1, safe_to_existing_atom/1, safe_to_existing_atom/2, - pub_props_to_packet/1 + pub_props_to_packet/1, + safe_filename/1 ]). -export([ @@ -708,3 +709,11 @@ pub_props_to_packet(Properties) -> true end, maps:filtermap(F, Properties). + +%% fix filename by replacing characters which could be invalid on some filesystems +%% with safe ones +-spec safe_filename(binary() | unicode:chardata()) -> binary() | [unicode:chardata()]. +safe_filename(Filename) when is_binary(Filename) -> + binary:replace(Filename, <<":">>, <<"-">>, [global]); +safe_filename(Filename) when is_list(Filename) -> + string:replace(Filename, ":", "-", all). diff --git a/apps/emqx_bridge/src/emqx_bridge_resource.erl b/apps/emqx_bridge/src/emqx_bridge_resource.erl index 4f8d248b2..b916a0fbe 100644 --- a/apps/emqx_bridge/src/emqx_bridge_resource.erl +++ b/apps/emqx_bridge/src/emqx_bridge_resource.erl @@ -216,7 +216,8 @@ recreate(Type, Name, Conf, Opts) -> ). create_dry_run(Type, Conf0) -> - TmpPath = iolist_to_binary(["bridges-create-dry-run:", emqx_misc:gen_id(8)]), + TmpPath0 = iolist_to_binary(["bridges-create-dry-run:", emqx_misc:gen_id(8)]), + TmpPath = emqx_misc:safe_filename(TmpPath0), Conf = emqx_map_lib:safe_atom_key_map(Conf0), case emqx_connector_ssl:convert_certs(TmpPath, Conf) of {error, Reason} -> diff --git a/apps/emqx_resource/src/emqx_resource_worker.erl b/apps/emqx_resource/src/emqx_resource_worker.erl index 7840fd474..697d5a84b 100644 --- a/apps/emqx_resource/src/emqx_resource_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_worker.erl @@ -871,8 +871,9 @@ queue_count(Q) -> replayq:count(Q). disk_queue_dir(Id, Index) -> - QDir = binary_to_list(Id) ++ ":" ++ integer_to_list(Index), - filename:join([emqx:data_dir(), "resource_worker", node(), QDir]). + QDir0 = binary_to_list(Id) ++ ":" ++ integer_to_list(Index), + QDir = filename:join([emqx:data_dir(), "resource_worker", node(), QDir0]), + emqx_misc:safe_filename(QDir). ensure_flush_timer(Data = #{tref := undefined, batch_time := T}) -> Ref = make_ref(), From 5ee36d440619890d7756942e15561eb86fc2bf71 Mon Sep 17 00:00:00 2001 From: Ivan Dyachkov Date: Mon, 16 Jan 2023 17:42:41 +0100 Subject: [PATCH 0189/1802] ci: fix typo in error message --- .github/workflows/run_jmeter_tests.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/run_jmeter_tests.yaml b/.github/workflows/run_jmeter_tests.yaml index 81923dba5..94cc91706 100644 --- a/.github/workflows/run_jmeter_tests.yaml +++ b/.github/workflows/run_jmeter_tests.yaml @@ -126,7 +126,7 @@ jobs: - name: check logs run: | if cat jmeter_logs/${{ matrix.scripts_type }}.jtl | grep -e 'true' > /dev/null 2>&1; then - echo "check logs filed" + echo "check logs failed" exit 1 fi - uses: actions/upload-artifact@v3 @@ -235,7 +235,7 @@ jobs: - name: check logs run: | if cat jmeter_logs/${{ matrix.scripts_type }}_${{ matrix.pgsql_tag }}.jtl | grep -e 'true' > /dev/null 2>&1; then - echo "check logs filed" + echo "check logs failed" exit 1 fi - uses: actions/upload-artifact@v3 @@ -341,7 +341,7 @@ jobs: - name: check logs run: | if cat jmeter_logs/${{ matrix.scripts_type }}_${{ matrix.mysql_tag }}.jtl | grep -e 'true' > /dev/null 2>&1; then - echo "check logs filed" + echo "check logs failed" exit 1 fi - uses: actions/upload-artifact@v3 @@ -439,7 +439,7 @@ jobs: - name: check logs run: | if cat jmeter_logs/${{ matrix.scripts_type }}.jtl | grep -e 'true' > /dev/null 2>&1; then - echo "check logs filed" + echo "check logs failed" exit 1 fi - uses: actions/upload-artifact@v3 @@ -531,7 +531,7 @@ jobs: - name: check logs run: | if cat jmeter_logs/${{ matrix.scripts_type }}_${{ matrix.mysql_tag }}.jtl | grep -e 'true' > /dev/null 2>&1; then - echo "check logs filed" + echo "check logs failed" exit 1 fi - uses: actions/upload-artifact@v3 From 55fe996d222b1bbcc159e91cb7629f77beede7ec Mon Sep 17 00:00:00 2001 From: Ivan Dyachkov Date: Mon, 16 Jan 2023 17:43:47 +0100 Subject: [PATCH 0190/1802] docs: add changelog --- changes/v5.0.15/fix-9780.en.md | 1 + changes/v5.0.15/fix-9780.zh.md | 1 + 2 files changed, 2 insertions(+) create mode 100644 changes/v5.0.15/fix-9780.en.md create mode 100644 changes/v5.0.15/fix-9780.zh.md diff --git a/changes/v5.0.15/fix-9780.en.md b/changes/v5.0.15/fix-9780.en.md new file mode 100644 index 000000000..cf777e6dc --- /dev/null +++ b/changes/v5.0.15/fix-9780.en.md @@ -0,0 +1 @@ +When creating disk queue directory for resource worker, substitute ':' with '-' in worker id. diff --git a/changes/v5.0.15/fix-9780.zh.md b/changes/v5.0.15/fix-9780.zh.md new file mode 100644 index 000000000..bc5079e1d --- /dev/null +++ b/changes/v5.0.15/fix-9780.zh.md @@ -0,0 +1 @@ +在为资源缓存进程创建磁盘队列目录时,在ID中用 '-' 代替 ':'。 From 86b4d4d23b643e3e16a810d3742767e998bdf49a Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Mon, 16 Jan 2023 12:57:06 +0100 Subject: [PATCH 0191/1802] docs: add missing change logs for #9750 --- changes/v5.0.15/fix-9750.en.md | 5 +++++ changes/v5.0.15/fix-9750.zh.md | 4 ++++ 2 files changed, 9 insertions(+) create mode 100644 changes/v5.0.15/fix-9750.en.md create mode 100644 changes/v5.0.15/fix-9750.zh.md diff --git a/changes/v5.0.15/fix-9750.en.md b/changes/v5.0.15/fix-9750.en.md new file mode 100644 index 000000000..98c07dfb6 --- /dev/null +++ b/changes/v5.0.15/fix-9750.en.md @@ -0,0 +1,5 @@ +Reload overriding configs after boot. +Prior to this change, two configs were allow to change from dashboard, but will not take effect after reboot: + * Logging (such as level) + * Prometheus configs + diff --git a/changes/v5.0.15/fix-9750.zh.md b/changes/v5.0.15/fix-9750.zh.md new file mode 100644 index 000000000..605d4c225 --- /dev/null +++ b/changes/v5.0.15/fix-9750.zh.md @@ -0,0 +1,4 @@ +启动后重新加载一些重载配置项。 +在此修复前,下面两个配置项允许从 Dashboard 控制台修改,但是在重启后无法生效: + * 日志 (例如日志级别) + * Prometheus 配置 From 33be7672ad40d7f05e575b4371f167fe436ad94a Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Mon, 16 Jan 2023 12:48:17 +0100 Subject: [PATCH 0192/1802] test: recover boot_modules app env after test --- apps/emqx/test/emqx_authentication_SUITE.erl | 1 + apps/emqx/test/emqx_boot_SUITE.erl | 36 +++++++++++--------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/apps/emqx/test/emqx_authentication_SUITE.erl b/apps/emqx/test/emqx_authentication_SUITE.erl index e2e95d7ad..3588f1c8e 100644 --- a/apps/emqx/test/emqx_authentication_SUITE.erl +++ b/apps/emqx/test/emqx_authentication_SUITE.erl @@ -128,6 +128,7 @@ all() -> init_per_suite(Config) -> application:set_env(ekka, strict_mode, true), + emqx_common_test_helpers:boot_modules(all), emqx_common_test_helpers:start_apps([]), Config. diff --git a/apps/emqx/test/emqx_boot_SUITE.erl b/apps/emqx/test/emqx_boot_SUITE.erl index 7d0a7b4d3..06f08afb8 100644 --- a/apps/emqx/test/emqx_boot_SUITE.erl +++ b/apps/emqx/test/emqx_boot_SUITE.erl @@ -24,19 +24,23 @@ all() -> emqx_common_test_helpers:all(?MODULE). t_is_enabled(_) -> - ok = application:set_env(emqx, boot_modules, all), - ?assert(emqx_boot:is_enabled(router)), - ?assert(emqx_boot:is_enabled(broker)), - ?assert(emqx_boot:is_enabled(listeners)), - ok = application:set_env(emqx, boot_modules, [router]), - ?assert(emqx_boot:is_enabled(router)), - ?assertNot(emqx_boot:is_enabled(broker)), - ?assertNot(emqx_boot:is_enabled(listeners)), - ok = application:set_env(emqx, boot_modules, [router, broker]), - ?assert(emqx_boot:is_enabled(router)), - ?assert(emqx_boot:is_enabled(broker)), - ?assertNot(emqx_boot:is_enabled(listeners)), - ok = application:set_env(emqx, boot_modules, [router, broker, listeners]), - ?assert(emqx_boot:is_enabled(router)), - ?assert(emqx_boot:is_enabled(broker)), - ?assert(emqx_boot:is_enabled(listeners)). + try + ok = application:set_env(emqx, boot_modules, all), + ?assert(emqx_boot:is_enabled(router)), + ?assert(emqx_boot:is_enabled(broker)), + ?assert(emqx_boot:is_enabled(listeners)), + ok = application:set_env(emqx, boot_modules, [router]), + ?assert(emqx_boot:is_enabled(router)), + ?assertNot(emqx_boot:is_enabled(broker)), + ?assertNot(emqx_boot:is_enabled(listeners)), + ok = application:set_env(emqx, boot_modules, [router, broker]), + ?assert(emqx_boot:is_enabled(router)), + ?assert(emqx_boot:is_enabled(broker)), + ?assertNot(emqx_boot:is_enabled(listeners)), + ok = application:set_env(emqx, boot_modules, [router, broker, listeners]), + ?assert(emqx_boot:is_enabled(router)), + ?assert(emqx_boot:is_enabled(broker)), + ?assert(emqx_boot:is_enabled(listeners)) + after + application:set_env(emqx, boot_modules, all) + end. From 40948f3c0bdb5fd026259ed4b6a635bd8003c347 Mon Sep 17 00:00:00 2001 From: JimMoen Date: Mon, 16 Jan 2023 11:49:18 +0800 Subject: [PATCH 0193/1802] fix(influxdb): authentication is not required for influxdb v1 api --- changes/v5.0.15/fix-9680.en.md | 1 + changes/v5.0.15/fix-9680.zh.md | 1 + .../src/emqx_ee_connector_influxdb.erl | 27 ++++++++++++------- 3 files changed, 20 insertions(+), 9 deletions(-) create mode 100644 changes/v5.0.15/fix-9680.en.md create mode 100644 changes/v5.0.15/fix-9680.zh.md diff --git a/changes/v5.0.15/fix-9680.en.md b/changes/v5.0.15/fix-9680.en.md new file mode 100644 index 000000000..2ee3caaa5 --- /dev/null +++ b/changes/v5.0.15/fix-9680.en.md @@ -0,0 +1 @@ +Fix the problem that username and password authentication is mandatory in Influxdb v1 write API. diff --git a/changes/v5.0.15/fix-9680.zh.md b/changes/v5.0.15/fix-9680.zh.md new file mode 100644 index 000000000..bd1ace306 --- /dev/null +++ b/changes/v5.0.15/fix-9680.zh.md @@ -0,0 +1 @@ +修复 InfluxDB v1 桥接写入 API 配置中强制需要用户名密码认证的问题。 diff --git a/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl b/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl index db99c4475..553e5369f 100644 --- a/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl +++ b/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl @@ -278,19 +278,18 @@ client_config( ] ++ protocol_config(Config). %% api v1 config -protocol_config(#{ - username := Username, - password := Password, - database := DB, - ssl := SSL -}) -> +protocol_config( + #{ + database := DB, + ssl := SSL + } = Config +) -> [ {protocol, http}, {version, v1}, - {username, str(Username)}, - {password, str(Password)}, {database, str(DB)} - ] ++ ssl_config(SSL); + ] ++ username(Config) ++ + password(Config) ++ ssl_config(SSL); %% api v2 config protocol_config(#{ bucket := Bucket, @@ -321,6 +320,16 @@ ssl_config(SSL = #{enable := true}) -> {transport, ssl} ] ++ maps:to_list(maps:remove(enable, SSL)). +username(#{username := Username}) -> + [{username, str(Username)}]; +username(_) -> + []. + +password(#{password := Password}) -> + [{password, str(Password)}]; +password(_) -> + []. + %% ------------------------------------------------------------------------------------------------- %% Query do_query(InstId, Client, Points) -> From a523fa2fa2e211b3b2109a8f1c623583336252ed Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Mon, 16 Jan 2023 17:33:28 +0800 Subject: [PATCH 0194/1802] feat: add dashboard password validate --- .../src/emqx_dashboard_admin.erl | 124 ++++++++++++++++-- .../test/emqx_dashboard_admin_SUITE.erl | 12 +- 2 files changed, 120 insertions(+), 16 deletions(-) diff --git a/apps/emqx_dashboard/src/emqx_dashboard_admin.erl b/apps/emqx_dashboard/src/emqx_dashboard_admin.erl index e36c2628b..b4f0d7d4a 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_admin.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_admin.erl @@ -92,21 +92,79 @@ add_default_user() -> add_user(Username, Password, Desc) when is_binary(Username), is_binary(Password) -> - case legal_username(Username) of - true -> - return( - mria:transaction(?DASHBOARD_SHARD, fun add_user_/3, [Username, Password, Desc]) - ); - false -> + case {legal_username(Username), legal_password(Password)} of + {ok, ok} -> do_add_user(Username, Password, Desc); + {{error, Reason}, _} -> {error, Reason}; + {_, {error, Reason}} -> {error, Reason} + end. + +do_add_user(Username, Password, Desc) -> + Res = mria:transaction(?DASHBOARD_SHARD, fun add_user_/3, [Username, Password, Desc]), + return(Res). + +%% 0-9 or A-Z or a-z or $_ +legal_username(<<>>) -> + {error, <<"Username can not be empty">>}; +legal_username(UserName) -> + case re:run(UserName, "^[_a-zA-Z0-9]*$", [{capture, none}]) of + nomatch -> {error, << "Bad Username." " Only upper and lower case letters, numbers and underscores are supported" - >>} + >>}; + match -> + ok end. -%% 0 - 9 or A -Z or a - z or $_ -legal_username(<<>>) -> false; -legal_username(UserName) -> nomatch /= re:run(UserName, "^[_a-zA-Z0-9]*$"). +-define(LOW_LETTER_CHARS, "abcdefghijklmnopqrstuvwxyz"). +-define(UPPER_LETTER_CHARS, "ABCDEFGHIJKLMNOPQRSTUVWXYZ"). +-define(LETTER, ?LOW_LETTER_CHARS ++ ?UPPER_LETTER_CHARS). +-define(NUMBER, "0123456789"). +-define(SPECIAL_CHARS, "!@#$%^&*()_+-=[]{}\"|;':,./<>?`~ "). +-define(INVALID_PASSWORD_MSG, << + "Bad username." + "At least two different kind of characters from groups of letters, numbers, and special characters." + "For example, if password is composed from letters, it must contain at least one number or a special character." +>>). +-define(BAD_PASSWORD_LEN, <<"The range of password length is 8~64">>). + +legal_password(Password) when is_binary(Password) -> + legal_password(binary_to_list(Password)); +legal_password(Password) when is_list(Password) -> + legal_password(Password, erlang:length(Password)). + +legal_password(Password, Len) when Len >= 8 andalso Len =< 64 -> + case is_mixed_password(Password) of + true -> ascii_character_validate(Password); + false -> {error, ?INVALID_PASSWORD_MSG} + end; +legal_password(_Password, _Len) -> + {error, ?BAD_PASSWORD_LEN}. + +%% The password must contain at least two different kind of characters +%% from groups of letters, numbers, and special characters. +is_mixed_password(Password) -> is_mixed_password(Password, [?NUMBER, ?LETTER, ?SPECIAL_CHARS], 0). + +is_mixed_password(_Password, _Chars, 2) -> + true; +is_mixed_password(_Password, [], _Count) -> + false; +is_mixed_password(Password, [Chars | Rest], Count) -> + NewCount = + case contain(Password, Chars) of + true -> Count + 1; + false -> Count + end, + is_mixed_password(Password, Rest, NewCount). + +%% regex-non-ascii-character, such as Chinese, Japanese, Korean, etc. +ascii_character_validate(Password) -> + case re:run(Password, "[^\\x00-\\x7F]+", [unicode, {capture, none}]) of + match -> {error, <<"Only ascii characters are allowed in the password">>}; + nomatch -> ok + end. + +contain(Xs, Spec) -> lists:any(fun(X) -> lists:member(X, Spec) end, Xs). %% black-magic: force overwrite a user force_add_user(Username, Password, Desc) -> @@ -188,7 +246,10 @@ change_password(Username, OldPasswd, NewPasswd) when is_binary(Username) -> end. change_password(Username, Password) when is_binary(Username), is_binary(Password) -> - change_password_hash(Username, hash(Password)). + case legal_password(Password) of + ok -> change_password_hash(Username, hash(Password)); + Error -> Error + end. change_password_hash(Username, PasswordHash) -> ChangePWD = @@ -292,6 +353,45 @@ add_default_user(Username, Password) when ?EMPTY_KEY(Username) orelse ?EMPTY_KEY {ok, empty}; add_default_user(Username, Password) -> case lookup_user(Username) of - [] -> add_user(Username, Password, <<"administrator">>); + [] -> do_add_user(Username, Password, <<"administrator">>); _ -> {ok, default_user_exists} end. + +-ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). + +legal_password_test() -> + ?assertEqual({error, ?BAD_PASSWORD_LEN}, legal_password(<<"123">>)), + MaxPassword = iolist_to_binary([lists:duplicate(63, "x"), "1"]), + ?assertEqual(ok, legal_password(MaxPassword)), + TooLongPassword = lists:duplicate(65, "y"), + ?assertEqual({error, ?BAD_PASSWORD_LEN}, legal_password(TooLongPassword)), + + ?assertEqual({error, ?INVALID_PASSWORD_MSG}, legal_password(<<"12345678">>)), + ?assertEqual({error, ?INVALID_PASSWORD_MSG}, legal_password(?LETTER)), + ?assertEqual({error, ?INVALID_PASSWORD_MSG}, legal_password(?NUMBER)), + ?assertEqual({error, ?INVALID_PASSWORD_MSG}, legal_password(?SPECIAL_CHARS)), + ?assertEqual({error, ?INVALID_PASSWORD_MSG}, legal_password(<<"映映映映无天在请"/utf8>>)), + ?assertEqual( + {error, <<"Only ascii characters are allowed in the password">>}, + legal_password(<<"️test_for_non_ascii1中"/utf8>>) + ), + ?assertEqual( + {error, <<"Only ascii characters are allowed in the password">>}, + legal_password(<<"云☁️test_for_unicode"/utf8>>) + ), + + ?assertEqual(ok, legal_password(?LOW_LETTER_CHARS ++ ?NUMBER)), + ?assertEqual(ok, legal_password(?UPPER_LETTER_CHARS ++ ?NUMBER)), + ?assertEqual(ok, legal_password(?LOW_LETTER_CHARS ++ ?SPECIAL_CHARS)), + ?assertEqual(ok, legal_password(?UPPER_LETTER_CHARS ++ ?SPECIAL_CHARS)), + ?assertEqual(ok, legal_password(?SPECIAL_CHARS ++ ?NUMBER)), + + ?assertEqual(ok, legal_password(<<"abckldiekflkdf12">>)), + ?assertEqual(ok, legal_password(<<"abckldiekflkdf w">>)), + ?assertEqual(ok, legal_password(<<"# abckldiekflkdf w">>)), + ?assertEqual(ok, legal_password(<<"# 12344858">>)), + ?assertEqual(ok, legal_password(<<"# %12344858">>)), + ok. + +-endif. diff --git a/apps/emqx_dashboard/test/emqx_dashboard_admin_SUITE.erl b/apps/emqx_dashboard/test/emqx_dashboard_admin_SUITE.erl index fefc492cc..9ae5d4418 100644 --- a/apps/emqx_dashboard/test/emqx_dashboard_admin_SUITE.erl +++ b/apps/emqx_dashboard/test/emqx_dashboard_admin_SUITE.erl @@ -51,7 +51,7 @@ end_suite() -> t_check_user(_) -> Username = <<"admin1">>, - Password = <<"public">>, + Password = <<"public_1">>, BadUsername = <<"admin_bad">>, BadPassword = <<"public_bad">>, EmptyUsername = <<>>, @@ -108,7 +108,7 @@ t_lookup_user(_) -> t_all_users(_) -> Username = <<"admin_all">>, - Password = <<"public">>, + Password = <<"public_2">>, {ok, _} = emqx_dashboard_admin:add_user(Username, Password, <<"desc">>), All = emqx_dashboard_admin:all_users(), ?assert(erlang:length(All) >= 1), @@ -153,6 +153,7 @@ t_change_password(_) -> Description = <<"change_description">>, NewPassword = <<"new_password">>, + NewBadPassword = <<"public">>, BadChangeUser = <<"change_user_bad">>, @@ -163,14 +164,17 @@ t_change_password(_) -> {error, <<"password_error">>} = emqx_dashboard_admin:change_password(User, OldPassword, NewPassword), + {error, <<"The range of password length is 8~64">>} = + emqx_dashboard_admin:change_password(User, NewPassword, NewBadPassword), + {error, <<"username_not_found">>} = emqx_dashboard_admin:change_password(BadChangeUser, OldPassword, NewPassword), ok. t_clean_token(_) -> Username = <<"admin_token">>, - Password = <<"public">>, - NewPassword = <<"public1">>, + Password = <<"public_www1">>, + NewPassword = <<"public_www2">>, {ok, _} = emqx_dashboard_admin:add_user(Username, Password, <<"desc">>), {ok, Token} = emqx_dashboard_admin:sign_token(Username, Password), ok = emqx_dashboard_admin:verify_token(Token), From 82af854c4a30bcf73b2d90d1bd2796ebce299031 Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Mon, 16 Jan 2023 23:05:06 +0800 Subject: [PATCH 0195/1802] test: dashbashboard new password failed --- .../test/emqx_dashboard_SUITE.erl | 43 ++++++++++--------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl b/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl index 23d1b40c1..1b2da167f 100644 --- a/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl +++ b/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl @@ -82,7 +82,7 @@ set_special_configs(_) -> t_overview(_) -> mnesia:clear_table(?ADMIN), - emqx_dashboard_admin:add_user(<<"admin">>, <<"public">>, <<"simple_description">>), + emqx_dashboard_admin:add_user(<<"admin">>, <<"public_www1">>, <<"simple_description">>), [ {ok, _} = request_dashboard(get, api_path([Overview]), auth_header_()) || Overview <- ?OVERVIEWS @@ -91,7 +91,7 @@ t_overview(_) -> t_admins_add_delete(_) -> mnesia:clear_table(?ADMIN), Desc = <<"simple description">>, - {ok, _} = emqx_dashboard_admin:add_user(<<"username">>, <<"password">>, Desc), + {ok, _} = emqx_dashboard_admin:add_user(<<"username">>, <<"password_0">>, Desc), {ok, _} = emqx_dashboard_admin:add_user(<<"username1">>, <<"password1">>, Desc), Admins = emqx_dashboard_admin:all_users(), ?assertEqual(2, length(Admins)), @@ -100,8 +100,8 @@ t_admins_add_delete(_) -> ?assertEqual(1, length(Users)), {ok, _} = emqx_dashboard_admin:change_password( <<"username">>, - <<"password">>, - <<"pwd">> + <<"password_0">>, + <<"new_pwd_1234">> ), timer:sleep(10), {ok, _} = emqx_dashboard_admin:remove_user(<<"username">>). @@ -109,12 +109,12 @@ t_admins_add_delete(_) -> t_admin_delete_self_failed(_) -> mnesia:clear_table(?ADMIN), Desc = <<"simple description">>, - _ = emqx_dashboard_admin:add_user(<<"username1">>, <<"password">>, Desc), + _ = emqx_dashboard_admin:add_user(<<"username1">>, <<"password_1">>, Desc), Admins = emqx_dashboard_admin:all_users(), ?assertEqual(1, length(Admins)), - Header = auth_header_(<<"username1">>, <<"password">>), + Header = auth_header_(<<"username1">>, <<"password_1">>), {error, {_, 400, _}} = request_dashboard(delete, api_path(["users", "username1"]), Header), - Token = ["Basic ", base64:encode("username1:password")], + Token = ["Basic ", base64:encode("username1:password_1")], Header2 = {"Authorization", Token}, {error, {_, 401, _}} = request_dashboard(delete, api_path(["users", "username1"]), Header2), mnesia:clear_table(?ADMIN). @@ -122,7 +122,8 @@ t_admin_delete_self_failed(_) -> t_rest_api(_Config) -> mnesia:clear_table(?ADMIN), Desc = <<"administrator">>, - emqx_dashboard_admin:add_user(<<"admin">>, <<"public">>, Desc), + Password = <<"public_www1">>, + emqx_dashboard_admin:add_user(<<"admin">>, Password, Desc), {ok, 200, Res0} = http_get(["users"]), ?assertEqual( [ @@ -136,7 +137,7 @@ t_rest_api(_Config) -> {ok, 200, _} = http_put(["users", "admin"], #{<<"description">> => <<"a_new_description">>}), {ok, 200, _} = http_post(["users"], #{ <<"username">> => <<"usera">>, - <<"password">> => <<"passwd">>, + <<"password">> => <<"passwd_01234">>, <<"description">> => Desc }), {ok, 204, _} = http_delete(["users", "usera"]), @@ -144,34 +145,34 @@ t_rest_api(_Config) -> {ok, 204, _} = http_post( ["users", "admin", "change_pwd"], #{ - <<"old_pwd">> => <<"public">>, - <<"new_pwd">> => <<"newpwd">> + <<"old_pwd">> => Password, + <<"new_pwd">> => <<"newpwd_lkdfki1">> } ), mnesia:clear_table(?ADMIN), - emqx_dashboard_admin:add_user(<<"admin">>, <<"public">>, <<"administrator">>), + emqx_dashboard_admin:add_user(<<"admin">>, Password, <<"administrator">>), ok. t_cli(_Config) -> [mria:dirty_delete(?ADMIN, Admin) || Admin <- mnesia:dirty_all_keys(?ADMIN)], - emqx_dashboard_cli:admins(["add", "username", "password"]), + emqx_dashboard_cli:admins(["add", "username", "password_ww2"]), [#?ADMIN{username = <<"username">>, pwdhash = <>}] = emqx_dashboard_admin:lookup_user(<<"username">>), - ?assertEqual(Hash, crypto:hash(sha256, <>/binary>>)), - emqx_dashboard_cli:admins(["passwd", "username", "newpassword"]), + ?assertEqual(Hash, crypto:hash(sha256, <>/binary>>)), + emqx_dashboard_cli:admins(["passwd", "username", "new_password"]), [#?ADMIN{username = <<"username">>, pwdhash = <>}] = emqx_dashboard_admin:lookup_user(<<"username">>), - ?assertEqual(Hash1, crypto:hash(sha256, <>/binary>>)), + ?assertEqual(Hash1, crypto:hash(sha256, <>/binary>>)), emqx_dashboard_cli:admins(["del", "username"]), [] = emqx_dashboard_admin:lookup_user(<<"username">>), - emqx_dashboard_cli:admins(["add", "admin1", "pass1"]), - emqx_dashboard_cli:admins(["add", "admin2", "passw2"]), + emqx_dashboard_cli:admins(["add", "admin1", "pass_lkdfkd1"]), + emqx_dashboard_cli:admins(["add", "admin2", "w_pass_lkdfkd2"]), AdminList = emqx_dashboard_admin:all_users(), ?assertEqual(2, length(AdminList)). t_lookup_by_username_jwt(_Config) -> User = bin(["user-", integer_to_list(random_num())]), - Pwd = bin(integer_to_list(random_num())), + Pwd = bin("t_password" ++ integer_to_list(random_num())), emqx_dashboard_token:sign(User, Pwd), ?assertMatch( [#?ADMIN_JWT{username = User}], @@ -185,7 +186,7 @@ t_lookup_by_username_jwt(_Config) -> t_clean_expired_jwt(_Config) -> User = bin(["user-", integer_to_list(random_num())]), - Pwd = bin(integer_to_list(random_num())), + Pwd = bin("t_password" ++ integer_to_list(random_num())), emqx_dashboard_token:sign(User, Pwd), [#?ADMIN_JWT{username = User, exptime = ExpTime}] = emqx_dashboard_token:lookup_by_username(User), @@ -239,7 +240,7 @@ do_request_dashboard(Method, Request) -> end. auth_header_() -> - auth_header_(<<"admin">>, <<"public">>). + auth_header_(<<"admin">>, <<"public_www1">>). auth_header_(Username, Password) -> {ok, Token} = emqx_dashboard_admin:sign_token(Username, Password), From c549379b7c682379315937581867eec832b37ab5 Mon Sep 17 00:00:00 2001 From: zhongwencool Date: Mon, 16 Jan 2023 18:01:38 +0800 Subject: [PATCH 0196/1802] chore: apply suggestions from code review Co-authored-by: Zaiming (Stone) Shi --- apps/emqx_dashboard/src/emqx_dashboard_admin.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/emqx_dashboard/src/emqx_dashboard_admin.erl b/apps/emqx_dashboard/src/emqx_dashboard_admin.erl index b4f0d7d4a..84641918f 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_admin.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_admin.erl @@ -122,8 +122,8 @@ legal_username(UserName) -> -define(NUMBER, "0123456789"). -define(SPECIAL_CHARS, "!@#$%^&*()_+-=[]{}\"|;':,./<>?`~ "). -define(INVALID_PASSWORD_MSG, << - "Bad username." - "At least two different kind of characters from groups of letters, numbers, and special characters." + "Bad password. " + "At least two different kind of characters from groups of letters, numbers, and special characters. " "For example, if password is composed from letters, it must contain at least one number or a special character." >>). -define(BAD_PASSWORD_LEN, <<"The range of password length is 8~64">>). From b9911e281e6976a9614e2f0e719542d4a32526d7 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Mon, 16 Jan 2023 11:42:07 +0100 Subject: [PATCH 0197/1802] docs: Update apps/emqx_dashboard/src/emqx_dashboard_admin.erl --- apps/emqx_dashboard/src/emqx_dashboard_admin.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx_dashboard/src/emqx_dashboard_admin.erl b/apps/emqx_dashboard/src/emqx_dashboard_admin.erl index 84641918f..aaa43d621 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_admin.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_admin.erl @@ -104,7 +104,7 @@ do_add_user(Username, Password, Desc) -> %% 0-9 or A-Z or a-z or $_ legal_username(<<>>) -> - {error, <<"Username can not be empty">>}; + {error, <<"Username cannot be empty">>}; legal_username(UserName) -> case re:run(UserName, "^[_a-zA-Z0-9]*$", [{capture, none}]) of nomatch -> From 2bfb37f7f05790429312e712f63d6d26bd6d1e5b Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Mon, 16 Jan 2023 23:36:33 +0800 Subject: [PATCH 0198/1802] chore: changelog for #9774 --- changes/v5.0.15/feat-9774.en.md | 3 +++ changes/v5.0.15/feat-9774.zh.md | 2 ++ changes/v5.0.15/{fix-9726-en.md => fix-9726.en.md} | 0 changes/v5.0.15/{fix-9726-zh.md => fix-9726.zh.md} | 0 changes/v5.0.15/{fix-9735-en.md => fix-9735.en.md} | 0 changes/v5.0.15/{fix-9735-zh.md => fix-9735.zh.md} | 0 changes/v5.0.15/{fix-9748-en.md => fix-9748.en.md} | 0 changes/v5.0.15/{fix-9748-zh.md => fix-9748.zh.md} | 0 changes/v5.0.15/{fix-9749-en.md => fix-9749.en.md} | 0 changes/v5.0.15/{fix-9749-zh.md => fix-9749.zh.md} | 0 10 files changed, 5 insertions(+) create mode 100644 changes/v5.0.15/feat-9774.en.md create mode 100644 changes/v5.0.15/feat-9774.zh.md rename changes/v5.0.15/{fix-9726-en.md => fix-9726.en.md} (100%) rename changes/v5.0.15/{fix-9726-zh.md => fix-9726.zh.md} (100%) rename changes/v5.0.15/{fix-9735-en.md => fix-9735.en.md} (100%) rename changes/v5.0.15/{fix-9735-zh.md => fix-9735.zh.md} (100%) rename changes/v5.0.15/{fix-9748-en.md => fix-9748.en.md} (100%) rename changes/v5.0.15/{fix-9748-zh.md => fix-9748.zh.md} (100%) rename changes/v5.0.15/{fix-9749-en.md => fix-9749.en.md} (100%) rename changes/v5.0.15/{fix-9749-zh.md => fix-9749.zh.md} (100%) diff --git a/changes/v5.0.15/feat-9774.en.md b/changes/v5.0.15/feat-9774.en.md new file mode 100644 index 000000000..722c4db6b --- /dev/null +++ b/changes/v5.0.15/feat-9774.en.md @@ -0,0 +1,3 @@ +Add a password complexity requirement when adding or modifying Dashboard users via the API. +Now password must contain at least 2 of alphabetic, numeric and special characters, +and must be 8 to 64 characters long. diff --git a/changes/v5.0.15/feat-9774.zh.md b/changes/v5.0.15/feat-9774.zh.md new file mode 100644 index 000000000..21bfddfaf --- /dev/null +++ b/changes/v5.0.15/feat-9774.zh.md @@ -0,0 +1,2 @@ +通过 API 添加、修改 Dashboard 用户时,增加对密码复杂度的要求。 +现在密码必须包含字母、数字以及特殊字符中的至少 2 种,并且长度范围必须是 8~64 个字符。 diff --git a/changes/v5.0.15/fix-9726-en.md b/changes/v5.0.15/fix-9726.en.md similarity index 100% rename from changes/v5.0.15/fix-9726-en.md rename to changes/v5.0.15/fix-9726.en.md diff --git a/changes/v5.0.15/fix-9726-zh.md b/changes/v5.0.15/fix-9726.zh.md similarity index 100% rename from changes/v5.0.15/fix-9726-zh.md rename to changes/v5.0.15/fix-9726.zh.md diff --git a/changes/v5.0.15/fix-9735-en.md b/changes/v5.0.15/fix-9735.en.md similarity index 100% rename from changes/v5.0.15/fix-9735-en.md rename to changes/v5.0.15/fix-9735.en.md diff --git a/changes/v5.0.15/fix-9735-zh.md b/changes/v5.0.15/fix-9735.zh.md similarity index 100% rename from changes/v5.0.15/fix-9735-zh.md rename to changes/v5.0.15/fix-9735.zh.md diff --git a/changes/v5.0.15/fix-9748-en.md b/changes/v5.0.15/fix-9748.en.md similarity index 100% rename from changes/v5.0.15/fix-9748-en.md rename to changes/v5.0.15/fix-9748.en.md diff --git a/changes/v5.0.15/fix-9748-zh.md b/changes/v5.0.15/fix-9748.zh.md similarity index 100% rename from changes/v5.0.15/fix-9748-zh.md rename to changes/v5.0.15/fix-9748.zh.md diff --git a/changes/v5.0.15/fix-9749-en.md b/changes/v5.0.15/fix-9749.en.md similarity index 100% rename from changes/v5.0.15/fix-9749-en.md rename to changes/v5.0.15/fix-9749.en.md diff --git a/changes/v5.0.15/fix-9749-zh.md b/changes/v5.0.15/fix-9749.zh.md similarity index 100% rename from changes/v5.0.15/fix-9749-zh.md rename to changes/v5.0.15/fix-9749.zh.md From 3dfdad9cc9479142d0b923a279454299919f4597 Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Tue, 17 Jan 2023 11:30:25 +0800 Subject: [PATCH 0199/1802] test: dashboard default password --- apps/emqx/test/emqx_common_test_helpers.erl | 1 + apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl | 14 ++++++++------ .../test/emqx_dashboard_monitor_SUITE.erl | 1 + 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/apps/emqx/test/emqx_common_test_helpers.erl b/apps/emqx/test/emqx_common_test_helpers.erl index e4f50f2a1..954151efa 100644 --- a/apps/emqx/test/emqx_common_test_helpers.erl +++ b/apps/emqx/test/emqx_common_test_helpers.erl @@ -175,6 +175,7 @@ start_apps(Apps, SpecAppConfig) when is_function(SpecAppConfig) -> %% Because, minirest, ekka etc.. application will scan these modules lists:foreach(fun load/1, [emqx | Apps]), ok = start_ekka(), + mnesia:clear_table(emqx_admin), ok = emqx_ratelimiter_SUITE:load_conf(), lists:foreach(fun(App) -> start_app(App, SpecAppConfig) end, [emqx | Apps]). diff --git a/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl b/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl index 1b2da167f..8190b7c54 100644 --- a/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl +++ b/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl @@ -61,6 +61,7 @@ end_suite() -> end_suite(Apps) -> application:unload(emqx_management), + mnesia:clear_table(?ADMIN), emqx_common_test_helpers:stop_apps(Apps ++ [emqx_dashboard]). init_per_suite(Config) -> @@ -83,8 +84,9 @@ set_special_configs(_) -> t_overview(_) -> mnesia:clear_table(?ADMIN), emqx_dashboard_admin:add_user(<<"admin">>, <<"public_www1">>, <<"simple_description">>), + Headers = auth_header_(<<"admin">>, <<"public_www1">>), [ - {ok, _} = request_dashboard(get, api_path([Overview]), auth_header_()) + {ok, _} = request_dashboard(get, api_path([Overview]), Headers) || Overview <- ?OVERVIEWS ]. @@ -209,16 +211,16 @@ random_num() -> erlang:system_time(nanosecond). http_get(Parts) -> - request_api(get, api_path(Parts), auth_header_()). + request_api(get, api_path(Parts), auth_header_(<<"admin">>, <<"public_www1">>)). http_delete(Parts) -> - request_api(delete, api_path(Parts), auth_header_()). + request_api(delete, api_path(Parts), auth_header_(<<"admin">>, <<"public_www1">>)). http_post(Parts, Body) -> - request_api(post, api_path(Parts), [], auth_header_(), Body). + request_api(post, api_path(Parts), [], auth_header_(<<"admin">>, <<"public_www1">>), Body). http_put(Parts, Body) -> - request_api(put, api_path(Parts), [], auth_header_(), Body). + request_api(put, api_path(Parts), [], auth_header_(<<"admin">>, <<"public_www1">>), Body). request_dashboard(Method, Url, Auth) -> Request = {Url, [Auth]}, @@ -240,7 +242,7 @@ do_request_dashboard(Method, Request) -> end. auth_header_() -> - auth_header_(<<"admin">>, <<"public_www1">>). + auth_header_(<<"admin">>, <<"public">>). auth_header_(Username, Password) -> {ok, Token} = emqx_dashboard_admin:sign_token(Username, Password), diff --git a/apps/emqx_dashboard/test/emqx_dashboard_monitor_SUITE.erl b/apps/emqx_dashboard/test/emqx_dashboard_monitor_SUITE.erl index 7d4980320..74c6d9cc1 100644 --- a/apps/emqx_dashboard/test/emqx_dashboard_monitor_SUITE.erl +++ b/apps/emqx_dashboard/test/emqx_dashboard_monitor_SUITE.erl @@ -33,6 +33,7 @@ all() -> emqx_common_test_helpers:all(?MODULE). init_per_suite(Config) -> + application:load(emqx_dashboard), mria:start(), emqx_common_test_helpers:start_apps([emqx_dashboard], fun set_special_configs/1), Config. From 44f635300d1e85d58daef054fd85789a4e19b13b Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Mon, 16 Jan 2023 16:30:18 +0800 Subject: [PATCH 0200/1802] chore: update mysql-otp to 1.7.2 --- apps/emqx_connector/rebar.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx_connector/rebar.config b/apps/emqx_connector/rebar.config index 970ea3b9d..8252c9cf4 100644 --- a/apps/emqx_connector/rebar.config +++ b/apps/emqx_connector/rebar.config @@ -9,7 +9,7 @@ {emqx, {path, "../emqx"}}, {emqx_resource, {path, "../emqx_resource"}}, {eldap2, {git, "https://github.com/emqx/eldap2", {tag, "v0.2.2"}}}, - {mysql, {git, "https://github.com/emqx/mysql-otp", {tag, "1.7.1"}}}, + {mysql, {git, "https://github.com/emqx/mysql-otp", {tag, "1.7.2"}}}, {epgsql, {git, "https://github.com/emqx/epgsql", {tag, "4.7.0.1"}}}, %% NOTE: mind poolboy version when updating mongodb-erlang version {mongodb, {git, "https://github.com/emqx/mongodb-erlang", {tag, "v3.0.19"}}}, From 1c7fe91a91376cbcf7882e2ea53957f54ecabca9 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Tue, 17 Jan 2023 16:33:54 +0800 Subject: [PATCH 0201/1802] fix: typos in emqx_ee_bridge_mongodb.conf --- lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_mongodb.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_mongodb.conf b/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_mongodb.conf index 81ebc1e31..053c932f7 100644 --- a/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_mongodb.conf +++ b/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_mongodb.conf @@ -28,7 +28,7 @@ emqx_ee_bridge_mongodb { } label { en: "Collection to be used" - zh: "将要使用的藏品" + zh: "将要使用的集合(Collection)" } } From bc69a003293e11944490bd9ec87ff9ab00b4dda4 Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Mon, 16 Jan 2023 16:27:34 +0100 Subject: [PATCH 0202/1802] test: get plugin template as artifact --- .../test/emqx_mgmt_api_plugins_SUITE.erl | 6 +- apps/emqx_plugins/test/emqx_plugins_SUITE.erl | 81 ++++++++----------- .../build-demo-plugin.sh | 24 ------ 3 files changed, 37 insertions(+), 74 deletions(-) delete mode 100755 apps/emqx_plugins/test/emqx_plugins_SUITE_data/build-demo-plugin.sh diff --git a/apps/emqx_management/test/emqx_mgmt_api_plugins_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_api_plugins_SUITE.erl index fd8d8b02e..12193914b 100644 --- a/apps/emqx_management/test/emqx_mgmt_api_plugins_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_api_plugins_SUITE.erl @@ -50,7 +50,7 @@ end_per_suite(Config) -> todo_t_plugins(Config) -> DemoShDir = proplists:get_value(demo_sh_dir, Config), - PackagePath = build_demo_plugin_package(DemoShDir), + PackagePath = get_demo_plugin_package(DemoShDir), ct:pal("package_location:~p install dir:~p", [PackagePath, emqx_plugins:install_dir()]), NameVsn = filename:basename(PackagePath, ?PACKAGE_SUFFIX), ok = emqx_plugins:delete_package(NameVsn), @@ -135,8 +135,8 @@ uninstall_plugin(Name) -> DeletePath = emqx_mgmt_api_test_util:api_path(["plugins", Name]), emqx_mgmt_api_test_util:request_api(delete, DeletePath). -build_demo_plugin_package(Dir) -> - #{package := Pkg} = emqx_plugins_SUITE:build_demo_plugin_package(), +get_demo_plugin_package(Dir) -> + #{package := Pkg} = emqx_plugins_SUITE:get_demo_plugin_package(), FileName = "emqx_plugin_template-" ++ ?EMQX_PLUGIN_TEMPLATE_VSN ++ ?PACKAGE_SUFFIX, PluginPath = "./" ++ FileName, Pkg = filename:join([Dir, FileName]), diff --git a/apps/emqx_plugins/test/emqx_plugins_SUITE.erl b/apps/emqx_plugins/test/emqx_plugins_SUITE.erl index 3823d940d..494b48f7f 100644 --- a/apps/emqx_plugins/test/emqx_plugins_SUITE.erl +++ b/apps/emqx_plugins/test/emqx_plugins_SUITE.erl @@ -19,11 +19,20 @@ -compile(export_all). -compile(nowarn_export_all). --include_lib("emqx/include/emqx.hrl"). -include_lib("eunit/include/eunit.hrl"). +-define(EMQX_PLUGIN_TEMPLATE_RELEASE_NAME, "emqx_plugin_template"). +-define(EMQX_PLUGIN_TEMPLATE_URL, + "https://github.com/emqx/emqx-plugin-template/releases/download/" +). -define(EMQX_PLUGIN_TEMPLATE_VSN, "5.0.0"). +-define(EMQX_PLUGIN_TEMPLATE_TAG, "5.0.0"). +-define(EMQX_ELIXIR_PLUGIN_TEMPLATE_RELEASE_NAME, "elixir_plugin_template"). +-define(EMQX_ELIXIR_PLUGIN_TEMPLATE_URL, + "https://github.com/emqx/emqx-elixir-plugin/releases/download/" +). -define(EMQX_ELIXIR_PLUGIN_TEMPLATE_VSN, "0.1.0"). +-define(EMQX_ELIXIR_PLUGIN_TEMPLATE_TAG, "0.1.0-2"). -define(PACKAGE_SUFFIX, ".tar.gz"). all() -> emqx_common_test_helpers:all(?MODULE). @@ -60,63 +69,42 @@ end_per_testcase(TestCase, Config) -> emqx_plugins:put_configured([]), ?MODULE:TestCase({'end', Config}). -build_demo_plugin_package() -> - build_demo_plugin_package( +get_demo_plugin_package() -> + get_demo_plugin_package( #{ - target_path => "_build/default/emqx_plugrel", - release_name => "emqx_plugin_template", - git_url => "https://github.com/emqx/emqx-plugin-template.git", + release_name => ?EMQX_PLUGIN_TEMPLATE_RELEASE_NAME, + git_url => ?EMQX_PLUGIN_TEMPLATE_URL, vsn => ?EMQX_PLUGIN_TEMPLATE_VSN, - workdir => "demo_src", + tag => ?EMQX_PLUGIN_TEMPLATE_TAG, shdir => emqx_plugins:install_dir() } ). -build_demo_plugin_package( +get_demo_plugin_package( #{ - target_path := TargetPath, release_name := ReleaseName, git_url := GitUrl, vsn := PluginVsn, - workdir := DemoWorkDir, + tag := ReleaseTag, shdir := WorkDir } = Opts ) -> - BuildSh = filename:join([WorkDir, "build-demo-plugin.sh"]), - Cmd = string:join( - [ - BuildSh, - PluginVsn, - TargetPath, - ReleaseName, - GitUrl, - DemoWorkDir - ], - " " - ), - case emqx_run_sh:do(Cmd, [{cd, WorkDir}]) of - {ok, _} -> - Pkg = filename:join([ - WorkDir, - ReleaseName ++ "-" ++ - PluginVsn ++ - ?PACKAGE_SUFFIX - ]), - case filelib:is_regular(Pkg) of - true -> Opts#{package => Pkg}; - false -> error(#{reason => unexpected_build_result, not_found => Pkg}) - end; - {error, {Rc, Output}} -> - io:format(user, "failed_to_build_demo_plugin, Exit = ~p, Output:~n~ts\n", [Rc, Output]), - error(failed_to_build_demo_plugin) - end. + TargetName = lists:flatten([ReleaseName, "-", PluginVsn, ?PACKAGE_SUFFIX]), + FileURI = lists:flatten(lists:join("/", [GitUrl, ReleaseTag, TargetName])), + {ok, {_, _, PluginBin}} = httpc:request(FileURI), + Pkg = filename:join([ + WorkDir, + TargetName + ]), + ok = file:write_file(Pkg, PluginBin), + Opts#{package => Pkg}. bin(A) when is_atom(A) -> atom_to_binary(A, utf8); bin(L) when is_list(L) -> unicode:characters_to_binary(L, utf8); bin(B) when is_binary(B) -> B. t_demo_install_start_stop_uninstall({init, Config}) -> - Opts = #{package := Package} = build_demo_plugin_package(), + Opts = #{package := Package} = get_demo_plugin_package(), NameVsn = filename:basename(Package, ?PACKAGE_SUFFIX), [ {name_vsn, NameVsn}, @@ -186,7 +174,7 @@ write_info_file(Config, NameVsn, Content) -> ok = file:write_file(InfoFile, Content). t_position({init, Config}) -> - #{package := Package} = build_demo_plugin_package(), + #{package := Package} = get_demo_plugin_package(), NameVsn = filename:basename(Package, ?PACKAGE_SUFFIX), [{name_vsn, NameVsn} | Config]; t_position({'end', _Config}) -> @@ -225,7 +213,7 @@ t_position(Config) -> ok. t_start_restart_and_stop({init, Config}) -> - #{package := Package} = build_demo_plugin_package(), + #{package := Package} = get_demo_plugin_package(), NameVsn = filename:basename(Package, ?PACKAGE_SUFFIX), [{name_vsn, NameVsn} | Config]; t_start_restart_and_stop({'end', _Config}) -> @@ -275,7 +263,7 @@ t_start_restart_and_stop(Config) -> ok. t_enable_disable({init, Config}) -> - #{package := Package} = build_demo_plugin_package(), + #{package := Package} = get_demo_plugin_package(), NameVsn = filename:basename(Package, ?PACKAGE_SUFFIX), [{name_vsn, NameVsn} | Config]; t_enable_disable({'end', Config}) -> @@ -388,14 +376,13 @@ t_bad_info_json(Config) -> t_elixir_plugin({init, Config}) -> Opts0 = #{ - target_path => "_build/prod/plugrelex/elixir_plugin_template", - release_name => "elixir_plugin_template", - git_url => "https://github.com/emqx/emqx-elixir-plugin.git", + release_name => ?EMQX_ELIXIR_PLUGIN_TEMPLATE_RELEASE_NAME, + git_url => ?EMQX_ELIXIR_PLUGIN_TEMPLATE_URL, vsn => ?EMQX_ELIXIR_PLUGIN_TEMPLATE_VSN, - workdir => "demo_src_elixir", + tag => ?EMQX_ELIXIR_PLUGIN_TEMPLATE_TAG, shdir => emqx_plugins:install_dir() }, - Opts = #{package := Package} = build_demo_plugin_package(Opts0), + Opts = #{package := Package} = get_demo_plugin_package(Opts0), NameVsn = filename:basename(Package, ?PACKAGE_SUFFIX), [ {name_vsn, NameVsn}, diff --git a/apps/emqx_plugins/test/emqx_plugins_SUITE_data/build-demo-plugin.sh b/apps/emqx_plugins/test/emqx_plugins_SUITE_data/build-demo-plugin.sh deleted file mode 100755 index 15255524e..000000000 --- a/apps/emqx_plugins/test/emqx_plugins_SUITE_data/build-demo-plugin.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -vsn="${1}" -target_path="${2}" -release_name="${3}" -git_url="${4}" -workdir="${5}" - -target_name="${release_name}-${vsn}.tar.gz" -target="$workdir/${target_path}/${target_name}" -if [ -f "${target}" ]; then - cp "$target" ./ - exit 0 -fi - -# cleanup -rm -rf "${workdir}" - -git clone "${git_url}" -b "${vsn}" "${workdir}" -make -C "$workdir" rel - -cp "$target" ./ From c3d5c25c26d8ebb9be2691f05e0cc4261c007ae4 Mon Sep 17 00:00:00 2001 From: firest Date: Tue, 17 Jan 2023 16:29:08 +0800 Subject: [PATCH 0203/1802] fix(bridges): fix a compatible problem for old webhook bridge config which created before the v5.0.12 --- .../i18n/emqx_bridge_webhook_schema.conf | 11 ++- apps/emqx_bridge/src/emqx_bridge.erl | 4 +- ....erl => emqx_bridge_compatible_config.erl} | 27 +++--- .../src/schema/emqx_bridge_schema.erl | 11 ++- .../src/schema/emqx_bridge_webhook_schema.erl | 9 ++ ...> emqx_bridge_compatible_config_tests.erl} | 84 +++++++++++++++---- 6 files changed, 118 insertions(+), 28 deletions(-) rename apps/emqx_bridge/src/schema/{emqx_bridge_mqtt_config.erl => emqx_bridge_compatible_config.erl} (82%) rename apps/emqx_bridge/test/{emqx_bridge_mqtt_config_tests.erl => emqx_bridge_compatible_config_tests.erl} (72%) diff --git a/apps/emqx_bridge/i18n/emqx_bridge_webhook_schema.conf b/apps/emqx_bridge/i18n/emqx_bridge_webhook_schema.conf index f58b59aad..a6e75a766 100644 --- a/apps/emqx_bridge/i18n/emqx_bridge_webhook_schema.conf +++ b/apps/emqx_bridge/i18n/emqx_bridge_webhook_schema.conf @@ -10,7 +10,16 @@ emqx_bridge_webhook_schema { zh: "启用/禁用 Bridge" } } - + config_direction { + desc { + en: """Deprecated, The direction of this bridge, MUST be 'egress'""" + zh: """已废弃,Bridge 的方向,必须是 egress""" + } + label: { + en: "Bridge Direction" + zh: "Bridge 方向" + } + } config_url { desc { en: """ diff --git a/apps/emqx_bridge/src/emqx_bridge.erl b/apps/emqx_bridge/src/emqx_bridge.erl index c86087014..fa391151c 100644 --- a/apps/emqx_bridge/src/emqx_bridge.erl +++ b/apps/emqx_bridge/src/emqx_bridge.erl @@ -232,7 +232,9 @@ lookup(Type, Name, RawConf) -> end. maybe_upgrade(mqtt, Config) -> - emqx_bridge_mqtt_config:maybe_upgrade(Config); + emqx_bridge_compatible_config:maybe_upgrade(Config); +maybe_upgrade(webhook, Config) -> + emqx_bridge_compatible_config:webhook_maybe_upgrade(Config); maybe_upgrade(_Other, Config) -> Config. diff --git a/apps/emqx_bridge/src/schema/emqx_bridge_mqtt_config.erl b/apps/emqx_bridge/src/schema/emqx_bridge_compatible_config.erl similarity index 82% rename from apps/emqx_bridge/src/schema/emqx_bridge_mqtt_config.erl rename to apps/emqx_bridge/src/schema/emqx_bridge_compatible_config.erl index 7bd83d139..862b5e188 100644 --- a/apps/emqx_bridge/src/schema/emqx_bridge_mqtt_config.erl +++ b/apps/emqx_bridge/src/schema/emqx_bridge_compatible_config.erl @@ -15,22 +15,23 @@ %%-------------------------------------------------------------------- %% @doc This module was created to convert old version (from v5.0.0 to v5.0.11) -%% mqtt connector configs to newer version (developed for enterprise edition). --module(emqx_bridge_mqtt_config). +%% mqtt/webhook connector configs to newer version (developed for enterprise edition). +-module(emqx_bridge_compatible_config). -export([ - upgrade_pre_ee/1, - maybe_upgrade/1 + upgrade_pre_ee/2, + maybe_upgrade/1, + webhook_maybe_upgrade/1 ]). -upgrade_pre_ee(undefined) -> +upgrade_pre_ee(undefined, _UpgradeFunc) -> undefined; -upgrade_pre_ee(Conf0) when is_map(Conf0) -> - maps:from_list(upgrade_pre_ee(maps:to_list(Conf0))); -upgrade_pre_ee([]) -> +upgrade_pre_ee(Conf0, UpgradeFunc) when is_map(Conf0) -> + maps:from_list(upgrade_pre_ee(maps:to_list(Conf0), UpgradeFunc)); +upgrade_pre_ee([], _UpgradeFunc) -> []; -upgrade_pre_ee([{Name, Config} | Bridges]) -> - [{Name, maybe_upgrade(Config)} | upgrade_pre_ee(Bridges)]. +upgrade_pre_ee([{Name, Config} | Bridges], UpgradeFunc) -> + [{Name, UpgradeFunc(Config)} | upgrade_pre_ee(Bridges, UpgradeFunc)]. maybe_upgrade(#{<<"connector">> := _} = Config0) -> Config1 = up(Config0), @@ -39,6 +40,12 @@ maybe_upgrade(#{<<"connector">> := _} = Config0) -> maybe_upgrade(NewVersion) -> NewVersion. +webhook_maybe_upgrade(#{<<"direction">> := _} = Config0) -> + Config1 = maps:remove(<<"direction">>, Config0), + Config1#{<<"resource_opts">> => default_resource_opts()}; +webhook_maybe_upgrade(NewVersion) -> + NewVersion. + binary_key({K, V}) -> {atom_to_binary(K, utf8), V}. diff --git a/apps/emqx_bridge/src/schema/emqx_bridge_schema.erl b/apps/emqx_bridge/src/schema/emqx_bridge_schema.erl index 09a99488e..4aecfac5d 100644 --- a/apps/emqx_bridge/src/schema/emqx_bridge_schema.erl +++ b/apps/emqx_bridge/src/schema/emqx_bridge_schema.erl @@ -121,7 +121,12 @@ fields(bridges) -> hoconsc:map(name, ref(emqx_bridge_webhook_schema, "config")), #{ desc => ?DESC("bridges_webhook"), - required => false + required => false, + converter => fun(X, _HoconOpts) -> + emqx_bridge_compatible_config:upgrade_pre_ee( + X, fun emqx_bridge_compatible_config:webhook_maybe_upgrade/1 + ) + end } )}, {mqtt, @@ -131,7 +136,9 @@ fields(bridges) -> desc => ?DESC("bridges_mqtt"), required => false, converter => fun(X, _HoconOpts) -> - emqx_bridge_mqtt_config:upgrade_pre_ee(X) + emqx_bridge_compatible_config:upgrade_pre_ee( + X, fun emqx_bridge_compatible_config:maybe_upgrade/1 + ) end } )} diff --git a/apps/emqx_bridge/src/schema/emqx_bridge_webhook_schema.erl b/apps/emqx_bridge/src/schema/emqx_bridge_webhook_schema.erl index b495436a4..1540f77bf 100644 --- a/apps/emqx_bridge/src/schema/emqx_bridge_webhook_schema.erl +++ b/apps/emqx_bridge/src/schema/emqx_bridge_webhook_schema.erl @@ -81,6 +81,15 @@ request_config() -> desc => ?DESC("config_url") } )}, + {direction, + mk( + egress, + #{ + desc => ?DESC("config_direction"), + required => {false, recursively}, + deprecated => {since, "5.0.12"} + } + )}, {local_topic, mk( binary(), diff --git a/apps/emqx_bridge/test/emqx_bridge_mqtt_config_tests.erl b/apps/emqx_bridge/test/emqx_bridge_compatible_config_tests.erl similarity index 72% rename from apps/emqx_bridge/test/emqx_bridge_mqtt_config_tests.erl rename to apps/emqx_bridge/test/emqx_bridge_compatible_config_tests.erl index 90723252d..a2671a40e 100644 --- a/apps/emqx_bridge/test/emqx_bridge_mqtt_config_tests.erl +++ b/apps/emqx_bridge/test/emqx_bridge_compatible_config_tests.erl @@ -13,7 +13,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_bridge_mqtt_config_tests). +-module(emqx_bridge_compatible_config_tests). -include_lib("eunit/include/eunit.hrl"). @@ -26,30 +26,54 @@ empty_config_test() -> %% ensure webhook config can be checked webhook_config_test() -> - Conf = parse(webhook_v5011_hocon()), + Conf1 = parse(webhook_v5011_hocon()), + Conf2 = parse(full_webhook_v5011_hocon()), + ?assertMatch( #{ - <<"bridges">> := - #{ - <<"webhook">> := #{ - <<"the_name">> := - #{ - <<"method">> := get, - <<"body">> := <<"${payload}">> - } - } + <<"bridges">> := #{ + <<"webhook">> := #{ + <<"the_name">> := + #{ + <<"method">> := get, + <<"body">> := <<"${payload}">> + } } + } }, - check(Conf) + check(Conf1) ), + + ?assertMatch( + #{ + <<"bridges">> := #{ + <<"webhook">> := #{ + <<"the_name">> := + #{ + <<"method">> := get, + <<"body">> := <<"${payload}">> + } + } + } + }, + check(Conf2) + ), + ok. up(#{<<"bridges">> := Bridges0} = Conf0) -> Bridges = up(Bridges0), Conf0#{<<"bridges">> := Bridges}; up(#{<<"mqtt">> := MqttBridges0} = Bridges) -> - MqttBridges = emqx_bridge_mqtt_config:upgrade_pre_ee(MqttBridges0), - Bridges#{<<"mqtt">> := MqttBridges}. + MqttBridges = emqx_bridge_compatible_config:upgrade_pre_ee( + MqttBridges0, fun emqx_bridge_compatible_config:maybe_upgrade/1 + ), + Bridges#{<<"mqtt">> := MqttBridges}; +up(#{<<"webhook">> := WebhookBridges0} = Bridges) -> + WebhookBridges = emqx_bridge_compatible_config:upgrade_pre_ee( + WebhookBridges0, fun emqx_bridge_compatible_config:webhook_maybe_upgrade/1 + ), + Bridges#{<<"webhook">> := WebhookBridges}. parse(HOCON) -> {ok, Conf} = hocon:binary(HOCON), @@ -108,6 +132,38 @@ bridges{ } """. +full_webhook_v5011_hocon() -> + "" + "\n" + "bridges{\n" + " webhook {\n" + " the_name{\n" + " body = \"${payload}\"\n" + " connect_timeout = \"5s\"\n" + " direction = \"egress\"\n" + " enable_pipelining = 100\n" + " headers {\"content-type\" = \"application/json\"}\n" + " max_retries = 3\n" + " method = \"get\"\n" + " pool_size = 4\n" + " pool_type = \"random\"\n" + " request_timeout = \"5s\"\n" + " ssl {\n" + " ciphers = \"\"\n" + " depth = 10\n" + " enable = false\n" + " reuse_sessions = true\n" + " secure_renegotiate = true\n" + " user_lookup_fun = \"emqx_tls_psk:lookup\"\n" + " verify = \"verify_peer\"\n" + " versions = [\"tlsv1.3\", \"tlsv1.2\", \"tlsv1.1\", \"tlsv1\"]\n" + " }\n" + " url = \"http://localhost:8080\"\n" + " }\n" + " }\n" + "}\n" + "". + %% erlfmt-ignore %% this is a generated from v5.0.11 mqtt_v5011_hocon() -> From d63de6b16754595e8be6998ac7976ffb6f7622da Mon Sep 17 00:00:00 2001 From: firest Date: Tue, 17 Jan 2023 16:36:46 +0800 Subject: [PATCH 0204/1802] chore: update changes --- changes/v5.0.15/fix-9787.en.md | 1 + changes/v5.0.15/fix-9787.zh.md | 1 + 2 files changed, 2 insertions(+) create mode 100644 changes/v5.0.15/fix-9787.en.md create mode 100644 changes/v5.0.15/fix-9787.zh.md diff --git a/changes/v5.0.15/fix-9787.en.md b/changes/v5.0.15/fix-9787.en.md new file mode 100644 index 000000000..b41180368 --- /dev/null +++ b/changes/v5.0.15/fix-9787.en.md @@ -0,0 +1 @@ +Fix a compatible problem for the `webhook` bridge configuration which was created before the v5.0.12. diff --git a/changes/v5.0.15/fix-9787.zh.md b/changes/v5.0.15/fix-9787.zh.md new file mode 100644 index 000000000..a9d758de0 --- /dev/null +++ b/changes/v5.0.15/fix-9787.zh.md @@ -0,0 +1 @@ +修复对在 v5.0.12 之前创建的 `webhook` 桥接配置的兼容问题。 From 8b3ec48406b47236efa5a5719ec98e04b53abb98 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Sat, 14 Jan 2023 07:33:52 +0100 Subject: [PATCH 0205/1802] refactor: node.config_files should be hidden as it's not configurable this config schema exists only for app-env mapping. users can not change it because the bootstraping config has to be etc/emqx.conf --- apps/emqx_conf/i18n/emqx_conf_schema.conf | 13 ------------ apps/emqx_conf/src/emqx_conf_schema.erl | 24 ++++++++++------------- 2 files changed, 10 insertions(+), 27 deletions(-) diff --git a/apps/emqx_conf/i18n/emqx_conf_schema.conf b/apps/emqx_conf/i18n/emqx_conf_schema.conf index 337823233..21943e036 100644 --- a/apps/emqx_conf/i18n/emqx_conf_schema.conf +++ b/apps/emqx_conf/i18n/emqx_conf_schema.conf @@ -456,19 +456,6 @@ the old dir should be deleted first.
} } - node_config_files { - desc { - en: """List of configuration files that are read during startup. The order is -significant: later configuration files override the previous ones. - """ - zh: """启动时读取的配置文件列表。后面的配置文件项覆盖前面的文件。""" - } - label { - en: "Config Files" - zh: "配置文件" - } - } - node_global_gc_interval { desc { en: """Periodic garbage collection interval. Set to disabled to have it disabled.""" diff --git a/apps/emqx_conf/src/emqx_conf_schema.erl b/apps/emqx_conf/src/emqx_conf_schema.erl index 7a20a88dc..3cfb74b0e 100644 --- a/apps/emqx_conf/src/emqx_conf_schema.erl +++ b/apps/emqx_conf/src/emqx_conf_schema.erl @@ -466,9 +466,9 @@ fields("node") -> hoconsc:array(string()), #{ mapping => "emqx.config_files", - default => undefined, - 'readOnly' => true, - desc => ?DESC(node_config_files) + hidden => true, + required => false, + 'readOnly' => true } )}, {"global_gc_interval", @@ -1037,17 +1037,13 @@ metrics_enabled(disabled) -> []. tr_default_config_driver(Conf) -> conf_get("rpc.driver", Conf). -tr_config_files(Conf) -> - case conf_get("emqx.config_files", Conf) of - [_ | _] = Files -> - Files; - _ -> - case os:getenv("EMQX_ETC_DIR") of - false -> - [filename:join([code:lib_dir(emqx), "etc", "emqx.conf"])]; - Dir -> - [filename:join([Dir, "emqx.conf"])] - end +tr_config_files(_Conf) -> + case os:getenv("EMQX_ETC_DIR") of + false -> + %% testing, or running emqx app as deps + [filename:join([code:lib_dir(emqx), "etc", "emqx.conf"])]; + Dir -> + [filename:join([Dir, "emqx.conf"])] end. tr_cluster_override_conf_file(Conf) -> From 4a7e74f5d6874210ce67a6c6fcb249ab32948cbd Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Sun, 15 Jan 2023 11:09:33 +0100 Subject: [PATCH 0206/1802] fix(schema): add password converter to ensure its binary() type --- apps/emqx/src/emqx_schema.erl | 20 +++++++++++++++++-- apps/emqx_conf/src/emqx_conf_schema.erl | 3 ++- .../src/emqx_connector_schema_lib.erl | 1 + .../src/mqtt/emqx_connector_mqtt_schema.erl | 3 ++- .../src/emqx_dashboard_schema.erl | 1 + apps/emqx_gateway/src/emqx_gateway_schema.erl | 8 +++++++- .../src/emqx_ee_bridge_kafka.erl | 7 ++++++- .../src/emqx_ee_connector_influxdb.erl | 8 +++++++- 8 files changed, 44 insertions(+), 7 deletions(-) diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index b6390081b..e4e02add7 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -114,6 +114,7 @@ -export([namespace/0, roots/0, roots/1, fields/1, desc/1, tags/0]). -export([conf_get/2, conf_get/3, keys/2, filter/1]). -export([server_ssl_opts_schema/2, client_ssl_opts_schema/1, ciphers_schema/1]). +-export([password_converter/2]). -export([authz_fields/0]). -export([sc/2, map/2]). @@ -1510,7 +1511,9 @@ fields("sysmon_top") -> #{ mapping => "system_monitor.db_password", default => "system_monitor_password", - desc => ?DESC(sysmon_top_db_password) + desc => ?DESC(sysmon_top_db_password), + converter => fun password_converter/2, + sensitive => true } )}, {"db_name", @@ -1900,7 +1903,8 @@ common_ssl_opts_schema(Defaults) -> required => false, example => <<"">>, format => <<"password">>, - desc => ?DESC(common_ssl_opts_schema_password) + desc => ?DESC(common_ssl_opts_schema_password), + converter => fun password_converter/2 } )}, {"versions", @@ -2068,6 +2072,18 @@ do_default_ciphers(_) -> %% otherwise resolve default ciphers list at runtime []. +password_converter(undefined, _) -> + undefined; +password_converter(I, _) when is_integer(I) -> + integer_to_binary(I); +password_converter(X, _) -> + try + iolist_to_binary(X) + catch + _:_ -> + throw("must_quote") + end. + authz_fields() -> [ {"no_match", diff --git a/apps/emqx_conf/src/emqx_conf_schema.erl b/apps/emqx_conf/src/emqx_conf_schema.erl index 7a20a88dc..5cdbc5480 100644 --- a/apps/emqx_conf/src/emqx_conf_schema.erl +++ b/apps/emqx_conf/src/emqx_conf_schema.erl @@ -408,7 +408,8 @@ fields("node") -> required => true, 'readOnly' => true, sensitive => true, - desc => ?DESC(node_cookie) + desc => ?DESC(node_cookie), + converter => fun emqx_schema:password_converter/2 } )}, {"process_limit", diff --git a/apps/emqx_connector/src/emqx_connector_schema_lib.erl b/apps/emqx_connector/src/emqx_connector_schema_lib.erl index 5d8f6941c..f64208311 100644 --- a/apps/emqx_connector/src/emqx_connector_schema_lib.erl +++ b/apps/emqx_connector/src/emqx_connector_schema_lib.erl @@ -101,6 +101,7 @@ password(desc) -> ?DESC("password"); password(required) -> false; password(format) -> <<"password">>; password(sensitive) -> true; +password(converter) -> fun emqx_schema:password_converter/2; password(_) -> undefined. auto_reconnect(type) -> boolean(); diff --git a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_schema.erl b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_schema.erl index 5e833aa99..be462fcc1 100644 --- a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_schema.erl +++ b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_schema.erl @@ -107,7 +107,8 @@ fields("server_configs") -> #{ format => <<"password">>, sensitive => true, - desc => ?DESC("password") + desc => ?DESC("password"), + converter => fun emqx_schema:password_converter/2 } )}, {clean_start, diff --git a/apps/emqx_dashboard/src/emqx_dashboard_schema.erl b/apps/emqx_dashboard/src/emqx_dashboard_schema.erl index 6742032d5..09dbaf78c 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_schema.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_schema.erl @@ -209,6 +209,7 @@ default_password(default) -> "public"; default_password(required) -> true; default_password('readOnly') -> true; default_password(sensitive) -> true; +default_password(converter) -> fun emqx_schema:password_converter/2; default_password(desc) -> ?DESC(default_password); default_password(_) -> undefined. diff --git a/apps/emqx_gateway/src/emqx_gateway_schema.erl b/apps/emqx_gateway/src/emqx_gateway_schema.erl index 804e1f862..4ea845ea1 100644 --- a/apps/emqx_gateway/src/emqx_gateway_schema.erl +++ b/apps/emqx_gateway/src/emqx_gateway_schema.erl @@ -380,7 +380,13 @@ fields(ssl_server_opts) -> fields(clientinfo_override) -> [ {username, sc(binary(), #{desc => ?DESC(gateway_common_clientinfo_override_username)})}, - {password, sc(binary(), #{desc => ?DESC(gateway_common_clientinfo_override_password)})}, + {password, + sc(binary(), #{ + desc => ?DESC(gateway_common_clientinfo_override_password), + sensitive => true, + format => <<"password">>, + converter => fun emqx_schema:password_converter/2 + })}, {clientid, sc(binary(), #{desc => ?DESC(gateway_common_clientinfo_override_clientid)})} ]; fields(lwm2m_translators) -> diff --git a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_kafka.erl b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_kafka.erl index 4a6c1411c..e694f6c15 100644 --- a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_kafka.erl +++ b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_kafka.erl @@ -116,7 +116,12 @@ fields(auth_username_password) -> })}, {username, mk(binary(), #{required => true, desc => ?DESC(auth_sasl_username)})}, {password, - mk(binary(), #{required => true, sensitive => true, desc => ?DESC(auth_sasl_password)})} + mk(binary(), #{ + required => true, + sensitive => true, + desc => ?DESC(auth_sasl_password), + converter => fun emqx_schema:password_converter/2 + })} ]; fields(auth_gssapi_kerberos) -> [ diff --git a/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl b/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl index 553e5369f..ff8c31997 100644 --- a/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl +++ b/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl @@ -157,7 +157,13 @@ fields(influxdb_api_v1) -> [ {database, mk(binary(), #{required => true, desc => ?DESC("database")})}, {username, mk(binary(), #{desc => ?DESC("username")})}, - {password, mk(binary(), #{desc => ?DESC("password"), format => <<"password">>})} + {password, + mk(binary(), #{ + desc => ?DESC("password"), + format => <<"password">>, + sensitive => true, + converter => fun emqx_schema:password_converter/2 + })} ] ++ emqx_connector_schema_lib:ssl_fields(); fields(influxdb_api_v2) -> fields(common) ++ From 2afbf6a406b6a5a1af2ac4c7eeeff2ff12aadc4b Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Sun, 15 Jan 2023 12:43:25 +0100 Subject: [PATCH 0207/1802] test: unlink process which is getting shutdown --- apps/emqx_authn/test/emqx_authn_SUITE.erl | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/emqx_authn/test/emqx_authn_SUITE.erl b/apps/emqx_authn/test/emqx_authn_SUITE.erl index 6b24f7231..b30d2fcff 100644 --- a/apps/emqx_authn/test/emqx_authn_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_SUITE.erl @@ -98,6 +98,7 @@ t_will_message_connection_denied(Config) when is_list(Config) -> {will_payload, <<"should not be published">>} ]), Ref = monitor(process, Publisher), + _ = unlink(Publisher), {error, _} = emqtt:connect(Publisher), receive {'DOWN', Ref, process, Publisher, Reason} -> From 263deae1f3250e5a7a05a4071dc65b7849e39177 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Mon, 16 Jan 2023 10:59:37 +0100 Subject: [PATCH 0208/1802] refactor: add a more generic name for password_converter --- apps/emqx/src/emqx_schema.erl | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index e4e02add7..8f016f684 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -114,7 +114,7 @@ -export([namespace/0, roots/0, roots/1, fields/1, desc/1, tags/0]). -export([conf_get/2, conf_get/3, keys/2, filter/1]). -export([server_ssl_opts_schema/2, client_ssl_opts_schema/1, ciphers_schema/1]). --export([password_converter/2]). +-export([password_converter/2, bin_str_converter/2]). -export([authz_fields/0]). -export([sc/2, map/2]). @@ -2072,11 +2072,14 @@ do_default_ciphers(_) -> %% otherwise resolve default ciphers list at runtime []. -password_converter(undefined, _) -> +password_converter(X, Opts) -> + bin_str_converter(X, Opts). + +bin_str_converter(undefined, _) -> undefined; -password_converter(I, _) when is_integer(I) -> +bin_str_converter(I, _) when is_integer(I) -> integer_to_binary(I); -password_converter(X, _) -> +bin_str_converter(X, _) -> try iolist_to_binary(X) catch From b793aad34496e35eec137a972143827f2bbe5ce1 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Tue, 17 Jan 2023 11:21:01 +0100 Subject: [PATCH 0209/1802] docs: add changelog --- changes/v5.0.15/fix-9765.en.md | 6 ++++++ changes/v5.0.15/fix-9765.zh.md | 7 +++++++ 2 files changed, 13 insertions(+) create mode 100644 changes/v5.0.15/fix-9765.en.md create mode 100644 changes/v5.0.15/fix-9765.zh.md diff --git a/changes/v5.0.15/fix-9765.en.md b/changes/v5.0.15/fix-9765.en.md new file mode 100644 index 000000000..8c1ed4ad1 --- /dev/null +++ b/changes/v5.0.15/fix-9765.en.md @@ -0,0 +1,6 @@ +Parse decimals as password from environment variable overrides correctly. +Prior to this change, config values for passwords are not allowed to be decimals. +e.g. `EMQX_FOOBAR__PASSWORD=12344` or `emqx.foobar.password=1234` +would result in a type check error, unless quoted as: +`EMQX_FOOBAR__PASSWORD='"12344"'` or `emqx.foobar.password="1234"`. +After this fix, the value does not have to be auoted. diff --git a/changes/v5.0.15/fix-9765.zh.md b/changes/v5.0.15/fix-9765.zh.md new file mode 100644 index 000000000..dd0b6a79c --- /dev/null +++ b/changes/v5.0.15/fix-9765.zh.md @@ -0,0 +1,7 @@ +允许使用纯数字作为密码配置。 +在此修复前,密码的配置必须是字符串,使用纯数字时,会报类型检查错误。 +例如,`EMQX_FOOBAR__PASSWORD=12344` 或 `emqx.foobar.password=1234` 会出错, +必须用引把值括起来才行: +`EMQX_FOOBAR__PASSWORD='"12344"'` 或 `emqx.foobar.password="1234"`。 +修复后可以不使用引号。在环境变量重载中使用更加方便。 + From 5a560131931a487a8232d09b96c3dd110ba98b35 Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Tue, 17 Jan 2023 20:17:45 +0800 Subject: [PATCH 0210/1802] docs: more detail about crashdump config --- apps/emqx/etc/vm.args.cloud | 8 -------- apps/emqx_conf/i18n/emqx_conf_schema.conf | 20 ++++++++++++++++---- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/apps/emqx/etc/vm.args.cloud b/apps/emqx/etc/vm.args.cloud index edd6944c2..9a4530ce5 100644 --- a/apps/emqx/etc/vm.args.cloud +++ b/apps/emqx/etc/vm.args.cloud @@ -24,9 +24,6 @@ ## Sets the maximum number of atoms the virtual machine can handle. #+t 1048576 -## Set the location of crash dumps -#-env ERL_CRASH_DUMP {{ platform_log_dir }}/crash.dump - ## Set how many times generational garbages collections can be done without ## forcing a fullsweep collection. -env ERL_FULLSWEEP_AFTER 1000 @@ -40,11 +37,6 @@ ## Prevent user from accidentally calling a function from the prompt that could harm a running system. -stdlib restricted_shell emqx_restricted_shell -## Specifies the net_kernel tick time in seconds. -## This is the approximate time a connected node may be unresponsive until -## it is considered down and thereby disconnected. --kernel net_ticktime 120 - ## Sets the distribution buffer busy limit (dist_buf_busy_limit). ## Preferably set in `emqx.conf`, #+zdbbl 8192 diff --git a/apps/emqx_conf/i18n/emqx_conf_schema.conf b/apps/emqx_conf/i18n/emqx_conf_schema.conf index 21943e036..131118f5b 100644 --- a/apps/emqx_conf/i18n/emqx_conf_schema.conf +++ b/apps/emqx_conf/i18n/emqx_conf_schema.conf @@ -480,8 +480,16 @@ the old dir should be deleted first.
node_crash_dump_seconds { desc { - en: """The number of seconds that the broker is allowed to spend writing a crash dump.""" - zh: """保存崩溃文件最大允许时间,如果文件太大,在规则时间内没有保存完成,则会直接结束。""" + en: """This variable gives the number of seconds that the emulator is allowed to spend writing a crash dump. When the given number of seconds have elapsed, the emulator is terminated. +- If setting to 0 seconds, the runtime system does not even attempt to write the crash dump file. It only terminates. +- If setting to a positive value S, wait for S seconds to complete the crash dump file and then terminates the runtime system with a SIGALRM signal. +- A negative value causes the termination of the runtime system to wait indefinitely until the crash dump file has been completely written. + """ + zh: """该配置给出了运行时系统允许花费的写入崩溃转储的秒数。当给定的秒数已经过去,运行时系统将被终止。 +- 如果设置为0秒,运行时会立即终止,不会尝试写入崩溃转储文件。 +- 如果设置为一个正数 S,节点会等待 S 秒来完成崩溃转储文件,然后用SIGALRM信号终止运行时系统。 +- 如果设置为一个负值导致运行时系统的终止等待无限期地直到崩溃转储文件已经完全写入。 +""" } label { en: "Crash Dump Seconds" @@ -491,10 +499,14 @@ the old dir should be deleted first.
node_crash_dump_bytes { desc { - en: """The maximum size of a crash dump file in bytes.""" + en: """This variable sets the maximum size of a crash dump file in bytes. +The crash dump will be truncated if this limit is exceeded. +If setting it to 0, the runtime system does not even attempt to write a crash dump file. +""" zh: """限制崩溃文件的大小,当崩溃时节点内存太大, 如果为了保存现场,需要全部存到崩溃文件中,此处限制最多能保存多大的文件。 - """ +如果超过此限制,崩溃转储将被截断。如果设置为0,系统不会尝试写入崩溃转储文件。 +""" } label { en: "Crash Dump Bytes" From cf99089f84d53b607b8b9493e04bdd0c063c21ea Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Tue, 17 Jan 2023 20:18:20 +0800 Subject: [PATCH 0211/1802] chore: add some trace func to user_default --- apps/emqx_machine/src/user_default.erl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/emqx_machine/src/user_default.erl b/apps/emqx_machine/src/user_default.erl index ce5397c26..3e561b929 100644 --- a/apps/emqx_machine/src/user_default.erl +++ b/apps/emqx_machine/src/user_default.erl @@ -26,6 +26,14 @@ %% API -export([lock/0, unlock/0]). +-export([t/1, t2/1, t/2, t2/2, t/3, t2/3]). lock() -> emqx_restricted_shell:lock(). unlock() -> emqx_restricted_shell:unlock(). + +t(M) -> recon_trace:calls({M, '_', return_trace}, 300). +t2(M) -> recon_trace:calls({M, '_', return_trace}, 300, [{args, arity}]). +t(M, F) -> recon_trace:calls({M, F, return_trace}, 300). +t2(M, F) -> recon_trace:calls({M, F, return_trace}, 300, [{args, arity}]). +t(M, F, A) -> recon_trace:calls({M, F, A}, 300). +t2(M, F, A) -> recon_trace:calls({M, F, A}, 300, [{args, arity}]). From f98786c91f33f24149f5df09421d61bbc214f31a Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Tue, 17 Jan 2023 20:47:54 +0800 Subject: [PATCH 0212/1802] fix: bad crash_dump_file default value in rpm --- apps/emqx_conf/src/emqx_conf_schema.erl | 11 ++++++++++- apps/emqx_machine/src/emqx_machine.app.src | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/apps/emqx_conf/src/emqx_conf_schema.erl b/apps/emqx_conf/src/emqx_conf_schema.erl index 3cfb74b0e..06ec7478a 100644 --- a/apps/emqx_conf/src/emqx_conf_schema.erl +++ b/apps/emqx_conf/src/emqx_conf_schema.erl @@ -487,7 +487,7 @@ fields("node") -> #{ mapping => "vm_args.-env ERL_CRASH_DUMP", desc => ?DESC(node_crash_dump_file), - default => "log/erl_crash.dump", + default => crash_dump_file_default(), 'readOnly' => true } )}, @@ -1296,6 +1296,15 @@ sort_log_levels(Levels) -> Levels ). +crash_dump_file_default() -> + case os:getenv("RUNNER_LOG_DIR") of + false -> + %% testing, or running emqx app as deps + "log/erl_crash.dump"; + Dir -> + [filename:join([Dir, "erl_crash.dump"])] + end. + %% utils -spec conf_get(string() | [string()], hocon:config()) -> term(). conf_get(Key, Conf) -> diff --git a/apps/emqx_machine/src/emqx_machine.app.src b/apps/emqx_machine/src/emqx_machine.app.src index fdfd2b28f..c805fdd25 100644 --- a/apps/emqx_machine/src/emqx_machine.app.src +++ b/apps/emqx_machine/src/emqx_machine.app.src @@ -3,7 +3,7 @@ {id, "emqx_machine"}, {description, "The EMQX Machine"}, % strict semver, bump manually! - {vsn, "0.1.2"}, + {vsn, "0.1.3"}, {modules, []}, {registered, []}, {applications, [kernel, stdlib]}, From 94296258b4712554764b6e3c660d0bad12669594 Mon Sep 17 00:00:00 2001 From: Erik Timan Date: Tue, 10 Jan 2023 11:43:15 +0100 Subject: [PATCH 0213/1802] test(emqx_management): refactor test suite to use common test utils for API calls --- .../test/emqx_mgmt_api_test_util.erl | 15 +- .../test/emqx_mgmt_api_trace_SUITE.erl | 137 +++++++----------- 2 files changed, 59 insertions(+), 93 deletions(-) diff --git a/apps/emqx_management/test/emqx_mgmt_api_test_util.erl b/apps/emqx_management/test/emqx_mgmt_api_test_util.erl index 82d55bb6a..abab3f9b0 100644 --- a/apps/emqx_management/test/emqx_mgmt_api_test_util.erl +++ b/apps/emqx_management/test/emqx_mgmt_api_test_util.erl @@ -61,7 +61,8 @@ uri(Parts) -> %% compatible_mode will return as same as 'emqx_dashboard_api_test_helpers:request' request_api_with_body(Method, Url, Body) -> - request_api(Method, Url, [], auth_header_(), Body, #{compatible_mode => true}). + Opts = #{compatible_mode => true, httpc_req_opts => [{body_format, binary}]}, + request_api(Method, Url, [], auth_header_(), Body, Opts). request_api(Method, Url) -> request_api(Method, Url, auth_header_()). @@ -111,15 +112,9 @@ request_api(Method, Url, QueryParams, AuthOrHeaders, Body, Opts) when do_request_api(Method, Request, Opts) -> ReturnAll = maps:get(return_all, Opts, false), CompatibleMode = maps:get(compatible_mode, Opts, false), - ReqOpts = - case CompatibleMode of - true -> - [{body_format, binary}]; - _ -> - [] - end, - ct:pal("Method: ~p, Request: ~p", [Method, Request]), - case httpc:request(Method, Request, [], ReqOpts) of + HttpcReqOpts = maps:get(httpc_req_opts, Opts, []), + ct:pal("Method: ~p, Request: ~p, Opts: ~p", [Method, Request, Opts]), + case httpc:request(Method, Request, [], HttpcReqOpts) of {error, socket_closed_remotely} -> {error, socket_closed_remotely}; {ok, {{_, Code, _}, _Headers, Body}} when CompatibleMode -> diff --git a/apps/emqx_management/test/emqx_mgmt_api_trace_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_api_trace_SUITE.erl index 0ba05b280..33240b64f 100644 --- a/apps/emqx_management/test/emqx_mgmt_api_trace_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_api_trace_SUITE.erl @@ -26,12 +26,6 @@ -include_lib("stdlib/include/zip.hrl"). -include_lib("snabbkaffe/include/snabbkaffe.hrl"). --define(HOST, "http://127.0.0.1:18083/"). --define(API_VERSION, "v5"). --define(BASE_PATH, "api"). - --import(emqx_dashboard_SUITE, [auth_header_/0]). - %%-------------------------------------------------------------------- %% Setups %%-------------------------------------------------------------------- @@ -49,14 +43,14 @@ end_per_suite(_) -> t_http_test(_Config) -> emqx_trace:clear(), load(), - Header = auth_header_(), %% list - {ok, Empty} = request_api(get, api_path("trace"), Header), + {ok, Empty} = request_api(get, api_path("trace")), ?assertEqual([], json(Empty)), %% create ErrorTrace = #{}, - {error, {"HTTP/1.1", 400, "Bad Request"}, Body} = - request_api(post, api_path("trace"), Header, ErrorTrace), + Opts = #{return_all => true}, + {error, {{"HTTP/1.1", 400, "Bad Request"}, _, Body}} = + emqx_mgmt_api_test_util:request_api(post, api_path("trace"), [], [], ErrorTrace, Opts), ?assertMatch(#{<<"code">> := <<"BAD_REQUEST">>}, json(Body)), Name = <<"test-name">>, @@ -66,15 +60,15 @@ t_http_test(_Config) -> {<<"topic">>, <<"/x/y/z">>} ], - {ok, Create} = request_api(post, api_path("trace"), Header, Trace), + {ok, Create} = request_api(post, api_path("trace"), Trace), ?assertMatch(#{<<"name">> := Name}, json(Create)), - {ok, List} = request_api(get, api_path("trace"), Header), + {ok, List} = request_api(get, api_path("trace")), [Data] = json(List), ?assertEqual(Name, maps:get(<<"name">>, Data)), %% update - {ok, Update} = request_api(put, api_path("trace/test-name/stop"), Header, #{}), + {ok, Update} = request_api(put, api_path("trace/test-name/stop"), #{}), ?assertEqual( #{ <<"enable">> => false, @@ -84,10 +78,10 @@ t_http_test(_Config) -> ), ?assertMatch( - {error, {"HTTP/1.1", 404, _}, _}, - request_api(put, api_path("trace/test-name-not-found/stop"), Header, #{}) + {error, {"HTTP/1.1", 404, _}}, + request_api(put, api_path("trace/test-name-not-found/stop"), #{}) ), - {ok, List1} = request_api(get, api_path("trace"), Header), + {ok, List1} = request_api(get, api_path("trace")), [Data1] = json(List1), Node = atom_to_binary(node()), ?assertMatch( @@ -104,11 +98,11 @@ t_http_test(_Config) -> ), %% delete - {ok, Delete} = request_api(delete, api_path("trace/test-name"), Header), + {ok, Delete} = request_api(delete, api_path("trace/test-name")), ?assertEqual(<<>>, Delete), - {error, {"HTTP/1.1", 404, "Not Found"}, DeleteNotFound} = - request_api(delete, api_path("trace/test-name"), Header), + {error, {{"HTTP/1.1", 404, "Not Found"}, _, DeleteNotFound}} = + emqx_mgmt_api_test_util:request_api(delete, api_path("trace/test-name"), [], [], [], Opts), ?assertEqual( #{ <<"code">> => <<"NOT_FOUND">>, @@ -117,14 +111,14 @@ t_http_test(_Config) -> json(DeleteNotFound) ), - {ok, List2} = request_api(get, api_path("trace"), Header), + {ok, List2} = request_api(get, api_path("trace")), ?assertEqual([], json(List2)), %% clear - {ok, Create1} = request_api(post, api_path("trace"), Header, Trace), + {ok, Create1} = request_api(post, api_path("trace"), Trace), ?assertMatch(#{<<"name">> := Name}, json(Create1)), - {ok, Clear} = request_api(delete, api_path("trace"), Header), + {ok, Clear} = request_api(delete, api_path("trace")), ?assertEqual(<<>>, Clear), unload(), @@ -132,27 +126,26 @@ t_http_test(_Config) -> t_create_failed(_Config) -> load(), - Header = auth_header_(), Trace = [{<<"type">>, <<"topic">>}, {<<"topic">>, <<"/x/y/z">>}], BadName1 = {<<"name">>, <<"test/bad">>}, ?assertMatch( - {error, {"HTTP/1.1", 400, _}, _}, - request_api(post, api_path("trace"), Header, [BadName1 | Trace]) + {error, {"HTTP/1.1", 400, _}}, + request_api(post, api_path("trace"), [BadName1 | Trace]) ), BadName2 = {<<"name">>, list_to_binary(lists:duplicate(257, "t"))}, ?assertMatch( - {error, {"HTTP/1.1", 400, _}, _}, - request_api(post, api_path("trace"), Header, [BadName2 | Trace]) + {error, {"HTTP/1.1", 400, _}}, + request_api(post, api_path("trace"), [BadName2 | Trace]) ), %% already_exist GoodName = {<<"name">>, <<"test-name-0">>}, - {ok, Create} = request_api(post, api_path("trace"), Header, [GoodName | Trace]), + {ok, Create} = request_api(post, api_path("trace"), [GoodName | Trace]), ?assertMatch(#{<<"name">> := <<"test-name-0">>}, json(Create)), ?assertMatch( - {error, {"HTTP/1.1", 409, _}, _}, - request_api(post, api_path("trace"), Header, [GoodName | Trace]) + {error, {"HTTP/1.1", 409, _}}, + request_api(post, api_path("trace"), [GoodName | Trace]) ), %% MAX Limited @@ -170,17 +163,17 @@ t_create_failed(_Config) -> ), GoodName1 = {<<"name">>, <<"test-name-1">>}, ?assertMatch( - {error, {"HTTP/1.1", 400, _}, _}, - request_api(post, api_path("trace"), Header, [GoodName1 | Trace]) + {error, {"HTTP/1.1", 400, _}}, + request_api(post, api_path("trace"), [GoodName1 | Trace]) ), %% clear - ?assertMatch({ok, _}, request_api(delete, api_path("trace"), Header, [])), - {ok, Create} = request_api(post, api_path("trace"), Header, [GoodName | Trace]), + ?assertMatch({ok, _}, request_api(delete, api_path("trace"), [])), + {ok, Create} = request_api(post, api_path("trace"), [GoodName | Trace]), %% new name but same trace GoodName2 = {<<"name">>, <<"test-name-1">>}, ?assertMatch( - {error, {"HTTP/1.1", 409, _}, _}, - request_api(post, api_path("trace"), Header, [GoodName2 | Trace]) + {error, {"HTTP/1.1", 409, _}}, + request_api(post, api_path("trace"), [GoodName2 | Trace]) ), unload(), @@ -202,14 +195,13 @@ t_log_file(_Config) -> || _ <- lists:seq(1, 5) ], ok = emqx_trace_handler_SUITE:filesync(Name, clientid), - Header = auth_header_(), ?assertMatch( - {error, {"HTTP/1.1", 404, "Not Found"}, _}, - request_api(get, api_path("trace/test_client_not_found/log_detail"), Header) + {error, {"HTTP/1.1", 404, "Not Found"}}, + request_api(get, api_path("trace/test_client_not_found/log_detail")) ), - {ok, Detail} = request_api(get, api_path("trace/test_client_id/log_detail"), Header), + {ok, Detail} = request_api(get, api_path("trace/test_client_id/log_detail")), ?assertMatch([#{<<"mtime">> := _, <<"size">> := _, <<"node">> := _}], json(Detail)), - {ok, Binary} = request_api(get, api_path("trace/test_client_id/download"), Header), + {ok, Binary} = request_api(get, api_path("trace/test_client_id/download")), {ok, [ Comment, #zip_file{ @@ -221,7 +213,7 @@ t_log_file(_Config) -> ZipNamePrefix = lists:flatten(io_lib:format("~s-trace_~s", [node(), Name])), ?assertNotEqual(nomatch, re:run(ZipName, [ZipNamePrefix])), Path = api_path("trace/test_client_id/download?node=" ++ atom_to_list(node())), - {ok, Binary2} = request_api(get, Path, Header), + {ok, Binary2} = request_api(get, Path), ?assertMatch( {ok, [ Comment, @@ -232,25 +224,22 @@ t_log_file(_Config) -> ]}, zip:table(Binary2) ), - {error, {_, 400, _}, _} = + {error, {_, 400, _}} = request_api( get, - api_path("trace/test_client_id/download?node=unknonwn_node"), - Header + api_path("trace/test_client_id/download?node=unknonwn_node") ), - {error, {_, 400, _}, _} = + {error, {_, 400, _}} = request_api( get, % known atom but unknown node - api_path("trace/test_client_id/download?node=undefined"), - Header + api_path("trace/test_client_id/download?node=undefined") ), ?assertMatch( - {error, {"HTTP/1.1", 404, "Not Found"}, _}, + {error, {"HTTP/1.1", 404, "Not Found"}}, request_api( get, - api_path("trace/test_client_not_found/download?node=" ++ atom_to_list(node())), - Header + api_path("trace/test_client_not_found/download?node=" ++ atom_to_list(node())) ) ), ok = emqtt:disconnect(Client), @@ -297,34 +286,30 @@ t_stream_log(_Config) -> ct:pal("FileName: ~p", [File]), {ok, FileBin} = file:read_file(File), ct:pal("FileBin: ~p ~s", [byte_size(FileBin), FileBin]), - Header = auth_header_(), - {ok, Binary} = request_api(get, api_path("trace/test_stream_log/log?bytes=10"), Header), + {ok, Binary} = request_api(get, api_path("trace/test_stream_log/log?bytes=10")), #{<<"meta">> := Meta, <<"items">> := Bin} = json(Binary), ?assertEqual(10, byte_size(Bin)), ?assertEqual(#{<<"position">> => 10, <<"bytes">> => 10}, Meta), Path = api_path("trace/test_stream_log/log?position=20&bytes=10"), - {ok, Binary1} = request_api(get, Path, Header), + {ok, Binary1} = request_api(get, Path), #{<<"meta">> := Meta1, <<"items">> := Bin1} = json(Binary1), ?assertEqual(#{<<"position">> => 30, <<"bytes">> => 10}, Meta1), ?assertEqual(10, byte_size(Bin1)), - {error, {_, 400, _}, _} = + {error, {_, 400, _}} = request_api( get, - api_path("trace/test_stream_log/log?node=unknonwn_node"), - Header + api_path("trace/test_stream_log/log?node=unknonwn_node") ), - {error, {_, 400, _}, _} = + {error, {_, 400, _}} = request_api( get, % known atom but not a node - api_path("trace/test_stream_log/log?node=undefined"), - Header + api_path("trace/test_stream_log/log?node=undefined") ), - {error, {_, 404, _}, _} = + {error, {_, 404, _}} = request_api( get, - api_path("trace/test_stream_log_not_found/log"), - Header + api_path("trace/test_stream_log_not_found/log") ), unload(), ok. @@ -332,29 +317,15 @@ t_stream_log(_Config) -> to_rfc3339(Second) -> list_to_binary(calendar:system_time_to_rfc3339(Second)). -request_api(Method, Url, Auth) -> do_request_api(Method, {Url, [Auth]}). +request_api(Method, Url) -> + request_api(Method, Url, []). -request_api(Method, Url, Auth, Body) -> - Request = {Url, [Auth], "application/json", emqx_json:encode(Body)}, - do_request_api(Method, Request). - -do_request_api(Method, Request) -> - ct:pal("Method: ~p, Request: ~p", [Method, Request]), - case httpc:request(Method, Request, [], [{body_format, binary}]) of - {error, socket_closed_remotely} -> - {error, socket_closed_remotely}; - {error, {shutdown, server_closed}} -> - {error, server_closed}; - {ok, {{"HTTP/1.1", Code, _}, _Headers, Return}} when - Code =:= 200 orelse Code =:= 201 orelse Code =:= 204 - -> - {ok, Return}; - {ok, {Reason, _Header, Body}} -> - {error, Reason, Body} - end. +request_api(Method, Url, Body) -> + Opts = #{httpc_req_opts => [{body_format, binary}]}, + emqx_mgmt_api_test_util:request_api(Method, Url, [], [], Body, Opts). api_path(Path) -> - ?HOST ++ filename:join([?BASE_PATH, ?API_VERSION, Path]). + emqx_mgmt_api_test_util:api_path([Path]). json(Data) -> {ok, Jsx} = emqx_json:safe_decode(Data, [return_maps]), From 30a5cfaa83ac5e90c1ce2a80a43a22c2f6dcee78 Mon Sep 17 00:00:00 2001 From: Erik Timan Date: Tue, 10 Jan 2023 15:58:34 +0100 Subject: [PATCH 0214/1802] fix(emqx_management): remove trace files after zip download We only deleted the resulting zip after a trace file download, not the actual trace files. This adds a deletion of the uncompressed trace files as well. It also creates unique directories when collecting trace files so that concurrent downloads doesn't overwrite files in transit. --- .../src/emqx_mgmt_api_trace.erl | 25 ++++++++--- .../test/emqx_mgmt_api_trace_SUITE.erl | 44 ++++++++++++++++++- 2 files changed, 61 insertions(+), 8 deletions(-) diff --git a/apps/emqx_management/src/emqx_mgmt_api_trace.erl b/apps/emqx_management/src/emqx_mgmt_api_trace.erl index d90aea9ef..5ad04a68c 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_trace.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_trace.erl @@ -20,6 +20,7 @@ -include_lib("kernel/include/file.hrl"). -include_lib("typerefl/include/types.hrl"). -include_lib("emqx/include/logger.hrl"). +-include_lib("snabbkaffe/include/snabbkaffe.hrl"). -export([ api_spec/0, @@ -461,16 +462,26 @@ download_trace_log(get, #{bindings := #{name := Name}, query_string := Query}) - case parse_node(Query, undefined) of {ok, Node} -> TraceFiles = collect_trace_file(Node, TraceLog), - ZipDir = emqx_trace:zip_dir(), + %% We generate a session ID so that we name files + %% with unique names. Then we won't cause + %% overwrites for concurrent requests. + SessionId = emqx_misc:gen_id(), + ZipDir = filename:join([emqx_trace:zip_dir(), SessionId]), + ok = file:make_dir(ZipDir), + %% Write files to ZipDir and create an in-memory zip file Zips = group_trace_file(ZipDir, TraceLog, TraceFiles), - FileName = binary_to_list(Name) ++ ".zip", - ZipFileName = filename:join([ZipDir, FileName]), - {ok, ZipFile} = zip:zip(ZipFileName, Zips, [{cwd, ZipDir}]), + ZipName = binary_to_list(Name) ++ ".zip", + {ok, {ZipName, Binary}} = zip:zip(ZipName, Zips, [memory, {cwd, ZipDir}]), %% emqx_trace:delete_files_after_send(ZipFileName, Zips), %% TODO use file replace file_binary.(delete file after send is not ready now). - {ok, Binary} = file:read_file(ZipFile), - ZipName = filename:basename(ZipFile), - _ = file:delete(ZipFile), + ok = file:del_dir_r(ZipDir), + ?tp(trace_api_download_trace_log, #{ + files => Zips, + name => Name, + session_id => SessionId, + zip_dir => ZipDir, + zip_name => ZipName + }), Headers = #{ <<"content-type">> => <<"application/x-zip">>, <<"content-disposition">> => iolist_to_binary( diff --git a/apps/emqx_management/test/emqx_mgmt_api_trace_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_api_trace_SUITE.erl index 33240b64f..6962a9043 100644 --- a/apps/emqx_management/test/emqx_mgmt_api_trace_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_api_trace_SUITE.erl @@ -168,7 +168,8 @@ t_create_failed(_Config) -> ), %% clear ?assertMatch({ok, _}, request_api(delete, api_path("trace"), [])), - {ok, Create} = request_api(post, api_path("trace"), [GoodName | Trace]), + {ok, Create1} = request_api(post, api_path("trace"), [GoodName | Trace]), + ?assertMatch(#{<<"name">> := <<"test-name-0">>}, json(Create1)), %% new name but same trace GoodName2 = {<<"name">>, <<"test-name-1">>}, ?assertMatch( @@ -314,6 +315,47 @@ t_stream_log(_Config) -> unload(), ok. +t_trace_files_are_deleted_after_download(_Config) -> + ClientId = <<"client-test-delete-after-download">>, + Now = erlang:system_time(second), + Name = <<"test_client_id">>, + load(), + create_trace(Name, ClientId, Now), + {ok, Client} = emqtt:start_link([{clean_start, true}, {clientid, ClientId}]), + {ok, _} = emqtt:connect(Client), + [ + begin + _ = emqtt:ping(Client) + end + || _ <- lists:seq(1, 5) + ], + ok = emqtt:disconnect(Client), + ok = emqx_trace_handler_SUITE:filesync(Name, clientid), + + %% Check that files have been removed after download and that zip + %% directories uses unique session ids + ?check_trace( + begin + %% Download two zip files + Path = api_path(["trace/", binary_to_list(Name), "/download"]), + {ok, Binary1} = request_api(get, Path), + {ok, Binary2} = request_api(get, Path), + ?assertMatch({ok, _}, zip:table(Binary1)), + ?assertMatch({ok, _}, zip:table(Binary2)) + end, + fun(Trace) -> + [ + #{session_id := SessionId1, zip_dir := ZipDir1}, + #{session_id := SessionId2, zip_dir := ZipDir2} + ] = ?of_kind(trace_api_download_trace_log, Trace), + ?assertEqual({error, enoent}, file:list_dir(ZipDir1)), + ?assertEqual({error, enoent}, file:list_dir(ZipDir2)), + ?assertNotEqual(SessionId1, SessionId2), + ?assertNotEqual(ZipDir1, ZipDir2) + end + ), + ok. + to_rfc3339(Second) -> list_to_binary(calendar:system_time_to_rfc3339(Second)). From 2cf297029d925640f8e660687e4b62b659f3669d Mon Sep 17 00:00:00 2001 From: Erik Timan Date: Tue, 17 Jan 2023 14:23:44 +0100 Subject: [PATCH 0215/1802] chore: add changes --- changes/v5.0.15/fix-9781.en.md | 1 + changes/v5.0.15/fix-9781.zh.md | 1 + 2 files changed, 2 insertions(+) create mode 100644 changes/v5.0.15/fix-9781.en.md create mode 100644 changes/v5.0.15/fix-9781.zh.md diff --git a/changes/v5.0.15/fix-9781.en.md b/changes/v5.0.15/fix-9781.en.md new file mode 100644 index 000000000..2b34ddc24 --- /dev/null +++ b/changes/v5.0.15/fix-9781.en.md @@ -0,0 +1 @@ +Trace files were left on a node when creating a zip file for download. They are now removed when the file is sent. Also, concurrent downloads will no longer interfere with each other. diff --git a/changes/v5.0.15/fix-9781.zh.md b/changes/v5.0.15/fix-9781.zh.md new file mode 100644 index 000000000..5c4cee0f5 --- /dev/null +++ b/changes/v5.0.15/fix-9781.zh.md @@ -0,0 +1 @@ +当下载 日志追踪 的日志时,一些中间文件将存留在处理节点上,现在这个问题得到了修复。同时,并发下载日志将不再相互干扰。 From e54f2f83b3222db496e04babc24c5dae02fdcce7 Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Tue, 17 Jan 2023 09:59:52 +0100 Subject: [PATCH 0216/1802] test: use same default timeout as elsewhere --- apps/emqx_resource/src/emqx_resource_manager.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx_resource/src/emqx_resource_manager.erl b/apps/emqx_resource/src/emqx_resource_manager.erl index 170dfe162..ab726976a 100644 --- a/apps/emqx_resource/src/emqx_resource_manager.erl +++ b/apps/emqx_resource/src/emqx_resource_manager.erl @@ -171,7 +171,7 @@ create_dry_run(ResourceType, Config) -> ok = emqx_resource_manager_sup:ensure_child( MgrId, ResId, <<"dry_run">>, ResourceType, Config, #{} ), - case wait_for_ready(ResId, 15000) of + case wait_for_ready(ResId, 5000) of ok -> remove(ResId); {error, Reason} -> From d164e5bc0f1ee78d3df23b7e6ac33c7ef447ce2a Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Tue, 17 Jan 2023 09:59:32 +0100 Subject: [PATCH 0217/1802] fix: don't require ssl conf --- apps/emqx_bridge/src/emqx_bridge_resource.erl | 4 +++- apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/emqx_bridge/src/emqx_bridge_resource.erl b/apps/emqx_bridge/src/emqx_bridge_resource.erl index 4f8d248b2..3b6b3a431 100644 --- a/apps/emqx_bridge/src/emqx_bridge_resource.erl +++ b/apps/emqx_bridge/src/emqx_bridge_resource.erl @@ -251,7 +251,9 @@ maybe_clear_certs(TmpPath, #{ssl := SslConf} = Conf) -> case is_tmp_path_conf(TmpPath, SslConf) of true -> emqx_connector_ssl:clear_certs(TmpPath, Conf); false -> ok - end. + end; +maybe_clear_certs(_TmpPath, _ConfWithoutSsl) -> + ok. is_tmp_path_conf(TmpPath, #{certfile := Certfile}) -> is_tmp_path(TmpPath, Certfile); diff --git a/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl b/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl index a5d7968a2..6b0b3a941 100644 --- a/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl +++ b/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl @@ -86,6 +86,7 @@ init_per_testcase(_, Config) -> {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000), {Port, Sock, Acceptor} = start_http_server(fun handle_fun_200_ok/2), [{port, Port}, {sock, Sock}, {acceptor, Acceptor} | Config]. + end_per_testcase(_, Config) -> Sock = ?config(sock, Config), Acceptor = ?config(acceptor, Config), From 5fc31dcd9ee0851cb4165cf8bf4c924a36747e34 Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Tue, 17 Jan 2023 15:27:58 +0100 Subject: [PATCH 0218/1802] fix: use atom keys in example to make sure they exist --- .../src/emqx_ee_bridge_gcp_pubsub.erl | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_gcp_pubsub.erl b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_gcp_pubsub.erl index 83fe31b49..1bee9e789 100644 --- a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_gcp_pubsub.erl +++ b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_gcp_pubsub.erl @@ -149,31 +149,31 @@ values(get) -> maps:merge(values(post), ?METRICS_EXAMPLE); values(post) -> #{ - <<"pubsub_topic">> => <<"mytopic">>, - <<"service_account_json">> => + pubsub_topic => <<"mytopic">>, + service_account_json => #{ - <<"auth_provider_x509_cert_url">> => + auth_provider_x509_cert_url => <<"https://www.googleapis.com/oauth2/v1/certs">>, - <<"auth_uri">> => + auth_uri => <<"https://accounts.google.com/o/oauth2/auth">>, - <<"client_email">> => + client_email => <<"test@myproject.iam.gserviceaccount.com">>, - <<"client_id">> => <<"123812831923812319190">>, - <<"client_x509_cert_url">> => + client_id => <<"123812831923812319190">>, + client_x509_cert_url => << "https://www.googleapis.com/robot/v1/" "metadata/x509/test%40myproject.iam.gserviceaccount.com" >>, - <<"private_key">> => + private_key => << "-----BEGIN PRIVATE KEY-----\n" "MIIEvQI..." >>, - <<"private_key_id">> => <<"kid">>, - <<"project_id">> => <<"myproject">>, - <<"token_uri">> => + private_key_id => <<"kid">>, + project_id => <<"myproject">>, + token_uri => <<"https://oauth2.googleapis.com/token">>, - <<"type">> => <<"service_account">> + type => <<"service_account">> } }; values(put) -> From f37b3e4bc410d201d71a96b64077e77e58bad8b8 Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Tue, 17 Jan 2023 15:28:18 +0100 Subject: [PATCH 0219/1802] test: test against `bridges_probe` API --- .../emqx_ee_bridge/test/emqx_ee_bridge_gcp_pubsub_SUITE.erl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_gcp_pubsub_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_gcp_pubsub_SUITE.erl index 0eadf46ad..9977e70a7 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_gcp_pubsub_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_gcp_pubsub_SUITE.erl @@ -196,13 +196,17 @@ create_bridge_http(Config, GCPPubSubConfigOverrides) -> Params = GCPPubSubConfig#{<<"type">> => TypeBin, <<"name">> => Name}, Path = emqx_mgmt_api_test_util:api_path(["bridges"]), AuthHeader = emqx_mgmt_api_test_util:auth_header_(), + ProbePath = emqx_mgmt_api_test_util:api_path(["bridges_probe"]), + ProbeResult = emqx_mgmt_api_test_util:request_api(post, ProbePath, "", AuthHeader, Params), ct:pal("creating bridge (via http): ~p", [Params]), + ct:pal("probe result: ~p", [ProbeResult]), Res = case emqx_mgmt_api_test_util:request_api(post, Path, "", AuthHeader, Params) of {ok, Res0} -> {ok, emqx_json:decode(Res0, [return_maps])}; Error -> Error end, ct:pal("bridge creation result: ~p", [Res]), + ?assertEqual(element(1, ProbeResult), element(1, Res)), Res. create_rule_and_action_http(Config) -> @@ -672,7 +676,7 @@ t_create_via_http(Config) -> create_bridge_http(Config), fun(Res, Trace) -> ?assertMatch({ok, _}, Res), - ?assertMatch([_], ?of_kind(gcp_pubsub_bridge_jwt_created, Trace)), + ?assertMatch([_, _], ?of_kind(gcp_pubsub_bridge_jwt_created, Trace)), ok end ), From 1d7364dab19d4a1cafc229b5620542d0b02d5d88 Mon Sep 17 00:00:00 2001 From: Ilya Averyanov Date: Mon, 16 Jan 2023 22:38:05 +0200 Subject: [PATCH 0220/1802] fix(authn): fix authn hook chain evaluation Stop authentication handling when `emqx_authentication` provides a definitive result. --- apps/emqx/src/emqx_authentication.erl | 2 +- apps/emqx/test/emqx_authentication_SUITE.erl | 23 +++++++++++++++----- changes/v5.0.15/fix-9785.en.md | 1 + changes/v5.0.15/fix-9785.zh.md | 1 + 4 files changed, 21 insertions(+), 6 deletions(-) create mode 100644 changes/v5.0.15/fix-9785.en.md create mode 100644 changes/v5.0.15/fix-9785.zh.md diff --git a/apps/emqx/src/emqx_authentication.erl b/apps/emqx/src/emqx_authentication.erl index 017bc982d..ec8a1c113 100644 --- a/apps/emqx/src/emqx_authentication.erl +++ b/apps/emqx/src/emqx_authentication.erl @@ -661,7 +661,7 @@ do_authenticate( _ -> ok end, - {ok, Result} + {stop, Result} catch Class:Reason:Stacktrace -> ?TRACE_AUTHN(warning, "authenticator_error", #{ diff --git a/apps/emqx/test/emqx_authentication_SUITE.erl b/apps/emqx/test/emqx_authentication_SUITE.erl index e2e95d7ad..f59f2ca02 100644 --- a/apps/emqx/test/emqx_authentication_SUITE.erl +++ b/apps/emqx/test/emqx_authentication_SUITE.erl @@ -106,6 +106,10 @@ authenticate(#{username := <<"good">>}, _State) -> {ok, #{is_superuser => true}}; authenticate(#{username := <<"ignore">>}, _State) -> ignore; +authenticate(#{username := <<"emqx_authn_ignore_for_hook_good">>}, _State) -> + ignore; +authenticate(#{username := <<"emqx_authn_ignore_for_hook_bad">>}, _State) -> + ignore; authenticate(#{username := _}, _State) -> {error, bad_username_or_password}. @@ -117,6 +121,10 @@ hook_authenticate(#{username := <<"hook_user_finally_good">>}, _AuthResult) -> {stop, {ok, ?NOT_SUPERUSER}}; hook_authenticate(#{username := <<"hook_user_finally_bad">>}, _AuthResult) -> {stop, {error, invalid_username}}; +hook_authenticate(#{username := <<"emqx_authn_ignore_for_hook_good">>}, _AuthResult) -> + {ok, {ok, ?NOT_SUPERUSER}}; +hook_authenticate(#{username := <<"emqx_authn_ignore_for_hook_bad">>}, _AuthResult) -> + {stop, {error, invalid_username}}; hook_authenticate(_ClientId, AuthResult) -> {ok, AuthResult}. @@ -594,12 +602,17 @@ t_combine_authn_and_callback(Config) when is_list(Config) -> ?assertAuthFailureForUser(bad), ?assertAuthFailureForUser(ignore), - %% lower-priority hook can overrride auth result, - %% because emqx_authentication permits/denies with {ok, ...} - ?assertAuthSuccessForUser(hook_user_good), - ?assertAuthFailureForUser(hook_user_bad), - ?assertAuthSuccessForUser(hook_user_finally_good), + %% lower-priority hook can overrride emqx_authentication result + %% for ignored users + ?assertAuthSuccessForUser(emqx_authn_ignore_for_hook_good), + ?assertAuthFailureForUser(emqx_authn_ignore_for_hook_bad), + + %% lower-priority hook cannot overrride + %% successful/unsuccessful emqx_authentication result + ?assertAuthFailureForUser(hook_user_finally_good), ?assertAuthFailureForUser(hook_user_finally_bad), + ?assertAuthFailureForUser(hook_user_good), + ?assertAuthFailureForUser(hook_user_bad), ok = unhook(); t_combine_authn_and_callback({'end', Config}) -> diff --git a/changes/v5.0.15/fix-9785.en.md b/changes/v5.0.15/fix-9785.en.md new file mode 100644 index 000000000..8af14b1ed --- /dev/null +++ b/changes/v5.0.15/fix-9785.en.md @@ -0,0 +1 @@ +Stop authentication hook chain if `emqx_authentication` provides a definitive result. diff --git a/changes/v5.0.15/fix-9785.zh.md b/changes/v5.0.15/fix-9785.zh.md new file mode 100644 index 000000000..6aa84f755 --- /dev/null +++ b/changes/v5.0.15/fix-9785.zh.md @@ -0,0 +1 @@ +如果 `emqx_authentication` 提供了确定的结果,则停止认证钩子链。 From 7a471faf672d5fb9aad6d78f55c8e9d62362e39a Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Tue, 17 Jan 2023 19:57:14 +0100 Subject: [PATCH 0221/1802] refactor(authn): use simple sync query for authn there is no need to route the authn queries through the buffer worker --- apps/emqx_authn/src/simple_authn/emqx_authn_http.erl | 2 +- apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl | 4 ++-- apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl | 2 +- apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl | 4 +++- apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl | 2 +- apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl | 2 +- apps/emqx_authn/test/emqx_authn_mysql_SUITE.erl | 4 ++-- apps/emqx_authn/test/emqx_authn_pgsql_SUITE.erl | 4 ++-- apps/emqx_authn/test/emqx_authn_redis_SUITE.erl | 2 +- 9 files changed, 14 insertions(+), 12 deletions(-) diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl index faa06b71a..b6b68eab9 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl @@ -192,7 +192,7 @@ authenticate( } = State ) -> Request = generate_request(Credential, State), - Response = emqx_resource:query(ResourceId, {Method, Request, RequestTimeout}), + Response = emqx_resource:simple_sync_query(ResourceId, {Method, Request, RequestTimeout}), ?TRACE_AUTHN_PROVIDER("http_response", #{ request => request_for_log(Credential, State), response => response_for_log(Response), diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl index 1c44b4d1f..852139875 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl @@ -187,7 +187,7 @@ update( #{use_jwks := true} = Config, #{jwk_resource := ResourceId} = State ) -> - case emqx_resource:query(ResourceId, {update, connector_opts(Config)}) of + case emqx_resource:simple_sync_query(ResourceId, {update, connector_opts(Config)}) of ok -> case maps:get(verify_claims, Config, undefined) of undefined -> @@ -229,7 +229,7 @@ authenticate( from := From } ) -> - case emqx_resource:query(ResourceId, get_jwks) of + case emqx_resource:simple_sync_query(ResourceId, get_jwks) of {error, Reason} -> ?TRACE_AUTHN_PROVIDER(error, "get_jwks_failed", #{ resource => ResourceId, diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl index 3f140a8eb..bcdc7b0b1 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl @@ -167,7 +167,7 @@ authenticate( } = State ) -> Filter = emqx_authn_utils:render_deep(FilterTemplate, Credential), - case emqx_resource:query(ResourceId, {find_one, Collection, Filter, #{}}) of + case emqx_resource:simple_sync_query(ResourceId, {find_one, Collection, Filter, #{}}) of {ok, undefined} -> ignore; {error, Reason} -> diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl index ffce42bb3..0e6eeb6af 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl @@ -118,7 +118,9 @@ authenticate( } ) -> Params = emqx_authn_utils:render_sql_params(TmplToken, Credential), - case emqx_resource:query(ResourceId, {prepared_query, ?PREPARE_KEY, Params, Timeout}) of + case + emqx_resource:simple_sync_query(ResourceId, {prepared_query, ?PREPARE_KEY, Params, Timeout}) + of {ok, _Columns, []} -> ignore; {ok, Columns, [Row | _]} -> diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl index 2d7974301..3d9e1e08f 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl @@ -120,7 +120,7 @@ authenticate( } ) -> Params = emqx_authn_utils:render_sql_params(PlaceHolders, Credential), - case emqx_resource:query(ResourceId, {prepared_query, ResourceId, Params}) of + case emqx_resource:simple_sync_query(ResourceId, {prepared_query, ResourceId, Params}) of {ok, _Columns, []} -> ignore; {ok, Columns, [Row | _]} -> diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl index 12b7422b5..8c9cb5333 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl @@ -139,7 +139,7 @@ authenticate( ) -> NKey = emqx_authn_utils:render_str(KeyTemplate, Credential), Command = [CommandName, NKey | Fields], - case emqx_resource:query(ResourceId, {cmd, Command}) of + case emqx_resource:simple_sync_query(ResourceId, {cmd, Command}) of {ok, []} -> ignore; {ok, Values} -> diff --git a/apps/emqx_authn/test/emqx_authn_mysql_SUITE.erl b/apps/emqx_authn/test/emqx_authn_mysql_SUITE.erl index 7681b6ee3..9d2c5cf63 100644 --- a/apps/emqx_authn/test/emqx_authn_mysql_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_mysql_SUITE.erl @@ -516,13 +516,13 @@ init_seeds() -> ). q(Sql) -> - emqx_resource:query( + emqx_resource:simple_sync_query( ?MYSQL_RESOURCE, {sql, Sql} ). q(Sql, Params) -> - emqx_resource:query( + emqx_resource:simple_sync_query( ?MYSQL_RESOURCE, {sql, Sql, Params} ). diff --git a/apps/emqx_authn/test/emqx_authn_pgsql_SUITE.erl b/apps/emqx_authn/test/emqx_authn_pgsql_SUITE.erl index 66529525b..ff0bfaea7 100644 --- a/apps/emqx_authn/test/emqx_authn_pgsql_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_pgsql_SUITE.erl @@ -594,13 +594,13 @@ create_user(Values) -> ok. q(Sql) -> - emqx_resource:query( + emqx_resource:simple_sync_query( ?PGSQL_RESOURCE, {query, Sql} ). q(Sql, Params) -> - emqx_resource:query( + emqx_resource:simple_sync_query( ?PGSQL_RESOURCE, {query, Sql, Params} ). diff --git a/apps/emqx_authn/test/emqx_authn_redis_SUITE.erl b/apps/emqx_authn/test/emqx_authn_redis_SUITE.erl index 1df1faa4c..4f25c8709 100644 --- a/apps/emqx_authn/test/emqx_authn_redis_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_redis_SUITE.erl @@ -588,7 +588,7 @@ init_seeds() -> ). q(Command) -> - emqx_resource:query( + emqx_resource:simple_sync_query( ?REDIS_RESOURCE, {cmd, Command} ). From fff6bf921f067c31550de31e99312dd0aada702d Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Tue, 17 Jan 2023 20:01:45 +0100 Subject: [PATCH 0222/1802] refactor(authz): call emqx_resource:simple_sync_query there is no need to route the request through the buffer workers --- apps/emqx_authz/src/emqx_authz_http.erl | 2 +- apps/emqx_authz/src/emqx_authz_mongodb.erl | 2 +- apps/emqx_authz/src/emqx_authz_mysql.erl | 4 +++- apps/emqx_authz/src/emqx_authz_postgresql.erl | 4 +++- apps/emqx_authz/src/emqx_authz_redis.erl | 2 +- apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl | 4 ++-- apps/emqx_authz/test/emqx_authz_postgresql_SUITE.erl | 4 ++-- apps/emqx_authz/test/emqx_authz_redis_SUITE.erl | 2 +- 8 files changed, 14 insertions(+), 10 deletions(-) diff --git a/apps/emqx_authz/src/emqx_authz_http.erl b/apps/emqx_authz/src/emqx_authz_http.erl index 4479d1483..852a667c8 100644 --- a/apps/emqx_authz/src/emqx_authz_http.erl +++ b/apps/emqx_authz/src/emqx_authz_http.erl @@ -82,7 +82,7 @@ authorize( } = Config ) -> Request = generate_request(PubSub, Topic, Client, Config), - case emqx_resource:query(ResourceID, {Method, Request, RequestTimeout}) of + case emqx_resource:simple_sync_query(ResourceID, {Method, Request, RequestTimeout}) of {ok, 204, _Headers} -> {matched, allow}; {ok, 200, Headers, Body} -> diff --git a/apps/emqx_authz/src/emqx_authz_mongodb.erl b/apps/emqx_authz/src/emqx_authz_mongodb.erl index 840ce8fb9..8d04a3229 100644 --- a/apps/emqx_authz/src/emqx_authz_mongodb.erl +++ b/apps/emqx_authz/src/emqx_authz_mongodb.erl @@ -79,7 +79,7 @@ authorize( RenderedFilter = emqx_authz_utils:render_deep(FilterTemplate, Client), Result = try - emqx_resource:query(ResourceID, {find, Collection, RenderedFilter, #{}}) + emqx_resource:simple_sync_query(ResourceID, {find, Collection, RenderedFilter, #{}}) catch error:Error -> {error, Error} end, diff --git a/apps/emqx_authz/src/emqx_authz_mysql.erl b/apps/emqx_authz/src/emqx_authz_mysql.erl index bd81a7398..768479e98 100644 --- a/apps/emqx_authz/src/emqx_authz_mysql.erl +++ b/apps/emqx_authz/src/emqx_authz_mysql.erl @@ -82,7 +82,9 @@ authorize( } ) -> RenderParams = emqx_authz_utils:render_sql_params(TmplToken, Client), - case emqx_resource:query(ResourceID, {prepared_query, ?PREPARE_KEY, RenderParams}) of + case + emqx_resource:simple_sync_query(ResourceID, {prepared_query, ?PREPARE_KEY, RenderParams}) + of {ok, _Columns, []} -> nomatch; {ok, Columns, Rows} -> diff --git a/apps/emqx_authz/src/emqx_authz_postgresql.erl b/apps/emqx_authz/src/emqx_authz_postgresql.erl index ae58efee1..05f2315a6 100644 --- a/apps/emqx_authz/src/emqx_authz_postgresql.erl +++ b/apps/emqx_authz/src/emqx_authz_postgresql.erl @@ -87,7 +87,9 @@ authorize( } ) -> RenderedParams = emqx_authz_utils:render_sql_params(Placeholders, Client), - case emqx_resource:query(ResourceID, {prepared_query, ResourceID, RenderedParams}) of + case + emqx_resource:simple_sync_query(ResourceID, {prepared_query, ResourceID, RenderedParams}) + of {ok, _Columns, []} -> nomatch; {ok, Columns, Rows} -> diff --git a/apps/emqx_authz/src/emqx_authz_redis.erl b/apps/emqx_authz/src/emqx_authz_redis.erl index 52469ad43..34c2038ab 100644 --- a/apps/emqx_authz/src/emqx_authz_redis.erl +++ b/apps/emqx_authz/src/emqx_authz_redis.erl @@ -78,7 +78,7 @@ authorize( } ) -> Cmd = emqx_authz_utils:render_deep(CmdTemplate, Client), - case emqx_resource:query(ResourceID, {cmd, Cmd}) of + case emqx_resource:simple_sync_query(ResourceID, {cmd, Cmd}) of {ok, []} -> nomatch; {ok, Rows} -> diff --git a/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl b/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl index 38c997f17..ad9a23377 100644 --- a/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl @@ -321,13 +321,13 @@ raw_mysql_authz_config() -> }. q(Sql) -> - emqx_resource:query( + emqx_resource:simple_sync_query( ?MYSQL_RESOURCE, {sql, Sql} ). q(Sql, Params) -> - emqx_resource:query( + emqx_resource:simple_sync_query( ?MYSQL_RESOURCE, {sql, Sql, Params} ). diff --git a/apps/emqx_authz/test/emqx_authz_postgresql_SUITE.erl b/apps/emqx_authz/test/emqx_authz_postgresql_SUITE.erl index fbe17f92e..f17c2de1c 100644 --- a/apps/emqx_authz/test/emqx_authz_postgresql_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_postgresql_SUITE.erl @@ -326,13 +326,13 @@ raw_pgsql_authz_config() -> }. q(Sql) -> - emqx_resource:query( + emqx_resource:simple_sync_query( ?PGSQL_RESOURCE, {query, Sql} ). insert(Sql, Params) -> - {ok, _} = emqx_resource:query( + {ok, _} = emqx_resource:simple_sync_query( ?PGSQL_RESOURCE, {query, Sql, Params} ), diff --git a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl index ae734f20d..15b180c96 100644 --- a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl @@ -263,7 +263,7 @@ raw_redis_authz_config() -> }. q(Command) -> - emqx_resource:query( + emqx_resource:simple_sync_query( ?REDIS_RESOURCE, {cmd, Command} ). From fc992f28bc78a743239ec1eafea221fc754c95aa Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Tue, 17 Jan 2023 20:13:01 +0100 Subject: [PATCH 0223/1802] test: add test coverage --- apps/emqx/test/emqx_schema_tests.erl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apps/emqx/test/emqx_schema_tests.erl b/apps/emqx/test/emqx_schema_tests.erl index b249dea92..e1ac1874f 100644 --- a/apps/emqx/test/emqx_schema_tests.erl +++ b/apps/emqx/test/emqx_schema_tests.erl @@ -465,3 +465,10 @@ converter_invalid_input_test() -> ?assertEqual(undefined, emqx_schema:convert_servers(undefined)), %% 'foo: bar' is a valid HOCON value, but 'bar' is not a port number ?assertThrow("bad_host_port", emqx_schema:convert_servers(#{foo => bar})). + +password_converter_test() -> + ?assertEqual(undefined, emqx_schema:password_converter(undefined, #{})), + ?assertEqual(<<"123">>, emqx_schema:password_converter(123, #{})), + ?assertEqual(<<"123">>, emqx_schema:password_converter(<<"123">>, #{})), + ?assertThrow("must_quote", emqx_schema:password_converter(foobar, #{})), + ok. From 4c25be8a2cdda769950ca14b1cc288aac25cceec Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Mon, 9 Jan 2023 15:16:23 -0300 Subject: [PATCH 0224/1802] chore(influxdb): remove deprecated value from example --- lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_influxdb.erl | 1 - 1 file changed, 1 deletion(-) diff --git a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_influxdb.erl b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_influxdb.erl index f0591a2e6..6d96e3883 100644 --- a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_influxdb.erl +++ b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_influxdb.erl @@ -92,7 +92,6 @@ values(common, Protocol, SupportUint, TypeOpts) -> "bool=${payload.bool}">>, precision => ms, resource_opts => #{ - enable_batch => false, batch_size => 100, batch_time => <<"20ms">> }, From c3835584674a0ac7f287a34353f0d13abcfaacad Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Fri, 6 Jan 2023 14:05:16 -0300 Subject: [PATCH 0225/1802] fix(buffer): fix `replayq` usages in buffer workers (5.0) https://emqx.atlassian.net/browse/EMQX-8700 Fixes a few errors in the usage of `replayq` queues. - Close `replayq` when `emqx_resource_worker` terminates. - Do not keep old references to `replayq` after any `pop`s. - Clear `replayq`'s data directories when removing a resource. --- .../src/emqx_resource_worker.erl | 72 ++++++++++++---- .../src/emqx_resource_worker_sup.erl | 7 +- .../test/emqx_connector_demo.erl | 16 ++++ .../test/emqx_resource_SUITE.erl | 82 +++++++++++++++++++ 4 files changed, 160 insertions(+), 17 deletions(-) diff --git a/apps/emqx_resource/src/emqx_resource_worker.erl b/apps/emqx_resource/src/emqx_resource_worker.erl index 7840fd474..9b264cfcb 100644 --- a/apps/emqx_resource/src/emqx_resource_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_worker.erl @@ -53,6 +53,8 @@ -export([reply_after_query/7, batch_reply_after_query/7]). +-export([clear_disk_queue_dir/2]). + -elvis([{elvis_style, dont_repeat_yourself, disable}]). -define(Q_ITEM(REQUEST), {q_item, REQUEST}). @@ -176,6 +178,7 @@ init({Id, Index, Opts}) -> resume_interval => maps:get(resume_interval, Opts, HCItvl), tref => undefined }, + ?tp(resource_worker_init, #{id => Id, index => Index}), {ok, blocked, St, {next_event, cast, resume}}. running(enter, _, St) -> @@ -232,7 +235,9 @@ blocked(info, Info, _Data) -> terminate(_Reason, #{id := Id, index := Index, queue := Q}) -> emqx_resource_metrics:inflight_set(Id, Index, 0), emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q)), - gproc_pool:disconnect_worker(Id, {Id, Index}). + gproc_pool:disconnect_worker(Id, {Id, Index}), + replayq:close(Q), + ok. code_change(_OldVsn, State, _Extra) -> {ok, State}. @@ -432,7 +437,11 @@ flush(Data0) -> {Q1, QAckRef, Batch0} = replayq:pop(Q0, #{count_limit => BatchSize}), Batch = [Item || ?Q_ITEM(Item) <- Batch0], IsBatch = BatchSize =/= 1, - do_flush(Data0, #{ + %% We *must* use the new queue, because we currently can't + %% `nack' a `pop'. + %% Maybe we could re-open the queue? + Data1 = Data0#{queue := Q1}, + do_flush(Data1, #{ new_queue => Q1, is_batch => IsBatch, batch => Batch, @@ -463,24 +472,35 @@ do_flush(Data0, #{is_batch := false, batch := Batch, ack_ref := QAckRef, new_que %% failed and is not async; keep the request in the queue to %% be retried {true, false} -> - {next_state, blocked, Data1}; + %% Note: currently, we cannot safely pop an item from + %% `replayq', keep the old reference to the queue and + %% later try to append new items to the old ref: by + %% popping an item, we may cause the side effect of + %% closing an open segment and opening a new one, and the + %% later `append' with the old file descriptor will fail + %% with `einval' because it has been closed. So we are + %% forced to re-append the item, changing the order of + %% requests... + ok = replayq:ack(Q1, QAckRef), + SentBatch = mark_as_sent(Batch), + Q2 = append_queue(Id, Index, Q1, SentBatch), + Data2 = Data1#{queue := Q2}, + {next_state, blocked, Data2}; %% failed and is async; remove the request from the queue, as %% it is already in inflight table {true, true} -> ok = replayq:ack(Q1, QAckRef), emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), - Data = Data1#{queue := Q1}, - {next_state, blocked, Data}; + {next_state, blocked, Data1}; %% success; just ack {false, _} -> ok = replayq:ack(Q1, QAckRef), emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), - Data2 = Data1#{queue := Q1}, case replayq:count(Q1) > 0 of true -> - {keep_state, Data2, [{next_event, internal, flush}]}; + {keep_state, Data1, [{next_event, internal, flush}]}; false -> - {keep_state, Data2} + {keep_state, Data1} end end; do_flush(Data0, #{is_batch := true, batch := Batch, ack_ref := QAckRef, new_queue := Q1}) -> @@ -498,28 +518,39 @@ do_flush(Data0, #{is_batch := true, batch := Batch, ack_ref := QAckRef, new_queu %% failed and is not async; keep the request in the queue to %% be retried {true, false} -> - {next_state, blocked, Data1}; + %% Note: currently, we cannot safely pop an item from + %% `replayq', keep the old reference to the queue and + %% later try to append new items to the old ref: by + %% popping an item, we may cause the side effect of + %% closing an open segment and opening a new one, and the + %% later `append' with the old file descriptor will fail + %% with `einval' because it has been closed. So we are + %% forced to re-append the item, changing the order of + %% requests... + ok = replayq:ack(Q1, QAckRef), + SentBatch = mark_as_sent(Batch), + Q2 = append_queue(Id, Index, Q1, SentBatch), + Data2 = Data1#{queue := Q2}, + {next_state, blocked, Data2}; %% failed and is async; remove the request from the queue, as %% it is already in inflight table {true, true} -> ok = replayq:ack(Q1, QAckRef), emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), - Data = Data1#{queue := Q1}, - {next_state, blocked, Data}; + {next_state, blocked, Data1}; %% success; just ack {false, _} -> ok = replayq:ack(Q1, QAckRef), emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), CurrentCount = replayq:count(Q1), - Data2 = Data1#{queue := Q1}, case {CurrentCount > 0, CurrentCount >= BatchSize} of {false, _} -> - {keep_state, Data2}; + {keep_state, Data1}; {true, true} -> - {keep_state, Data2, [{next_event, internal, flush}]}; + {keep_state, Data1, [{next_event, internal, flush}]}; {true, false} -> - Data3 = ensure_flush_timer(Data2), - {keep_state, Data3} + Data2 = ensure_flush_timer(Data1), + {keep_state, Data2} end end. @@ -874,6 +905,15 @@ disk_queue_dir(Id, Index) -> QDir = binary_to_list(Id) ++ ":" ++ integer_to_list(Index), filename:join([emqx:data_dir(), "resource_worker", node(), QDir]). +clear_disk_queue_dir(Id, Index) -> + ReplayQDir = disk_queue_dir(Id, Index), + case file:del_dir_r(ReplayQDir) of + {error, enoent} -> + ok; + Res -> + Res + end. + ensure_flush_timer(Data = #{tref := undefined, batch_time := T}) -> Ref = make_ref(), TRef = erlang:send_after(T, self(), {flush, Ref}), diff --git a/apps/emqx_resource/src/emqx_resource_worker_sup.erl b/apps/emqx_resource/src/emqx_resource_worker_sup.erl index b6557620c..a5de76eca 100644 --- a/apps/emqx_resource/src/emqx_resource_worker_sup.erl +++ b/apps/emqx_resource/src/emqx_resource_worker_sup.erl @@ -67,7 +67,8 @@ stop_workers(ResId, Opts) -> WorkerPoolSize = worker_pool_size(Opts), lists:foreach( fun(Idx) -> - ensure_worker_removed(ResId, Idx) + ensure_worker_removed(ResId, Idx), + ensure_disk_queue_dir_absent(ResId, Idx) end, lists:seq(1, WorkerPoolSize) ), @@ -127,6 +128,10 @@ ensure_worker_removed(ResId, Idx) -> {error, Reason} end. +ensure_disk_queue_dir_absent(ResourceId, Index) -> + ok = emqx_resource_worker:clear_disk_queue_dir(ResourceId, Index), + ok. + ensure_worker_pool_removed(ResId) -> try gproc_pool:delete(ResId) diff --git a/apps/emqx_resource/test/emqx_connector_demo.erl b/apps/emqx_resource/test/emqx_connector_demo.erl index 692895548..6c03e93cc 100644 --- a/apps/emqx_resource/test/emqx_connector_demo.erl +++ b/apps/emqx_resource/test/emqx_connector_demo.erl @@ -88,6 +88,19 @@ on_query(_InstId, block, #{pid := Pid}) -> on_query(_InstId, resume, #{pid := Pid}) -> Pid ! resume, ok; +on_query(_InstId, {big_payload, Payload}, #{pid := Pid}) -> + ReqRef = make_ref(), + From = {self(), ReqRef}, + Pid ! {From, {big_payload, Payload}}, + receive + {ReqRef, ok} -> + ?tp(connector_demo_big_payload, #{payload => Payload}), + ok; + {ReqRef, incorrect_status} -> + {error, {recoverable_error, incorrect_status}} + after 1000 -> + {error, timeout} + end; on_query(_InstId, {inc_counter, N}, #{pid := Pid}) -> ReqRef = make_ref(), From = {self(), ReqRef}, @@ -216,6 +229,9 @@ counter_loop( {{FromPid, ReqRef}, {inc, _N}} when Status == blocked -> FromPid ! {ReqRef, incorrect_status}, State#{incorrect_status_count := IncorrectCount + 1}; + {{FromPid, ReqRef}, {big_payload, _Payload}} when Status == blocked -> + FromPid ! {ReqRef, incorrect_status}, + State#{incorrect_status_count := IncorrectCount + 1}; {get, ReplyFun} -> apply_reply(ReplyFun, Num), State; diff --git a/apps/emqx_resource/test/emqx_resource_SUITE.erl b/apps/emqx_resource/test/emqx_resource_SUITE.erl index cdec414c9..ea0f4411c 100644 --- a/apps/emqx_resource/test/emqx_resource_SUITE.erl +++ b/apps/emqx_resource/test/emqx_resource_SUITE.erl @@ -1116,6 +1116,88 @@ t_retry_batch(_Config) -> ), ok. +t_delete_and_re_create_with_same_name(_Config) -> + {ok, _} = emqx_resource:create( + ?ID, + ?DEFAULT_RESOURCE_GROUP, + ?TEST_RESOURCE, + #{name => test_resource}, + #{ + query_mode => sync, + batch_size => 1, + worker_pool_size => 2, + queue_seg_bytes => 100, + resume_interval => 1_000 + } + ), + %% pre-condition: we should have just created a new queue + Queuing0 = emqx_resource_metrics:queuing_get(?ID), + ?assertEqual(0, Queuing0), + ?check_trace( + begin + ?assertMatch(ok, emqx_resource:simple_sync_query(?ID, block)), + NumRequests = 10, + {ok, SRef} = snabbkaffe:subscribe( + ?match_event(#{?snk_kind := resource_worker_appended_to_queue}), + %% +1 because the first request will fail, + %% block the resource, and will be + %% re-appended to the queue. + NumRequests + 1, + _Timeout = 5_000 + ), + %% ensure replayq offloads to disk + Payload = binary:copy(<<"a">>, 119), + lists:foreach( + fun(N) -> + {error, _} = + emqx_resource:query( + ?ID, + {big_payload, <<(integer_to_binary(N))/binary, Payload/binary>>} + ) + end, + lists:seq(1, NumRequests) + ), + + {ok, _} = snabbkaffe:receive_events(SRef), + + %% ensure that stuff got enqueued into disk + Queuing1 = emqx_resource_metrics:queuing_get(?ID), + ?assertEqual(NumRequests, Queuing1), + + %% now, we delete the resource + ok = emqx_resource:remove_local(?ID), + ?assertEqual({error, not_found}, emqx_resource_manager:lookup(?ID)), + + %% re-create the resource with the *same name* + {{ok, _}, {ok, _Events}} = + ?wait_async_action( + emqx_resource:create( + ?ID, + ?DEFAULT_RESOURCE_GROUP, + ?TEST_RESOURCE, + #{name => test_resource}, + #{ + query_mode => async, + batch_size => 1, + worker_pool_size => 2, + queue_seg_bytes => 100, + resume_interval => 1_000 + } + ), + #{?snk_kind := resource_worker_enter_blocked}, + 5_000 + ), + + %% it shouldn't have anything enqueued, as it's a fresh resource + Queuing2 = emqx_resource_metrics:queuing_get(?ID), + ?assertEqual(0, Queuing2), + + ok + end, + [] + ), + ok. + %%------------------------------------------------------------------------------ %% Helpers %%------------------------------------------------------------------------------ From ff23d25e8babf22db1716f2062e57a2b4d14d8a4 Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Mon, 9 Jan 2023 15:49:09 -0300 Subject: [PATCH 0226/1802] chore(replayq): update replayq -> 0.3.6 and use `clean_start` for buffer workers So we can truly avoid resuming work after a node restart. --- apps/emqx_resource/src/emqx_resource_worker.erl | 2 +- mix.exs | 2 +- rebar.config | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/emqx_resource/src/emqx_resource_worker.erl b/apps/emqx_resource/src/emqx_resource_worker.erl index 9b264cfcb..8d30ef3f1 100644 --- a/apps/emqx_resource/src/emqx_resource_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_worker.erl @@ -158,7 +158,7 @@ init({Id, Index, Opts}) -> max_total_bytes => TotalBytes, %% we don't want to retain the queue after %% resource restarts. - offload => true, + offload => {true, volatile}, seg_bytes => SegBytes, sizer => fun ?MODULE:estimate_size/1 }, diff --git a/mix.exs b/mix.exs index e124116c8..9f24cc088 100644 --- a/mix.exs +++ b/mix.exs @@ -58,7 +58,7 @@ defmodule EMQXUmbrella.MixProject do {:grpc, github: "emqx/grpc-erl", tag: "0.6.7", override: true}, {:minirest, github: "emqx/minirest", tag: "1.3.7", override: true}, {:ecpool, github: "emqx/ecpool", tag: "0.5.2", override: true}, - {:replayq, github: "emqx/replayq", tag: "0.3.5", override: true}, + {:replayq, github: "emqx/replayq", tag: "0.3.6", override: true}, {:pbkdf2, github: "emqx/erlang-pbkdf2", tag: "2.0.4", override: true}, {:emqtt, github: "emqx/emqtt", tag: "1.7.0-rc.2", override: true}, {:rulesql, github: "emqx/rulesql", tag: "0.1.4"}, diff --git a/rebar.config b/rebar.config index bfdc3e4ee..c2fc678dd 100644 --- a/rebar.config +++ b/rebar.config @@ -60,7 +60,7 @@ , {grpc, {git, "https://github.com/emqx/grpc-erl", {tag, "0.6.7"}}} , {minirest, {git, "https://github.com/emqx/minirest", {tag, "1.3.7"}}} , {ecpool, {git, "https://github.com/emqx/ecpool", {tag, "0.5.2"}}} - , {replayq, {git, "https://github.com/emqx/replayq.git", {tag, "0.3.5"}}} + , {replayq, {git, "https://github.com/emqx/replayq.git", {tag, "0.3.6"}}} , {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}} , {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.7.0-rc.2"}}} , {rulesql, {git, "https://github.com/emqx/rulesql", {tag, "0.1.4"}}} From 32a9e603131d84d93ac041b349ab47cf6d6c3f0b Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Tue, 10 Jan 2023 18:33:52 -0300 Subject: [PATCH 0227/1802] feat(buffer_worker): also use the inflight table for sync requests Related: https://emqx.atlassian.net/browse/EMQX-8692 This should also correctly account for `retried.*` metrics for sync requests. Also fixes cases where race conditions for retrying async requests could potentially lead to inconsistent metrics. Fixes more cases where a stale reference to `replayq` was being held accidentally after a `pop`. --- .../test/emqx_bridge_mqtt_SUITE.erl | 3 +- .../src/emqx_resource_worker.erl | 419 +++++++++++------- .../test/emqx_resource_SUITE.erl | 76 +++- .../test/emqx_ee_bridge_gcp_pubsub_SUITE.erl | 22 +- 4 files changed, 347 insertions(+), 173 deletions(-) diff --git a/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl b/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl index b3c2b8d85..048c039bf 100644 --- a/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl +++ b/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl @@ -920,7 +920,8 @@ t_mqtt_conn_bridge_egress_reconnect(_) -> <<"matched">> := Matched, <<"success">> := 1, <<"failed">> := 0, - <<"queuing">> := 2 + <<"queuing">> := 1, + <<"inflight">> := 1 } when Matched >= 3, maps:get(<<"metrics">>, DecodedMetrics1) ), diff --git a/apps/emqx_resource/src/emqx_resource_worker.erl b/apps/emqx_resource/src/emqx_resource_worker.erl index 8d30ef3f1..4f7344772 100644 --- a/apps/emqx_resource/src/emqx_resource_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_worker.erl @@ -114,10 +114,13 @@ simple_sync_query(Id, Request) -> %% would mess up the metrics anyway. `undefined' is ignored by %% `emqx_resource_metrics:*_shift/3'. Index = undefined, - QueryOpts = #{}, + QueryOpts = #{perform_inflight_capacity_check => false}, emqx_resource_metrics:matched_inc(Id), - Result = call_query(sync, Id, Index, ?QUERY(self(), Request, false), QueryOpts), - _ = handle_query_result(Id, Result, false, false), + Ref = make_message_ref(), + Result = call_query(sync, Id, Index, Ref, ?QUERY(self(), Request, false), QueryOpts), + HasBeenSent = false, + BlockWorker = false, + _ = handle_query_result(Id, Result, HasBeenSent, BlockWorker), Result. -spec simple_async_query(id(), request(), reply_fun()) -> Result :: term(). @@ -129,10 +132,13 @@ simple_async_query(Id, Request, ReplyFun) -> %% would mess up the metrics anyway. `undefined' is ignored by %% `emqx_resource_metrics:*_shift/3'. Index = undefined, - QueryOpts = #{}, + QueryOpts = #{perform_inflight_capacity_check => false}, emqx_resource_metrics:matched_inc(Id), - Result = call_query(async, Id, Index, ?QUERY(ReplyFun, Request, false), QueryOpts), - _ = handle_query_result(Id, Result, false, false), + Ref = make_message_ref(), + Result = call_query(async, Id, Index, Ref, ?QUERY(ReplyFun, Request, false), QueryOpts), + HasBeenSent = false, + BlockWorker = false, + _ = handle_query_result(Id, Result, HasBeenSent, BlockWorker), Result. -spec block(pid()) -> ok. @@ -312,16 +318,27 @@ retry_queue( empty -> {next_state, running, Data0}; {Q1, QAckRef, [?QUERY(_, Request, HasBeenSent) = Query]} -> + Data = Data0#{queue := Q1}, QueryOpts = #{inflight_name => InflightTID}, - Result = call_query(configured, Id, Index, Query, QueryOpts), + Ref = make_message_ref(), + Result = call_query(configured, Id, Index, Ref, Query, QueryOpts), Reply = ?REPLY(undefined, Request, HasBeenSent, Result), case reply_caller(Id, Reply) of true -> - {keep_state, Data0, {state_timeout, ResumeT, resume}}; + %% Still failed, but now it's in the inflight + %% table and marked as sent, except if the result + %% says inflight is full. In this case, we must + %% ensure it's indeed in the inflight table or + %% risk lose it. + ok = replayq:ack(Q1, QAckRef), + is_inflight_full_result(Result) andalso + inflight_append(InflightTID, Ref, Query, Id, Index), + emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), + {keep_state, Data, {state_timeout, ResumeT, resume}}; false -> ok = replayq:ack(Q1, QAckRef), + is_async(Id) orelse inflight_drop(InflightTID, Ref, Id, Index), emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), - Data = Data0#{queue := Q1}, retry_queue(Data) end end; @@ -340,8 +357,10 @@ retry_queue( empty -> {next_state, running, Data0}; {Q1, QAckRef, Batch0} -> + Data = Data0#{queue := Q1}, QueryOpts = #{inflight_name => InflightTID}, - Result = call_query(configured, Id, Index, Batch0, QueryOpts), + Ref = make_message_ref(), + Result = call_query(configured, Id, Index, Ref, Batch0, QueryOpts), %% The caller has been replied with ?RESOURCE_ERROR(blocked, _) before saving into the queue, %% we now change the 'from' field to 'undefined' so it will not reply the caller again. Batch = [ @@ -351,12 +370,21 @@ retry_queue( case batch_reply_caller(Id, Result, Batch) of true -> ?tp(resource_worker_retry_queue_batch_failed, #{batch => Batch}), - {keep_state, Data0, {state_timeout, ResumeT, resume}}; + %% Still failed, but now it's in the inflight + %% table and marked as sent, except if the result + %% says inflight is full. In this case, we must + %% ensure it's indeed in the inflight table or + %% risk lose it. + ok = replayq:ack(Q1, QAckRef), + is_inflight_full_result(Result) andalso + inflight_append(InflightTID, Ref, Batch, Id, Index), + emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), + {keep_state, Data, {state_timeout, ResumeT, resume}}; false -> ?tp(resource_worker_retry_queue_batch_succeeded, #{batch => Batch}), ok = replayq:ack(Q1, QAckRef), + is_async(Id) orelse inflight_drop(InflightTID, Ref, Id, Index), emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), - Data = Data0#{queue := Q1}, retry_queue(Data) end end. @@ -371,15 +399,25 @@ retry_inflight_sync( QueryOpts = #{}, %% if we are retrying an inflight query, it has been sent HasBeenSent = true, - Result = call_query(sync, Id, Index, QueryOrBatch, QueryOpts), + Result = call_query(sync, Id, Index, Ref, QueryOrBatch, QueryOpts), BlockWorker = false, - case handle_query_result(Id, Result, HasBeenSent, BlockWorker) of + case handle_query_result_pure(Id, Result, HasBeenSent, BlockWorker) of %% Send failed because resource is down - true -> + {true, PostFn} -> + PostFn(), + ?tp(resource_worker_retry_inflight_failed, #{query_or_batch => QueryOrBatch}), {keep_state, Data0, {state_timeout, ResumeT, resume}}; %% Send ok or failed but the resource is working - false -> - inflight_drop(InflightTID, Ref, Id, Index), + {false, PostFn} -> + IsDropped = inflight_drop(InflightTID, Ref, Id, Index), + %% we need to defer bumping the counters after + %% `inflight_drop' to avoid the race condition when an + %% inflight request might get completed concurrently with + %% the retry, bumping them twice. Since both inflight + %% requests (repeated and original) have the safe `Ref', + %% we bump the counter when removing it from the table. + IsDropped andalso PostFn(), + ?tp(resource_worker_retry_inflight_succeeded, #{query_or_batch => QueryOrBatch}), do_resume(Data0) end. @@ -441,10 +479,12 @@ flush(Data0) -> %% `nack' a `pop'. %% Maybe we could re-open the queue? Data1 = Data0#{queue := Q1}, + Ref = make_message_ref(), do_flush(Data1, #{ new_queue => Q1, is_batch => IsBatch, batch => Batch, + ref => Ref, ack_ref => QAckRef }) end. @@ -455,7 +495,16 @@ flush(Data0) -> ack_ref := replayq:ack_ref() }) -> gen_statem:event_handler_result(state(), data()). -do_flush(Data0, #{is_batch := false, batch := Batch, ack_ref := QAckRef, new_queue := Q1}) -> +do_flush( + Data0, + #{ + is_batch := false, + batch := Batch, + ref := Ref, + ack_ref := QAckRef, + new_queue := Q1 + } +) -> #{ id := Id, index := Index, @@ -464,37 +513,26 @@ do_flush(Data0, #{is_batch := false, batch := Batch, ack_ref := QAckRef, new_que %% unwrap when not batching (i.e., batch size == 1) [?QUERY(From, CoreReq, HasBeenSent) = Request] = Batch, QueryOpts = #{inflight_name => InflightTID}, - Result = call_query(configured, Id, Index, Request, QueryOpts), - IsAsync = is_async(Id), + Result = call_query(configured, Id, Index, Ref, Request, QueryOpts), Data1 = cancel_flush_timer(Data0), Reply = ?REPLY(From, CoreReq, HasBeenSent, Result), - case {reply_caller(Id, Reply), IsAsync} of - %% failed and is not async; keep the request in the queue to - %% be retried - {true, false} -> - %% Note: currently, we cannot safely pop an item from - %% `replayq', keep the old reference to the queue and - %% later try to append new items to the old ref: by - %% popping an item, we may cause the side effect of - %% closing an open segment and opening a new one, and the - %% later `append' with the old file descriptor will fail - %% with `einval' because it has been closed. So we are - %% forced to re-append the item, changing the order of - %% requests... - ok = replayq:ack(Q1, QAckRef), - SentBatch = mark_as_sent(Batch), - Q2 = append_queue(Id, Index, Q1, SentBatch), - Data2 = Data1#{queue := Q2}, - {next_state, blocked, Data2}; - %% failed and is async; remove the request from the queue, as - %% it is already in inflight table - {true, true} -> + case reply_caller(Id, Reply) of + %% Failed; remove the request from the queue, as we cannot pop + %% from it again. But we must ensure it's in the inflight + %% table, even if it's full, so we don't lose the request. + %% And only in that case. + true -> ok = replayq:ack(Q1, QAckRef), + ShouldPreserveInInflight = + is_inflight_full_result(Result) orelse + is_not_connected_result(Result), + ShouldPreserveInInflight andalso inflight_append(InflightTID, Ref, Request, Id, Index), emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), {next_state, blocked, Data1}; - %% success; just ack - {false, _} -> + %% Success; just ack. + false -> ok = replayq:ack(Q1, QAckRef), + is_async(Id) orelse inflight_drop(InflightTID, Ref, Id, Index), emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), case replayq:count(Q1) > 0 of true -> @@ -503,7 +541,13 @@ do_flush(Data0, #{is_batch := false, batch := Batch, ack_ref := QAckRef, new_que {keep_state, Data1} end end; -do_flush(Data0, #{is_batch := true, batch := Batch, ack_ref := QAckRef, new_queue := Q1}) -> +do_flush(Data0, #{ + is_batch := true, + batch := Batch, + ref := Ref, + ack_ref := QAckRef, + new_queue := Q1 +}) -> #{ id := Id, index := Index, @@ -511,36 +555,25 @@ do_flush(Data0, #{is_batch := true, batch := Batch, ack_ref := QAckRef, new_queu inflight_tid := InflightTID } = Data0, QueryOpts = #{inflight_name => InflightTID}, - Result = call_query(configured, Id, Index, Batch, QueryOpts), - IsAsync = is_async(Id), + Result = call_query(configured, Id, Index, Ref, Batch, QueryOpts), Data1 = cancel_flush_timer(Data0), - case {batch_reply_caller(Id, Result, Batch), IsAsync} of - %% failed and is not async; keep the request in the queue to - %% be retried - {true, false} -> - %% Note: currently, we cannot safely pop an item from - %% `replayq', keep the old reference to the queue and - %% later try to append new items to the old ref: by - %% popping an item, we may cause the side effect of - %% closing an open segment and opening a new one, and the - %% later `append' with the old file descriptor will fail - %% with `einval' because it has been closed. So we are - %% forced to re-append the item, changing the order of - %% requests... - ok = replayq:ack(Q1, QAckRef), - SentBatch = mark_as_sent(Batch), - Q2 = append_queue(Id, Index, Q1, SentBatch), - Data2 = Data1#{queue := Q2}, - {next_state, blocked, Data2}; - %% failed and is async; remove the request from the queue, as - %% it is already in inflight table - {true, true} -> + case batch_reply_caller(Id, Result, Batch) of + %% Failed; remove the request from the queue, as we cannot pop + %% from it again. But we must ensure it's in the inflight + %% table, even if it's full, so we don't lose the request. + %% And only in that case. + true -> ok = replayq:ack(Q1, QAckRef), + ShouldPreserveInInflight = + is_inflight_full_result(Result) orelse + is_not_connected_result(Result), + ShouldPreserveInInflight andalso inflight_append(InflightTID, Ref, Batch, Id, Index), emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), {next_state, blocked, Data1}; - %% success; just ack - {false, _} -> + %% Success; just ack. + false -> ok = replayq:ack(Q1, QAckRef), + is_async(Id) orelse inflight_drop(InflightTID, Ref, Id, Index), emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), CurrentCount = replayq:count(Q1), case {CurrentCount > 0, CurrentCount >= BatchSize} of @@ -555,23 +588,34 @@ do_flush(Data0, #{is_batch := true, batch := Batch, ack_ref := QAckRef, new_queu end. batch_reply_caller(Id, BatchResult, Batch) -> + {ShouldBlock, PostFns} = batch_reply_caller_defer_metrics(Id, BatchResult, Batch), + lists:foreach(fun(F) -> F() end, PostFns), + ShouldBlock. + +batch_reply_caller_defer_metrics(Id, BatchResult, Batch) -> lists:foldl( - fun(Reply, BlockWorker) -> - reply_caller(Id, Reply, BlockWorker) + fun(Reply, {BlockWorker, PostFns}) -> + {ShouldBlock, PostFn} = reply_caller_defer_metrics(Id, Reply, BlockWorker), + {ShouldBlock, [PostFn | PostFns]} end, - false, + {false, []}, %% the `Mod:on_batch_query/3` returns a single result for a batch, %% so we need to expand ?EXPAND(BatchResult, Batch) ). reply_caller(Id, Reply) -> - BlockWorker = false, - reply_caller(Id, Reply, BlockWorker). + {ShouldBlock, PostFn} = reply_caller_defer_metrics(Id, Reply), + PostFn(), + ShouldBlock. -reply_caller(Id, ?REPLY(undefined, _, HasBeenSent, Result), BlockWorker) -> - handle_query_result(Id, Result, HasBeenSent, BlockWorker); -reply_caller(Id, ?REPLY({ReplyFun, Args}, _, HasBeenSent, Result), BlockWorker) when +reply_caller_defer_metrics(Id, Reply) -> + BlockWorker = false, + reply_caller_defer_metrics(Id, Reply, BlockWorker). + +reply_caller_defer_metrics(Id, ?REPLY(undefined, _, HasBeenSent, Result), BlockWorker) -> + handle_query_result_pure(Id, Result, HasBeenSent, BlockWorker); +reply_caller_defer_metrics(Id, ?REPLY({ReplyFun, Args}, _, HasBeenSent, Result), BlockWorker) when is_function(ReplyFun) -> _ = @@ -579,55 +623,96 @@ reply_caller(Id, ?REPLY({ReplyFun, Args}, _, HasBeenSent, Result), BlockWorker) {async_return, _} -> no_reply_for_now; _ -> apply(ReplyFun, Args ++ [Result]) end, - handle_query_result(Id, Result, HasBeenSent, BlockWorker); -reply_caller(Id, ?REPLY(From, _, HasBeenSent, Result), BlockWorker) -> + handle_query_result_pure(Id, Result, HasBeenSent, BlockWorker); +reply_caller_defer_metrics(Id, ?REPLY(From, _, HasBeenSent, Result), BlockWorker) -> gen_statem:reply(From, Result), - handle_query_result(Id, Result, HasBeenSent, BlockWorker). + handle_query_result_pure(Id, Result, HasBeenSent, BlockWorker). -handle_query_result(Id, ?RESOURCE_ERROR_M(exception, Msg), HasBeenSent, BlockWorker) -> - ?SLOG(error, #{msg => resource_exception, info => Msg}), - inc_sent_failed(Id, HasBeenSent), - BlockWorker; -handle_query_result(_Id, ?RESOURCE_ERROR_M(NotWorking, _), _HasBeenSent, _) when +handle_query_result(Id, Result, HasBeenSent, BlockWorker) -> + {ShouldBlock, PostFn} = handle_query_result_pure(Id, Result, HasBeenSent, BlockWorker), + PostFn(), + ShouldBlock. + +handle_query_result_pure(Id, ?RESOURCE_ERROR_M(exception, Msg), HasBeenSent, BlockWorker) -> + PostFn = fun() -> + ?SLOG(error, #{msg => resource_exception, info => Msg}), + inc_sent_failed(Id, HasBeenSent), + ok + end, + {BlockWorker, PostFn}; +handle_query_result_pure(_Id, ?RESOURCE_ERROR_M(NotWorking, _), _HasBeenSent, _) when NotWorking == not_connected; NotWorking == blocked -> - true; -handle_query_result(Id, ?RESOURCE_ERROR_M(not_found, Msg), _HasBeenSent, BlockWorker) -> - ?SLOG(error, #{id => Id, msg => resource_not_found, info => Msg}), - emqx_resource_metrics:dropped_resource_not_found_inc(Id), - BlockWorker; -handle_query_result(Id, ?RESOURCE_ERROR_M(stopped, Msg), _HasBeenSent, BlockWorker) -> - ?SLOG(error, #{id => Id, msg => resource_stopped, info => Msg}), - emqx_resource_metrics:dropped_resource_stopped_inc(Id), - BlockWorker; -handle_query_result(Id, ?RESOURCE_ERROR_M(Reason, _), _HasBeenSent, BlockWorker) -> - ?SLOG(error, #{id => Id, msg => other_resource_error, reason => Reason}), - emqx_resource_metrics:dropped_other_inc(Id), - BlockWorker; -handle_query_result(Id, {error, {recoverable_error, Reason}}, _HasBeenSent, _BlockWorker) -> + {true, fun() -> ok end}; +handle_query_result_pure(Id, ?RESOURCE_ERROR_M(not_found, Msg), _HasBeenSent, BlockWorker) -> + PostFn = fun() -> + ?SLOG(error, #{id => Id, msg => resource_not_found, info => Msg}), + emqx_resource_metrics:dropped_resource_not_found_inc(Id), + ok + end, + {BlockWorker, PostFn}; +handle_query_result_pure(Id, ?RESOURCE_ERROR_M(stopped, Msg), _HasBeenSent, BlockWorker) -> + PostFn = fun() -> + ?SLOG(error, #{id => Id, msg => resource_stopped, info => Msg}), + emqx_resource_metrics:dropped_resource_stopped_inc(Id), + ok + end, + {BlockWorker, PostFn}; +handle_query_result_pure(Id, ?RESOURCE_ERROR_M(Reason, _), _HasBeenSent, BlockWorker) -> + PostFn = fun() -> + ?SLOG(error, #{id => Id, msg => other_resource_error, reason => Reason}), + emqx_resource_metrics:dropped_other_inc(Id), + ok + end, + {BlockWorker, PostFn}; +handle_query_result_pure(Id, {error, {recoverable_error, Reason}}, _HasBeenSent, _BlockWorker) -> %% the message will be queued in replayq or inflight window, %% i.e. the counter 'queuing' or 'dropped' will increase, so we pretend that we have not %% sent this message. - ?SLOG(warning, #{id => Id, msg => recoverable_error, reason => Reason}), - true; -handle_query_result(Id, {error, Reason}, HasBeenSent, BlockWorker) -> - ?SLOG(error, #{id => Id, msg => send_error, reason => Reason}), - inc_sent_failed(Id, HasBeenSent), - BlockWorker; -handle_query_result(_Id, {async_return, inflight_full}, _HasBeenSent, _BlockWorker) -> - true; -handle_query_result(Id, {async_return, {error, Msg}}, HasBeenSent, BlockWorker) -> - ?SLOG(error, #{id => Id, msg => async_send_error, info => Msg}), - inc_sent_failed(Id, HasBeenSent), - BlockWorker; -handle_query_result(_Id, {async_return, ok}, _HasBeenSent, BlockWorker) -> - BlockWorker; -handle_query_result(Id, Result, HasBeenSent, BlockWorker) -> - assert_ok_result(Result), - inc_sent_success(Id, HasBeenSent), - BlockWorker. + PostFn = fun() -> + ?SLOG(warning, #{id => Id, msg => recoverable_error, reason => Reason}), + ok + end, + {true, PostFn}; +handle_query_result_pure(Id, {error, Reason}, HasBeenSent, BlockWorker) -> + PostFn = fun() -> + ?SLOG(error, #{id => Id, msg => send_error, reason => Reason}), + inc_sent_failed(Id, HasBeenSent), + ok + end, + {BlockWorker, PostFn}; +handle_query_result_pure(_Id, {async_return, inflight_full}, _HasBeenSent, _BlockWorker) -> + {true, fun() -> ok end}; +handle_query_result_pure(Id, {async_return, {error, Msg}}, HasBeenSent, BlockWorker) -> + PostFn = fun() -> + ?SLOG(error, #{id => Id, msg => async_send_error, info => Msg}), + inc_sent_failed(Id, HasBeenSent), + ok + end, + {BlockWorker, PostFn}; +handle_query_result_pure(_Id, {async_return, ok}, _HasBeenSent, BlockWorker) -> + {BlockWorker, fun() -> ok end}; +handle_query_result_pure(Id, Result, HasBeenSent, BlockWorker) -> + PostFn = fun() -> + assert_ok_result(Result), + inc_sent_success(Id, HasBeenSent), + ok + end, + {BlockWorker, PostFn}. -call_query(QM0, Id, Index, Query, QueryOpts) -> +is_inflight_full_result({async_return, inflight_full}) -> + true; +is_inflight_full_result(_) -> + false. + +is_not_connected_result(?RESOURCE_ERROR_M(Error, _)) when + Error =:= not_connected; Error =:= blocked +-> + true; +is_not_connected_result(_) -> + false. + +call_query(QM0, Id, Index, Ref, Query, QueryOpts) -> ?tp(call_query_enter, #{id => Id, query => Query}), case emqx_resource_manager:ets_lookup(Id) of {ok, _Group, #{mod := Mod, state := ResSt, status := connected} = Data} -> @@ -637,7 +722,7 @@ call_query(QM0, Id, Index, Query, QueryOpts) -> _ -> QM0 end, CM = maps:get(callback_mode, Data), - apply_query_fun(call_mode(QM, CM), Mod, Id, Index, Query, ResSt, QueryOpts); + apply_query_fun(call_mode(QM, CM), Mod, Id, Index, Ref, Query, ResSt, QueryOpts); {ok, _Group, #{status := stopped}} -> ?RESOURCE_ERROR(stopped, "resource stopped or disabled"); {ok, _Group, #{status := S}} when S == connecting; S == disconnected -> @@ -664,20 +749,34 @@ call_query(QM0, Id, Index, Query, QueryOpts) -> end ). -apply_query_fun(sync, Mod, Id, _Index, ?QUERY(_, Request, _) = _Query, ResSt, _QueryOpts) -> - ?tp(call_query, #{id => Id, mod => Mod, query => _Query, res_st => ResSt}), - ?APPLY_RESOURCE(call_query, Mod:on_query(Id, Request, ResSt), Request); -apply_query_fun(async, Mod, Id, Index, ?QUERY(_, Request, _) = Query, ResSt, QueryOpts) -> +apply_query_fun(sync, Mod, Id, Index, Ref, ?QUERY(_, Request, _) = Query, ResSt, QueryOpts) -> + ?tp(call_query, #{id => Id, mod => Mod, query => Query, res_st => ResSt}), + InflightTID = maps:get(inflight_name, QueryOpts, undefined), + PerformInflightCapacityCheck = maps:get(perform_inflight_capacity_check, QueryOpts, true), + ?APPLY_RESOURCE( + call_query, + case PerformInflightCapacityCheck andalso is_inflight_full(InflightTID) of + true -> + %% should be kept in the inflight table and retried + %% when resuming. + {async_return, inflight_full}; + false -> + ok = inflight_append(InflightTID, Ref, Query, Id, Index), + Mod:on_query(Id, Request, ResSt) + end, + Request + ); +apply_query_fun(async, Mod, Id, Index, Ref, ?QUERY(_, Request, _) = Query, ResSt, QueryOpts) -> ?tp(call_query_async, #{id => Id, mod => Mod, query => Query, res_st => ResSt}), InflightTID = maps:get(inflight_name, QueryOpts, undefined), + PerformInflightCapacityCheck = maps:get(perform_inflight_capacity_check, QueryOpts, true), ?APPLY_RESOURCE( call_query_async, - case is_inflight_full(InflightTID) of + case PerformInflightCapacityCheck andalso is_inflight_full(InflightTID) of true -> {async_return, inflight_full}; false -> ReplyFun = fun ?MODULE:reply_after_query/7, - Ref = make_message_ref(), Args = [self(), Id, Index, InflightTID, Ref, Query], ok = inflight_append(InflightTID, Ref, Query, Id, Index), Result = Mod:on_query_async(Id, Request, {ReplyFun, Args}, ResSt), @@ -685,21 +784,35 @@ apply_query_fun(async, Mod, Id, Index, ?QUERY(_, Request, _) = Query, ResSt, Que end, Request ); -apply_query_fun(sync, Mod, Id, _Index, [?QUERY(_, _, _) | _] = Batch, ResSt, _QueryOpts) -> +apply_query_fun(sync, Mod, Id, Index, Ref, [?QUERY(_, _, _) | _] = Batch, ResSt, QueryOpts) -> ?tp(call_batch_query, #{id => Id, mod => Mod, batch => Batch, res_st => ResSt}), + InflightTID = maps:get(inflight_name, QueryOpts, undefined), + PerformInflightCapacityCheck = maps:get(perform_inflight_capacity_check, QueryOpts, true), Requests = [Request || ?QUERY(_From, Request, _) <- Batch], - ?APPLY_RESOURCE(call_batch_query, Mod:on_batch_query(Id, Requests, ResSt), Batch); -apply_query_fun(async, Mod, Id, Index, [?QUERY(_, _, _) | _] = Batch, ResSt, QueryOpts) -> + ?APPLY_RESOURCE( + call_batch_query, + case PerformInflightCapacityCheck andalso is_inflight_full(InflightTID) of + true -> + %% should be kept in the inflight table and retried + %% when resuming. + {async_return, inflight_full}; + false -> + ok = inflight_append(InflightTID, Ref, Batch, Id, Index), + Mod:on_batch_query(Id, Requests, ResSt) + end, + Batch + ); +apply_query_fun(async, Mod, Id, Index, Ref, [?QUERY(_, _, _) | _] = Batch, ResSt, QueryOpts) -> ?tp(call_batch_query_async, #{id => Id, mod => Mod, batch => Batch, res_st => ResSt}), InflightTID = maps:get(inflight_name, QueryOpts, undefined), + PerformInflightCapacityCheck = maps:get(perform_inflight_capacity_check, QueryOpts, true), ?APPLY_RESOURCE( call_batch_query_async, - case is_inflight_full(InflightTID) of + case PerformInflightCapacityCheck andalso is_inflight_full(InflightTID) of true -> {async_return, inflight_full}; false -> ReplyFun = fun ?MODULE:batch_reply_after_query/7, - Ref = make_message_ref(), ReplyFunAndArgs = {ReplyFun, [self(), Id, Index, InflightTID, Ref, Batch]}, Requests = [Request || ?QUERY(_From, Request, _) <- Batch], ok = inflight_append(InflightTID, Ref, Batch, Id, Index), @@ -713,29 +826,36 @@ reply_after_query(Pid, Id, Index, InflightTID, Ref, ?QUERY(From, Request, HasBee %% NOTE: 'inflight' is the count of messages that were sent async %% but received no ACK, NOT the number of messages queued in the %% inflight window. - case reply_caller(Id, ?REPLY(From, Request, HasBeenSent, Result)) of - true -> + case reply_caller_defer_metrics(Id, ?REPLY(From, Request, HasBeenSent, Result)) of + {true, PostFn} -> + PostFn(), ?MODULE:block(Pid); - false -> - drop_inflight_and_resume(Pid, InflightTID, Ref, Id, Index) + {false, PostFn} -> + IsDropped = drop_inflight_and_resume(Pid, InflightTID, Ref, Id, Index), + IsDropped andalso PostFn(), + ok end. batch_reply_after_query(Pid, Id, Index, InflightTID, Ref, Batch, Result) -> %% NOTE: 'inflight' is the count of messages that were sent async %% but received no ACK, NOT the number of messages queued in the %% inflight window. - case batch_reply_caller(Id, Result, Batch) of - true -> + case batch_reply_caller_defer_metrics(Id, Result, Batch) of + {true, PostFns} -> + lists:foreach(fun(F) -> F() end, PostFns), ?MODULE:block(Pid); - false -> - drop_inflight_and_resume(Pid, InflightTID, Ref, Id, Index) + {false, PostFns} -> + IsDropped = drop_inflight_and_resume(Pid, InflightTID, Ref, Id, Index), + IsDropped andalso lists:foreach(fun(F) -> F() end, PostFns), + ok end. drop_inflight_and_resume(Pid, InflightTID, Ref, Id, Index) -> case is_inflight_full(InflightTID) of true -> - inflight_drop(InflightTID, Ref, Id, Index), - ?MODULE:resume(Pid); + IsDropped = inflight_drop(InflightTID, Ref, Id, Index), + ?MODULE:resume(Pid), + IsDropped; false -> inflight_drop(InflightTID, Ref, Id, Index) end. @@ -838,16 +958,18 @@ inflight_append(undefined, _Ref, _Query, _Id, _Index) -> ok; inflight_append(InflightTID, Ref, [?QUERY(_, _, _) | _] = Batch0, Id, Index) -> Batch = mark_as_sent(Batch0), - ets:insert(InflightTID, {Ref, Batch}), + IsNew = ets:insert_new(InflightTID, {Ref, Batch}), BatchSize = length(Batch), - ets:update_counter(InflightTID, ?SIZE_REF, {2, BatchSize}), + IsNew andalso ets:update_counter(InflightTID, ?SIZE_REF, {2, BatchSize}), emqx_resource_metrics:inflight_set(Id, Index, inflight_num_msgs(InflightTID)), + ?tp(resource_worker_appended_to_inflight, #{batch => Batch, is_new => IsNew}), ok; inflight_append(InflightTID, Ref, ?QUERY(_From, _Req, _HasBeenSent) = Query0, Id, Index) -> Query = mark_as_sent(Query0), - ets:insert(InflightTID, {Ref, Query}), - ets:update_counter(InflightTID, ?SIZE_REF, {2, 1}), + IsNew = ets:insert_new(InflightTID, {Ref, Query}), + IsNew andalso ets:update_counter(InflightTID, ?SIZE_REF, {2, 1}), emqx_resource_metrics:inflight_set(Id, Index, inflight_num_msgs(InflightTID)), + ?tp(resource_worker_appended_to_inflight, #{query => Query, is_new => IsNew}), ok; inflight_append(InflightTID, Ref, Data, _Id, _Index) -> ets:insert(InflightTID, {Ref, Data}), @@ -855,8 +977,8 @@ inflight_append(InflightTID, Ref, Data, _Id, _Index) -> %% the inflight metric. ok. -inflight_drop(undefined, _, _Id, _Index) -> - ok; +inflight_drop(undefined, _Ref, _Id, _Index) -> + false; inflight_drop(InflightTID, Ref, Id, Index) -> Count = case ets:take(InflightTID, Ref) of @@ -864,9 +986,10 @@ inflight_drop(InflightTID, Ref, Id, Index) -> [{Ref, [?QUERY(_, _, _) | _] = Batch}] -> length(Batch); _ -> 0 end, - Count > 0 andalso ets:update_counter(InflightTID, ?SIZE_REF, {2, -Count, 0, 0}), + IsDropped = Count > 0, + IsDropped andalso ets:update_counter(InflightTID, ?SIZE_REF, {2, -Count, 0, 0}), emqx_resource_metrics:inflight_set(Id, Index, inflight_num_msgs(InflightTID)), - ok. + IsDropped. %%============================================================================== diff --git a/apps/emqx_resource/test/emqx_resource_SUITE.erl b/apps/emqx_resource/test/emqx_resource_SUITE.erl index ea0f4411c..a2767ee6c 100644 --- a/apps/emqx_resource/test/emqx_resource_SUITE.erl +++ b/apps/emqx_resource/test/emqx_resource_SUITE.erl @@ -411,8 +411,21 @@ t_query_counter_async_inflight(_) -> %% send async query to make the inflight window full ?check_trace( - ?TRACE_OPTS, - inc_counter_in_parallel(WindowSize, ReqOpts), + begin + {ok, SRef} = snabbkaffe:subscribe( + ?match_event( + #{ + ?snk_kind := resource_worker_appended_to_inflight, + is_new := true + } + ), + WindowSize, + _Timeout = 5_000 + ), + inc_counter_in_parallel(WindowSize, ReqOpts), + {ok, _} = snabbkaffe:receive_events(SRef), + ok + end, fun(Trace) -> QueryTrace = ?of_kind(call_query_async, Trace), ?assertMatch([#{query := {query, _, {inc_counter, 1}, _}} | _], QueryTrace) @@ -455,29 +468,29 @@ t_query_counter_async_inflight(_) -> %% +1 because the tmp_query above will be retried and succeed %% this time. WindowSize + 1, - _Timeout = 60_000 + _Timeout0 = 10_000 ), ?assertMatch(ok, emqx_resource:simple_sync_query(?ID, resume)), tap_metrics(?LINE), {ok, _} = snabbkaffe:receive_events(SRef0), + tap_metrics(?LINE), %% since the previous tmp_query was enqueued to be retried, we %% take it again from the table; this time, it should have %% succeeded. ?assertMatch([{tmp_query, ok}], ets:take(Tab0, tmp_query)), - ?assertEqual(WindowSize, ets:info(Tab0, size)), + ?assertEqual(WindowSize, ets:info(Tab0, size), #{tab => ets:tab2list(Tab0)}), tap_metrics(?LINE), %% send async query, this time everything should be ok. Num = 10, ?check_trace( - ?TRACE_OPTS, begin {ok, SRef} = snabbkaffe:subscribe( ?match_event(#{?snk_kind := connector_demo_inc_counter_async}), Num, - _Timeout = 60_000 + _Timeout0 = 10_000 ), - inc_counter_in_parallel(Num, ReqOpts), + inc_counter_in_parallel_increasing(Num, 1, ReqOpts), {ok, _} = snabbkaffe:receive_events(SRef), ok end, @@ -502,17 +515,18 @@ t_query_counter_async_inflight(_) -> ), %% this will block the resource_worker - ok = emqx_resource:query(?ID, {inc_counter, 1}), + ok = emqx_resource:query(?ID, {inc_counter, 4}), Sent = WindowSize + Num + WindowSize, {ok, SRef1} = snabbkaffe:subscribe( ?match_event(#{?snk_kind := connector_demo_inc_counter_async}), WindowSize, - _Timeout = 60_000 + _Timeout0 = 10_000 ), ?assertMatch(ok, emqx_resource:simple_sync_query(?ID, resume)), {ok, _} = snabbkaffe:receive_events(SRef1), ?assertEqual(Sent, ets:info(Tab0, size)), + tap_metrics(?LINE), {ok, Counter} = emqx_resource:simple_sync_query(?ID, get_counter), ct:pal("get_counter: ~p, sent: ~p", [Counter, Sent]), @@ -842,6 +856,8 @@ t_stop_start(_) -> ?assert(is_process_alive(Pid0)), %% metrics are reset when recreating + %% depending on timing, might show the request we just did. + ct:sleep(500), ?assertEqual(0, emqx_resource_metrics:inflight_get(?ID)), ok = emqx_resource:stop(?ID), @@ -861,6 +877,7 @@ t_stop_start(_) -> ?assert(is_process_alive(Pid1)), %% now stop while resetting the metrics + ct:sleep(500), emqx_resource_metrics:inflight_set(?ID, WorkerID0, 1), emqx_resource_metrics:inflight_set(?ID, WorkerID1, 4), ?assertEqual(5, emqx_resource_metrics:inflight_get(?ID)), @@ -1067,7 +1084,7 @@ t_retry_batch(_Config) -> %% batch shall remain enqueued. {ok, _} = snabbkaffe:block_until( - ?match_n_events(2, #{?snk_kind := resource_worker_retry_queue_batch_failed}), + ?match_n_events(2, #{?snk_kind := resource_worker_retry_inflight_failed}), 5_000 ), %% should not have increased the matched count with the retries @@ -1079,7 +1096,7 @@ t_retry_batch(_Config) -> {ok, {ok, _}} = ?wait_async_action( ok = emqx_resource:simple_sync_query(?ID, resume), - #{?snk_kind := resource_worker_retry_queue_batch_succeeded}, + #{?snk_kind := resource_worker_retry_inflight_succeeded}, 5_000 ), %% 1 more because of the `resume' call @@ -1132,17 +1149,16 @@ t_delete_and_re_create_with_same_name(_Config) -> ), %% pre-condition: we should have just created a new queue Queuing0 = emqx_resource_metrics:queuing_get(?ID), + Inflight0 = emqx_resource_metrics:inflight_get(?ID), ?assertEqual(0, Queuing0), + ?assertEqual(0, Inflight0), ?check_trace( begin ?assertMatch(ok, emqx_resource:simple_sync_query(?ID, block)), NumRequests = 10, {ok, SRef} = snabbkaffe:subscribe( ?match_event(#{?snk_kind := resource_worker_appended_to_queue}), - %% +1 because the first request will fail, - %% block the resource, and will be - %% re-appended to the queue. - NumRequests + 1, + NumRequests, _Timeout = 5_000 ), %% ensure replayq offloads to disk @@ -1161,8 +1177,11 @@ t_delete_and_re_create_with_same_name(_Config) -> {ok, _} = snabbkaffe:receive_events(SRef), %% ensure that stuff got enqueued into disk + tap_metrics(?LINE), Queuing1 = emqx_resource_metrics:queuing_get(?ID), - ?assertEqual(NumRequests, Queuing1), + Inflight1 = emqx_resource_metrics:inflight_get(?ID), + ?assertEqual(NumRequests - 1, Queuing1), + ?assertEqual(1, Inflight1), %% now, we delete the resource ok = emqx_resource:remove_local(?ID), @@ -1190,7 +1209,9 @@ t_delete_and_re_create_with_same_name(_Config) -> %% it shouldn't have anything enqueued, as it's a fresh resource Queuing2 = emqx_resource_metrics:queuing_get(?ID), + Inflight2 = emqx_resource_metrics:queuing_get(?ID), ?assertEqual(0, Queuing2), + ?assertEqual(0, Inflight2), ok end, @@ -1227,6 +1248,29 @@ inc_counter_in_parallel(N, Opts0) -> || Pid <- Pids ]. +inc_counter_in_parallel_increasing(N, StartN, Opts0) -> + Parent = self(), + Pids = [ + erlang:spawn(fun() -> + Opts = + case is_function(Opts0) of + true -> Opts0(); + false -> Opts0 + end, + emqx_resource:query(?ID, {inc_counter, M}, Opts), + Parent ! {complete, self()} + end) + || M <- lists:seq(StartN, StartN + N - 1) + ], + [ + receive + {complete, Pid} -> ok + after 1000 -> + ct:fail({wait_for_query_timeout, Pid}) + end + || Pid <- Pids + ]. + bin_config() -> <<"\"name\": \"test_resource\"">>. diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_gcp_pubsub_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_gcp_pubsub_SUITE.erl index 0eadf46ad..4c20705b6 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_gcp_pubsub_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_gcp_pubsub_SUITE.erl @@ -508,14 +508,16 @@ install_telemetry_handler(TestCase) -> wait_until_gauge_is(GaugeName, ExpectedValue, Timeout) -> Events = receive_all_events(GaugeName, Timeout), - case lists:last(Events) of + case length(Events) > 0 andalso lists:last(Events) of #{measurements := #{gauge_set := ExpectedValue}} -> ok; #{measurements := #{gauge_set := Value}} -> ct:fail( "gauge ~p didn't reach expected value ~p; last value: ~p", [GaugeName, ExpectedValue, Value] - ) + ); + false -> + ct:pal("no ~p gauge events received!", [GaugeName]) end. receive_all_events(EventName, Timeout) -> @@ -605,6 +607,8 @@ t_publish_success(Config) -> ResourceId, #{n_events => ExpectedInflightEvents, timeout => 5_000} ), + wait_until_gauge_is(queuing, 0, 500), + wait_until_gauge_is(inflight, 0, 500), assert_metrics( #{ dropped => 0, @@ -653,6 +657,8 @@ t_publish_success_local_topic(Config) -> ResourceId, #{n_events => ExpectedInflightEvents, timeout => 5_000} ), + wait_until_gauge_is(queuing, 0, 500), + wait_until_gauge_is(inflight, 0, 500), assert_metrics( #{ dropped => 0, @@ -739,6 +745,8 @@ t_publish_templated(Config) -> ResourceId, #{n_events => ExpectedInflightEvents, timeout => 5_000} ), + wait_until_gauge_is(queuing, 0, 500), + wait_until_gauge_is(inflight, 0, 500), assert_metrics( #{ dropped => 0, @@ -1120,19 +1128,17 @@ do_econnrefused_or_timeout_test(Config, Error) -> ResourceId ); {_, sync} -> - wait_telemetry_event(TelemetryTable, queuing, ResourceId, #{ - timeout => 10_000, n_events => 2 - }), %% even waiting, hard to avoid flakiness... simpler to just sleep %% a bit until stabilization. - ct:sleep(200), + wait_until_gauge_is(queuing, 0, 500), + wait_until_gauge_is(inflight, 1, 500), assert_metrics( #{ dropped => 0, failed => 0, - inflight => 0, + inflight => 1, matched => 1, - queuing => 1, + queuing => 0, retried => 0, success => 0 }, From 4c04a0137079a475a198c141e1c05bd81d6a4c0a Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Wed, 11 Jan 2023 16:19:32 -0300 Subject: [PATCH 0228/1802] refactor(buffer_worker): remove `?Q_ITEM` wrapping and use lightweight size estimate --- .../src/emqx_resource_worker.erl | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/apps/emqx_resource/src/emqx_resource_worker.erl b/apps/emqx_resource/src/emqx_resource_worker.erl index 4f7344772..f07c678b3 100644 --- a/apps/emqx_resource/src/emqx_resource_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_worker.erl @@ -57,8 +57,6 @@ -elvis([{elvis_style, dont_repeat_yourself, disable}]). --define(Q_ITEM(REQUEST), {q_item, REQUEST}). - -define(COLLECT_REQ_LIMIT, 1000). -define(SEND_REQ(FROM, REQUEST), {'$send_req', FROM, REQUEST}). -define(QUERY(FROM, REQUEST, SENT), {query, FROM, REQUEST, SENT}). @@ -472,8 +470,7 @@ flush(Data0) -> Data = cancel_flush_timer(Data0), {keep_state, Data}; _ -> - {Q1, QAckRef, Batch0} = replayq:pop(Q0, #{count_limit => BatchSize}), - Batch = [Item || ?Q_ITEM(Item) <- Batch0], + {Q1, QAckRef, Batch} = replayq:pop(Q0, #{count_limit => BatchSize}), IsBatch = BatchSize =/= 1, %% We *must* use the new queue, because we currently can't %% `nack' a `pop'. @@ -862,16 +859,18 @@ drop_inflight_and_resume(Pid, InflightTID, Ref, Id, Index) -> %%============================================================================== %% operations for queue -queue_item_marshaller(?Q_ITEM(_) = I) -> - term_to_binary(I); queue_item_marshaller(Bin) when is_binary(Bin) -> - binary_to_term(Bin). + binary_to_term(Bin); +queue_item_marshaller(Item) -> + term_to_binary(Item). estimate_size(QItem) -> - size(queue_item_marshaller(QItem)). + erlang:external_size(QItem). -spec append_queue(id(), index(), replayq:q(), [queue_query()]) -> replayq:q(). -append_queue(Id, Index, Q, Queries) -> +append_queue(Id, Index, Q, Queries) when not is_binary(Q) -> + %% we must not append a raw binary because the marshaller will get + %% lost. Q2 = case replayq:overflow(Q) of Overflow when Overflow =< 0 -> @@ -885,22 +884,19 @@ append_queue(Id, Index, Q, Queries) -> ?SLOG(error, #{msg => drop_query, reason => queue_full, dropped => Dropped}), Q1 end, - Items = [?Q_ITEM(X) || X <- Queries], - Q3 = replayq:append(Q2, Items), + Q3 = replayq:append(Q2, Queries), emqx_resource_metrics:queuing_set(Id, Index, replayq:count(Q3)), ?tp(resource_worker_appended_to_queue, #{id => Id, items => Queries}), Q3. -spec get_first_n_from_queue(replayq:q(), pos_integer()) -> - empty | {replayq:q(), replayq:ack_ref(), [?Q_ITEM(?QUERY(_From, _Request, _HasBeenSent))]}. + empty | {replayq:q(), replayq:ack_ref(), [?QUERY(_From, _Request, _HasBeenSent)]}. get_first_n_from_queue(Q, N) -> case replayq:count(Q) of 0 -> empty; _ -> - {NewQ, QAckRef, Items} = replayq:pop(Q, #{count_limit => N}), - Queries = [X || ?Q_ITEM(X) <- Items], - {NewQ, QAckRef, Queries} + replayq:pop(Q, #{count_limit => N}) end. %%============================================================================== From 477c55d8ef09c2860d73e95ba01b64b84c21c628 Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Thu, 12 Jan 2023 14:04:13 -0300 Subject: [PATCH 0229/1802] fix: sanitizy replayq dir filepath Colons (`:`) are not allowed in Windows. --- apps/emqx_resource/src/emqx_resource_worker.erl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/emqx_resource/src/emqx_resource_worker.erl b/apps/emqx_resource/src/emqx_resource_worker.erl index f07c678b3..4d3e5be5a 100644 --- a/apps/emqx_resource/src/emqx_resource_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_worker.erl @@ -1021,9 +1021,13 @@ queue_count(Q) -> replayq:count(Q). disk_queue_dir(Id, Index) -> - QDir = binary_to_list(Id) ++ ":" ++ integer_to_list(Index), + QDir0 = binary_to_list(Id) ++ "_" ++ integer_to_list(Index), + QDir = sanitizy_file_path(QDir0), filename:join([emqx:data_dir(), "resource_worker", node(), QDir]). +sanitizy_file_path(Filepath) -> + iolist_to_binary(string:replace(Filepath, ":", "_", all)). + clear_disk_queue_dir(Id, Index) -> ReplayQDir = disk_queue_dir(Id, Index), case file:del_dir_r(ReplayQDir) of From af6807e863c107a9c913c643edb21352e2eb51b8 Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Thu, 12 Jan 2023 14:24:34 -0300 Subject: [PATCH 0230/1802] refactor: cancel flush timer sooner Avoids the cancellation being delayed. --- .../src/emqx_resource_worker.erl | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/apps/emqx_resource/src/emqx_resource_worker.erl b/apps/emqx_resource/src/emqx_resource_worker.erl index 4d3e5be5a..5edee241f 100644 --- a/apps/emqx_resource/src/emqx_resource_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_worker.erl @@ -445,17 +445,17 @@ handle_query_requests(Request0, Data0) -> Data = Data0#{queue := NewQ}, maybe_flush(Data). -maybe_flush(Data) -> +maybe_flush(Data0) -> #{ batch_size := BatchSize, queue := Q - } = Data, + } = Data0, QueueCount = queue_count(Q), case QueueCount >= BatchSize of true -> - flush(Data); + flush(Data0); false -> - {keep_state, ensure_flush_timer(Data)} + {keep_state, ensure_flush_timer(Data0)} end. %% Called during the `running' state only. @@ -465,19 +465,19 @@ flush(Data0) -> batch_size := BatchSize, queue := Q0 } = Data0, + Data1 = cancel_flush_timer(Data0), case replayq:count(Q0) of 0 -> - Data = cancel_flush_timer(Data0), - {keep_state, Data}; + {keep_state, Data1}; _ -> {Q1, QAckRef, Batch} = replayq:pop(Q0, #{count_limit => BatchSize}), IsBatch = BatchSize =/= 1, %% We *must* use the new queue, because we currently can't %% `nack' a `pop'. %% Maybe we could re-open the queue? - Data1 = Data0#{queue := Q1}, + Data2 = Data1#{queue := Q1}, Ref = make_message_ref(), - do_flush(Data1, #{ + do_flush(Data2, #{ new_queue => Q1, is_batch => IsBatch, batch => Batch, @@ -511,7 +511,6 @@ do_flush( [?QUERY(From, CoreReq, HasBeenSent) = Request] = Batch, QueryOpts = #{inflight_name => InflightTID}, Result = call_query(configured, Id, Index, Ref, Request, QueryOpts), - Data1 = cancel_flush_timer(Data0), Reply = ?REPLY(From, CoreReq, HasBeenSent, Result), case reply_caller(Id, Reply) of %% Failed; remove the request from the queue, as we cannot pop @@ -525,7 +524,7 @@ do_flush( is_not_connected_result(Result), ShouldPreserveInInflight andalso inflight_append(InflightTID, Ref, Request, Id, Index), emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), - {next_state, blocked, Data1}; + {next_state, blocked, Data0}; %% Success; just ack. false -> ok = replayq:ack(Q1, QAckRef), @@ -533,9 +532,9 @@ do_flush( emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), case replayq:count(Q1) > 0 of true -> - {keep_state, Data1, [{next_event, internal, flush}]}; + {keep_state, Data0, [{next_event, internal, flush}]}; false -> - {keep_state, Data1} + {keep_state, Data0} end end; do_flush(Data0, #{ @@ -553,7 +552,6 @@ do_flush(Data0, #{ } = Data0, QueryOpts = #{inflight_name => InflightTID}, Result = call_query(configured, Id, Index, Ref, Batch, QueryOpts), - Data1 = cancel_flush_timer(Data0), case batch_reply_caller(Id, Result, Batch) of %% Failed; remove the request from the queue, as we cannot pop %% from it again. But we must ensure it's in the inflight @@ -566,7 +564,7 @@ do_flush(Data0, #{ is_not_connected_result(Result), ShouldPreserveInInflight andalso inflight_append(InflightTID, Ref, Batch, Id, Index), emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), - {next_state, blocked, Data1}; + {next_state, blocked, Data0}; %% Success; just ack. false -> ok = replayq:ack(Q1, QAckRef), @@ -575,12 +573,12 @@ do_flush(Data0, #{ CurrentCount = replayq:count(Q1), case {CurrentCount > 0, CurrentCount >= BatchSize} of {false, _} -> - {keep_state, Data1}; + {keep_state, Data0}; {true, true} -> - {keep_state, Data1, [{next_event, internal, flush}]}; + {keep_state, Data0, [{next_event, internal, flush}]}; {true, false} -> - Data2 = ensure_flush_timer(Data1), - {keep_state, Data2} + Data1 = ensure_flush_timer(Data0), + {keep_state, Data1} end end. From 249c4c1c799584090bccc64fc596c883c9dbeb1a Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Thu, 12 Jan 2023 18:26:38 +0100 Subject: [PATCH 0231/1802] refactor: use 'bufs' for resource worker replayq dir --- apps/emqx_resource/src/emqx_resource_worker.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx_resource/src/emqx_resource_worker.erl b/apps/emqx_resource/src/emqx_resource_worker.erl index 5edee241f..d301ce03c 100644 --- a/apps/emqx_resource/src/emqx_resource_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_worker.erl @@ -1021,7 +1021,7 @@ queue_count(Q) -> disk_queue_dir(Id, Index) -> QDir0 = binary_to_list(Id) ++ "_" ++ integer_to_list(Index), QDir = sanitizy_file_path(QDir0), - filename:join([emqx:data_dir(), "resource_worker", node(), QDir]). + filename:join([emqx:data_dir(), "bufs", node(), QDir]). sanitizy_file_path(Filepath) -> iolist_to_binary(string:replace(Filepath, ":", "_", all)). From 618b97870bf82f3381b19a3cf0123657fa43f789 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Thu, 12 Jan 2023 18:51:52 +0100 Subject: [PATCH 0232/1802] refactor: call local function queue_count everywhere --- apps/emqx_resource/src/emqx_resource_worker.erl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/emqx_resource/src/emqx_resource_worker.erl b/apps/emqx_resource/src/emqx_resource_worker.erl index d301ce03c..02edba2b9 100644 --- a/apps/emqx_resource/src/emqx_resource_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_worker.erl @@ -466,7 +466,7 @@ flush(Data0) -> queue := Q0 } = Data0, Data1 = cancel_flush_timer(Data0), - case replayq:count(Q0) of + case queue_count(Q0) of 0 -> {keep_state, Data1}; _ -> @@ -530,7 +530,7 @@ do_flush( ok = replayq:ack(Q1, QAckRef), is_async(Id) orelse inflight_drop(InflightTID, Ref, Id, Index), emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), - case replayq:count(Q1) > 0 of + case queue_count(Q1) > 0 of true -> {keep_state, Data0, [{next_event, internal, flush}]}; false -> @@ -570,7 +570,7 @@ do_flush(Data0, #{ ok = replayq:ack(Q1, QAckRef), is_async(Id) orelse inflight_drop(InflightTID, Ref, Id, Index), emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), - CurrentCount = replayq:count(Q1), + CurrentCount = queue_count(Q1), case {CurrentCount > 0, CurrentCount >= BatchSize} of {false, _} -> {keep_state, Data0}; @@ -883,14 +883,14 @@ append_queue(Id, Index, Q, Queries) when not is_binary(Q) -> Q1 end, Q3 = replayq:append(Q2, Queries), - emqx_resource_metrics:queuing_set(Id, Index, replayq:count(Q3)), + emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q3)), ?tp(resource_worker_appended_to_queue, #{id => Id, items => Queries}), Q3. -spec get_first_n_from_queue(replayq:q(), pos_integer()) -> empty | {replayq:q(), replayq:ack_ref(), [?QUERY(_From, _Request, _HasBeenSent)]}. get_first_n_from_queue(Q, N) -> - case replayq:count(Q) of + case queue_count(Q) of 0 -> empty; _ -> From cdd8de11b0aa64fc233a6b97636bb400bf19437f Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Thu, 12 Jan 2023 18:52:25 +0100 Subject: [PATCH 0233/1802] chore: fix a typo in function name --- apps/emqx_resource/src/emqx_resource_worker.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/emqx_resource/src/emqx_resource_worker.erl b/apps/emqx_resource/src/emqx_resource_worker.erl index 02edba2b9..a0040922e 100644 --- a/apps/emqx_resource/src/emqx_resource_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_worker.erl @@ -1020,10 +1020,10 @@ queue_count(Q) -> disk_queue_dir(Id, Index) -> QDir0 = binary_to_list(Id) ++ "_" ++ integer_to_list(Index), - QDir = sanitizy_file_path(QDir0), + QDir = sanitize_file_path(QDir0), filename:join([emqx:data_dir(), "bufs", node(), QDir]). -sanitizy_file_path(Filepath) -> +sanitize_file_path(Filepath) -> iolist_to_binary(string:replace(Filepath, ":", "_", all)). clear_disk_queue_dir(Id, Index) -> From fecdbac9a8e959af5d035db10e07d48981469947 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Thu, 12 Jan 2023 19:54:47 +0100 Subject: [PATCH 0234/1802] refactor: rename a few functions --- .../src/emqx_resource_worker.erl | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/apps/emqx_resource/src/emqx_resource_worker.erl b/apps/emqx_resource/src/emqx_resource_worker.erl index a0040922e..fdc4e3104 100644 --- a/apps/emqx_resource/src/emqx_resource_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_worker.erl @@ -210,9 +210,9 @@ blocked(enter, _, #{resume_interval := ResumeT} = _St) -> blocked(cast, block, _St) -> keep_state_and_data; blocked(cast, resume, St) -> - do_resume(St); + resume_from_blocked(St); blocked(state_timeout, resume, St) -> - do_resume(St); + resume_from_blocked(St); blocked(info, ?SEND_REQ(ReqFrom, {query, Request, Opts}), Data0) -> #{ id := Id, @@ -227,6 +227,7 @@ blocked(info, ?SEND_REQ(ReqFrom, {query, Request, Opts}), Data0) -> Error = ?RESOURCE_ERROR(blocked, "resource is blocked"), HasBeenSent = false, _ = reply_caller(Id, ?REPLY(From, Request, HasBeenSent, Error)), + %% TODO collect requests NewQ = append_queue(Id, Index, Q, [?QUERY(From, Request, HasBeenSent)]), Data = Data0#{queue := NewQ}, {keep_state, Data}; @@ -291,14 +292,14 @@ pick_cast(Id, Key, Query) -> ok end). -do_resume(#{id := Id, inflight_tid := InflightTID} = Data) -> +resume_from_blocked(Data) -> case inflight_get_first(InflightTID) of empty -> retry_queue(Data); {Ref, FirstQuery} -> %% We retry msgs in inflight window sync, as if we send them %% async, they will be appended to the end of inflight window again. - retry_inflight_sync(Id, Ref, FirstQuery, InflightTID, Data) + retry_inflight_sync(Ref, FirstQuery, Data) end. retry_queue( @@ -387,14 +388,10 @@ retry_queue( end end. -retry_inflight_sync( - Id, - Ref, - QueryOrBatch, - InflightTID, - #{index := Index, resume_interval := ResumeT} = Data0 -) -> - QueryOpts = #{}, +retry_inflight_sync(Ref, QueryOrBatch, Data0) -> + #{id := Id, inflight_tid := InflightTID, index := Index, resume_interval := ResumeT} = + Data0 + QueryOpts = #{}, %% if we are retrying an inflight query, it has been sent HasBeenSent = true, Result = call_query(sync, Id, Index, Ref, QueryOrBatch, QueryOpts), @@ -416,7 +413,7 @@ retry_inflight_sync( %% we bump the counter when removing it from the table. IsDropped andalso PostFn(), ?tp(resource_worker_retry_inflight_succeeded, #{query_or_batch => QueryOrBatch}), - do_resume(Data0) + resume_from_blocked(Data0) end. %% Called during the `running' state only. From 4cb83d0c9acf544eb1df124d413fac26bb30e36b Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Thu, 12 Jan 2023 16:07:25 -0300 Subject: [PATCH 0235/1802] fix: fix some expressions after refactoring --- apps/emqx_resource/src/emqx_resource_worker.erl | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/emqx_resource/src/emqx_resource_worker.erl b/apps/emqx_resource/src/emqx_resource_worker.erl index fdc4e3104..a0facbed3 100644 --- a/apps/emqx_resource/src/emqx_resource_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_worker.erl @@ -293,6 +293,7 @@ pick_cast(Id, Key, Query) -> end). resume_from_blocked(Data) -> + #{inflight_tid := InflightTID} = Data, case inflight_get_first(InflightTID) of empty -> retry_queue(Data); @@ -389,9 +390,13 @@ retry_queue( end. retry_inflight_sync(Ref, QueryOrBatch, Data0) -> - #{id := Id, inflight_tid := InflightTID, index := Index, resume_interval := ResumeT} = - Data0 - QueryOpts = #{}, + #{ + id := Id, + inflight_tid := InflightTID, + index := Index, + resume_interval := ResumeT + } = Data0, + QueryOpts = #{}, %% if we are retrying an inflight query, it has been sent HasBeenSent = true, Result = call_query(sync, Id, Index, Ref, QueryOrBatch, QueryOpts), From 81fc561ed5eea50a777821c280e20581064b28cc Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Thu, 12 Jan 2023 16:17:29 -0300 Subject: [PATCH 0236/1802] fix(buffer_worker): check for overflow after enqueuing new requests --- .../src/emqx_resource_worker.erl | 12 +++--- .../test/emqx_resource_SUITE.erl | 38 +++++++++++++++++++ 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/apps/emqx_resource/src/emqx_resource_worker.erl b/apps/emqx_resource/src/emqx_resource_worker.erl index a0facbed3..eb4921c72 100644 --- a/apps/emqx_resource/src/emqx_resource_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_worker.erl @@ -871,23 +871,23 @@ estimate_size(QItem) -> append_queue(Id, Index, Q, Queries) when not is_binary(Q) -> %% we must not append a raw binary because the marshaller will get %% lost. + Q0 = replayq:append(Q, Queries), Q2 = - case replayq:overflow(Q) of + case replayq:overflow(Q0) of Overflow when Overflow =< 0 -> - Q; + Q0; Overflow -> PopOpts = #{bytes_limit => Overflow, count_limit => 999999999}, - {Q1, QAckRef, Items2} = replayq:pop(Q, PopOpts), + {Q1, QAckRef, Items2} = replayq:pop(Q0, PopOpts), ok = replayq:ack(Q1, QAckRef), Dropped = length(Items2), emqx_resource_metrics:dropped_queue_full_inc(Id), ?SLOG(error, #{msg => drop_query, reason => queue_full, dropped => Dropped}), Q1 end, - Q3 = replayq:append(Q2, Queries), - emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q3)), + emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q2)), ?tp(resource_worker_appended_to_queue, #{id => Id, items => Queries}), - Q3. + Q2. -spec get_first_n_from_queue(replayq:q(), pos_integer()) -> empty | {replayq:q(), replayq:ack_ref(), [?QUERY(_From, _Request, _HasBeenSent)]}. diff --git a/apps/emqx_resource/test/emqx_resource_SUITE.erl b/apps/emqx_resource/test/emqx_resource_SUITE.erl index a2767ee6c..047245c8b 100644 --- a/apps/emqx_resource/test/emqx_resource_SUITE.erl +++ b/apps/emqx_resource/test/emqx_resource_SUITE.erl @@ -1219,6 +1219,44 @@ t_delete_and_re_create_with_same_name(_Config) -> ), ok. +%% check that, if we configure a max queue size too small, then we +%% never send requests and always overflow. +t_always_overflow(_Config) -> + {ok, _} = emqx_resource:create( + ?ID, + ?DEFAULT_RESOURCE_GROUP, + ?TEST_RESOURCE, + #{name => test_resource}, + #{ + query_mode => sync, + batch_size => 1, + worker_pool_size => 1, + max_queue_bytes => 1, + resume_interval => 1_000 + } + ), + ?check_trace( + begin + Payload = binary:copy(<<"a">>, 100), + %% since it's sync and it should never send a request, this + %% errors with `timeout'. + ?assertError( + timeout, + emqx_resource:query( + ?ID, + {big_payload, Payload}, + #{timeout => 500} + ) + ), + ok + end, + fun(Trace) -> + ?assertEqual([], ?of_kind(call_query_enter, Trace)), + ok + end + ), + ok. + %%------------------------------------------------------------------------------ %% Helpers %%------------------------------------------------------------------------------ From d6a9d0aa484935e3f4bb1b85ca750c3e03334873 Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Thu, 12 Jan 2023 16:19:02 -0300 Subject: [PATCH 0237/1802] fix: set queuing to 0 after buffer worker termination --- apps/emqx_resource/src/emqx_resource_worker.erl | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/emqx_resource/src/emqx_resource_worker.erl b/apps/emqx_resource/src/emqx_resource_worker.erl index eb4921c72..deca3a5cd 100644 --- a/apps/emqx_resource/src/emqx_resource_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_worker.erl @@ -238,10 +238,12 @@ blocked(info, Info, _Data) -> keep_state_and_data. terminate(_Reason, #{id := Id, index := Index, queue := Q}) -> - emqx_resource_metrics:inflight_set(Id, Index, 0), - emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q)), - gproc_pool:disconnect_worker(Id, {Id, Index}), replayq:close(Q), + emqx_resource_metrics:inflight_set(Id, Index, 0), + %% since we want volatile queues, this will be 0 after + %% termination. + emqx_resource_metrics:queuing_set(Id, Index, 0), + gproc_pool:disconnect_worker(Id, {Id, Index}), ok. code_change(_OldVsn, State, _Extra) -> From d4724d6ce9e3d71a3d2071efd589ef1c65ce5391 Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Thu, 12 Jan 2023 16:38:28 -0300 Subject: [PATCH 0238/1802] refactor: remove redundant function `retry_queue` does basically what the running state does, now that we refactored the buffer workers to always use the queue. --- .../src/emqx_resource_worker.erl | 98 +------------------ 1 file changed, 1 insertion(+), 97 deletions(-) diff --git a/apps/emqx_resource/src/emqx_resource_worker.erl b/apps/emqx_resource/src/emqx_resource_worker.erl index deca3a5cd..7ac6c5250 100644 --- a/apps/emqx_resource/src/emqx_resource_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_worker.erl @@ -298,99 +298,13 @@ resume_from_blocked(Data) -> #{inflight_tid := InflightTID} = Data, case inflight_get_first(InflightTID) of empty -> - retry_queue(Data); + {next_state, running, Data}; {Ref, FirstQuery} -> %% We retry msgs in inflight window sync, as if we send them %% async, they will be appended to the end of inflight window again. retry_inflight_sync(Ref, FirstQuery, Data) end. -retry_queue( - #{ - queue := Q0, - id := Id, - index := Index, - batch_size := 1, - inflight_tid := InflightTID, - resume_interval := ResumeT - } = Data0 -) -> - %% no batching - case get_first_n_from_queue(Q0, 1) of - empty -> - {next_state, running, Data0}; - {Q1, QAckRef, [?QUERY(_, Request, HasBeenSent) = Query]} -> - Data = Data0#{queue := Q1}, - QueryOpts = #{inflight_name => InflightTID}, - Ref = make_message_ref(), - Result = call_query(configured, Id, Index, Ref, Query, QueryOpts), - Reply = ?REPLY(undefined, Request, HasBeenSent, Result), - case reply_caller(Id, Reply) of - true -> - %% Still failed, but now it's in the inflight - %% table and marked as sent, except if the result - %% says inflight is full. In this case, we must - %% ensure it's indeed in the inflight table or - %% risk lose it. - ok = replayq:ack(Q1, QAckRef), - is_inflight_full_result(Result) andalso - inflight_append(InflightTID, Ref, Query, Id, Index), - emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), - {keep_state, Data, {state_timeout, ResumeT, resume}}; - false -> - ok = replayq:ack(Q1, QAckRef), - is_async(Id) orelse inflight_drop(InflightTID, Ref, Id, Index), - emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), - retry_queue(Data) - end - end; -retry_queue( - #{ - queue := Q, - id := Id, - index := Index, - batch_size := BatchSize, - inflight_tid := InflightTID, - resume_interval := ResumeT - } = Data0 -) -> - %% batching - case get_first_n_from_queue(Q, BatchSize) of - empty -> - {next_state, running, Data0}; - {Q1, QAckRef, Batch0} -> - Data = Data0#{queue := Q1}, - QueryOpts = #{inflight_name => InflightTID}, - Ref = make_message_ref(), - Result = call_query(configured, Id, Index, Ref, Batch0, QueryOpts), - %% The caller has been replied with ?RESOURCE_ERROR(blocked, _) before saving into the queue, - %% we now change the 'from' field to 'undefined' so it will not reply the caller again. - Batch = [ - ?QUERY(undefined, Request, HasBeenSent0) - || ?QUERY(_, Request, HasBeenSent0) <- Batch0 - ], - case batch_reply_caller(Id, Result, Batch) of - true -> - ?tp(resource_worker_retry_queue_batch_failed, #{batch => Batch}), - %% Still failed, but now it's in the inflight - %% table and marked as sent, except if the result - %% says inflight is full. In this case, we must - %% ensure it's indeed in the inflight table or - %% risk lose it. - ok = replayq:ack(Q1, QAckRef), - is_inflight_full_result(Result) andalso - inflight_append(InflightTID, Ref, Batch, Id, Index), - emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), - {keep_state, Data, {state_timeout, ResumeT, resume}}; - false -> - ?tp(resource_worker_retry_queue_batch_succeeded, #{batch => Batch}), - ok = replayq:ack(Q1, QAckRef), - is_async(Id) orelse inflight_drop(InflightTID, Ref, Id, Index), - emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), - retry_queue(Data) - end - end. - retry_inflight_sync(Ref, QueryOrBatch, Data0) -> #{ id := Id, @@ -891,16 +805,6 @@ append_queue(Id, Index, Q, Queries) when not is_binary(Q) -> ?tp(resource_worker_appended_to_queue, #{id => Id, items => Queries}), Q2. --spec get_first_n_from_queue(replayq:q(), pos_integer()) -> - empty | {replayq:q(), replayq:ack_ref(), [?QUERY(_From, _Request, _HasBeenSent)]}. -get_first_n_from_queue(Q, N) -> - case queue_count(Q) of - 0 -> - empty; - _ -> - replayq:pop(Q, #{count_limit => N}) - end. - %%============================================================================== %% the inflight queue for async query -define(MAX_SIZE_REF, -1). From 196bf1c5ba0e04ada28646e0931ef1e601d7644d Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Thu, 12 Jan 2023 16:47:45 -0300 Subject: [PATCH 0239/1802] feat: mass collect calls from mailbox also when blocked --- .../src/emqx_resource_worker.erl | 30 +++++++------------ 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/apps/emqx_resource/src/emqx_resource_worker.erl b/apps/emqx_resource/src/emqx_resource_worker.erl index 7ac6c5250..a37d78494 100644 --- a/apps/emqx_resource/src/emqx_resource_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_worker.erl @@ -213,23 +213,11 @@ blocked(cast, resume, St) -> resume_from_blocked(St); blocked(state_timeout, resume, St) -> resume_from_blocked(St); -blocked(info, ?SEND_REQ(ReqFrom, {query, Request, Opts}), Data0) -> - #{ - id := Id, - index := Index, - queue := Q - } = Data0, - From = - case ReqFrom of - undefined -> maps:get(async_reply_fun, Opts, undefined); - From1 -> From1 - end, +blocked(info, ?SEND_REQ(_ReqFrom, {query, _Request, _Opts}) = Request0, Data0) -> + #{id := Id} = Data0, + {Queries, Data} = collect_and_enqueue_query_requests(Request0, Data0), Error = ?RESOURCE_ERROR(blocked, "resource is blocked"), - HasBeenSent = false, - _ = reply_caller(Id, ?REPLY(From, Request, HasBeenSent, Error)), - %% TODO collect requests - NewQ = append_queue(Id, Index, Q, [?QUERY(From, Request, HasBeenSent)]), - Data = Data0#{queue := NewQ}, + _ = batch_reply_caller(Id, Error, Queries), {keep_state, Data}; blocked(info, {flush, _Ref}, _Data) -> keep_state_and_data; @@ -340,13 +328,17 @@ retry_inflight_sync(Ref, QueryOrBatch, Data0) -> %% Called during the `running' state only. -spec handle_query_requests(?SEND_REQ(request_from(), request()), data()) -> data(). handle_query_requests(Request0, Data0) -> + {_Queries, Data} = collect_and_enqueue_query_requests(Request0, Data0), + maybe_flush(Data). + +collect_and_enqueue_query_requests(Request0, Data0) -> #{ id := Id, index := Index, queue := Q } = Data0, Requests = collect_requests([Request0], ?COLLECT_REQ_LIMIT), - QueueItems = + Queries = lists:map( fun (?SEND_REQ(undefined = _From, {query, Req, Opts})) -> @@ -359,9 +351,9 @@ handle_query_requests(Request0, Data0) -> end, Requests ), - NewQ = append_queue(Id, Index, Q, QueueItems), + NewQ = append_queue(Id, Index, Q, Queries), Data = Data0#{queue := NewQ}, - maybe_flush(Data). + {Queries, Data}. maybe_flush(Data0) -> #{ From 7401d6f0ce5f0d374b278e9d1bbe32db82bc373e Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Fri, 13 Jan 2023 10:17:15 -0300 Subject: [PATCH 0240/1802] refactor: rename ack fn --- .../src/emqx_resource_worker.erl | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/apps/emqx_resource/src/emqx_resource_worker.erl b/apps/emqx_resource/src/emqx_resource_worker.erl index a37d78494..d3b797fd5 100644 --- a/apps/emqx_resource/src/emqx_resource_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_worker.erl @@ -313,14 +313,14 @@ retry_inflight_sync(Ref, QueryOrBatch, Data0) -> {keep_state, Data0, {state_timeout, ResumeT, resume}}; %% Send ok or failed but the resource is working {false, PostFn} -> - IsDropped = inflight_drop(InflightTID, Ref, Id, Index), + IsAcked = ack_inflight(InflightTID, Ref, Id, Index), %% we need to defer bumping the counters after %% `inflight_drop' to avoid the race condition when an %% inflight request might get completed concurrently with %% the retry, bumping them twice. Since both inflight %% requests (repeated and original) have the safe `Ref', %% we bump the counter when removing it from the table. - IsDropped andalso PostFn(), + IsAcked andalso PostFn(), ?tp(resource_worker_retry_inflight_succeeded, #{query_or_batch => QueryOrBatch}), resume_from_blocked(Data0) end. @@ -438,7 +438,7 @@ do_flush( %% Success; just ack. false -> ok = replayq:ack(Q1, QAckRef), - is_async(Id) orelse inflight_drop(InflightTID, Ref, Id, Index), + is_async(Id) orelse ack_inflight(InflightTID, Ref, Id, Index), emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), case queue_count(Q1) > 0 of true -> @@ -478,7 +478,7 @@ do_flush(Data0, #{ %% Success; just ack. false -> ok = replayq:ack(Q1, QAckRef), - is_async(Id) orelse inflight_drop(InflightTID, Ref, Id, Index), + is_async(Id) orelse ack_inflight(InflightTID, Ref, Id, Index), emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), CurrentCount = queue_count(Q1), case {CurrentCount > 0, CurrentCount >= BatchSize} of @@ -736,8 +736,8 @@ reply_after_query(Pid, Id, Index, InflightTID, Ref, ?QUERY(From, Request, HasBee PostFn(), ?MODULE:block(Pid); {false, PostFn} -> - IsDropped = drop_inflight_and_resume(Pid, InflightTID, Ref, Id, Index), - IsDropped andalso PostFn(), + IsAcked = ack_inflight_and_resume(Pid, InflightTID, Ref, Id, Index), + IsAcked andalso PostFn(), ok end. @@ -750,19 +750,19 @@ batch_reply_after_query(Pid, Id, Index, InflightTID, Ref, Batch, Result) -> lists:foreach(fun(F) -> F() end, PostFns), ?MODULE:block(Pid); {false, PostFns} -> - IsDropped = drop_inflight_and_resume(Pid, InflightTID, Ref, Id, Index), - IsDropped andalso lists:foreach(fun(F) -> F() end, PostFns), + IsAcked = ack_inflight_and_resume(Pid, InflightTID, Ref, Id, Index), + IsAcked andalso lists:foreach(fun(F) -> F() end, PostFns), ok end. -drop_inflight_and_resume(Pid, InflightTID, Ref, Id, Index) -> +ack_inflight_and_resume(Pid, InflightTID, Ref, Id, Index) -> case is_inflight_full(InflightTID) of true -> - IsDropped = inflight_drop(InflightTID, Ref, Id, Index), + IsAcked = ack_inflight(InflightTID, Ref, Id, Index), ?MODULE:resume(Pid), - IsDropped; + IsAcked; false -> - inflight_drop(InflightTID, Ref, Id, Index) + ack_inflight(InflightTID, Ref, Id, Index) end. %%============================================================================== @@ -871,19 +871,19 @@ inflight_append(InflightTID, Ref, Data, _Id, _Index) -> %% the inflight metric. ok. -inflight_drop(undefined, _Ref, _Id, _Index) -> +ack_inflight(undefined, _Ref, _Id, _Index) -> false; -inflight_drop(InflightTID, Ref, Id, Index) -> +ack_inflight(InflightTID, Ref, Id, Index) -> Count = case ets:take(InflightTID, Ref) of [{Ref, ?QUERY(_, _, _)}] -> 1; [{Ref, [?QUERY(_, _, _) | _] = Batch}] -> length(Batch); _ -> 0 end, - IsDropped = Count > 0, - IsDropped andalso ets:update_counter(InflightTID, ?SIZE_REF, {2, -Count, 0, 0}), + IsAcked = Count > 0, + IsAcked andalso ets:update_counter(InflightTID, ?SIZE_REF, {2, -Count, 0, 0}), emqx_resource_metrics:inflight_set(Id, Index, inflight_num_msgs(InflightTID)), - IsDropped. + IsAcked. %%============================================================================== From 30a227bd3832e8749b008ab6ab847de55f82e85a Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Fri, 13 Jan 2023 10:26:11 -0300 Subject: [PATCH 0241/1802] refactor: rename `resume` state timeout to `unblock` --- apps/emqx_resource/src/emqx_resource_worker.erl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/emqx_resource/src/emqx_resource_worker.erl b/apps/emqx_resource/src/emqx_resource_worker.erl index d3b797fd5..4e9e75085 100644 --- a/apps/emqx_resource/src/emqx_resource_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_worker.erl @@ -206,12 +206,12 @@ running(info, Info, _St) -> blocked(enter, _, #{resume_interval := ResumeT} = _St) -> ?tp(resource_worker_enter_blocked, #{}), - {keep_state_and_data, {state_timeout, ResumeT, resume}}; + {keep_state_and_data, {state_timeout, ResumeT, unblock}}; blocked(cast, block, _St) -> keep_state_and_data; blocked(cast, resume, St) -> resume_from_blocked(St); -blocked(state_timeout, resume, St) -> +blocked(state_timeout, unblock, St) -> resume_from_blocked(St); blocked(info, ?SEND_REQ(_ReqFrom, {query, _Request, _Opts}) = Request0, Data0) -> #{id := Id} = Data0, @@ -310,7 +310,7 @@ retry_inflight_sync(Ref, QueryOrBatch, Data0) -> {true, PostFn} -> PostFn(), ?tp(resource_worker_retry_inflight_failed, #{query_or_batch => QueryOrBatch}), - {keep_state, Data0, {state_timeout, ResumeT, resume}}; + {keep_state, Data0, {state_timeout, ResumeT, unblock}}; %% Send ok or failed but the resource is working {false, PostFn} -> IsAcked = ack_inflight(InflightTID, Ref, Id, Index), From 478fcc6ffd52d9bc17ee58b773ca28cf8fb5c4dd Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Fri, 13 Jan 2023 13:50:41 -0300 Subject: [PATCH 0242/1802] test: fix flaky test --- .../test/emqx_bridge_mqtt_SUITE.erl | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl b/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl index 048c039bf..1f03863ae 100644 --- a/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl +++ b/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl @@ -953,18 +953,17 @@ t_mqtt_conn_bridge_egress_reconnect(_) -> ok. assert_mqtt_msg_received(Topic, Payload) -> - ?assert( - receive - {deliver, Topic, #message{payload = Payload}} -> - ct:pal("Got mqtt message: ~p on topic ~p", [Payload, Topic]), - true; - Msg -> - ct:pal("Unexpected Msg: ~p", [Msg]), - false - after 100 -> - false - end - ). + ct:pal("checking if ~p has been received on ~p", [Payload, Topic]), + receive + {deliver, Topic, #message{payload = Payload}} -> + ct:pal("Got mqtt message: ~p on topic ~p", [Payload, Topic]), + ok; + Msg -> + ct:pal("Unexpected Msg: ~p", [Msg]), + assert_mqtt_msg_received(Topic, Payload) + after 100 -> + ct:fail("timeout waiting for ~p on topic ~p", [Payload, Topic]) + end. request(Method, Url, Body) -> request(<<"connector_admin">>, Method, Url, Body). From bd95a954096f25b770edb30b2ea2905d05ccc4a5 Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Fri, 13 Jan 2023 15:58:45 -0300 Subject: [PATCH 0243/1802] refactor: remove redundant `BlockWorker` arg, change boolean to ack/nack `BlockWorker` was always false (ack). Also, changed the return to something more semantic than a boolean to avoid [boolean blindness](https://runtimeverification.com/blog/code-smell-boolean-blindness/) --- .../src/emqx_resource_worker.erl | 99 +++++++++---------- 1 file changed, 46 insertions(+), 53 deletions(-) diff --git a/apps/emqx_resource/src/emqx_resource_worker.erl b/apps/emqx_resource/src/emqx_resource_worker.erl index 4e9e75085..2d646f4b8 100644 --- a/apps/emqx_resource/src/emqx_resource_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_worker.erl @@ -117,8 +117,7 @@ simple_sync_query(Id, Request) -> Ref = make_message_ref(), Result = call_query(sync, Id, Index, Ref, ?QUERY(self(), Request, false), QueryOpts), HasBeenSent = false, - BlockWorker = false, - _ = handle_query_result(Id, Result, HasBeenSent, BlockWorker), + _ = handle_query_result(Id, Result, HasBeenSent), Result. -spec simple_async_query(id(), request(), reply_fun()) -> Result :: term(). @@ -135,8 +134,7 @@ simple_async_query(Id, Request, ReplyFun) -> Ref = make_message_ref(), Result = call_query(async, Id, Index, Ref, ?QUERY(ReplyFun, Request, false), QueryOpts), HasBeenSent = false, - BlockWorker = false, - _ = handle_query_result(Id, Result, HasBeenSent, BlockWorker), + _ = handle_query_result(Id, Result, HasBeenSent), Result. -spec block(pid()) -> ok. @@ -304,15 +302,14 @@ retry_inflight_sync(Ref, QueryOrBatch, Data0) -> %% if we are retrying an inflight query, it has been sent HasBeenSent = true, Result = call_query(sync, Id, Index, Ref, QueryOrBatch, QueryOpts), - BlockWorker = false, - case handle_query_result_pure(Id, Result, HasBeenSent, BlockWorker) of + case handle_query_result_pure(Id, Result, HasBeenSent) of %% Send failed because resource is down - {true, PostFn} -> + {nack, PostFn} -> PostFn(), ?tp(resource_worker_retry_inflight_failed, #{query_or_batch => QueryOrBatch}), {keep_state, Data0, {state_timeout, ResumeT, unblock}}; %% Send ok or failed but the resource is working - {false, PostFn} -> + {ack, PostFn} -> IsAcked = ack_inflight(InflightTID, Ref, Id, Index), %% we need to defer bumping the counters after %% `inflight_drop' to avoid the race condition when an @@ -427,7 +424,7 @@ do_flush( %% from it again. But we must ensure it's in the inflight %% table, even if it's full, so we don't lose the request. %% And only in that case. - true -> + nack -> ok = replayq:ack(Q1, QAckRef), ShouldPreserveInInflight = is_inflight_full_result(Result) orelse @@ -436,7 +433,7 @@ do_flush( emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), {next_state, blocked, Data0}; %% Success; just ack. - false -> + ack -> ok = replayq:ack(Q1, QAckRef), is_async(Id) orelse ack_inflight(InflightTID, Ref, Id, Index), emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), @@ -467,7 +464,7 @@ do_flush(Data0, #{ %% from it again. But we must ensure it's in the inflight %% table, even if it's full, so we don't lose the request. %% And only in that case. - true -> + nack -> ok = replayq:ack(Q1, QAckRef), ShouldPreserveInInflight = is_inflight_full_result(Result) orelse @@ -476,7 +473,7 @@ do_flush(Data0, #{ emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), {next_state, blocked, Data0}; %% Success; just ack. - false -> + ack -> ok = replayq:ack(Q1, QAckRef), is_async(Id) orelse ack_inflight(InflightTID, Ref, Id, Index), emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), @@ -499,11 +496,11 @@ batch_reply_caller(Id, BatchResult, Batch) -> batch_reply_caller_defer_metrics(Id, BatchResult, Batch) -> lists:foldl( - fun(Reply, {BlockWorker, PostFns}) -> - {ShouldBlock, PostFn} = reply_caller_defer_metrics(Id, Reply, BlockWorker), + fun(Reply, {_ShouldBlock, PostFns}) -> + {ShouldBlock, PostFn} = reply_caller_defer_metrics(Id, Reply), {ShouldBlock, [PostFn | PostFns]} end, - {false, []}, + {ack, []}, %% the `Mod:on_batch_query/3` returns a single result for a batch, %% so we need to expand ?EXPAND(BatchResult, Batch) @@ -514,13 +511,9 @@ reply_caller(Id, Reply) -> PostFn(), ShouldBlock. -reply_caller_defer_metrics(Id, Reply) -> - BlockWorker = false, - reply_caller_defer_metrics(Id, Reply, BlockWorker). - -reply_caller_defer_metrics(Id, ?REPLY(undefined, _, HasBeenSent, Result), BlockWorker) -> - handle_query_result_pure(Id, Result, HasBeenSent, BlockWorker); -reply_caller_defer_metrics(Id, ?REPLY({ReplyFun, Args}, _, HasBeenSent, Result), BlockWorker) when +reply_caller_defer_metrics(Id, ?REPLY(undefined, _, HasBeenSent, Result)) -> + handle_query_result_pure(Id, Result, HasBeenSent); +reply_caller_defer_metrics(Id, ?REPLY({ReplyFun, Args}, _, HasBeenSent, Result)) when is_function(ReplyFun) -> _ = @@ -528,49 +521,49 @@ reply_caller_defer_metrics(Id, ?REPLY({ReplyFun, Args}, _, HasBeenSent, Result), {async_return, _} -> no_reply_for_now; _ -> apply(ReplyFun, Args ++ [Result]) end, - handle_query_result_pure(Id, Result, HasBeenSent, BlockWorker); -reply_caller_defer_metrics(Id, ?REPLY(From, _, HasBeenSent, Result), BlockWorker) -> + handle_query_result_pure(Id, Result, HasBeenSent); +reply_caller_defer_metrics(Id, ?REPLY(From, _, HasBeenSent, Result)) -> gen_statem:reply(From, Result), - handle_query_result_pure(Id, Result, HasBeenSent, BlockWorker). + handle_query_result_pure(Id, Result, HasBeenSent). -handle_query_result(Id, Result, HasBeenSent, BlockWorker) -> - {ShouldBlock, PostFn} = handle_query_result_pure(Id, Result, HasBeenSent, BlockWorker), +handle_query_result(Id, Result, HasBeenSent) -> + {ShouldBlock, PostFn} = handle_query_result_pure(Id, Result, HasBeenSent), PostFn(), ShouldBlock. -handle_query_result_pure(Id, ?RESOURCE_ERROR_M(exception, Msg), HasBeenSent, BlockWorker) -> +handle_query_result_pure(Id, ?RESOURCE_ERROR_M(exception, Msg), HasBeenSent) -> PostFn = fun() -> ?SLOG(error, #{msg => resource_exception, info => Msg}), inc_sent_failed(Id, HasBeenSent), ok end, - {BlockWorker, PostFn}; -handle_query_result_pure(_Id, ?RESOURCE_ERROR_M(NotWorking, _), _HasBeenSent, _) when + {ack, PostFn}; +handle_query_result_pure(_Id, ?RESOURCE_ERROR_M(NotWorking, _), _HasBeenSent) when NotWorking == not_connected; NotWorking == blocked -> - {true, fun() -> ok end}; -handle_query_result_pure(Id, ?RESOURCE_ERROR_M(not_found, Msg), _HasBeenSent, BlockWorker) -> + {nack, fun() -> ok end}; +handle_query_result_pure(Id, ?RESOURCE_ERROR_M(not_found, Msg), _HasBeenSent) -> PostFn = fun() -> ?SLOG(error, #{id => Id, msg => resource_not_found, info => Msg}), emqx_resource_metrics:dropped_resource_not_found_inc(Id), ok end, - {BlockWorker, PostFn}; -handle_query_result_pure(Id, ?RESOURCE_ERROR_M(stopped, Msg), _HasBeenSent, BlockWorker) -> + {ack, PostFn}; +handle_query_result_pure(Id, ?RESOURCE_ERROR_M(stopped, Msg), _HasBeenSent) -> PostFn = fun() -> ?SLOG(error, #{id => Id, msg => resource_stopped, info => Msg}), emqx_resource_metrics:dropped_resource_stopped_inc(Id), ok end, - {BlockWorker, PostFn}; -handle_query_result_pure(Id, ?RESOURCE_ERROR_M(Reason, _), _HasBeenSent, BlockWorker) -> + {ack, PostFn}; +handle_query_result_pure(Id, ?RESOURCE_ERROR_M(Reason, _), _HasBeenSent) -> PostFn = fun() -> ?SLOG(error, #{id => Id, msg => other_resource_error, reason => Reason}), emqx_resource_metrics:dropped_other_inc(Id), ok end, - {BlockWorker, PostFn}; -handle_query_result_pure(Id, {error, {recoverable_error, Reason}}, _HasBeenSent, _BlockWorker) -> + {ack, PostFn}; +handle_query_result_pure(Id, {error, {recoverable_error, Reason}}, _HasBeenSent) -> %% the message will be queued in replayq or inflight window, %% i.e. the counter 'queuing' or 'dropped' will increase, so we pretend that we have not %% sent this message. @@ -578,32 +571,32 @@ handle_query_result_pure(Id, {error, {recoverable_error, Reason}}, _HasBeenSent, ?SLOG(warning, #{id => Id, msg => recoverable_error, reason => Reason}), ok end, - {true, PostFn}; -handle_query_result_pure(Id, {error, Reason}, HasBeenSent, BlockWorker) -> + {nack, PostFn}; +handle_query_result_pure(Id, {error, Reason}, HasBeenSent) -> PostFn = fun() -> ?SLOG(error, #{id => Id, msg => send_error, reason => Reason}), inc_sent_failed(Id, HasBeenSent), ok end, - {BlockWorker, PostFn}; -handle_query_result_pure(_Id, {async_return, inflight_full}, _HasBeenSent, _BlockWorker) -> - {true, fun() -> ok end}; -handle_query_result_pure(Id, {async_return, {error, Msg}}, HasBeenSent, BlockWorker) -> + {ack, PostFn}; +handle_query_result_pure(_Id, {async_return, inflight_full}, _HasBeenSent) -> + {nack, fun() -> ok end}; +handle_query_result_pure(Id, {async_return, {error, Msg}}, HasBeenSent) -> PostFn = fun() -> ?SLOG(error, #{id => Id, msg => async_send_error, info => Msg}), inc_sent_failed(Id, HasBeenSent), ok end, - {BlockWorker, PostFn}; -handle_query_result_pure(_Id, {async_return, ok}, _HasBeenSent, BlockWorker) -> - {BlockWorker, fun() -> ok end}; -handle_query_result_pure(Id, Result, HasBeenSent, BlockWorker) -> + {ack, PostFn}; +handle_query_result_pure(_Id, {async_return, ok}, _HasBeenSent) -> + {ack, fun() -> ok end}; +handle_query_result_pure(Id, Result, HasBeenSent) -> PostFn = fun() -> assert_ok_result(Result), inc_sent_success(Id, HasBeenSent), ok end, - {BlockWorker, PostFn}. + {ack, PostFn}. is_inflight_full_result({async_return, inflight_full}) -> true; @@ -732,10 +725,10 @@ reply_after_query(Pid, Id, Index, InflightTID, Ref, ?QUERY(From, Request, HasBee %% but received no ACK, NOT the number of messages queued in the %% inflight window. case reply_caller_defer_metrics(Id, ?REPLY(From, Request, HasBeenSent, Result)) of - {true, PostFn} -> + {nack, PostFn} -> PostFn(), ?MODULE:block(Pid); - {false, PostFn} -> + {ack, PostFn} -> IsAcked = ack_inflight_and_resume(Pid, InflightTID, Ref, Id, Index), IsAcked andalso PostFn(), ok @@ -746,10 +739,10 @@ batch_reply_after_query(Pid, Id, Index, InflightTID, Ref, Batch, Result) -> %% but received no ACK, NOT the number of messages queued in the %% inflight window. case batch_reply_caller_defer_metrics(Id, Result, Batch) of - {true, PostFns} -> + {nack, PostFns} -> lists:foreach(fun(F) -> F() end, PostFns), ?MODULE:block(Pid); - {false, PostFns} -> + {ack, PostFns} -> IsAcked = ack_inflight_and_resume(Pid, InflightTID, Ref, Id, Index), IsAcked andalso lists:foreach(fun(F) -> F() end, PostFns), ok From 344eeebe6321a602631b663724fc2bcaf311afd4 Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Fri, 13 Jan 2023 16:23:12 -0300 Subject: [PATCH 0244/1802] fix: always ack async replies The caller should decide if it should retry in that case, to avoid overwhelming the resource with retries. --- .../src/emqx_resource_worker.erl | 42 +++++++++++-------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/apps/emqx_resource/src/emqx_resource_worker.erl b/apps/emqx_resource/src/emqx_resource_worker.erl index 2d646f4b8..f9940210a 100644 --- a/apps/emqx_resource/src/emqx_resource_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_worker.erl @@ -724,13 +724,17 @@ reply_after_query(Pid, Id, Index, InflightTID, Ref, ?QUERY(From, Request, HasBee %% NOTE: 'inflight' is the count of messages that were sent async %% but received no ACK, NOT the number of messages queued in the %% inflight window. - case reply_caller_defer_metrics(Id, ?REPLY(From, Request, HasBeenSent, Result)) of - {nack, PostFn} -> - PostFn(), + {Action, PostFn} = reply_caller_defer_metrics(Id, ?REPLY(From, Request, HasBeenSent, Result)), + %% Should always ack async inflight requests that + %% returned, otherwise the request will get retried. The + %% caller has just been notified of the failure and should + %% decide if it wants to retry or not. + IsAcked = ack_inflight(InflightTID, Ref, Id, Index), + IsAcked andalso PostFn(), + case Action of + nack -> ?MODULE:block(Pid); - {ack, PostFn} -> - IsAcked = ack_inflight_and_resume(Pid, InflightTID, Ref, Id, Index), - IsAcked andalso PostFn(), + ack -> ok end. @@ -738,25 +742,29 @@ batch_reply_after_query(Pid, Id, Index, InflightTID, Ref, Batch, Result) -> %% NOTE: 'inflight' is the count of messages that were sent async %% but received no ACK, NOT the number of messages queued in the %% inflight window. - case batch_reply_caller_defer_metrics(Id, Result, Batch) of - {nack, PostFns} -> - lists:foreach(fun(F) -> F() end, PostFns), + {Action, PostFns} = batch_reply_caller_defer_metrics(Id, Result, Batch), + %% Should always ack async inflight requests that + %% returned, otherwise the request will get retried. The + %% caller has just been notified of the failure and should + %% decide if it wants to retry or not. + IsAcked = ack_inflight(InflightTID, Ref, Id, Index), + IsAcked andalso lists:foreach(fun(F) -> F() end, PostFns), + case Action of + nack -> ?MODULE:block(Pid); - {ack, PostFns} -> - IsAcked = ack_inflight_and_resume(Pid, InflightTID, Ref, Id, Index), - IsAcked andalso lists:foreach(fun(F) -> F() end, PostFns), + ack -> ok end. ack_inflight_and_resume(Pid, InflightTID, Ref, Id, Index) -> + IsAcked = ack_inflight(InflightTID, Ref, Id, Index), case is_inflight_full(InflightTID) of true -> - IsAcked = ack_inflight(InflightTID, Ref, Id, Index), - ?MODULE:resume(Pid), - IsAcked; + ok; false -> - ack_inflight(InflightTID, Ref, Id, Index) - end. + ?MODULE:resume(Pid) + end, + IsAcked. %%============================================================================== %% operations for queue From 5dd24a64c384ea7dc1f2850b2727d311f9a4fca5 Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Fri, 13 Jan 2023 17:30:54 -0300 Subject: [PATCH 0245/1802] refactor(buffer_worker): check if inflight is full before flushing --- .../src/emqx_resource_worker.erl | 11 +++-- .../test/emqx_resource_SUITE.erl | 49 +++++++++---------- 2 files changed, 31 insertions(+), 29 deletions(-) diff --git a/apps/emqx_resource/src/emqx_resource_worker.erl b/apps/emqx_resource/src/emqx_resource_worker.erl index f9940210a..eafe7cea6 100644 --- a/apps/emqx_resource/src/emqx_resource_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_worker.erl @@ -370,13 +370,18 @@ maybe_flush(Data0) -> flush(Data0) -> #{ batch_size := BatchSize, + inflight_tid := InflightTID, queue := Q0 } = Data0, Data1 = cancel_flush_timer(Data0), - case queue_count(Q0) of - 0 -> + case {queue_count(Q0), is_inflight_full(InflightTID)} of + {0, _} -> {keep_state, Data1}; - _ -> + {_, true} -> + ?tp(resource_worker_flush_but_inflight_full, #{}), + Data2 = ensure_flush_timer(Data1), + {keep_state, Data2}; + {_, false} -> {Q1, QAckRef, Batch} = replayq:pop(Q0, #{count_limit => BatchSize}), IsBatch = BatchSize =/= 1, %% We *must* use the new queue, because we currently can't diff --git a/apps/emqx_resource/test/emqx_resource_SUITE.erl b/apps/emqx_resource/test/emqx_resource_SUITE.erl index 047245c8b..d837cbc8c 100644 --- a/apps/emqx_resource/test/emqx_resource_SUITE.erl +++ b/apps/emqx_resource/test/emqx_resource_SUITE.erl @@ -436,8 +436,8 @@ t_query_counter_async_inflight(_) -> %% this will block the resource_worker as the inflight window is full now {ok, {ok, _}} = ?wait_async_action( - emqx_resource:query(?ID, {inc_counter, 2}), - #{?snk_kind := resource_worker_enter_blocked}, + emqx_resource:query(?ID, {inc_counter, 199}), + #{?snk_kind := resource_worker_flush_but_inflight_full}, 1_000 ), ?assertMatch(0, ets:info(Tab0, size)), @@ -449,25 +449,24 @@ t_query_counter_async_inflight(_) -> ets:insert(Tab, {Ref, Result}), ?tp(tmp_query_inserted, #{}) end, - {ok, {ok, _}} = - ?wait_async_action( - emqx_resource:query(?ID, {inc_counter, 3}, #{ - async_reply_fun => {Insert, [Tab0, tmp_query]} - }), - #{?snk_kind := tmp_query_inserted}, - 1_000 - ), %% since this counts as a failure, it'll be enqueued and retried %% later, when the resource is unblocked. - ?assertMatch([{_, {error, {resource_error, #{reason := blocked}}}}], ets:take(Tab0, tmp_query)), + {ok, {ok, _}} = + ?wait_async_action( + emqx_resource:query(?ID, {inc_counter, 99}, #{ + async_reply_fun => {Insert, [Tab0, tmp_query]} + }), + #{?snk_kind := resource_worker_appended_to_queue}, + 1_000 + ), tap_metrics(?LINE), %% all responses should be received after the resource is resumed. {ok, SRef0} = snabbkaffe:subscribe( ?match_event(#{?snk_kind := connector_demo_inc_counter_async}), - %% +1 because the tmp_query above will be retried and succeed - %% this time. - WindowSize + 1, + %% +2 because the tmp_query above will be retried and succeed + %% this time, and there was the inc 199 request as well. + WindowSize + 2, _Timeout0 = 10_000 ), ?assertMatch(ok, emqx_resource:simple_sync_query(?ID, resume)), @@ -478,8 +477,6 @@ t_query_counter_async_inflight(_) -> %% take it again from the table; this time, it should have %% succeeded. ?assertMatch([{tmp_query, ok}], ets:take(Tab0, tmp_query)), - ?assertEqual(WindowSize, ets:info(Tab0, size), #{tab => ets:tab2list(Tab0)}), - tap_metrics(?LINE), %% send async query, this time everything should be ok. Num = 10, @@ -496,11 +493,12 @@ t_query_counter_async_inflight(_) -> end, fun(Trace) -> QueryTrace = ?of_kind(call_query_async, Trace), - ?assertMatch([#{query := {query, _, {inc_counter, _}, _}} | _], QueryTrace) + ?assertMatch([#{query := {query, _, {inc_counter, _}, _}} | _], QueryTrace), + ?assertEqual(WindowSize + Num, ets:info(Tab0, size), #{tab => ets:tab2list(Tab0)}), + tap_metrics(?LINE), + ok end ), - ?assertEqual(WindowSize + Num, ets:info(Tab0, size), #{tab => ets:tab2list(Tab0)}), - tap_metrics(?LINE), %% block the resource ?assertMatch(ok, emqx_resource:simple_sync_query(?ID, block)), @@ -525,7 +523,7 @@ t_query_counter_async_inflight(_) -> ), ?assertMatch(ok, emqx_resource:simple_sync_query(?ID, resume)), {ok, _} = snabbkaffe:receive_events(SRef1), - ?assertEqual(Sent, ets:info(Tab0, size)), + ?assertEqual(Sent, ets:info(Tab0, size), #{tab => ets:tab2list(Tab0)}), tap_metrics(?LINE), {ok, Counter} = emqx_resource:simple_sync_query(?ID, get_counter), @@ -642,7 +640,7 @@ t_query_counter_async_inflight_batch(_) -> {ok, {ok, _}} = ?wait_async_action( emqx_resource:query(?ID, {inc_counter, 2}), - #{?snk_kind := resource_worker_enter_blocked}, + #{?snk_kind := resource_worker_flush_but_inflight_full}, 5_000 ), ?assertMatch(0, ets:info(Tab0, size)), @@ -658,17 +656,16 @@ t_query_counter_async_inflight_batch(_) -> ets:insert(Tab, {Ref, Result}), ?tp(tmp_query_inserted, #{}) end, + %% since this counts as a failure, it'll be enqueued and retried + %% later, when the resource is unblocked. {ok, {ok, _}} = ?wait_async_action( emqx_resource:query(?ID, {inc_counter, 3}, #{ async_reply_fun => {Insert, [Tab0, tmp_query]} }), - #{?snk_kind := tmp_query_inserted}, + #{?snk_kind := resource_worker_appended_to_queue}, 1_000 ), - %% since this counts as a failure, it'll be enqueued and retried - %% later, when the resource is unblocked. - ?assertMatch([{_, {error, {resource_error, #{reason := blocked}}}}], ets:take(Tab0, tmp_query)), tap_metrics(?LINE), %% all responses should be received after the resource is resumed. @@ -745,7 +742,7 @@ t_query_counter_async_inflight_batch(_) -> ), ?assertMatch(ok, emqx_resource:simple_sync_query(?ID, resume)), {ok, _} = snabbkaffe:receive_events(SRef1), - ?assertEqual(Sent, ets:info(Tab0, size)), + ?assertEqual(Sent, ets:info(Tab0, size), #{tab => ets:tab2list(Tab0)}), {ok, Counter} = emqx_resource:simple_sync_query(?ID, get_counter), ct:pal("get_counter: ~p, sent: ~p", [Counter, Sent]), From 5425f3d88e1efa0112fa791a80080480ea9edf9d Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Fri, 13 Jan 2023 17:33:57 -0300 Subject: [PATCH 0246/1802] refactor: rm unused fn --- apps/emqx_resource/src/emqx_resource_worker.erl | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/apps/emqx_resource/src/emqx_resource_worker.erl b/apps/emqx_resource/src/emqx_resource_worker.erl index eafe7cea6..d681c82c3 100644 --- a/apps/emqx_resource/src/emqx_resource_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_worker.erl @@ -761,16 +761,6 @@ batch_reply_after_query(Pid, Id, Index, InflightTID, Ref, Batch, Result) -> ok end. -ack_inflight_and_resume(Pid, InflightTID, Ref, Id, Index) -> - IsAcked = ack_inflight(InflightTID, Ref, Id, Index), - case is_inflight_full(InflightTID) of - true -> - ok; - false -> - ?MODULE:resume(Pid) - end, - IsAcked. - %%============================================================================== %% operations for queue queue_item_marshaller(Bin) when is_binary(Bin) -> From 731ac6567a7d7be8f714b9a3fcb23bc8fc3b2b0b Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Mon, 16 Jan 2023 11:50:00 -0300 Subject: [PATCH 0247/1802] fix(buffer_worker): don't retry all kinds of inflight requests Some requests should not be retried during the blocked state. For example, if some async requests are just taking some time to process, we should avoid retrying them periodically, lest risk overloading the downstream further. --- .../src/emqx_resource_worker.erl | 209 ++++++++++++++---- .../test/emqx_connector_demo.erl | 49 +++- .../test/emqx_resource_SUITE.erl | 192 ++++++++++++++++ 3 files changed, 399 insertions(+), 51 deletions(-) diff --git a/apps/emqx_resource/src/emqx_resource_worker.erl b/apps/emqx_resource/src/emqx_resource_worker.erl index d681c82c3..0cdd02f35 100644 --- a/apps/emqx_resource/src/emqx_resource_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_worker.erl @@ -23,6 +23,7 @@ -include("emqx_resource_utils.hrl"). -include("emqx_resource_errors.hrl"). -include_lib("emqx/include/logger.hrl"). +-include_lib("stdlib/include/ms_transform.hrl"). -include_lib("snabbkaffe/include/snabbkaffe.hrl"). -behaviour(gen_statem). @@ -65,6 +66,10 @@ ?REPLY(FROM, REQUEST, SENT, RESULT) || ?QUERY(FROM, REQUEST, SENT) <- BATCH ]). +-define(INFLIGHT_ITEM(Ref, BatchOrQuery, IsRetriable, WorkerPid), + {Ref, BatchOrQuery, IsRetriable, WorkerPid} +). +-define(RETRY_IDX, 3). -type id() :: binary(). -type index() :: pos_integer(). @@ -282,7 +287,7 @@ pick_cast(Id, Key, Query) -> resume_from_blocked(Data) -> #{inflight_tid := InflightTID} = Data, - case inflight_get_first(InflightTID) of + case inflight_get_first_retriable(InflightTID) of empty -> {next_state, running, Data}; {Ref, FirstQuery} -> @@ -298,6 +303,7 @@ retry_inflight_sync(Ref, QueryOrBatch, Data0) -> index := Index, resume_interval := ResumeT } = Data0, + ?tp(resource_worker_retry_inflight, #{query_or_batch => QueryOrBatch, ref => Ref}), QueryOpts = #{}, %% if we are retrying an inflight query, it has been sent HasBeenSent = true, @@ -306,7 +312,13 @@ retry_inflight_sync(Ref, QueryOrBatch, Data0) -> %% Send failed because resource is down {nack, PostFn} -> PostFn(), - ?tp(resource_worker_retry_inflight_failed, #{query_or_batch => QueryOrBatch}), + ?tp( + resource_worker_retry_inflight_failed, + #{ + ref => Ref, + query_or_batch => QueryOrBatch + } + ), {keep_state, Data0, {state_timeout, ResumeT, unblock}}; %% Send ok or failed but the resource is working {ack, PostFn} -> @@ -318,7 +330,13 @@ retry_inflight_sync(Ref, QueryOrBatch, Data0) -> %% requests (repeated and original) have the safe `Ref', %% we bump the counter when removing it from the table. IsAcked andalso PostFn(), - ?tp(resource_worker_retry_inflight_succeeded, #{query_or_batch => QueryOrBatch}), + ?tp( + resource_worker_retry_inflight_succeeded, + #{ + ref => Ref, + query_or_batch => QueryOrBatch + } + ), resume_from_blocked(Data0) end. @@ -431,17 +449,37 @@ do_flush( %% And only in that case. nack -> ok = replayq:ack(Q1, QAckRef), - ShouldPreserveInInflight = - is_inflight_full_result(Result) orelse + %% We might get a retriable response without having added + %% the request to the inflight table (e.g.: sync request, + %% but resource health check failed prior to calling and + %% so we didn't even call it). In that case, we must then + %% add it to the inflight table. + IsRetriable = + is_recoverable_error_result(Result) orelse is_not_connected_result(Result), - ShouldPreserveInInflight andalso inflight_append(InflightTID, Ref, Request, Id, Index), + ShouldPreserveInInflight = is_not_connected_result(Result), + WorkerPid = undefined, + InflightItem = ?INFLIGHT_ITEM(Ref, Request, IsRetriable, WorkerPid), + ShouldPreserveInInflight andalso + inflight_append(InflightTID, InflightItem, Id, Index), + IsRetriable andalso mark_inflight_as_retriable(InflightTID, Ref), emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), + ?tp( + resource_worker_flush_nack, + #{ + ref => Ref, + is_retriable => IsRetriable, + batch_or_query => Request, + result => Result + } + ), {next_state, blocked, Data0}; %% Success; just ack. ack -> ok = replayq:ack(Q1, QAckRef), is_async(Id) orelse ack_inflight(InflightTID, Ref, Id, Index), emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), + ?tp(resource_worker_flush_ack, #{batch_or_query => Request}), case queue_count(Q1) > 0 of true -> {keep_state, Data0, [{next_event, internal, flush}]}; @@ -471,17 +509,37 @@ do_flush(Data0, #{ %% And only in that case. nack -> ok = replayq:ack(Q1, QAckRef), - ShouldPreserveInInflight = - is_inflight_full_result(Result) orelse + %% We might get a retriable response without having added + %% the request to the inflight table (e.g.: sync request, + %% but resource health check failed prior to calling and + %% so we didn't even call it). In that case, we must then + %% add it to the inflight table. + IsRetriable = + is_recoverable_error_result(Result) orelse is_not_connected_result(Result), - ShouldPreserveInInflight andalso inflight_append(InflightTID, Ref, Batch, Id, Index), + ShouldPreserveInInflight = is_not_connected_result(Result), + WorkerPid = undefined, + InflightItem = ?INFLIGHT_ITEM(Ref, Batch, IsRetriable, WorkerPid), + ShouldPreserveInInflight andalso + inflight_append(InflightTID, InflightItem, Id, Index), + IsRetriable andalso mark_inflight_as_retriable(InflightTID, Ref), emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), + ?tp( + resource_worker_flush_nack, + #{ + ref => Ref, + is_retriable => IsRetriable, + batch_or_query => Batch, + result => Result + } + ), {next_state, blocked, Data0}; %% Success; just ack. ack -> ok = replayq:ack(Q1, QAckRef), is_async(Id) orelse ack_inflight(InflightTID, Ref, Id, Index), emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), + ?tp(resource_worker_flush_ack, #{batch_or_query => Batch}), CurrentCount = queue_count(Q1), case {CurrentCount > 0, CurrentCount >= BatchSize} of {false, _} -> @@ -603,11 +661,6 @@ handle_query_result_pure(Id, Result, HasBeenSent) -> end, {ack, PostFn}. -is_inflight_full_result({async_return, inflight_full}) -> - true; -is_inflight_full_result(_) -> - false. - is_not_connected_result(?RESOURCE_ERROR_M(Error, _)) when Error =:= not_connected; Error =:= blocked -> @@ -615,6 +668,11 @@ is_not_connected_result(?RESOURCE_ERROR_M(Error, _)) when is_not_connected_result(_) -> false. +is_recoverable_error_result({error, {recoverable_error, _Reason}}) -> + true; +is_recoverable_error_result(_) -> + false. + call_query(QM0, Id, Index, Ref, Query, QueryOpts) -> ?tp(call_query_enter, #{id => Id, query => Query}), case emqx_resource_manager:ets_lookup(Id) of @@ -653,7 +711,7 @@ call_query(QM0, Id, Index, Ref, Query, QueryOpts) -> ). apply_query_fun(sync, Mod, Id, Index, Ref, ?QUERY(_, Request, _) = Query, ResSt, QueryOpts) -> - ?tp(call_query, #{id => Id, mod => Mod, query => Query, res_st => ResSt}), + ?tp(call_query, #{id => Id, mod => Mod, query => Query, res_st => ResSt, call_mode => sync}), InflightTID = maps:get(inflight_name, QueryOpts, undefined), PerformInflightCapacityCheck = maps:get(perform_inflight_capacity_check, QueryOpts, true), ?APPLY_RESOURCE( @@ -664,13 +722,18 @@ apply_query_fun(sync, Mod, Id, Index, Ref, ?QUERY(_, Request, _) = Query, ResSt, %% when resuming. {async_return, inflight_full}; false -> - ok = inflight_append(InflightTID, Ref, Query, Id, Index), + IsRetriable = false, + WorkerPid = undefined, + InflightItem = ?INFLIGHT_ITEM(Ref, Query, IsRetriable, WorkerPid), + ok = inflight_append(InflightTID, InflightItem, Id, Index), Mod:on_query(Id, Request, ResSt) end, Request ); apply_query_fun(async, Mod, Id, Index, Ref, ?QUERY(_, Request, _) = Query, ResSt, QueryOpts) -> - ?tp(call_query_async, #{id => Id, mod => Mod, query => Query, res_st => ResSt}), + ?tp(call_query_async, #{ + id => Id, mod => Mod, query => Query, res_st => ResSt, call_mode => async + }), InflightTID = maps:get(inflight_name, QueryOpts, undefined), PerformInflightCapacityCheck = maps:get(perform_inflight_capacity_check, QueryOpts, true), ?APPLY_RESOURCE( @@ -681,14 +744,19 @@ apply_query_fun(async, Mod, Id, Index, Ref, ?QUERY(_, Request, _) = Query, ResSt false -> ReplyFun = fun ?MODULE:reply_after_query/7, Args = [self(), Id, Index, InflightTID, Ref, Query], - ok = inflight_append(InflightTID, Ref, Query, Id, Index), + IsRetriable = false, + WorkerPid = undefined, + InflightItem = ?INFLIGHT_ITEM(Ref, Query, IsRetriable, WorkerPid), + ok = inflight_append(InflightTID, InflightItem, Id, Index), Result = Mod:on_query_async(Id, Request, {ReplyFun, Args}, ResSt), {async_return, Result} end, Request ); apply_query_fun(sync, Mod, Id, Index, Ref, [?QUERY(_, _, _) | _] = Batch, ResSt, QueryOpts) -> - ?tp(call_batch_query, #{id => Id, mod => Mod, batch => Batch, res_st => ResSt}), + ?tp(call_batch_query, #{ + id => Id, mod => Mod, batch => Batch, res_st => ResSt, call_mode => sync + }), InflightTID = maps:get(inflight_name, QueryOpts, undefined), PerformInflightCapacityCheck = maps:get(perform_inflight_capacity_check, QueryOpts, true), Requests = [Request || ?QUERY(_From, Request, _) <- Batch], @@ -700,13 +768,18 @@ apply_query_fun(sync, Mod, Id, Index, Ref, [?QUERY(_, _, _) | _] = Batch, ResSt, %% when resuming. {async_return, inflight_full}; false -> - ok = inflight_append(InflightTID, Ref, Batch, Id, Index), + IsRetriable = false, + WorkerPid = undefined, + InflightItem = ?INFLIGHT_ITEM(Ref, Batch, IsRetriable, WorkerPid), + ok = inflight_append(InflightTID, InflightItem, Id, Index), Mod:on_batch_query(Id, Requests, ResSt) end, Batch ); apply_query_fun(async, Mod, Id, Index, Ref, [?QUERY(_, _, _) | _] = Batch, ResSt, QueryOpts) -> - ?tp(call_batch_query_async, #{id => Id, mod => Mod, batch => Batch, res_st => ResSt}), + ?tp(call_batch_query_async, #{ + id => Id, mod => Mod, batch => Batch, res_st => ResSt, call_mode => async + }), InflightTID = maps:get(inflight_name, QueryOpts, undefined), PerformInflightCapacityCheck = maps:get(perform_inflight_capacity_check, QueryOpts, true), ?APPLY_RESOURCE( @@ -718,7 +791,10 @@ apply_query_fun(async, Mod, Id, Index, Ref, [?QUERY(_, _, _) | _] = Batch, ResSt ReplyFun = fun ?MODULE:batch_reply_after_query/7, ReplyFunAndArgs = {ReplyFun, [self(), Id, Index, InflightTID, Ref, Batch]}, Requests = [Request || ?QUERY(_From, Request, _) <- Batch], - ok = inflight_append(InflightTID, Ref, Batch, Id, Index), + IsRetriable = false, + WorkerPid = undefined, + InflightItem = ?INFLIGHT_ITEM(Ref, Batch, IsRetriable, WorkerPid), + ok = inflight_append(InflightTID, InflightItem, Id, Index), Result = Mod:on_batch_query_async(Id, Requests, ReplyFunAndArgs, ResSt), {async_return, Result} end, @@ -738,8 +814,18 @@ reply_after_query(Pid, Id, Index, InflightTID, Ref, ?QUERY(From, Request, HasBee IsAcked andalso PostFn(), case Action of nack -> + ?tp(resource_worker_reply_after_query, #{ + action => nack, + batch_or_query => ?QUERY(From, Request, HasBeenSent), + result => Result + }), ?MODULE:block(Pid); ack -> + ?tp(resource_worker_reply_after_query, #{ + action => ack, + batch_or_query => ?QUERY(From, Request, HasBeenSent), + result => Result + }), ok end. @@ -756,8 +842,14 @@ batch_reply_after_query(Pid, Id, Index, InflightTID, Ref, Batch, Result) -> IsAcked andalso lists:foreach(fun(F) -> F() end, PostFns), case Action of nack -> + ?tp(resource_worker_reply_after_query, #{ + action => nack, batch_or_query => Batch, result => Result + }), ?MODULE:block(Pid); ack -> + ?tp(resource_worker_reply_after_query, #{ + action => ack, batch_or_query => Batch, result => Result + }), ok end. @@ -802,24 +894,28 @@ inflight_new(InfltWinSZ, Id, Index) -> emqx_resource_worker_inflight_tab, [ordered_set, public, {write_concurrency, true}] ), - inflight_append(TableId, ?MAX_SIZE_REF, {max_size, InfltWinSZ}, Id, Index), + inflight_append(TableId, {?MAX_SIZE_REF, {max_size, InfltWinSZ}}, Id, Index), %% we use this counter because we might deal with batches as %% elements. - inflight_append(TableId, ?SIZE_REF, 0, Id, Index), + inflight_append(TableId, {?SIZE_REF, 0}, Id, Index), TableId. -inflight_get_first(InflightTID) -> - case ets:next(InflightTID, ?MAX_SIZE_REF) of +-spec inflight_get_first_retriable(ets:tid()) -> + empty | {integer(), [?QUERY(_, _, _)] | ?QUERY(_, _, _)}. +inflight_get_first_retriable(InflightTID) -> + MatchSpec = + ets:fun2ms( + fun(?INFLIGHT_ITEM(Ref, BatchOrQuery, IsRetriable, _WorkerPid)) when + IsRetriable =:= true + -> + {Ref, BatchOrQuery} + end + ), + case ets:select(InflightTID, MatchSpec, _Limit = 1) of '$end_of_table' -> empty; - Ref -> - case ets:lookup(InflightTID, Ref) of - [Object] -> - Object; - [] -> - %% it might have been dropped - inflight_get_first(InflightTID) - end + {[{Ref, BatchOrQuery}], _Continuation} -> + {Ref, BatchOrQuery} end. is_inflight_full(undefined) -> @@ -844,37 +940,60 @@ inflight_num_msgs(InflightTID) -> [{_, Size}] = ets:lookup(InflightTID, ?SIZE_REF), Size. -inflight_append(undefined, _Ref, _Query, _Id, _Index) -> +inflight_append(undefined, _InflightItem, _Id, _Index) -> ok; -inflight_append(InflightTID, Ref, [?QUERY(_, _, _) | _] = Batch0, Id, Index) -> +inflight_append( + InflightTID, + ?INFLIGHT_ITEM(Ref, [?QUERY(_, _, _) | _] = Batch0, IsRetriable, WorkerPid), + Id, + Index +) -> Batch = mark_as_sent(Batch0), - IsNew = ets:insert_new(InflightTID, {Ref, Batch}), + InflightItem = ?INFLIGHT_ITEM(Ref, Batch, IsRetriable, WorkerPid), + IsNew = ets:insert_new(InflightTID, InflightItem), BatchSize = length(Batch), IsNew andalso ets:update_counter(InflightTID, ?SIZE_REF, {2, BatchSize}), emqx_resource_metrics:inflight_set(Id, Index, inflight_num_msgs(InflightTID)), - ?tp(resource_worker_appended_to_inflight, #{batch => Batch, is_new => IsNew}), + ?tp(resource_worker_appended_to_inflight, #{item => InflightItem, is_new => IsNew}), ok; -inflight_append(InflightTID, Ref, ?QUERY(_From, _Req, _HasBeenSent) = Query0, Id, Index) -> +inflight_append( + InflightTID, + ?INFLIGHT_ITEM(Ref, ?QUERY(_From, _Req, _HasBeenSent) = Query0, IsRetriable, WorkerPid), + Id, + Index +) -> Query = mark_as_sent(Query0), - IsNew = ets:insert_new(InflightTID, {Ref, Query}), + InflightItem = ?INFLIGHT_ITEM(Ref, Query, IsRetriable, WorkerPid), + IsNew = ets:insert_new(InflightTID, InflightItem), IsNew andalso ets:update_counter(InflightTID, ?SIZE_REF, {2, 1}), emqx_resource_metrics:inflight_set(Id, Index, inflight_num_msgs(InflightTID)), - ?tp(resource_worker_appended_to_inflight, #{query => Query, is_new => IsNew}), + ?tp(resource_worker_appended_to_inflight, #{item => InflightItem, is_new => IsNew}), ok; -inflight_append(InflightTID, Ref, Data, _Id, _Index) -> +inflight_append(InflightTID, {Ref, Data}, _Id, _Index) -> ets:insert(InflightTID, {Ref, Data}), %% this is a metadata row being inserted; therefore, we don't bump %% the inflight metric. ok. +%% a request was already appended and originally not retriable, but an +%% error occurred and it is now retriable. +mark_inflight_as_retriable(undefined, _Ref) -> + ok; +mark_inflight_as_retriable(InflightTID, Ref) -> + _ = ets:update_element(InflightTID, Ref, {?RETRY_IDX, true}), + ok. + ack_inflight(undefined, _Ref, _Id, _Index) -> false; ack_inflight(InflightTID, Ref, Id, Index) -> Count = case ets:take(InflightTID, Ref) of - [{Ref, ?QUERY(_, _, _)}] -> 1; - [{Ref, [?QUERY(_, _, _) | _] = Batch}] -> length(Batch); - _ -> 0 + [?INFLIGHT_ITEM(Ref, ?QUERY(_, _, _), _IsRetriable, _WorkerPid)] -> + 1; + [?INFLIGHT_ITEM(Ref, [?QUERY(_, _, _) | _] = Batch, _IsRetriable, _WorkerPid)] -> + length(Batch); + _ -> + 0 end, IsAcked = Count > 0, IsAcked andalso ets:update_counter(InflightTID, ?SIZE_REF, {2, -Count, 0, 0}), diff --git a/apps/emqx_resource/test/emqx_connector_demo.erl b/apps/emqx_resource/test/emqx_connector_demo.erl index 6c03e93cc..bbd2ba058 100644 --- a/apps/emqx_resource/test/emqx_connector_demo.erl +++ b/apps/emqx_resource/test/emqx_connector_demo.erl @@ -85,6 +85,9 @@ on_query(_InstId, get_state_failed, State) -> on_query(_InstId, block, #{pid := Pid}) -> Pid ! block, ok; +on_query(_InstId, block_now, #{pid := Pid}) -> + Pid ! block, + {error, {resource_error, #{reason => blocked, msg => blocked}}}; on_query(_InstId, resume, #{pid := Pid}) -> Pid ! resume, ok; @@ -138,7 +141,13 @@ on_query_async(_InstId, {inc_counter, N}, ReplyFun, #{pid := Pid}) -> ok; on_query_async(_InstId, get_counter, ReplyFun, #{pid := Pid}) -> Pid ! {get, ReplyFun}, - ok. + ok; +on_query_async(_InstId, block_now, ReplyFun, #{pid := Pid}) -> + Pid ! {block_now, ReplyFun}, + {ok, Pid}; +on_query_async(_InstId, {big_payload, Payload}, ReplyFun, #{pid := Pid}) -> + Pid ! {big_payload, Payload, ReplyFun}, + {ok, Pid}. on_batch_query(InstId, BatchReq, State) -> %% Requests can be either 'get_counter' or 'inc_counter', but @@ -147,17 +156,22 @@ on_batch_query(InstId, BatchReq, State) -> {inc_counter, _} -> batch_inc_counter(sync, InstId, BatchReq, State); get_counter -> - batch_get_counter(sync, InstId, State) + batch_get_counter(sync, InstId, State); + {big_payload, _Payload} -> + batch_big_payload(sync, InstId, BatchReq, State) end. on_batch_query_async(InstId, BatchReq, ReplyFunAndArgs, State) -> - %% Requests can be either 'get_counter' or 'inc_counter', but - %% cannot be mixed. + %% Requests can be of multiple types, but cannot be mixed. case hd(BatchReq) of {inc_counter, _} -> batch_inc_counter({async, ReplyFunAndArgs}, InstId, BatchReq, State); get_counter -> - batch_get_counter({async, ReplyFunAndArgs}, InstId, State) + batch_get_counter({async, ReplyFunAndArgs}, InstId, State); + block_now -> + on_query_async(InstId, block_now, ReplyFunAndArgs, State); + {big_payload, _Payload} -> + batch_big_payload({async, ReplyFunAndArgs}, InstId, BatchReq, State) end. batch_inc_counter(CallMode, InstId, BatchReq, State) -> @@ -184,6 +198,19 @@ batch_get_counter(sync, InstId, State) -> batch_get_counter({async, ReplyFunAndArgs}, InstId, State) -> on_query_async(InstId, get_counter, ReplyFunAndArgs, State). +batch_big_payload(sync, InstId, Batch, State) -> + [Res | _] = lists:map( + fun(Req = {big_payload, _}) -> on_query(InstId, Req, State) end, + Batch + ), + Res; +batch_big_payload({async, ReplyFunAndArgs}, InstId, Batch, State = #{pid := Pid}) -> + lists:foreach( + fun(Req = {big_payload, _}) -> on_query_async(InstId, Req, ReplyFunAndArgs, State) end, + Batch + ), + {ok, Pid}. + on_get_status(_InstId, #{health_check_error := true}) -> disconnected; on_get_status(_InstId, #{pid := Pid}) -> @@ -199,7 +226,11 @@ spawn_counter_process(Name, Register) -> Pid. counter_loop() -> - counter_loop(#{counter => 0, status => running, incorrect_status_count => 0}). + counter_loop(#{ + counter => 0, + status => running, + incorrect_status_count => 0 + }). counter_loop( #{ @@ -213,6 +244,12 @@ counter_loop( block -> ct:pal("counter recv: ~p", [block]), State#{status => blocked}; + {block_now, ReplyFun} -> + ct:pal("counter recv: ~p", [block_now]), + apply_reply( + ReplyFun, {error, {resource_error, #{reason => blocked, msg => blocked}}} + ), + State#{status => blocked}; resume -> {messages, Msgs} = erlang:process_info(self(), messages), ct:pal("counter recv: ~p, buffered msgs: ~p", [resume, length(Msgs)]), diff --git a/apps/emqx_resource/test/emqx_resource_SUITE.erl b/apps/emqx_resource/test/emqx_resource_SUITE.erl index d837cbc8c..2d6856acf 100644 --- a/apps/emqx_resource/test/emqx_resource_SUITE.erl +++ b/apps/emqx_resource/test/emqx_resource_SUITE.erl @@ -1254,6 +1254,174 @@ t_always_overflow(_Config) -> ), ok. +t_retry_sync_inflight(_Config) -> + ResumeInterval = 1_000, + emqx_connector_demo:set_callback_mode(always_sync), + {ok, _} = emqx_resource:create( + ?ID, + ?DEFAULT_RESOURCE_GROUP, + ?TEST_RESOURCE, + #{name => test_resource}, + #{ + query_mode => sync, + batch_size => 1, + worker_pool_size => 1, + resume_interval => ResumeInterval + } + ), + QueryOpts = #{}, + ?check_trace( + begin + %% now really make the resource go into `blocked' state. + %% this results in a retriable error when sync. + ok = emqx_resource:simple_sync_query(?ID, block), + {{error, {recoverable_error, incorrect_status}}, {ok, _}} = + ?wait_async_action( + emqx_resource:query(?ID, {big_payload, <<"a">>}, QueryOpts), + #{?snk_kind := resource_worker_retry_inflight_failed}, + ResumeInterval * 2 + ), + {ok, {ok, _}} = + ?wait_async_action( + ok = emqx_resource:simple_sync_query(?ID, resume), + #{?snk_kind := resource_worker_retry_inflight_succeeded}, + ResumeInterval * 3 + ), + ok + end, + [fun ?MODULE:assert_retry_fail_then_succeed_inflight/1] + ), + ok. + +t_retry_sync_inflight_batch(_Config) -> + ResumeInterval = 1_000, + emqx_connector_demo:set_callback_mode(always_sync), + {ok, _} = emqx_resource:create( + ?ID, + ?DEFAULT_RESOURCE_GROUP, + ?TEST_RESOURCE, + #{name => test_resource}, + #{ + query_mode => sync, + batch_size => 2, + batch_time => 200, + worker_pool_size => 1, + resume_interval => ResumeInterval + } + ), + QueryOpts = #{}, + ?check_trace( + begin + %% now really make the resource go into `blocked' state. + %% this results in a retriable error when sync. + ok = emqx_resource:simple_sync_query(?ID, block), + {{error, {recoverable_error, incorrect_status}}, {ok, _}} = + ?wait_async_action( + emqx_resource:query(?ID, {big_payload, <<"a">>}, QueryOpts), + #{?snk_kind := resource_worker_retry_inflight_failed}, + ResumeInterval * 2 + ), + {ok, {ok, _}} = + ?wait_async_action( + ok = emqx_resource:simple_sync_query(?ID, resume), + #{?snk_kind := resource_worker_retry_inflight_succeeded}, + ResumeInterval * 3 + ), + ok + end, + [fun ?MODULE:assert_retry_fail_then_succeed_inflight/1] + ), + ok. + +t_dont_retry_async_inflight(_Config) -> + ResumeInterval = 1_000, + emqx_connector_demo:set_callback_mode(async_if_possible), + {ok, _} = emqx_resource:create( + ?ID, + ?DEFAULT_RESOURCE_GROUP, + ?TEST_RESOURCE, + #{name => test_resource}, + #{ + query_mode => async, + batch_size => 1, + worker_pool_size => 1, + resume_interval => ResumeInterval + } + ), + QueryOpts = #{}, + ?check_trace( + begin + %% block, + {ok, {ok, _}} = + ?wait_async_action( + emqx_resource:query(?ID, block_now), + #{?snk_kind := resource_worker_enter_blocked}, + ResumeInterval * 2 + ), + + %% then send an async request; that shouldn't be retriable. + {ok, {ok, _}} = + ?wait_async_action( + emqx_resource:query(?ID, {big_payload, <<"b">>}, QueryOpts), + #{?snk_kind := resource_worker_flush_ack}, + ResumeInterval * 2 + ), + + %% will re-enter running because the single request is not retriable + {ok, _} = ?block_until( + #{?snk_kind := resource_worker_enter_running}, ResumeInterval * 2 + ), + ok + end, + [fun ?MODULE:assert_no_retry_inflight/1] + ), + ok. + +t_dont_retry_async_inflight_batch(_Config) -> + ResumeInterval = 1_000, + emqx_connector_demo:set_callback_mode(async_if_possible), + {ok, _} = emqx_resource:create( + ?ID, + ?DEFAULT_RESOURCE_GROUP, + ?TEST_RESOURCE, + #{name => test_resource}, + #{ + query_mode => async, + batch_size => 2, + batch_time => 200, + worker_pool_size => 1, + resume_interval => ResumeInterval + } + ), + QueryOpts = #{}, + ?check_trace( + begin + %% block, + {ok, {ok, _}} = + ?wait_async_action( + emqx_resource:query(?ID, block_now), + #{?snk_kind := resource_worker_enter_blocked}, + ResumeInterval * 2 + ), + + %% then send an async request; that shouldn't be retriable. + {ok, {ok, _}} = + ?wait_async_action( + emqx_resource:query(?ID, {big_payload, <<"b">>}, QueryOpts), + #{?snk_kind := resource_worker_flush_ack}, + ResumeInterval * 2 + ), + + %% will re-enter running because the single request is not retriable + {ok, _} = ?block_until( + #{?snk_kind := resource_worker_enter_running}, ResumeInterval * 2 + ), + ok + end, + [fun ?MODULE:assert_no_retry_inflight/1] + ), + ok. + %%------------------------------------------------------------------------------ %% Helpers %%------------------------------------------------------------------------------ @@ -1317,3 +1485,27 @@ tap_metrics(Line) -> {ok, _, #{metrics := #{counters := C, gauges := G}}} = emqx_resource:get_instance(?ID), ct:pal("metrics (l. ~b): ~p", [Line, #{counters => C, gauges => G}]), #{counters => C, gauges => G}. + +assert_no_retry_inflight(Trace) -> + ?assertEqual([], ?of_kind(resource_worker_retry_inflight_failed, Trace)), + ?assertEqual([], ?of_kind(resource_worker_retry_inflight_succeeded, Trace)), + ok. + +assert_retry_fail_then_succeed_inflight(Trace) -> + ?assert( + ?strict_causality( + #{?snk_kind := resource_worker_flush_nack, ref := _Ref}, + #{?snk_kind := resource_worker_retry_inflight_failed, ref := _Ref}, + Trace + ) + ), + %% not strict causality because it might retry more than once + %% before restoring the resource health. + ?assert( + ?causality( + #{?snk_kind := resource_worker_retry_inflight_failed, ref := _Ref}, + #{?snk_kind := resource_worker_retry_inflight_succeeded, ref := _Ref}, + Trace + ) + ), + ok. From 006b4bda97dc2bfe563df071be21befd5358079f Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Mon, 16 Jan 2023 15:16:41 -0300 Subject: [PATCH 0248/1802] feat(buffer_worker): monitor async workers and cancel their inflight requests upon death --- .../src/emqx_connector_http.erl | 3 +- .../src/emqx_connector_mqtt.erl | 11 +- .../src/emqx_resource_worker.erl | 184 ++++++++++++++---- .../test/emqx_connector_demo.erl | 4 +- .../test/emqx_resource_SUITE.erl | 80 ++++++++ lib-ee/emqx_ee_connector/rebar.config | 2 +- .../src/emqx_ee_connector_gcp_pubsub.erl | 9 +- .../src/emqx_ee_connector_influxdb.erl | 2 +- mix.exs | 2 +- 9 files changed, 243 insertions(+), 54 deletions(-) diff --git a/apps/emqx_connector/src/emqx_connector_http.erl b/apps/emqx_connector/src/emqx_connector_http.erl index d68f624e4..7f84c665a 100644 --- a/apps/emqx_connector/src/emqx_connector_http.erl +++ b/apps/emqx_connector/src/emqx_connector_http.erl @@ -380,7 +380,8 @@ on_query_async( NRequest, Timeout, {fun ?MODULE:reply_delegator/2, [ReplyFunAndArgs]} - ). + ), + {ok, Worker}. on_get_status(_InstId, #{pool_name := PoolName, connect_timeout := Timeout} = State) -> case do_get_status(PoolName, Timeout) of diff --git a/apps/emqx_connector/src/emqx_connector_mqtt.erl b/apps/emqx_connector/src/emqx_connector_mqtt.erl index 585122539..71ed81dda 100644 --- a/apps/emqx_connector/src/emqx_connector_mqtt.erl +++ b/apps/emqx_connector/src/emqx_connector_mqtt.erl @@ -198,7 +198,10 @@ on_query_async( #{name := InstanceId} ) -> ?TRACE("QUERY", "async_send_msg_to_remote_node", #{message => Msg, connector => InstanceId}), - emqx_connector_mqtt_worker:send_to_remote_async(InstanceId, Msg, {ReplyFun, Args}). + %% this is a cast, currently. + ok = emqx_connector_mqtt_worker:send_to_remote_async(InstanceId, Msg, {ReplyFun, Args}), + WorkerPid = get_worker_pid(InstanceId), + {ok, WorkerPid}. on_get_status(_InstId, #{name := InstanceId}) -> case emqx_connector_mqtt_worker:status(InstanceId) of @@ -212,6 +215,12 @@ ensure_mqtt_worker_started(InstanceId, BridgeConf) -> {error, Reason} -> {error, Reason} end. +%% mqtt workers, when created and called via bridge callbacks, are +%% registered. +-spec get_worker_pid(atom()) -> pid(). +get_worker_pid(InstanceId) -> + whereis(InstanceId). + make_sub_confs(EmptyMap, _Conf, _) when map_size(EmptyMap) == 0 -> undefined; make_sub_confs(undefined, _Conf, _) -> diff --git a/apps/emqx_resource/src/emqx_resource_worker.erl b/apps/emqx_resource/src/emqx_resource_worker.erl index 0cdd02f35..4e0ae9352 100644 --- a/apps/emqx_resource/src/emqx_resource_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_worker.erl @@ -66,10 +66,11 @@ ?REPLY(FROM, REQUEST, SENT, RESULT) || ?QUERY(FROM, REQUEST, SENT) <- BATCH ]). --define(INFLIGHT_ITEM(Ref, BatchOrQuery, IsRetriable, WorkerPid), - {Ref, BatchOrQuery, IsRetriable, WorkerPid} +-define(INFLIGHT_ITEM(Ref, BatchOrQuery, IsRetriable, WorkerMRef), + {Ref, BatchOrQuery, IsRetriable, WorkerMRef} ). -define(RETRY_IDX, 3). +-define(WORKER_MREF_IDX, 4). -type id() :: binary(). -type index() :: pos_integer(). @@ -79,14 +80,15 @@ -type request_from() :: undefined | gen_statem:from(). -type state() :: blocked | running. -type data() :: #{ - id => id(), - index => index(), - inflight_tid => ets:tid(), - batch_size => pos_integer(), - batch_time => timer:time(), - queue => replayq:q(), - resume_interval => timer:time(), - tref => undefined | timer:tref() + id := id(), + index := index(), + inflight_tid := ets:tid(), + async_workers := #{pid() => reference()}, + batch_size := pos_integer(), + batch_time := timer:time(), + queue := replayq:q(), + resume_interval := timer:time(), + tref := undefined | timer:tref() }. callback_mode() -> [state_functions, state_enter]. @@ -172,21 +174,22 @@ init({Id, Index, Opts}) -> Queue = replayq:open(QueueOpts), emqx_resource_metrics:queuing_set(Id, Index, queue_count(Queue)), emqx_resource_metrics:inflight_set(Id, Index, 0), - InfltWinSZ = maps:get(async_inflight_window, Opts, ?DEFAULT_INFLIGHT), - InflightTID = inflight_new(InfltWinSZ, Id, Index), - HCItvl = maps:get(health_check_interval, Opts, ?HEALTHCHECK_INTERVAL), - St = #{ + InflightWinSize = maps:get(async_inflight_window, Opts, ?DEFAULT_INFLIGHT), + InflightTID = inflight_new(InflightWinSize, Id, Index), + HealthCheckInterval = maps:get(health_check_interval, Opts, ?HEALTHCHECK_INTERVAL), + Data = #{ id => Id, index => Index, inflight_tid => InflightTID, + async_workers => #{}, batch_size => BatchSize, batch_time => maps:get(batch_time, Opts, ?DEFAULT_BATCH_TIME), queue => Queue, - resume_interval => maps:get(resume_interval, Opts, HCItvl), + resume_interval => maps:get(resume_interval, Opts, HealthCheckInterval), tref => undefined }, ?tp(resource_worker_init, #{id => Id, index => Index}), - {ok, blocked, St, {next_event, cast, resume}}. + {ok, blocked, Data, {next_event, cast, resume}}. running(enter, _, St) -> ?tp(resource_worker_enter_running, #{}), @@ -203,6 +206,11 @@ running(internal, flush, St) -> flush(St); running(info, {flush, _Ref}, _St) -> keep_state_and_data; +running(info, {'DOWN', _MRef, process, Pid, Reason}, Data0 = #{async_workers := AsyncWorkers0}) when + is_map_key(Pid, AsyncWorkers0) +-> + ?SLOG(info, #{msg => async_worker_died, state => running, reason => Reason}), + handle_async_worker_down(Data0, Pid); running(info, Info, _St) -> ?SLOG(error, #{msg => unexpected_msg, state => running, info => Info}), keep_state_and_data. @@ -224,6 +232,11 @@ blocked(info, ?SEND_REQ(_ReqFrom, {query, _Request, _Opts}) = Request0, Data0) - {keep_state, Data}; blocked(info, {flush, _Ref}, _Data) -> keep_state_and_data; +blocked(info, {'DOWN', _MRef, process, Pid, Reason}, Data0 = #{async_workers := AsyncWorkers0}) when + is_map_key(Pid, AsyncWorkers0) +-> + ?SLOG(info, #{msg => async_worker_died, state => blocked, reason => Reason}), + handle_async_worker_down(Data0, Pid); blocked(info, Info, _Data) -> ?SLOG(error, #{msg => unexpected_msg, state => blocked, info => Info}), keep_state_and_data. @@ -458,11 +471,15 @@ do_flush( is_recoverable_error_result(Result) orelse is_not_connected_result(Result), ShouldPreserveInInflight = is_not_connected_result(Result), - WorkerPid = undefined, - InflightItem = ?INFLIGHT_ITEM(Ref, Request, IsRetriable, WorkerPid), + %% we set it atomically just below; a limitation of having + %% to use tuples for atomic ets updates + WorkerMRef0 = undefined, + InflightItem = ?INFLIGHT_ITEM(Ref, Request, IsRetriable, WorkerMRef0), ShouldPreserveInInflight andalso inflight_append(InflightTID, InflightItem, Id, Index), IsRetriable andalso mark_inflight_as_retriable(InflightTID, Ref), + {Data1, WorkerMRef} = ensure_async_worker_monitored(Data0, Result), + store_async_worker_reference(InflightTID, Ref, WorkerMRef), emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), ?tp( resource_worker_flush_nack, @@ -473,18 +490,20 @@ do_flush( result => Result } ), - {next_state, blocked, Data0}; + {next_state, blocked, Data1}; %% Success; just ack. ack -> ok = replayq:ack(Q1, QAckRef), is_async(Id) orelse ack_inflight(InflightTID, Ref, Id, Index), + {Data1, WorkerMRef} = ensure_async_worker_monitored(Data0, Result), + store_async_worker_reference(InflightTID, Ref, WorkerMRef), emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), ?tp(resource_worker_flush_ack, #{batch_or_query => Request}), case queue_count(Q1) > 0 of true -> - {keep_state, Data0, [{next_event, internal, flush}]}; + {keep_state, Data1, [{next_event, internal, flush}]}; false -> - {keep_state, Data0} + {keep_state, Data1} end end; do_flush(Data0, #{ @@ -518,11 +537,15 @@ do_flush(Data0, #{ is_recoverable_error_result(Result) orelse is_not_connected_result(Result), ShouldPreserveInInflight = is_not_connected_result(Result), - WorkerPid = undefined, - InflightItem = ?INFLIGHT_ITEM(Ref, Batch, IsRetriable, WorkerPid), + %% we set it atomically just below; a limitation of having + %% to use tuples for atomic ets updates + WorkerMRef0 = undefined, + InflightItem = ?INFLIGHT_ITEM(Ref, Batch, IsRetriable, WorkerMRef0), ShouldPreserveInInflight andalso inflight_append(InflightTID, InflightItem, Id, Index), IsRetriable andalso mark_inflight_as_retriable(InflightTID, Ref), + {Data1, WorkerMRef} = ensure_async_worker_monitored(Data0, Result), + store_async_worker_reference(InflightTID, Ref, WorkerMRef), emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), ?tp( resource_worker_flush_nack, @@ -533,22 +556,24 @@ do_flush(Data0, #{ result => Result } ), - {next_state, blocked, Data0}; + {next_state, blocked, Data1}; %% Success; just ack. ack -> ok = replayq:ack(Q1, QAckRef), is_async(Id) orelse ack_inflight(InflightTID, Ref, Id, Index), + {Data1, WorkerMRef} = ensure_async_worker_monitored(Data0, Result), + store_async_worker_reference(InflightTID, Ref, WorkerMRef), emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), ?tp(resource_worker_flush_ack, #{batch_or_query => Batch}), CurrentCount = queue_count(Q1), case {CurrentCount > 0, CurrentCount >= BatchSize} of {false, _} -> - {keep_state, Data0}; + {keep_state, Data1}; {true, true} -> - {keep_state, Data0, [{next_event, internal, flush}]}; + {keep_state, Data1, [{next_event, internal, flush}]}; {true, false} -> - Data1 = ensure_flush_timer(Data0), - {keep_state, Data1} + Data2 = ensure_flush_timer(Data1), + {keep_state, Data2} end end. @@ -653,6 +678,8 @@ handle_query_result_pure(Id, {async_return, {error, Msg}}, HasBeenSent) -> {ack, PostFn}; handle_query_result_pure(_Id, {async_return, ok}, _HasBeenSent) -> {ack, fun() -> ok end}; +handle_query_result_pure(_Id, {async_return, {ok, Pid}}, _HasBeenSent) when is_pid(Pid) -> + {ack, fun() -> ok end}; handle_query_result_pure(Id, Result, HasBeenSent) -> PostFn = fun() -> assert_ok_result(Result), @@ -661,6 +688,13 @@ handle_query_result_pure(Id, Result, HasBeenSent) -> end, {ack, PostFn}. +handle_async_worker_down(Data0, Pid) -> + #{async_workers := AsyncWorkers0} = Data0, + {WorkerMRef, AsyncWorkers} = maps:take(Pid, AsyncWorkers0), + Data = Data0#{async_workers := AsyncWorkers}, + cancel_inflight_items(Data, WorkerMRef), + {keep_state, Data}. + is_not_connected_result(?RESOURCE_ERROR_M(Error, _)) when Error =:= not_connected; Error =:= blocked -> @@ -723,8 +757,8 @@ apply_query_fun(sync, Mod, Id, Index, Ref, ?QUERY(_, Request, _) = Query, ResSt, {async_return, inflight_full}; false -> IsRetriable = false, - WorkerPid = undefined, - InflightItem = ?INFLIGHT_ITEM(Ref, Query, IsRetriable, WorkerPid), + WorkerMRef = undefined, + InflightItem = ?INFLIGHT_ITEM(Ref, Query, IsRetriable, WorkerMRef), ok = inflight_append(InflightTID, InflightItem, Id, Index), Mod:on_query(Id, Request, ResSt) end, @@ -745,8 +779,8 @@ apply_query_fun(async, Mod, Id, Index, Ref, ?QUERY(_, Request, _) = Query, ResSt ReplyFun = fun ?MODULE:reply_after_query/7, Args = [self(), Id, Index, InflightTID, Ref, Query], IsRetriable = false, - WorkerPid = undefined, - InflightItem = ?INFLIGHT_ITEM(Ref, Query, IsRetriable, WorkerPid), + WorkerMRef = undefined, + InflightItem = ?INFLIGHT_ITEM(Ref, Query, IsRetriable, WorkerMRef), ok = inflight_append(InflightTID, InflightItem, Id, Index), Result = Mod:on_query_async(Id, Request, {ReplyFun, Args}, ResSt), {async_return, Result} @@ -769,8 +803,8 @@ apply_query_fun(sync, Mod, Id, Index, Ref, [?QUERY(_, _, _) | _] = Batch, ResSt, {async_return, inflight_full}; false -> IsRetriable = false, - WorkerPid = undefined, - InflightItem = ?INFLIGHT_ITEM(Ref, Batch, IsRetriable, WorkerPid), + WorkerMRef = undefined, + InflightItem = ?INFLIGHT_ITEM(Ref, Batch, IsRetriable, WorkerMRef), ok = inflight_append(InflightTID, InflightItem, Id, Index), Mod:on_batch_query(Id, Requests, ResSt) end, @@ -792,8 +826,8 @@ apply_query_fun(async, Mod, Id, Index, Ref, [?QUERY(_, _, _) | _] = Batch, ResSt ReplyFunAndArgs = {ReplyFun, [self(), Id, Index, InflightTID, Ref, Batch]}, Requests = [Request || ?QUERY(_From, Request, _) <- Batch], IsRetriable = false, - WorkerPid = undefined, - InflightItem = ?INFLIGHT_ITEM(Ref, Batch, IsRetriable, WorkerPid), + WorkerMRef = undefined, + InflightItem = ?INFLIGHT_ITEM(Ref, Batch, IsRetriable, WorkerMRef), ok = inflight_append(InflightTID, InflightItem, Id, Index), Result = Mod:on_batch_query_async(Id, Requests, ReplyFunAndArgs, ResSt), {async_return, Result} @@ -905,7 +939,7 @@ inflight_new(InfltWinSZ, Id, Index) -> inflight_get_first_retriable(InflightTID) -> MatchSpec = ets:fun2ms( - fun(?INFLIGHT_ITEM(Ref, BatchOrQuery, IsRetriable, _WorkerPid)) when + fun(?INFLIGHT_ITEM(Ref, BatchOrQuery, IsRetriable, _WorkerMRef)) when IsRetriable =:= true -> {Ref, BatchOrQuery} @@ -944,12 +978,12 @@ inflight_append(undefined, _InflightItem, _Id, _Index) -> ok; inflight_append( InflightTID, - ?INFLIGHT_ITEM(Ref, [?QUERY(_, _, _) | _] = Batch0, IsRetriable, WorkerPid), + ?INFLIGHT_ITEM(Ref, [?QUERY(_, _, _) | _] = Batch0, IsRetriable, WorkerMRef), Id, Index ) -> Batch = mark_as_sent(Batch0), - InflightItem = ?INFLIGHT_ITEM(Ref, Batch, IsRetriable, WorkerPid), + InflightItem = ?INFLIGHT_ITEM(Ref, Batch, IsRetriable, WorkerMRef), IsNew = ets:insert_new(InflightTID, InflightItem), BatchSize = length(Batch), IsNew andalso ets:update_counter(InflightTID, ?SIZE_REF, {2, BatchSize}), @@ -958,12 +992,12 @@ inflight_append( ok; inflight_append( InflightTID, - ?INFLIGHT_ITEM(Ref, ?QUERY(_From, _Req, _HasBeenSent) = Query0, IsRetriable, WorkerPid), + ?INFLIGHT_ITEM(Ref, ?QUERY(_From, _Req, _HasBeenSent) = Query0, IsRetriable, WorkerMRef), Id, Index ) -> Query = mark_as_sent(Query0), - InflightItem = ?INFLIGHT_ITEM(Ref, Query, IsRetriable, WorkerPid), + InflightItem = ?INFLIGHT_ITEM(Ref, Query, IsRetriable, WorkerMRef), IsNew = ets:insert_new(InflightTID, InflightItem), IsNew andalso ets:update_counter(InflightTID, ?SIZE_REF, {2, 1}), emqx_resource_metrics:inflight_set(Id, Index, inflight_num_msgs(InflightTID)), @@ -983,14 +1017,46 @@ mark_inflight_as_retriable(InflightTID, Ref) -> _ = ets:update_element(InflightTID, Ref, {?RETRY_IDX, true}), ok. +%% Track each worker pid only once. +ensure_async_worker_monitored( + Data0 = #{async_workers := AsyncWorkers}, {async_return, {ok, WorkerPid}} = _Result +) when + is_pid(WorkerPid), is_map_key(WorkerPid, AsyncWorkers) +-> + WorkerMRef = maps:get(WorkerPid, AsyncWorkers), + {Data0, WorkerMRef}; +ensure_async_worker_monitored( + Data0 = #{async_workers := AsyncWorkers0}, {async_return, {ok, WorkerPid}} +) when + is_pid(WorkerPid) +-> + WorkerMRef = monitor(process, WorkerPid), + AsyncWorkers = AsyncWorkers0#{WorkerPid => WorkerMRef}, + Data = Data0#{async_workers := AsyncWorkers}, + {Data, WorkerMRef}; +ensure_async_worker_monitored(Data0, _Result) -> + {Data0, undefined}. + +store_async_worker_reference(undefined = _InflightTID, _Ref, _WorkerMRef) -> + ok; +store_async_worker_reference(_InflightTID, _Ref, undefined = _WorkerRef) -> + ok; +store_async_worker_reference(InflightTID, Ref, WorkerMRef) when + is_reference(WorkerMRef) +-> + _ = ets:update_element( + InflightTID, Ref, {?WORKER_MREF_IDX, WorkerMRef} + ), + ok. + ack_inflight(undefined, _Ref, _Id, _Index) -> false; ack_inflight(InflightTID, Ref, Id, Index) -> Count = case ets:take(InflightTID, Ref) of - [?INFLIGHT_ITEM(Ref, ?QUERY(_, _, _), _IsRetriable, _WorkerPid)] -> + [?INFLIGHT_ITEM(Ref, ?QUERY(_, _, _), _IsRetriable, _WorkerMRef)] -> 1; - [?INFLIGHT_ITEM(Ref, [?QUERY(_, _, _) | _] = Batch, _IsRetriable, _WorkerPid)] -> + [?INFLIGHT_ITEM(Ref, [?QUERY(_, _, _) | _] = Batch, _IsRetriable, _WorkerMRef)] -> length(Batch); _ -> 0 @@ -1000,6 +1066,38 @@ ack_inflight(InflightTID, Ref, Id, Index) -> emqx_resource_metrics:inflight_set(Id, Index, inflight_num_msgs(InflightTID)), IsAcked. +cancel_inflight_items(Data, WorkerMRef) -> + #{inflight_tid := InflightTID} = Data, + MatchSpec = + ets:fun2ms( + fun(?INFLIGHT_ITEM(Ref, _BatchOrQuery, _IsRetriable, WorkerMRef0)) when + WorkerMRef =:= WorkerMRef0 + -> + Ref + end + ), + Refs = ets:select(InflightTID, MatchSpec), + lists:foreach(fun(Ref) -> do_cancel_inflight_item(Data, Ref) end, Refs). + +do_cancel_inflight_item(Data, Ref) -> + #{id := Id, index := Index, inflight_tid := InflightTID} = Data, + {Count, Batch} = + case ets:take(InflightTID, Ref) of + [?INFLIGHT_ITEM(Ref, ?QUERY(_, _, _) = Query, _IsRetriable, _WorkerMRef)] -> + {1, [Query]}; + [?INFLIGHT_ITEM(Ref, [?QUERY(_, _, _) | _] = Batch0, _IsRetriable, _WorkerMRef)] -> + {length(Batch0), Batch0}; + _ -> + {0, []} + end, + IsAcked = Count > 0, + IsAcked andalso ets:update_counter(InflightTID, ?SIZE_REF, {2, -Count, 0, 0}), + emqx_resource_metrics:inflight_set(Id, Index, inflight_num_msgs(InflightTID)), + Result = {error, interrupted}, + _ = batch_reply_caller(Id, Result, Batch), + ?tp(resource_worker_cancelled_inflight, #{ref => Ref}), + ok. + %%============================================================================== inc_sent_failed(Id, _HasBeenSent = true) -> diff --git a/apps/emqx_resource/test/emqx_connector_demo.erl b/apps/emqx_resource/test/emqx_connector_demo.erl index bbd2ba058..15d4a3b46 100644 --- a/apps/emqx_resource/test/emqx_connector_demo.erl +++ b/apps/emqx_resource/test/emqx_connector_demo.erl @@ -138,10 +138,10 @@ on_query(_InstId, get_counter, #{pid := Pid}) -> on_query_async(_InstId, {inc_counter, N}, ReplyFun, #{pid := Pid}) -> Pid ! {inc, N, ReplyFun}, - ok; + {ok, Pid}; on_query_async(_InstId, get_counter, ReplyFun, #{pid := Pid}) -> Pid ! {get, ReplyFun}, - ok; + {ok, Pid}; on_query_async(_InstId, block_now, ReplyFun, #{pid := Pid}) -> Pid ! {block_now, ReplyFun}, {ok, Pid}; diff --git a/apps/emqx_resource/test/emqx_resource_SUITE.erl b/apps/emqx_resource/test/emqx_resource_SUITE.erl index 2d6856acf..4ffb259e8 100644 --- a/apps/emqx_resource/test/emqx_resource_SUITE.erl +++ b/apps/emqx_resource/test/emqx_resource_SUITE.erl @@ -1422,6 +1422,86 @@ t_dont_retry_async_inflight_batch(_Config) -> ), ok. +%% check that we monitor async worker pids and abort their inflight +%% requests if they die. +t_async_pool_worker_death(_Config) -> + ResumeInterval = 1_000, + emqx_connector_demo:set_callback_mode(async_if_possible), + {ok, _} = emqx_resource:create( + ?ID, + ?DEFAULT_RESOURCE_GROUP, + ?TEST_RESOURCE, + #{name => test_resource}, + #{ + query_mode => async, + batch_size => 1, + worker_pool_size => 2, + resume_interval => ResumeInterval + } + ), + Tab0 = ets:new(?FUNCTION_NAME, [bag, public]), + Insert0 = fun(Tab, Ref, Result) -> + ct:pal("inserting ~p", [{Ref, Result}]), + ets:insert(Tab, {Ref, Result}) + end, + ReqOpts = fun() -> #{async_reply_fun => {Insert0, [Tab0, make_ref()]}} end, + ?check_trace( + begin + ok = emqx_resource:simple_sync_query(?ID, block), + + NumReqs = 10, + {ok, SRef0} = + snabbkaffe:subscribe( + ?match_event(#{?snk_kind := resource_worker_appended_to_inflight}), + NumReqs, + 1_000 + ), + inc_counter_in_parallel_increasing(NumReqs, 1, ReqOpts), + {ok, _} = snabbkaffe:receive_events(SRef0), + + Inflight0 = emqx_resource_metrics:inflight_get(?ID), + ?assertEqual(NumReqs, Inflight0), + + %% grab one of the worker pids and kill it + {ok, SRef1} = + snabbkaffe:subscribe( + ?match_event(#{?snk_kind := resource_worker_cancelled_inflight}), + NumReqs, + 1_000 + ), + {ok, #{pid := Pid0}} = emqx_resource:simple_sync_query(?ID, get_state), + MRef = monitor(process, Pid0), + ct:pal("will kill ~p", [Pid0]), + exit(Pid0, kill), + receive + {'DOWN', MRef, process, Pid0, killed} -> + ct:pal("~p killed", [Pid0]), + ok + after 200 -> + ct:fail("worker should have died") + end, + + %% inflight requests should have been cancelled + {ok, _} = snabbkaffe:receive_events(SRef1), + Inflight1 = emqx_resource_metrics:inflight_get(?ID), + ?assertEqual(0, Inflight1), + + ?assert( + lists:all( + fun + ({_, {error, interrupted}}) -> true; + (_) -> false + end, + ets:tab2list(Tab0) + ), + #{tab => ets:tab2list(Tab0)} + ), + ok + end, + [] + ), + ok. + %%------------------------------------------------------------------------------ %% Helpers %%------------------------------------------------------------------------------ diff --git a/lib-ee/emqx_ee_connector/rebar.config b/lib-ee/emqx_ee_connector/rebar.config index 262641d44..3af1868c7 100644 --- a/lib-ee/emqx_ee_connector/rebar.config +++ b/lib-ee/emqx_ee_connector/rebar.config @@ -1,7 +1,7 @@ {erl_opts, [debug_info]}. {deps, [ {hstreamdb_erl, {git, "https://github.com/hstreamdb/hstreamdb_erl.git", {tag, "0.2.5"}}}, - {influxdb, {git, "https://github.com/emqx/influxdb-client-erl", {tag, "1.1.6"}}}, + {influxdb, {git, "https://github.com/emqx/influxdb-client-erl", {tag, "1.1.7"}}}, {emqx, {path, "../../apps/emqx"}} ]}. diff --git a/lib-ee/emqx_ee_connector/src/emqx_ee_connector_gcp_pubsub.erl b/lib-ee/emqx_ee_connector/src/emqx_ee_connector_gcp_pubsub.erl index 37d193edf..041bdec08 100644 --- a/lib-ee/emqx_ee_connector/src/emqx_ee_connector_gcp_pubsub.erl +++ b/lib-ee/emqx_ee_connector/src/emqx_ee_connector_gcp_pubsub.erl @@ -178,7 +178,7 @@ on_query(BridgeId, {send_message, Selected}, State) -> {send_message, map()}, {ReplyFun :: function(), Args :: list()}, state() -) -> ok. +) -> {ok, pid()}. on_query_async(BridgeId, {send_message, Selected}, ReplyFunAndArgs, State) -> Requests = [{send_message, Selected}], ?TRACE( @@ -210,7 +210,7 @@ on_batch_query(BridgeId, Requests, State) -> [{send_message, map()}], {ReplyFun :: function(), Args :: list()}, state() -) -> ok. +) -> {ok, pid()}. on_batch_query_async(BridgeId, Requests, ReplyFunAndArgs, State) -> ?TRACE( "QUERY_ASYNC", @@ -496,7 +496,7 @@ do_send_requests_sync(State, Requests, ResourceId) -> [{send_message, map()}], {ReplyFun :: function(), Args :: list()}, resource_id() -) -> ok. +) -> {ok, pid()}. do_send_requests_async(State, Requests, ReplyFunAndArgs, ResourceId) -> #{ pool_name := PoolName, @@ -531,7 +531,8 @@ do_send_requests_async(State, Requests, ReplyFunAndArgs, ResourceId) -> Request, RequestTimeout, {fun ?MODULE:reply_delegator/3, [ResourceId, ReplyFunAndArgs]} - ). + ), + {ok, Worker}. -spec reply_delegator( resource_id(), diff --git a/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl b/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl index 553e5369f..f966f56c6 100644 --- a/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl +++ b/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl @@ -356,7 +356,7 @@ do_async_query(InstId, Client, Points, ReplyFunAndArgs) -> connector => InstId, points => Points }), - ok = influxdb:write_async(Client, Points, ReplyFunAndArgs). + {ok, _WorkerPid} = influxdb:write_async(Client, Points, ReplyFunAndArgs). %% ------------------------------------------------------------------------------------------------- %% Tags & Fields Config Trans diff --git a/mix.exs b/mix.exs index 9f24cc088..7deab2625 100644 --- a/mix.exs +++ b/mix.exs @@ -131,7 +131,7 @@ defmodule EMQXUmbrella.MixProject do defp enterprise_deps(_profile_info = %{edition_type: :enterprise}) do [ {:hstreamdb_erl, github: "hstreamdb/hstreamdb_erl", tag: "0.2.5"}, - {:influxdb, github: "emqx/influxdb-client-erl", tag: "1.1.6", override: true}, + {:influxdb, github: "emqx/influxdb-client-erl", tag: "1.1.7", override: true}, {:wolff, github: "kafka4beam/wolff", tag: "1.7.4"}, {:kafka_protocol, github: "kafka4beam/kafka_protocol", tag: "4.1.2", override: true}, {:brod_gssapi, github: "kafka4beam/brod_gssapi", tag: "v0.1.0-rc1"}, From bd0e2a74baf0eaf45e574b41af7291160db78807 Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Mon, 16 Jan 2023 16:56:07 -0300 Subject: [PATCH 0249/1802] refactor: rename inflight_name field to inflight_tid --- .../src/emqx_resource_worker.erl | 116 ++++++++---------- 1 file changed, 52 insertions(+), 64 deletions(-) diff --git a/apps/emqx_resource/src/emqx_resource_worker.erl b/apps/emqx_resource/src/emqx_resource_worker.erl index 4e0ae9352..ca8e244ae 100644 --- a/apps/emqx_resource/src/emqx_resource_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_worker.erl @@ -301,12 +301,22 @@ pick_cast(Id, Key, Query) -> resume_from_blocked(Data) -> #{inflight_tid := InflightTID} = Data, case inflight_get_first_retriable(InflightTID) of - empty -> - {next_state, running, Data}; + none -> + case is_inflight_full(InflightTID) of + true -> + {keep_state, Data}; + false -> + {next_state, running, Data} + end; {Ref, FirstQuery} -> %% We retry msgs in inflight window sync, as if we send them %% async, they will be appended to the end of inflight window again. - retry_inflight_sync(Ref, FirstQuery, Data) + case is_inflight_full(InflightTID) of + true -> + {keep_state, Data}; + false -> + retry_inflight_sync(Ref, FirstQuery, Data) + end end. retry_inflight_sync(Ref, QueryOrBatch, Data0) -> @@ -452,7 +462,7 @@ do_flush( } = Data0, %% unwrap when not batching (i.e., batch size == 1) [?QUERY(From, CoreReq, HasBeenSent) = Request] = Batch, - QueryOpts = #{inflight_name => InflightTID}, + QueryOpts = #{inflight_tid => InflightTID}, Result = call_query(configured, Id, Index, Ref, Request, QueryOpts), Reply = ?REPLY(From, CoreReq, HasBeenSent, Result), case reply_caller(Id, Reply) of @@ -519,7 +529,7 @@ do_flush(Data0, #{ batch_size := BatchSize, inflight_tid := InflightTID } = Data0, - QueryOpts = #{inflight_name => InflightTID}, + QueryOpts = #{inflight_tid => InflightTID}, Result = call_query(configured, Id, Index, Ref, Batch, QueryOpts), case batch_reply_caller(Id, Result, Batch) of %% Failed; remove the request from the queue, as we cannot pop @@ -746,21 +756,15 @@ call_query(QM0, Id, Index, Ref, Query, QueryOpts) -> apply_query_fun(sync, Mod, Id, Index, Ref, ?QUERY(_, Request, _) = Query, ResSt, QueryOpts) -> ?tp(call_query, #{id => Id, mod => Mod, query => Query, res_st => ResSt, call_mode => sync}), - InflightTID = maps:get(inflight_name, QueryOpts, undefined), - PerformInflightCapacityCheck = maps:get(perform_inflight_capacity_check, QueryOpts, true), + InflightTID = maps:get(inflight_tid, QueryOpts, undefined), ?APPLY_RESOURCE( call_query, - case PerformInflightCapacityCheck andalso is_inflight_full(InflightTID) of - true -> - %% should be kept in the inflight table and retried - %% when resuming. - {async_return, inflight_full}; - false -> - IsRetriable = false, - WorkerMRef = undefined, - InflightItem = ?INFLIGHT_ITEM(Ref, Query, IsRetriable, WorkerMRef), - ok = inflight_append(InflightTID, InflightItem, Id, Index), - Mod:on_query(Id, Request, ResSt) + begin + IsRetriable = false, + WorkerMRef = undefined, + InflightItem = ?INFLIGHT_ITEM(Ref, Query, IsRetriable, WorkerMRef), + ok = inflight_append(InflightTID, InflightItem, Id, Index), + Mod:on_query(Id, Request, ResSt) end, Request ); @@ -768,22 +772,18 @@ apply_query_fun(async, Mod, Id, Index, Ref, ?QUERY(_, Request, _) = Query, ResSt ?tp(call_query_async, #{ id => Id, mod => Mod, query => Query, res_st => ResSt, call_mode => async }), - InflightTID = maps:get(inflight_name, QueryOpts, undefined), - PerformInflightCapacityCheck = maps:get(perform_inflight_capacity_check, QueryOpts, true), + InflightTID = maps:get(inflight_tid, QueryOpts, undefined), ?APPLY_RESOURCE( call_query_async, - case PerformInflightCapacityCheck andalso is_inflight_full(InflightTID) of - true -> - {async_return, inflight_full}; - false -> - ReplyFun = fun ?MODULE:reply_after_query/7, - Args = [self(), Id, Index, InflightTID, Ref, Query], - IsRetriable = false, - WorkerMRef = undefined, - InflightItem = ?INFLIGHT_ITEM(Ref, Query, IsRetriable, WorkerMRef), - ok = inflight_append(InflightTID, InflightItem, Id, Index), - Result = Mod:on_query_async(Id, Request, {ReplyFun, Args}, ResSt), - {async_return, Result} + begin + ReplyFun = fun ?MODULE:reply_after_query/7, + Args = [self(), Id, Index, InflightTID, Ref, Query], + IsRetriable = false, + WorkerMRef = undefined, + InflightItem = ?INFLIGHT_ITEM(Ref, Query, IsRetriable, WorkerMRef), + ok = inflight_append(InflightTID, InflightItem, Id, Index), + Result = Mod:on_query_async(Id, Request, {ReplyFun, Args}, ResSt), + {async_return, Result} end, Request ); @@ -791,22 +791,16 @@ apply_query_fun(sync, Mod, Id, Index, Ref, [?QUERY(_, _, _) | _] = Batch, ResSt, ?tp(call_batch_query, #{ id => Id, mod => Mod, batch => Batch, res_st => ResSt, call_mode => sync }), - InflightTID = maps:get(inflight_name, QueryOpts, undefined), - PerformInflightCapacityCheck = maps:get(perform_inflight_capacity_check, QueryOpts, true), + InflightTID = maps:get(inflight_tid, QueryOpts, undefined), Requests = [Request || ?QUERY(_From, Request, _) <- Batch], ?APPLY_RESOURCE( call_batch_query, - case PerformInflightCapacityCheck andalso is_inflight_full(InflightTID) of - true -> - %% should be kept in the inflight table and retried - %% when resuming. - {async_return, inflight_full}; - false -> - IsRetriable = false, - WorkerMRef = undefined, - InflightItem = ?INFLIGHT_ITEM(Ref, Batch, IsRetriable, WorkerMRef), - ok = inflight_append(InflightTID, InflightItem, Id, Index), - Mod:on_batch_query(Id, Requests, ResSt) + begin + IsRetriable = false, + WorkerMRef = undefined, + InflightItem = ?INFLIGHT_ITEM(Ref, Batch, IsRetriable, WorkerMRef), + ok = inflight_append(InflightTID, InflightItem, Id, Index), + Mod:on_batch_query(Id, Requests, ResSt) end, Batch ); @@ -814,23 +808,19 @@ apply_query_fun(async, Mod, Id, Index, Ref, [?QUERY(_, _, _) | _] = Batch, ResSt ?tp(call_batch_query_async, #{ id => Id, mod => Mod, batch => Batch, res_st => ResSt, call_mode => async }), - InflightTID = maps:get(inflight_name, QueryOpts, undefined), - PerformInflightCapacityCheck = maps:get(perform_inflight_capacity_check, QueryOpts, true), + InflightTID = maps:get(inflight_tid, QueryOpts, undefined), ?APPLY_RESOURCE( call_batch_query_async, - case PerformInflightCapacityCheck andalso is_inflight_full(InflightTID) of - true -> - {async_return, inflight_full}; - false -> - ReplyFun = fun ?MODULE:batch_reply_after_query/7, - ReplyFunAndArgs = {ReplyFun, [self(), Id, Index, InflightTID, Ref, Batch]}, - Requests = [Request || ?QUERY(_From, Request, _) <- Batch], - IsRetriable = false, - WorkerMRef = undefined, - InflightItem = ?INFLIGHT_ITEM(Ref, Batch, IsRetriable, WorkerMRef), - ok = inflight_append(InflightTID, InflightItem, Id, Index), - Result = Mod:on_batch_query_async(Id, Requests, ReplyFunAndArgs, ResSt), - {async_return, Result} + begin + ReplyFun = fun ?MODULE:batch_reply_after_query/7, + ReplyFunAndArgs = {ReplyFun, [self(), Id, Index, InflightTID, Ref, Batch]}, + Requests = [Request || ?QUERY(_From, Request, _) <- Batch], + IsRetriable = false, + WorkerMRef = undefined, + InflightItem = ?INFLIGHT_ITEM(Ref, Batch, IsRetriable, WorkerMRef), + ok = inflight_append(InflightTID, InflightItem, Id, Index), + Result = Mod:on_batch_query_async(Id, Requests, ReplyFunAndArgs, ResSt), + {async_return, Result} end, Batch ). @@ -935,7 +925,7 @@ inflight_new(InfltWinSZ, Id, Index) -> TableId. -spec inflight_get_first_retriable(ets:tid()) -> - empty | {integer(), [?QUERY(_, _, _)] | ?QUERY(_, _, _)}. + none | {integer(), [?QUERY(_, _, _)] | ?QUERY(_, _, _)}. inflight_get_first_retriable(InflightTID) -> MatchSpec = ets:fun2ms( @@ -947,13 +937,11 @@ inflight_get_first_retriable(InflightTID) -> ), case ets:select(InflightTID, MatchSpec, _Limit = 1) of '$end_of_table' -> - empty; + none; {[{Ref, BatchOrQuery}], _Continuation} -> {Ref, BatchOrQuery} end. -is_inflight_full(undefined) -> - false; is_inflight_full(InflightTID) -> [{_, {max_size, MaxSize}}] = ets:lookup(InflightTID, ?MAX_SIZE_REF), %% we consider number of batches rather than number of messages From b5aaef084c2225d3ccf8f93cc9ce09288015cda0 Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Mon, 16 Jan 2023 18:09:05 -0300 Subject: [PATCH 0250/1802] refactor: enter running state directly now that we don't have the possibility of dirty disk queues (we always use volatile replayq), we will never resume old work. --- apps/emqx_resource/src/emqx_resource_worker.erl | 2 +- apps/emqx_resource/test/emqx_resource_SUITE.erl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/emqx_resource/src/emqx_resource_worker.erl b/apps/emqx_resource/src/emqx_resource_worker.erl index ca8e244ae..59fd41a03 100644 --- a/apps/emqx_resource/src/emqx_resource_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_worker.erl @@ -189,7 +189,7 @@ init({Id, Index, Opts}) -> tref => undefined }, ?tp(resource_worker_init, #{id => Id, index => Index}), - {ok, blocked, Data, {next_event, cast, resume}}. + {ok, running, Data}. running(enter, _, St) -> ?tp(resource_worker_enter_running, #{}), diff --git a/apps/emqx_resource/test/emqx_resource_SUITE.erl b/apps/emqx_resource/test/emqx_resource_SUITE.erl index 4ffb259e8..da140489e 100644 --- a/apps/emqx_resource/test/emqx_resource_SUITE.erl +++ b/apps/emqx_resource/test/emqx_resource_SUITE.erl @@ -1200,7 +1200,7 @@ t_delete_and_re_create_with_same_name(_Config) -> resume_interval => 1_000 } ), - #{?snk_kind := resource_worker_enter_blocked}, + #{?snk_kind := resource_worker_enter_running}, 5_000 ), From 3ba65c4377bec508956a7a9a72d063cc7c4757f4 Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Mon, 16 Jan 2023 18:12:30 -0300 Subject: [PATCH 0251/1802] feat: poke the buffer workers when inflight is no longer full if max inflight = 1, then we only make progress based on the state timer, since the callbacks were not poking the buffer workers. --- apps/emqx_resource/src/emqx_resource_worker.erl | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/apps/emqx_resource/src/emqx_resource_worker.erl b/apps/emqx_resource/src/emqx_resource_worker.erl index 59fd41a03..6875341b4 100644 --- a/apps/emqx_resource/src/emqx_resource_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_worker.erl @@ -33,7 +33,8 @@ sync_query/3, async_query/3, block/1, - resume/1 + resume/1, + flush_worker/1 ]). -export([ @@ -152,6 +153,10 @@ block(ServerRef) -> resume(ServerRef) -> gen_statem:cast(ServerRef, resume). +-spec flush_worker(pid()) -> ok. +flush_worker(ServerRef) -> + gen_statem:cast(ServerRef, flush). + -spec init({id(), pos_integer(), map()}) -> gen_statem:init_result(state(), data()). init({Id, Index, Opts}) -> process_flag(trap_exit, true), @@ -196,6 +201,8 @@ running(enter, _, St) -> maybe_flush(St); running(cast, resume, _St) -> keep_state_and_data; +running(cast, flush, Data) -> + flush(Data); running(cast, block, St) -> {next_state, blocked, St}; running(info, ?SEND_REQ(_From, _Req) = Request0, Data) -> @@ -222,6 +229,8 @@ blocked(cast, block, _St) -> keep_state_and_data; blocked(cast, resume, St) -> resume_from_blocked(St); +blocked(cast, flush, Data) -> + resume_from_blocked(Data); blocked(state_timeout, unblock, St) -> resume_from_blocked(St); blocked(info, ?SEND_REQ(_ReqFrom, {query, _Request, _Opts}) = Request0, Data0) -> @@ -834,6 +843,7 @@ reply_after_query(Pid, Id, Index, InflightTID, Ref, ?QUERY(From, Request, HasBee %% returned, otherwise the request will get retried. The %% caller has just been notified of the failure and should %% decide if it wants to retry or not. + IsFullBefore = is_inflight_full(InflightTID), IsAcked = ack_inflight(InflightTID, Ref, Id, Index), IsAcked andalso PostFn(), case Action of @@ -850,6 +860,7 @@ reply_after_query(Pid, Id, Index, InflightTID, Ref, ?QUERY(From, Request, HasBee batch_or_query => ?QUERY(From, Request, HasBeenSent), result => Result }), + IsFullBefore andalso ?MODULE:flush_worker(Pid), ok end. @@ -862,6 +873,7 @@ batch_reply_after_query(Pid, Id, Index, InflightTID, Ref, Batch, Result) -> %% returned, otherwise the request will get retried. The %% caller has just been notified of the failure and should %% decide if it wants to retry or not. + IsFullBefore = is_inflight_full(InflightTID), IsAcked = ack_inflight(InflightTID, Ref, Id, Index), IsAcked andalso lists:foreach(fun(F) -> F() end, PostFns), case Action of @@ -874,6 +886,7 @@ batch_reply_after_query(Pid, Id, Index, InflightTID, Ref, Batch, Result) -> ?tp(resource_worker_reply_after_query, #{ action => ack, batch_or_query => Batch, result => Result }), + IsFullBefore andalso ?MODULE:flush_worker(Pid), ok end. @@ -942,6 +955,8 @@ inflight_get_first_retriable(InflightTID) -> {Ref, BatchOrQuery} end. +is_inflight_full(undefined) -> + false; is_inflight_full(InflightTID) -> [{_, {max_size, MaxSize}}] = ets:lookup(InflightTID, ?MAX_SIZE_REF), %% we consider number of batches rather than number of messages From b82009bc2955760259b696e242efa702a35865ce Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Mon, 16 Jan 2023 18:16:56 -0300 Subject: [PATCH 0252/1802] refactor: use monotonic times as refs and store initial times when creating ets with this, we may measure latencies in the future. --- apps/emqx_resource/src/emqx_resource_worker.erl | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/apps/emqx_resource/src/emqx_resource_worker.erl b/apps/emqx_resource/src/emqx_resource_worker.erl index 6875341b4..b1a34355b 100644 --- a/apps/emqx_resource/src/emqx_resource_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_worker.erl @@ -924,17 +924,24 @@ append_queue(Id, Index, Q, Queries) when not is_binary(Q) -> %%============================================================================== %% the inflight queue for async query --define(MAX_SIZE_REF, -1). --define(SIZE_REF, -2). +-define(MAX_SIZE_REF, max_size). +-define(SIZE_REF, size). +-define(INITIAL_TIME_REF, initial_time). +-define(INITIAL_MONOTONIC_TIME_REF, initial_monotonic_time). + inflight_new(InfltWinSZ, Id, Index) -> TableId = ets:new( emqx_resource_worker_inflight_tab, [ordered_set, public, {write_concurrency, true}] ), - inflight_append(TableId, {?MAX_SIZE_REF, {max_size, InfltWinSZ}}, Id, Index), + inflight_append(TableId, {?MAX_SIZE_REF, InfltWinSZ}, Id, Index), %% we use this counter because we might deal with batches as %% elements. inflight_append(TableId, {?SIZE_REF, 0}, Id, Index), + inflight_append(TableId, {?INITIAL_TIME_REF, erlang:system_time()}, Id, Index), + inflight_append( + TableId, {?INITIAL_MONOTONIC_TIME_REF, erlang:monotonic_time(nanosecond)}, Id, Index + ), TableId. -spec inflight_get_first_retriable(ets:tid()) -> @@ -958,7 +965,7 @@ inflight_get_first_retriable(InflightTID) -> is_inflight_full(undefined) -> false; is_inflight_full(InflightTID) -> - [{_, {max_size, MaxSize}}] = ets:lookup(InflightTID, ?MAX_SIZE_REF), + [{_, MaxSize}] = ets:lookup(InflightTID, ?MAX_SIZE_REF), %% we consider number of batches rather than number of messages %% because one batch request may hold several messages. Size = inflight_num_batches(InflightTID), @@ -1165,7 +1172,7 @@ cancel_flush_timer(St = #{tref := {TRef, _Ref}}) -> St#{tref => undefined}. make_message_ref() -> - erlang:unique_integer([monotonic, positive]). + erlang:monotonic_time(nanosecond). collect_requests(Acc, Limit) -> Count = length(Acc), From fa01deb3ebdf27e0463fee6dc423265cb016dc81 Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Tue, 17 Jan 2023 11:55:40 -0300 Subject: [PATCH 0253/1802] chore: retry as much as possible, don't reply to caller too soon --- apps/emqx_authz/src/emqx_authz_http.erl | 12 +- .../emqx_authz/test/emqx_authz_http_SUITE.erl | 2 +- apps/emqx_bridge/src/emqx_bridge.erl | 7 +- .../test/emqx_bridge_mqtt_SUITE.erl | 19 +- .../i18n/emqx_resource_schema_i18n.conf | 11 + .../src/emqx_resource_worker.erl | 295 +++++++++--------- .../src/schema/emqx_resource_schema.erl | 6 + .../test/emqx_connector_demo.erl | 6 + .../test/emqx_resource_SUITE.erl | 227 ++++++++------ .../test/emqx_ee_bridge_gcp_pubsub_SUITE.erl | 26 +- 10 files changed, 331 insertions(+), 280 deletions(-) diff --git a/apps/emqx_authz/src/emqx_authz_http.erl b/apps/emqx_authz/src/emqx_authz_http.erl index 852a667c8..a300291d1 100644 --- a/apps/emqx_authz/src/emqx_authz_http.erl +++ b/apps/emqx_authz/src/emqx_authz_http.erl @@ -82,7 +82,7 @@ authorize( } = Config ) -> Request = generate_request(PubSub, Topic, Client, Config), - case emqx_resource:simple_sync_query(ResourceID, {Method, Request, RequestTimeout}) of + try emqx_resource:simple_sync_query(ResourceID, {Method, Request, RequestTimeout}) of {ok, 204, _Headers} -> {matched, allow}; {ok, 200, Headers, Body} -> @@ -112,6 +112,16 @@ authorize( reason => Reason }), ignore + catch + error:timeout -> + Reason = timeout, + ?tp(authz_http_request_failure, #{error => Reason}), + ?SLOG(error, #{ + msg => "http_server_query_failed", + resource => ResourceID, + reason => Reason + }), + ignore end. log_nomtach_msg(Status, Headers, Body) -> diff --git a/apps/emqx_authz/test/emqx_authz_http_SUITE.erl b/apps/emqx_authz/test/emqx_authz_http_SUITE.erl index e91da9829..e5a72f680 100644 --- a/apps/emqx_authz/test/emqx_authz_http_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_http_SUITE.erl @@ -172,7 +172,7 @@ t_response_handling(_Config) -> [ #{ ?snk_kind := authz_http_request_failure, - error := {recoverable_error, econnrefused} + error := timeout } ], ?of_kind(authz_http_request_failure, Trace) diff --git a/apps/emqx_bridge/src/emqx_bridge.erl b/apps/emqx_bridge/src/emqx_bridge.erl index c86087014..38fe0a144 100644 --- a/apps/emqx_bridge/src/emqx_bridge.erl +++ b/apps/emqx_bridge/src/emqx_bridge.erl @@ -170,8 +170,11 @@ send_message(BridgeId, Message) -> case emqx:get_config([bridges, BridgeType, BridgeName], not_found) of not_found -> {error, {bridge_not_found, BridgeId}}; - #{enable := true} -> - emqx_resource:query(ResId, {send_message, Message}); + #{enable := true} = Config -> + Timeout = emqx_map_lib:deep_get( + [resource_opts, request_timeout], Config, timer:seconds(15) + ), + emqx_resource:query(ResId, {send_message, Message}, #{timeout => Timeout}); #{enable := false} -> {error, {bridge_stopped, BridgeId}} end. diff --git a/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl b/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl index 1f03863ae..d20d3bc10 100644 --- a/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl +++ b/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl @@ -145,10 +145,12 @@ set_special_configs(_) -> init_per_testcase(_, Config) -> {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000), + ok = snabbkaffe:start_trace(), Config. end_per_testcase(_, _Config) -> clear_resources(), emqx_common_test_helpers:call_janitor(), + snabbkaffe:stop(), ok. clear_resources() -> @@ -478,8 +480,6 @@ t_egress_custom_clientid_prefix(_Config) -> end, {ok, 204, <<>>} = request(delete, uri(["bridges", BridgeIDEgress]), []), - {ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []), - ok. t_mqtt_conn_bridge_ingress_and_egress(_) -> @@ -830,6 +830,7 @@ t_mqtt_conn_bridge_egress_reconnect(_) -> <<"resource_opts">> => #{ <<"worker_pool_size">> => 2, <<"query_mode">> => <<"sync">>, + <<"request_timeout">> => <<"500ms">>, %% to make it check the healthy quickly <<"health_check_interval">> => <<"0.5s">> } @@ -880,17 +881,14 @@ t_mqtt_conn_bridge_egress_reconnect(_) -> ok = emqx_listeners:stop_listener('tcp:default'), ct:sleep(1500), - %% PUBLISH 2 messages to the 'local' broker, the message should - ok = snabbkaffe:start_trace(), + %% PUBLISH 2 messages to the 'local' broker, the messages should + %% be enqueued and the resource will block {ok, SRef} = snabbkaffe:subscribe( fun - ( - #{ - ?snk_kind := call_query_enter, - query := {query, _From, {send_message, #{}}, _Sent} - } - ) -> + (#{?snk_kind := resource_worker_retry_inflight_failed}) -> + true; + (#{?snk_kind := resource_worker_flush_nack}) -> true; (_) -> false @@ -903,7 +901,6 @@ t_mqtt_conn_bridge_egress_reconnect(_) -> emqx:publish(emqx_message:make(LocalTopic, Payload1)), emqx:publish(emqx_message:make(LocalTopic, Payload2)), {ok, _} = snabbkaffe:receive_events(SRef), - ok = snabbkaffe:stop(), %% verify the metrics of the bridge, the message should be queued {ok, 200, BridgeStr1} = request(get, uri(["bridges", BridgeIDEgress]), []), diff --git a/apps/emqx_resource/i18n/emqx_resource_schema_i18n.conf b/apps/emqx_resource/i18n/emqx_resource_schema_i18n.conf index 0b6cbd0a2..de76967ab 100644 --- a/apps/emqx_resource/i18n/emqx_resource_schema_i18n.conf +++ b/apps/emqx_resource/i18n/emqx_resource_schema_i18n.conf @@ -89,6 +89,17 @@ For bridges only have ingress direction data flow, it can be set to 0 otherwise } } + request_timeout { + desc { + en: """Timeout for requests. If query_mode is sync, calls to the resource will be blocked for this amount of time before timing out.""" + zh: """请求的超时。 如果query_modesync,对资源的调用将在超时前被阻断这一时间。""" + } + label { + en: """Request timeout""" + zh: """请求超时""" + } + } + enable_batch { desc { en: """Batch mode enabled.""" diff --git a/apps/emqx_resource/src/emqx_resource_worker.erl b/apps/emqx_resource/src/emqx_resource_worker.erl index b1a34355b..2ef1cbed4 100644 --- a/apps/emqx_resource/src/emqx_resource_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_worker.erl @@ -100,7 +100,7 @@ start_link(Id, Index, Opts) -> -spec sync_query(id(), request(), query_opts()) -> Result :: term(). sync_query(Id, Request, Opts) -> PickKey = maps:get(pick_key, Opts, self()), - Timeout = maps:get(timeout, Opts, infinity), + Timeout = maps:get(timeout, Opts, timer:seconds(15)), emqx_resource_metrics:matched_inc(Id), pick_call(Id, PickKey, {query, Request, Opts}, Timeout). @@ -234,10 +234,7 @@ blocked(cast, flush, Data) -> blocked(state_timeout, unblock, St) -> resume_from_blocked(St); blocked(info, ?SEND_REQ(_ReqFrom, {query, _Request, _Opts}) = Request0, Data0) -> - #{id := Id} = Data0, - {Queries, Data} = collect_and_enqueue_query_requests(Request0, Data0), - Error = ?RESOURCE_ERROR(blocked, "resource is blocked"), - _ = batch_reply_caller(Id, Error, Queries), + {_Queries, Data} = collect_and_enqueue_query_requests(Request0, Data0), {keep_state, Data}; blocked(info, {flush, _Ref}, _Data) -> keep_state_and_data; @@ -337,10 +334,16 @@ retry_inflight_sync(Ref, QueryOrBatch, Data0) -> } = Data0, ?tp(resource_worker_retry_inflight, #{query_or_batch => QueryOrBatch, ref => Ref}), QueryOpts = #{}, - %% if we are retrying an inflight query, it has been sent - HasBeenSent = true, Result = call_query(sync, Id, Index, Ref, QueryOrBatch, QueryOpts), - case handle_query_result_pure(Id, Result, HasBeenSent) of + ReplyResult = + case QueryOrBatch of + ?QUERY(From, CoreReq, HasBeenSent) -> + Reply = ?REPLY(From, CoreReq, HasBeenSent, Result), + reply_caller_defer_metrics(Id, Reply); + [?QUERY(_, _, _) | _] = Batch -> + batch_reply_caller_defer_metrics(Id, Result, Batch) + end, + case ReplyResult of %% Send failed because resource is down {nack, PostFn} -> PostFn(), @@ -476,27 +479,20 @@ do_flush( Reply = ?REPLY(From, CoreReq, HasBeenSent, Result), case reply_caller(Id, Reply) of %% Failed; remove the request from the queue, as we cannot pop - %% from it again. But we must ensure it's in the inflight - %% table, even if it's full, so we don't lose the request. - %% And only in that case. + %% from it again, but we'll retry it using the inflight table. nack -> ok = replayq:ack(Q1, QAckRef), - %% We might get a retriable response without having added - %% the request to the inflight table (e.g.: sync request, - %% but resource health check failed prior to calling and - %% so we didn't even call it). In that case, we must then - %% add it to the inflight table. - IsRetriable = - is_recoverable_error_result(Result) orelse - is_not_connected_result(Result), - ShouldPreserveInInflight = is_not_connected_result(Result), %% we set it atomically just below; a limitation of having %% to use tuples for atomic ets updates + IsRetriable = true, WorkerMRef0 = undefined, InflightItem = ?INFLIGHT_ITEM(Ref, Request, IsRetriable, WorkerMRef0), - ShouldPreserveInInflight andalso - inflight_append(InflightTID, InflightItem, Id, Index), - IsRetriable andalso mark_inflight_as_retriable(InflightTID, Ref), + %% we must append again to the table to ensure that the + %% request will be retried (i.e., it might not have been + %% inserted during `call_query' if the resource was down + %% and/or if it was a sync request). + inflight_append(InflightTID, InflightItem, Id, Index), + mark_inflight_as_retriable(InflightTID, Ref), {Data1, WorkerMRef} = ensure_async_worker_monitored(Data0, Result), store_async_worker_reference(InflightTID, Ref, WorkerMRef), emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), @@ -513,11 +509,21 @@ do_flush( %% Success; just ack. ack -> ok = replayq:ack(Q1, QAckRef), + %% Async requests are acked later when the async worker + %% calls the corresponding callback function. Also, we + %% must ensure the async worker is being monitored for + %% such requests. is_async(Id) orelse ack_inflight(InflightTID, Ref, Id, Index), {Data1, WorkerMRef} = ensure_async_worker_monitored(Data0, Result), store_async_worker_reference(InflightTID, Ref, WorkerMRef), emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), - ?tp(resource_worker_flush_ack, #{batch_or_query => Request}), + ?tp( + resource_worker_flush_ack, + #{ + batch_or_query => Request, + result => Result + } + ), case queue_count(Q1) > 0 of true -> {keep_state, Data1, [{next_event, internal, flush}]}; @@ -542,27 +548,20 @@ do_flush(Data0, #{ Result = call_query(configured, Id, Index, Ref, Batch, QueryOpts), case batch_reply_caller(Id, Result, Batch) of %% Failed; remove the request from the queue, as we cannot pop - %% from it again. But we must ensure it's in the inflight - %% table, even if it's full, so we don't lose the request. - %% And only in that case. + %% from it again, but we'll retry it using the inflight table. nack -> ok = replayq:ack(Q1, QAckRef), - %% We might get a retriable response without having added - %% the request to the inflight table (e.g.: sync request, - %% but resource health check failed prior to calling and - %% so we didn't even call it). In that case, we must then - %% add it to the inflight table. - IsRetriable = - is_recoverable_error_result(Result) orelse - is_not_connected_result(Result), - ShouldPreserveInInflight = is_not_connected_result(Result), %% we set it atomically just below; a limitation of having %% to use tuples for atomic ets updates + IsRetriable = true, WorkerMRef0 = undefined, InflightItem = ?INFLIGHT_ITEM(Ref, Batch, IsRetriable, WorkerMRef0), - ShouldPreserveInInflight andalso - inflight_append(InflightTID, InflightItem, Id, Index), - IsRetriable andalso mark_inflight_as_retriable(InflightTID, Ref), + %% we must append again to the table to ensure that the + %% request will be retried (i.e., it might not have been + %% inserted during `call_query' if the resource was down + %% and/or if it was a sync request). + inflight_append(InflightTID, InflightItem, Id, Index), + mark_inflight_as_retriable(InflightTID, Ref), {Data1, WorkerMRef} = ensure_async_worker_monitored(Data0, Result), store_async_worker_reference(InflightTID, Ref, WorkerMRef), emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), @@ -579,11 +578,21 @@ do_flush(Data0, #{ %% Success; just ack. ack -> ok = replayq:ack(Q1, QAckRef), + %% Async requests are acked later when the async worker + %% calls the corresponding callback function. Also, we + %% must ensure the async worker is being monitored for + %% such requests. is_async(Id) orelse ack_inflight(InflightTID, Ref, Id, Index), {Data1, WorkerMRef} = ensure_async_worker_monitored(Data0, Result), store_async_worker_reference(InflightTID, Ref, WorkerMRef), emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), - ?tp(resource_worker_flush_ack, #{batch_or_query => Batch}), + ?tp( + resource_worker_flush_ack, + #{ + batch_or_query => Batch, + result => Result + } + ), CurrentCount = queue_count(Q1), case {CurrentCount > 0, CurrentCount >= BatchSize} of {false, _} -> @@ -597,54 +606,79 @@ do_flush(Data0, #{ end. batch_reply_caller(Id, BatchResult, Batch) -> - {ShouldBlock, PostFns} = batch_reply_caller_defer_metrics(Id, BatchResult, Batch), - lists:foreach(fun(F) -> F() end, PostFns), - ShouldBlock. - -batch_reply_caller_defer_metrics(Id, BatchResult, Batch) -> - lists:foldl( - fun(Reply, {_ShouldBlock, PostFns}) -> - {ShouldBlock, PostFn} = reply_caller_defer_metrics(Id, Reply), - {ShouldBlock, [PostFn | PostFns]} - end, - {ack, []}, - %% the `Mod:on_batch_query/3` returns a single result for a batch, - %% so we need to expand - ?EXPAND(BatchResult, Batch) - ). - -reply_caller(Id, Reply) -> - {ShouldBlock, PostFn} = reply_caller_defer_metrics(Id, Reply), + {ShouldBlock, PostFn} = batch_reply_caller_defer_metrics(Id, BatchResult, Batch), PostFn(), ShouldBlock. +batch_reply_caller_defer_metrics(Id, BatchResult, Batch) -> + {ShouldAck, PostFns} = + lists:foldl( + fun(Reply, {_ShouldAck, PostFns}) -> + {ShouldAck, PostFn} = reply_caller_defer_metrics(Id, Reply), + {ShouldAck, [PostFn | PostFns]} + end, + {ack, []}, + %% the `Mod:on_batch_query/3` returns a single result for a batch, + %% so we need to expand + ?EXPAND(BatchResult, Batch) + ), + PostFn = fun() -> lists:foreach(fun(F) -> F() end, PostFns) end, + {ShouldAck, PostFn}. + +reply_caller(Id, Reply) -> + {ShouldAck, PostFn} = reply_caller_defer_metrics(Id, Reply), + PostFn(), + ShouldAck. + +%% Should only reply to the caller when the decision is final (not +%% retriable). See comment on `handle_query_result_pure'. reply_caller_defer_metrics(Id, ?REPLY(undefined, _, HasBeenSent, Result)) -> handle_query_result_pure(Id, Result, HasBeenSent); reply_caller_defer_metrics(Id, ?REPLY({ReplyFun, Args}, _, HasBeenSent, Result)) when is_function(ReplyFun) -> - _ = - case Result of - {async_return, _} -> no_reply_for_now; - _ -> apply(ReplyFun, Args ++ [Result]) - end, - handle_query_result_pure(Id, Result, HasBeenSent); + {ShouldAck, PostFn} = handle_query_result_pure(Id, Result, HasBeenSent), + case {ShouldAck, Result} of + {nack, _} -> + ok; + {ack, {async_return, _}} -> + ok; + {ack, _} -> + apply(ReplyFun, Args ++ [Result]), + ok + end, + {ShouldAck, PostFn}; reply_caller_defer_metrics(Id, ?REPLY(From, _, HasBeenSent, Result)) -> - gen_statem:reply(From, Result), - handle_query_result_pure(Id, Result, HasBeenSent). + {ShouldAck, PostFn} = handle_query_result_pure(Id, Result, HasBeenSent), + case {ShouldAck, Result} of + {nack, _} -> + ok; + {ack, {async_return, _}} -> + ok; + {ack, _} -> + gen_statem:reply(From, Result), + ok + end, + {ShouldAck, PostFn}. handle_query_result(Id, Result, HasBeenSent) -> {ShouldBlock, PostFn} = handle_query_result_pure(Id, Result, HasBeenSent), PostFn(), ShouldBlock. -handle_query_result_pure(Id, ?RESOURCE_ERROR_M(exception, Msg), HasBeenSent) -> +%% We should always retry (nack), except when: +%% * resource is not found +%% * resource is stopped +%% * the result is a success (or at least a delayed result) +%% We also retry even sync requests. In that case, we shouldn't reply +%% the caller until one of those final results above happen. +handle_query_result_pure(_Id, ?RESOURCE_ERROR_M(exception, Msg), _HasBeenSent) -> PostFn = fun() -> ?SLOG(error, #{msg => resource_exception, info => Msg}), - inc_sent_failed(Id, HasBeenSent), + %% inc_sent_failed(Id, HasBeenSent), ok end, - {ack, PostFn}; + {nack, PostFn}; handle_query_result_pure(_Id, ?RESOURCE_ERROR_M(NotWorking, _), _HasBeenSent) when NotWorking == not_connected; NotWorking == blocked -> @@ -666,10 +700,12 @@ handle_query_result_pure(Id, ?RESOURCE_ERROR_M(stopped, Msg), _HasBeenSent) -> handle_query_result_pure(Id, ?RESOURCE_ERROR_M(Reason, _), _HasBeenSent) -> PostFn = fun() -> ?SLOG(error, #{id => Id, msg => other_resource_error, reason => Reason}), - emqx_resource_metrics:dropped_other_inc(Id), + %% emqx_resource_metrics:dropped_other_inc(Id), ok end, - {ack, PostFn}; + {nack, PostFn}; +%% TODO: invert this logic: we should differentiate errors that are +%% irrecoverable; all others are deemed recoverable. handle_query_result_pure(Id, {error, {recoverable_error, Reason}}, _HasBeenSent) -> %% the message will be queued in replayq or inflight window, %% i.e. the counter 'queuing' or 'dropped' will increase, so we pretend that we have not @@ -679,22 +715,18 @@ handle_query_result_pure(Id, {error, {recoverable_error, Reason}}, _HasBeenSent) ok end, {nack, PostFn}; -handle_query_result_pure(Id, {error, Reason}, HasBeenSent) -> +handle_query_result_pure(Id, {error, Reason}, _HasBeenSent) -> PostFn = fun() -> ?SLOG(error, #{id => Id, msg => send_error, reason => Reason}), - inc_sent_failed(Id, HasBeenSent), ok end, - {ack, PostFn}; -handle_query_result_pure(_Id, {async_return, inflight_full}, _HasBeenSent) -> - {nack, fun() -> ok end}; -handle_query_result_pure(Id, {async_return, {error, Msg}}, HasBeenSent) -> + {nack, PostFn}; +handle_query_result_pure(Id, {async_return, {error, Msg}}, _HasBeenSent) -> PostFn = fun() -> ?SLOG(error, #{id => Id, msg => async_send_error, info => Msg}), - inc_sent_failed(Id, HasBeenSent), ok end, - {ack, PostFn}; + {nack, PostFn}; handle_query_result_pure(_Id, {async_return, ok}, _HasBeenSent) -> {ack, fun() -> ok end}; handle_query_result_pure(_Id, {async_return, {ok, Pid}}, _HasBeenSent) when is_pid(Pid) -> @@ -714,18 +746,6 @@ handle_async_worker_down(Data0, Pid) -> cancel_inflight_items(Data, WorkerMRef), {keep_state, Data}. -is_not_connected_result(?RESOURCE_ERROR_M(Error, _)) when - Error =:= not_connected; Error =:= blocked --> - true; -is_not_connected_result(_) -> - false. - -is_recoverable_error_result({error, {recoverable_error, _Reason}}) -> - true; -is_recoverable_error_result(_) -> - false. - call_query(QM0, Id, Index, Ref, Query, QueryOpts) -> ?tp(call_query_enter, #{id => Id, query => Query}), case emqx_resource_manager:ets_lookup(Id) of @@ -735,8 +755,9 @@ call_query(QM0, Id, Index, Ref, Query, QueryOpts) -> configured -> maps:get(query_mode, Data); _ -> QM0 end, - CM = maps:get(callback_mode, Data), - apply_query_fun(call_mode(QM, CM), Mod, Id, Index, Ref, Query, ResSt, QueryOpts); + CBM = maps:get(callback_mode, Data), + CallMode = call_mode(QM, CBM), + apply_query_fun(CallMode, Mod, Id, Index, Ref, Query, ResSt, QueryOpts); {ok, _Group, #{status := stopped}} -> ?RESOURCE_ERROR(stopped, "resource stopped or disabled"); {ok, _Group, #{status := S}} when S == connecting; S == disconnected -> @@ -763,20 +784,9 @@ call_query(QM0, Id, Index, Ref, Query, QueryOpts) -> end ). -apply_query_fun(sync, Mod, Id, Index, Ref, ?QUERY(_, Request, _) = Query, ResSt, QueryOpts) -> - ?tp(call_query, #{id => Id, mod => Mod, query => Query, res_st => ResSt, call_mode => sync}), - InflightTID = maps:get(inflight_tid, QueryOpts, undefined), - ?APPLY_RESOURCE( - call_query, - begin - IsRetriable = false, - WorkerMRef = undefined, - InflightItem = ?INFLIGHT_ITEM(Ref, Query, IsRetriable, WorkerMRef), - ok = inflight_append(InflightTID, InflightItem, Id, Index), - Mod:on_query(Id, Request, ResSt) - end, - Request - ); +apply_query_fun(sync, Mod, Id, _Index, _Ref, ?QUERY(_, Request, _) = _Query, ResSt, _QueryOpts) -> + ?tp(call_query, #{id => Id, mod => Mod, query => _Query, res_st => ResSt, call_mode => sync}), + ?APPLY_RESOURCE(call_query, Mod:on_query(Id, Request, ResSt), Request); apply_query_fun(async, Mod, Id, Index, Ref, ?QUERY(_, Request, _) = Query, ResSt, QueryOpts) -> ?tp(call_query_async, #{ id => Id, mod => Mod, query => Query, res_st => ResSt, call_mode => async @@ -796,23 +806,12 @@ apply_query_fun(async, Mod, Id, Index, Ref, ?QUERY(_, Request, _) = Query, ResSt end, Request ); -apply_query_fun(sync, Mod, Id, Index, Ref, [?QUERY(_, _, _) | _] = Batch, ResSt, QueryOpts) -> +apply_query_fun(sync, Mod, Id, _Index, _Ref, [?QUERY(_, _, _) | _] = Batch, ResSt, _QueryOpts) -> ?tp(call_batch_query, #{ id => Id, mod => Mod, batch => Batch, res_st => ResSt, call_mode => sync }), - InflightTID = maps:get(inflight_tid, QueryOpts, undefined), Requests = [Request || ?QUERY(_From, Request, _) <- Batch], - ?APPLY_RESOURCE( - call_batch_query, - begin - IsRetriable = false, - WorkerMRef = undefined, - InflightItem = ?INFLIGHT_ITEM(Ref, Batch, IsRetriable, WorkerMRef), - ok = inflight_append(InflightTID, InflightItem, Id, Index), - Mod:on_batch_query(Id, Requests, ResSt) - end, - Batch - ); + ?APPLY_RESOURCE(call_batch_query, Mod:on_batch_query(Id, Requests, ResSt), Batch); apply_query_fun(async, Mod, Id, Index, Ref, [?QUERY(_, _, _) | _] = Batch, ResSt, QueryOpts) -> ?tp(call_batch_query_async, #{ id => Id, mod => Mod, batch => Batch, res_st => ResSt, call_mode => async @@ -839,27 +838,27 @@ reply_after_query(Pid, Id, Index, InflightTID, Ref, ?QUERY(From, Request, HasBee %% but received no ACK, NOT the number of messages queued in the %% inflight window. {Action, PostFn} = reply_caller_defer_metrics(Id, ?REPLY(From, Request, HasBeenSent, Result)), - %% Should always ack async inflight requests that - %% returned, otherwise the request will get retried. The - %% caller has just been notified of the failure and should - %% decide if it wants to retry or not. - IsFullBefore = is_inflight_full(InflightTID), - IsAcked = ack_inflight(InflightTID, Ref, Id, Index), - IsAcked andalso PostFn(), case Action of nack -> + %% Keep retrying. ?tp(resource_worker_reply_after_query, #{ - action => nack, + action => Action, batch_or_query => ?QUERY(From, Request, HasBeenSent), + ref => Ref, result => Result }), + mark_inflight_as_retriable(InflightTID, Ref), ?MODULE:block(Pid); ack -> ?tp(resource_worker_reply_after_query, #{ - action => ack, + action => Action, batch_or_query => ?QUERY(From, Request, HasBeenSent), + ref => Ref, result => Result }), + IsFullBefore = is_inflight_full(InflightTID), + IsAcked = ack_inflight(InflightTID, Ref, Id, Index), + IsAcked andalso PostFn(), IsFullBefore andalso ?MODULE:flush_worker(Pid), ok end. @@ -868,24 +867,28 @@ batch_reply_after_query(Pid, Id, Index, InflightTID, Ref, Batch, Result) -> %% NOTE: 'inflight' is the count of messages that were sent async %% but received no ACK, NOT the number of messages queued in the %% inflight window. - {Action, PostFns} = batch_reply_caller_defer_metrics(Id, Result, Batch), - %% Should always ack async inflight requests that - %% returned, otherwise the request will get retried. The - %% caller has just been notified of the failure and should - %% decide if it wants to retry or not. - IsFullBefore = is_inflight_full(InflightTID), - IsAcked = ack_inflight(InflightTID, Ref, Id, Index), - IsAcked andalso lists:foreach(fun(F) -> F() end, PostFns), + {Action, PostFn} = batch_reply_caller_defer_metrics(Id, Result, Batch), case Action of nack -> + %% Keep retrying. ?tp(resource_worker_reply_after_query, #{ - action => nack, batch_or_query => Batch, result => Result + action => nack, + batch_or_query => Batch, + ref => Ref, + result => Result }), + mark_inflight_as_retriable(InflightTID, Ref), ?MODULE:block(Pid); ack -> ?tp(resource_worker_reply_after_query, #{ - action => ack, batch_or_query => Batch, result => Result + action => ack, + batch_or_query => Batch, + ref => Ref, + result => Result }), + IsFullBefore = is_inflight_full(InflightTID), + IsAcked = ack_inflight(InflightTID, Ref, Id, Index), + IsAcked andalso PostFn(), IsFullBefore andalso ?MODULE:flush_worker(Pid), ok end. @@ -919,7 +922,14 @@ append_queue(Id, Index, Q, Queries) when not is_binary(Q) -> Q1 end, emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q2)), - ?tp(resource_worker_appended_to_queue, #{id => Id, items => Queries}), + ?tp( + resource_worker_appended_to_queue, + #{ + id => Id, + items => Queries, + queue_count => queue_count(Q2) + } + ), Q2. %%============================================================================== @@ -1110,11 +1120,6 @@ do_cancel_inflight_item(Data, Ref) -> %%============================================================================== -inc_sent_failed(Id, _HasBeenSent = true) -> - emqx_resource_metrics:retried_failed_inc(Id); -inc_sent_failed(Id, _HasBeenSent) -> - emqx_resource_metrics:failed_inc(Id). - inc_sent_success(Id, _HasBeenSent = true) -> emqx_resource_metrics:retried_success_inc(Id); inc_sent_success(Id, _HasBeenSent) -> diff --git a/apps/emqx_resource/src/schema/emqx_resource_schema.erl b/apps/emqx_resource/src/schema/emqx_resource_schema.erl index d105b21ef..ea5ee97ca 100644 --- a/apps/emqx_resource/src/schema/emqx_resource_schema.erl +++ b/apps/emqx_resource/src/schema/emqx_resource_schema.erl @@ -48,6 +48,7 @@ fields("creation_opts") -> {health_check_interval, fun health_check_interval/1}, {auto_restart_interval, fun auto_restart_interval/1}, {query_mode, fun query_mode/1}, + {request_timeout, fun request_timeout/1}, {async_inflight_window, fun async_inflight_window/1}, {enable_batch, fun enable_batch/1}, {batch_size, fun batch_size/1}, @@ -80,6 +81,11 @@ query_mode(default) -> async; query_mode(required) -> false; query_mode(_) -> undefined. +request_timeout(type) -> hoconsc:union([infinity, emqx_schema:duration_ms()]); +request_timeout(desc) -> ?DESC("request_timeout"); +request_timeout(default) -> <<"15s">>; +request_timeout(_) -> undefined. + enable_batch(type) -> boolean(); enable_batch(required) -> false; enable_batch(default) -> true; diff --git a/apps/emqx_resource/test/emqx_connector_demo.erl b/apps/emqx_resource/test/emqx_connector_demo.erl index 15d4a3b46..c2b0c5733 100644 --- a/apps/emqx_resource/test/emqx_connector_demo.erl +++ b/apps/emqx_resource/test/emqx_connector_demo.erl @@ -259,6 +259,9 @@ counter_loop( apply_reply(ReplyFun, ok), ?tp(connector_demo_inc_counter_async, #{n => N}), State#{counter => Num + N}; + {big_payload, _Payload, ReplyFun} when Status == blocked -> + apply_reply(ReplyFun, {error, blocked}), + State; {{FromPid, ReqRef}, {inc, N}} when Status == running -> %ct:pal("sync counter recv: ~p", [{inc, N}]), FromPid ! {ReqRef, ok}, @@ -269,6 +272,9 @@ counter_loop( {{FromPid, ReqRef}, {big_payload, _Payload}} when Status == blocked -> FromPid ! {ReqRef, incorrect_status}, State#{incorrect_status_count := IncorrectCount + 1}; + {{FromPid, ReqRef}, {big_payload, _Payload}} when Status == running -> + FromPid ! {ReqRef, ok}, + State; {get, ReplyFun} -> apply_reply(ReplyFun, Num), State; diff --git a/apps/emqx_resource/test/emqx_resource_SUITE.erl b/apps/emqx_resource/test/emqx_resource_SUITE.erl index da140489e..f71dc4bb9 100644 --- a/apps/emqx_resource/test/emqx_resource_SUITE.erl +++ b/apps/emqx_resource/test/emqx_resource_SUITE.erl @@ -411,35 +411,18 @@ t_query_counter_async_inflight(_) -> %% send async query to make the inflight window full ?check_trace( - begin - {ok, SRef} = snabbkaffe:subscribe( - ?match_event( - #{ - ?snk_kind := resource_worker_appended_to_inflight, - is_new := true - } - ), - WindowSize, - _Timeout = 5_000 + {_, {ok, _}} = + ?wait_async_action( + inc_counter_in_parallel(WindowSize, ReqOpts), + #{?snk_kind := resource_worker_flush_but_inflight_full}, + 1_000 ), - inc_counter_in_parallel(WindowSize, ReqOpts), - {ok, _} = snabbkaffe:receive_events(SRef), - ok - end, fun(Trace) -> QueryTrace = ?of_kind(call_query_async, Trace), ?assertMatch([#{query := {query, _, {inc_counter, 1}, _}} | _], QueryTrace) end ), tap_metrics(?LINE), - - %% this will block the resource_worker as the inflight window is full now - {ok, {ok, _}} = - ?wait_async_action( - emqx_resource:query(?ID, {inc_counter, 199}), - #{?snk_kind := resource_worker_flush_but_inflight_full}, - 1_000 - ), ?assertMatch(0, ets:info(Tab0, size)), tap_metrics(?LINE), @@ -464,9 +447,9 @@ t_query_counter_async_inflight(_) -> %% all responses should be received after the resource is resumed. {ok, SRef0} = snabbkaffe:subscribe( ?match_event(#{?snk_kind := connector_demo_inc_counter_async}), - %% +2 because the tmp_query above will be retried and succeed - %% this time, and there was the inc 199 request as well. - WindowSize + 2, + %% +1 because the tmp_query above will be retried and succeed + %% this time. + WindowSize + 1, _Timeout0 = 10_000 ), ?assertMatch(ok, emqx_resource:simple_sync_query(?ID, resume)), @@ -504,8 +487,12 @@ t_query_counter_async_inflight(_) -> ?assertMatch(ok, emqx_resource:simple_sync_query(?ID, block)), %% again, send async query to make the inflight window full ?check_trace( - ?TRACE_OPTS, - inc_counter_in_parallel(WindowSize, ReqOpts), + {_, {ok, _}} = + ?wait_async_action( + inc_counter_in_parallel(WindowSize, ReqOpts), + #{?snk_kind := resource_worker_flush_but_inflight_full}, + 1_000 + ), fun(Trace) -> QueryTrace = ?of_kind(call_query_async, Trace), ?assertMatch([#{query := {query, _, {inc_counter, 1}, _}} | _], QueryTrace) @@ -584,7 +571,7 @@ t_query_counter_async_inflight_batch(_) -> end, ReqOpts = fun() -> #{async_reply_fun => {Insert0, [Tab0, make_ref()]}} end, BatchSize = 2, - WindowSize = 3, + WindowSize = 15, {ok, _} = emqx_resource:create_local( ?ID, ?DEFAULT_RESOURCE_GROUP, @@ -606,16 +593,12 @@ t_query_counter_async_inflight_batch(_) -> %% send async query to make the inflight window full NumMsgs = BatchSize * WindowSize, ?check_trace( - begin - {ok, SRef} = snabbkaffe:subscribe( - ?match_event(#{?snk_kind := call_batch_query_async}), - WindowSize, - _Timeout = 60_000 + {_, {ok, _}} = + ?wait_async_action( + inc_counter_in_parallel(NumMsgs, ReqOpts), + #{?snk_kind := resource_worker_flush_but_inflight_full}, + 5_000 ), - inc_counter_in_parallel(NumMsgs, ReqOpts), - {ok, _} = snabbkaffe:receive_events(SRef), - ok - end, fun(Trace) -> QueryTrace = ?of_kind(call_batch_query_async, Trace), ?assertMatch( @@ -674,7 +657,7 @@ t_query_counter_async_inflight_batch(_) -> %% +1 because the tmp_query above will be retried and succeed %% this time. WindowSize + 1, - _Timeout = 60_000 + 10_000 ), ?assertMatch(ok, emqx_resource:simple_sync_query(?ID, resume)), tap_metrics(?LINE), @@ -695,7 +678,7 @@ t_query_counter_async_inflight_batch(_) -> {ok, SRef} = snabbkaffe:subscribe( ?match_event(#{?snk_kind := connector_demo_inc_counter_async}), NumBatches1, - _Timeout = 60_000 + 10_000 ), inc_counter_in_parallel(NumMsgs1, ReqOpts), {ok, _} = snabbkaffe:receive_events(SRef), @@ -720,8 +703,12 @@ t_query_counter_async_inflight_batch(_) -> ?assertMatch(ok, emqx_resource:simple_sync_query(?ID, block)), %% again, send async query to make the inflight window full ?check_trace( - ?TRACE_OPTS, - inc_counter_in_parallel(WindowSize, ReqOpts), + {_, {ok, _}} = + ?wait_async_action( + inc_counter_in_parallel(NumMsgs, ReqOpts), + #{?snk_kind := resource_worker_flush_but_inflight_full}, + 5_000 + ), fun(Trace) -> QueryTrace = ?of_kind(call_batch_query_async, Trace), ?assertMatch( @@ -734,11 +721,11 @@ t_query_counter_async_inflight_batch(_) -> %% this will block the resource_worker ok = emqx_resource:query(?ID, {inc_counter, 1}), - Sent = NumMsgs + NumMsgs1 + WindowSize, + Sent = NumMsgs + NumMsgs1 + NumMsgs, {ok, SRef1} = snabbkaffe:subscribe( ?match_event(#{?snk_kind := connector_demo_inc_counter_async}), WindowSize, - _Timeout = 60_000 + 10_000 ), ?assertMatch(ok, emqx_resource:simple_sync_query(?ID, resume)), {ok, _} = snabbkaffe:receive_events(SRef1), @@ -785,10 +772,8 @@ t_healthy_timeout(_) -> %% the ?TEST_RESOURCE always returns the `Mod:on_get_status/2` 300ms later. #{health_check_interval => 200} ), - ?assertMatch( - ?RESOURCE_ERROR(not_connected), - emqx_resource:query(?ID, get_state) - ), + ?assertError(timeout, emqx_resource:query(?ID, get_state, #{timeout => 1_000})), + ?assertMatch({ok, _Group, #{status := disconnected}}, emqx_resource_manager:ets_lookup(?ID)), ok = emqx_resource:remove_local(?ID). t_healthy(_) -> @@ -1131,6 +1116,7 @@ t_retry_batch(_Config) -> ok. t_delete_and_re_create_with_same_name(_Config) -> + NumBufferWorkers = 2, {ok, _} = emqx_resource:create( ?ID, ?DEFAULT_RESOURCE_GROUP, @@ -1139,7 +1125,7 @@ t_delete_and_re_create_with_same_name(_Config) -> #{ query_mode => sync, batch_size => 1, - worker_pool_size => 2, + worker_pool_size => NumBufferWorkers, queue_seg_bytes => 100, resume_interval => 1_000 } @@ -1154,19 +1140,21 @@ t_delete_and_re_create_with_same_name(_Config) -> ?assertMatch(ok, emqx_resource:simple_sync_query(?ID, block)), NumRequests = 10, {ok, SRef} = snabbkaffe:subscribe( - ?match_event(#{?snk_kind := resource_worker_appended_to_queue}), - NumRequests, + ?match_event(#{?snk_kind := resource_worker_enter_blocked}), + NumBufferWorkers, _Timeout = 5_000 ), %% ensure replayq offloads to disk Payload = binary:copy(<<"a">>, 119), lists:foreach( fun(N) -> - {error, _} = - emqx_resource:query( - ?ID, - {big_payload, <<(integer_to_binary(N))/binary, Payload/binary>>} - ) + spawn_link(fun() -> + {error, _} = + emqx_resource:query( + ?ID, + {big_payload, <<(integer_to_binary(N))/binary, Payload/binary>>} + ) + end) end, lists:seq(1, NumRequests) ), @@ -1177,10 +1165,11 @@ t_delete_and_re_create_with_same_name(_Config) -> tap_metrics(?LINE), Queuing1 = emqx_resource_metrics:queuing_get(?ID), Inflight1 = emqx_resource_metrics:inflight_get(?ID), - ?assertEqual(NumRequests - 1, Queuing1), - ?assertEqual(1, Inflight1), + ?assert(Queuing1 > 0), + ?assertEqual(2, Inflight1), %% now, we delete the resource + process_flag(trap_exit, true), ok = emqx_resource:remove_local(?ID), ?assertEqual({error, not_found}, emqx_resource_manager:lookup(?ID)), @@ -1275,9 +1264,13 @@ t_retry_sync_inflight(_Config) -> %% now really make the resource go into `blocked' state. %% this results in a retriable error when sync. ok = emqx_resource:simple_sync_query(?ID, block), - {{error, {recoverable_error, incorrect_status}}, {ok, _}} = + TestPid = self(), + {_, {ok, _}} = ?wait_async_action( - emqx_resource:query(?ID, {big_payload, <<"a">>}, QueryOpts), + spawn_link(fun() -> + Res = emqx_resource:query(?ID, {big_payload, <<"a">>}, QueryOpts), + TestPid ! {res, Res} + end), #{?snk_kind := resource_worker_retry_inflight_failed}, ResumeInterval * 2 ), @@ -1287,9 +1280,15 @@ t_retry_sync_inflight(_Config) -> #{?snk_kind := resource_worker_retry_inflight_succeeded}, ResumeInterval * 3 ), + receive + {res, Res} -> + ?assertEqual(ok, Res) + after 5_000 -> + ct:fail("no response") + end, ok end, - [fun ?MODULE:assert_retry_fail_then_succeed_inflight/1] + [fun ?MODULE:assert_sync_retry_fail_then_succeed_inflight/1] ), ok. @@ -1312,12 +1311,17 @@ t_retry_sync_inflight_batch(_Config) -> QueryOpts = #{}, ?check_trace( begin - %% now really make the resource go into `blocked' state. - %% this results in a retriable error when sync. + %% make the resource go into `blocked' state. this + %% results in a retriable error when sync. ok = emqx_resource:simple_sync_query(?ID, block), - {{error, {recoverable_error, incorrect_status}}, {ok, _}} = + process_flag(trap_exit, true), + TestPid = self(), + {_, {ok, _}} = ?wait_async_action( - emqx_resource:query(?ID, {big_payload, <<"a">>}, QueryOpts), + spawn_link(fun() -> + Res = emqx_resource:query(?ID, {big_payload, <<"a">>}, QueryOpts), + TestPid ! {res, Res} + end), #{?snk_kind := resource_worker_retry_inflight_failed}, ResumeInterval * 2 ), @@ -1327,13 +1331,19 @@ t_retry_sync_inflight_batch(_Config) -> #{?snk_kind := resource_worker_retry_inflight_succeeded}, ResumeInterval * 3 ), + receive + {res, Res} -> + ?assertEqual(ok, Res) + after 5_000 -> + ct:fail("no response") + end, ok end, - [fun ?MODULE:assert_retry_fail_then_succeed_inflight/1] + [fun ?MODULE:assert_sync_retry_fail_then_succeed_inflight/1] ), ok. -t_dont_retry_async_inflight(_Config) -> +t_retry_async_inflight(_Config) -> ResumeInterval = 1_000, emqx_connector_demo:set_callback_mode(async_if_possible), {ok, _} = emqx_resource:create( @@ -1351,33 +1361,31 @@ t_dont_retry_async_inflight(_Config) -> QueryOpts = #{}, ?check_trace( begin - %% block, - {ok, {ok, _}} = - ?wait_async_action( - emqx_resource:query(?ID, block_now), - #{?snk_kind := resource_worker_enter_blocked}, - ResumeInterval * 2 - ), + %% block + ok = emqx_resource:simple_sync_query(?ID, block), - %% then send an async request; that shouldn't be retriable. + %% then send an async request; that should be retriable. {ok, {ok, _}} = ?wait_async_action( emqx_resource:query(?ID, {big_payload, <<"b">>}, QueryOpts), - #{?snk_kind := resource_worker_flush_ack}, + #{?snk_kind := resource_worker_retry_inflight_failed}, ResumeInterval * 2 ), - %% will re-enter running because the single request is not retriable - {ok, _} = ?block_until( - #{?snk_kind := resource_worker_enter_running}, ResumeInterval * 2 - ), + %% will reply with success after the resource is healed + {ok, {ok, _}} = + ?wait_async_action( + emqx_resource:simple_sync_query(?ID, resume), + #{?snk_kind := resource_worker_enter_running}, + ResumeInterval * 2 + ), ok end, - [fun ?MODULE:assert_no_retry_inflight/1] + [fun ?MODULE:assert_async_retry_fail_then_succeed_inflight/1] ), ok. -t_dont_retry_async_inflight_batch(_Config) -> +t_retry_async_inflight_batch(_Config) -> ResumeInterval = 1_000, emqx_connector_demo:set_callback_mode(async_if_possible), {ok, _} = emqx_resource:create( @@ -1396,29 +1404,27 @@ t_dont_retry_async_inflight_batch(_Config) -> QueryOpts = #{}, ?check_trace( begin - %% block, - {ok, {ok, _}} = - ?wait_async_action( - emqx_resource:query(?ID, block_now), - #{?snk_kind := resource_worker_enter_blocked}, - ResumeInterval * 2 - ), + %% block + ok = emqx_resource:simple_sync_query(?ID, block), - %% then send an async request; that shouldn't be retriable. + %% then send an async request; that should be retriable. {ok, {ok, _}} = ?wait_async_action( emqx_resource:query(?ID, {big_payload, <<"b">>}, QueryOpts), - #{?snk_kind := resource_worker_flush_ack}, + #{?snk_kind := resource_worker_retry_inflight_failed}, ResumeInterval * 2 ), - %% will re-enter running because the single request is not retriable - {ok, _} = ?block_until( - #{?snk_kind := resource_worker_enter_running}, ResumeInterval * 2 - ), + %% will reply with success after the resource is healed + {ok, {ok, _}} = + ?wait_async_action( + emqx_resource:simple_sync_query(?ID, resume), + #{?snk_kind := resource_worker_enter_running}, + ResumeInterval * 2 + ), ok end, - [fun ?MODULE:assert_no_retry_inflight/1] + [fun ?MODULE:assert_async_retry_fail_then_succeed_inflight/1] ), ok. @@ -1529,7 +1535,8 @@ inc_counter_in_parallel(N, Opts0) -> ct:fail({wait_for_query_timeout, Pid}) end || Pid <- Pids - ]. + ], + ok. inc_counter_in_parallel_increasing(N, StartN, Opts0) -> Parent = self(), @@ -1566,12 +1573,8 @@ tap_metrics(Line) -> ct:pal("metrics (l. ~b): ~p", [Line, #{counters => C, gauges => G}]), #{counters => C, gauges => G}. -assert_no_retry_inflight(Trace) -> - ?assertEqual([], ?of_kind(resource_worker_retry_inflight_failed, Trace)), - ?assertEqual([], ?of_kind(resource_worker_retry_inflight_succeeded, Trace)), - ok. - -assert_retry_fail_then_succeed_inflight(Trace) -> +assert_sync_retry_fail_then_succeed_inflight(Trace) -> + ct:pal(" ~p", [Trace]), ?assert( ?strict_causality( #{?snk_kind := resource_worker_flush_nack, ref := _Ref}, @@ -1589,3 +1592,23 @@ assert_retry_fail_then_succeed_inflight(Trace) -> ) ), ok. + +assert_async_retry_fail_then_succeed_inflight(Trace) -> + ct:pal(" ~p", [Trace]), + ?assert( + ?strict_causality( + #{?snk_kind := resource_worker_reply_after_query, action := nack, ref := _Ref}, + #{?snk_kind := resource_worker_retry_inflight_failed, ref := _Ref}, + Trace + ) + ), + %% not strict causality because it might retry more than once + %% before restoring the resource health. + ?assert( + ?causality( + #{?snk_kind := resource_worker_retry_inflight_failed, ref := _Ref}, + #{?snk_kind := resource_worker_retry_inflight_succeeded, ref := _Ref}, + Trace + ) + ), + ok. diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_gcp_pubsub_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_gcp_pubsub_SUITE.erl index 4c20705b6..22a5dc859 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_gcp_pubsub_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_gcp_pubsub_SUITE.erl @@ -108,6 +108,7 @@ end_per_group(_Group, _Config) -> init_per_testcase(TestCase, Config0) when TestCase =:= t_publish_success_batch -> + ct:timetrap({seconds, 30}), case ?config(batch_size, Config0) of 1 -> [{skip_due_to_no_batching, true}]; @@ -120,6 +121,7 @@ init_per_testcase(TestCase, Config0) when [{telemetry_table, Tid} | Config] end; init_per_testcase(TestCase, Config0) -> + ct:timetrap({seconds, 30}), {ok, _} = start_echo_http_server(), delete_all_bridges(), Tid = install_telemetry_handler(TestCase), @@ -283,6 +285,7 @@ gcp_pubsub_config(Config) -> " pool_size = 1\n" " pipelining = ~b\n" " resource_opts = {\n" + " request_timeout = 500ms\n" " worker_pool_size = 1\n" " query_mode = ~s\n" " batch_size = ~b\n" @@ -1266,7 +1269,6 @@ t_failure_no_body(Config) -> t_unrecoverable_error(Config) -> ResourceId = ?config(resource_id, Config), - TelemetryTable = ?config(telemetry_table, Config), QueryMode = ?config(query_mode, Config), TestPid = self(), FailureNoBodyHandler = @@ -1328,26 +1330,14 @@ t_unrecoverable_error(Config) -> ok end ), - wait_telemetry_event(TelemetryTable, failed, ResourceId), - ExpectedInflightEvents = - case QueryMode of - sync -> 1; - async -> 3 - end, - wait_telemetry_event( - TelemetryTable, - inflight, - ResourceId, - #{n_events => ExpectedInflightEvents, timeout => 5_000} - ), - %% even waiting, hard to avoid flakiness... simpler to just sleep - %% a bit until stabilization. - ct:sleep(200), + + wait_until_gauge_is(queuing, 0, _Timeout = 400), + wait_until_gauge_is(inflight, 1, _Timeout = 400), assert_metrics( #{ dropped => 0, - failed => 1, - inflight => 0, + failed => 0, + inflight => 1, matched => 1, queuing => 0, retried => 0, From 4ed7bff33f9c27fe91a6924302cf7ad358d302fc Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Tue, 17 Jan 2023 14:50:04 -0300 Subject: [PATCH 0254/1802] chore: fix dialyzer warnings --- apps/emqx_resource/src/emqx_resource_worker.erl | 11 ++++++++--- apps/emqx_resource/src/emqx_resource_worker_sup.erl | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/apps/emqx_resource/src/emqx_resource_worker.erl b/apps/emqx_resource/src/emqx_resource_worker.erl index 2ef1cbed4..51a95424a 100644 --- a/apps/emqx_resource/src/emqx_resource_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_worker.erl @@ -80,6 +80,7 @@ -type from() :: pid() | reply_fun() | request_from(). -type request_from() :: undefined | gen_statem:from(). -type state() :: blocked | running. +-type inflight_key() :: integer(). -type data() :: #{ id := id(), index := index(), @@ -248,7 +249,7 @@ blocked(info, Info, _Data) -> keep_state_and_data. terminate(_Reason, #{id := Id, index := Index, queue := Q}) -> - replayq:close(Q), + _ = replayq:close(Q), emqx_resource_metrics:inflight_set(Id, Index, 0), %% since we want volatile queues, this will be 0 after %% termination. @@ -376,7 +377,8 @@ retry_inflight_sync(Ref, QueryOrBatch, Data0) -> end. %% Called during the `running' state only. --spec handle_query_requests(?SEND_REQ(request_from(), request()), data()) -> data(). +-spec handle_query_requests(?SEND_REQ(request_from(), request()), data()) -> + gen_statem:event_handler_result(state(), data()). handle_query_requests(Request0, Data0) -> {_Queries, Data} = collect_and_enqueue_query_requests(Request0, Data0), maybe_flush(Data). @@ -454,7 +456,9 @@ flush(Data0) -> -spec do_flush(data(), #{ is_batch := boolean(), batch := [?QUERY(from(), request(), boolean())], - ack_ref := replayq:ack_ref() + ack_ref := replayq:ack_ref(), + ref := inflight_key(), + new_queue := replayq:q() }) -> gen_statem:event_handler_result(state(), data()). do_flush( @@ -1176,6 +1180,7 @@ cancel_flush_timer(St = #{tref := {TRef, _Ref}}) -> _ = erlang:cancel_timer(TRef), St#{tref => undefined}. +-spec make_message_ref() -> inflight_key(). make_message_ref() -> erlang:monotonic_time(nanosecond). diff --git a/apps/emqx_resource/src/emqx_resource_worker_sup.erl b/apps/emqx_resource/src/emqx_resource_worker_sup.erl index a5de76eca..8b0ce2c65 100644 --- a/apps/emqx_resource/src/emqx_resource_worker_sup.erl +++ b/apps/emqx_resource/src/emqx_resource_worker_sup.erl @@ -67,7 +67,7 @@ stop_workers(ResId, Opts) -> WorkerPoolSize = worker_pool_size(Opts), lists:foreach( fun(Idx) -> - ensure_worker_removed(ResId, Idx), + _ = ensure_worker_removed(ResId, Idx), ensure_disk_queue_dir_absent(ResId, Idx) end, lists:seq(1, WorkerPoolSize) From 087b66726346fc8c3a92867663181cf18ff508d1 Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Tue, 17 Jan 2023 16:22:41 -0300 Subject: [PATCH 0255/1802] fix(buffer_worker): allow signalling unrecoverable errors --- apps/emqx_authz/src/emqx_authz_http.erl | 12 +- .../emqx_authz/test/emqx_authz_http_SUITE.erl | 2 +- .../src/emqx_connector_mysql.erl | 12 +- .../src/emqx_connector_pgsql.erl | 67 ++++--- .../src/emqx_connector_redis.erl | 20 +- .../test/emqx_connector_redis_SUITE.erl | 8 +- apps/emqx_resource/src/emqx_resource.erl | 7 +- .../src/emqx_resource_worker.erl | 172 +++++++++++------- .../test/emqx_ee_bridge_influxdb_SUITE.erl | 40 ++-- .../test/emqx_ee_bridge_mysql_SUITE.erl | 59 ++++-- .../test/emqx_ee_bridge_pgsql_SUITE.erl | 45 +++-- .../src/emqx_ee_connector_influxdb.erl | 8 +- 12 files changed, 279 insertions(+), 173 deletions(-) diff --git a/apps/emqx_authz/src/emqx_authz_http.erl b/apps/emqx_authz/src/emqx_authz_http.erl index a300291d1..852a667c8 100644 --- a/apps/emqx_authz/src/emqx_authz_http.erl +++ b/apps/emqx_authz/src/emqx_authz_http.erl @@ -82,7 +82,7 @@ authorize( } = Config ) -> Request = generate_request(PubSub, Topic, Client, Config), - try emqx_resource:simple_sync_query(ResourceID, {Method, Request, RequestTimeout}) of + case emqx_resource:simple_sync_query(ResourceID, {Method, Request, RequestTimeout}) of {ok, 204, _Headers} -> {matched, allow}; {ok, 200, Headers, Body} -> @@ -112,16 +112,6 @@ authorize( reason => Reason }), ignore - catch - error:timeout -> - Reason = timeout, - ?tp(authz_http_request_failure, #{error => Reason}), - ?SLOG(error, #{ - msg => "http_server_query_failed", - resource => ResourceID, - reason => Reason - }), - ignore end. log_nomtach_msg(Status, Headers, Body) -> diff --git a/apps/emqx_authz/test/emqx_authz_http_SUITE.erl b/apps/emqx_authz/test/emqx_authz_http_SUITE.erl index e5a72f680..e91da9829 100644 --- a/apps/emqx_authz/test/emqx_authz_http_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_http_SUITE.erl @@ -172,7 +172,7 @@ t_response_handling(_Config) -> [ #{ ?snk_kind := authz_http_request_failure, - error := timeout + error := {recoverable_error, econnrefused} } ], ?of_kind(authz_http_request_failure, Trace) diff --git a/apps/emqx_connector/src/emqx_connector_mysql.erl b/apps/emqx_connector/src/emqx_connector_mysql.erl index c144c48e9..066e053d4 100644 --- a/apps/emqx_connector/src/emqx_connector_mysql.erl +++ b/apps/emqx_connector/src/emqx_connector_mysql.erl @@ -192,7 +192,7 @@ on_batch_query( {Key, _} -> case maps:get(Key, Inserts, undefined) of undefined -> - {error, batch_select_not_implemented}; + {error, {unrecoverable_error, batch_select_not_implemented}}; InsertSQL -> Tokens = maps:get(Key, ParamsTokens), on_batch_insert(InstId, BatchReq, InsertSQL, Tokens, State) @@ -200,7 +200,7 @@ on_batch_query( Request -> LogMeta = #{connector => InstId, first_request => Request, state => State}, ?SLOG(error, LogMeta#{msg => "invalid request"}), - {error, invalid_request} + {error, {unrecoverable_error, invalid_request}} end. mysql_function(sql) -> @@ -267,7 +267,7 @@ init_prepare(State = #{prepare_statement := Prepares, poolname := PoolName}) -> maybe_prepare_sql(SQLOrKey, Prepares, PoolName) -> case maps:is_key(SQLOrKey, Prepares) of true -> prepare_sql(Prepares, PoolName); - false -> {error, prepared_statement_invalid} + false -> {error, {unrecoverable_error, prepared_statement_invalid}} end. prepare_sql(Prepares, PoolName) when is_map(Prepares) -> @@ -465,12 +465,12 @@ do_sql_query(SQLFunc, Conn, SQLOrKey, Data, Timeout, LogMeta) -> LogMeta#{msg => "mysql_connector_do_sql_query_failed", reason => Reason} ), {error, {recoverable_error, Reason}}; - {error, Reason} = Result -> + {error, Reason} -> ?SLOG( error, LogMeta#{msg => "mysql_connector_do_sql_query_failed", reason => Reason} ), - Result; + {error, {unrecoverable_error, Reason}}; Result -> ?tp( mysql_connector_query_return, @@ -483,5 +483,5 @@ do_sql_query(SQLFunc, Conn, SQLOrKey, Data, Timeout, LogMeta) -> error, LogMeta#{msg => "mysql_connector_invalid_params", params => Data} ), - {error, {invalid_params, Data}} + {error, {unrecoverable_error, {invalid_params, Data}}} end. diff --git a/apps/emqx_connector/src/emqx_connector_pgsql.erl b/apps/emqx_connector/src/emqx_connector_pgsql.erl index 9965ff3b4..34defb5e5 100644 --- a/apps/emqx_connector/src/emqx_connector_pgsql.erl +++ b/apps/emqx_connector/src/emqx_connector_pgsql.erl @@ -153,7 +153,8 @@ on_query( }), Type = pgsql_query_type(TypeOrKey), {NameOrSQL2, Data} = proc_sql_params(TypeOrKey, NameOrSQL, Params, State), - on_sql_query(InstId, PoolName, Type, NameOrSQL2, Data). + Res = on_sql_query(InstId, PoolName, Type, NameOrSQL2, Data), + handle_result(Res). pgsql_query_type(sql) -> query; @@ -182,23 +183,17 @@ on_batch_query( msg => "batch prepare not implemented" }, ?SLOG(error, Log), - {error, batch_prepare_not_implemented}; + {error, {unrecoverable_error, batch_prepare_not_implemented}}; TokenList -> {_, Datas} = lists:unzip(BatchReq), Datas2 = [emqx_plugin_libs_rule:proc_sql(TokenList, Data) || Data <- Datas], St = maps:get(BinKey, Sts), - {_Column, Results} = on_sql_query(InstId, PoolName, execute_batch, St, Datas2), - %% this local function only suits for the result of batch insert - TransResult = fun - Trans([{ok, Count} | T], Acc) -> - Trans(T, Acc + Count); - Trans([{error, _} = Error | _], _Acc) -> - Error; - Trans([], Acc) -> - {ok, Acc} - end, - - TransResult(Results, 0) + case on_sql_query(InstId, PoolName, execute_batch, St, Datas2) of + {error, Error} -> + {error, Error}; + {_Column, Results} -> + handle_batch_result(Results, 0) + end end; _ -> Log = #{ @@ -208,7 +203,7 @@ on_batch_query( msg => "invalid request" }, ?SLOG(error, Log), - {error, invalid_request} + {error, {unrecoverable_error, invalid_request}} end. proc_sql_params(query, SQLOrKey, Params, _State) -> @@ -225,24 +220,38 @@ proc_sql_params(TypeOrKey, SQLOrData, Params, #{params_tokens := ParamsTokens}) end. on_sql_query(InstId, PoolName, Type, NameOrSQL, Data) -> - Result = ecpool:pick_and_do(PoolName, {?MODULE, Type, [NameOrSQL, Data]}, no_handover), - case Result of - {error, Reason} -> + try ecpool:pick_and_do(PoolName, {?MODULE, Type, [NameOrSQL, Data]}, no_handover) of + {error, Reason} = Result -> + ?tp( + pgsql_connector_query_return, + #{error => Reason} + ), ?SLOG(error, #{ msg => "postgresql connector do sql query failed", connector => InstId, type => Type, sql => NameOrSQL, reason => Reason - }); - _ -> + }), + Result; + Result -> ?tp( pgsql_connector_query_return, #{result => Result} ), - ok - end, - Result. + Result + catch + error:function_clause:Stacktrace -> + ?SLOG(error, #{ + msg => "postgresql connector do sql query failed", + connector => InstId, + type => Type, + sql => NameOrSQL, + reason => function_clause, + stacktrace => Stacktrace + }), + {error, {unrecoverable_error, invalid_request}} + end. on_get_status(_InstId, #{poolname := Pool} = State) -> case emqx_plugin_libs_pool:health_check_ecpool_workers(Pool, fun ?MODULE:do_get_status/1) of @@ -407,3 +416,15 @@ to_bin(Bin) when is_binary(Bin) -> Bin; to_bin(Atom) when is_atom(Atom) -> erlang:atom_to_binary(Atom). + +handle_result({error, Error}) -> + {error, {unrecoverable_error, Error}}; +handle_result(Res) -> + Res. + +handle_batch_result([{ok, Count} | Rest], Acc) -> + handle_batch_result(Rest, Acc + Count); +handle_batch_result([{error, Error} | _Rest], _Acc) -> + {error, {unrecoverable_error, Error}}; +handle_batch_result([], Acc) -> + {ok, Acc}. diff --git a/apps/emqx_connector/src/emqx_connector_redis.erl b/apps/emqx_connector/src/emqx_connector_redis.erl index 7fdf9d28d..4bb46bca3 100644 --- a/apps/emqx_connector/src/emqx_connector_redis.erl +++ b/apps/emqx_connector/src/emqx_connector_redis.erl @@ -207,11 +207,23 @@ do_query(InstId, Query, #{poolname := PoolName, type := Type} = State) -> connector => InstId, query => Query, reason => Reason - }); + }), + case is_unrecoverable_error(Reason) of + true -> + {error, {unrecoverable_error, Reason}}; + false -> + Result + end; _ -> - ok - end, - Result. + Result + end. + +is_unrecoverable_error(Results) when is_list(Results) -> + lists:any(fun is_unrecoverable_error/1, Results); +is_unrecoverable_error({error, <<"ERR unknown command ", _/binary>>}) -> + true; +is_unrecoverable_error(_) -> + false. extract_eredis_cluster_workers(PoolName) -> lists:flatten([ diff --git a/apps/emqx_connector/test/emqx_connector_redis_SUITE.erl b/apps/emqx_connector/test/emqx_connector_redis_SUITE.erl index a1d8fe9d5..87d2b8e21 100644 --- a/apps/emqx_connector/test/emqx_connector_redis_SUITE.erl +++ b/apps/emqx_connector/test/emqx_connector_redis_SUITE.erl @@ -128,8 +128,12 @@ perform_lifecycle_check(PoolName, InitialConfig, RedisCommand) -> emqx_resource:query(PoolName, {cmds, [RedisCommand, RedisCommand]}) ), ?assertMatch( - {error, [{ok, <<"PONG">>}, {error, _}]}, - emqx_resource:query(PoolName, {cmds, [RedisCommand, [<<"INVALID_COMMAND">>]]}) + {error, {unrecoverable_error, [{ok, <<"PONG">>}, {error, _}]}}, + emqx_resource:query( + PoolName, + {cmds, [RedisCommand, [<<"INVALID_COMMAND">>]]}, + #{timeout => 500} + ) ), ?assertEqual(ok, emqx_resource:stop(PoolName)), % Resource will be listed still, but state will be changed and healthcheck will fail diff --git a/apps/emqx_resource/src/emqx_resource.erl b/apps/emqx_resource/src/emqx_resource.erl index aff66c287..0fd21bfcd 100644 --- a/apps/emqx_resource/src/emqx_resource.erl +++ b/apps/emqx_resource/src/emqx_resource.erl @@ -79,8 +79,7 @@ query/2, query/3, %% query the instance without batching and queuing messages. - simple_sync_query/2, - simple_async_query/3 + simple_sync_query/2 ]). %% Direct calls to the callback module @@ -278,10 +277,6 @@ query(ResId, Request, Opts) -> simple_sync_query(ResId, Request) -> emqx_resource_worker:simple_sync_query(ResId, Request). --spec simple_async_query(resource_id(), Request :: term(), reply_fun()) -> Result :: term(). -simple_async_query(ResId, Request, ReplyFun) -> - emqx_resource_worker:simple_async_query(ResId, Request, ReplyFun). - -spec start(resource_id()) -> ok | {error, Reason :: term()}. start(ResId) -> start(ResId, #{}). diff --git a/apps/emqx_resource/src/emqx_resource_worker.erl b/apps/emqx_resource/src/emqx_resource_worker.erl index 51a95424a..6b739a44c 100644 --- a/apps/emqx_resource/src/emqx_resource_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_worker.erl @@ -38,8 +38,7 @@ ]). -export([ - simple_sync_query/2, - simple_async_query/3 + simple_sync_query/2 ]). -export([ @@ -53,7 +52,7 @@ -export([queue_item_marshaller/1, estimate_size/1]). --export([reply_after_query/7, batch_reply_after_query/7]). +-export([reply_after_query/8, batch_reply_after_query/8]). -export([clear_disk_queue_dir/2]). @@ -121,7 +120,7 @@ simple_sync_query(Id, Request) -> %% would mess up the metrics anyway. `undefined' is ignored by %% `emqx_resource_metrics:*_shift/3'. Index = undefined, - QueryOpts = #{perform_inflight_capacity_check => false}, + QueryOpts = #{simple_query => true}, emqx_resource_metrics:matched_inc(Id), Ref = make_message_ref(), Result = call_query(sync, Id, Index, Ref, ?QUERY(self(), Request, false), QueryOpts), @@ -129,23 +128,6 @@ simple_sync_query(Id, Request) -> _ = handle_query_result(Id, Result, HasBeenSent), Result. --spec simple_async_query(id(), request(), reply_fun()) -> Result :: term(). -simple_async_query(Id, Request, ReplyFun) -> - %% Note: since calling this function implies in bypassing the - %% buffer workers, and each buffer worker index is used when - %% collecting gauge metrics, we use this dummy index. If this - %% call ends up calling buffering functions, that's a bug and - %% would mess up the metrics anyway. `undefined' is ignored by - %% `emqx_resource_metrics:*_shift/3'. - Index = undefined, - QueryOpts = #{perform_inflight_capacity_check => false}, - emqx_resource_metrics:matched_inc(Id), - Ref = make_message_ref(), - Result = call_query(async, Id, Index, Ref, ?QUERY(ReplyFun, Request, false), QueryOpts), - HasBeenSent = false, - _ = handle_query_result(Id, Result, HasBeenSent), - Result. - -spec block(pid()) -> ok. block(ServerRef) -> gen_statem:cast(ServerRef, block). @@ -334,15 +316,15 @@ retry_inflight_sync(Ref, QueryOrBatch, Data0) -> resume_interval := ResumeT } = Data0, ?tp(resource_worker_retry_inflight, #{query_or_batch => QueryOrBatch, ref => Ref}), - QueryOpts = #{}, + QueryOpts = #{simple_query => false}, Result = call_query(sync, Id, Index, Ref, QueryOrBatch, QueryOpts), ReplyResult = case QueryOrBatch of ?QUERY(From, CoreReq, HasBeenSent) -> Reply = ?REPLY(From, CoreReq, HasBeenSent, Result), - reply_caller_defer_metrics(Id, Reply); + reply_caller_defer_metrics(Id, Reply, QueryOpts); [?QUERY(_, _, _) | _] = Batch -> - batch_reply_caller_defer_metrics(Id, Result, Batch) + batch_reply_caller_defer_metrics(Id, Result, Batch, QueryOpts) end, case ReplyResult of %% Send failed because resource is down @@ -478,10 +460,10 @@ do_flush( } = Data0, %% unwrap when not batching (i.e., batch size == 1) [?QUERY(From, CoreReq, HasBeenSent) = Request] = Batch, - QueryOpts = #{inflight_tid => InflightTID}, + QueryOpts = #{inflight_tid => InflightTID, simple_query => false}, Result = call_query(configured, Id, Index, Ref, Request, QueryOpts), Reply = ?REPLY(From, CoreReq, HasBeenSent, Result), - case reply_caller(Id, Reply) of + case reply_caller(Id, Reply, QueryOpts) of %% Failed; remove the request from the queue, as we cannot pop %% from it again, but we'll retry it using the inflight table. nack -> @@ -517,7 +499,15 @@ do_flush( %% calls the corresponding callback function. Also, we %% must ensure the async worker is being monitored for %% such requests. - is_async(Id) orelse ack_inflight(InflightTID, Ref, Id, Index), + IsUnrecoverableError = is_unrecoverable_error(Result), + case is_async_return(Result) of + true when IsUnrecoverableError -> + ack_inflight(InflightTID, Ref, Id, Index); + true -> + ok; + false -> + ack_inflight(InflightTID, Ref, Id, Index) + end, {Data1, WorkerMRef} = ensure_async_worker_monitored(Data0, Result), store_async_worker_reference(InflightTID, Ref, WorkerMRef), emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), @@ -548,9 +538,9 @@ do_flush(Data0, #{ batch_size := BatchSize, inflight_tid := InflightTID } = Data0, - QueryOpts = #{inflight_tid => InflightTID}, + QueryOpts = #{inflight_tid => InflightTID, simple_query => false}, Result = call_query(configured, Id, Index, Ref, Batch, QueryOpts), - case batch_reply_caller(Id, Result, Batch) of + case batch_reply_caller(Id, Result, Batch, QueryOpts) of %% Failed; remove the request from the queue, as we cannot pop %% from it again, but we'll retry it using the inflight table. nack -> @@ -586,7 +576,15 @@ do_flush(Data0, #{ %% calls the corresponding callback function. Also, we %% must ensure the async worker is being monitored for %% such requests. - is_async(Id) orelse ack_inflight(InflightTID, Ref, Id, Index), + IsUnrecoverableError = is_unrecoverable_error(Result), + case is_async_return(Result) of + true when IsUnrecoverableError -> + ack_inflight(InflightTID, Ref, Id, Index); + true -> + ok; + false -> + ack_inflight(InflightTID, Ref, Id, Index) + end, {Data1, WorkerMRef} = ensure_async_worker_monitored(Data0, Result), store_async_worker_reference(InflightTID, Ref, WorkerMRef), emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), @@ -609,16 +607,16 @@ do_flush(Data0, #{ end end. -batch_reply_caller(Id, BatchResult, Batch) -> - {ShouldBlock, PostFn} = batch_reply_caller_defer_metrics(Id, BatchResult, Batch), +batch_reply_caller(Id, BatchResult, Batch, QueryOpts) -> + {ShouldBlock, PostFn} = batch_reply_caller_defer_metrics(Id, BatchResult, Batch, QueryOpts), PostFn(), ShouldBlock. -batch_reply_caller_defer_metrics(Id, BatchResult, Batch) -> +batch_reply_caller_defer_metrics(Id, BatchResult, Batch, QueryOpts) -> {ShouldAck, PostFns} = lists:foldl( fun(Reply, {_ShouldAck, PostFns}) -> - {ShouldAck, PostFn} = reply_caller_defer_metrics(Id, Reply), + {ShouldAck, PostFn} = reply_caller_defer_metrics(Id, Reply, QueryOpts), {ShouldAck, [PostFn | PostFns]} end, {ack, []}, @@ -629,37 +627,53 @@ batch_reply_caller_defer_metrics(Id, BatchResult, Batch) -> PostFn = fun() -> lists:foreach(fun(F) -> F() end, PostFns) end, {ShouldAck, PostFn}. -reply_caller(Id, Reply) -> - {ShouldAck, PostFn} = reply_caller_defer_metrics(Id, Reply), +reply_caller(Id, Reply, QueryOpts) -> + {ShouldAck, PostFn} = reply_caller_defer_metrics(Id, Reply, QueryOpts), PostFn(), ShouldAck. %% Should only reply to the caller when the decision is final (not %% retriable). See comment on `handle_query_result_pure'. -reply_caller_defer_metrics(Id, ?REPLY(undefined, _, HasBeenSent, Result)) -> +reply_caller_defer_metrics(Id, ?REPLY(undefined, _, HasBeenSent, Result), _QueryOpts) -> handle_query_result_pure(Id, Result, HasBeenSent); -reply_caller_defer_metrics(Id, ?REPLY({ReplyFun, Args}, _, HasBeenSent, Result)) when +reply_caller_defer_metrics(Id, ?REPLY({ReplyFun, Args}, _, HasBeenSent, Result), QueryOpts) when is_function(ReplyFun) -> + IsSimpleQuery = maps:get(simple_query, QueryOpts, false), + IsUnrecoverableError = is_unrecoverable_error(Result), {ShouldAck, PostFn} = handle_query_result_pure(Id, Result, HasBeenSent), - case {ShouldAck, Result} of - {nack, _} -> + case {ShouldAck, Result, IsUnrecoverableError, IsSimpleQuery} of + {ack, {async_return, _}, true, _} -> + apply(ReplyFun, Args ++ [Result]), ok; - {ack, {async_return, _}} -> + {ack, {async_return, _}, false, _} -> ok; - {ack, _} -> + {_, _, _, true} -> + apply(ReplyFun, Args ++ [Result]), + ok; + {nack, _, _, _} -> + ok; + {ack, _, _, _} -> apply(ReplyFun, Args ++ [Result]), ok end, {ShouldAck, PostFn}; -reply_caller_defer_metrics(Id, ?REPLY(From, _, HasBeenSent, Result)) -> +reply_caller_defer_metrics(Id, ?REPLY(From, _, HasBeenSent, Result), QueryOpts) -> + IsSimpleQuery = maps:get(simple_query, QueryOpts, false), + IsUnrecoverableError = is_unrecoverable_error(Result), {ShouldAck, PostFn} = handle_query_result_pure(Id, Result, HasBeenSent), - case {ShouldAck, Result} of - {nack, _} -> + case {ShouldAck, Result, IsUnrecoverableError, IsSimpleQuery} of + {ack, {async_return, _}, true, _} -> + gen_statem:reply(From, Result), ok; - {ack, {async_return, _}} -> + {ack, {async_return, _}, false, _} -> ok; - {ack, _} -> + {_, _, _, true} -> + gen_statem:reply(From, Result), + ok; + {nack, _, _, _} -> + ok; + {ack, _, _, _} -> gen_statem:reply(From, Result), ok end, @@ -679,7 +693,6 @@ handle_query_result(Id, Result, HasBeenSent) -> handle_query_result_pure(_Id, ?RESOURCE_ERROR_M(exception, Msg), _HasBeenSent) -> PostFn = fun() -> ?SLOG(error, #{msg => resource_exception, info => Msg}), - %% inc_sent_failed(Id, HasBeenSent), ok end, {nack, PostFn}; @@ -704,12 +717,16 @@ handle_query_result_pure(Id, ?RESOURCE_ERROR_M(stopped, Msg), _HasBeenSent) -> handle_query_result_pure(Id, ?RESOURCE_ERROR_M(Reason, _), _HasBeenSent) -> PostFn = fun() -> ?SLOG(error, #{id => Id, msg => other_resource_error, reason => Reason}), - %% emqx_resource_metrics:dropped_other_inc(Id), ok end, {nack, PostFn}; -%% TODO: invert this logic: we should differentiate errors that are -%% irrecoverable; all others are deemed recoverable. +handle_query_result_pure(Id, {error, {unrecoverable_error, Reason}}, HasBeenSent) -> + PostFn = fun() -> + ?SLOG(error, #{id => Id, msg => unrecoverable_error, reason => Reason}), + inc_sent_failed(Id, HasBeenSent), + ok + end, + {ack, PostFn}; handle_query_result_pure(Id, {error, {recoverable_error, Reason}}, _HasBeenSent) -> %% the message will be queued in replayq or inflight window, %% i.e. the counter 'queuing' or 'dropped' will increase, so we pretend that we have not @@ -725,6 +742,13 @@ handle_query_result_pure(Id, {error, Reason}, _HasBeenSent) -> ok end, {nack, PostFn}; +handle_query_result_pure(Id, {async_return, {error, {unrecoverable_error, Reason}}}, HasBeenSent) -> + PostFn = fun() -> + ?SLOG(error, #{id => Id, msg => unrecoverable_error, reason => Reason}), + inc_sent_failed(Id, HasBeenSent), + ok + end, + {ack, PostFn}; handle_query_result_pure(Id, {async_return, {error, Msg}}, _HasBeenSent) -> PostFn = fun() -> ?SLOG(error, #{id => Id, msg => async_send_error, info => Msg}), @@ -799,8 +823,8 @@ apply_query_fun(async, Mod, Id, Index, Ref, ?QUERY(_, Request, _) = Query, ResSt ?APPLY_RESOURCE( call_query_async, begin - ReplyFun = fun ?MODULE:reply_after_query/7, - Args = [self(), Id, Index, InflightTID, Ref, Query], + ReplyFun = fun ?MODULE:reply_after_query/8, + Args = [self(), Id, Index, InflightTID, Ref, Query, QueryOpts], IsRetriable = false, WorkerMRef = undefined, InflightItem = ?INFLIGHT_ITEM(Ref, Query, IsRetriable, WorkerMRef), @@ -824,8 +848,8 @@ apply_query_fun(async, Mod, Id, Index, Ref, [?QUERY(_, _, _) | _] = Batch, ResSt ?APPLY_RESOURCE( call_batch_query_async, begin - ReplyFun = fun ?MODULE:batch_reply_after_query/7, - ReplyFunAndArgs = {ReplyFun, [self(), Id, Index, InflightTID, Ref, Batch]}, + ReplyFun = fun ?MODULE:batch_reply_after_query/8, + ReplyFunAndArgs = {ReplyFun, [self(), Id, Index, InflightTID, Ref, Batch, QueryOpts]}, Requests = [Request || ?QUERY(_From, Request, _) <- Batch], IsRetriable = false, WorkerMRef = undefined, @@ -837,11 +861,15 @@ apply_query_fun(async, Mod, Id, Index, Ref, [?QUERY(_, _, _) | _] = Batch, ResSt Batch ). -reply_after_query(Pid, Id, Index, InflightTID, Ref, ?QUERY(From, Request, HasBeenSent), Result) -> +reply_after_query( + Pid, Id, Index, InflightTID, Ref, ?QUERY(From, Request, HasBeenSent), QueryOpts, Result +) -> %% NOTE: 'inflight' is the count of messages that were sent async %% but received no ACK, NOT the number of messages queued in the %% inflight window. - {Action, PostFn} = reply_caller_defer_metrics(Id, ?REPLY(From, Request, HasBeenSent, Result)), + {Action, PostFn} = reply_caller_defer_metrics( + Id, ?REPLY(From, Request, HasBeenSent, Result), QueryOpts + ), case Action of nack -> %% Keep retrying. @@ -867,11 +895,11 @@ reply_after_query(Pid, Id, Index, InflightTID, Ref, ?QUERY(From, Request, HasBee ok end. -batch_reply_after_query(Pid, Id, Index, InflightTID, Ref, Batch, Result) -> +batch_reply_after_query(Pid, Id, Index, InflightTID, Ref, Batch, QueryOpts, Result) -> %% NOTE: 'inflight' is the count of messages that were sent async %% but received no ACK, NOT the number of messages queued in the %% inflight window. - {Action, PostFn} = batch_reply_caller_defer_metrics(Id, Result, Batch), + {Action, PostFn} = batch_reply_caller_defer_metrics(Id, Result, Batch, QueryOpts), case Action of nack -> %% Keep retrying. @@ -1118,12 +1146,18 @@ do_cancel_inflight_item(Data, Ref) -> IsAcked andalso ets:update_counter(InflightTID, ?SIZE_REF, {2, -Count, 0, 0}), emqx_resource_metrics:inflight_set(Id, Index, inflight_num_msgs(InflightTID)), Result = {error, interrupted}, - _ = batch_reply_caller(Id, Result, Batch), + QueryOpts = #{simple_query => false}, + _ = batch_reply_caller(Id, Result, Batch, QueryOpts), ?tp(resource_worker_cancelled_inflight, #{ref => Ref}), ok. %%============================================================================== +inc_sent_failed(Id, _HasBeenSent = true) -> + emqx_resource_metrics:retried_failed_inc(Id); +inc_sent_failed(Id, _HasBeenSent) -> + emqx_resource_metrics:failed_inc(Id). + inc_sent_success(Id, _HasBeenSent = true) -> emqx_resource_metrics:retried_success_inc(Id); inc_sent_success(Id, _HasBeenSent) -> @@ -1204,10 +1238,14 @@ mark_as_sent(?QUERY(From, Req, _)) -> HasBeenSent = true, ?QUERY(From, Req, HasBeenSent). -is_async(ResourceId) -> - case emqx_resource_manager:ets_lookup(ResourceId) of - {ok, _Group, #{query_mode := QM, callback_mode := CM}} -> - call_mode(QM, CM) =:= async; - _ -> - false - end. +is_unrecoverable_error({error, {unrecoverable_error, _}}) -> + true; +is_unrecoverable_error({async_return, Result}) -> + is_unrecoverable_error(Result); +is_unrecoverable_error(_) -> + false. + +is_async_return({async_return, _}) -> + true; +is_async_return(_) -> + false. diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_influxdb_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_influxdb_SUITE.erl index bb87a9f37..0372c21ea 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_influxdb_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_influxdb_SUITE.erl @@ -778,15 +778,25 @@ t_bad_timestamp(Config) -> {async, false} -> ?assertEqual(ok, Return), ?assertMatch( - [#{error := [{error, {bad_timestamp, [<<"bad_timestamp">>]}}]}], + [ + #{ + error := [ + {error, {bad_timestamp, [<<"bad_timestamp">>]}} + ] + } + ], ?of_kind(influxdb_connector_send_query_error, Trace) ); {sync, false} -> ?assertEqual( - {error, [{error, {bad_timestamp, [<<"bad_timestamp">>]}}]}, Return + {error, + {unrecoverable_error, [ + {error, {bad_timestamp, [<<"bad_timestamp">>]}} + ]}}, + Return ); {sync, true} -> - ?assertEqual({error, points_trans_failed}, Return) + ?assertEqual({error, {unrecoverable_error, points_trans_failed}}, Return) end, ok end @@ -894,11 +904,19 @@ t_write_failure(Config) -> }, ?check_trace( emqx_common_test_helpers:with_failure(down, ProxyName, ProxyHost, ProxyPort, fun() -> - send_message(Config, SentData) - end), - fun(Result, _Trace) -> case QueryMode of sync -> + ?assertError(timeout, send_message(Config, SentData)); + async -> + ?assertEqual(ok, send_message(Config, SentData)) + end + end), + fun(Trace0) -> + case QueryMode of + sync -> + Trace = ?of_kind(resource_worker_flush_nack, Trace0), + ?assertMatch([_ | _], Trace), + [#{result := Result} | _] = Trace, ?assert( {error, {error, {closed, "The connection was lost."}}} =:= Result orelse {error, {error, closed}} =:= Result orelse @@ -906,7 +924,7 @@ t_write_failure(Config) -> #{got => Result} ); async -> - ?assertEqual(ok, Result) + ok end, ok end @@ -938,11 +956,7 @@ t_missing_field(Config) -> begin emqx:publish(Msg0), emqx:publish(Msg1), - NEvents = - case IsBatch of - true -> 1; - false -> 2 - end, + NEvents = 1, {ok, _} = snabbkaffe:block_until( ?match_n_events(NEvents, #{ @@ -964,7 +978,7 @@ t_missing_field(Config) -> ); false -> ?assertMatch( - [#{error := [{error, no_fields}]}, #{error := [{error, no_fields}]} | _], + [#{error := [{error, no_fields}]} | _], ?of_kind(influxdb_connector_send_query_error, Trace) ) end, diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_mysql_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_mysql_SUITE.erl index 812c4ee85..ce38c357d 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_mysql_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_mysql_SUITE.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved. +% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved. %%-------------------------------------------------------------------- -module(emqx_ee_bridge_mysql_SUITE). @@ -170,6 +170,7 @@ mysql_config(BridgeType, Config) -> " password = ~p\n" " sql = ~p\n" " resource_opts = {\n" + " request_timeout = 500ms\n" " batch_size = ~b\n" " query_mode = ~s\n" " }\n" @@ -397,20 +398,32 @@ t_write_failure(Config) -> ProxyName = ?config(proxy_name, Config), ProxyPort = ?config(proxy_port, Config), ProxyHost = ?config(proxy_host, Config), + QueryMode = ?config(query_mode, Config), {ok, _} = create_bridge(Config), Val = integer_to_binary(erlang:unique_integer()), SentData = #{payload => Val, timestamp => 1668602148000}, ?check_trace( emqx_common_test_helpers:with_failure(down, ProxyName, ProxyHost, ProxyPort, fun() -> - send_message(Config, SentData) + case QueryMode of + sync -> + ?assertError(timeout, send_message(Config, SentData)); + async -> + send_message(Config, SentData) + end end), - fun - ({error, {resource_error, _}}, _Trace) -> - ok; - ({error, {recoverable_error, disconnected}}, _Trace) -> - ok; - (_, _Trace) -> - ?assert(false) + fun(Trace0) -> + ct:pal("trace: ~p", [Trace0]), + Trace = ?of_kind(resource_worker_flush_nack, Trace0), + ?assertMatch([#{result := {error, _}} | _], Trace), + [#{result := {error, Error}} | _] = Trace, + case Error of + {resource_error, _} -> + ok; + {recoverable_error, disconnected} -> + ok; + _ -> + ct:fail("unexpected error: ~p", [Error]) + end end ), ok. @@ -424,10 +437,10 @@ t_write_timeout(Config) -> {ok, _} = create_bridge(Config), Val = integer_to_binary(erlang:unique_integer()), SentData = #{payload => Val, timestamp => 1668602148000}, - Timeout = 10, + Timeout = 1000, emqx_common_test_helpers:with_failure(timeout, ProxyName, ProxyHost, ProxyPort, fun() -> - ?assertMatch( - {error, {resource_error, _}}, + ?assertError( + timeout, query_resource(Config, {send_message, SentData, [], Timeout}) ) end), @@ -443,7 +456,7 @@ t_simple_sql_query(Config) -> BatchSize = ?config(batch_size, Config), IsBatch = BatchSize > 1, case IsBatch of - true -> ?assertEqual({error, batch_select_not_implemented}, Result); + true -> ?assertEqual({error, {unrecoverable_error, batch_select_not_implemented}}, Result); false -> ?assertEqual({ok, [<<"T">>], [[1]]}, Result) end, ok. @@ -459,10 +472,16 @@ t_missing_data(Config) -> case IsBatch of true -> ?assertMatch( - {error, {1292, _, <<"Truncated incorrect DOUBLE value: 'undefined'">>}}, Result + {error, + {unrecoverable_error, + {1292, _, <<"Truncated incorrect DOUBLE value: 'undefined'">>}}}, + Result ); false -> - ?assertMatch({error, {1048, _, <<"Column 'arrived' cannot be null">>}}, Result) + ?assertMatch( + {error, {unrecoverable_error, {1048, _, <<"Column 'arrived' cannot be null">>}}}, + Result + ) end, ok. @@ -476,8 +495,10 @@ t_bad_sql_parameter(Config) -> BatchSize = ?config(batch_size, Config), IsBatch = BatchSize > 1, case IsBatch of - true -> ?assertEqual({error, invalid_request}, Result); - false -> ?assertEqual({error, {invalid_params, [bad_parameter]}}, Result) + true -> + ?assertEqual({error, {unrecoverable_error, invalid_request}}, Result); + false -> + ?assertEqual({error, {unrecoverable_error, {invalid_params, [bad_parameter]}}}, Result) end, ok. @@ -491,8 +512,8 @@ t_unprepared_statement_query(Config) -> BatchSize = ?config(batch_size, Config), IsBatch = BatchSize > 1, case IsBatch of - true -> ?assertEqual({error, invalid_request}, Result); - false -> ?assertEqual({error, prepared_statement_invalid}, Result) + true -> ?assertEqual({error, {unrecoverable_error, invalid_request}}, Result); + false -> ?assertEqual({error, {unrecoverable_error, prepared_statement_invalid}}, Result) end, ok. diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_pgsql_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_pgsql_SUITE.erl index c2ff6fa8f..f39ecc1dc 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_pgsql_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_pgsql_SUITE.erl @@ -191,6 +191,7 @@ pgsql_config(BridgeType, Config) -> " password = ~p\n" " sql = ~p\n" " resource_opts = {\n" + " request_timeout = 500ms\n" " batch_size = ~b\n" " query_mode = ~s\n" " }\n" @@ -415,20 +416,32 @@ t_write_failure(Config) -> ProxyName = ?config(proxy_name, Config), ProxyPort = ?config(proxy_port, Config), ProxyHost = ?config(proxy_host, Config), + QueryMode = ?config(query_mode, Config), {ok, _} = create_bridge(Config), Val = integer_to_binary(erlang:unique_integer()), SentData = #{payload => Val, timestamp => 1668602148000}, ?check_trace( emqx_common_test_helpers:with_failure(down, ProxyName, ProxyHost, ProxyPort, fun() -> - send_message(Config, SentData) + case QueryMode of + sync -> + ?assertError(timeout, send_message(Config, SentData)); + async -> + send_message(Config, SentData) + end end), - fun - ({error, {resource_error, _}}, _Trace) -> - ok; - ({error, {recoverable_error, disconnected}}, _Trace) -> - ok; - (_, _Trace) -> - ?assert(false) + fun(Trace0) -> + ct:pal("trace: ~p", [Trace0]), + Trace = ?of_kind(resource_worker_flush_nack, Trace0), + ?assertMatch([#{result := {error, _}} | _], Trace), + [#{result := {error, Error}} | _] = Trace, + case Error of + {resource_error, _} -> + ok; + disconnected -> + ok; + _ -> + ct:fail("unexpected error: ~p", [Error]) + end end ), ok. @@ -442,12 +455,9 @@ t_write_timeout(Config) -> {ok, _} = create_bridge(Config), Val = integer_to_binary(erlang:unique_integer()), SentData = #{payload => Val, timestamp => 1668602148000}, - Timeout = 10, + Timeout = 1000, emqx_common_test_helpers:with_failure(timeout, ProxyName, ProxyHost, ProxyPort, fun() -> - ?assertMatch( - {error, {resource_error, _}}, - query_resource(Config, {send_message, SentData, [], Timeout}) - ) + ?assertError(timeout, query_resource(Config, {send_message, SentData, [], Timeout})) end), ok. @@ -459,7 +469,7 @@ t_simple_sql_query(Config) -> Request = {sql, <<"SELECT count(1) AS T">>}, Result = query_resource(Config, Request), case ?config(enable_batch, Config) of - true -> ?assertEqual({error, batch_prepare_not_implemented}, Result); + true -> ?assertEqual({error, {unrecoverable_error, batch_prepare_not_implemented}}, Result); false -> ?assertMatch({ok, _, [{1}]}, Result) end, ok. @@ -471,7 +481,8 @@ t_missing_data(Config) -> ), Result = send_message(Config, #{}), ?assertMatch( - {error, {error, error, <<"23502">>, not_null_violation, _, _}}, Result + {error, {unrecoverable_error, {error, error, <<"23502">>, not_null_violation, _, _}}}, + Result ), ok. @@ -484,10 +495,10 @@ t_bad_sql_parameter(Config) -> Result = query_resource(Config, Request), case ?config(enable_batch, Config) of true -> - ?assertEqual({error, invalid_request}, Result); + ?assertEqual({error, {unrecoverable_error, invalid_request}}, Result); false -> ?assertMatch( - {error, {resource_error, _}}, Result + {error, {unrecoverable_error, _}}, Result ) end, ok. diff --git a/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl b/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl index f966f56c6..0d21b381e 100644 --- a/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl +++ b/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl @@ -56,13 +56,13 @@ on_query(InstId, {send_message, Data}, _State = #{write_syntax := SyntaxLines, c #{points => Points, batch => false, mode => sync} ), do_query(InstId, Client, Points); - {error, ErrorPoints} = Err -> + {error, ErrorPoints} -> ?tp( influxdb_connector_send_query_error, #{batch => false, mode => sync, error => ErrorPoints} ), log_error_points(InstId, ErrorPoints), - Err + {error, {unrecoverable_error, ErrorPoints}} end. %% Once a Batched Data trans to points failed. @@ -80,7 +80,7 @@ on_batch_query(InstId, BatchData, _State = #{write_syntax := SyntaxLines, client influxdb_connector_send_query_error, #{batch => true, mode => sync, error => Reason} ), - {error, Reason} + {error, {unrecoverable_error, Reason}} end. on_query_async( @@ -123,7 +123,7 @@ on_batch_query_async( influxdb_connector_send_query_error, #{batch => true, mode => async, error => Reason} ), - {error, Reason} + {error, {unrecoverable_error, Reason}} end. on_get_status(_InstId, #{client := Client}) -> From 5c2ac0ac818e8fcec198fe18ea150a17f5039a66 Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Tue, 17 Jan 2023 18:21:11 -0300 Subject: [PATCH 0256/1802] chore: don't cancel inflight items upon worker death; retry them --- .../src/emqx_resource_worker.erl | 32 ++++--------------- .../test/emqx_resource_SUITE.erl | 23 ++++--------- 2 files changed, 14 insertions(+), 41 deletions(-) diff --git a/apps/emqx_resource/src/emqx_resource_worker.erl b/apps/emqx_resource/src/emqx_resource_worker.erl index 6b739a44c..698d783f6 100644 --- a/apps/emqx_resource/src/emqx_resource_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_worker.erl @@ -771,7 +771,7 @@ handle_async_worker_down(Data0, Pid) -> #{async_workers := AsyncWorkers0} = Data0, {WorkerMRef, AsyncWorkers} = maps:take(Pid, AsyncWorkers0), Data = Data0#{async_workers := AsyncWorkers}, - cancel_inflight_items(Data, WorkerMRef), + mark_inflight_items_as_retriable(Data, WorkerMRef), {keep_state, Data}. call_query(QM0, Id, Index, Ref, Query, QueryOpts) -> @@ -1118,37 +1118,19 @@ ack_inflight(InflightTID, Ref, Id, Index) -> emqx_resource_metrics:inflight_set(Id, Index, inflight_num_msgs(InflightTID)), IsAcked. -cancel_inflight_items(Data, WorkerMRef) -> +mark_inflight_items_as_retriable(Data, WorkerMRef) -> #{inflight_tid := InflightTID} = Data, + IsRetriable = true, MatchSpec = ets:fun2ms( - fun(?INFLIGHT_ITEM(Ref, _BatchOrQuery, _IsRetriable, WorkerMRef0)) when + fun(?INFLIGHT_ITEM(Ref, BatchOrQuery, _IsRetriable, WorkerMRef0)) when WorkerMRef =:= WorkerMRef0 -> - Ref + ?INFLIGHT_ITEM(Ref, BatchOrQuery, IsRetriable, WorkerMRef0) end ), - Refs = ets:select(InflightTID, MatchSpec), - lists:foreach(fun(Ref) -> do_cancel_inflight_item(Data, Ref) end, Refs). - -do_cancel_inflight_item(Data, Ref) -> - #{id := Id, index := Index, inflight_tid := InflightTID} = Data, - {Count, Batch} = - case ets:take(InflightTID, Ref) of - [?INFLIGHT_ITEM(Ref, ?QUERY(_, _, _) = Query, _IsRetriable, _WorkerMRef)] -> - {1, [Query]}; - [?INFLIGHT_ITEM(Ref, [?QUERY(_, _, _) | _] = Batch0, _IsRetriable, _WorkerMRef)] -> - {length(Batch0), Batch0}; - _ -> - {0, []} - end, - IsAcked = Count > 0, - IsAcked andalso ets:update_counter(InflightTID, ?SIZE_REF, {2, -Count, 0, 0}), - emqx_resource_metrics:inflight_set(Id, Index, inflight_num_msgs(InflightTID)), - Result = {error, interrupted}, - QueryOpts = #{simple_query => false}, - _ = batch_reply_caller(Id, Result, Batch, QueryOpts), - ?tp(resource_worker_cancelled_inflight, #{ref => Ref}), + _NumAffected = ets:select_replace(InflightTID, MatchSpec), + ?tp(resource_worker_worker_down_update, #{num_affected => _NumAffected}), ok. %%============================================================================== diff --git a/apps/emqx_resource/test/emqx_resource_SUITE.erl b/apps/emqx_resource/test/emqx_resource_SUITE.erl index f71dc4bb9..97bc8da66 100644 --- a/apps/emqx_resource/test/emqx_resource_SUITE.erl +++ b/apps/emqx_resource/test/emqx_resource_SUITE.erl @@ -1432,6 +1432,7 @@ t_retry_async_inflight_batch(_Config) -> %% requests if they die. t_async_pool_worker_death(_Config) -> ResumeInterval = 1_000, + NumBufferWorkers = 2, emqx_connector_demo:set_callback_mode(async_if_possible), {ok, _} = emqx_resource:create( ?ID, @@ -1441,7 +1442,7 @@ t_async_pool_worker_death(_Config) -> #{ query_mode => async, batch_size => 1, - worker_pool_size => 2, + worker_pool_size => NumBufferWorkers, resume_interval => ResumeInterval } ), @@ -1471,9 +1472,9 @@ t_async_pool_worker_death(_Config) -> %% grab one of the worker pids and kill it {ok, SRef1} = snabbkaffe:subscribe( - ?match_event(#{?snk_kind := resource_worker_cancelled_inflight}), - NumReqs, - 1_000 + ?match_event(#{?snk_kind := resource_worker_worker_down_update}), + NumBufferWorkers, + 10_000 ), {ok, #{pid := Pid0}} = emqx_resource:simple_sync_query(?ID, get_state), MRef = monitor(process, Pid0), @@ -1487,21 +1488,11 @@ t_async_pool_worker_death(_Config) -> ct:fail("worker should have died") end, - %% inflight requests should have been cancelled + %% inflight requests should have been marked as retriable {ok, _} = snabbkaffe:receive_events(SRef1), Inflight1 = emqx_resource_metrics:inflight_get(?ID), - ?assertEqual(0, Inflight1), + ?assertEqual(NumReqs, Inflight1), - ?assert( - lists:all( - fun - ({_, {error, interrupted}}) -> true; - (_) -> false - end, - ets:tab2list(Tab0) - ), - #{tab => ets:tab2list(Tab0)} - ), ok end, [] From b8a92b7371fa0a4bbbd7dddcc1a1736f7e7656f0 Mon Sep 17 00:00:00 2001 From: Rory Z Date: Wed, 18 Jan 2023 09:56:21 +0800 Subject: [PATCH 0257/1802] fix(helm): fix error environment variables --- .../emqx-enterprise/templates/StatefulSet.yaml | 14 +++++++------- deploy/charts/emqx/templates/StatefulSet.yaml | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/deploy/charts/emqx-enterprise/templates/StatefulSet.yaml b/deploy/charts/emqx-enterprise/templates/StatefulSet.yaml index 5f259dc35..0917cadac 100644 --- a/deploy/charts/emqx-enterprise/templates/StatefulSet.yaml +++ b/deploy/charts/emqx-enterprise/templates/StatefulSet.yaml @@ -98,15 +98,15 @@ spec: {{- end }} ports: - name: mqtt - containerPort: {{ .Values.emqxConfig.EMQX_LISTENERS__TCP__DEFAULT | default 1883 }} + containerPort: {{ .Values.emqxConfig.EMQX_LISTENERS__TCP__DEFAULT__BIND | default 1883 }} - name: mqttssl - containerPort: {{ .Values.emqxConfig.EMQX_LISTENERS__SSL__DEFAULT | default 8883 }} + containerPort: {{ .Values.emqxConfig.EMQX_LISTENERS__SSL__DEFAULT__BIND | default 8883 }} - name: ws - containerPort: {{ .Values.emqxConfig.EMQX_LISTENERS__WS__DEFAULT | default 8083 }} + containerPort: {{ .Values.emqxConfig.EMQX_LISTENERS__WS__DEFAULT__BIND | default 8083 }} - name: wss - containerPort: {{ .Values.emqxConfig.EMQX_LISTENERS__WSS__DEFAULT | default 8084 }} + containerPort: {{ .Values.emqxConfig.EMQX_LISTENERS__WSS__DEFAULT__BIND | default 8084 }} - name: dashboard - containerPort: {{ .Values.emqxConfig.EMQX_DASHBOARD__LISTENER__HTTP | default 18083 }} + containerPort: {{ .Values.emqxConfig.EMQX_DASHBOARD__LISTENER__HTTP__BIND | default 18083 }} {{- if not (empty .Values.emqxConfig.EMQX_LISTENERS__TCP__DEFAULT) }} - name: internalmqtt containerPort: {{ .Values.emqxConfig.EMQX_LISTENERS__TCP__DEFAULT }} @@ -143,14 +143,14 @@ spec: readinessProbe: httpGet: path: /status - port: {{ .Values.emqxConfig.EMQX_DASHBOARD__LISTENER__HTTP | default 18083 }} + port: {{ .Values.emqxConfig.EMQX_DASHBOARD__LISTENER__HTTP__BIND | default 18083 }} initialDelaySeconds: 10 periodSeconds: 5 failureThreshold: 30 livenessProbe: httpGet: path: /status - port: {{ .Values.emqxConfig.EMQX_DASHBOARD__LISTENER__HTTP | default 18083 }} + port: {{ .Values.emqxConfig.EMQX_DASHBOARD__LISTENER__HTTP__BIND | default 18083 }} initialDelaySeconds: 60 periodSeconds: 30 failureThreshold: 10 diff --git a/deploy/charts/emqx/templates/StatefulSet.yaml b/deploy/charts/emqx/templates/StatefulSet.yaml index 5f259dc35..0917cadac 100644 --- a/deploy/charts/emqx/templates/StatefulSet.yaml +++ b/deploy/charts/emqx/templates/StatefulSet.yaml @@ -98,15 +98,15 @@ spec: {{- end }} ports: - name: mqtt - containerPort: {{ .Values.emqxConfig.EMQX_LISTENERS__TCP__DEFAULT | default 1883 }} + containerPort: {{ .Values.emqxConfig.EMQX_LISTENERS__TCP__DEFAULT__BIND | default 1883 }} - name: mqttssl - containerPort: {{ .Values.emqxConfig.EMQX_LISTENERS__SSL__DEFAULT | default 8883 }} + containerPort: {{ .Values.emqxConfig.EMQX_LISTENERS__SSL__DEFAULT__BIND | default 8883 }} - name: ws - containerPort: {{ .Values.emqxConfig.EMQX_LISTENERS__WS__DEFAULT | default 8083 }} + containerPort: {{ .Values.emqxConfig.EMQX_LISTENERS__WS__DEFAULT__BIND | default 8083 }} - name: wss - containerPort: {{ .Values.emqxConfig.EMQX_LISTENERS__WSS__DEFAULT | default 8084 }} + containerPort: {{ .Values.emqxConfig.EMQX_LISTENERS__WSS__DEFAULT__BIND | default 8084 }} - name: dashboard - containerPort: {{ .Values.emqxConfig.EMQX_DASHBOARD__LISTENER__HTTP | default 18083 }} + containerPort: {{ .Values.emqxConfig.EMQX_DASHBOARD__LISTENER__HTTP__BIND | default 18083 }} {{- if not (empty .Values.emqxConfig.EMQX_LISTENERS__TCP__DEFAULT) }} - name: internalmqtt containerPort: {{ .Values.emqxConfig.EMQX_LISTENERS__TCP__DEFAULT }} @@ -143,14 +143,14 @@ spec: readinessProbe: httpGet: path: /status - port: {{ .Values.emqxConfig.EMQX_DASHBOARD__LISTENER__HTTP | default 18083 }} + port: {{ .Values.emqxConfig.EMQX_DASHBOARD__LISTENER__HTTP__BIND | default 18083 }} initialDelaySeconds: 10 periodSeconds: 5 failureThreshold: 30 livenessProbe: httpGet: path: /status - port: {{ .Values.emqxConfig.EMQX_DASHBOARD__LISTENER__HTTP | default 18083 }} + port: {{ .Values.emqxConfig.EMQX_DASHBOARD__LISTENER__HTTP__BIND | default 18083 }} initialDelaySeconds: 60 periodSeconds: 30 failureThreshold: 10 From faf5916ed6878111915aafecb3eee90a41e167a2 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Wed, 18 Jan 2023 07:52:28 +0100 Subject: [PATCH 0258/1802] test: relax recoverable/unrecoverable error check for now, treat all other errors unrecoverable --- apps/emqx_resource/src/emqx_resource_worker.erl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apps/emqx_resource/src/emqx_resource_worker.erl b/apps/emqx_resource/src/emqx_resource_worker.erl index 698d783f6..6b923f19d 100644 --- a/apps/emqx_resource/src/emqx_resource_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_worker.erl @@ -1222,8 +1222,15 @@ mark_as_sent(?QUERY(From, Req, _)) -> is_unrecoverable_error({error, {unrecoverable_error, _}}) -> true; +is_unrecoverable_error({error, {recoverable_error, _}}) -> + false; is_unrecoverable_error({async_return, Result}) -> is_unrecoverable_error(Result); +is_unrecoverable_error({error, _}) -> + %% TODO: delete this clause. + %% Ideally all errors except for 'unrecoverable_error' should be + %% retried, including DB schema errors. + true; is_unrecoverable_error(_) -> false. From 0d852d9122e0edced20fc9a29e34cdcdcbb120d1 Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Wed, 18 Jan 2023 16:20:21 +0800 Subject: [PATCH 0259/1802] docs: improve the dashboard's document --- .../i18n/emqx_dashboard_api_i18n.conf | 10 +-- .../i18n/emqx_dashboard_i18n.conf | 61 +++++++++++-------- .../src/emqx_dashboard_schema.erl | 4 +- 3 files changed, 41 insertions(+), 34 deletions(-) diff --git a/apps/emqx_dashboard/i18n/emqx_dashboard_api_i18n.conf b/apps/emqx_dashboard/i18n/emqx_dashboard_api_i18n.conf index 602af24c7..9a6390a02 100644 --- a/apps/emqx_dashboard/i18n/emqx_dashboard_api_i18n.conf +++ b/apps/emqx_dashboard/i18n/emqx_dashboard_api_i18n.conf @@ -31,7 +31,7 @@ emqx_dashboard_api { license { desc { en: """EMQX License. opensource or enterprise""" - zh: """EMQX 许可。开源版本 或者企业版""" + zh: """EMQX 许可类型。可为 opensource 或 enterprise""" } } @@ -44,15 +44,15 @@ emqx_dashboard_api { login_api { desc { - en: """Dashboard Auth. Get Token""" - zh: """Dashboard 认证。获取 Token""" + en: """Get Dashboard Auth Token.""" + zh: """获取 Dashboard 认证 Token。""" } } login_success { desc { - en: """Dashboard Auth. Success""" - zh: """Dashboard 认证。成功""" + en: """Dashboard Auth Success""" + zh: """Dashboard 认证成功""" } } diff --git a/apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf b/apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf index 872cfdf26..5b782dd85 100644 --- a/apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf +++ b/apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf @@ -8,7 +8,10 @@ For example, an HTTP listener can listen on all configured IP addresses on a given port for a machine by specifying the IP address 0.0.0.0. Alternatively, the HTTP listener can specify a unique IP address for each listener, but use the same port.""" - zh: """仪表盘监听器设置。""" + zh: """Dashboard 监听器设置。监听器必须有唯一的端口号和IP地址的组合。 +例如,可以通过指定IP地址 0.0.0.0 来监听机器上给定端口上的所有配置的IP地址。 +或者,可以为每个监听器指定唯一的IP地址,但使用相同的端口。 +""" } label { en: "Listeners" @@ -18,14 +21,14 @@ but use the same port.""" sample_interval { desc { en: """How often to update metrics displayed in the dashboard. -Note: `sample_interval` should be a divisor of 60.""" - zh: """更新仪表板中显示的指标的时间间隔。必须小于60,且被60的整除。""" +Note: `sample_interval` should be a divisor of 60, default is 10s.""" + zh: """Dashboard 中图表指标的时间间隔。必须小于60,且被60的整除,默认设置 10s。""" } } token_expired_time { desc { - en: "JWT token expiration time." - zh: "JWT token 过期时间" + en: "JWT token expiration time. Default is 60 minutes" + zh: "JWT token 过期时间。默认设置为 60 分钟。" } label { en: "Token expired time" @@ -34,8 +37,8 @@ Note: `sample_interval` should be a divisor of 60.""" } num_acceptors { desc { - en: "Socket acceptor pool size for TCP protocols." - zh: "TCP协议的Socket acceptor池大小" + en: "Socket acceptor pool size for TCP protocols. default is the number of schedulers online" + zh: "TCP协议的Socket acceptor池大小, 默认设置在线的调度器数量(通常为 CPU 核数)" } label { en: "Number of acceptors" @@ -45,7 +48,7 @@ Note: `sample_interval` should be a divisor of 60.""" max_connections { desc { en: "Maximum number of simultaneous connections." - zh: "同时处理的最大连接数" + zh: "同时处理的最大连接数。" } label { en: "Maximum connections" @@ -55,7 +58,7 @@ Note: `sample_interval` should be a divisor of 60.""" backlog { desc { en: "Defines the maximum length that the queue of pending connections can grow to." - zh: "排队等待连接的队列的最大长度" + zh: "排队等待连接的队列的最大长度。" } label { en: "Backlog" @@ -65,7 +68,7 @@ Note: `sample_interval` should be a divisor of 60.""" send_timeout { desc { en: "Send timeout for the socket." - zh: "Socket发送超时时间" + zh: "Socket发送超时时间。" } label { en: "Send timeout" @@ -75,7 +78,7 @@ Note: `sample_interval` should be a divisor of 60.""" inet6 { desc { en: "Enable IPv6 support, default is false, which means IPv4 only." - zh: "启用IPv6, 如果机器不支持IPv6,请关闭此选项,否则会导致仪表盘无法使用。" + zh: "启用IPv6, 如果机器不支持IPv6,请关闭此选项,否则会导致 Dashboard 无法使用。" } label { en: "IPv6" @@ -84,7 +87,8 @@ Note: `sample_interval` should be a divisor of 60.""" } ipv6_v6only { desc { - en: "Disable IPv4-to-IPv6 mapping for the listener." + en: """Disable IPv4-to-IPv6 mapping for the listener. +The configuration is only valid when the inet6 is true.""" zh: "当开启 inet6 功能的同时禁用 IPv4-to-IPv6 映射。该配置仅在 inet6 功能开启时有效。" } label { @@ -95,17 +99,17 @@ Note: `sample_interval` should be a divisor of 60.""" desc_dashboard { desc { en: "Configuration for EMQX dashboard." - zh: "EMQX仪表板配置" + zh: "EMQX Dashboard 配置。" } label { en: "Dashboard" - zh: "仪表板" + zh: "Dashboard" } } desc_listeners { desc { en: "Configuration for the dashboard listener." - zh: "仪表板监听器配置" + zh: "Dashboard 监听器配置。" } label { en: "Listeners" @@ -115,7 +119,7 @@ Note: `sample_interval` should be a divisor of 60.""" desc_http { desc { en: "Configuration for the dashboard listener (plaintext)." - zh: "仪表板监听器(HTTP)配置" + zh: "Dashboard 监听器(HTTP)配置。" } label { en: "HTTP" @@ -125,7 +129,7 @@ Note: `sample_interval` should be a divisor of 60.""" desc_https { desc { en: "Configuration for the dashboard listener (TLS)." - zh: "仪表板监听器(HTTPS)配置" + zh: "Dashboard 监听器(HTTPS)配置。" } label { en: "HTTPS" @@ -135,7 +139,7 @@ Note: `sample_interval` should be a divisor of 60.""" listener_enable { desc { en: "Ignore or enable this listener" - zh: "忽略或启用该监听器配置" + zh: "忽略或启用该监听器。" } label { en: "Enable" @@ -145,7 +149,7 @@ Note: `sample_interval` should be a divisor of 60.""" bind { desc { en: "Port without IP(18083) or port with specified IP(127.0.0.1:18083)." - zh: "监听的地址与端口,在dashboard更新此配置时,会重启dashboard服务。" + zh: "监听地址和端口,热更新此配置时,会重启 Dashboard 服务。" } label { en: "Bind" @@ -155,7 +159,7 @@ Note: `sample_interval` should be a divisor of 60.""" default_username { desc { en: "The default username of the automatically created dashboard user." - zh: "默认的仪表板用户名" + zh: "Dashboard 的默认用户名。" } label { en: "Default username" @@ -165,9 +169,12 @@ Note: `sample_interval` should be a divisor of 60.""" default_password { desc { en: """The initial default password for dashboard 'admin' user. -For safety, it should be changed as soon as possible.""" - zh: """默认的仪表板用户密码 -为了安全,应该尽快修改密码。""" +For safety, it should be changed as soon as possible. +This value is not valid when you log in to Dashboard for the first time via the web +and change to a complex password as prompted. +""" + zh: """Dashboard 的默认密码,为了安全,应该尽快修改密码。 +当通过网页首次登录 Dashboard 并按提示修改成复杂密码后,此值就会失效。""" } label { en: "Default password" @@ -179,7 +186,7 @@ For safety, it should be changed as soon as possible.""" en: """Support Cross-Origin Resource Sharing (CORS). Allows a server to indicate any origins (domain, scheme, or port) other than its own from which a browser should permit loading resources.""" - zh: """支持跨域资源共享(CORS) + zh: """支持跨域资源共享(CORS), 允许服务器指示任何来源(域名、协议或端口),除了本服务器之外的任何浏览器应允许加载资源。""" } label { @@ -190,7 +197,7 @@ its own from which a browser should permit loading resources.""" i18n_lang { desc { en: "Internationalization language support." - zh: "swagger多语言支持" + zh: "设置 Swagger 多语言的版本,可为 en 或 zh。" } label { en: "I18n language" @@ -199,8 +206,8 @@ its own from which a browser should permit loading resources.""" } bootstrap_users_file { desc { - en: "Deprecated, use api_key.bootstrap_file" - zh: "已废弃,请使用 api_key.bootstrap_file" + en: "Deprecated, use api_key.bootstrap_file." + zh: "已废弃,请使用 api_key.bootstrap_file。" } label { en: """Deprecated""" diff --git a/apps/emqx_dashboard/src/emqx_dashboard_schema.erl b/apps/emqx_dashboard/src/emqx_dashboard_schema.erl index 6742032d5..90a9742cd 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_schema.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_schema.erl @@ -117,7 +117,7 @@ common_listener_fields() -> ?HOCON( integer(), #{ - default => 4, + default => erlang:system_info(schedulers_online), desc => ?DESC(num_acceptors) } )}, @@ -141,7 +141,7 @@ common_listener_fields() -> ?HOCON( emqx_schema:duration(), #{ - default => "5s", + default => "10s", desc => ?DESC(send_timeout) } )}, From fb84d5b8176ab9d6d5f517844abfc49b9d64307b Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Wed, 18 Jan 2023 17:06:46 +0800 Subject: [PATCH 0260/1802] chore: make spellcheck happy --- apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf b/apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf index 5b782dd85..9f6e2e6b0 100644 --- a/apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf +++ b/apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf @@ -37,7 +37,7 @@ Note: `sample_interval` should be a divisor of 60, default is 10s.""" } num_acceptors { desc { - en: "Socket acceptor pool size for TCP protocols. default is the number of schedulers online" + en: "Socket acceptor pool size for TCP protocols. Default is the number of schedulers online" zh: "TCP协议的Socket acceptor池大小, 默认设置在线的调度器数量(通常为 CPU 核数)" } label { From 42182279b7296f60cd926304bbf3a97ecfdf7466 Mon Sep 17 00:00:00 2001 From: Erik Timan Date: Wed, 18 Jan 2023 10:20:41 +0100 Subject: [PATCH 0261/1802] fix(emqx_management): ensure trace file dir is deleted on zip exception --- apps/emqx_management/src/emqx_mgmt_api_trace.erl | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/apps/emqx_management/src/emqx_mgmt_api_trace.erl b/apps/emqx_management/src/emqx_mgmt_api_trace.erl index 5ad04a68c..cc4a905a4 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_trace.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_trace.erl @@ -471,10 +471,15 @@ download_trace_log(get, #{bindings := #{name := Name}, query_string := Query}) - %% Write files to ZipDir and create an in-memory zip file Zips = group_trace_file(ZipDir, TraceLog, TraceFiles), ZipName = binary_to_list(Name) ++ ".zip", - {ok, {ZipName, Binary}} = zip:zip(ZipName, Zips, [memory, {cwd, ZipDir}]), - %% emqx_trace:delete_files_after_send(ZipFileName, Zips), - %% TODO use file replace file_binary.(delete file after send is not ready now). - ok = file:del_dir_r(ZipDir), + Binary = + try + {ok, {ZipName, Bin}} = zip:zip(ZipName, Zips, [memory, {cwd, ZipDir}]), + Bin + after + %% emqx_trace:delete_files_after_send(ZipFileName, Zips), + %% TODO use file replace file_binary.(delete file after send is not ready now). + ok = file:del_dir_r(ZipDir) + end, ?tp(trace_api_download_trace_log, #{ files => Zips, name => Name, From 3891aeb5fcbb0ccc880ac3695eeff716cd6333b3 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Wed, 18 Jan 2023 09:49:14 +0100 Subject: [PATCH 0262/1802] ci(ct/run.sh): best-effort file ownership restore --- scripts/ct/run.sh | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/scripts/ct/run.sh b/scripts/ct/run.sh index 3a7b40317..07d45efe1 100755 --- a/scripts/ct/run.sh +++ b/scripts/ct/run.sh @@ -169,11 +169,6 @@ else export UID_GID="$ORIG_UID_GID" fi -if [ "$STOP" = 'no' ]; then - # shellcheck disable=2086 # no quotes for F_OPTIONS - docker-compose $F_OPTIONS up -d --build --remove-orphans -fi - # /emqx is where the source dir is mounted to the Erlang container # in .ci/docker-compose-file/docker-compose.yaml TTY='' @@ -181,18 +176,29 @@ if [[ -t 1 ]]; then TTY='-t' fi +function restore_ownership { + if ! sudo chown -R "$ORIG_UID_GID" . >/dev/null 2>&1; then + docker exec -i $TTY -u root:root "$ERLANG_CONTAINER" bash -c "chown -R $ORIG_UID_GID /emqx" >/dev/null 2>&1 || true + fi +} + +restore_ownership +trap restore_ownership EXIT + + +if [ "$STOP" = 'no' ]; then + # some left-over log file has to be deleted before a new docker-compose up + rm -f '.ci/docker-compose-file/redis/*.log' + # shellcheck disable=2086 # no quotes for F_OPTIONS + docker-compose $F_OPTIONS up -d --build --remove-orphans +fi + echo "Fixing file owners and permissions for $UID_GID" # rebar and hex cache directory need to be writable by $UID docker exec -i $TTY -u root:root "$ERLANG_CONTAINER" bash -c "mkdir -p /.cache && chown $UID_GID /.cache && chown -R $UID_GID /emqx" # need to initialize .erlang.cookie manually here because / is not writable by $UID docker exec -i $TTY -u root:root "$ERLANG_CONTAINER" bash -c "openssl rand -base64 16 > /.erlang.cookie && chown $UID_GID /.erlang.cookie && chmod 0400 /.erlang.cookie" -restore_ownership() { - if [[ "$ORIG_UID_GID" != "$UID_GID" ]]; then - docker exec -i $TTY -u root:root "$ERLANG_CONTAINER" bash -c "chown -R $ORIG_UID_GID /emqx" - fi -} - if [ "$ONLY_UP" = 'yes' ]; then exit 0 fi @@ -204,10 +210,8 @@ if [ "$STOP" = 'yes' ]; then docker-compose $F_OPTIONS down --remove-orphans elif [ "$ATTACH" = 'yes' ]; then docker exec -it "$ERLANG_CONTAINER" bash - restore_ownership elif [ "$CONSOLE" = 'yes' ]; then docker exec -e PROFILE="$PROFILE" -i $TTY "$ERLANG_CONTAINER" bash -c "make run" - restore_ownership else if [ -z "${REBAR3CT:-}" ]; then docker exec -e IS_CI="$IS_CI" -e PROFILE="$PROFILE" -i $TTY "$ERLANG_CONTAINER" bash -c "BUILD_WITHOUT_QUIC=1 make ${WHICH_APP}-ct" From 3872c4451fb0c9a64a7fb470a06211a6773c3a25 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Wed, 18 Jan 2023 10:04:38 +0100 Subject: [PATCH 0263/1802] fix(kafka): olp fix was accidentally deleted in 0fd8880d0a --- .../src/kafka/emqx_bridge_impl_kafka_producer.erl | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/lib-ee/emqx_ee_bridge/src/kafka/emqx_bridge_impl_kafka_producer.erl b/lib-ee/emqx_ee_bridge/src/kafka/emqx_bridge_impl_kafka_producer.erl index d7b4de0c0..bec3c49fa 100644 --- a/lib-ee/emqx_ee_bridge/src/kafka/emqx_bridge_impl_kafka_producer.erl +++ b/lib-ee/emqx_ee_bridge/src/kafka/emqx_bridge_impl_kafka_producer.erl @@ -253,10 +253,14 @@ producers_config(BridgeName, ClientId, Input) -> mode := BufferMode, per_partition_limit := PerPartitionLimit, segment_bytes := SegmentBytes, - memory_overload_protection := MemOLP + memory_overload_protection := MemOLP0 } } = Input, - + MemOLP = + case os:type() of + {unix, linux} -> MemOLP0; + _ -> false + end, {OffloadMode, ReplayqDir} = case BufferMode of memory -> {false, false}; @@ -268,7 +272,7 @@ producers_config(BridgeName, ClientId, Input) -> ResourceID = emqx_bridge_resource:resource_id(BridgeType, BridgeName), #{ name => make_producer_name(BridgeName), - partitioner => PartitionStrategy, + partitioner => partitioner(PartitionStrategy), partition_count_refresh_interval_seconds => PCntRefreshInterval, replayq_dir => ReplayqDir, replayq_offload_mode => OffloadMode, @@ -282,6 +286,11 @@ producers_config(BridgeName, ClientId, Input) -> telemetry_meta_data => #{bridge_id => ResourceID} }. +%% Wolff API is a batch API. +%% key_dispatch only looks at the first element, so it's named 'first_key_dispatch' +partitioner(random) -> random; +partitioner(key_dispatch) -> first_key_dispatch. + replayq_dir(ClientId) -> filename:join([emqx:data_dir(), "kafka", ClientId]). From dcb2fd2daf6d734b7650c0dc362823263ed72a67 Mon Sep 17 00:00:00 2001 From: Ivan Dyachkov Date: Wed, 18 Jan 2023 10:37:42 +0100 Subject: [PATCH 0264/1802] docs: update docker documentation - loaded plugins and loaded modules are not applicable in 5.0 - add information on how to correctly persist configuration in `/etc/` - remove --platform arg from Dockerfile - update documentation links in charts readme --- deploy/charts/emqx/README.md | 7 +- deploy/docker/Dockerfile | 3 +- deploy/docker/Dockerfile.alpine | 3 +- deploy/docker/README.md | 136 +++----------------------------- 4 files changed, 17 insertions(+), 132 deletions(-) diff --git a/deploy/charts/emqx/README.md b/deploy/charts/emqx/README.md index b07bf35ae..1a2090320 100644 --- a/deploy/charts/emqx/README.md +++ b/deploy/charts/emqx/README.md @@ -102,10 +102,9 @@ The following table lists the configurable [EMQX](https://www.emqx.io/)-specific default values. Parameter | Description | Default Value --- | --- | --- -`emqxConfig` | Map of [configuration](https://www.emqx.io/docs/en/latest/configuration/configuration.html) items -expressed as [environment variables](https://www.emqx.io/docs/en/v4.3/configuration/environment-variable.html) (prefix -can be omitted) or using the configuration -files [namespaced dotted notation](https://www.emqx.io/docs/en/latest/configuration/configuration.html) | `nil` +`emqxConfig` | Map of [configuration](https://www.emqx.io/docs/en/v5.0/admin/cfg.html) items +expressed as [environment variables](https://www.emqx.io/docs/en/v5.0/admin/cfg.html#environment-variables) (prefix `EMQX_` can be omitted) or using the configuration +files [namespaced dotted notation](https://www.emqx.io/docs/en/v5.0/admin/cfg.html#syntax) | `nil` `emqxLicenseSecretName` | Name of the secret that holds the license information | `nil` ## SSL settings diff --git a/deploy/docker/Dockerfile b/deploy/docker/Dockerfile index 03533eec4..4a00c68fb 100644 --- a/deploy/docker/Dockerfile +++ b/deploy/docker/Dockerfile @@ -1,7 +1,6 @@ ARG BUILD_FROM=ghcr.io/emqx/emqx-builder/5.0-26:1.13.4-24.3.4.2-1-debian11 ARG RUN_FROM=debian:11-slim -ARG BUILDPLATFORM=linux/amd64 -FROM --platform=$BUILDPLATFORM ${BUILD_FROM} AS builder +FROM ${BUILD_FROM} AS builder COPY . /emqx diff --git a/deploy/docker/Dockerfile.alpine b/deploy/docker/Dockerfile.alpine index ebce2f539..0f72be9ab 100644 --- a/deploy/docker/Dockerfile.alpine +++ b/deploy/docker/Dockerfile.alpine @@ -1,7 +1,6 @@ ARG BUILD_FROM=ghcr.io/emqx/emqx-builder/5.0-26:1.13.4-24.3.4.2-1-alpine3.15.1 ARG RUN_FROM=alpine:3.15.1 -ARG BUILDPLATFORM=linux/amd64 -FROM --platform=$BUILDPLATFORM ${BUILD_FROM} AS builder +FROM ${BUILD_FROM} AS builder RUN apk add --no-cache \ autoconf \ diff --git a/deploy/docker/README.md b/deploy/docker/README.md index c6ffb2559..e6067a203 100644 --- a/deploy/docker/README.md +++ b/deploy/docker/README.md @@ -48,7 +48,7 @@ The EMQX broker runs as Linux user `emqx` in the docker container. ### Configuration -Use the environment variable to configure the EMQX docker container. +All EMQX Configuration in [`etc/emqx.conf`](https://github.com/emqx/emqx/blob/master/apps/emqx/etc/emqx.conf) can be configured via environment variables. By default, the environment variables with `EMQX_` prefix are mapped to key-value pairs in configuration files. @@ -70,142 +70,28 @@ If `HOCON_ENV_OVERRIDE_PREFIX=DEV_` is set: ```bash DEV_LISTENER__SSL__EXTERNAL__ACCEPTORS <--> listener.ssl.external.acceptors DEV_MQTT__MAX_PACKET_SIZE <--> mqtt.max_packet_size +DEV_LISTENERS__TCP__DEFAULT__BIND <--> listeners.tcp.default.bind ``` -Non mapped environment variables: +For example, set MQTT TCP port to 1883 -```bash -EMQX_NAME -EMQX_HOST +```console +$ docker run -d --name emqx -e DEV_LISTENERS__TCP__DEFAULT__BIND=1883 -p 18083:18083 -p 1883:1883 emqx/emqx:latest ``` -These environment variables will ignore for configuration file. +Please read more about EMQX configuration in the [official documentation](https://www.emqx.io/docs/en/v5.0/admin/cfg.html). -#### EMQX Configuration - -> NOTE: All EMQX Configuration in [`etc/emqx.conf`](https://github.com/emqx/emqx/blob/master/apps/emqx/etc/emqx.conf) can be configured via environment variables. The following list is just an example, not a complete configuration. +#### EMQX node name configuration | Options | Default | Mapped | Description | | ---------------------------| ------------------ | ------------------------- | ------------------------------------- | | `EMQX_NAME` | container name | none | EMQX node short name | | `EMQX_HOST` | container IP | none | EMQX node host, IP or FQDN | -The list is incomplete and may be changed with [`etc/emqx.conf`](https://github.com/emqx/emqx/blob/master/apps/emqx/etc/emqx.conf) and plugin configuration files. But the mapping rule is similar. +These environment variables are used during container startup phase only in [docker-entrypoint.sh](./docker-entrypoint.sh). -If set `EMQX_NAME` and `EMQX_HOST`, and unset `EMQX_NODE_NAME`, `EMQX_NODE_NAME=$EMQX_NAME@$EMQX_HOST`. - -For example, set MQTT TCP port to 1883 - -```console -$ docker run -d --name emqx -e EMQX__LISTENERS__TCP__DEFAULT__BIND=1883 -p 18083:18083 -p 1883:1883 emqx/emqx:latest -``` - -#### EMQX Loaded Modules Configuration - -| Options | Default | Description | -| ----------------------- | ------------------ | ------------------------------------- | -| `EMQX_LOADED_MODULES` | see content below | default EMQX loaded modules | - -Default environment variable `EMQX_LOADED_MODULES`, including - -+ `emqx_mod_presence` - -```bash -# The default EMQX_LOADED_MODULES env -EMQX_LOADED_MODULES="emqx_mod_presence" -``` - -For example, set `EMQX_LOADED_MODULES=emqx_mod_delayed,emqx_mod_rewrite` to load these two modules. - -You can use comma, space or other separator that you want. - -All the modules defined in env `EMQX_LOADED_MODULES` will be loaded. - -```bash -EMQX_LOADED_MODULES="emqx_mod_delayed,emqx_mod_rewrite" -EMQX_LOADED_MODULES="emqx_mod_delayed emqx_mod_rewrite" -EMQX_LOADED_MODULES="emqx_mod_delayed | emqx_mod_rewrite" -``` - -#### EMQX Loaded Plugins Configuration - -| Options | Default | Description | -| ----------------------- | ------------------ | ------------------------------------- | -| `EMQX_LOADED_PLUGINS` | see content below | default EMQX loaded plugins | - -Default environment variable `EMQX_LOADED_PLUGINS`, including - -+ `emqx_recon` -+ `emqx_retainer` -+ `emqx_rule_engine` -+ `emqx_management` -+ `emqx_dashboard` - -```bash -# The default EMQX_LOADED_PLUGINS env -EMQX_LOADED_PLUGINS="emqx_recon,emqx_retainer,emqx_management,emqx_dashboard" -``` - -For example, set `EMQX_LOADED_PLUGINS= emqx_retainer,emqx_rule_engine` to load these two plugins. - -You can use comma, space or other separator that you want. - -All the plugins defined in `EMQX_LOADED_PLUGINS` will be loaded. - -```bash -EMQX_LOADED_PLUGINS="emqx_retainer,emqx_rule_engine" -EMQX_LOADED_PLUGINS="emqx_retainer emqx_rule_engine" -EMQX_LOADED_PLUGINS="emqx_retainer | emqx_rule_engine" -``` - -#### EMQX Plugins Configuration - -The environment variables which with `EMQX_` prefix are mapped to all EMQX plugins' configuration file, `.` get replaced by `__`. - -Example: - -```bash -EMQX_RETAINER__STORAGE_TYPE <--> retainer.storage_type -EMQX_RETAINER__MAX_PAYLOAD_SIZE <--> retainer.max_payload_size -``` - -Don't worry about where to find the configuration file of EMQX plugins, this docker image will find and configure them automatically using some magic. - -All EMQX plugins can be configured this way, following the environment variables mapping rule above. - -Assume you are using Redis auth plugin, for example: - -```bash -#EMQX_RETAINER__STORAGE_TYPE = "ram" -#EMQX_RETAINER.MAX_PAYLOAD_SIZE = 1MB - -docker run -d --name emqx -p 18083:18083 -p 1883:1883 \ - -e EMQX_LISTENERS__TCP__DEFAULT=1883 \ - -e EMQX_LOADED_PLUGINS="emqx_retainer" \ - -e EMQX_RETAINER__STORAGE_TYPE = "ram" \ - -e EMQX_RETAINER__MAX_PAYLOAD_SIZE = 1MB \ - emqx/emqx:latest -``` - -For numbered configuration options where the number is next to a `.` such as: - -+ backend.redis.pool1.server -+ backend.redis.hook.message.publish.1 - -You can configure an arbitrary number of them as long as each has a unique number for its own configuration option: - -```bash -docker run -d --name emqx -p 18083:18083 -p 1883:1883 \ - -e EMQX_BACKEND_REDIS_POOL1__SERVER=127.0.0.1:6379 \ - [...] - -e EMQX_BACKEND__REDIS__POOL5__SERVER=127.0.0.5:6379 \ - -e EMQX_BACKEND__REDIS__HOOK_MESSAGE__PUBLISH__1='{"topic": "persistent/topic1", "action": {"function": "on_message_publish"}, "pool": "pool1"}' \ - -e EMQX_BACKEND__REDIS__HOOK_MESSAGE__PUBLISH__2='{"topic": "persistent/topic2", "action": {"function": "on_message_publish"}, "pool": "pool1"}' \ - -e EMQX_BACKEND__REDIS__HOOK_MESSAGE__PUBLISH__3='{"topic": "persistent/topic3", "action": {"function": "on_message_publish"}, "pool": "pool1"}' \ - [...] - -e EMQX_BACKEND__REDIS__HOOK_MESSAGE__PUBLISH__13='{"topic": "persistent/topic13", "action": {"function": "on_message_publish"}, "pool": "pool1"}' \ - emqx/emqx:latest -``` +If `EMQX_NAME` and `EMQX_HOST` are set, and `EMQX_NODE_NAME` is not set, `EMQX_NODE_NAME=$EMQX_NAME@$EMQX_HOST`. +Otherwise `EMQX_NODE_NAME` is taken verbatim. ### Cluster @@ -296,6 +182,8 @@ services: - vol-emqx-log:/opt/emqx/log ``` +Note that `/opt/emqx/etc` contains some essential configuration files. If you want to mount a host directory in the container to persist configuration overrides, you will need to bootstrap it with [default configuration files](https://github.com/emqx/emqx/tree/master/apps/emqx/etc). + ### Kernel Tuning Under Linux host machine, the easiest way is [Tuning guide](https://www.emqx.io/docs/en/latest/deploy/tune.html). From 55275f15a7603aac44ec2133364d5249533718e4 Mon Sep 17 00:00:00 2001 From: Ivan Dyachkov Date: Wed, 18 Jan 2023 11:05:59 +0100 Subject: [PATCH 0265/1802] chore: include Rory in CODEOWNERS as reviewer for /deploy/ --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a74fcb27e..32be1bfe1 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -35,4 +35,4 @@ /.ci/ @emqx/emqx-review-board @id /scripts/ @emqx/emqx-review-board @id /build @emqx/emqx-review-board @id -/deploy/ @emqx/emqx-review-board @id +/deploy/ @emqx/emqx-review-board @id @Rory-Z From 8f275a66d0b2361039b2401fb68dc4103d8d58ef Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Wed, 18 Jan 2023 10:05:31 +0100 Subject: [PATCH 0266/1802] test: add coverage for key_dispatch partition strategy --- .../emqx_bridge_impl_kafka_producer_SUITE.erl | 69 ++++++++++++++----- 1 file changed, 50 insertions(+), 19 deletions(-) diff --git a/lib-ee/emqx_ee_bridge/test/emqx_bridge_impl_kafka_producer_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_bridge_impl_kafka_producer_SUITE.erl index 2d67a9941..14567dd39 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_bridge_impl_kafka_producer_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_bridge_impl_kafka_producer_SUITE.erl @@ -109,6 +109,9 @@ set_special_configs(_) -> t_publish_no_auth(_CtConfig) -> publish_with_and_without_ssl("none"). +t_publish_no_auth_key_dispatch(_CtConfig) -> + publish_with_and_without_ssl("none", #{"partition_strategy" => "key_dispatch"}). + t_publish_sasl_plain(_CtConfig) -> publish_with_and_without_ssl(valid_sasl_plain_settings()). @@ -404,20 +407,35 @@ t_failed_creation_then_fix(_Config) -> %%------------------------------------------------------------------------------ publish_with_and_without_ssl(AuthSettings) -> - publish_helper(#{ - auth_settings => AuthSettings, - ssl_settings => #{} - }), - publish_helper(#{ - auth_settings => AuthSettings, - ssl_settings => valid_ssl_settings() - }), + publish_with_and_without_ssl(AuthSettings, #{}). + +publish_with_and_without_ssl(AuthSettings, Config) -> + publish_helper( + #{ + auth_settings => AuthSettings, + ssl_settings => #{} + }, + Config + ), + publish_helper( + #{ + auth_settings => AuthSettings, + ssl_settings => valid_ssl_settings() + }, + Config + ), ok. -publish_helper(#{ - auth_settings := AuthSettings, - ssl_settings := SSLSettings -}) -> +publish_helper(AuthSettings) -> + publish_helper(AuthSettings, #{}). + +publish_helper( + #{ + auth_settings := AuthSettings, + ssl_settings := SSLSettings + }, + Conf0 +) -> HostsString = case {AuthSettings, SSLSettings} of {"none", Map} when map_size(Map) =:= 0 -> @@ -434,13 +452,17 @@ publish_helper(#{ InstId = emqx_bridge_resource:resource_id("kafka", Name), BridgeId = emqx_bridge_resource:bridge_id("kafka", Name), KafkaTopic = "test-topic-one-partition", - Conf = config(#{ - "authentication" => AuthSettings, - "kafka_hosts_string" => HostsString, - "kafka_topic" => KafkaTopic, - "instance_id" => InstId, - "ssl" => SSLSettings - }), + Conf = config( + #{ + "authentication" => AuthSettings, + "kafka_hosts_string" => HostsString, + "kafka_topic" => KafkaTopic, + "instance_id" => InstId, + "ssl" => SSLSettings + }, + Conf0 + ), + emqx_bridge_resource:create(kafka, erlang:list_to_atom(Name), Conf, #{}), %% To make sure we get unique value timer:sleep(1), @@ -463,7 +485,15 @@ publish_helper(#{ ok = emqx_bridge_resource:remove(BridgeId), ok. +default_config() -> + #{"partition_strategy" => "random"}. + config(Args) -> + config(Args, #{}). + +config(Args0, More) -> + Args1 = maps:merge(default_config(), Args0), + Args = maps:merge(Args1, More), ConfText = hocon_config(Args), ct:pal("Running tests with conf:\n~s", [ConfText]), {ok, Conf} = hocon:binary(ConfText), @@ -506,6 +536,7 @@ producer = { kafka = { topic = \"{{ kafka_topic }}\" message = {key = \"${clientid}\", value = \"${.payload}\"} + partition_strategy = {{ partition_strategy }} } } """. From d7242739e0311e28e0a22a4ff2de9461bc63b9e4 Mon Sep 17 00:00:00 2001 From: ieQu1 <99872536+ieQu1@users.noreply.github.com> Date: Wed, 18 Jan 2023 12:01:03 +0100 Subject: [PATCH 0267/1802] chore(ekka): Bump version to 0.13.9 --- apps/emqx/rebar.config | 2 +- mix.exs | 2 +- rebar.config | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/emqx/rebar.config b/apps/emqx/rebar.config index 82be1cb9a..0ea42c0fb 100644 --- a/apps/emqx/rebar.config +++ b/apps/emqx/rebar.config @@ -27,7 +27,7 @@ {jiffy, {git, "https://github.com/emqx/jiffy", {tag, "1.0.5"}}}, {cowboy, {git, "https://github.com/emqx/cowboy", {tag, "2.9.0"}}}, {esockd, {git, "https://github.com/emqx/esockd", {tag, "5.9.4"}}}, - {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.13.7"}}}, + {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.13.9"}}}, {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.8.1"}}}, {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.35.0"}}}, {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}}, diff --git a/mix.exs b/mix.exs index e124116c8..aaa2666ad 100644 --- a/mix.exs +++ b/mix.exs @@ -53,7 +53,7 @@ defmodule EMQXUmbrella.MixProject do {:cowboy, github: "emqx/cowboy", tag: "2.9.0", override: true}, {:esockd, github: "emqx/esockd", tag: "5.9.4", override: true}, {:rocksdb, github: "emqx/erlang-rocksdb", tag: "1.7.2-emqx-7", override: true}, - {:ekka, github: "emqx/ekka", tag: "0.13.7", override: true}, + {:ekka, github: "emqx/ekka", tag: "0.13.9", override: true}, {:gen_rpc, github: "emqx/gen_rpc", tag: "2.8.1", override: true}, {:grpc, github: "emqx/grpc-erl", tag: "0.6.7", override: true}, {:minirest, github: "emqx/minirest", tag: "1.3.7", override: true}, diff --git a/rebar.config b/rebar.config index bfdc3e4ee..28c0f67de 100644 --- a/rebar.config +++ b/rebar.config @@ -55,7 +55,7 @@ , {cowboy, {git, "https://github.com/emqx/cowboy", {tag, "2.9.0"}}} , {esockd, {git, "https://github.com/emqx/esockd", {tag, "5.9.4"}}} , {rocksdb, {git, "https://github.com/emqx/erlang-rocksdb", {tag, "1.7.2-emqx-7"}}} - , {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.13.7"}}} + , {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.13.9"}}} , {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.8.1"}}} , {grpc, {git, "https://github.com/emqx/grpc-erl", {tag, "0.6.7"}}} , {minirest, {git, "https://github.com/emqx/minirest", {tag, "1.3.7"}}} From 44a6e5ed15ec6122bd76ee890075a45c08aff98d Mon Sep 17 00:00:00 2001 From: Ilya Averyanov Date: Wed, 18 Jan 2023 14:33:45 +0200 Subject: [PATCH 0268/1802] chore(resources): add missing parameters to emqx_resource schema --- apps/emqx_resource/include/emqx_resource.hrl | 8 ++++++++ apps/emqx_resource/src/emqx_resource_manager.erl | 5 ----- .../src/schema/emqx_resource_schema.erl | 14 ++++++++++++++ changes/v5.0.15/feat-9628.en.md | 1 + changes/v5.0.15/feat-9628.zh.md | 1 + 5 files changed, 24 insertions(+), 5 deletions(-) create mode 100644 changes/v5.0.15/feat-9628.en.md create mode 100644 changes/v5.0.15/feat-9628.zh.md diff --git a/apps/emqx_resource/include/emqx_resource.hrl b/apps/emqx_resource/include/emqx_resource.hrl index d7b080ae8..5785ddead 100644 --- a/apps/emqx_resource/include/emqx_resource.hrl +++ b/apps/emqx_resource/include/emqx_resource.hrl @@ -101,6 +101,14 @@ -define(HEALTHCHECK_INTERVAL, 15000). -define(HEALTHCHECK_INTERVAL_RAW, <<"15s">>). +%% milliseconds +-define(START_TIMEOUT, 5000). +-define(START_TIMEOUT_RAW, <<"5s">>). + +%% boolean +-define(START_AFTER_CREATED, true). +-define(START_AFTER_CREATED_RAW, <<"true">>). + %% milliseconds -define(AUTO_RESTART_INTERVAL, 60000). -define(AUTO_RESTART_INTERVAL_RAW, <<"60s">>). diff --git a/apps/emqx_resource/src/emqx_resource_manager.erl b/apps/emqx_resource/src/emqx_resource_manager.erl index ab726976a..1dd088fca 100644 --- a/apps/emqx_resource/src/emqx_resource_manager.erl +++ b/apps/emqx_resource/src/emqx_resource_manager.erl @@ -116,11 +116,6 @@ create_and_return_data(MgrId, ResId, Group, ResourceType, Config, Opts) -> {ok, _Group, Data} = lookup(ResId), {ok, Data}. -%% internal configs --define(START_AFTER_CREATED, true). -%% in milliseconds --define(START_TIMEOUT, 5000). - %% @doc Create a resource_manager and wait until it is running create(MgrId, ResId, Group, ResourceType, Config, Opts) -> % The state machine will make the actual call to the callback/resource module after init diff --git a/apps/emqx_resource/src/schema/emqx_resource_schema.erl b/apps/emqx_resource/src/schema/emqx_resource_schema.erl index ea5ee97ca..39513e28c 100644 --- a/apps/emqx_resource/src/schema/emqx_resource_schema.erl +++ b/apps/emqx_resource/src/schema/emqx_resource_schema.erl @@ -46,6 +46,8 @@ fields("creation_opts") -> [ {worker_pool_size, fun worker_pool_size/1}, {health_check_interval, fun health_check_interval/1}, + {start_after_created, fun start_after_created/1}, + {start_timeout, fun start_timeout/1}, {auto_restart_interval, fun auto_restart_interval/1}, {query_mode, fun query_mode/1}, {request_timeout, fun request_timeout/1}, @@ -69,6 +71,18 @@ health_check_interval(default) -> ?HEALTHCHECK_INTERVAL_RAW; health_check_interval(required) -> false; health_check_interval(_) -> undefined. +start_after_created(type) -> boolean(); +start_after_created(desc) -> ?DESC("start_after_created"); +start_after_created(default) -> ?START_AFTER_CREATED_RAW; +start_after_created(required) -> false; +start_after_created(_) -> undefined. + +start_timeout(type) -> emqx_schema:duration_ms(); +start_timeout(desc) -> ?DESC("start_timeout"); +start_timeout(default) -> ?START_TIMEOUT_RAW; +start_timeout(required) -> false; +start_timeout(_) -> undefined. + auto_restart_interval(type) -> hoconsc:union([infinity, emqx_schema:duration_ms()]); auto_restart_interval(desc) -> ?DESC("auto_restart_interval"); auto_restart_interval(default) -> ?AUTO_RESTART_INTERVAL_RAW; diff --git a/changes/v5.0.15/feat-9628.en.md b/changes/v5.0.15/feat-9628.en.md new file mode 100644 index 000000000..6f814dd21 --- /dev/null +++ b/changes/v5.0.15/feat-9628.en.md @@ -0,0 +1 @@ +Expose additional resource configuration parameters: `start_after_created` and `start_timeout`. diff --git a/changes/v5.0.15/feat-9628.zh.md b/changes/v5.0.15/feat-9628.zh.md new file mode 100644 index 000000000..fee14181b --- /dev/null +++ b/changes/v5.0.15/feat-9628.zh.md @@ -0,0 +1 @@ +为桥接资源增加了配置参数:`start_after_created` 和 `start_timeout`。 From f6fbbf3ee3ffd9816cb456e5097df2bcf843b3ec Mon Sep 17 00:00:00 2001 From: Ilya Averyanov Date: Wed, 18 Jan 2023 14:34:11 +0200 Subject: [PATCH 0269/1802] chore(bridges): reduce Redis bridge flakyness --- .ci/docker-compose-file/Makefile.local | 2 +- .../test/emqx_ee_bridge_redis_SUITE.erl | 92 +++++++++++-------- 2 files changed, 53 insertions(+), 41 deletions(-) diff --git a/.ci/docker-compose-file/Makefile.local b/.ci/docker-compose-file/Makefile.local index ff4f348b0..2cf0802ce 100644 --- a/.ci/docker-compose-file/Makefile.local +++ b/.ci/docker-compose-file/Makefile.local @@ -52,7 +52,7 @@ down: down --remove-orphans ct: - docker exec -i "$(CONTAINER)" bash -c "rebar3 ct --name 'test@127.0.0.1' -v --suite $(SUITE)" + docker exec -i "$(CONTAINER)" bash -c "rebar3 ct --name 'test@127.0.0.1' --readable true -v --suite $(SUITE)" ct-all: docker exec -i "$(CONTAINER)" bash -c "make ct" diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_redis_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_redis_SUITE.erl index feebcfade..2b67787b2 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_redis_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_redis_SUITE.erl @@ -137,6 +137,7 @@ end_per_suite(_Config) -> ok. init_per_testcase(_Testcase, Config) -> + ok = delete_all_rules(), ok = delete_all_bridges(), case ?config(transport_type, Config) of undefined -> @@ -248,29 +249,27 @@ t_check_replay(Config) -> ), ?check_trace( - begin - ?wait_async_action( - with_down_failure(Config, ProxyName, fun() -> - {_, {ok, _}} = - ?wait_async_action( - lists:foreach( - fun(_) -> - _ = publish_message(Topic, <<"test_payload">>) - end, - lists:seq(1, ?BATCH_SIZE) - ), - #{ - ?snk_kind := redis_ee_connector_send_done, - batch := true, - result := {error, _} - }, - 10_000 - ) - end), - #{?snk_kind := redis_ee_connector_send_done, batch := true, result := {ok, _}}, - 10_000 - ) - end, + ?wait_async_action( + with_down_failure(Config, ProxyName, fun() -> + {_, {ok, _}} = + ?wait_async_action( + lists:foreach( + fun(_) -> + _ = publish_message(Topic, <<"test_payload">>) + end, + lists:seq(1, ?BATCH_SIZE) + ), + #{ + ?snk_kind := redis_ee_connector_send_done, + batch := true, + result := {error, _} + }, + 10_000 + ) + end), + #{?snk_kind := redis_ee_connector_send_done, batch := true, result := {ok, _}}, + 10_000 + ), fun(Trace) -> ?assert( ?strict_causality( @@ -340,7 +339,7 @@ with_down_failure(Config, Name, F) -> ProxyHost = ?config(proxy_host, Config), emqx_common_test_helpers:with_failure(down, Name, ProxyHost, ProxyPort, F). -check_resource_queries(ResourceId, Topic, IsBatch) -> +check_resource_queries(ResourceId, BaseTopic, IsBatch) -> RandomPayload = rand:bytes(20), N = case IsBatch of @@ -348,18 +347,18 @@ check_resource_queries(ResourceId, Topic, IsBatch) -> false -> 1 end, ?check_trace( - begin - ?wait_async_action( - lists:foreach( - fun(_) -> - _ = publish_message(Topic, RandomPayload) - end, - lists:seq(1, N) - ), - #{?snk_kind := redis_ee_connector_send_done, batch := IsBatch}, - 1000 - ) - end, + ?wait_async_action( + lists:foreach( + fun(I) -> + IBin = integer_to_binary(I), + Topic = <>, + _ = publish_message(Topic, RandomPayload) + end, + lists:seq(1, N) + ), + #{?snk_kind := redis_ee_connector_send_done, batch := IsBatch}, + 5000 + ), fun(Trace) -> AddedMsgCount = length(added_msgs(ResourceId, RandomPayload)), case IsBatch of @@ -394,6 +393,14 @@ conf_schema(StructName) -> roots => [{root, hoconsc:ref(emqx_ee_bridge_redis, StructName)}] }. +delete_all_rules() -> + lists:foreach( + fun(#{id := RuleId}) -> + emqx_rule_engine:delete_rule(RuleId) + end, + emqx_rule_engine:get_rules() + ). + delete_all_bridges() -> lists:foreach( fun(#{name := Name, type := Type}) -> @@ -490,7 +497,8 @@ toxiproxy_redis_bridge_config() -> <<"query_mode">> => <<"async">>, <<"worker_pool_size">> => <<"1">>, <<"batch_size">> => integer_to_binary(?BATCH_SIZE), - <<"health_check_interval">> => <<"1s">> + <<"health_check_interval">> => <<"1s">>, + <<"start_timeout">> => <<"15s">> } }, maps:merge(Conf0, ?COMMON_REDIS_OPTS). @@ -500,8 +508,10 @@ invalid_command_bridge_config() -> Conf1 = maps:merge(Conf0, ?COMMON_REDIS_OPTS), Conf1#{ <<"resource_opts">> => #{ + <<"query_mode">> => <<"sync">>, <<"batch_size">> => <<"1">>, - <<"worker_pool_size">> => <<"1">> + <<"worker_pool_size">> => <<"1">>, + <<"start_timeout">> => <<"15s">> }, <<"command_template">> => [<<"BAD">>, <<"COMMAND">>, <<"${payload}">>] }. @@ -510,12 +520,14 @@ resource_configs() -> #{ batch_off => #{ <<"query_mode">> => <<"sync">>, - <<"batch_size">> => <<"1">> + <<"batch_size">> => <<"1">>, + <<"start_timeout">> => <<"15s">> }, batch_on => #{ <<"query_mode">> => <<"async">>, <<"worker_pool_size">> => <<"1">>, - <<"batch_size">> => integer_to_binary(?BATCH_SIZE) + <<"batch_size">> => integer_to_binary(?BATCH_SIZE), + <<"start_timeout">> => <<"15s">> } }. From 5f5f34bd94e1aed47760ee16f4fecfac587ba6a0 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Wed, 18 Jan 2023 14:47:23 +0100 Subject: [PATCH 0270/1802] docs: fix a typo in changelog --- changes/v5.0.15/fix-9765.en.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changes/v5.0.15/fix-9765.en.md b/changes/v5.0.15/fix-9765.en.md index 8c1ed4ad1..7de7e55f3 100644 --- a/changes/v5.0.15/fix-9765.en.md +++ b/changes/v5.0.15/fix-9765.en.md @@ -3,4 +3,4 @@ Prior to this change, config values for passwords are not allowed to be decimals e.g. `EMQX_FOOBAR__PASSWORD=12344` or `emqx.foobar.password=1234` would result in a type check error, unless quoted as: `EMQX_FOOBAR__PASSWORD='"12344"'` or `emqx.foobar.password="1234"`. -After this fix, the value does not have to be auoted. +After this fix, the value does not have to be quoted. From fb763ecebda3c4644f06e3719c975c074d7b2553 Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Wed, 18 Jan 2023 15:49:57 +0100 Subject: [PATCH 0271/1802] feat: support HAProxy protocol for dashboard API --- .../i18n/emqx_dashboard_i18n.conf | 10 +++ apps/emqx_dashboard/src/emqx_dashboard.erl | 19 +++-- .../src/emqx_dashboard_schema.erl | 8 ++ .../test/emqx_dashboard_api_test_helpers.erl | 7 +- .../test/emqx_dashboard_haproxy_SUITE.erl | 83 +++++++++++++++++++ rebar.config | 2 +- 6 files changed, 122 insertions(+), 7 deletions(-) create mode 100644 apps/emqx_dashboard/test/emqx_dashboard_haproxy_SUITE.erl diff --git a/apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf b/apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf index 872cfdf26..cc0d0244b 100644 --- a/apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf +++ b/apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf @@ -92,6 +92,16 @@ Note: `sample_interval` should be a divisor of 60.""" zh: "IPv6 only" } } + proxy_header { + desc { + en: "Enable support for HAProxy header. Be aware once enabled regular HTTP requests can't be handled anymore." + zh: "[FIXME]" + } + label: { + en: "Enable support for HAProxy header" + zh: "[FIXME]" + } + } desc_dashboard { desc { en: "Configuration for EMQX dashboard." diff --git a/apps/emqx_dashboard/src/emqx_dashboard.erl b/apps/emqx_dashboard/src/emqx_dashboard.erl index 36c7660cc..2afc8b362 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard.erl @@ -92,8 +92,8 @@ start_listeners(Listeners) -> }, Res = lists:foldl( - fun({Name, Protocol, Bind, RanchOptions}, Acc) -> - Minirest = BaseMinirest#{protocol => Protocol}, + fun({Name, Protocol, Bind, RanchOptions, ProtoOpts}, Acc) -> + Minirest = BaseMinirest#{protocol => Protocol, protocol_options => ProtoOpts}, case minirest:start(Name, RanchOptions, Minirest) of {ok, _} -> ?ULOG("Listener ~ts on ~ts started.~n", [ @@ -125,7 +125,7 @@ stop_listeners(Listeners) -> ?SLOG(warning, #{msg => "stop_listener_failed", name => Name, port => Port}) end end - || {Name, _, Port, _} <- listeners(Listeners) + || {Name, _, Port, _, _} <- listeners(Listeners) ], ok. @@ -164,7 +164,13 @@ listeners(Listeners) -> maps:get(enable, Conf) andalso begin {Conf1, Bind} = ip_port(Conf), - {true, {listener_name(Protocol), Protocol, Bind, ranch_opts(Conf1)}} + {true, { + listener_name(Protocol), + Protocol, + Bind, + ranch_opts(Conf1), + proto_opts(Conf1) + }} end end, maps:to_list(Listeners) @@ -197,7 +203,7 @@ ranch_opts(Options) -> SocketOpts = maps:fold( fun filter_false/3, [], - maps:without([enable, inet6, ipv6_v6only | Keys], Options) + maps:without([enable, inet6, ipv6_v6only, proxy_header | Keys], Options) ), InetOpts = case Options of @@ -210,6 +216,9 @@ ranch_opts(Options) -> end, RanchOpts#{socket_opts => InetOpts ++ SocketOpts}. +proto_opts(Options) -> + maps:with([proxy_header], Options). + filter_false(_K, false, S) -> S; filter_false(K, V, S) -> [{K, V} | S]. diff --git a/apps/emqx_dashboard/src/emqx_dashboard_schema.erl b/apps/emqx_dashboard/src/emqx_dashboard_schema.erl index 6742032d5..a7a8c7828 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_schema.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_schema.erl @@ -160,6 +160,14 @@ common_listener_fields() -> default => false, desc => ?DESC(ipv6_v6only) } + )}, + {"proxy_header", + ?HOCON( + boolean(), + #{ + desc => ?DESC(proxy_header), + default => false + } )} ]. diff --git a/apps/emqx_dashboard/test/emqx_dashboard_api_test_helpers.erl b/apps/emqx_dashboard/test/emqx_dashboard_api_test_helpers.erl index 87a3654ac..8df130897 100644 --- a/apps/emqx_dashboard/test/emqx_dashboard_api_test_helpers.erl +++ b/apps/emqx_dashboard/test/emqx_dashboard_api_test_helpers.erl @@ -19,6 +19,7 @@ -export([ set_default_config/0, set_default_config/1, + set_default_config/2, request/2, request/3, request/4, @@ -36,6 +37,9 @@ set_default_config() -> set_default_config(<<"admin">>). set_default_config(DefaultUsername) -> + set_default_config(DefaultUsername, false). + +set_default_config(DefaultUsername, HAProxyEnabled) -> Config = #{ listeners => #{ http => #{ @@ -46,7 +50,8 @@ set_default_config(DefaultUsername) -> max_connections => 512, num_acceptors => 4, send_timeout => 5000, - backlog => 512 + backlog => 512, + proxy_header => HAProxyEnabled } }, default_username => DefaultUsername, diff --git a/apps/emqx_dashboard/test/emqx_dashboard_haproxy_SUITE.erl b/apps/emqx_dashboard/test/emqx_dashboard_haproxy_SUITE.erl new file mode 100644 index 000000000..2fceeee6e --- /dev/null +++ b/apps/emqx_dashboard/test/emqx_dashboard_haproxy_SUITE.erl @@ -0,0 +1,83 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_dashboard_haproxy_SUITE). + +-compile(nowarn_export_all). +-compile(export_all). + +-import( + emqx_common_test_http, + [ + request_api/3 + ] +). + +-include_lib("eunit/include/eunit.hrl"). +-include("emqx_dashboard.hrl"). + +-define(HOST, "http://127.0.0.1:18083"). + +-define(BASE_PATH, "/"). + +all() -> + emqx_common_test_helpers:all(?MODULE). + +end_suite() -> + end_suite([]). + +end_suite(Apps) -> + application:unload(emqx_management), + mnesia:clear_table(?ADMIN), + emqx_common_test_helpers:stop_apps(Apps ++ [emqx_dashboard]). + +init_per_suite(Config) -> + emqx_common_test_helpers:start_apps( + [emqx_management, emqx_dashboard], + fun set_special_configs/1 + ), + Config. + +end_per_suite(_Config) -> + emqx_common_test_helpers:stop_apps([emqx_dashboard, emqx_management]), + mria:stop(). + +set_special_configs(emqx_dashboard) -> + emqx_dashboard_api_test_helpers:set_default_config(<<"admin">>, true), + ok; +set_special_configs(_) -> + ok. + +disabled_t_status(_) -> + %% no easy way since httpc doesn't support emulating the haproxy protocol + {ok, 200, _Res} = http_get(["status"]), + ok. + +%%------------------------------------------------------------------------------ +%% Internal functions +%%------------------------------------------------------------------------------ +http_get(Parts) -> + request_api(get, api_path(Parts), auth_header_()). + +auth_header_() -> + auth_header_(<<"admin">>, <<"public">>). + +auth_header_(Username, Password) -> + {ok, Token} = emqx_dashboard_admin:sign_token(Username, Password), + {"Authorization", "Bearer " ++ binary_to_list(Token)}. + +api_path(Parts) -> + ?HOST ++ filename:join([?BASE_PATH | Parts]). diff --git a/rebar.config b/rebar.config index bfdc3e4ee..a20bc7060 100644 --- a/rebar.config +++ b/rebar.config @@ -58,7 +58,7 @@ , {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.13.7"}}} , {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.8.1"}}} , {grpc, {git, "https://github.com/emqx/grpc-erl", {tag, "0.6.7"}}} - , {minirest, {git, "https://github.com/emqx/minirest", {tag, "1.3.7"}}} + , {minirest, {git, "https://github.com/emqx/minirest", {tag, "1.3.8"}}} , {ecpool, {git, "https://github.com/emqx/ecpool", {tag, "0.5.2"}}} , {replayq, {git, "https://github.com/emqx/replayq.git", {tag, "0.3.5"}}} , {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}} From 1c279546761630a5bdec23a31f36cafdf0a5ac77 Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Wed, 18 Jan 2023 15:54:53 +0100 Subject: [PATCH 0272/1802] chore: add changelog --- changes/v5.0.15/feat-9802.en.md | 1 + changes/v5.0.15/feat-9802.zh.md | 1 + 2 files changed, 2 insertions(+) create mode 100644 changes/v5.0.15/feat-9802.en.md create mode 100644 changes/v5.0.15/feat-9802.zh.md diff --git a/changes/v5.0.15/feat-9802.en.md b/changes/v5.0.15/feat-9802.en.md new file mode 100644 index 000000000..ac314879a --- /dev/null +++ b/changes/v5.0.15/feat-9802.en.md @@ -0,0 +1 @@ +Support HAProxy protocol for dashboard API. diff --git a/changes/v5.0.15/feat-9802.zh.md b/changes/v5.0.15/feat-9802.zh.md new file mode 100644 index 000000000..97a926b1a --- /dev/null +++ b/changes/v5.0.15/feat-9802.zh.md @@ -0,0 +1 @@ +[FIXME] Support HAProxy protocol for dashboard API. From 62b01b35013e9ce3a53883cc0e4422659b4fb0a3 Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Wed, 18 Jan 2023 16:31:14 +0100 Subject: [PATCH 0273/1802] fix: version mismatch between erlang and elixir --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index e124116c8..9fcc7b076 100644 --- a/mix.exs +++ b/mix.exs @@ -56,7 +56,7 @@ defmodule EMQXUmbrella.MixProject do {:ekka, github: "emqx/ekka", tag: "0.13.7", override: true}, {:gen_rpc, github: "emqx/gen_rpc", tag: "2.8.1", override: true}, {:grpc, github: "emqx/grpc-erl", tag: "0.6.7", override: true}, - {:minirest, github: "emqx/minirest", tag: "1.3.7", override: true}, + {:minirest, github: "emqx/minirest", tag: "1.3.8", override: true}, {:ecpool, github: "emqx/ecpool", tag: "0.5.2", override: true}, {:replayq, github: "emqx/replayq", tag: "0.3.5", override: true}, {:pbkdf2, github: "emqx/erlang-pbkdf2", tag: "2.0.4", override: true}, From f9c3115cd4c4f85f9dca5c3510884d12baaa48c2 Mon Sep 17 00:00:00 2001 From: Ilya Averyanov Date: Tue, 17 Jan 2023 16:12:09 +0200 Subject: [PATCH 0274/1802] chore(bridges): add more helpful descriptions Add more helpful descriptions for `command_template` option of Redis bridge. --- lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_redis.conf | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_redis.conf b/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_redis.conf index a5744df4c..0589899b5 100644 --- a/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_redis.conf +++ b/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_redis.conf @@ -19,8 +19,13 @@ will be forwarded. command_template { desc { - en: """Redis Command Template""" - zh: """Redis Command 模板""" + en: """Redis command template used to export messages. Each list element stands for a command name or its argument. +For example, to push payloads in a Redis list by key `msgs`, the elements should be the following: +`rpush`, `msgs`, `${payload}`. +""" + zh: """用于推送数据的 Redis 命令模板。 每个列表元素代表一个命令名称或其参数。 +例如,要通过键值 `msgs` 将消息体推送到 Redis 列表中,数组元素应该是: `rpush`, `msgs`, `${payload}`。 +""" } label { en: "Redis Command Template" From 1716a5da995c46889c11e5d58f0f94156daae4bd Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Wed, 18 Jan 2023 17:22:05 +0100 Subject: [PATCH 0275/1802] chore: bump version to e5.0.0-rc.1 --- apps/emqx/include/emqx_release.hrl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx/include/emqx_release.hrl b/apps/emqx/include/emqx_release.hrl index e749ebc2e..a4963f31a 100644 --- a/apps/emqx/include/emqx_release.hrl +++ b/apps/emqx/include/emqx_release.hrl @@ -35,7 +35,7 @@ -define(EMQX_RELEASE_CE, "5.0.14"). %% Enterprise edition --define(EMQX_RELEASE_EE, "5.0.0-beta.6"). +-define(EMQX_RELEASE_EE, "5.0.0-rc.1"). %% the HTTP API version -define(EMQX_API_VERSION, "5.0"). From 941216b6d0f02afbfd065c53330710fc26198ec5 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Wed, 18 Jan 2023 17:25:05 +0100 Subject: [PATCH 0276/1802] chore: disable hot-upgrade for now --- scripts/rel/cut.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/rel/cut.sh b/scripts/rel/cut.sh index d35176600..976a8fb78 100755 --- a/scripts/rel/cut.sh +++ b/scripts/rel/cut.sh @@ -53,7 +53,8 @@ case "$TAG" in e*) TAG_PREFIX='e' PROFILE='emqx-enterprise' - SKIP_APPUP='no' + #TODO change to no when we are ready to support hot-upgrade + SKIP_APPUP='yes' ;; -h|--help) usage From e8910c97485bf5e61523a21a9959aeb20c21427b Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Wed, 18 Jan 2023 17:32:36 +0100 Subject: [PATCH 0277/1802] chore: do not gnerate change log when first release or alpha, beta, rc --- scripts/rel/cut.sh | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/scripts/rel/cut.sh b/scripts/rel/cut.sh index 976a8fb78..e03d5eff4 100755 --- a/scripts/rel/cut.sh +++ b/scripts/rel/cut.sh @@ -218,7 +218,24 @@ generate_changelog () { if [ "$DRYRUN" = 'yes' ]; then logmsg "Release tag is ready to be created with command: git tag $TAG" else - generate_changelog + case "$TAG" in + *rc*) + true + ;; + *alpha*) + true + ;; + *beta*) + true + ;; + e5.0.0*) + # the first release has no change log + true + ;; + *) + generate_changelog + ;; + esac git tag "$TAG" logmsg "$TAG is created OK." fi From 47f796dd12c6c79424ac3a8c5eb03ae95bdac27f Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Wed, 18 Jan 2023 15:07:38 -0300 Subject: [PATCH 0278/1802] refactor: rename `emqx_resource_worker` -> `emqx_resource_buffer_worker` To make it more clear that it's purpose is serve as a buffering layer. --- apps/emqx_bridge/src/emqx_bridge.erl | 2 +- .../test/emqx_bridge_mqtt_SUITE.erl | 4 +- apps/emqx_resource/src/emqx_resource.erl | 10 ++-- ...er.erl => emqx_resource_buffer_worker.erl} | 42 ++++++------- ...rl => emqx_resource_buffer_worker_sup.erl} | 8 +-- .../src/emqx_resource_manager.erl | 8 +-- apps/emqx_resource/src/emqx_resource_sup.erl | 4 +- .../test/emqx_resource_SUITE.erl | 60 +++++++++---------- .../test/emqx_ee_bridge_influxdb_SUITE.erl | 2 +- .../test/emqx_ee_bridge_mysql_SUITE.erl | 2 +- .../test/emqx_ee_bridge_pgsql_SUITE.erl | 2 +- 11 files changed, 72 insertions(+), 72 deletions(-) rename apps/emqx_resource/src/{emqx_resource_worker.erl => emqx_resource_buffer_worker.erl} (97%) rename apps/emqx_resource/src/{emqx_resource_worker_sup.erl => emqx_resource_buffer_worker_sup.erl} (94%) diff --git a/apps/emqx_bridge/src/emqx_bridge.erl b/apps/emqx_bridge/src/emqx_bridge.erl index 59b41c7fa..5b3fe796b 100644 --- a/apps/emqx_bridge/src/emqx_bridge.erl +++ b/apps/emqx_bridge/src/emqx_bridge.erl @@ -65,7 +65,7 @@ load() -> fun({Type, NamedConf}) -> lists:foreach( fun({Name, Conf}) -> - %% fetch opts for `emqx_resource_worker` + %% fetch opts for `emqx_resource_buffer_worker` ResOpts = emqx_resource:fetch_creation_opts(Conf), safe_load_bridge(Type, Name, Conf, ResOpts) end, diff --git a/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl b/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl index d20d3bc10..1b4eac73e 100644 --- a/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl +++ b/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl @@ -886,9 +886,9 @@ t_mqtt_conn_bridge_egress_reconnect(_) -> {ok, SRef} = snabbkaffe:subscribe( fun - (#{?snk_kind := resource_worker_retry_inflight_failed}) -> + (#{?snk_kind := buffer_worker_retry_inflight_failed}) -> true; - (#{?snk_kind := resource_worker_flush_nack}) -> + (#{?snk_kind := buffer_worker_flush_nack}) -> true; (_) -> false diff --git a/apps/emqx_resource/src/emqx_resource.erl b/apps/emqx_resource/src/emqx_resource.erl index 0fd21bfcd..bb27b6acd 100644 --- a/apps/emqx_resource/src/emqx_resource.erl +++ b/apps/emqx_resource/src/emqx_resource.erl @@ -255,7 +255,7 @@ reset_metrics(ResId) -> query(ResId, Request) -> query(ResId, Request, #{}). --spec query(resource_id(), Request :: term(), emqx_resource_worker:query_opts()) -> +-spec query(resource_id(), Request :: term(), emqx_resource_buffer_worker:query_opts()) -> Result :: term(). query(ResId, Request, Opts) -> case emqx_resource_manager:ets_lookup(ResId) of @@ -263,11 +263,11 @@ query(ResId, Request, Opts) -> IsBufferSupported = is_buffer_supported(Module), case {IsBufferSupported, QM} of {true, _} -> - emqx_resource_worker:simple_sync_query(ResId, Request); + emqx_resource_buffer_worker:simple_sync_query(ResId, Request); {false, sync} -> - emqx_resource_worker:sync_query(ResId, Request, Opts); + emqx_resource_buffer_worker:sync_query(ResId, Request, Opts); {false, async} -> - emqx_resource_worker:async_query(ResId, Request, Opts) + emqx_resource_buffer_worker:async_query(ResId, Request, Opts) end; {error, not_found} -> ?RESOURCE_ERROR(not_found, "resource not found") @@ -275,7 +275,7 @@ query(ResId, Request, Opts) -> -spec simple_sync_query(resource_id(), Request :: term()) -> Result :: term(). simple_sync_query(ResId, Request) -> - emqx_resource_worker:simple_sync_query(ResId, Request). + emqx_resource_buffer_worker:simple_sync_query(ResId, Request). -spec start(resource_id()) -> ok | {error, Reason :: term()}. start(ResId) -> diff --git a/apps/emqx_resource/src/emqx_resource_worker.erl b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl similarity index 97% rename from apps/emqx_resource/src/emqx_resource_worker.erl rename to apps/emqx_resource/src/emqx_resource_buffer_worker.erl index d7010c3bd..4b16ed3d5 100644 --- a/apps/emqx_resource/src/emqx_resource_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl @@ -17,7 +17,7 @@ %% This module implements async message sending, disk message queuing, %% and message batching using ReplayQ. --module(emqx_resource_worker). +-module(emqx_resource_buffer_worker). -include("emqx_resource.hrl"). -include("emqx_resource_utils.hrl"). @@ -176,11 +176,11 @@ init({Id, Index, Opts}) -> resume_interval => maps:get(resume_interval, Opts, HealthCheckInterval), tref => undefined }, - ?tp(resource_worker_init, #{id => Id, index => Index}), + ?tp(buffer_worker_init, #{id => Id, index => Index}), {ok, running, Data}. running(enter, _, St) -> - ?tp(resource_worker_enter_running, #{}), + ?tp(buffer_worker_enter_running, #{}), maybe_flush(St); running(cast, resume, _St) -> keep_state_and_data; @@ -206,7 +206,7 @@ running(info, Info, _St) -> keep_state_and_data. blocked(enter, _, #{resume_interval := ResumeT} = _St) -> - ?tp(resource_worker_enter_blocked, #{}), + ?tp(buffer_worker_enter_blocked, #{}), {keep_state_and_data, {state_timeout, ResumeT, unblock}}; blocked(cast, block, _St) -> keep_state_and_data; @@ -315,7 +315,7 @@ retry_inflight_sync(Ref, QueryOrBatch, Data0) -> index := Index, resume_interval := ResumeT } = Data0, - ?tp(resource_worker_retry_inflight, #{query_or_batch => QueryOrBatch, ref => Ref}), + ?tp(buffer_worker_retry_inflight, #{query_or_batch => QueryOrBatch, ref => Ref}), QueryOpts = #{simple_query => false}, Result = call_query(sync, Id, Index, Ref, QueryOrBatch, QueryOpts), ReplyResult = @@ -331,7 +331,7 @@ retry_inflight_sync(Ref, QueryOrBatch, Data0) -> {nack, PostFn} -> PostFn(), ?tp( - resource_worker_retry_inflight_failed, + buffer_worker_retry_inflight_failed, #{ ref => Ref, query_or_batch => QueryOrBatch @@ -349,7 +349,7 @@ retry_inflight_sync(Ref, QueryOrBatch, Data0) -> %% we bump the counter when removing it from the table. IsAcked andalso PostFn(), ?tp( - resource_worker_retry_inflight_succeeded, + buffer_worker_retry_inflight_succeeded, #{ ref => Ref, query_or_batch => QueryOrBatch @@ -415,7 +415,7 @@ flush(Data0) -> {0, _} -> {keep_state, Data1}; {_, true} -> - ?tp(resource_worker_flush_but_inflight_full, #{}), + ?tp(buffer_worker_flush_but_inflight_full, #{}), Data2 = ensure_flush_timer(Data1), {keep_state, Data2}; {_, false} -> @@ -483,7 +483,7 @@ do_flush( store_async_worker_reference(InflightTID, Ref, WorkerMRef), emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), ?tp( - resource_worker_flush_nack, + buffer_worker_flush_nack, #{ ref => Ref, is_retriable => IsRetriable, @@ -512,7 +512,7 @@ do_flush( store_async_worker_reference(InflightTID, Ref, WorkerMRef), emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), ?tp( - resource_worker_flush_ack, + buffer_worker_flush_ack, #{ batch_or_query => Request, result => Result @@ -560,7 +560,7 @@ do_flush(Data0, #{ store_async_worker_reference(InflightTID, Ref, WorkerMRef), emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), ?tp( - resource_worker_flush_nack, + buffer_worker_flush_nack, #{ ref => Ref, is_retriable => IsRetriable, @@ -589,7 +589,7 @@ do_flush(Data0, #{ store_async_worker_reference(InflightTID, Ref, WorkerMRef), emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), ?tp( - resource_worker_flush_ack, + buffer_worker_flush_ack, #{ batch_or_query => Batch, result => Result @@ -873,7 +873,7 @@ reply_after_query( case Action of nack -> %% Keep retrying. - ?tp(resource_worker_reply_after_query, #{ + ?tp(buffer_worker_reply_after_query, #{ action => Action, batch_or_query => ?QUERY(From, Request, HasBeenSent), ref => Ref, @@ -882,7 +882,7 @@ reply_after_query( mark_inflight_as_retriable(InflightTID, Ref), ?MODULE:block(Pid); ack -> - ?tp(resource_worker_reply_after_query, #{ + ?tp(buffer_worker_reply_after_query, #{ action => Action, batch_or_query => ?QUERY(From, Request, HasBeenSent), ref => Ref, @@ -903,7 +903,7 @@ batch_reply_after_query(Pid, Id, Index, InflightTID, Ref, Batch, QueryOpts, Resu case Action of nack -> %% Keep retrying. - ?tp(resource_worker_reply_after_query, #{ + ?tp(buffer_worker_reply_after_query, #{ action => nack, batch_or_query => Batch, ref => Ref, @@ -912,7 +912,7 @@ batch_reply_after_query(Pid, Id, Index, InflightTID, Ref, Batch, QueryOpts, Resu mark_inflight_as_retriable(InflightTID, Ref), ?MODULE:block(Pid); ack -> - ?tp(resource_worker_reply_after_query, #{ + ?tp(buffer_worker_reply_after_query, #{ action => ack, batch_or_query => Batch, ref => Ref, @@ -955,7 +955,7 @@ append_queue(Id, Index, Q, Queries) when not is_binary(Q) -> end, emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q2)), ?tp( - resource_worker_appended_to_queue, + buffer_worker_appended_to_queue, #{ id => Id, items => Queries, @@ -973,7 +973,7 @@ append_queue(Id, Index, Q, Queries) when not is_binary(Q) -> inflight_new(InfltWinSZ, Id, Index) -> TableId = ets:new( - emqx_resource_worker_inflight_tab, + emqx_resource_buffer_worker_inflight_tab, [ordered_set, public, {write_concurrency, true}] ), inflight_append(TableId, {?MAX_SIZE_REF, InfltWinSZ}, Id, Index), @@ -1040,7 +1040,7 @@ inflight_append( BatchSize = length(Batch), IsNew andalso ets:update_counter(InflightTID, ?SIZE_REF, {2, BatchSize}), emqx_resource_metrics:inflight_set(Id, Index, inflight_num_msgs(InflightTID)), - ?tp(resource_worker_appended_to_inflight, #{item => InflightItem, is_new => IsNew}), + ?tp(buffer_worker_appended_to_inflight, #{item => InflightItem, is_new => IsNew}), ok; inflight_append( InflightTID, @@ -1053,7 +1053,7 @@ inflight_append( IsNew = ets:insert_new(InflightTID, InflightItem), IsNew andalso ets:update_counter(InflightTID, ?SIZE_REF, {2, 1}), emqx_resource_metrics:inflight_set(Id, Index, inflight_num_msgs(InflightTID)), - ?tp(resource_worker_appended_to_inflight, #{item => InflightItem, is_new => IsNew}), + ?tp(buffer_worker_appended_to_inflight, #{item => InflightItem, is_new => IsNew}), ok; inflight_append(InflightTID, {Ref, Data}, _Id, _Index) -> ets:insert(InflightTID, {Ref, Data}), @@ -1130,7 +1130,7 @@ mark_inflight_items_as_retriable(Data, WorkerMRef) -> end ), _NumAffected = ets:select_replace(InflightTID, MatchSpec), - ?tp(resource_worker_worker_down_update, #{num_affected => _NumAffected}), + ?tp(buffer_worker_worker_down_update, #{num_affected => _NumAffected}), ok. %%============================================================================== diff --git a/apps/emqx_resource/src/emqx_resource_worker_sup.erl b/apps/emqx_resource/src/emqx_resource_buffer_worker_sup.erl similarity index 94% rename from apps/emqx_resource/src/emqx_resource_worker_sup.erl rename to apps/emqx_resource/src/emqx_resource_buffer_worker_sup.erl index 8b0ce2c65..4987946c9 100644 --- a/apps/emqx_resource/src/emqx_resource_worker_sup.erl +++ b/apps/emqx_resource/src/emqx_resource_buffer_worker_sup.erl @@ -13,7 +13,7 @@ %% See the License for the specific language governing permissions and %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_resource_worker_sup). +-module(emqx_resource_buffer_worker_sup). -behaviour(supervisor). %%%============================================================================= @@ -99,7 +99,7 @@ ensure_worker_added(ResId, Idx) -> -define(CHILD_ID(MOD, RESID, INDEX), {MOD, RESID, INDEX}). ensure_worker_started(ResId, Idx, Opts) -> - Mod = emqx_resource_worker, + Mod = emqx_resource_buffer_worker, Spec = #{ id => ?CHILD_ID(Mod, ResId, Idx), start => {Mod, start_link, [ResId, Idx, Opts]}, @@ -116,7 +116,7 @@ ensure_worker_started(ResId, Idx, Opts) -> end. ensure_worker_removed(ResId, Idx) -> - ChildId = ?CHILD_ID(emqx_resource_worker, ResId, Idx), + ChildId = ?CHILD_ID(emqx_resource_buffer_worker, ResId, Idx), case supervisor:terminate_child(?SERVER, ChildId) of ok -> Res = supervisor:delete_child(?SERVER, ChildId), @@ -129,7 +129,7 @@ ensure_worker_removed(ResId, Idx) -> end. ensure_disk_queue_dir_absent(ResourceId, Index) -> - ok = emqx_resource_worker:clear_disk_queue_dir(ResourceId, Index), + ok = emqx_resource_buffer_worker:clear_disk_queue_dir(ResourceId, Index), ok. ensure_worker_pool_removed(ResId) -> diff --git a/apps/emqx_resource/src/emqx_resource_manager.erl b/apps/emqx_resource/src/emqx_resource_manager.erl index ab726976a..95d1ed1d2 100644 --- a/apps/emqx_resource/src/emqx_resource_manager.erl +++ b/apps/emqx_resource/src/emqx_resource_manager.erl @@ -150,7 +150,7 @@ create(MgrId, ResId, Group, ResourceType, Config, Opts) -> %% buffer, so there is no need for resource workers ok; false -> - ok = emqx_resource_worker_sup:start_workers(ResId, Opts), + ok = emqx_resource_buffer_worker_sup:start_workers(ResId, Opts), case maps:get(start_after_created, Opts, ?START_AFTER_CREATED) of true -> wait_for_ready(ResId, maps:get(start_timeout, Opts, ?START_TIMEOUT)); @@ -473,7 +473,7 @@ retry_actions(Data) -> handle_remove_event(From, ClearMetrics, Data) -> stop_resource(Data), - ok = emqx_resource_worker_sup:stop_workers(Data#data.id, Data#data.opts), + ok = emqx_resource_buffer_worker_sup:stop_workers(Data#data.id, Data#data.opts), case ClearMetrics of true -> ok = emqx_metrics_worker:clear_metrics(?RES_METRICS, Data#data.id); false -> ok @@ -587,9 +587,9 @@ maybe_alarm(_Status, ResId) -> maybe_resume_resource_workers(connected) -> lists:foreach( fun({_, Pid, _, _}) -> - emqx_resource_worker:resume(Pid) + emqx_resource_buffer_worker:resume(Pid) end, - supervisor:which_children(emqx_resource_worker_sup) + supervisor:which_children(emqx_resource_buffer_worker_sup) ); maybe_resume_resource_workers(_) -> ok. diff --git a/apps/emqx_resource/src/emqx_resource_sup.erl b/apps/emqx_resource/src/emqx_resource_sup.erl index ea31b8b6b..4d9abb03d 100644 --- a/apps/emqx_resource/src/emqx_resource_sup.erl +++ b/apps/emqx_resource/src/emqx_resource_sup.erl @@ -39,8 +39,8 @@ init([]) -> modules => [emqx_resource_manager_sup] }, WorkerSup = #{ - id => emqx_resource_worker_sup, - start => {emqx_resource_worker_sup, start_link, []}, + id => emqx_resource_buffer_worker_sup, + start => {emqx_resource_buffer_worker_sup, start_link, []}, restart => permanent, shutdown => infinity, type => supervisor diff --git a/apps/emqx_resource/test/emqx_resource_SUITE.erl b/apps/emqx_resource/test/emqx_resource_SUITE.erl index 97bc8da66..c9325af2b 100644 --- a/apps/emqx_resource/test/emqx_resource_SUITE.erl +++ b/apps/emqx_resource/test/emqx_resource_SUITE.erl @@ -414,7 +414,7 @@ t_query_counter_async_inflight(_) -> {_, {ok, _}} = ?wait_async_action( inc_counter_in_parallel(WindowSize, ReqOpts), - #{?snk_kind := resource_worker_flush_but_inflight_full}, + #{?snk_kind := buffer_worker_flush_but_inflight_full}, 1_000 ), fun(Trace) -> @@ -439,7 +439,7 @@ t_query_counter_async_inflight(_) -> emqx_resource:query(?ID, {inc_counter, 99}, #{ async_reply_fun => {Insert, [Tab0, tmp_query]} }), - #{?snk_kind := resource_worker_appended_to_queue}, + #{?snk_kind := buffer_worker_appended_to_queue}, 1_000 ), tap_metrics(?LINE), @@ -490,7 +490,7 @@ t_query_counter_async_inflight(_) -> {_, {ok, _}} = ?wait_async_action( inc_counter_in_parallel(WindowSize, ReqOpts), - #{?snk_kind := resource_worker_flush_but_inflight_full}, + #{?snk_kind := buffer_worker_flush_but_inflight_full}, 1_000 ), fun(Trace) -> @@ -596,7 +596,7 @@ t_query_counter_async_inflight_batch(_) -> {_, {ok, _}} = ?wait_async_action( inc_counter_in_parallel(NumMsgs, ReqOpts), - #{?snk_kind := resource_worker_flush_but_inflight_full}, + #{?snk_kind := buffer_worker_flush_but_inflight_full}, 5_000 ), fun(Trace) -> @@ -623,7 +623,7 @@ t_query_counter_async_inflight_batch(_) -> {ok, {ok, _}} = ?wait_async_action( emqx_resource:query(?ID, {inc_counter, 2}), - #{?snk_kind := resource_worker_flush_but_inflight_full}, + #{?snk_kind := buffer_worker_flush_but_inflight_full}, 5_000 ), ?assertMatch(0, ets:info(Tab0, size)), @@ -646,7 +646,7 @@ t_query_counter_async_inflight_batch(_) -> emqx_resource:query(?ID, {inc_counter, 3}, #{ async_reply_fun => {Insert, [Tab0, tmp_query]} }), - #{?snk_kind := resource_worker_appended_to_queue}, + #{?snk_kind := buffer_worker_appended_to_queue}, 1_000 ), tap_metrics(?LINE), @@ -706,7 +706,7 @@ t_query_counter_async_inflight_batch(_) -> {_, {ok, _}} = ?wait_async_action( inc_counter_in_parallel(NumMsgs, ReqOpts), - #{?snk_kind := resource_worker_flush_but_inflight_full}, + #{?snk_kind := buffer_worker_flush_but_inflight_full}, 5_000 ), fun(Trace) -> @@ -1055,7 +1055,7 @@ t_retry_batch(_Config) -> end, Payloads ), - #{?snk_kind := resource_worker_enter_blocked}, + #{?snk_kind := buffer_worker_enter_blocked}, 5_000 ), %% now the individual messages should have been counted @@ -1066,7 +1066,7 @@ t_retry_batch(_Config) -> %% batch shall remain enqueued. {ok, _} = snabbkaffe:block_until( - ?match_n_events(2, #{?snk_kind := resource_worker_retry_inflight_failed}), + ?match_n_events(2, #{?snk_kind := buffer_worker_retry_inflight_failed}), 5_000 ), %% should not have increased the matched count with the retries @@ -1078,7 +1078,7 @@ t_retry_batch(_Config) -> {ok, {ok, _}} = ?wait_async_action( ok = emqx_resource:simple_sync_query(?ID, resume), - #{?snk_kind := resource_worker_retry_inflight_succeeded}, + #{?snk_kind := buffer_worker_retry_inflight_succeeded}, 5_000 ), %% 1 more because of the `resume' call @@ -1140,7 +1140,7 @@ t_delete_and_re_create_with_same_name(_Config) -> ?assertMatch(ok, emqx_resource:simple_sync_query(?ID, block)), NumRequests = 10, {ok, SRef} = snabbkaffe:subscribe( - ?match_event(#{?snk_kind := resource_worker_enter_blocked}), + ?match_event(#{?snk_kind := buffer_worker_enter_blocked}), NumBufferWorkers, _Timeout = 5_000 ), @@ -1189,7 +1189,7 @@ t_delete_and_re_create_with_same_name(_Config) -> resume_interval => 1_000 } ), - #{?snk_kind := resource_worker_enter_running}, + #{?snk_kind := buffer_worker_enter_running}, 5_000 ), @@ -1271,13 +1271,13 @@ t_retry_sync_inflight(_Config) -> Res = emqx_resource:query(?ID, {big_payload, <<"a">>}, QueryOpts), TestPid ! {res, Res} end), - #{?snk_kind := resource_worker_retry_inflight_failed}, + #{?snk_kind := buffer_worker_retry_inflight_failed}, ResumeInterval * 2 ), {ok, {ok, _}} = ?wait_async_action( ok = emqx_resource:simple_sync_query(?ID, resume), - #{?snk_kind := resource_worker_retry_inflight_succeeded}, + #{?snk_kind := buffer_worker_retry_inflight_succeeded}, ResumeInterval * 3 ), receive @@ -1322,13 +1322,13 @@ t_retry_sync_inflight_batch(_Config) -> Res = emqx_resource:query(?ID, {big_payload, <<"a">>}, QueryOpts), TestPid ! {res, Res} end), - #{?snk_kind := resource_worker_retry_inflight_failed}, + #{?snk_kind := buffer_worker_retry_inflight_failed}, ResumeInterval * 2 ), {ok, {ok, _}} = ?wait_async_action( ok = emqx_resource:simple_sync_query(?ID, resume), - #{?snk_kind := resource_worker_retry_inflight_succeeded}, + #{?snk_kind := buffer_worker_retry_inflight_succeeded}, ResumeInterval * 3 ), receive @@ -1368,7 +1368,7 @@ t_retry_async_inflight(_Config) -> {ok, {ok, _}} = ?wait_async_action( emqx_resource:query(?ID, {big_payload, <<"b">>}, QueryOpts), - #{?snk_kind := resource_worker_retry_inflight_failed}, + #{?snk_kind := buffer_worker_retry_inflight_failed}, ResumeInterval * 2 ), @@ -1376,7 +1376,7 @@ t_retry_async_inflight(_Config) -> {ok, {ok, _}} = ?wait_async_action( emqx_resource:simple_sync_query(?ID, resume), - #{?snk_kind := resource_worker_enter_running}, + #{?snk_kind := buffer_worker_enter_running}, ResumeInterval * 2 ), ok @@ -1411,7 +1411,7 @@ t_retry_async_inflight_batch(_Config) -> {ok, {ok, _}} = ?wait_async_action( emqx_resource:query(?ID, {big_payload, <<"b">>}, QueryOpts), - #{?snk_kind := resource_worker_retry_inflight_failed}, + #{?snk_kind := buffer_worker_retry_inflight_failed}, ResumeInterval * 2 ), @@ -1419,7 +1419,7 @@ t_retry_async_inflight_batch(_Config) -> {ok, {ok, _}} = ?wait_async_action( emqx_resource:simple_sync_query(?ID, resume), - #{?snk_kind := resource_worker_enter_running}, + #{?snk_kind := buffer_worker_enter_running}, ResumeInterval * 2 ), ok @@ -1459,7 +1459,7 @@ t_async_pool_worker_death(_Config) -> NumReqs = 10, {ok, SRef0} = snabbkaffe:subscribe( - ?match_event(#{?snk_kind := resource_worker_appended_to_inflight}), + ?match_event(#{?snk_kind := buffer_worker_appended_to_inflight}), NumReqs, 1_000 ), @@ -1472,7 +1472,7 @@ t_async_pool_worker_death(_Config) -> %% grab one of the worker pids and kill it {ok, SRef1} = snabbkaffe:subscribe( - ?match_event(#{?snk_kind := resource_worker_worker_down_update}), + ?match_event(#{?snk_kind := buffer_worker_worker_down_update}), NumBufferWorkers, 10_000 ), @@ -1568,8 +1568,8 @@ assert_sync_retry_fail_then_succeed_inflight(Trace) -> ct:pal(" ~p", [Trace]), ?assert( ?strict_causality( - #{?snk_kind := resource_worker_flush_nack, ref := _Ref}, - #{?snk_kind := resource_worker_retry_inflight_failed, ref := _Ref}, + #{?snk_kind := buffer_worker_flush_nack, ref := _Ref}, + #{?snk_kind := buffer_worker_retry_inflight_failed, ref := _Ref}, Trace ) ), @@ -1577,8 +1577,8 @@ assert_sync_retry_fail_then_succeed_inflight(Trace) -> %% before restoring the resource health. ?assert( ?causality( - #{?snk_kind := resource_worker_retry_inflight_failed, ref := _Ref}, - #{?snk_kind := resource_worker_retry_inflight_succeeded, ref := _Ref}, + #{?snk_kind := buffer_worker_retry_inflight_failed, ref := _Ref}, + #{?snk_kind := buffer_worker_retry_inflight_succeeded, ref := _Ref}, Trace ) ), @@ -1588,8 +1588,8 @@ assert_async_retry_fail_then_succeed_inflight(Trace) -> ct:pal(" ~p", [Trace]), ?assert( ?strict_causality( - #{?snk_kind := resource_worker_reply_after_query, action := nack, ref := _Ref}, - #{?snk_kind := resource_worker_retry_inflight_failed, ref := _Ref}, + #{?snk_kind := buffer_worker_reply_after_query, action := nack, ref := _Ref}, + #{?snk_kind := buffer_worker_retry_inflight_failed, ref := _Ref}, Trace ) ), @@ -1597,8 +1597,8 @@ assert_async_retry_fail_then_succeed_inflight(Trace) -> %% before restoring the resource health. ?assert( ?causality( - #{?snk_kind := resource_worker_retry_inflight_failed, ref := _Ref}, - #{?snk_kind := resource_worker_retry_inflight_succeeded, ref := _Ref}, + #{?snk_kind := buffer_worker_retry_inflight_failed, ref := _Ref}, + #{?snk_kind := buffer_worker_retry_inflight_succeeded, ref := _Ref}, Trace ) ), diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_influxdb_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_influxdb_SUITE.erl index 0372c21ea..fc7dce418 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_influxdb_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_influxdb_SUITE.erl @@ -914,7 +914,7 @@ t_write_failure(Config) -> fun(Trace0) -> case QueryMode of sync -> - Trace = ?of_kind(resource_worker_flush_nack, Trace0), + Trace = ?of_kind(buffer_worker_flush_nack, Trace0), ?assertMatch([_ | _], Trace), [#{result := Result} | _] = Trace, ?assert( diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_mysql_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_mysql_SUITE.erl index ce38c357d..3bac01c66 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_mysql_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_mysql_SUITE.erl @@ -413,7 +413,7 @@ t_write_failure(Config) -> end), fun(Trace0) -> ct:pal("trace: ~p", [Trace0]), - Trace = ?of_kind(resource_worker_flush_nack, Trace0), + Trace = ?of_kind(buffer_worker_flush_nack, Trace0), ?assertMatch([#{result := {error, _}} | _], Trace), [#{result := {error, Error}} | _] = Trace, case Error of diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_pgsql_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_pgsql_SUITE.erl index f39ecc1dc..bdbbed8cf 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_pgsql_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_pgsql_SUITE.erl @@ -431,7 +431,7 @@ t_write_failure(Config) -> end), fun(Trace0) -> ct:pal("trace: ~p", [Trace0]), - Trace = ?of_kind(resource_worker_flush_nack, Trace0), + Trace = ?of_kind(buffer_worker_flush_nack, Trace0), ?assertMatch([#{result := {error, _}} | _], Trace), [#{result := {error, Error}} | _] = Trace, case Error of From a5424959c6fec3fa1ddb0bf4efd1bf71d6637206 Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Wed, 18 Jan 2023 15:32:44 -0300 Subject: [PATCH 0279/1802] refactor: avoid operator precedence bugs --- lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_mysql.erl | 3 ++- lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_pgsql.erl | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_mysql.erl b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_mysql.erl index 71f8a8399..fadf05848 100644 --- a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_mysql.erl +++ b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_mysql.erl @@ -95,7 +95,8 @@ fields("config") -> } )} ] ++ - emqx_connector_mysql:fields(config) -- emqx_connector_schema_lib:prepare_statement_fields(); + (emqx_connector_mysql:fields(config) -- + emqx_connector_schema_lib:prepare_statement_fields()); fields("creation_opts") -> Opts = emqx_resource_schema:fields("creation_opts"), [O || {Field, _} = O <- Opts, not is_hidden_opts(Field)]; diff --git a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_pgsql.erl b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_pgsql.erl index 7e21d4dd7..8bf7b1969 100644 --- a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_pgsql.erl +++ b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_pgsql.erl @@ -97,7 +97,8 @@ fields("config") -> } )} ] ++ - emqx_connector_pgsql:fields(config) -- emqx_connector_schema_lib:prepare_statement_fields(); + (emqx_connector_pgsql:fields(config) -- + emqx_connector_schema_lib:prepare_statement_fields()); fields("creation_opts") -> Opts = emqx_resource_schema:fields("creation_opts"), [O || {Field, _} = O <- Opts, not is_hidden_opts(Field)]; From 4cdfa088ae9eb74e2d278bfef97c3eb6a960417f Mon Sep 17 00:00:00 2001 From: Rory Z Date: Thu, 19 Jan 2023 11:22:09 +0800 Subject: [PATCH 0280/1802] feat(helm): user can define service account by userself --- deploy/charts/emqx-enterprise/README.md | 14 ++++++----- .../templates/StatefulSet.yaml | 4 +-- .../emqx-enterprise/templates/_helpers.tpl | 11 ++++++++ .../emqx-enterprise/templates/rbac.yaml | 25 ++++++++++++++++--- deploy/charts/emqx-enterprise/values.yaml | 9 +++++++ deploy/charts/emqx/README.md | 3 +++ deploy/charts/emqx/templates/StatefulSet.yaml | 4 +-- deploy/charts/emqx/templates/_helpers.tpl | 11 ++++++++ deploy/charts/emqx/templates/rbac.yaml | 25 ++++++++++++++++--- deploy/charts/emqx/values.yaml | 9 +++++++ 10 files changed, 97 insertions(+), 18 deletions(-) diff --git a/deploy/charts/emqx-enterprise/README.md b/deploy/charts/emqx-enterprise/README.md index 33a3fa22f..2899dc7e0 100644 --- a/deploy/charts/emqx-enterprise/README.md +++ b/deploy/charts/emqx-enterprise/README.md @@ -14,14 +14,14 @@ To install the chart with the release name `my-emqx`: + From github ``` $ git clone https://github.com/emqx/emqx.git - $ cd emqx/deploy/charts/emqx + $ cd emqx/deploy/charts/emqx-enterprise $ helm install my-emqx . ``` + From chart repos ``` helm repo add emqx https://repos.emqx.io/charts - helm install my-emqx emqx/emqx + helm install my-emqx emqx/emqx-enterprise ``` > If you want to install an unstable version, you need to add `--devel` when you execute the `helm install` command. @@ -43,6 +43,9 @@ The following table lists the configurable parameters of the emqx chart and thei | `image.repository` | EMQX Image name | `emqx/emqx-enterprise` | | `image.pullPolicy` | The image pull policy | IfNotPresent | | `image.pullSecrets ` | The image pull secrets | `[]` (does not add image pull secrets to deployed pods) | +| `serviceAccount.create` | If `true`, create a new service account | `true` | +| `serviceAccount.name` | Service account to be used. If not set and `serviceAccount.create` is `true`, a name is generated using the fullname template | | +| `serviceAccount.annotations` | Annotations to add to the service account | | | `envFromSecret` | The name pull a secret in the same kubernetes namespace which contains values that will be added to the environment | nil | | `recreatePods` | Forces the recreation of pods during upgrades, which can be useful to always apply the most recent configuration. | false | | `podAnnotations ` | Annotations for pod | `{}` | @@ -102,10 +105,9 @@ The following table lists the configurable [EMQX](https://www.emqx.io/)-specific default values. Parameter | Description | Default Value --- | --- | --- -`emqxConfig` | Map of [configuration](https://www.emqx.io/docs/en/latest/configuration/configuration.html) items -expressed as [environment variables](https://www.emqx.io/docs/en/v4.3/configuration/environment-variable.html) (prefix -can be omitted) or using the configuration -files [namespaced dotted notation](https://www.emqx.io/docs/en/latest/configuration/configuration.html) | `nil` +`emqxConfig` | Map of [configuration](https://www.emqx.io/docs/en/v5.0/admin/cfg.html) items +expressed as [environment variables](https://www.emqx.io/docs/en/v5.0/admin/cfg.html#environment-variables) (prefix `EMQX_` can be omitted) or using the configuration +files [namespaced dotted notation](https://www.emqx.io/docs/en/v5.0/admin/cfg.html#syntax) | `nil` `emqxLicenseSecretName` | Name of the secret that holds the license information | `nil` ## SSL settings diff --git a/deploy/charts/emqx-enterprise/templates/StatefulSet.yaml b/deploy/charts/emqx-enterprise/templates/StatefulSet.yaml index 0917cadac..3932dc727 100644 --- a/deploy/charts/emqx-enterprise/templates/StatefulSet.yaml +++ b/deploy/charts/emqx-enterprise/templates/StatefulSet.yaml @@ -52,6 +52,7 @@ spec: checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum | quote }} {{- end }} spec: + serviceAccountName: {{ include "emqx.serviceAccountName" . }} volumes: {{- if .Values.ssl.enabled }} - name: ssl-cert @@ -73,9 +74,6 @@ spec: secret: secretName: {{ .Values.emqxLicenseSecretName }} {{- end }} - {{- if eq .Values.emqxConfig.EMQX_CLUSTER__DISCOVERY_STRATEGY "k8s"}} - serviceAccountName: {{ include "emqx.fullname" . }} - {{- end }} {{- if .Values.podSecurityContext.enabled }} securityContext: {{- omit .Values.podSecurityContext "enabled" | toYaml | nindent 8 }} {{- end }} diff --git a/deploy/charts/emqx-enterprise/templates/_helpers.tpl b/deploy/charts/emqx-enterprise/templates/_helpers.tpl index e55740b01..64acf7961 100644 --- a/deploy/charts/emqx-enterprise/templates/_helpers.tpl +++ b/deploy/charts/emqx-enterprise/templates/_helpers.tpl @@ -42,3 +42,14 @@ Get ssl secret name . {{ include "emqx.fullname" . }}-tls {{- end -}} {{- end -}} + +{{/* +Create the name of the service account to use +*/}} +{{- define "emqx.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "emqx.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/deploy/charts/emqx-enterprise/templates/rbac.yaml b/deploy/charts/emqx-enterprise/templates/rbac.yaml index f2cdd3601..9faf4e147 100644 --- a/deploy/charts/emqx-enterprise/templates/rbac.yaml +++ b/deploy/charts/emqx-enterprise/templates/rbac.yaml @@ -1,10 +1,23 @@ -{{- if eq .Values.emqxConfig.EMQX_CLUSTER__DISCOVERY_STRATEGY "k8s"}} +{{- if .Values.serviceAccount.create }} apiVersion: v1 kind: ServiceAccount metadata: + name: {{ include "emqx.serviceAccountName" . }} namespace: {{ .Release.Namespace }} - name: {{ include "emqx.fullname" . }} + labels: + app.kubernetes.io/name: {{ include "emqx.name" . }} + helm.sh/chart: {{ include "emqx.chart" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} + --- +{{- if .Values.serviceAccount.create }} +{{- if eq .Values.emqxConfig.EMQX_CLUSTER__DISCOVERY_STRATEGY "k8s" }} kind: Role {{- if semverCompare ">=1.17-0" .Capabilities.KubeVersion.GitVersion }} apiVersion: rbac.authorization.k8s.io/v1 @@ -23,7 +36,12 @@ rules: - get - watch - list +{{- end }} +{{- end }} + --- +{{- if .Values.serviceAccount.create }} +{{- if eq .Values.emqxConfig.EMQX_CLUSTER__DISCOVERY_STRATEGY "k8s" }} kind: RoleBinding {{- if semverCompare ">=1.17-0" .Capabilities.KubeVersion.GitVersion }} apiVersion: rbac.authorization.k8s.io/v1 @@ -35,10 +53,11 @@ metadata: name: {{ include "emqx.fullname" . }} subjects: - kind: ServiceAccount - name: {{ include "emqx.fullname" . }} + name: {{ include "emqx.serviceAccountName" . }} namespace: {{ .Release.Namespace }} roleRef: kind: Role name: {{ include "emqx.fullname" . }} apiGroup: rbac.authorization.k8s.io {{- end }} +{{- end }} diff --git a/deploy/charts/emqx-enterprise/values.yaml b/deploy/charts/emqx-enterprise/values.yaml index 0396b2b20..b9507c5a0 100644 --- a/deploy/charts/emqx-enterprise/values.yaml +++ b/deploy/charts/emqx-enterprise/values.yaml @@ -16,6 +16,15 @@ image: # pullSecrets: # - myRegistryKeySecretName +serviceAccount: + # Specifies whether a service account should be created + # If set false, means you need create service account by yourself + create: true + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + # Annotations to add to the service account + annotations: {} # The name of a secret in the same kubernetes namespace which contains values to # be added to the environment (must be manually created) diff --git a/deploy/charts/emqx/README.md b/deploy/charts/emqx/README.md index 1a2090320..6ee3617ce 100644 --- a/deploy/charts/emqx/README.md +++ b/deploy/charts/emqx/README.md @@ -43,6 +43,9 @@ The following table lists the configurable parameters of the emqx chart and thei | `image.repository` | EMQX Image name | emqx/emqx | | `image.pullPolicy` | The image pull policy | IfNotPresent | | `image.pullSecrets ` | The image pull secrets | `[]` (does not add image pull secrets to deployed pods) | +| `serviceAccount.create` | If `true`, create a new service account | `true` | +| `serviceAccount.name` | Service account to be used. If not set and `serviceAccount.create` is `true`, a name is generated using the fullname template | | +| `serviceAccount.annotations` | Annotations to add to the service account | | | `envFromSecret` | The name pull a secret in the same kubernetes namespace which contains values that will be added to the environment | nil | | `recreatePods` | Forces the recreation of pods during upgrades, which can be useful to always apply the most recent configuration. | false | | `podAnnotations ` | Annotations for pod | `{}` | diff --git a/deploy/charts/emqx/templates/StatefulSet.yaml b/deploy/charts/emqx/templates/StatefulSet.yaml index 0917cadac..3932dc727 100644 --- a/deploy/charts/emqx/templates/StatefulSet.yaml +++ b/deploy/charts/emqx/templates/StatefulSet.yaml @@ -52,6 +52,7 @@ spec: checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum | quote }} {{- end }} spec: + serviceAccountName: {{ include "emqx.serviceAccountName" . }} volumes: {{- if .Values.ssl.enabled }} - name: ssl-cert @@ -73,9 +74,6 @@ spec: secret: secretName: {{ .Values.emqxLicenseSecretName }} {{- end }} - {{- if eq .Values.emqxConfig.EMQX_CLUSTER__DISCOVERY_STRATEGY "k8s"}} - serviceAccountName: {{ include "emqx.fullname" . }} - {{- end }} {{- if .Values.podSecurityContext.enabled }} securityContext: {{- omit .Values.podSecurityContext "enabled" | toYaml | nindent 8 }} {{- end }} diff --git a/deploy/charts/emqx/templates/_helpers.tpl b/deploy/charts/emqx/templates/_helpers.tpl index e55740b01..64acf7961 100644 --- a/deploy/charts/emqx/templates/_helpers.tpl +++ b/deploy/charts/emqx/templates/_helpers.tpl @@ -42,3 +42,14 @@ Get ssl secret name . {{ include "emqx.fullname" . }}-tls {{- end -}} {{- end -}} + +{{/* +Create the name of the service account to use +*/}} +{{- define "emqx.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "emqx.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/deploy/charts/emqx/templates/rbac.yaml b/deploy/charts/emqx/templates/rbac.yaml index f2cdd3601..9faf4e147 100644 --- a/deploy/charts/emqx/templates/rbac.yaml +++ b/deploy/charts/emqx/templates/rbac.yaml @@ -1,10 +1,23 @@ -{{- if eq .Values.emqxConfig.EMQX_CLUSTER__DISCOVERY_STRATEGY "k8s"}} +{{- if .Values.serviceAccount.create }} apiVersion: v1 kind: ServiceAccount metadata: + name: {{ include "emqx.serviceAccountName" . }} namespace: {{ .Release.Namespace }} - name: {{ include "emqx.fullname" . }} + labels: + app.kubernetes.io/name: {{ include "emqx.name" . }} + helm.sh/chart: {{ include "emqx.chart" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} + --- +{{- if .Values.serviceAccount.create }} +{{- if eq .Values.emqxConfig.EMQX_CLUSTER__DISCOVERY_STRATEGY "k8s" }} kind: Role {{- if semverCompare ">=1.17-0" .Capabilities.KubeVersion.GitVersion }} apiVersion: rbac.authorization.k8s.io/v1 @@ -23,7 +36,12 @@ rules: - get - watch - list +{{- end }} +{{- end }} + --- +{{- if .Values.serviceAccount.create }} +{{- if eq .Values.emqxConfig.EMQX_CLUSTER__DISCOVERY_STRATEGY "k8s" }} kind: RoleBinding {{- if semverCompare ">=1.17-0" .Capabilities.KubeVersion.GitVersion }} apiVersion: rbac.authorization.k8s.io/v1 @@ -35,10 +53,11 @@ metadata: name: {{ include "emqx.fullname" . }} subjects: - kind: ServiceAccount - name: {{ include "emqx.fullname" . }} + name: {{ include "emqx.serviceAccountName" . }} namespace: {{ .Release.Namespace }} roleRef: kind: Role name: {{ include "emqx.fullname" . }} apiGroup: rbac.authorization.k8s.io {{- end }} +{{- end }} diff --git a/deploy/charts/emqx/values.yaml b/deploy/charts/emqx/values.yaml index 4fb263c7a..0423c8cdf 100644 --- a/deploy/charts/emqx/values.yaml +++ b/deploy/charts/emqx/values.yaml @@ -16,6 +16,15 @@ image: # pullSecrets: # - myRegistryKeySecretName +serviceAccount: + # Specifies whether a service account should be created + # If set false, means you need create service account by yourself + create: true + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + # Annotations to add to the service account + annotations: {} # The name of a secret in the same kubernetes namespace which contains values to # be added to the environment (must be manually created) From a4583d2afa1fa9d404b0a8fea41653c76ebb914e Mon Sep 17 00:00:00 2001 From: Parham Alvani Date: Thu, 19 Jan 2023 13:46:44 +0330 Subject: [PATCH 0281/1802] fix: Correct chart variable for internal MQTT port and HTTPS dashboard. These variables are coming from v4, and they were incorrect. This commit will fix them. --- deploy/charts/emqx-enterprise/templates/StatefulSet.yaml | 8 ++++---- deploy/charts/emqx-enterprise/templates/service.yaml | 2 +- deploy/charts/emqx/templates/StatefulSet.yaml | 8 ++++---- deploy/charts/emqx/templates/service.yaml | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/deploy/charts/emqx-enterprise/templates/StatefulSet.yaml b/deploy/charts/emqx-enterprise/templates/StatefulSet.yaml index 0917cadac..022c4ee62 100644 --- a/deploy/charts/emqx-enterprise/templates/StatefulSet.yaml +++ b/deploy/charts/emqx-enterprise/templates/StatefulSet.yaml @@ -107,13 +107,13 @@ spec: containerPort: {{ .Values.emqxConfig.EMQX_LISTENERS__WSS__DEFAULT__BIND | default 8084 }} - name: dashboard containerPort: {{ .Values.emqxConfig.EMQX_DASHBOARD__LISTENER__HTTP__BIND | default 18083 }} - {{- if not (empty .Values.emqxConfig.EMQX_LISTENERS__TCP__DEFAULT) }} + {{- if not (empty .Values.emqxConfig.EMQX_LISTENERS__TCP__INTERNAL__BIND) }} - name: internalmqtt - containerPort: {{ .Values.emqxConfig.EMQX_LISTENERS__TCP__DEFAULT }} + containerPort: {{ .Values.emqxConfig.EMQX_LISTENERS__TCP__INTERNAL__BIND }} {{- end }} - {{- if not (empty .Values.emqxConfig.EMQX_DASHBOARD__LISTENER__HTTPS) }} + {{- if not (empty .Values.emqxConfig.EMQX_DASHBOARD__LISTENER__HTTPS__BIND) }} - name: dashboardtls - containerPort: {{ .Values.emqxConfig.EMQX_DASHBOARD__LISTENER__HTTPS }} + containerPort: {{ .Values.emqxConfig.EMQX_DASHBOARD__LISTENER__HTTPS__BIND }} {{- end }} - name: ekka containerPort: 4370 diff --git a/deploy/charts/emqx-enterprise/templates/service.yaml b/deploy/charts/emqx-enterprise/templates/service.yaml index 301213150..0fe3dc411 100644 --- a/deploy/charts/emqx-enterprise/templates/service.yaml +++ b/deploy/charts/emqx-enterprise/templates/service.yaml @@ -38,7 +38,7 @@ spec: {{- else if eq .Values.service.type "ClusterIP" }} nodePort: null {{- end }} - {{- if not (empty .Values.emqxConfig.EMQX_LISTENERS__TCP__DEFAULT) }} + {{- if not (empty .Values.emqxConfig.EMQX_LISTENERS__TCP__INTERNAL__BIND) }} - name: internalmqtt port: {{ .Values.service.internalmqtt | default 11883 }} protocol: TCP diff --git a/deploy/charts/emqx/templates/StatefulSet.yaml b/deploy/charts/emqx/templates/StatefulSet.yaml index 0917cadac..022c4ee62 100644 --- a/deploy/charts/emqx/templates/StatefulSet.yaml +++ b/deploy/charts/emqx/templates/StatefulSet.yaml @@ -107,13 +107,13 @@ spec: containerPort: {{ .Values.emqxConfig.EMQX_LISTENERS__WSS__DEFAULT__BIND | default 8084 }} - name: dashboard containerPort: {{ .Values.emqxConfig.EMQX_DASHBOARD__LISTENER__HTTP__BIND | default 18083 }} - {{- if not (empty .Values.emqxConfig.EMQX_LISTENERS__TCP__DEFAULT) }} + {{- if not (empty .Values.emqxConfig.EMQX_LISTENERS__TCP__INTERNAL__BIND) }} - name: internalmqtt - containerPort: {{ .Values.emqxConfig.EMQX_LISTENERS__TCP__DEFAULT }} + containerPort: {{ .Values.emqxConfig.EMQX_LISTENERS__TCP__INTERNAL__BIND }} {{- end }} - {{- if not (empty .Values.emqxConfig.EMQX_DASHBOARD__LISTENER__HTTPS) }} + {{- if not (empty .Values.emqxConfig.EMQX_DASHBOARD__LISTENER__HTTPS__BIND) }} - name: dashboardtls - containerPort: {{ .Values.emqxConfig.EMQX_DASHBOARD__LISTENER__HTTPS }} + containerPort: {{ .Values.emqxConfig.EMQX_DASHBOARD__LISTENER__HTTPS__BIND }} {{- end }} - name: ekka containerPort: 4370 diff --git a/deploy/charts/emqx/templates/service.yaml b/deploy/charts/emqx/templates/service.yaml index 301213150..5b6376a85 100644 --- a/deploy/charts/emqx/templates/service.yaml +++ b/deploy/charts/emqx/templates/service.yaml @@ -121,7 +121,7 @@ spec: port: {{ .Values.service.mqtt | default 1883 }} protocol: TCP targetPort: mqtt - {{- if not (empty .Values.emqxConfig.EMQX_LISTENERS__TCP__DEFAULT) }} + {{- if not (empty .Values.emqxConfig.EMQX_LISTENERS__TCP__INTERNAL__BIND) }} - name: internalmqtt port: {{ .Values.service.internalmqtt | default 11883 }} protocol: TCP From 622e42a8caf7066ec8365dd200655a40ec13e305 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Thu, 19 Jan 2023 11:45:09 +0100 Subject: [PATCH 0282/1802] chore: pin enterprise dashboard at version e1.0.1 --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index fc87f1d95..0e2aacf31 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ export EMQX_DEFAULT_RUNNER = debian:11-slim export OTP_VSN ?= $(shell $(CURDIR)/scripts/get-otp-vsn.sh) export ELIXIR_VSN ?= $(shell $(CURDIR)/scripts/get-elixir-vsn.sh) export EMQX_DASHBOARD_VERSION ?= v1.1.5 -export EMQX_EE_DASHBOARD_VERSION ?= e1.0.1-beta.13 +export EMQX_EE_DASHBOARD_VERSION ?= e1.0.1 export EMQX_REL_FORM ?= tgz export QUICER_DOWNLOAD_FROM_RELEASE = 1 ifeq ($(OS),Windows_NT) From d755b43c774bf623f58f6db82059cbe69dcbbbd9 Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Thu, 19 Jan 2023 09:24:45 -0300 Subject: [PATCH 0283/1802] fix(jwt_worker): handle exceptions when decoding jwk from pem Returns a more controlled error if users attempt to use the Service Account JSON from the GCP PubSub example from swagger, which is redacted. --- .../src/emqx_connector_jwt_worker.erl | 13 +++++++++++- .../test/emqx_connector_jwt_worker_SUITE.erl | 20 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/apps/emqx_connector/src/emqx_connector_jwt_worker.erl b/apps/emqx_connector/src/emqx_connector_jwt_worker.erl index e51b9bbee..b13e74a4d 100644 --- a/apps/emqx_connector/src/emqx_connector_jwt_worker.erl +++ b/apps/emqx_connector/src/emqx_connector_jwt_worker.erl @@ -120,7 +120,7 @@ init(#{private_key := PrivateKeyPEM} = Config) -> handle_continue({make_key, PrivateKeyPEM}, State0) -> ?tp(connector_jwt_worker_make_key, #{state => State0}), - case jose_jwk:from_pem(PrivateKeyPEM) of + try jose_jwk:from_pem(PrivateKeyPEM) of JWK = #jose_jwk{} -> State = State0#{jwk := JWK}, {noreply, State, {continue, create_token}}; @@ -135,6 +135,17 @@ handle_continue({make_key, PrivateKeyPEM}, State0) -> Error = {invalid_private_key, Error0}, ?tp(connector_jwt_worker_startup_error, #{error => Error}), {stop, {shutdown, {error, Error}}, State0} + catch + Kind:Error -> + ?tp( + error, + connector_jwt_worker_startup_error, + #{ + kind => Kind, + error => Error + } + ), + {stop, {shutdown, {error, Error}}, State0} end; handle_continue(create_token, State0) -> State = generate_and_store_jwt(State0), diff --git a/apps/emqx_connector/test/emqx_connector_jwt_worker_SUITE.erl b/apps/emqx_connector/test/emqx_connector_jwt_worker_SUITE.erl index eb104801c..a079d632f 100644 --- a/apps/emqx_connector/test/emqx_connector_jwt_worker_SUITE.erl +++ b/apps/emqx_connector/test/emqx_connector_jwt_worker_SUITE.erl @@ -364,3 +364,23 @@ t_unknown_requests(_Config) -> gen_server:cast(Worker, unknown_cast), ?assertEqual({error, bad_call}, gen_server:call(Worker, unknown_call)), ok. + +t_truncated_private_key(_Config) -> + Config0 = generate_config(), + Config = Config0#{private_key := <<"-----BEGIN PRIVATE KEY-----\nMIIEvQI...">>}, + process_flag(trap_exit, true), + ?check_trace( + ?wait_async_action( + ?assertMatch({ok, _}, emqx_connector_jwt_worker:start_link(Config)), + #{?snk_kind := connector_jwt_worker_startup_error}, + 1_000 + ), + fun(Trace) -> + ?assertMatch( + [#{error := function_clause}], + ?of_kind(connector_jwt_worker_startup_error, Trace) + ), + ok + end + ), + ok. From 6bde1173afecd84ef5dc644cc1bfec45d9800e69 Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Thu, 19 Jan 2023 13:26:36 +0100 Subject: [PATCH 0284/1802] chore: add translations --- apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf | 6 +++--- changes/v5.0.15/feat-9802.zh.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf b/apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf index cc0d0244b..8efacbdfb 100644 --- a/apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf +++ b/apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf @@ -94,12 +94,12 @@ Note: `sample_interval` should be a divisor of 60.""" } proxy_header { desc { - en: "Enable support for HAProxy header. Be aware once enabled regular HTTP requests can't be handled anymore." - zh: "[FIXME]" + en: "Enable support for `HAProxy` header. Be aware once enabled regular HTTP requests can't be handled anymore." + zh: "开启对 `HAProxy` 的支持,注意:一旦开启了这个功能,就无法再处理普通的 HTTP 请求了。" } label: { en: "Enable support for HAProxy header" - zh: "[FIXME]" + zh: "开启对 `HAProxy` 的支持" } } desc_dashboard { diff --git a/changes/v5.0.15/feat-9802.zh.md b/changes/v5.0.15/feat-9802.zh.md index 97a926b1a..9afcd8f17 100644 --- a/changes/v5.0.15/feat-9802.zh.md +++ b/changes/v5.0.15/feat-9802.zh.md @@ -1 +1 @@ -[FIXME] Support HAProxy protocol for dashboard API. +现在 dashboard 增加了对 `HAProxy` 协议的支持。 From 16f45a60fdc52fa275eeaa53ef5938db0c6b729a Mon Sep 17 00:00:00 2001 From: JimMoen Date: Fri, 20 Jan 2023 11:46:11 +0800 Subject: [PATCH 0285/1802] chore: i18n typo fix --- apps/emqx_connector/i18n/emqx_connector_redis.conf | 2 +- apps/emqx_resource/i18n/emqx_resource_schema_i18n.conf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/emqx_connector/i18n/emqx_connector_redis.conf b/apps/emqx_connector/i18n/emqx_connector_redis.conf index f42f38f30..e8e05d08f 100644 --- a/apps/emqx_connector/i18n/emqx_connector_redis.conf +++ b/apps/emqx_connector/i18n/emqx_connector_redis.conf @@ -54,7 +54,7 @@ The Redis default port 6379 is used if `[:Port]` is not specified. zh: """ 将要连接的 IPv4 或 IPv6 地址,或者主机名。
主机名具有以下形式:`Host[:Port]`。
-如果未指定 `[:Port]`,则使用 MongoDB 默认端口 27017。 +如果未指定 `[:Port]`,则使用 Redis 默认端口 6379。 """ } label: { diff --git a/apps/emqx_resource/i18n/emqx_resource_schema_i18n.conf b/apps/emqx_resource/i18n/emqx_resource_schema_i18n.conf index de76967ab..7e9fcd9e7 100644 --- a/apps/emqx_resource/i18n/emqx_resource_schema_i18n.conf +++ b/apps/emqx_resource/i18n/emqx_resource_schema_i18n.conf @@ -26,7 +26,7 @@ emqx_resource_schema { desc { en: """The number of buffer workers. Only applicable for egress type bridges. For bridges only have ingress direction data flow, it can be set to 0 otherwise must be greater than 0.""" - zh: """缓存队列 worker 数量。仅对 egress 类型的桥接有意义。当桥接仅有 ingress 方向时,可设置为 0,否则必须大于 0)。""" + zh: """缓存队列 worker 数量。仅对 egress 类型的桥接有意义。当桥接仅有 ingress 方向时,可设置为 0,否则必须大于 0。""" } label { en: """Buffer Pool Size""" From 34c1ebbe483daf19e7aba2c53116f28698075322 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Fri, 20 Jan 2023 11:09:57 +0100 Subject: [PATCH 0286/1802] chore: upgrade to dashboard v1.1.6 --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 0e2aacf31..4be33b567 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ export EMQX_DEFAULT_BUILDER = ghcr.io/emqx/emqx-builder/5.0-26:1.13.4-24.3.4.2-1 export EMQX_DEFAULT_RUNNER = debian:11-slim export OTP_VSN ?= $(shell $(CURDIR)/scripts/get-otp-vsn.sh) export ELIXIR_VSN ?= $(shell $(CURDIR)/scripts/get-elixir-vsn.sh) -export EMQX_DASHBOARD_VERSION ?= v1.1.5 +export EMQX_DASHBOARD_VERSION ?= v1.1.6 export EMQX_EE_DASHBOARD_VERSION ?= e1.0.1 export EMQX_REL_FORM ?= tgz export QUICER_DOWNLOAD_FROM_RELEASE = 1 From 57607ca0ce6dd7729bd1d34beced74de803ea5f7 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Fri, 20 Jan 2023 11:10:46 +0100 Subject: [PATCH 0287/1802] chore: prepare for v5.0.15 release --- apps/emqx/include/emqx_release.hrl | 2 +- deploy/charts/emqx/Chart.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/emqx/include/emqx_release.hrl b/apps/emqx/include/emqx_release.hrl index a4963f31a..e9eab302c 100644 --- a/apps/emqx/include/emqx_release.hrl +++ b/apps/emqx/include/emqx_release.hrl @@ -32,7 +32,7 @@ %% `apps/emqx/src/bpapi/README.md' %% Community edition --define(EMQX_RELEASE_CE, "5.0.14"). +-define(EMQX_RELEASE_CE, "5.0.15-rc.1"). %% Enterprise edition -define(EMQX_RELEASE_EE, "5.0.0-rc.1"). diff --git a/deploy/charts/emqx/Chart.yaml b/deploy/charts/emqx/Chart.yaml index ae48f9de2..fb689839c 100644 --- a/deploy/charts/emqx/Chart.yaml +++ b/deploy/charts/emqx/Chart.yaml @@ -14,8 +14,8 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. -version: 5.0.14 +version: 5.0.15 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. -appVersion: 5.0.14 +appVersion: 5.0.15 From 8c52264c41064c2fa8c96016afa70e6ba82b4be5 Mon Sep 17 00:00:00 2001 From: Kjell Winblad Date: Mon, 16 Jan 2023 13:01:51 +0100 Subject: [PATCH 0288/1802] fix: atom leak when doing Kafka bridge dry-run A new atom was created every time one did a dry run of a Kafka bridge (that is, clicking the Test button in the settings dialog for the bridge). After this fix, we will only create a new atom when a bridge with a new name is created. This should be acceptable as bridges with new names are created relatively rarely and it seems to be useful to have an unique atom for every Wolff producer. Fixes: https://emqx.atlassian.net/browse/EMQX-8739 --- .../kafka/emqx_bridge_impl_kafka_producer.erl | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/lib-ee/emqx_ee_bridge/src/kafka/emqx_bridge_impl_kafka_producer.erl b/lib-ee/emqx_ee_bridge/src/kafka/emqx_bridge_impl_kafka_producer.erl index bec3c49fa..25741b6cd 100644 --- a/lib-ee/emqx_ee_bridge/src/kafka/emqx_bridge_impl_kafka_producer.erl +++ b/lib-ee/emqx_ee_bridge/src/kafka/emqx_bridge_impl_kafka_producer.erl @@ -75,7 +75,16 @@ on_start(InstId, Config) -> }), throw(failed_to_start_kafka_client) end, - WolffProducerConfig = producers_config(BridgeName, ClientId, ProducerConfig), + %% Check if this is a dry run + TestIdStart = string:find(InstId, ?TEST_ID_PREFIX), + IsDryRun = + case TestIdStart of + nomatch -> + false; + _ -> + string:equal(TestIdStart, InstId) + end, + WolffProducerConfig = producers_config(BridgeName, ClientId, ProducerConfig, IsDryRun), case wolff:ensure_supervised_producers(ClientId, KafkaTopic, WolffProducerConfig) of {ok, Producers} -> {ok, #{ @@ -241,7 +250,7 @@ ssl(#{enable := true} = SSL) -> ssl(_) -> []. -producers_config(BridgeName, ClientId, Input) -> +producers_config(BridgeName, ClientId, Input, IsDryRun) -> #{ max_batch_bytes := MaxBatchBytes, compression := Compression, @@ -271,7 +280,7 @@ producers_config(BridgeName, ClientId, Input) -> BridgeType = kafka, ResourceID = emqx_bridge_resource:resource_id(BridgeType, BridgeName), #{ - name => make_producer_name(BridgeName), + name => make_producer_name(BridgeName, IsDryRun), partitioner => partitioner(PartitionStrategy), partition_count_refresh_interval_seconds => PCntRefreshInterval, replayq_dir => ReplayqDir, @@ -302,12 +311,20 @@ make_client_id(BridgeName) -> %% Producer name must be an atom which will be used as a ETS table name for %% partition worker lookup. -make_producer_name(BridgeName) when is_atom(BridgeName) -> - make_producer_name(atom_to_list(BridgeName)); -make_producer_name(BridgeName) -> - %% Woff needs atom for ets table name registration - %% The assumption here is bridge is not often re-created - binary_to_atom(iolist_to_binary(["kafka_producer_", BridgeName])). +make_producer_name(BridgeName, IsDryRun) when is_atom(BridgeName) -> + make_producer_name(atom_to_list(BridgeName), IsDryRun); +make_producer_name(BridgeName, IsDryRun) -> + %% Woff needs an atom for ets table name registration. The assumption here is + %% that bridges with new names are not often created. + case IsDryRun of + true -> + %% It is a dry run and we don't want to leak too many atoms + %% so we use the default producer name instead of creating + %% an unique name. + probing_wolff_producers; + false -> + binary_to_atom(iolist_to_binary(["kafka_producer_", BridgeName])) + end. with_log_at_error(Fun, Log) -> try From 6fa6c679bb7f5c3dbe74a3450984d9875d63ca2a Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Thu, 19 Jan 2023 18:07:08 -0300 Subject: [PATCH 0289/1802] feat(buffer_worker): add expiration time to requests With this, we avoid performing work or replying to callers that are no longer waiting on a result. Also introduces two new counters: - `dropped.expired` :: happens when a request expires before being sent downstream - `late_reply` :: when a response is receive from downstream, but the caller is no longer for a reply because the request has expired, and the caller might even have retried it. --- apps/emqx_bridge/include/emqx_bridge.hrl | 10 +- apps/emqx_bridge/src/emqx_bridge_api.erl | 12 +- .../test/emqx_bridge_mqtt_SUITE.erl | 15 +- apps/emqx_resource/include/emqx_resource.hrl | 2 + .../src/emqx_resource_buffer_worker.erl | 319 ++++++-- .../src/emqx_resource_manager.erl | 2 + .../src/emqx_resource_metrics.erl | 37 + .../test/emqx_connector_demo.erl | 2 +- .../test/emqx_resource_SUITE.erl | 770 +++++++++++++++++- .../test/emqx_ee_bridge_gcp_pubsub_SUITE.erl | 8 +- .../src/emqx_ee_connector_gcp_pubsub.erl | 1 - 11 files changed, 1090 insertions(+), 88 deletions(-) diff --git a/apps/emqx_bridge/include/emqx_bridge.hrl b/apps/emqx_bridge/include/emqx_bridge.hrl index d8229cc77..d062b4b7f 100644 --- a/apps/emqx_bridge/include/emqx_bridge.hrl +++ b/apps/emqx_bridge/include/emqx_bridge.hrl @@ -16,19 +16,21 @@ -define(EMPTY_METRICS, ?METRICS( - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ) ). -define(METRICS( Dropped, DroppedOther, + DroppedExpired, DroppedQueueFull, DroppedResourceNotFound, DroppedResourceStopped, Matched, Queued, Retried, + LateReply, SentFailed, SentInflight, SentSucc, @@ -40,12 +42,14 @@ #{ 'dropped' => Dropped, 'dropped.other' => DroppedOther, + 'dropped.expired' => DroppedExpired, 'dropped.queue_full' => DroppedQueueFull, 'dropped.resource_not_found' => DroppedResourceNotFound, 'dropped.resource_stopped' => DroppedResourceStopped, 'matched' => Matched, 'queuing' => Queued, 'retried' => Retried, + 'late_reply' => LateReply, 'failed' => SentFailed, 'inflight' => SentInflight, 'success' => SentSucc, @@ -59,12 +63,14 @@ -define(metrics( Dropped, DroppedOther, + DroppedExpired, DroppedQueueFull, DroppedResourceNotFound, DroppedResourceStopped, Matched, Queued, Retried, + LateReply, SentFailed, SentInflight, SentSucc, @@ -76,12 +82,14 @@ #{ 'dropped' := Dropped, 'dropped.other' := DroppedOther, + 'dropped.expired' := DroppedExpired, 'dropped.queue_full' := DroppedQueueFull, 'dropped.resource_not_found' := DroppedResourceNotFound, 'dropped.resource_stopped' := DroppedResourceStopped, 'matched' := Matched, 'queuing' := Queued, 'retried' := Retried, + 'late_reply' := LateReply, 'failed' := SentFailed, 'inflight' := SentInflight, 'success' := SentSucc, diff --git a/apps/emqx_bridge/src/emqx_bridge_api.erl b/apps/emqx_bridge/src/emqx_bridge_api.erl index 50505effc..2c43ce5d7 100644 --- a/apps/emqx_bridge/src/emqx_bridge_api.erl +++ b/apps/emqx_bridge/src/emqx_bridge_api.erl @@ -751,11 +751,11 @@ aggregate_metrics(AllMetrics) -> fun( #{ metrics := ?metrics( - M1, M2, M3, M4, M5, M6, M7, M8, M9, M10, M11, M12, M13, M14, M15 + M1, M2, M3, M4, M5, M6, M7, M8, M9, M10, M11, M12, M13, M14, M15, M16, M17 ) }, ?metrics( - N1, N2, N3, N4, N5, N6, N7, N8, N9, N10, N11, N12, N13, N14, N15 + N1, N2, N3, N4, N5, N6, N7, N8, N9, N10, N11, N12, N13, N14, N15, N16, N17 ) ) -> ?METRICS( @@ -773,7 +773,9 @@ aggregate_metrics(AllMetrics) -> M12 + N12, M13 + N13, M14 + N14, - M15 + N15 + M15 + N15, + M16 + N16, + M17 + N17 ) end, InitMetrics, @@ -805,11 +807,13 @@ format_metrics(#{ counters := #{ 'dropped' := Dropped, 'dropped.other' := DroppedOther, + 'dropped.expired' := DroppedExpired, 'dropped.queue_full' := DroppedQueueFull, 'dropped.resource_not_found' := DroppedResourceNotFound, 'dropped.resource_stopped' := DroppedResourceStopped, 'matched' := Matched, 'retried' := Retried, + 'late_reply' := LateReply, 'failed' := SentFailed, 'success' := SentSucc, 'received' := Rcvd @@ -824,12 +828,14 @@ format_metrics(#{ ?METRICS( Dropped, DroppedOther, + DroppedExpired, DroppedQueueFull, DroppedResourceNotFound, DroppedResourceStopped, Matched, Queued, Retried, + LateReply, SentFailed, SentInflight, SentSucc, diff --git a/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl b/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl index 1b4eac73e..1f5b06fab 100644 --- a/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl +++ b/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl @@ -830,7 +830,8 @@ t_mqtt_conn_bridge_egress_reconnect(_) -> <<"resource_opts">> => #{ <<"worker_pool_size">> => 2, <<"query_mode">> => <<"sync">>, - <<"request_timeout">> => <<"500ms">>, + %% using a long time so we can test recovery + <<"request_timeout">> => <<"15s">>, %% to make it check the healthy quickly <<"health_check_interval">> => <<"0.5s">> } @@ -898,8 +899,10 @@ t_mqtt_conn_bridge_egress_reconnect(_) -> ), Payload1 = <<"hello2">>, Payload2 = <<"hello3">>, - emqx:publish(emqx_message:make(LocalTopic, Payload1)), - emqx:publish(emqx_message:make(LocalTopic, Payload2)), + %% we need to to it in other processes because it'll block due to + %% the long timeout + spawn(fun() -> emqx:publish(emqx_message:make(LocalTopic, Payload1)) end), + spawn(fun() -> emqx:publish(emqx_message:make(LocalTopic, Payload2)) end), {ok, _} = snabbkaffe:receive_events(SRef), %% verify the metrics of the bridge, the message should be queued @@ -917,9 +920,9 @@ t_mqtt_conn_bridge_egress_reconnect(_) -> <<"matched">> := Matched, <<"success">> := 1, <<"failed">> := 0, - <<"queuing">> := 1, - <<"inflight">> := 1 - } when Matched >= 3, + <<"queuing">> := Queuing, + <<"inflight">> := Inflight + } when Matched >= 3 andalso Inflight + Queuing == 2, maps:get(<<"metrics">>, DecodedMetrics1) ), diff --git a/apps/emqx_resource/include/emqx_resource.hrl b/apps/emqx_resource/include/emqx_resource.hrl index d7b080ae8..4f2a4883b 100644 --- a/apps/emqx_resource/include/emqx_resource.hrl +++ b/apps/emqx_resource/include/emqx_resource.hrl @@ -29,6 +29,8 @@ -type query_opts() :: #{ %% The key used for picking a resource worker pick_key => term(), + timeout => timeout(), + expire_at => infinity | integer(), async_reply_fun => reply_fun() }. -type resource_data() :: #{ diff --git a/apps/emqx_resource/src/emqx_resource_buffer_worker.erl b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl index 4b16ed3d5..8b79ce5a8 100644 --- a/apps/emqx_resource/src/emqx_resource_buffer_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl @@ -60,21 +60,23 @@ -define(COLLECT_REQ_LIMIT, 1000). -define(SEND_REQ(FROM, REQUEST), {'$send_req', FROM, REQUEST}). --define(QUERY(FROM, REQUEST, SENT), {query, FROM, REQUEST, SENT}). +-define(QUERY(FROM, REQUEST, SENT, EXPIRE_AT), {query, FROM, REQUEST, SENT, EXPIRE_AT}). -define(REPLY(FROM, REQUEST, SENT, RESULT), {reply, FROM, REQUEST, SENT, RESULT}). -define(EXPAND(RESULT, BATCH), [ ?REPLY(FROM, REQUEST, SENT, RESULT) - || ?QUERY(FROM, REQUEST, SENT) <- BATCH + || ?QUERY(FROM, REQUEST, SENT, _EXPIRE_AT) <- BATCH ]). -define(INFLIGHT_ITEM(Ref, BatchOrQuery, IsRetriable, WorkerMRef), {Ref, BatchOrQuery, IsRetriable, WorkerMRef} ). +-define(ITEM_IDX, 2). -define(RETRY_IDX, 3). -define(WORKER_MREF_IDX, 4). -type id() :: binary(). -type index() :: pos_integer(). --type queue_query() :: ?QUERY(from(), request(), HasBeenSent :: boolean()). +-type expire_at() :: infinity | integer(). +-type queue_query() :: ?QUERY(from(), request(), HasBeenSent :: boolean(), expire_at()). -type request() :: term(). -type from() :: pid() | reply_fun() | request_from(). -type request_from() :: undefined | gen_statem:from(). @@ -98,14 +100,18 @@ start_link(Id, Index, Opts) -> gen_statem:start_link(?MODULE, {Id, Index, Opts}, []). -spec sync_query(id(), request(), query_opts()) -> Result :: term(). -sync_query(Id, Request, Opts) -> +sync_query(Id, Request, Opts0) -> + Opts1 = ensure_timeout_query_opts(Opts0, sync), + Opts = ensure_expire_at(Opts1), PickKey = maps:get(pick_key, Opts, self()), - Timeout = maps:get(timeout, Opts, timer:seconds(15)), + Timeout = maps:get(timeout, Opts), emqx_resource_metrics:matched_inc(Id), pick_call(Id, PickKey, {query, Request, Opts}, Timeout). -spec async_query(id(), request(), query_opts()) -> Result :: term(). -async_query(Id, Request, Opts) -> +async_query(Id, Request, Opts0) -> + Opts1 = ensure_timeout_query_opts(Opts0, async), + Opts = ensure_expire_at(Opts1), PickKey = maps:get(pick_key, Opts, self()), emqx_resource_metrics:matched_inc(Id), pick_cast(Id, PickKey, {query, Request, Opts}). @@ -120,11 +126,15 @@ simple_sync_query(Id, Request) -> %% would mess up the metrics anyway. `undefined' is ignored by %% `emqx_resource_metrics:*_shift/3'. Index = undefined, - QueryOpts = #{simple_query => true}, + QueryOpts0 = #{simple_query => true, timeout => infinity}, + QueryOpts = #{expire_at := ExpireAt} = ensure_expire_at(QueryOpts0), emqx_resource_metrics:matched_inc(Id), Ref = make_message_ref(), - Result = call_query(sync, Id, Index, Ref, ?QUERY(self(), Request, false), QueryOpts), HasBeenSent = false, + From = self(), + Result = call_query( + sync, Id, Index, Ref, ?QUERY(From, Request, HasBeenSent, ExpireAt), QueryOpts + ), _ = handle_query_result(Id, Result, HasBeenSent), Result. @@ -179,9 +189,14 @@ init({Id, Index, Opts}) -> ?tp(buffer_worker_init, #{id => Id, index => Index}), {ok, running, Data}. -running(enter, _, St) -> +running(enter, _, Data) -> ?tp(buffer_worker_enter_running, #{}), - maybe_flush(St); + %% According to `gen_statem' laws, we mustn't call `maybe_flush' + %% directly because it may decide to return `{next_state, blocked, _}', + %% and that's an invalid response for a state enter call. + %% Returning a next event from a state enter call is also + %% prohibited. + {keep_state, ensure_flush_timer(Data, 0)}; running(cast, resume, _St) -> keep_state_and_data; running(cast, flush, Data) -> @@ -243,9 +258,9 @@ code_change(_OldVsn, State, _Extra) -> {ok, State}. %%============================================================================== --define(PICK(ID, KEY, EXPR), +-define(PICK(ID, KEY, PID, EXPR), try gproc_pool:pick_worker(ID, KEY) of - Pid when is_pid(Pid) -> + PID when is_pid(PID) -> EXPR; _ -> ?RESOURCE_ERROR(worker_not_created, "resource not created") @@ -258,7 +273,7 @@ code_change(_OldVsn, State, _Extra) -> ). pick_call(Id, Key, Query, Timeout) -> - ?PICK(Id, Key, begin + ?PICK(Id, Key, Pid, begin Caller = self(), MRef = erlang:monitor(process, Pid, [{alias, reply_demonitor}]), From = {Caller, MRef}, @@ -281,15 +296,21 @@ pick_call(Id, Key, Query, Timeout) -> end). pick_cast(Id, Key, Query) -> - ?PICK(Id, Key, begin + ?PICK(Id, Key, Pid, begin From = undefined, erlang:send(Pid, ?SEND_REQ(From, Query)), ok end). resume_from_blocked(Data) -> - #{inflight_tid := InflightTID} = Data, - case inflight_get_first_retriable(InflightTID) of + ?tp(buffer_worker_resume_from_blocked_enter, #{}), + #{ + id := Id, + index := Index, + inflight_tid := InflightTID + } = Data, + Now = now_(), + case inflight_get_first_retriable(InflightTID, Now) of none -> case is_inflight_full(InflightTID) of true -> @@ -297,14 +318,32 @@ resume_from_blocked(Data) -> false -> {next_state, running, Data} end; - {Ref, FirstQuery} -> + {expired, Ref, Batch} -> + IsAcked = ack_inflight(InflightTID, Ref, Id, Index), + IsAcked andalso emqx_resource_metrics:dropped_expired_inc(Id, length(Batch)), + ?tp(buffer_worker_retry_expired, #{expired => Batch}), + resume_from_blocked(Data); + {single, Ref, Query} -> %% We retry msgs in inflight window sync, as if we send them %% async, they will be appended to the end of inflight window again. case is_inflight_full(InflightTID) of true -> {keep_state, Data}; false -> - retry_inflight_sync(Ref, FirstQuery, Data) + retry_inflight_sync(Ref, Query, Data) + end; + {batch, Ref, NotExpired, Expired} -> + update_inflight_item(InflightTID, Ref, NotExpired), + NumExpired = length(Expired), + emqx_resource_metrics:dropped_expired_inc(Id, NumExpired), + NumExpired > 0 andalso ?tp(buffer_worker_retry_expired, #{expired => Expired}), + %% We retry msgs in inflight window sync, as if we send them + %% async, they will be appended to the end of inflight window again. + case is_inflight_full(InflightTID) of + true -> + {keep_state, Data}; + false -> + retry_inflight_sync(Ref, NotExpired, Data) end end. @@ -320,10 +359,10 @@ retry_inflight_sync(Ref, QueryOrBatch, Data0) -> Result = call_query(sync, Id, Index, Ref, QueryOrBatch, QueryOpts), ReplyResult = case QueryOrBatch of - ?QUERY(From, CoreReq, HasBeenSent) -> + ?QUERY(From, CoreReq, HasBeenSent, _ExpireAt) -> Reply = ?REPLY(From, CoreReq, HasBeenSent, Result), reply_caller_defer_metrics(Id, Reply, QueryOpts); - [?QUERY(_, _, _) | _] = Batch -> + [?QUERY(_, _, _, _) | _] = Batch -> batch_reply_caller_defer_metrics(Id, Result, Batch, QueryOpts) end, case ReplyResult of @@ -378,10 +417,12 @@ collect_and_enqueue_query_requests(Request0, Data0) -> (?SEND_REQ(undefined = _From, {query, Req, Opts})) -> ReplyFun = maps:get(async_reply_fun, Opts, undefined), HasBeenSent = false, - ?QUERY(ReplyFun, Req, HasBeenSent); - (?SEND_REQ(From, {query, Req, _Opts})) -> + ExpireAt = maps:get(expire_at, Opts), + ?QUERY(ReplyFun, Req, HasBeenSent, ExpireAt); + (?SEND_REQ(From, {query, Req, Opts})) -> HasBeenSent = false, - ?QUERY(From, Req, HasBeenSent) + ExpireAt = maps:get(expire_at, Opts), + ?QUERY(From, Req, HasBeenSent, ExpireAt) end, Requests ), @@ -406,6 +447,8 @@ maybe_flush(Data0) -> -spec flush(data()) -> gen_statem:event_handler_result(state(), data()). flush(Data0) -> #{ + id := Id, + index := Index, batch_size := BatchSize, inflight_tid := InflightTID, queue := Q0 @@ -419,25 +462,45 @@ flush(Data0) -> Data2 = ensure_flush_timer(Data1), {keep_state, Data2}; {_, false} -> + ?tp(buffer_worker_flush_before_pop, #{}), {Q1, QAckRef, Batch} = replayq:pop(Q0, #{count_limit => BatchSize}), - IsBatch = BatchSize =/= 1, - %% We *must* use the new queue, because we currently can't - %% `nack' a `pop'. - %% Maybe we could re-open the queue? Data2 = Data1#{queue := Q1}, - Ref = make_message_ref(), - do_flush(Data2, #{ - new_queue => Q1, - is_batch => IsBatch, - batch => Batch, - ref => Ref, - ack_ref => QAckRef - }) + ?tp(buffer_worker_flush_before_sieve_expired, #{}), + Now = now_(), + %% if the request has expired, the caller is no longer + %% waiting for a response. + case sieve_expired_requests(Batch, Now) of + all_expired -> + ok = replayq:ack(Q1, QAckRef), + emqx_resource_metrics:dropped_expired_inc(Id, length(Batch)), + emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q1)), + ?tp(buffer_worker_flush_all_expired, #{batch => Batch}), + flush(Data2); + {NotExpired, Expired} -> + NumExpired = length(Expired), + emqx_resource_metrics:dropped_expired_inc(Id, NumExpired), + IsBatch = BatchSize =/= 1, + %% We *must* use the new queue, because we currently can't + %% `nack' a `pop'. + %% Maybe we could re-open the queue? + ?tp( + buffer_worker_flush_potentially_partial, + #{expired => Expired, not_expired => NotExpired} + ), + Ref = make_message_ref(), + do_flush(Data2, #{ + new_queue => Q1, + is_batch => IsBatch, + batch => NotExpired, + ref => Ref, + ack_ref => QAckRef + }) + end end. -spec do_flush(data(), #{ is_batch := boolean(), - batch := [?QUERY(from(), request(), boolean())], + batch := [queue_query()], ack_ref := replayq:ack_ref(), ref := inflight_key(), new_queue := replayq:q() @@ -459,7 +522,7 @@ do_flush( inflight_tid := InflightTID } = Data0, %% unwrap when not batching (i.e., batch size == 1) - [?QUERY(From, CoreReq, HasBeenSent) = Request] = Batch, + [?QUERY(From, CoreReq, HasBeenSent, _ExpireAt) = Request] = Batch, QueryOpts = #{inflight_tid => InflightTID, simple_query => false}, Result = call_query(configured, Id, Index, Ref, Request, QueryOpts), Reply = ?REPLY(From, CoreReq, HasBeenSent, Result), @@ -812,10 +875,10 @@ call_query(QM0, Id, Index, Ref, Query, QueryOpts) -> end ). -apply_query_fun(sync, Mod, Id, _Index, _Ref, ?QUERY(_, Request, _) = _Query, ResSt, _QueryOpts) -> +apply_query_fun(sync, Mod, Id, _Index, _Ref, ?QUERY(_, Request, _, _) = _Query, ResSt, _QueryOpts) -> ?tp(call_query, #{id => Id, mod => Mod, query => _Query, res_st => ResSt, call_mode => sync}), ?APPLY_RESOURCE(call_query, Mod:on_query(Id, Request, ResSt), Request); -apply_query_fun(async, Mod, Id, Index, Ref, ?QUERY(_, Request, _) = Query, ResSt, QueryOpts) -> +apply_query_fun(async, Mod, Id, Index, Ref, ?QUERY(_, Request, _, _) = Query, ResSt, QueryOpts) -> ?tp(call_query_async, #{ id => Id, mod => Mod, query => Query, res_st => ResSt, call_mode => async }), @@ -834,13 +897,13 @@ apply_query_fun(async, Mod, Id, Index, Ref, ?QUERY(_, Request, _) = Query, ResSt end, Request ); -apply_query_fun(sync, Mod, Id, _Index, _Ref, [?QUERY(_, _, _) | _] = Batch, ResSt, _QueryOpts) -> +apply_query_fun(sync, Mod, Id, _Index, _Ref, [?QUERY(_, _, _, _) | _] = Batch, ResSt, _QueryOpts) -> ?tp(call_batch_query, #{ id => Id, mod => Mod, batch => Batch, res_st => ResSt, call_mode => sync }), - Requests = [Request || ?QUERY(_From, Request, _) <- Batch], + Requests = [Request || ?QUERY(_From, Request, _, _ExpireAt) <- Batch], ?APPLY_RESOURCE(call_batch_query, Mod:on_batch_query(Id, Requests, ResSt), Batch); -apply_query_fun(async, Mod, Id, Index, Ref, [?QUERY(_, _, _) | _] = Batch, ResSt, QueryOpts) -> +apply_query_fun(async, Mod, Id, Index, Ref, [?QUERY(_, _, _, _) | _] = Batch, ResSt, QueryOpts) -> ?tp(call_batch_query_async, #{ id => Id, mod => Mod, batch => Batch, res_st => ResSt, call_mode => async }), @@ -850,7 +913,7 @@ apply_query_fun(async, Mod, Id, Index, Ref, [?QUERY(_, _, _) | _] = Batch, ResSt begin ReplyFun = fun ?MODULE:batch_reply_after_query/8, ReplyFunAndArgs = {ReplyFun, [self(), Id, Index, InflightTID, Ref, Batch, QueryOpts]}, - Requests = [Request || ?QUERY(_From, Request, _) <- Batch], + Requests = [Request || ?QUERY(_From, Request, _, _ExpireAt) <- Batch], IsRetriable = false, WorkerMRef = undefined, InflightItem = ?INFLIGHT_ITEM(Ref, Batch, IsRetriable, WorkerMRef), @@ -862,7 +925,41 @@ apply_query_fun(async, Mod, Id, Index, Ref, [?QUERY(_, _, _) | _] = Batch, ResSt ). reply_after_query( - Pid, Id, Index, InflightTID, Ref, ?QUERY(From, Request, HasBeenSent), QueryOpts, Result + Pid, + Id, + Index, + InflightTID, + Ref, + ?QUERY(_From, _Request, _HasBeenSent, ExpireAt) = Query, + QueryOpts, + Result +) -> + ?tp( + buffer_worker_reply_after_query_enter, + #{batch_or_query => [Query], ref => Ref} + ), + Now = now_(), + case is_expired(ExpireAt, Now) of + true -> + IsFullBefore = is_inflight_full(InflightTID), + IsAcked = ack_inflight(InflightTID, Ref, Id, Index), + IsAcked andalso emqx_resource_metrics:late_reply_inc(Id), + IsFullBefore andalso ?MODULE:flush_worker(Pid), + ?tp(buffer_worker_reply_after_query_expired, #{expired => [Query]}), + ok; + false -> + do_reply_after_query(Pid, Id, Index, InflightTID, Ref, Query, QueryOpts, Result) + end. + +do_reply_after_query( + Pid, + Id, + Index, + InflightTID, + Ref, + ?QUERY(From, Request, HasBeenSent, _ExpireAt), + QueryOpts, + Result ) -> %% NOTE: 'inflight' is the count of messages that were sent async %% but received no ACK, NOT the number of messages queued in the @@ -875,7 +972,7 @@ reply_after_query( %% Keep retrying. ?tp(buffer_worker_reply_after_query, #{ action => Action, - batch_or_query => ?QUERY(From, Request, HasBeenSent), + batch_or_query => ?QUERY(From, Request, HasBeenSent, _ExpireAt), ref => Ref, result => Result }), @@ -884,7 +981,7 @@ reply_after_query( ack -> ?tp(buffer_worker_reply_after_query, #{ action => Action, - batch_or_query => ?QUERY(From, Request, HasBeenSent), + batch_or_query => ?QUERY(From, Request, HasBeenSent, _ExpireAt), ref => Ref, result => Result }), @@ -896,6 +993,34 @@ reply_after_query( end. batch_reply_after_query(Pid, Id, Index, InflightTID, Ref, Batch, QueryOpts, Result) -> + ?tp( + buffer_worker_reply_after_query_enter, + #{batch_or_query => Batch, ref => Ref} + ), + Now = now_(), + case sieve_expired_requests(Batch, Now) of + all_expired -> + IsFullBefore = is_inflight_full(InflightTID), + IsAcked = ack_inflight(InflightTID, Ref, Id, Index), + IsAcked andalso emqx_resource_metrics:late_reply_inc(Id), + IsFullBefore andalso ?MODULE:flush_worker(Pid), + ?tp(buffer_worker_reply_after_query_expired, #{expired => Batch}), + ok; + {NotExpired, Expired} -> + NumExpired = length(Expired), + emqx_resource_metrics:late_reply_inc(Id, NumExpired), + NumExpired > 0 andalso + ?tp(buffer_worker_reply_after_query_expired, #{expired => Expired}), + do_batch_reply_after_query( + Pid, Id, Index, InflightTID, Ref, NotExpired, QueryOpts, Result + ) + end. + +do_batch_reply_after_query(Pid, Id, Index, InflightTID, Ref, Batch, QueryOpts, Result) -> + ?tp( + buffer_worker_reply_after_query_enter, + #{batch_or_query => Batch, ref => Ref} + ), %% NOTE: 'inflight' is the count of messages that were sent async %% but received no ACK, NOT the number of messages queued in the %% inflight window. @@ -986,9 +1111,12 @@ inflight_new(InfltWinSZ, Id, Index) -> ), TableId. --spec inflight_get_first_retriable(ets:tid()) -> - none | {integer(), [?QUERY(_, _, _)] | ?QUERY(_, _, _)}. -inflight_get_first_retriable(InflightTID) -> +-spec inflight_get_first_retriable(ets:tid(), integer()) -> + none + | {expired, inflight_key(), [queue_query()]} + | {single, inflight_key(), queue_query()} + | {batch, inflight_key(), _NotExpired :: [queue_query()], _Expired :: [queue_query()]}. +inflight_get_first_retriable(InflightTID, Now) -> MatchSpec = ets:fun2ms( fun(?INFLIGHT_ITEM(Ref, BatchOrQuery, IsRetriable, _WorkerMRef)) when @@ -1000,8 +1128,22 @@ inflight_get_first_retriable(InflightTID) -> case ets:select(InflightTID, MatchSpec, _Limit = 1) of '$end_of_table' -> none; - {[{Ref, BatchOrQuery}], _Continuation} -> - {Ref, BatchOrQuery} + {[{Ref, Query = ?QUERY(_From, _CoreReq, _HasBeenSent, ExpireAt)}], _Continuation} -> + case is_expired(ExpireAt, Now) of + true -> + {expired, Ref, [Query]}; + false -> + {single, Ref, Query} + end; + {[{Ref, Batch = [_ | _]}], _Continuation} -> + %% batch is non-empty because we check that in + %% `sieve_expired_requests'. + case sieve_expired_requests(Batch, Now) of + all_expired -> + {expired, Ref, Batch}; + {NotExpired, Expired} -> + {batch, Ref, NotExpired, Expired} + end end. is_inflight_full(undefined) -> @@ -1030,7 +1172,7 @@ inflight_append(undefined, _InflightItem, _Id, _Index) -> ok; inflight_append( InflightTID, - ?INFLIGHT_ITEM(Ref, [?QUERY(_, _, _) | _] = Batch0, IsRetriable, WorkerMRef), + ?INFLIGHT_ITEM(Ref, [?QUERY(_, _, _, _) | _] = Batch0, IsRetriable, WorkerMRef), Id, Index ) -> @@ -1044,7 +1186,9 @@ inflight_append( ok; inflight_append( InflightTID, - ?INFLIGHT_ITEM(Ref, ?QUERY(_From, _Req, _HasBeenSent) = Query0, IsRetriable, WorkerMRef), + ?INFLIGHT_ITEM( + Ref, ?QUERY(_From, _Req, _HasBeenSent, _ExpireAt) = Query0, IsRetriable, WorkerMRef + ), Id, Index ) -> @@ -1106,9 +1250,9 @@ ack_inflight(undefined, _Ref, _Id, _Index) -> ack_inflight(InflightTID, Ref, Id, Index) -> Count = case ets:take(InflightTID, Ref) of - [?INFLIGHT_ITEM(Ref, ?QUERY(_, _, _), _IsRetriable, _WorkerMRef)] -> + [?INFLIGHT_ITEM(Ref, ?QUERY(_, _, _, _), _IsRetriable, _WorkerMRef)] -> 1; - [?INFLIGHT_ITEM(Ref, [?QUERY(_, _, _) | _] = Batch, _IsRetriable, _WorkerMRef)] -> + [?INFLIGHT_ITEM(Ref, [?QUERY(_, _, _, _) | _] = Batch, _IsRetriable, _WorkerMRef)] -> length(Batch); _ -> 0 @@ -1133,6 +1277,12 @@ mark_inflight_items_as_retriable(Data, WorkerMRef) -> ?tp(buffer_worker_worker_down_update, #{num_affected => _NumAffected}), ok. +%% used to update a batch after dropping expired individual queries. +update_inflight_item(InflightTID, Ref, NewBatch) -> + _ = ets:update_element(InflightTID, Ref, {?ITEM_IDX, NewBatch}), + ?tp(buffer_worker_worker_update_inflight_item, #{ref => Ref}), + ok. + %%============================================================================== inc_sent_failed(Id, _HasBeenSent = true) -> @@ -1180,11 +1330,14 @@ clear_disk_queue_dir(Id, Index) -> Res end. -ensure_flush_timer(Data = #{tref := undefined, batch_time := T}) -> +ensure_flush_timer(Data = #{batch_time := T}) -> + ensure_flush_timer(Data, T). + +ensure_flush_timer(Data = #{tref := undefined}, T) -> Ref = make_ref(), TRef = erlang:send_after(T, self(), {flush, Ref}), Data#{tref => {TRef, Ref}}; -ensure_flush_timer(Data) -> +ensure_flush_timer(Data, _T) -> Data. cancel_flush_timer(St = #{tref := undefined}) -> @@ -1195,7 +1348,7 @@ cancel_flush_timer(St = #{tref := {TRef, _Ref}}) -> -spec make_message_ref() -> inflight_key(). make_message_ref() -> - erlang:monotonic_time(nanosecond). + now_(). collect_requests(Acc, Limit) -> Count = length(Acc), @@ -1213,9 +1366,9 @@ do_collect_requests(Acc, Count, Limit) -> mark_as_sent(Batch) when is_list(Batch) -> lists:map(fun mark_as_sent/1, Batch); -mark_as_sent(?QUERY(From, Req, _)) -> +mark_as_sent(?QUERY(From, Req, _HasBeenSent, ExpireAt)) -> HasBeenSent = true, - ?QUERY(From, Req, HasBeenSent). + ?QUERY(From, Req, HasBeenSent, ExpireAt). is_unrecoverable_error({error, {unrecoverable_error, _}}) -> true; @@ -1235,3 +1388,49 @@ is_async_return({async_return, _}) -> true; is_async_return(_) -> false. + +sieve_expired_requests(Batch, Now) -> + {Expired, NotExpired} = + lists:partition( + fun(?QUERY(_From, _CoreReq, _HasBeenSent, ExpireAt)) -> + is_expired(ExpireAt, Now) + end, + Batch + ), + case {NotExpired, Expired} of + {[], []} -> + %% Should be impossible for batch_size >= 1. + all_expired; + {[], [_ | _]} -> + all_expired; + {[_ | _], _} -> + {NotExpired, Expired} + end. + +-spec is_expired(infinity | integer(), integer()) -> boolean(). +is_expired(infinity = _ExpireAt, _Now) -> + false; +is_expired(ExpireAt, Now) -> + Now > ExpireAt. + +now_() -> + erlang:monotonic_time(nanosecond). + +-spec ensure_timeout_query_opts(query_opts(), sync | async) -> query_opts(). +ensure_timeout_query_opts(#{timeout := _} = Opts, _SyncOrAsync) -> + Opts; +ensure_timeout_query_opts(#{} = Opts0, sync) -> + TimeoutMS = timer:seconds(15), + Opts0#{timeout => TimeoutMS}; +ensure_timeout_query_opts(#{} = Opts0, async) -> + Opts0#{timeout => infinity}. + +-spec ensure_expire_at(query_opts()) -> query_opts(). +ensure_expire_at(#{expire_at := _} = Opts) -> + Opts; +ensure_expire_at(#{timeout := infinity} = Opts) -> + Opts#{expire_at => infinity}; +ensure_expire_at(#{timeout := TimeoutMS} = Opts) -> + TimeoutNS = erlang:convert_time_unit(TimeoutMS, millisecond, nanosecond), + ExpireAt = now_() + TimeoutNS, + Opts#{expire_at => ExpireAt}. diff --git a/apps/emqx_resource/src/emqx_resource_manager.erl b/apps/emqx_resource/src/emqx_resource_manager.erl index 95d1ed1d2..c01158b0a 100644 --- a/apps/emqx_resource/src/emqx_resource_manager.erl +++ b/apps/emqx_resource/src/emqx_resource_manager.erl @@ -134,8 +134,10 @@ create(MgrId, ResId, Group, ResourceType, Config, Opts) -> 'retried.success', 'retried.failed', 'success', + 'late_reply', 'failed', 'dropped', + 'dropped.expired', 'dropped.queue_full', 'dropped.resource_not_found', 'dropped.resource_stopped', diff --git a/apps/emqx_resource/src/emqx_resource_metrics.erl b/apps/emqx_resource/src/emqx_resource_metrics.erl index 455be9c22..28507e291 100644 --- a/apps/emqx_resource/src/emqx_resource_metrics.erl +++ b/apps/emqx_resource/src/emqx_resource_metrics.erl @@ -34,6 +34,9 @@ dropped_other_inc/1, dropped_other_inc/2, dropped_other_get/1, + dropped_expired_inc/1, + dropped_expired_inc/2, + dropped_expired_get/1, dropped_queue_full_inc/1, dropped_queue_full_inc/2, dropped_queue_full_get/1, @@ -46,6 +49,9 @@ failed_inc/1, failed_inc/2, failed_get/1, + late_reply_inc/1, + late_reply_inc/2, + late_reply_get/1, matched_inc/1, matched_inc/2, matched_get/1, @@ -75,9 +81,11 @@ events() -> [?TELEMETRY_PREFIX, Event] || Event <- [ dropped_other, + dropped_expired, dropped_queue_full, dropped_resource_not_found, dropped_resource_stopped, + late_reply, failed, inflight, matched, @@ -114,6 +122,9 @@ handle_telemetry_event( dropped_other -> emqx_metrics_worker:inc(?RES_METRICS, ID, 'dropped', Val), emqx_metrics_worker:inc(?RES_METRICS, ID, 'dropped.other', Val); + dropped_expired -> + emqx_metrics_worker:inc(?RES_METRICS, ID, 'dropped', Val), + emqx_metrics_worker:inc(?RES_METRICS, ID, 'dropped.expired', Val); dropped_queue_full -> emqx_metrics_worker:inc(?RES_METRICS, ID, 'dropped', Val), emqx_metrics_worker:inc(?RES_METRICS, ID, 'dropped.queue_full', Val); @@ -123,6 +134,8 @@ handle_telemetry_event( dropped_resource_stopped -> emqx_metrics_worker:inc(?RES_METRICS, ID, 'dropped', Val), emqx_metrics_worker:inc(?RES_METRICS, ID, 'dropped.resource_stopped', Val); + late_reply -> + emqx_metrics_worker:inc(?RES_METRICS, ID, 'late_reply', Val); failed -> emqx_metrics_worker:inc(?RES_METRICS, ID, 'failed', Val); matched -> @@ -211,6 +224,30 @@ dropped_other_inc(ID, Val) -> dropped_other_get(ID) -> emqx_metrics_worker:get(?RES_METRICS, ID, 'dropped.other'). +%% @doc Count of messages dropped due to being expired before being sent. +dropped_expired_inc(ID) -> + dropped_expired_inc(ID, 1). + +dropped_expired_inc(ID, Val) -> + telemetry:execute([?TELEMETRY_PREFIX, dropped_expired], #{counter_inc => Val}, #{ + resource_id => ID + }). + +dropped_expired_get(ID) -> + emqx_metrics_worker:get(?RES_METRICS, ID, 'dropped.expired'). + +%% @doc Count of messages that were sent but received a late reply. +late_reply_inc(ID) -> + late_reply_inc(ID, 1). + +late_reply_inc(ID, Val) -> + telemetry:execute([?TELEMETRY_PREFIX, late_reply], #{counter_inc => Val}, #{ + resource_id => ID + }). + +late_reply_get(ID) -> + emqx_metrics_worker:get(?RES_METRICS, ID, 'late_reply'). + %% @doc Count of messages dropped because the queue was full dropped_queue_full_inc(ID) -> dropped_queue_full_inc(ID, 1). diff --git a/apps/emqx_resource/test/emqx_connector_demo.erl b/apps/emqx_resource/test/emqx_connector_demo.erl index c2b0c5733..4e3423808 100644 --- a/apps/emqx_resource/test/emqx_connector_demo.erl +++ b/apps/emqx_resource/test/emqx_connector_demo.erl @@ -260,7 +260,7 @@ counter_loop( ?tp(connector_demo_inc_counter_async, #{n => N}), State#{counter => Num + N}; {big_payload, _Payload, ReplyFun} when Status == blocked -> - apply_reply(ReplyFun, {error, blocked}), + apply_reply(ReplyFun, {error, {recoverable_error, blocked}}), State; {{FromPid, ReqRef}, {inc, N}} when Status == running -> %ct:pal("sync counter recv: ~p", [{inc, N}]), diff --git a/apps/emqx_resource/test/emqx_resource_SUITE.erl b/apps/emqx_resource/test/emqx_resource_SUITE.erl index c9325af2b..9b2af74f6 100644 --- a/apps/emqx_resource/test/emqx_resource_SUITE.erl +++ b/apps/emqx_resource/test/emqx_resource_SUITE.erl @@ -232,7 +232,7 @@ t_batch_query_counter(_) -> fun(Result, Trace) -> ?assertMatch({ok, 0}, Result), QueryTrace = ?of_kind(call_batch_query, Trace), - ?assertMatch([#{batch := [{query, _, get_counter, _}]}], QueryTrace) + ?assertMatch([#{batch := [{query, _, get_counter, _, _}]}], QueryTrace) end ), @@ -284,7 +284,7 @@ t_query_counter_async_query(_) -> fun(Trace) -> %% the callback_mode of 'emqx_connector_demo' is 'always_sync'. QueryTrace = ?of_kind(call_query, Trace), - ?assertMatch([#{query := {query, _, {inc_counter, 1}, _}} | _], QueryTrace) + ?assertMatch([#{query := {query, _, {inc_counter, 1}, _, _}} | _], QueryTrace) end ), %% simple query ignores the query_mode and batching settings in the resource_worker @@ -295,7 +295,7 @@ t_query_counter_async_query(_) -> ?assertMatch({ok, 1000}, Result), %% the callback_mode if 'emqx_connector_demo' is 'always_sync'. QueryTrace = ?of_kind(call_query, Trace), - ?assertMatch([#{query := {query, _, get_counter, _}}], QueryTrace) + ?assertMatch([#{query := {query, _, get_counter, _, _}}], QueryTrace) end ), {ok, _, #{metrics := #{counters := C}}} = emqx_resource:get_instance(?ID), @@ -337,7 +337,7 @@ t_query_counter_async_callback(_) -> end, fun(Trace) -> QueryTrace = ?of_kind(call_query_async, Trace), - ?assertMatch([#{query := {query, _, {inc_counter, 1}, _}} | _], QueryTrace) + ?assertMatch([#{query := {query, _, {inc_counter, 1}, _, _}} | _], QueryTrace) end ), @@ -348,7 +348,7 @@ t_query_counter_async_callback(_) -> fun(Result, Trace) -> ?assertMatch({ok, 1000}, Result), QueryTrace = ?of_kind(call_query, Trace), - ?assertMatch([#{query := {query, _, get_counter, _}}], QueryTrace) + ?assertMatch([#{query := {query, _, get_counter, _, _}}], QueryTrace) end ), {ok, _, #{metrics := #{counters := C}}} = emqx_resource:get_instance(?ID), @@ -419,7 +419,7 @@ t_query_counter_async_inflight(_) -> ), fun(Trace) -> QueryTrace = ?of_kind(call_query_async, Trace), - ?assertMatch([#{query := {query, _, {inc_counter, 1}, _}} | _], QueryTrace) + ?assertMatch([#{query := {query, _, {inc_counter, 1}, _, _}} | _], QueryTrace) end ), tap_metrics(?LINE), @@ -476,7 +476,7 @@ t_query_counter_async_inflight(_) -> end, fun(Trace) -> QueryTrace = ?of_kind(call_query_async, Trace), - ?assertMatch([#{query := {query, _, {inc_counter, _}, _}} | _], QueryTrace), + ?assertMatch([#{query := {query, _, {inc_counter, _}, _, _}} | _], QueryTrace), ?assertEqual(WindowSize + Num, ets:info(Tab0, size), #{tab => ets:tab2list(Tab0)}), tap_metrics(?LINE), ok @@ -495,7 +495,7 @@ t_query_counter_async_inflight(_) -> ), fun(Trace) -> QueryTrace = ?of_kind(call_query_async, Trace), - ?assertMatch([#{query := {query, _, {inc_counter, 1}, _}} | _], QueryTrace) + ?assertMatch([#{query := {query, _, {inc_counter, 1}, _, _}} | _], QueryTrace) end ), @@ -605,8 +605,8 @@ t_query_counter_async_inflight_batch(_) -> [ #{ batch := [ - {query, _, {inc_counter, 1}, _}, - {query, _, {inc_counter, 1}, _} + {query, _, {inc_counter, 1}, _, _}, + {query, _, {inc_counter, 1}, _, _} ] } | _ @@ -687,7 +687,7 @@ t_query_counter_async_inflight_batch(_) -> fun(Trace) -> QueryTrace = ?of_kind(call_batch_query_async, Trace), ?assertMatch( - [#{batch := [{query, _, {inc_counter, _}, _} | _]} | _], + [#{batch := [{query, _, {inc_counter, _}, _, _} | _]} | _], QueryTrace ) end @@ -712,7 +712,7 @@ t_query_counter_async_inflight_batch(_) -> fun(Trace) -> QueryTrace = ?of_kind(call_batch_query_async, Trace), ?assertMatch( - [#{batch := [{query, _, {inc_counter, _}, _} | _]} | _], + [#{batch := [{query, _, {inc_counter, _}, _, _} | _]} | _], QueryTrace ) end @@ -1499,9 +1499,680 @@ t_async_pool_worker_death(_Config) -> ), ok. +t_expiration_sync_before_sending(_Config) -> + emqx_connector_demo:set_callback_mode(always_sync), + {ok, _} = emqx_resource:create( + ?ID, + ?DEFAULT_RESOURCE_GROUP, + ?TEST_RESOURCE, + #{name => test_resource}, + #{ + query_mode => sync, + batch_size => 1, + worker_pool_size => 1, + resume_interval => 1_000 + } + ), + do_t_expiration_before_sending(sync). + +t_expiration_sync_batch_before_sending(_Config) -> + emqx_connector_demo:set_callback_mode(always_sync), + {ok, _} = emqx_resource:create( + ?ID, + ?DEFAULT_RESOURCE_GROUP, + ?TEST_RESOURCE, + #{name => test_resource}, + #{ + query_mode => sync, + batch_size => 2, + batch_time => 100, + worker_pool_size => 1, + resume_interval => 1_000 + } + ), + do_t_expiration_before_sending(sync). + +t_expiration_async_before_sending(_Config) -> + emqx_connector_demo:set_callback_mode(async_if_possible), + {ok, _} = emqx_resource:create( + ?ID, + ?DEFAULT_RESOURCE_GROUP, + ?TEST_RESOURCE, + #{name => test_resource}, + #{ + query_mode => async, + batch_size => 1, + worker_pool_size => 1, + resume_interval => 1_000 + } + ), + do_t_expiration_before_sending(async). + +t_expiration_async_batch_before_sending(_Config) -> + emqx_connector_demo:set_callback_mode(async_if_possible), + {ok, _} = emqx_resource:create( + ?ID, + ?DEFAULT_RESOURCE_GROUP, + ?TEST_RESOURCE, + #{name => test_resource}, + #{ + query_mode => async, + batch_size => 2, + batch_time => 100, + worker_pool_size => 1, + resume_interval => 1_000 + } + ), + do_t_expiration_before_sending(async). + +do_t_expiration_before_sending(QueryMode) -> + ?check_trace( + begin + ok = emqx_resource:simple_sync_query(?ID, block), + + ?force_ordering( + #{?snk_kind := buffer_worker_flush_before_pop}, + #{?snk_kind := delay_enter} + ), + ?force_ordering( + #{?snk_kind := delay}, + #{?snk_kind := buffer_worker_flush_before_sieve_expired} + ), + + TimeoutMS = 100, + spawn_link(fun() -> + case QueryMode of + sync -> + ?assertError( + timeout, + emqx_resource:query(?ID, {inc_counter, 99}, #{timeout => TimeoutMS}) + ); + async -> + ?assertEqual( + ok, emqx_resource:query(?ID, {inc_counter, 99}, #{timeout => TimeoutMS}) + ) + end + end), + spawn_link(fun() -> + ?tp(delay_enter, #{}), + ct:sleep(2 * TimeoutMS), + ?tp(delay, #{}), + ok + end), + + {ok, _} = ?block_until(#{?snk_kind := buffer_worker_flush_all_expired}, 4 * TimeoutMS), + ok + end, + fun(Trace) -> + ?assertMatch( + [#{batch := [{query, _, {inc_counter, 99}, _, _}]}], + ?of_kind(buffer_worker_flush_all_expired, Trace) + ), + Metrics = tap_metrics(?LINE), + ?assertMatch( + #{ + counters := #{ + matched := 2, + %% the block call + success := 1, + dropped := 1, + 'dropped.expired' := 1, + retried := 0, + failed := 0 + } + }, + Metrics + ), + ok + end + ), + ok. + +t_expiration_sync_before_sending_partial_batch(_Config) -> + emqx_connector_demo:set_callback_mode(always_sync), + {ok, _} = emqx_resource:create( + ?ID, + ?DEFAULT_RESOURCE_GROUP, + ?TEST_RESOURCE, + #{name => test_resource}, + #{ + query_mode => sync, + batch_size => 2, + batch_time => 100, + worker_pool_size => 1, + resume_interval => 1_000 + } + ), + install_telemetry_handler(?FUNCTION_NAME), + do_t_expiration_before_sending_partial_batch(sync). + +t_expiration_async_before_sending_partial_batch(_Config) -> + emqx_connector_demo:set_callback_mode(async_if_possible), + {ok, _} = emqx_resource:create( + ?ID, + ?DEFAULT_RESOURCE_GROUP, + ?TEST_RESOURCE, + #{name => test_resource}, + #{ + query_mode => async, + batch_size => 2, + batch_time => 100, + worker_pool_size => 1, + resume_interval => 1_000 + } + ), + install_telemetry_handler(?FUNCTION_NAME), + do_t_expiration_before_sending_partial_batch(async). + +do_t_expiration_before_sending_partial_batch(QueryMode) -> + ?check_trace( + begin + ok = emqx_resource:simple_sync_query(?ID, block), + + ?force_ordering( + #{?snk_kind := buffer_worker_flush_before_pop}, + #{?snk_kind := delay_enter} + ), + ?force_ordering( + #{?snk_kind := delay}, + #{?snk_kind := buffer_worker_flush_before_sieve_expired} + ), + + Pid0 = + spawn_link(fun() -> + ?assertEqual( + ok, emqx_resource:query(?ID, {inc_counter, 99}, #{timeout => infinity}) + ), + ?tp(infinity_query_returned, #{}) + end), + TimeoutMS = 100, + Pid1 = + spawn_link(fun() -> + case QueryMode of + sync -> + ?assertError( + timeout, + emqx_resource:query(?ID, {inc_counter, 199}, #{timeout => TimeoutMS}) + ); + async -> + ?assertEqual( + ok, + emqx_resource:query(?ID, {inc_counter, 199}, #{timeout => TimeoutMS}) + ) + end + end), + Pid2 = + spawn_link(fun() -> + ?tp(delay_enter, #{}), + ct:sleep(2 * TimeoutMS), + ?tp(delay, #{}), + ok + end), + + {ok, _} = ?block_until( + #{?snk_kind := buffer_worker_flush_potentially_partial}, 4 * TimeoutMS + ), + ok = emqx_resource:simple_sync_query(?ID, resume), + case QueryMode of + async -> + {ok, _} = ?block_until( + #{ + ?snk_kind := buffer_worker_reply_after_query, + action := ack, + batch_or_query := [{query, _, {inc_counter, 99}, _, _}] + }, + 10 * TimeoutMS + ); + sync -> + %% more time because it needs to retry if sync + {ok, _} = ?block_until(#{?snk_kind := infinity_query_returned}, 20 * TimeoutMS) + end, + + lists:foreach( + fun(Pid) -> + unlink(Pid), + exit(Pid, kill) + end, + [Pid0, Pid1, Pid2] + ), + ok + end, + fun(Trace) -> + ?assertMatch( + [ + #{ + expired := [{query, _, {inc_counter, 199}, _, _}], + not_expired := [{query, _, {inc_counter, 99}, _, _}] + } + ], + ?of_kind(buffer_worker_flush_potentially_partial, Trace) + ), + wait_until_gauge_is(inflight, 0, 500), + Metrics = tap_metrics(?LINE), + case QueryMode of + async -> + ?assertMatch( + #{ + counters := #{ + matched := 4, + %% the block call, the request with + %% infinity timeout, and the resume + %% call. + success := 3, + dropped := 1, + 'dropped.expired' := 1, + %% was sent successfully and held by + %% the test connector. + retried := 0, + failed := 0 + } + }, + Metrics + ); + sync -> + ?assertMatch( + #{ + counters := #{ + matched := 4, + %% the block call, the request with + %% infinity timeout, and the resume + %% call. + success := 3, + dropped := 1, + 'dropped.expired' := 1, + %% currently, the test connector + %% replies with an error that may make + %% the buffer worker retry. + retried := Retried, + failed := 0 + } + } when Retried =< 1, + Metrics + ) + end, + ok + end + ), + ok. + +t_expiration_async_after_reply(_Config) -> + emqx_connector_demo:set_callback_mode(async_if_possible), + {ok, _} = emqx_resource:create( + ?ID, + ?DEFAULT_RESOURCE_GROUP, + ?TEST_RESOURCE, + #{name => test_resource}, + #{ + query_mode => async, + batch_size => 1, + batch_time => 100, + worker_pool_size => 1, + resume_interval => 1_000 + } + ), + install_telemetry_handler(?FUNCTION_NAME), + do_t_expiration_async_after_reply(single). + +t_expiration_async_batch_after_reply(_Config) -> + emqx_connector_demo:set_callback_mode(async_if_possible), + {ok, _} = emqx_resource:create( + ?ID, + ?DEFAULT_RESOURCE_GROUP, + ?TEST_RESOURCE, + #{name => test_resource}, + #{ + query_mode => async, + batch_size => 2, + batch_time => 100, + worker_pool_size => 1, + resume_interval => 2_000 + } + ), + install_telemetry_handler(?FUNCTION_NAME), + do_t_expiration_async_after_reply(batch). + +do_t_expiration_async_after_reply(IsBatch) -> + ?check_trace( + begin + NAcks = + case IsBatch of + batch -> 1; + single -> 2 + end, + ?force_ordering( + #{?snk_kind := buffer_worker_flush_ack}, + NAcks, + #{?snk_kind := delay_enter}, + _Guard = true + ), + ?force_ordering( + #{?snk_kind := delay}, + #{ + ?snk_kind := buffer_worker_reply_after_query_enter, + batch_or_query := [{query, _, {inc_counter, 199}, _, _} | _] + } + ), + + TimeoutMS = 100, + ?assertEqual( + ok, + emqx_resource:query(?ID, {inc_counter, 199}, #{timeout => TimeoutMS}) + ), + ?assertEqual( + ok, emqx_resource:query(?ID, {inc_counter, 99}, #{timeout => infinity}) + ), + Pid0 = + spawn_link(fun() -> + ?tp(delay_enter, #{}), + ct:sleep(2 * TimeoutMS), + ?tp(delay, #{}), + ok + end), + + {ok, _} = ?block_until( + #{?snk_kind := buffer_worker_flush_potentially_partial}, 4 * TimeoutMS + ), + {ok, _} = ?block_until( + #{?snk_kind := buffer_worker_reply_after_query_expired}, 10 * TimeoutMS + ), + + unlink(Pid0), + exit(Pid0, kill), + ok + end, + fun(Trace) -> + ?assertMatch( + [ + #{ + expired := [{query, _, {inc_counter, 199}, _, _}] + } + ], + ?of_kind(buffer_worker_reply_after_query_expired, Trace) + ), + wait_telemetry_event(success, #{n_events => 1, timeout => 4_000}), + Metrics = tap_metrics(?LINE), + ?assertMatch( + #{ + counters := #{ + matched := 2, + %% the request with infinity timeout. + success := 1, + dropped := 0, + late_reply := 1, + retried := 0, + failed := 0 + } + }, + Metrics + ), + ok + end + ), + ok. + +t_expiration_batch_all_expired_after_reply(_Config) -> + ResumeInterval = 300, + emqx_connector_demo:set_callback_mode(async_if_possible), + {ok, _} = emqx_resource:create( + ?ID, + ?DEFAULT_RESOURCE_GROUP, + ?TEST_RESOURCE, + #{name => test_resource}, + #{ + query_mode => async, + batch_size => 2, + batch_time => 100, + worker_pool_size => 1, + resume_interval => ResumeInterval + } + ), + ?check_trace( + begin + ?force_ordering( + #{?snk_kind := buffer_worker_flush_ack}, + #{?snk_kind := delay_enter} + ), + ?force_ordering( + #{?snk_kind := delay}, + #{ + ?snk_kind := buffer_worker_reply_after_query_enter, + batch_or_query := [{query, _, {inc_counter, 199}, _, _} | _] + } + ), + + TimeoutMS = 200, + ?assertEqual( + ok, + emqx_resource:query(?ID, {inc_counter, 199}, #{timeout => TimeoutMS}) + ), + Pid0 = + spawn_link(fun() -> + ?tp(delay_enter, #{}), + ct:sleep(2 * TimeoutMS), + ?tp(delay, #{}), + ok + end), + + {ok, _} = ?block_until( + #{?snk_kind := buffer_worker_reply_after_query_expired}, 10 * TimeoutMS + ), + + unlink(Pid0), + exit(Pid0, kill), + ok + end, + fun(Trace) -> + ?assertMatch( + [ + #{ + expired := [{query, _, {inc_counter, 199}, _, _}] + } + ], + ?of_kind(buffer_worker_reply_after_query_expired, Trace) + ), + Metrics = tap_metrics(?LINE), + ?assertMatch( + #{ + counters := #{ + matched := 1, + success := 0, + dropped := 0, + late_reply := 1, + retried := 0, + failed := 0 + } + }, + Metrics + ), + ok + end + ), + ok. + +t_expiration_retry(_Config) -> + emqx_connector_demo:set_callback_mode(always_sync), + {ok, _} = emqx_resource:create( + ?ID, + ?DEFAULT_RESOURCE_GROUP, + ?TEST_RESOURCE, + #{name => test_resource}, + #{ + query_mode => sync, + batch_size => 1, + batch_time => 100, + worker_pool_size => 1, + resume_interval => 300 + } + ), + do_t_expiration_retry(single). + +t_expiration_retry_batch(_Config) -> + emqx_connector_demo:set_callback_mode(always_sync), + {ok, _} = emqx_resource:create( + ?ID, + ?DEFAULT_RESOURCE_GROUP, + ?TEST_RESOURCE, + #{name => test_resource}, + #{ + query_mode => sync, + batch_size => 2, + batch_time => 100, + worker_pool_size => 1, + resume_interval => 300 + } + ), + do_t_expiration_retry(batch). + +do_t_expiration_retry(IsBatch) -> + ResumeInterval = 300, + ?check_trace( + begin + ok = emqx_resource:simple_sync_query(?ID, block), + + {ok, SRef0} = snabbkaffe:subscribe( + ?match_event(#{?snk_kind := buffer_worker_flush_nack}), + 1, + 200 + ), + TimeoutMS = 100, + %% the request that expires must be first, so it's the + %% head of the inflight table (and retriable). + {ok, SRef1} = snabbkaffe:subscribe( + ?match_event(#{?snk_kind := buffer_worker_appended_to_queue}), + 1, + ResumeInterval * 2 + ), + spawn_link(fun() -> + ?assertError( + timeout, + emqx_resource:query( + ?ID, + {inc_counter, 1}, + #{timeout => TimeoutMS} + ) + ) + end), + Pid1 = + spawn_link(fun() -> + receive + go -> ok + end, + ?assertEqual( + ok, + emqx_resource:query( + ?ID, + {inc_counter, 2}, + #{timeout => infinity} + ) + ) + end), + {ok, _} = snabbkaffe:receive_events(SRef1), + Pid1 ! go, + {ok, _} = snabbkaffe:receive_events(SRef0), + + {ok, _} = + ?block_until( + #{?snk_kind := buffer_worker_retry_expired}, + ResumeInterval * 10 + ), + + SuccessEventKind = + case IsBatch of + batch -> buffer_worker_retry_inflight_succeeded; + single -> buffer_worker_flush_ack + end, + {ok, {ok, _}} = + ?wait_async_action( + emqx_resource:simple_sync_query(?ID, resume), + #{?snk_kind := SuccessEventKind}, + ResumeInterval * 5 + ), + + ok + end, + fun(Trace) -> + ?assertMatch( + [#{expired := [{query, _, {inc_counter, 1}, _, _}]}], + ?of_kind(buffer_worker_retry_expired, Trace) + ), + ok + end + ), + ok. + +t_expiration_retry_batch_multiple_times(_Config) -> + ResumeInterval = 300, + emqx_connector_demo:set_callback_mode(always_sync), + {ok, _} = emqx_resource:create( + ?ID, + ?DEFAULT_RESOURCE_GROUP, + ?TEST_RESOURCE, + #{name => test_resource}, + #{ + query_mode => sync, + batch_size => 2, + batch_time => 100, + worker_pool_size => 1, + resume_interval => ResumeInterval + } + ), + ?check_trace( + begin + ok = emqx_resource:simple_sync_query(?ID, block), + + {ok, SRef} = snabbkaffe:subscribe( + ?match_event(#{?snk_kind := buffer_worker_flush_nack}), + 1, + 200 + ), + TimeoutMS = 100, + spawn_link(fun() -> + ?assertError( + timeout, + emqx_resource:query( + ?ID, + {inc_counter, 1}, + #{timeout => TimeoutMS} + ) + ) + end), + spawn_link(fun() -> + ?assertError( + timeout, + emqx_resource:query( + ?ID, + {inc_counter, 2}, + #{timeout => ResumeInterval + TimeoutMS} + ) + ) + end), + {ok, _} = snabbkaffe:receive_events(SRef), + + {ok, _} = + snabbkaffe:block_until( + ?match_n_events(2, #{?snk_kind := buffer_worker_retry_expired}), + ResumeInterval * 10 + ), + + ok + end, + fun(Trace) -> + ?assertMatch( + [ + #{expired := [{query, _, {inc_counter, 1}, _, _}]}, + #{expired := [{query, _, {inc_counter, 2}, _, _}]} + ], + ?of_kind(buffer_worker_retry_expired, Trace) + ), + ok + end + ), + ok. + %%------------------------------------------------------------------------------ %% Helpers %%------------------------------------------------------------------------------ + inc_counter_in_parallel(N) -> inc_counter_in_parallel(N, #{}). @@ -1564,6 +2235,81 @@ tap_metrics(Line) -> ct:pal("metrics (l. ~b): ~p", [Line, #{counters => C, gauges => G}]), #{counters => C, gauges => G}. +install_telemetry_handler(TestCase) -> + Tid = ets:new(TestCase, [ordered_set, public]), + HandlerId = TestCase, + TestPid = self(), + _ = telemetry:attach_many( + HandlerId, + emqx_resource_metrics:events(), + fun(EventName, Measurements, Metadata, _Config) -> + Data = #{ + name => EventName, + measurements => Measurements, + metadata => Metadata + }, + ets:insert(Tid, {erlang:monotonic_time(), Data}), + TestPid ! {telemetry, Data}, + ok + end, + unused_config + ), + on_exit(fun() -> + telemetry:detach(HandlerId), + ets:delete(Tid) + end), + put({?MODULE, telemetry_table}, Tid), + Tid. + +wait_until_gauge_is(GaugeName, ExpectedValue, Timeout) -> + Events = receive_all_events(GaugeName, Timeout), + case length(Events) > 0 andalso lists:last(Events) of + #{measurements := #{gauge_set := ExpectedValue}} -> + ok; + #{measurements := #{gauge_set := Value}} -> + ct:fail( + "gauge ~p didn't reach expected value ~p; last value: ~p", + [GaugeName, ExpectedValue, Value] + ); + false -> + ct:pal("no ~p gauge events received!", [GaugeName]) + end. + +receive_all_events(EventName, Timeout) -> + receive_all_events(EventName, Timeout, []). + +receive_all_events(EventName, Timeout, Acc) -> + receive + {telemetry, #{name := [_, _, EventName]} = Event} -> + receive_all_events(EventName, Timeout, [Event | Acc]) + after Timeout -> + lists:reverse(Acc) + end. + +wait_telemetry_event(EventName) -> + wait_telemetry_event(EventName, #{timeout => 5_000, n_events => 1}). + +wait_telemetry_event( + EventName, + Opts0 +) -> + DefaultOpts = #{timeout => 5_000, n_events => 1}, + #{timeout := Timeout, n_events := NEvents} = maps:merge(DefaultOpts, Opts0), + wait_n_events(NEvents, Timeout, EventName). + +wait_n_events(NEvents, _Timeout, _EventName) when NEvents =< 0 -> + ok; +wait_n_events(NEvents, Timeout, EventName) -> + TelemetryTable = get({?MODULE, telemetry_table}), + receive + {telemetry, #{name := [_, _, EventName]}} -> + wait_n_events(NEvents - 1, Timeout, EventName) + after Timeout -> + RecordedEvents = ets:tab2list(TelemetryTable), + ct:pal("recorded events: ~p", [RecordedEvents]), + error({timeout_waiting_for_telemetry, EventName}) + end. + assert_sync_retry_fail_then_succeed_inflight(Trace) -> ct:pal(" ~p", [Trace]), ?assert( diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_gcp_pubsub_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_gcp_pubsub_SUITE.erl index 47a4646de..c9560739f 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_gcp_pubsub_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_gcp_pubsub_SUITE.erl @@ -350,10 +350,12 @@ service_account_json(PrivateKeyPEM) -> metrics_mapping() -> #{ dropped => fun emqx_resource_metrics:dropped_get/1, + dropped_expired => fun emqx_resource_metrics:dropped_expired_get/1, dropped_other => fun emqx_resource_metrics:dropped_other_get/1, dropped_queue_full => fun emqx_resource_metrics:dropped_queue_full_get/1, dropped_resource_not_found => fun emqx_resource_metrics:dropped_resource_not_found_get/1, dropped_resource_stopped => fun emqx_resource_metrics:dropped_resource_stopped_get/1, + late_reply => fun emqx_resource_metrics:late_reply_get/1, failed => fun emqx_resource_metrics:failed_get/1, inflight => fun emqx_resource_metrics:inflight_get/1, matched => fun emqx_resource_metrics:matched_get/1, @@ -1117,9 +1119,6 @@ do_econnrefused_or_timeout_test(Config, Error) -> CurrentMetrics ); {timeout, async} -> - wait_telemetry_event(TelemetryTable, success, ResourceId, #{ - timeout => 10_000, n_events => 2 - }), wait_until_gauge_is(inflight, 0, _Timeout = 400), wait_until_gauge_is(queuing, 0, _Timeout = 400), assert_metrics( @@ -1130,7 +1129,8 @@ do_econnrefused_or_timeout_test(Config, Error) -> matched => 2, queuing => 0, retried => 0, - success => 2 + success => 0, + late_reply => 2 }, ResourceId ); diff --git a/lib-ee/emqx_ee_connector/src/emqx_ee_connector_gcp_pubsub.erl b/lib-ee/emqx_ee_connector/src/emqx_ee_connector_gcp_pubsub.erl index 041bdec08..898c36fe0 100644 --- a/lib-ee/emqx_ee_connector/src/emqx_ee_connector_gcp_pubsub.erl +++ b/lib-ee/emqx_ee_connector/src/emqx_ee_connector_gcp_pubsub.erl @@ -58,7 +58,6 @@ %% emqx_resource API %%------------------------------------------------------------------------------------------------- -%% TODO: check is_buffer_supported() -> false. callback_mode() -> async_if_possible. From ca4a262b758ba7adc537813292f76242bf4f96c1 Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Fri, 20 Jan 2023 08:24:53 -0300 Subject: [PATCH 0290/1802] refactor: re-organize dealing with unrecoverable errors --- .../test/emqx_bridge_mqtt_SUITE.erl | 11 ++- .../src/emqx_connector_pgsql.erl | 6 +- .../src/emqx_resource_buffer_worker.erl | 81 ++++++++++--------- .../test/emqx_ee_bridge_gcp_pubsub_SUITE.erl | 15 ++-- .../test/emqx_ee_bridge_influxdb_SUITE.erl | 33 +++++++- .../test/emqx_ee_bridge_mysql_SUITE.erl | 2 +- .../test/emqx_ee_bridge_pgsql_SUITE.erl | 26 ++++-- .../src/emqx_ee_connector_influxdb.erl | 34 +++++++- 8 files changed, 141 insertions(+), 67 deletions(-) diff --git a/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl b/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl index 1f5b06fab..a99f06f20 100644 --- a/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl +++ b/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl @@ -957,12 +957,11 @@ assert_mqtt_msg_received(Topic, Payload) -> receive {deliver, Topic, #message{payload = Payload}} -> ct:pal("Got mqtt message: ~p on topic ~p", [Payload, Topic]), - ok; - Msg -> - ct:pal("Unexpected Msg: ~p", [Msg]), - assert_mqtt_msg_received(Topic, Payload) - after 100 -> - ct:fail("timeout waiting for ~p on topic ~p", [Payload, Topic]) + ok + after 300 -> + {messages, Messages} = process_info(self(), messages), + Msg = io_lib:format("timeout waiting for ~p on topic ~p", [Payload, Topic]), + error({Msg, #{messages => Messages}}) end. request(Method, Url, Body) -> diff --git a/apps/emqx_connector/src/emqx_connector_pgsql.erl b/apps/emqx_connector/src/emqx_connector_pgsql.erl index 34defb5e5..890227b9d 100644 --- a/apps/emqx_connector/src/emqx_connector_pgsql.erl +++ b/apps/emqx_connector/src/emqx_connector_pgsql.erl @@ -189,8 +189,8 @@ on_batch_query( Datas2 = [emqx_plugin_libs_rule:proc_sql(TokenList, Data) || Data <- Datas], St = maps:get(BinKey, Sts), case on_sql_query(InstId, PoolName, execute_batch, St, Datas2) of - {error, Error} -> - {error, Error}; + {error, _Error} = Result -> + handle_result(Result); {_Column, Results} -> handle_batch_result(Results, 0) end @@ -417,6 +417,8 @@ to_bin(Bin) when is_binary(Bin) -> to_bin(Atom) when is_atom(Atom) -> erlang:atom_to_binary(Atom). +handle_result({error, disconnected}) -> + {error, {recoverable_error, disconnected}}; handle_result({error, Error}) -> {error, {unrecoverable_error, Error}}; handle_result(Res) -> diff --git a/apps/emqx_resource/src/emqx_resource_buffer_worker.erl b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl index 8b79ce5a8..669b8e474 100644 --- a/apps/emqx_resource/src/emqx_resource_buffer_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl @@ -783,45 +783,26 @@ handle_query_result_pure(Id, ?RESOURCE_ERROR_M(Reason, _), _HasBeenSent) -> ok end, {nack, PostFn}; -handle_query_result_pure(Id, {error, {unrecoverable_error, Reason}}, HasBeenSent) -> - PostFn = fun() -> - ?SLOG(error, #{id => Id, msg => unrecoverable_error, reason => Reason}), - inc_sent_failed(Id, HasBeenSent), - ok - end, - {ack, PostFn}; -handle_query_result_pure(Id, {error, {recoverable_error, Reason}}, _HasBeenSent) -> - %% the message will be queued in replayq or inflight window, - %% i.e. the counter 'queuing' or 'dropped' will increase, so we pretend that we have not - %% sent this message. - PostFn = fun() -> - ?SLOG(warning, #{id => Id, msg => recoverable_error, reason => Reason}), - ok - end, - {nack, PostFn}; -handle_query_result_pure(Id, {error, Reason}, _HasBeenSent) -> - PostFn = fun() -> - ?SLOG(error, #{id => Id, msg => send_error, reason => Reason}), - ok - end, - {nack, PostFn}; -handle_query_result_pure(Id, {async_return, {error, {unrecoverable_error, Reason}}}, HasBeenSent) -> - PostFn = fun() -> - ?SLOG(error, #{id => Id, msg => unrecoverable_error, reason => Reason}), - inc_sent_failed(Id, HasBeenSent), - ok - end, - {ack, PostFn}; -handle_query_result_pure(Id, {async_return, {error, Msg}}, _HasBeenSent) -> - PostFn = fun() -> - ?SLOG(error, #{id => Id, msg => async_send_error, info => Msg}), - ok - end, - {nack, PostFn}; -handle_query_result_pure(_Id, {async_return, ok}, _HasBeenSent) -> - {ack, fun() -> ok end}; -handle_query_result_pure(_Id, {async_return, {ok, Pid}}, _HasBeenSent) when is_pid(Pid) -> - {ack, fun() -> ok end}; +handle_query_result_pure(Id, {error, Reason} = Error, HasBeenSent) -> + case is_unrecoverable_error(Error) of + true -> + PostFn = + fun() -> + ?SLOG(error, #{id => Id, msg => unrecoverable_error, reason => Reason}), + inc_sent_failed(Id, HasBeenSent), + ok + end, + {ack, PostFn}; + false -> + PostFn = + fun() -> + ?SLOG(error, #{id => Id, msg => send_error, reason => Reason}), + ok + end, + {nack, PostFn} + end; +handle_query_result_pure(Id, {async_return, Result}, HasBeenSent) -> + handle_query_async_result_pure(Id, Result, HasBeenSent); handle_query_result_pure(Id, Result, HasBeenSent) -> PostFn = fun() -> assert_ok_result(Result), @@ -830,6 +811,28 @@ handle_query_result_pure(Id, Result, HasBeenSent) -> end, {ack, PostFn}. +handle_query_async_result_pure(Id, {error, Reason} = Error, HasBeenSent) -> + case is_unrecoverable_error(Error) of + true -> + PostFn = + fun() -> + ?SLOG(error, #{id => Id, msg => unrecoverable_error, reason => Reason}), + inc_sent_failed(Id, HasBeenSent), + ok + end, + {ack, PostFn}; + false -> + PostFn = fun() -> + ?SLOG(error, #{id => Id, msg => async_send_error, reason => Reason}), + ok + end, + {nack, PostFn} + end; +handle_query_async_result_pure(_Id, {ok, Pid}, _HasBeenSent) when is_pid(Pid) -> + {ack, fun() -> ok end}; +handle_query_async_result_pure(_Id, ok, _HasBeenSent) -> + {ack, fun() -> ok end}. + handle_async_worker_down(Data0, Pid) -> #{async_workers := AsyncWorkers0} = Data0, {WorkerMRef, AsyncWorkers} = maps:take(Pid, AsyncWorkers0), diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_gcp_pubsub_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_gcp_pubsub_SUITE.erl index c9560739f..247b7799b 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_gcp_pubsub_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_gcp_pubsub_SUITE.erl @@ -1135,8 +1135,6 @@ do_econnrefused_or_timeout_test(Config, Error) -> ResourceId ); {_, sync} -> - %% even waiting, hard to avoid flakiness... simpler to just sleep - %% a bit until stabilization. wait_until_gauge_is(queuing, 0, 500), wait_until_gauge_is(inflight, 1, 500), assert_metrics( @@ -1336,12 +1334,19 @@ t_unrecoverable_error(Config) -> ), wait_until_gauge_is(queuing, 0, _Timeout = 400), - wait_until_gauge_is(inflight, 1, _Timeout = 400), + %% TODO: once temporary clause in + %% `emqx_resource_buffer_worker:is_unrecoverable_error' + %% that marks all unknown errors as unrecoverable is + %% removed, this inflight should be 1, because we retry if + %% the worker is killed. + wait_until_gauge_is(inflight, 0, _Timeout = 400), assert_metrics( #{ dropped => 0, - failed => 0, - inflight => 1, + %% FIXME: see comment above; failed should be 0 + %% and inflight should be 1. + failed => 1, + inflight => 0, matched => 1, queuing => 0, retried => 0, diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_influxdb_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_influxdb_SUITE.erl index fc7dce418..e1899b1b2 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_influxdb_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_influxdb_SUITE.erl @@ -277,6 +277,7 @@ influxdb_config(apiv1 = Type, InfluxDBHost, InfluxDBPort, Config) -> " precision = ns\n" " write_syntax = \"~s\"\n" " resource_opts = {\n" + " request_timeout = 1s\n" " query_mode = ~s\n" " batch_size = ~b\n" " }\n" @@ -313,6 +314,7 @@ influxdb_config(apiv2 = Type, InfluxDBHost, InfluxDBPort, Config) -> " precision = ns\n" " write_syntax = \"~s\"\n" " resource_opts = {\n" + " request_timeout = 1s\n" " query_mode = ~s\n" " batch_size = ~b\n" " }\n" @@ -906,9 +908,23 @@ t_write_failure(Config) -> emqx_common_test_helpers:with_failure(down, ProxyName, ProxyHost, ProxyPort, fun() -> case QueryMode of sync -> - ?assertError(timeout, send_message(Config, SentData)); + {_, {ok, _}} = + ?wait_async_action( + try + send_message(Config, SentData) + catch + error:timeout -> + {error, timeout} + end, + #{?snk_kind := buffer_worker_flush_nack}, + 1_000 + ); async -> - ?assertEqual(ok, send_message(Config, SentData)) + ?wait_async_action( + ?assertEqual(ok, send_message(Config, SentData)), + #{?snk_kind := buffer_worker_reply_after_query}, + 1_000 + ) end end), fun(Trace0) -> @@ -920,11 +936,20 @@ t_write_failure(Config) -> ?assert( {error, {error, {closed, "The connection was lost."}}} =:= Result orelse {error, {error, closed}} =:= Result orelse - {error, {error, econnrefused}} =:= Result, + {error, {recoverable_error, {error, econnrefused}}} =:= Result, #{got => Result} ); async -> - ok + Trace = ?of_kind(buffer_worker_reply_after_query, Trace0), + ?assertMatch([#{action := nack} | _], Trace), + [#{result := Result} | _] = Trace, + ?assert( + {error, {recoverable_error, {closed, "The connection was lost."}}} =:= + Result orelse + {error, {error, closed}} =:= Result orelse + {error, {recoverable_error, econnrefused}} =:= Result, + #{got => Result} + ) end, ok end diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_mysql_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_mysql_SUITE.erl index 3bac01c66..57792b366 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_mysql_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_mysql_SUITE.erl @@ -228,7 +228,7 @@ query_resource(Config, Request) -> Name = ?config(mysql_name, Config), BridgeType = ?config(mysql_bridge_type, Config), ResourceID = emqx_bridge_resource:resource_id(BridgeType, Name), - emqx_resource:query(ResourceID, Request). + emqx_resource:query(ResourceID, Request, #{timeout => 500}). unprepare(Config, Key) -> Name = ?config(mysql_name, Config), diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_pgsql_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_pgsql_SUITE.erl index bdbbed8cf..25752f685 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_pgsql_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_pgsql_SUITE.erl @@ -249,7 +249,7 @@ query_resource(Config, Request) -> Name = ?config(pgsql_name, Config), BridgeType = ?config(pgsql_bridge_type, Config), ResourceID = emqx_bridge_resource:resource_id(BridgeType, Name), - emqx_resource:query(ResourceID, Request). + emqx_resource:query(ResourceID, Request, #{timeout => 1_000}). connect_direct_pgsql(Config) -> Opts = #{ @@ -422,12 +422,22 @@ t_write_failure(Config) -> SentData = #{payload => Val, timestamp => 1668602148000}, ?check_trace( emqx_common_test_helpers:with_failure(down, ProxyName, ProxyHost, ProxyPort, fun() -> - case QueryMode of - sync -> - ?assertError(timeout, send_message(Config, SentData)); - async -> - send_message(Config, SentData) - end + {_, {ok, _}} = + ?wait_async_action( + case QueryMode of + sync -> + try + send_message(Config, SentData) + catch + error:timeout -> + {error, timeout} + end; + async -> + send_message(Config, SentData) + end, + #{?snk_kind := buffer_worker_flush_nack}, + 1_000 + ) end), fun(Trace0) -> ct:pal("trace: ~p", [Trace0]), @@ -437,7 +447,7 @@ t_write_failure(Config) -> case Error of {resource_error, _} -> ok; - disconnected -> + {recoverable_error, disconnected} -> ok; _ -> ct:fail("unexpected error: ~p", [Error]) diff --git a/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl b/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl index 0037242b3..1ae4d9874 100644 --- a/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl +++ b/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl @@ -26,6 +26,7 @@ on_batch_query_async/4, on_get_status/2 ]). +-export([reply_callback/2]). -export([ namespace/0, @@ -353,7 +354,12 @@ do_query(InstId, Client, Points) -> connector => InstId, reason => Reason }), - Err + case is_unrecoverable_error(Err) of + true -> + {error, {unrecoverable_error, Reason}}; + false -> + {error, {recoverable_error, Reason}} + end end. do_async_query(InstId, Client, Points, ReplyFunAndArgs) -> @@ -362,7 +368,20 @@ do_async_query(InstId, Client, Points, ReplyFunAndArgs) -> connector => InstId, points => Points }), - {ok, _WorkerPid} = influxdb:write_async(Client, Points, ReplyFunAndArgs). + WrappedReplyFunAndArgs = {fun ?MODULE:reply_callback/2, [ReplyFunAndArgs]}, + {ok, _WorkerPid} = influxdb:write_async(Client, Points, WrappedReplyFunAndArgs). + +reply_callback(ReplyFunAndArgs, {error, Reason} = Error) -> + case is_unrecoverable_error(Error) of + true -> + Result = {error, {unrecoverable_error, Reason}}, + emqx_resource:apply_reply_fun(ReplyFunAndArgs, Result); + false -> + Result = {error, {recoverable_error, Reason}}, + emqx_resource:apply_reply_fun(ReplyFunAndArgs, Result) + end; +reply_callback(ReplyFunAndArgs, Result) -> + emqx_resource:apply_reply_fun(ReplyFunAndArgs, Result). %% ------------------------------------------------------------------------------------------------- %% Tags & Fields Config Trans @@ -583,6 +602,17 @@ str(B) when is_binary(B) -> str(S) when is_list(S) -> S. +is_unrecoverable_error({error, {unrecoverable_error, _}}) -> + true; +is_unrecoverable_error({error, {recoverable_error, _}}) -> + false; +is_unrecoverable_error({error, {error, econnrefused}}) -> + false; +is_unrecoverable_error({error, econnrefused}) -> + false; +is_unrecoverable_error(_) -> + false. + %%=================================================================== %% eunit tests %%=================================================================== From 727100e094e162d7d65371dda430d96adcd5f5e2 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Fri, 20 Jan 2023 16:42:01 +0100 Subject: [PATCH 0291/1802] chore: prepare for v5.0.15 release --- apps/emqx/include/emqx_release.hrl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx/include/emqx_release.hrl b/apps/emqx/include/emqx_release.hrl index e9eab302c..7437bc299 100644 --- a/apps/emqx/include/emqx_release.hrl +++ b/apps/emqx/include/emqx_release.hrl @@ -32,7 +32,7 @@ %% `apps/emqx/src/bpapi/README.md' %% Community edition --define(EMQX_RELEASE_CE, "5.0.15-rc.1"). +-define(EMQX_RELEASE_CE, "5.0.15"). %% Enterprise edition -define(EMQX_RELEASE_EE, "5.0.0-rc.1"). From 92797d7260c0688af7baa137e9e1e1c1593227ab Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Fri, 20 Jan 2023 17:00:55 +0100 Subject: [PATCH 0292/1802] chore: Generate changelog for v5.0.15 --- changes/v5.0.15-en.md | 79 +++++++++++++++++++++++++++++++++++++++++++ changes/v5.0.15-zh.md | 76 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 155 insertions(+) create mode 100644 changes/v5.0.15-en.md create mode 100644 changes/v5.0.15-zh.md diff --git a/changes/v5.0.15-en.md b/changes/v5.0.15-en.md new file mode 100644 index 000000000..f77753c25 --- /dev/null +++ b/changes/v5.0.15-en.md @@ -0,0 +1,79 @@ +# v5.0.15 + +## Enhancements + +- [#9569](https://github.com/emqx/emqx/pull/9569) Refactor `/authorization/sources/built_in_database/` by adding `rules/` to the path. + +- [#9585](https://github.com/emqx/emqx/pull/9585) `/bridges_probe` API endpoint to test params for creating a new data bridge. + +- [#9586](https://github.com/emqx/emqx/pull/9586) Basic auth is no longer allowed for API calls, must use API key instead. + +- [#9628](https://github.com/emqx/emqx/pull/9628) Expose additional resource configuration parameters: `start_after_created` and `start_timeout`. + +- [#9722](https://github.com/emqx/emqx/pull/9722) Add the following configuration options for Pushing metrics to Prometheus Push Gateway: + - `headers`: Allows custom HTTP request headers. + - `job_name`: allows to customize the name of the Job pushed to Push Gateway. + +- [#9725](https://github.com/emqx/emqx/pull/9725) Remove the config `auto_reconnect` from the emqx_authz, emqx_authn and data-bridge componets. + This is because we have another config with similar functions: `resource_opts.auto_restart_interval`。 + + The functions of these two config are difficult to distinguish, which will lead to confusion. + After this change, `auto_reconnect` will not be configurable (always be true), and the underlying + drivers that support this config will automatically reconnect the abnormally disconnected + connection every `2s`. + + And the config `resource_opts.auto_restart_interval` is still available for user. + It is the time interval that emqx restarts the resource when the connection cannot be + established for some reason. + +- [#9736](https://github.com/emqx/emqx/pull/9736) Refactor of /bridges API to make it more consistent with other APIs: + - bridge enable/disable is now done via the endpoint `/bridges/{id}/enable/[true,false]` + - `/bridges/{id}/operation/{operation}` endpoints are now `/bridges/{id}/{operation}` + - metrics are moved out from the GET `/bridges/{id}` response and can now be fetched via `/bridges/{id}/metrics` + - the `bridges/{id}/reset_metrics` endpoint is now `/bridges/{id}/metrics/reset` + +- [#9774](https://github.com/emqx/emqx/pull/9774) Add a password complexity requirement when adding or modifying Dashboard users via the API. + Now password must contain at least 2 of alphabetic, numeric and special characters, + and must be 8 to 64 characters long. + +## Bug fixes + +- [#9626](https://github.com/emqx/emqx/pull/9626) Return authorization settings with default values. + The authorization cache is enabled by default, but due to the missing default value in `GET` response of `/authorization/settings`, it seemed to be disabled from the dashboard. + +- [#9680](https://github.com/emqx/emqx/pull/9680) Fix the problem that username and password authentication is mandatory in Influxdb v1 write API. + +- [#9726](https://github.com/emqx/emqx/pull/9726) Client fuzzy search API results were missing information which could tell if more results are available in the next pages, this is now fixed by providing `hasnext` flag in the response. + +- [#9735](https://github.com/emqx/emqx/pull/9735) Password information has been removed from information log messages for http, ldap, mongo, mqtt, mysql, pgsql and redis. + +- [#9748](https://github.com/emqx/emqx/pull/9748) Listeners not configured with `max_connections` will cause the cluster `/listeners` API to return 500 error. + +- [#9749](https://github.com/emqx/emqx/pull/9749) In some cases search APIs could respond with an incorrect `count` value in the metadata, that is usually much bigger than expected, this is now fixed. + +- [#9750](https://github.com/emqx/emqx/pull/9750) Reload overriding configs after boot. + Prior to this change, two configs were allow to change from dashboard, but will not take effect after reboot: + * Logging (such as level) + * Prometheus configs + + +- [#9751](https://github.com/emqx/emqx/pull/9751) Fix that obsoleted cert file will not be deleted after the listener is updated/deleted + +- [#9763](https://github.com/emqx/emqx/pull/9763) Fix an authentication exception when password is not provided + +- [#9765](https://github.com/emqx/emqx/pull/9765) Parse decimals as password from environment variable overrides correctly. + Prior to this change, config values for passwords are not allowed to be decimals. + e.g. `EMQX_FOOBAR__PASSWORD=12344` or `emqx.foobar.password=1234` + would result in a type check error, unless quoted as: + `EMQX_FOOBAR__PASSWORD='"12344"'` or `emqx.foobar.password="1234"`. + After this fix, the value does not have to be quoted. + +- [#9769](https://github.com/emqx/emqx/pull/9769) Fix Erlang shell prompt version prefix. e5.0.15 -> v5.0.15 + +- [#9780](https://github.com/emqx/emqx/pull/9780) When creating disk queue directory for resource worker, substitute ':' with '-' in worker id. + +- [#9781](https://github.com/emqx/emqx/pull/9781) Trace files were left on a node when creating a zip file for download. They are now removed when the file is sent. Also, concurrent downloads will no longer interfere with each other. + +- [#9785](https://github.com/emqx/emqx/pull/9785) Stop authentication hook chain if `emqx_authentication` provides a definitive result. + +- [#9787](https://github.com/emqx/emqx/pull/9787) Fix a compatible problem for the `webhook` bridge configuration which was created before the v5.0.12. diff --git a/changes/v5.0.15-zh.md b/changes/v5.0.15-zh.md new file mode 100644 index 000000000..679298613 --- /dev/null +++ b/changes/v5.0.15-zh.md @@ -0,0 +1,76 @@ +# v5.0.15 + +## 增强 + +- [#9569](https://github.com/emqx/emqx/pull/9569) 重构 `/authorization/sources/built_in_database/` 接口,将 `rules/` 添加到了其路径中。 + +- [#9585](https://github.com/emqx/emqx/pull/9585) 添加新 API 接口 `/bridges_probe` 用于测试创建桥接的参数是否可用。 + +- [#9586](https://github.com/emqx/emqx/pull/9586) API 调用不再支持基于 `username:password` 的 `baisc` 认证, 现在 API 必须通过 API Key 才能进行调用。 + +- [#9628](https://github.com/emqx/emqx/pull/9628) 为桥接资源增加了配置参数:`start_after_created` 和 `start_timeout`。 + +- [#9722](https://github.com/emqx/emqx/pull/9722) 为 Prometheus 推送到 Push Gateway 新增以下配置项: + - `headers`:允许自定义 HTTP 请求头。 + - `job_name`:允许自定义推送到 Push Gateway 的 Job 名称。 + +- [#9725](https://github.com/emqx/emqx/pull/9725) 从认证、鉴权和数据桥接功能中,删除 `auto_reconnect` 配置项,因为我们还有另一个功能类似的配置项: + `resource_opts.auto_restart_interval`。 + + 这两个配置项的功能难以区分,会导致困惑。此修改之后,`auto_reconnect` 将不可配置(永远为 true), + 支持此配置的底层驱动将以 `2s` 为周期自动重连异常断开的连接。 + + 而 `resource_opts.auto_restart_interval` 配置项仍然开放给用户配置,它是资源因为某些原因 + 无法建立连接的时候,emqx 重新启动该资源的时间间隔。 + +- [#9736](https://github.com/emqx/emqx/pull/9736) 重构部分 /bridges 的API 使得其和其他 API 能够更加一致: + - 桥接的启用和禁用现在是通过 `/bridges/{id}/enable/[true,false]` API 来实现的 + - 使用 `/bridges/{id}/{operation}` 替换了旧的 `/bridges/{id}/operation/{operation}` API + - 指标数据从 `/bridges/{id}` 的响应消息中移除,现在可以使用新的 API `/bridges/{id}/metrics` 进行访问 + - 使用 `/bridges/{id}/metrics/reset` 替换了旧的 `bridges/{id}/reset_metrics` API + +- [#9774](https://github.com/emqx/emqx/pull/9774) 通过 API 添加、修改 Dashboard 用户时,增加对密码复杂度的要求。 + 现在密码必须包含字母、数字以及特殊字符中的至少 2 种,并且长度范围必须是 8~64 个字符。 + +## 修复 + +- [#9626](https://github.com/emqx/emqx/pull/9626) 为授权设置 API 返回默认值。 + 授权缓存默认为开启,但是在此修复前,因为默认值在 `/authorization/settings` 这个 API 的返回值中缺失, + 使得在仪表盘配置页面中看起来是关闭了。 + +- [#9680](https://github.com/emqx/emqx/pull/9680) 修复 InfluxDB v1 桥接写入 API 配置中强制需要用户名密码认证的问题。 + +- [#9726](https://github.com/emqx/emqx/pull/9726) 在此修复前,客户端模糊搜索 API 缺少一些可以用于判断是否可以继续翻页的信息,现在通过在响应中提供 `hasnext` 标志来解决这个问题。 + +- [#9735](https://github.com/emqx/emqx/pull/9735) 密码信息已从http、ldap、mongo、mqtt、mysql、pgsql和redis的信息日志消息中删除。 + +- [#9748](https://github.com/emqx/emqx/pull/9748) 监听器不配置 `max_connections` 时会导致集群 `/listeners` 接口返回 500 错误。 + +- [#9749](https://github.com/emqx/emqx/pull/9749) 在某些情况下,搜索 API 可能会在元数据中响应不正确的 `count` 值,这通常比预期的要大得多,现在已经修复了。 + +- [#9750](https://github.com/emqx/emqx/pull/9750) 启动后重新加载一些重载配置项。 + 在此修复前,下面两个配置项允许从 Dashboard 控制台修改,但是在重启后无法生效: + * 日志 (例如日志级别) + * Prometheus 配置 + +- [#9751](https://github.com/emqx/emqx/pull/9751) 修复在更新或者删除监听器后,过时的证书文件没有被删除的问题。 + +- [#9763](https://github.com/emqx/emqx/pull/9763) 修复客户端没有提供密码时的一个异常 + +- [#9765](https://github.com/emqx/emqx/pull/9765) 允许使用纯数字作为密码配置。 + 在此修复前,密码的配置必须是字符串,使用纯数字时,会报类型检查错误。 + 例如,`EMQX_FOOBAR__PASSWORD=12344` 或 `emqx.foobar.password=1234` 会出错, + 必须用引把值括起来才行: + `EMQX_FOOBAR__PASSWORD='"12344"'` 或 `emqx.foobar.password="1234"`。 + 修复后可以不使用引号。在环境变量重载中使用更加方便。 + + +- [#9769](https://github.com/emqx/emqx/pull/9769) 修复 Eralng 控制台版本号前缀的打印错误 e5.0.15 -> v5.0.15 + +- [#9780](https://github.com/emqx/emqx/pull/9780) 在为资源缓存进程创建磁盘队列目录时,在ID中用 '-' 代替 ':'。 + +- [#9781](https://github.com/emqx/emqx/pull/9781) 当下载 日志追踪 的日志时,一些中间文件将存留在处理节点上,现在这个问题得到了修复。同时,并发下载日志将不再相互干扰。 + +- [#9785](https://github.com/emqx/emqx/pull/9785) 如果 `emqx_authentication` 提供了确定的结果,则停止认证钩子链。 + +- [#9787](https://github.com/emqx/emqx/pull/9787) 修复对在 v5.0.12 之前创建的 `webhook` 桥接配置的兼容问题。 From e68ee4c2ffcb52a7b17f94220ac46c3d15ce60c9 Mon Sep 17 00:00:00 2001 From: Ivan Dyachkov Date: Mon, 23 Jan 2023 13:30:00 +0100 Subject: [PATCH 0293/1802] ci: build ubuntu 22.04 packages --- .../workflows/build_and_push_docker_images.yaml | 4 ++-- .github/workflows/build_packages.yaml | 15 ++++++++------- .github/workflows/build_slim_packages.yaml | 6 ++++-- .github/workflows/check_deps_integrity.yaml | 2 +- .github/workflows/code_style_check.yaml | 2 +- .github/workflows/elixir_apps_check.yaml | 2 +- .github/workflows/elixir_deps_check.yaml | 2 +- .github/workflows/elixir_release.yml | 2 +- .github/workflows/run_emqx_app_tests.yaml | 2 +- .github/workflows/run_fvt_tests.yaml | 6 +++--- .github/workflows/run_relup_tests.yaml | 2 +- .github/workflows/run_test_cases.yaml | 6 +++--- 12 files changed, 27 insertions(+), 24 deletions(-) diff --git a/.github/workflows/build_and_push_docker_images.yaml b/.github/workflows/build_and_push_docker_images.yaml index 71515f699..213ab27fd 100644 --- a/.github/workflows/build_and_push_docker_images.yaml +++ b/.github/workflows/build_and_push_docker_images.yaml @@ -25,7 +25,7 @@ jobs: prepare: runs-on: ubuntu-20.04 # prepare source with any OTP version, no need for a matrix - container: "ghcr.io/emqx/emqx-builder/5.0-26:1.13.4-24.3.4.2-1-ubuntu20.04" + container: "ghcr.io/emqx/emqx-builder/5.0-27:1.13.4-24.3.4.2-1-ubuntu20.04" outputs: PROFILE: ${{ steps.get_profile.outputs.PROFILE }} @@ -129,7 +129,7 @@ jobs: # NOTE: 'otp' and 'elixir' are to configure emqx-builder image # only support latest otp and elixir, not a matrix builder: - - 5.0-26 # update to latest + - 5.0-27 # update to latest otp: - 24.3.4.2-1 # switch to 25 once ready to release 5.1 elixir: diff --git a/.github/workflows/build_packages.yaml b/.github/workflows/build_packages.yaml index 8764c7097..2ec7dac3e 100644 --- a/.github/workflows/build_packages.yaml +++ b/.github/workflows/build_packages.yaml @@ -23,7 +23,7 @@ on: jobs: prepare: runs-on: ubuntu-20.04 - container: ghcr.io/emqx/emqx-builder/5.0-26:1.13.4-24.3.4.2-1-ubuntu20.04 + container: ghcr.io/emqx/emqx-builder/5.0-27:1.13.4-24.3.4.2-1-ubuntu20.04 outputs: BUILD_PROFILE: ${{ steps.get_profile.outputs.BUILD_PROFILE }} IS_EXACT_TAG: ${{ steps.get_profile.outputs.IS_EXACT_TAG }} @@ -204,6 +204,7 @@ jobs: - amd64 - arm64 os: + - ubuntu22.04 - ubuntu20.04 - ubuntu18.04 - debian11 @@ -215,7 +216,7 @@ jobs: - aws-arm64 - ubuntu-20.04 builder: - - 5.0-26 + - 5.0-27 elixir: - 1.13.4 exclude: @@ -227,17 +228,17 @@ jobs: - profile: emqx otp: 25.1.2-2 arch: amd64 - os: ubuntu20.04 - build_machine: ubuntu-20.04 - builder: 5.0-26 + os: ubuntu22.04 + build_machine: ubuntu-22.04 + builder: 5.0-27 elixir: 1.13.4 release_with: elixir - profile: emqx otp: 25.1.2-2 arch: amd64 os: amzn2 - build_machine: ubuntu-20.04 - builder: 5.0-26 + build_machine: ubuntu-22.04 + builder: 5.0-27 elixir: 1.13.4 release_with: elixir diff --git a/.github/workflows/build_slim_packages.yaml b/.github/workflows/build_slim_packages.yaml index bfdeb1fb8..a02a9db07 100644 --- a/.github/workflows/build_slim_packages.yaml +++ b/.github/workflows/build_slim_packages.yaml @@ -30,10 +30,12 @@ jobs: matrix: profile: - ["emqx", "24.3.4.2-1", "el7"] - - ["emqx", "25.1.2-2", "ubuntu20.04"] + - ["emqx", "24.3.4.2-1", "ubuntu20.04"] + - ["emqx", "25.1.2-2", "ubuntu22.04"] - ["emqx-enterprise", "24.3.4.2-1", "ubuntu20.04"] + - ["emqx-enterprise", "25.1.2-2", "ubuntu22.04"] builder: - - 5.0-26 + - 5.0-27 elixir: - 1.13.4 diff --git a/.github/workflows/check_deps_integrity.yaml b/.github/workflows/check_deps_integrity.yaml index 59fc01d74..6d4f53dbc 100644 --- a/.github/workflows/check_deps_integrity.yaml +++ b/.github/workflows/check_deps_integrity.yaml @@ -5,7 +5,7 @@ on: [pull_request, push] jobs: check_deps_integrity: runs-on: ubuntu-20.04 - container: ghcr.io/emqx/emqx-builder/5.0-26:1.13.4-25.1.2-2-ubuntu20.04 + container: ghcr.io/emqx/emqx-builder/5.0-27:1.13.4-25.1.2-2-ubuntu20.04 steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/code_style_check.yaml b/.github/workflows/code_style_check.yaml index 6ab8ea8b6..38e3a5b8b 100644 --- a/.github/workflows/code_style_check.yaml +++ b/.github/workflows/code_style_check.yaml @@ -5,7 +5,7 @@ on: [pull_request] jobs: code_style_check: runs-on: ubuntu-20.04 - container: "ghcr.io/emqx/emqx-builder/5.0-26:1.13.4-25.1.2-2-ubuntu20.04" + container: "ghcr.io/emqx/emqx-builder/5.0-27:1.13.4-25.1.2-2-ubuntu20.04" steps: - uses: actions/checkout@v3 with: diff --git a/.github/workflows/elixir_apps_check.yaml b/.github/workflows/elixir_apps_check.yaml index e912db10f..892cd3a05 100644 --- a/.github/workflows/elixir_apps_check.yaml +++ b/.github/workflows/elixir_apps_check.yaml @@ -8,7 +8,7 @@ jobs: elixir_apps_check: runs-on: ubuntu-latest # just use the latest builder - container: "ghcr.io/emqx/emqx-builder/5.0-26:1.13.4-25.1.2-2-ubuntu20.04" + container: "ghcr.io/emqx/emqx-builder/5.0-27:1.13.4-25.1.2-2-ubuntu20.04" strategy: fail-fast: false diff --git a/.github/workflows/elixir_deps_check.yaml b/.github/workflows/elixir_deps_check.yaml index 5e64d69c4..40d70a902 100644 --- a/.github/workflows/elixir_deps_check.yaml +++ b/.github/workflows/elixir_deps_check.yaml @@ -7,7 +7,7 @@ on: [pull_request, push] jobs: elixir_deps_check: runs-on: ubuntu-20.04 - container: ghcr.io/emqx/emqx-builder/5.0-26:1.13.4-25.1.2-2-ubuntu20.04 + container: ghcr.io/emqx/emqx-builder/5.0-27:1.13.4-25.1.2-2-ubuntu20.04 steps: - name: Checkout diff --git a/.github/workflows/elixir_release.yml b/.github/workflows/elixir_release.yml index f53051d1d..b93b3d63c 100644 --- a/.github/workflows/elixir_release.yml +++ b/.github/workflows/elixir_release.yml @@ -17,7 +17,7 @@ jobs: profile: - emqx - emqx-enterprise - container: ghcr.io/emqx/emqx-builder/5.0-26:1.13.4-25.1.2-2-ubuntu20.04 + container: ghcr.io/emqx/emqx-builder/5.0-27:1.13.4-25.1.2-2-ubuntu20.04 steps: - name: Checkout uses: actions/checkout@v3 diff --git a/.github/workflows/run_emqx_app_tests.yaml b/.github/workflows/run_emqx_app_tests.yaml index ddbb14609..0b3f21b4a 100644 --- a/.github/workflows/run_emqx_app_tests.yaml +++ b/.github/workflows/run_emqx_app_tests.yaml @@ -12,7 +12,7 @@ jobs: strategy: matrix: builder: - - 5.0-26 + - 5.0-27 otp: - 24.3.4.2-1 - 25.1.2-2 diff --git a/.github/workflows/run_fvt_tests.yaml b/.github/workflows/run_fvt_tests.yaml index 1a4568725..f233562d1 100644 --- a/.github/workflows/run_fvt_tests.yaml +++ b/.github/workflows/run_fvt_tests.yaml @@ -16,7 +16,7 @@ jobs: prepare: runs-on: ubuntu-20.04 # prepare source with any OTP version, no need for a matrix - container: ghcr.io/emqx/emqx-builder/5.0-26:1.13.4-24.3.4.2-1-alpine3.15.1 + container: ghcr.io/emqx/emqx-builder/5.0-27:1.13.4-24.3.4.2-1-alpine3.15.1 steps: - uses: actions/checkout@v3 @@ -49,7 +49,7 @@ jobs: os: - ["alpine3.15.1", "alpine:3.15.1"] builder: - - 5.0-26 + - 5.0-27 otp: - 24.3.4.2-1 elixir: @@ -122,7 +122,7 @@ jobs: os: - ["debian11", "debian:11-slim"] builder: - - 5.0-26 + - 5.0-27 otp: - 24.3.4.2-1 elixir: diff --git a/.github/workflows/run_relup_tests.yaml b/.github/workflows/run_relup_tests.yaml index a6854aa40..9ae82a706 100644 --- a/.github/workflows/run_relup_tests.yaml +++ b/.github/workflows/run_relup_tests.yaml @@ -15,7 +15,7 @@ on: jobs: relup_test_plan: runs-on: ubuntu-20.04 - container: "ghcr.io/emqx/emqx-builder/5.0-26:1.13.4-24.3.4.2-1-ubuntu20.04" + container: "ghcr.io/emqx/emqx-builder/5.0-27:1.13.4-24.3.4.2-1-ubuntu20.04" outputs: CUR_EE_VSN: ${{ steps.find-versions.outputs.CUR_EE_VSN }} OLD_VERSIONS: ${{ steps.find-versions.outputs.OLD_VERSIONS }} diff --git a/.github/workflows/run_test_cases.yaml b/.github/workflows/run_test_cases.yaml index 91a087c95..f9a689b3c 100644 --- a/.github/workflows/run_test_cases.yaml +++ b/.github/workflows/run_test_cases.yaml @@ -30,12 +30,12 @@ jobs: MATRIX="$(echo "${APPS}" | jq -c ' [ (.[] | select(.profile == "emqx") | . + { - builder: "5.0-26", + builder: "5.0-27", otp: "25.1.2-2", elixir: "1.13.4" }), (.[] | select(.profile == "emqx-enterprise") | . + { - builder: "5.0-26", + builder: "5.0-27", otp: ["24.3.4.2-1", "25.1.2-2"][], elixir: "1.13.4" }) @@ -222,7 +222,7 @@ jobs: - ct - ct_docker runs-on: ubuntu-20.04 - container: "ghcr.io/emqx/emqx-builder/5.0-26:1.13.4-24.3.4.2-1-ubuntu20.04" + container: "ghcr.io/emqx/emqx-builder/5.0-27:1.13.4-24.3.4.2-1-ubuntu20.04" steps: - uses: AutoModality/action-clean@v1 - uses: actions/download-artifact@v3 From a0100c0a4e6dd849b5f8ec32dddc2304dfd65d80 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Mon, 23 Jan 2023 16:18:07 +0100 Subject: [PATCH 0294/1802] chore: bump app versions --- apps/emqx_bridge/src/emqx_bridge.app.src | 2 +- apps/emqx_resource/src/emqx_resource.app.src | 2 +- lib-ee/emqx_ee_bridge/src/emqx_ee_bridge.app.src | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/emqx_bridge/src/emqx_bridge.app.src b/apps/emqx_bridge/src/emqx_bridge.app.src index 39cb1b18b..ca0001319 100644 --- a/apps/emqx_bridge/src/emqx_bridge.app.src +++ b/apps/emqx_bridge/src/emqx_bridge.app.src @@ -1,7 +1,7 @@ %% -*- mode: erlang -*- {application, emqx_bridge, [ {description, "EMQX bridges"}, - {vsn, "0.1.9"}, + {vsn, "0.1.10"}, {registered, []}, {mod, {emqx_bridge_app, []}}, {applications, [ diff --git a/apps/emqx_resource/src/emqx_resource.app.src b/apps/emqx_resource/src/emqx_resource.app.src index e3a37fd10..6bd9fd213 100644 --- a/apps/emqx_resource/src/emqx_resource.app.src +++ b/apps/emqx_resource/src/emqx_resource.app.src @@ -1,7 +1,7 @@ %% -*- mode: erlang -*- {application, emqx_resource, [ {description, "Manager for all external resources"}, - {vsn, "0.1.5"}, + {vsn, "0.1.6"}, {registered, []}, {mod, {emqx_resource_app, []}}, {applications, [ diff --git a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge.app.src b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge.app.src index 11831ab06..b2a3c80c6 100644 --- a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge.app.src +++ b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge.app.src @@ -1,6 +1,6 @@ {application, emqx_ee_bridge, [ {description, "EMQX Enterprise data bridges"}, - {vsn, "0.1.3"}, + {vsn, "0.1.4"}, {registered, []}, {applications, [ kernel, From 140cda2f134d4d82b1794235743216c60d9e79de Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Mon, 23 Jan 2023 19:38:08 +0100 Subject: [PATCH 0295/1802] fix(emqx_connection): crash when idle_timeout is set to infinity --- apps/emqx/i18n/emqx_schema_i18n.conf | 11 ++++-- apps/emqx/src/emqx.app.src | 2 +- apps/emqx/src/emqx_connection.erl | 9 +++-- apps/emqx/test/emqx_client_SUITE.erl | 52 ++++++++++++++++++++++++---- changes/v5.0.16/fix-9834.en.md | 1 + changes/v5.0.16/fix-9834.zh.md | 1 + 6 files changed, 64 insertions(+), 12 deletions(-) create mode 100644 changes/v5.0.16/fix-9834.en.md create mode 100644 changes/v5.0.16/fix-9834.zh.md diff --git a/apps/emqx/i18n/emqx_schema_i18n.conf b/apps/emqx/i18n/emqx_schema_i18n.conf index 0665cfb09..0ab781280 100644 --- a/apps/emqx/i18n/emqx_schema_i18n.conf +++ b/apps/emqx/i18n/emqx_schema_i18n.conf @@ -656,8 +656,15 @@ mqtt 下所有的配置作为全局的默认值存在,它可以被 zone< mqtt_idle_timeout { desc { - en: """After the TCP connection is established, if the MQTT CONNECT packet from the client is not received within the time specified by idle_timeout, the connection will be disconnected.""" - zh: """TCP 连接建立后,如果在 idle_timeout 指定的时间内未收到客户端的 MQTT CONNECT 报文,则连接将被断开。""" + en: """After the TCP connection is established, if the MQTT CONNECT packet from the client is +not received within the time specified by idle_timeout, the connection will be disconnected. +After the CONNECT packet has been accepted by EMQX, if the connection idles for this long time, +then the Erlang process is put to hibernation to save OS resources. Note: long idle_timeout +interval may impose risk at the system if large number of malicious clients only establish connections +but do not send any data.""" + zh: """TCP 连接建立后,如果在 idle_timeout 指定的时间内未收到客户端的 MQTT CONNECT 报文,则连接将被断开。 +如果连接在 CONNECT 报文被 EMQX 接受之后空闲超过该时长,那么服务这个连接的 Erlang 进程会进入休眠以节省系统资源。 +注意,该配置值如果设置过大的情况下,如果大量恶意客户端只连接,但不发任何数据,可能会导致系统资源被恶意消耗。""" } label: { en: """Idle Timeout""" diff --git a/apps/emqx/src/emqx.app.src b/apps/emqx/src/emqx.app.src index 40d2796cd..270d36a5e 100644 --- a/apps/emqx/src/emqx.app.src +++ b/apps/emqx/src/emqx.app.src @@ -3,7 +3,7 @@ {id, "emqx"}, {description, "EMQX Core"}, % strict semver, bump manually! - {vsn, "5.0.15"}, + {vsn, "5.0.16"}, {modules, []}, {registered, []}, {applications, [ diff --git a/apps/emqx/src/emqx_connection.erl b/apps/emqx/src/emqx_connection.erl index 0c6481399..5ed302a6f 100644 --- a/apps/emqx/src/emqx_connection.erl +++ b/apps/emqx/src/emqx_connection.erl @@ -403,14 +403,19 @@ exit_on_sock_error(Reason) -> recvloop( Parent, State = #state{ - idle_timeout = IdleTimeout, + idle_timeout = IdleTimeout0, zone = Zone } ) -> + IdleTimeout = + case IdleTimeout0 of + infinity -> infinity; + _ -> IdleTimeout0 + 100 + end, receive Msg -> handle_recv(Msg, Parent, State) - after IdleTimeout + 100 -> + after IdleTimeout -> case emqx_olp:backoff_hibernation(Zone) of true -> recvloop(Parent, State); diff --git a/apps/emqx/test/emqx_client_SUITE.erl b/apps/emqx/test/emqx_client_SUITE.erl index 79c934b47..8effb60ef 100644 --- a/apps/emqx/test/emqx_client_SUITE.erl +++ b/apps/emqx/test/emqx_client_SUITE.erl @@ -43,6 +43,8 @@ <<"TopicA/#">> ]). +-define(WAIT(EXPR, ATTEMPTS), wait(fun() -> EXPR end, ATTEMPTS)). + all() -> [ {group, mqttv3}, @@ -85,6 +87,12 @@ init_per_suite(Config) -> end_per_suite(_Config) -> emqx_common_test_helpers:stop_apps([]). +init_per_testcase(_Case, Config) -> + Config. + +end_per_testcase(_Case, _Config) -> + emqx_config:put_zone_conf(default, [mqtt, idle_timeout], 15000). + %%-------------------------------------------------------------------- %% Test cases for MQTT v3 %%-------------------------------------------------------------------- @@ -101,16 +109,35 @@ t_basic_v4(_Config) -> t_cm(_) -> emqx_config:put_zone_conf(default, [mqtt, idle_timeout], 1000), - ClientId = <<"myclient">>, + ClientId = atom_to_binary(?FUNCTION_NAME), {ok, C} = emqtt:start_link([{clientid, ClientId}]), {ok, _} = emqtt:connect(C), - ct:sleep(500), - #{clientinfo := #{clientid := ClientId}} = emqx_cm:get_chan_info(ClientId), + ?WAIT(#{clientinfo := #{clientid := ClientId}} = emqx_cm:get_chan_info(ClientId), 2), emqtt:subscribe(C, <<"mytopic">>, 0), - ct:sleep(1200), - Stats = emqx_cm:get_chan_stats(ClientId), - ?assertEqual(1, proplists:get_value(subscriptions_cnt, Stats)), - emqx_config:put_zone_conf(default, [mqtt, idle_timeout], 15000). + ?WAIT( + begin + Stats = emqx_cm:get_chan_stats(ClientId), + ?assertEqual(1, proplists:get_value(subscriptions_cnt, Stats)) + end, + 2 + ), + ok. + +t_idle_timeout_infinity(_) -> + emqx_config:put_zone_conf(default, [mqtt, idle_timeout], infinity), + ClientId = atom_to_binary(?FUNCTION_NAME), + {ok, C} = emqtt:start_link([{clientid, ClientId}]), + {ok, _} = emqtt:connect(C), + ?WAIT(#{clientinfo := #{clientid := ClientId}} = emqx_cm:get_chan_info(ClientId), 2), + emqtt:subscribe(C, <<"mytopic">>, 0), + ?WAIT( + begin + Stats = emqx_cm:get_chan_stats(ClientId), + ?assertEqual(1, proplists:get_value(subscriptions_cnt, Stats)) + end, + 2 + ), + ok. t_cm_registry(_) -> Children = supervisor:which_children(emqx_cm_sup), @@ -363,3 +390,14 @@ tls_certcn_as_clientid(TLSVsn, RequiredTLSVsn) -> #{clientinfo := #{clientid := CN}} = emqx_cm:get_chan_info(CN), confirm_tls_version(Client, RequiredTLSVsn), emqtt:disconnect(Client). + +wait(F, 1) -> + F(); +wait(F, Attempts) when Attempts > 0 -> + try + F() + catch + _:_ -> + timer:sleep(1000), + wait(F, Attempts - 1) + end. diff --git a/changes/v5.0.16/fix-9834.en.md b/changes/v5.0.16/fix-9834.en.md new file mode 100644 index 000000000..d5ad1f67a --- /dev/null +++ b/changes/v5.0.16/fix-9834.en.md @@ -0,0 +1 @@ +Allow `mqtt.idle_timeout` to be set to `infinity` diff --git a/changes/v5.0.16/fix-9834.zh.md b/changes/v5.0.16/fix-9834.zh.md new file mode 100644 index 000000000..06eafc1a0 --- /dev/null +++ b/changes/v5.0.16/fix-9834.zh.md @@ -0,0 +1 @@ +允许配置项 `mqtt.idle_timeout` 设置成 `infinity` From 7575120ea694e0ebacb5b43bdfcc685ae3c29c7e Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Tue, 24 Jan 2023 10:54:20 +0100 Subject: [PATCH 0296/1802] test: use snabbkaffe retry macro --- apps/emqx/test/emqx_client_SUITE.erl | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/apps/emqx/test/emqx_client_SUITE.erl b/apps/emqx/test/emqx_client_SUITE.erl index 8effb60ef..2f433c73d 100644 --- a/apps/emqx/test/emqx_client_SUITE.erl +++ b/apps/emqx/test/emqx_client_SUITE.erl @@ -24,6 +24,7 @@ -include_lib("emqx/include/emqx_mqtt.hrl"). -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). +-include_lib("snabbkaffe/include/snabbkaffe.hrl"). -define(TOPICS, [ <<"TopicA">>, @@ -43,7 +44,7 @@ <<"TopicA/#">> ]). --define(WAIT(EXPR, ATTEMPTS), wait(fun() -> EXPR end, ATTEMPTS)). +-define(WAIT(EXPR, ATTEMPTS), ?retry(1000, ATTEMPTS, EXPR)). all() -> [ @@ -390,14 +391,3 @@ tls_certcn_as_clientid(TLSVsn, RequiredTLSVsn) -> #{clientinfo := #{clientid := CN}} = emqx_cm:get_chan_info(CN), confirm_tls_version(Client, RequiredTLSVsn), emqtt:disconnect(Client). - -wait(F, 1) -> - F(); -wait(F, Attempts) when Attempts > 0 -> - try - F() - catch - _:_ -> - timer:sleep(1000), - wait(F, Attempts - 1) - end. From 502b826396377ae5741b265c14aa0798c6461f2e Mon Sep 17 00:00:00 2001 From: Erik Timan Date: Mon, 23 Jan 2023 16:18:04 +0100 Subject: [PATCH 0297/1802] test: add influxdb docker test files --- scripts/test/influx/certs/server.key | 28 ++++++ scripts/test/influx/certs/server_combined.pem | 69 ++++++++++++++ scripts/test/influx/docker-compose.yaml | 77 +++++++++++++++ scripts/test/influx/emqx.conf | 94 +++++++++++++++++++ scripts/test/influx/influx-bridge.conf | 46 +++++++++ scripts/test/influx/start.sh | 34 +++++++ 6 files changed, 348 insertions(+) create mode 100644 scripts/test/influx/certs/server.key create mode 100644 scripts/test/influx/certs/server_combined.pem create mode 100644 scripts/test/influx/docker-compose.yaml create mode 100644 scripts/test/influx/emqx.conf create mode 100644 scripts/test/influx/influx-bridge.conf create mode 100755 scripts/test/influx/start.sh diff --git a/scripts/test/influx/certs/server.key b/scripts/test/influx/certs/server.key new file mode 100644 index 000000000..e99276f95 --- /dev/null +++ b/scripts/test/influx/certs/server.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDstiij2PRYssNx +P5k+0YlWLgx57xOz5zpzjyjavDvFUmboEJH2VG2A7ITQtXlTqrqB3rxbGEDYjltm +RG/tjm4g5GQ3RgfupZdsNommgzE4/bH0PT6PwYTlt83596Vu9hFqUF5a0rtYF8RJ +gXHXzDeraz25JoLcuNgGlSrpgioryx6NZB/oBkbuHbGL7fmYuRO8zQSxknHtg/Jr +K4YqQaw66VFQoydyzcGp9b8KjMYjyBVjNvt7xbvvCLrMZ6XMAhPYyquRhQC00ydJ +OHavzcauARAQH20noHpKU8/W8HSwBOKMsYdh0vxLnFwnrP9qOjMoB1G6DtpL8yGc +dsWSkXJTAgMBAAECggEAI4uxy/OcXbrFFpB9AViTIwSEjcWU/oL5Mrk2DVgO3FwP +ZUciZAyG6mvvVdf/1syLIqLBQ3yqNYV1vC8ID8g/nrjX7mTvHqscc1I37BqZ8vub +FBjOTZAQgN56aSWttgfwKDgsCGofWo8sbqKoPHDyOyTo0+YwkyyUDrOqX1OojtgG +Vv+0jKZfc6zzGWoG7HXAAMHYlnLDcO/XqFGiqWG0jVQtKaHOYndYzbWQHg/h4kOr +NcpOCryAUOWWZWjO212qINAYmN/v+ietoemrNKInYzkF6na0TCjOyhzurMdhO1VF +H0D/q0QKZp0BurWMBgoVMEu/L+5NKnVAdS417DojoQKBgQDydlesHeoAlTEKfSOf +4Un/n2ylU7Xu6V17RV+oTUESFlxl+2IeQJ1FuTtnW7LWaT/+jCkqNoxpcj+Aq1Ov +tsecy894hEy1cKOTD8g0BHqGMQ96El/vy4cphBGrmJ1BHd8t+B7u2kuDJO+Jap/r +R0FMtQvkcsZX/JlVnKl2j/OVdwKBgQD57Z4riMKCyEh99y0e5dK2cDjm+NhEErxs +C3AV5ye1rApf5HR4qYaEwdfqMk1mU1eQKQG3nLMEYNDx+ofMNxnJ8xWFtlLOCeRA +bAaD/O9lqvVNV6HDamT85xxrgi6glJRVmlglr1qDutvqKEbt5Cy81hVrGg09prmw +0DdKXlpxBQKBgQC4xicaK9Hgj9HHxIYZmX7jJ4OMmrbTU1TCVKQJhpZISnIv5dto +YqV4NSloIyGu9JaoTfgxC1EGKuLqE6a7CfglLcz2cxxiSWze7fAMdD1Ldqfsx4QF +In3mzZcJxkxRC4rBZY1tST6yursxZehpTOweoPpnJnCxLPdNnw4mJJ3lEQKBgF+2 +Wh5970+6d9iVslfk1786VvBNWDUpn7MY5kHdFl4Clq6GhPw4n+VzfsaXoQCQ5jW1 +Ho08m2oP0bT5psvURjIKv7FHU4rISvtI423kjaG3yN1ns/Fsa2RW9ZFOd6DThcMK +qrsz3kwYYWPZLOCpw2BF9Enh3jRAB1jpRGNcovIpAoGAQJJSNcZDr1JY3NppBZ9U +7OqcQypQ8PP7jBpszQrWid8BGGK082UjO+okG7f3wQjmRYaPpuS/Rqa50ikmLscZ +aO6Y4+rUGmZH6G7r/hk9Pu8AO3JDRH/gNImZy6X1Ae8NDoeHRYzXtz57aGlOY+gU +rcgGpevWU5XzAtEudxOMRsk= +-----END PRIVATE KEY----- diff --git a/scripts/test/influx/certs/server_combined.pem b/scripts/test/influx/certs/server_combined.pem new file mode 100644 index 000000000..8d0efa591 --- /dev/null +++ b/scripts/test/influx/certs/server_combined.pem @@ -0,0 +1,69 @@ +-----BEGIN CERTIFICATE----- +MIIDvTCCAqWgAwIBAgIFFnRIM4UwDQYJKoZIhvcNAQELBQAwbTELMAkGA1UEBhMC +U0UxEjAQBgNVBAgMCVN0b2NraG9sbTESMBAGA1UECgwJTXlPcmdOYW1lMRkwFwYD +VQQLDBBNeUludGVybWVkaWF0ZUNBMRswGQYDVQQDDBJNeUludGVybWVkaWF0ZUNB +LTEwHhcNMjMwMTIzMTQxNjI1WhcNMzMwMTIwMTQxNjI1WjBrMQswCQYDVQQGEwJT +RTESMBAGA1UECAwJU3RvY2tob2xtMRIwEAYDVQQKDAlNeU9yZ05hbWUxEjAQBgNV +BAsMCU15U2VydmljZTEgMB4GA1UEAwwXaW5mbHV4ZGIuZW1xeC5pbmZsdXguaW8w +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDstiij2PRYssNxP5k+0YlW +Lgx57xOz5zpzjyjavDvFUmboEJH2VG2A7ITQtXlTqrqB3rxbGEDYjltmRG/tjm4g +5GQ3RgfupZdsNommgzE4/bH0PT6PwYTlt83596Vu9hFqUF5a0rtYF8RJgXHXzDer +az25JoLcuNgGlSrpgioryx6NZB/oBkbuHbGL7fmYuRO8zQSxknHtg/JrK4YqQaw6 +6VFQoydyzcGp9b8KjMYjyBVjNvt7xbvvCLrMZ6XMAhPYyquRhQC00ydJOHavzcau +ARAQH20noHpKU8/W8HSwBOKMsYdh0vxLnFwnrP9qOjMoB1G6DtpL8yGcdsWSkXJT +AgMBAAGjZjBkMCIGA1UdEQQbMBmCF2luZmx1eGRiLmVtcXguaW5mbHV4LmlvMB0G +A1UdDgQWBBSz+wgHJ2TZNGasVR+BSMqRpg3pSTAfBgNVHSMEGDAWgBRfr50a+u+U +2sPlCD7ft2gOjNIdOjANBgkqhkiG9w0BAQsFAAOCAQEAAInxCAHkEVbPQhijpl+C +vblGHxS4aPGGAmIU7rS2CsZQaXRLZNC++syMZQzOKtLOIzLT8WSgs026/tLsFQk0 +5VCLbgnQqS2wblFylGribUiT/dSMmiMvGylc6MHJnbEyiNKtlB88paTLuDC81lIZ +KDBEYSQTfaykm3MXjeZGB5sHBcKeBWCkDY9O1YvsP/5a9zyZiC1c+XTTlB5Lnzgo +aL7SWB88J+mF0cn5iwitwTg0ryZtO5fG8qmqaAs+G8u9oVytnxi+WBmjukjnFdzC +SlRFYGY0KZmg1ZGlLCzBS3Bi3q+ck8yfv2rj2WBqklOISzSnp5VaRC6gcUDNE0XY +1g== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEODCCAyCgAwIBAgIFFnRIM4UwDQYJKoZIhvcNAQELBQAwbzELMAkGA1UEBhMC +U0UxEjAQBgNVBAgMCVN0b2NraG9sbTESMBAGA1UEBwwJU3RvY2tob2xtMRIwEAYD +VQQKDAlNeU9yZ05hbWUxETAPBgNVBAsMCE15Um9vdENBMREwDwYDVQQDDAhNeVJv +b3RDQTAeFw0yMzAxMjMxNDE2MjVaFw0zMzAxMjAxNDE2MjVaMG0xCzAJBgNVBAYT +AlNFMRIwEAYDVQQIDAlTdG9ja2hvbG0xEjAQBgNVBAoMCU15T3JnTmFtZTEZMBcG +A1UECwwQTXlJbnRlcm1lZGlhdGVDQTEbMBkGA1UEAwwSTXlJbnRlcm1lZGlhdGVD +QS0xMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApYFU7RYTvI8eH6Y6 +yVgjwWWMWSqU3Q97G8l2zHcgOCrph8xQRM8tp0wsoPpvj6SYvohzPJyIxK6tFiQg +FvPrk4JQdQWNEK7ogRwqbeFX017vVsQIDMAsO5V5cqoRjOghV1QvHkz5+oVQt09w +whwk47IrsKhwiXtQrFuGi3w7WNTkwh1FX3V8ZrqCVr02/f1GmODYxPqvuXVtf42B +iewWOvnHs58vyMPvts/Ngh4HBzQncSudeI9GDsPscAn6I5YmjBQkGsQQb7cygd4p +u3OrMw/Sw2e85N6fBM6+b4ZTUEaz6EC2ymBNiJtkLDzkM3ZaP19GluYy17PBi8ZB +UO+rowIDAQABo4HcMIHZMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/ +MB0GA1UdDgQWBBRfr50a+u+U2sPlCD7ft2gOjNIdOjCBlgYDVR0jBIGOMIGLoXOk +cTBvMQswCQYDVQQGEwJTRTESMBAGA1UECAwJU3RvY2tob2xtMRIwEAYDVQQHDAlT +dG9ja2hvbG0xEjAQBgNVBAoMCU15T3JnTmFtZTERMA8GA1UECwwITXlSb290Q0Ex +ETAPBgNVBAMMCE15Um9vdENBghRfGvHfslLmWduK/DoD5zdiKKaPsDANBgkqhkiG +9w0BAQsFAAOCAQEAKoEj6H7tBDiD/sqPuxABGJI4dT6jqSFskF2An95FlwTHLu4b +kBxfb1r3emaqM6t+dgEjupqa6Mte9/rQmynGn+U9IFn3dOo95FPmf8igQGGcO7Tz +Wl1pTm2S7m5+fQAgC94jmBk8JUXO+fGJe33Mx+os1ZaE26KvbhQzTk8NpYfK+cp2 ++K4HsaVogr+mR139YNbKD00xexGQdxPzdBO4EHEsCI+6W7ELFz/iaj7S6XTe9YW2 +Uj858vOWhL6bg2zWhb/ER2MqbB05gOQhf8Rw+4iL+45AiGFrdyD4jXaqMy7lR4dv +fBlPwX60owxqgtKc4QC8/rSKLteSey01WV1SXA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDZTCCAk0CFF8a8d+yUuZZ24r8OgPnN2Iopo+wMA0GCSqGSIb3DQEBCwUAMG8x +CzAJBgNVBAYTAlNFMRIwEAYDVQQIDAlTdG9ja2hvbG0xEjAQBgNVBAcMCVN0b2Nr +aG9sbTESMBAGA1UECgwJTXlPcmdOYW1lMREwDwYDVQQLDAhNeVJvb3RDQTERMA8G +A1UEAwwITXlSb290Q0EwHhcNMjMwMTIzMTQxNjI0WhcNMzMwMTIwMTQxNjI0WjBv +MQswCQYDVQQGEwJTRTESMBAGA1UECAwJU3RvY2tob2xtMRIwEAYDVQQHDAlTdG9j +a2hvbG0xEjAQBgNVBAoMCU15T3JnTmFtZTERMA8GA1UECwwITXlSb290Q0ExETAP +BgNVBAMMCE15Um9vdENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +4aktTF0LaTO6XYvZCGM7vI1XaXIh0eEvYmnLW0di2JD/CajTAqyGbLkpHCDVJDrE +zIXP4v+u872RtfOHDcXnK8fU0hYv4r6KG4YW8PAQouPDuP992WbpuxircOq4b7Z8 +ED5RIxKqZxFJYLmkrvk+cJyqOOIUdbre1+82Wh7TJzBkNJNVRYVb6PzX3qCoVxgy +3tyZCW7K/2wjs8Rde3eyTJuOFsKRaGk5ycS4OMAPqFc3Y1uw5yIDTkWMp/dtbBIK +jVDg6DcY79Vlz293u7Y03Je4xK1HksxpxcU/sMqgtsUB+HKbUBDhLUNkxRRkCw1N ++5PVkP8rmxGuQbIgYxOMOQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBQ9hrNHYcL +m9Vh5Q94Pg+hPk6Q97wRv9lKsimQgVutLXBjC8+NfHGwJchMRlnKrcRHeYTzYIUi +TXxLrRbAMLnAeyeKlGhjqqw4VDORJFvt0I82ia6jrKNlbwGqQ6v+SKoXu85HuJv+ +Er8Iz9X6l1aZ2hEBFy4W4IuYy+fPBlrN74aUdSLcekgLbLj8n0mjQjWD5wzQ4Gsf +8dwm0rsbh6UoX8/XOMkSXP9SlKsNyzA4ZHTZ9aZoehoe0G5sUbfDumUaONmp4oOj +EFntuk5AsbxqzQTSYP4jrLc7/D2hOCJBn93nfZ+raMyD6mjttfk52aeYwFLuPihL +FoanUYSck91R +-----END CERTIFICATE----- diff --git a/scripts/test/influx/docker-compose.yaml b/scripts/test/influx/docker-compose.yaml new file mode 100644 index 000000000..60b4c3421 --- /dev/null +++ b/scripts/test/influx/docker-compose.yaml @@ -0,0 +1,77 @@ +version: '3.9' + +services: + influxdb_server: + container_name: influxdb_server + image: influxdb:2.5.0 + hostname: influxdb.emqx.influx.io + expose: + - "8086" + - "8083" + ports: + - "8086:8086" + - "8083:8083" + environment: + DOCKER_INFLUXDB_INIT_MODE: setup + DOCKER_INFLUXDB_INIT_USERNAME: root + DOCKER_INFLUXDB_INIT_PASSWORD: emqx@123 + DOCKER_INFLUXDB_INIT_ORG: emqx + DOCKER_INFLUXDB_INIT_BUCKET: mqtt + DOCKER_INFLUXDB_INIT_ADMIN_TOKEN: abcdefg + volumes: + - "${PWD}/certs/server_combined.pem:/etc/influxdb/cert.pem" + - "${PWD}/certs/server.key:/etc/influxdb/key.pem" +# - "../../../.ci/docker-compose-file/certs/server.crt:/etc/influxdb/cert.pem" +# - "../../../.ci/docker-compose-file/certs/server.key:/etc/influxdb/key.pem" + - "../../../.ci/docker-compose-file/influxdb/setup-v1.sh:/docker-entrypoint-initdb.d/setup-v1.sh" + restart: always + command: + - influxd + - --tls-cert=/etc/influxdb/cert.pem + - --tls-key=/etc/influxdb/key.pem + networks: + - emqx_test + + emqx_1: + container_name: emqx_1 + image: emqx/emqx-enterprise:${EMQX_TAG} + hostname: emqx_1.emqx.influx.io + ports: + - "1883:1883" + - "18083:18083" + environment: + EMQX_LOG__CONSOLE_HANDLER__LEVEL: warning + EMQX_NODE_NAME: "emqx@emqx_1.emqx.influx.io" + volumes: + - "${PWD}/emqx.conf:/opt/emqx/etc/emqx.conf" + - "${PWD}/influx-bridge.conf:/opt/emqx/etc/influx-bridge.conf" + networks: + - emqx_test + + emqx_2: + container_name: emqx_2 + image: emqx/emqx-enterprise:${EMQX_TAG} + hostname: emqx_1.emqx.influx.io + environment: + EMQX_LOG__CONSOLE_HANDLER__LEVEL: warning + EMQX_NODE_NAME: "emqx@emqx_2.emqx.influx.io" + ports: + - "1884:1883" + - "18084:18083" + volumes: + - "${PWD}/emqx.conf:/opt/emqx/etc/emqx.conf" + - "${PWD}/influx-bridge.conf:/opt/emqx/etc/influx-bridge.conf" + networks: + - emqx_test + +networks: + emqx_test: + driver: bridge + name: emqx.influx.io + ipam: + driver: default + config: + - subnet: 172.100.240.0/24 + gateway: 172.100.240.1 + - subnet: 2001:3200:3110::/64 + gateway: 2001:3200:3210::1 diff --git a/scripts/test/influx/emqx.conf b/scripts/test/influx/emqx.conf new file mode 100644 index 000000000..8fd114f50 --- /dev/null +++ b/scripts/test/influx/emqx.conf @@ -0,0 +1,94 @@ +## NOTE: +## Configs in this file might be overridden by: +## 1. Environment variables which start with 'EMQX_' prefix +## 2. File $EMQX_NODE__DATA_DIR/configs/cluster-override.conf +## 3. File $EMQX_NODE__DATA_DIR/configs/local-override.conf +## +## The *-override.conf files are overwritten at runtime when changes +## are made from EMQX dashboard UI, management HTTP API, or CLI. +## All configuration details can be found in emqx.conf.example + +node { + cookie = "influx" + data_dir = "data" +} + +log { + file_handlers.default { + level = warning + file = "log/emqx.log" + } +} + +cluster { + name = emqxcl + discovery_strategy = manual +} + + +listeners.tcp.default { + bind = "0.0.0.0:1883" + max_connections = 1024000 +} + +listeners.ssl.default { + bind = "0.0.0.0:8883" + max_connections = 512000 + ssl_options { + keyfile = "etc/certs/key.pem" + certfile = "etc/certs/cert.pem" + cacertfile = "etc/certs/cacert.pem" + } +} + +listeners.ws.default { + bind = "0.0.0.0:8083" + max_connections = 1024000 + websocket.mqtt_path = "/mqtt" +} + +listeners.wss.default { + bind = "0.0.0.0:8084" + max_connections = 512000 + websocket.mqtt_path = "/mqtt" + ssl_options { + keyfile = "etc/certs/key.pem" + certfile = "etc/certs/cert.pem" + cacertfile = "etc/certs/cacert.pem" + } +} + +# listeners.quic.default { +# enabled = true +# bind = "0.0.0.0:14567" +# max_connections = 1024000 +# keyfile = "etc/certs/key.pem" +# certfile = "etc/certs/cert.pem" +#} + +dashboard { + listeners.http { + bind = 18083 + } + default_username = "admin" + default_password = "public" +} + +authorization { + deny_action = ignore + no_match = allow + cache = { enable = true } + sources = [ + { + type = file + enable = true + # This file is immutable to EMQX. + # Once new rules are created from dashboard UI or HTTP API, + # the file 'data/authz/acl.conf' is used instead of this one + path = "etc/acl.conf" + } + ] +} + +include emqx-enterprise.conf +include influx-bridge.conf \ No newline at end of file diff --git a/scripts/test/influx/influx-bridge.conf b/scripts/test/influx/influx-bridge.conf new file mode 100644 index 000000000..b513b962f --- /dev/null +++ b/scripts/test/influx/influx-bridge.conf @@ -0,0 +1,46 @@ +bridges { + "influxdb_api_v2" { + test { + bucket = "mqtt" + enable = true + org = "emqx" + precision = "ms" + resource_opts { + async_inflight_window = 100 + auto_restart_interval = "60s" + batch_size = 100 + batch_time = "10ms" + health_check_interval = "15s" + max_queue_bytes = "1GB" + query_mode = "sync" + request_timeout = "15s" + start_after_created = "true" + start_timeout = "5s" + worker_pool_size = 4 + } + server = "influxdb.emqx.influx.io:8086" + ssl { + ciphers = [] + depth = 10 + enable = false + reuse_sessions = true + secure_renegotiate = true + user_lookup_fun = "emqx_tls_psk:lookup" + verify = "verify_peer" + versions = ["tlsv1.3", "tlsv1.2", "tlsv1.1", "tlsv1"] + } + token = "abcdefg" + write_syntax = "mqtt,clientid=${clientid} value=${payload.value}" + } + } +} +log { + console_handler {enable = true, level = "warning"} + file_handlers { + default { + enable = false + file = "log/emqx.log" + level = "warning" + } + } +} \ No newline at end of file diff --git a/scripts/test/influx/start.sh b/scripts/test/influx/start.sh new file mode 100755 index 000000000..7ce9c399c --- /dev/null +++ b/scripts/test/influx/start.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash + +NET='emqx.influx.io' +NODE1="emqx@emqx_1.$NET" +NODE2="emqx@emqx_2.$NET" +export EMQX_TAG='5.0.0-rc.1-g92797d72' + +cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")" + +docker rm -f emqx_1 emqx_2 influxdb_server +docker-compose up -d + +wait_limit=60 +wait_for_emqx() { + container="$1" + wait_limit="$2" + wait_sec=0 + while ! docker exec "$container" emqx_ctl status >/dev/null 2>&1; do + wait_sec=$(( wait_sec + 1 )) + if [ $wait_sec -gt "$wait_limit" ]; then + echo "timeout wait for EMQX" + exit 1 + fi + echo -n '.' + sleep 1 + done +} + +wait_for_emqx emqx_1 30 +wait_for_emqx emqx_2 30 + +echo + +docker exec emqx_1 emqx_ctl cluster join "$NODE2" From 9fc586f670f75d2ddf3aea38d865256e168ad79d Mon Sep 17 00:00:00 2001 From: Erik Timan Date: Mon, 23 Jan 2023 16:44:59 +0100 Subject: [PATCH 0298/1802] test: add influx docker test certs --- scripts/test/influx/certs/ca.pem | 21 ++++++++++++ scripts/test/influx/certs/client.key | 28 ++++++++++++++++ scripts/test/influx/certs/client.pem | 46 ++++++++++++++++++++++++++ scripts/test/influx/certs/server.pem | 48 ++++++++++++++++++++++++++++ 4 files changed, 143 insertions(+) create mode 100644 scripts/test/influx/certs/ca.pem create mode 100644 scripts/test/influx/certs/client.key create mode 100644 scripts/test/influx/certs/client.pem create mode 100644 scripts/test/influx/certs/server.pem diff --git a/scripts/test/influx/certs/ca.pem b/scripts/test/influx/certs/ca.pem new file mode 100644 index 000000000..0044e44ea --- /dev/null +++ b/scripts/test/influx/certs/ca.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDZTCCAk0CFF8a8d+yUuZZ24r8OgPnN2Iopo+wMA0GCSqGSIb3DQEBCwUAMG8x +CzAJBgNVBAYTAlNFMRIwEAYDVQQIDAlTdG9ja2hvbG0xEjAQBgNVBAcMCVN0b2Nr +aG9sbTESMBAGA1UECgwJTXlPcmdOYW1lMREwDwYDVQQLDAhNeVJvb3RDQTERMA8G +A1UEAwwITXlSb290Q0EwHhcNMjMwMTIzMTQxNjI0WhcNMzMwMTIwMTQxNjI0WjBv +MQswCQYDVQQGEwJTRTESMBAGA1UECAwJU3RvY2tob2xtMRIwEAYDVQQHDAlTdG9j +a2hvbG0xEjAQBgNVBAoMCU15T3JnTmFtZTERMA8GA1UECwwITXlSb290Q0ExETAP +BgNVBAMMCE15Um9vdENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +4aktTF0LaTO6XYvZCGM7vI1XaXIh0eEvYmnLW0di2JD/CajTAqyGbLkpHCDVJDrE +zIXP4v+u872RtfOHDcXnK8fU0hYv4r6KG4YW8PAQouPDuP992WbpuxircOq4b7Z8 +ED5RIxKqZxFJYLmkrvk+cJyqOOIUdbre1+82Wh7TJzBkNJNVRYVb6PzX3qCoVxgy +3tyZCW7K/2wjs8Rde3eyTJuOFsKRaGk5ycS4OMAPqFc3Y1uw5yIDTkWMp/dtbBIK +jVDg6DcY79Vlz293u7Y03Je4xK1HksxpxcU/sMqgtsUB+HKbUBDhLUNkxRRkCw1N ++5PVkP8rmxGuQbIgYxOMOQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBQ9hrNHYcL +m9Vh5Q94Pg+hPk6Q97wRv9lKsimQgVutLXBjC8+NfHGwJchMRlnKrcRHeYTzYIUi +TXxLrRbAMLnAeyeKlGhjqqw4VDORJFvt0I82ia6jrKNlbwGqQ6v+SKoXu85HuJv+ +Er8Iz9X6l1aZ2hEBFy4W4IuYy+fPBlrN74aUdSLcekgLbLj8n0mjQjWD5wzQ4Gsf +8dwm0rsbh6UoX8/XOMkSXP9SlKsNyzA4ZHTZ9aZoehoe0G5sUbfDumUaONmp4oOj +EFntuk5AsbxqzQTSYP4jrLc7/D2hOCJBn93nfZ+raMyD6mjttfk52aeYwFLuPihL +FoanUYSck91R +-----END CERTIFICATE----- diff --git a/scripts/test/influx/certs/client.key b/scripts/test/influx/certs/client.key new file mode 100644 index 000000000..931b36442 --- /dev/null +++ b/scripts/test/influx/certs/client.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDK1dJ41zpZ6Sha +RuzL6CqqCXO0R6n5FlQVLIaY+0JCEDJbFqlZ9OtIj96Gh5vlHuxvf2s3fnKBdp0t +pov5ekwavEmEeT0L3h80E21HwGraVHILf2WuBKzpwJlO3Ilwv3s0qfADv7caKPtP +3iQ8EpLMZ2oJrl4jdJ/9Ss3jhV5yTqmxJgY2g1HAOQtpXbbzynoBC3tu8abVd/0k +euimPwg0sr3maphxfbKjUQSi7cn/NzP+BurlTxfy+c3ajeqdFYoG4M1TBpqGn1xb +7gmq37TFxXFvfjmW5Jx9yNtx78drpggOJHcRYXWzF78gHEEf+VbMMBBiQ6B59sOY +x6vn2nM7AgMBAAECggEAO7Iid54gdkXAspEdMAXsSI7vlqdgxwZjSLgKBQwtu/O4 +XkaVIPyMIpb1DvLqVsx7GXWOwI5edOvL5puVpp7vtw6jNHHaHZwJ4vG0v+FJkWg5 +CFk+O7D0kvc0J5mQebZhNGavVOsfoHtfO2pNj8CHo9DrzF2dJyIz1AAoCEZAPbOS +EIiOD7935iZS3YrEdwdVMSq9RfJ95IN58r6Gqqk4tnTazpq9Qe/+8sJJdBqfYRQ2 +R3DxV86uIxbVncPK5SuCXiC4GFYFSoQO2g25pw6AUH7meZUNekXcQtH+i+OU2+lv +Y8W/JIEg1HdxmHDMZd8t25B8BtmWEJCVdv3JbfyVwQKBgQDLhEiZAX3dT1E7xLgM +ecQRdbSIFXoPIsfmOJQY2VOnwSqcqB1lqEo9rmyLLJLsx5AEa5/npm5lbVPQp4Yc +8ng1WUdmZm0A5uKp8mM6XvDOdnIJXv9+onz7aJWs6Xk4d2r3QI/B36q+CoOjmgTO +m/zGB4MWrzYSBMPwlJWLlw6m1wKBgQD/JIxS3tX/P/ro+AekuBcFyk4oiVuClNVF +4G2JEZ6QV76wilvn+R/ocaM5Wdg3a9iRa96PmwqqbcxPiLL08LbtIYG/W7B11Y1t +vSa9LrtN9HI2XaQePfF4aCZQ23e3alNWSCaPeG0QWcoSmzCMOpGXvV8LcLjdo7kr +C5XS3DyePQKBgQDKyw3sKOhaVmcDv4oGJG+TVk6GK6Bs5KN8bHRmLmiFiJ4FbB+e +5IrV27CdHdVpy5FeVkAIfHcU6lPSXpvszE+sGUJkbzu+4lKKNdVanLAniy8m11qP +vSn8+sHqgtsfwR79MYAOF+nzZFa39wlDN3wxd9rQB9dBLS5x1B0P833OEQKBgBUg +vqoCH/ByKOHv4Boo0Gc/WHeM4FIQ5xRRQGDQX7QOQBE4c+HixFyoRF70bK4yNQ3d +w+FacmiojPmtFTypTkyWsLra6eLoGz0SI8oYu5P6ARc1a+jBMmTJIxshAWnj2SDU ++rR4wXdQKCagSUXjd7j8ConU/MoFy8ifvflnGXH1AoGAE8LwsVp/+sn0o1cZl8+Z +aQJDXHyf61F0GGzf7rHV4L/dQhZDxTLNHRMCSQDrPBEoge0OjHGDkJqo8e4ugHNm +150I/FW/AGd3jZgZ8FPApjbCbD9CoxsV+pFpIqE1AJ9TkrdAxTXGIJa0a/hoA0et +S8eNqGQ9RrWQyOJdKhRHjl4= +-----END PRIVATE KEY----- diff --git a/scripts/test/influx/certs/client.pem b/scripts/test/influx/certs/client.pem new file mode 100644 index 000000000..6c633deb4 --- /dev/null +++ b/scripts/test/influx/certs/client.pem @@ -0,0 +1,46 @@ +-----BEGIN CERTIFICATE----- +MIIDazCCAlMCFHQbLq+eVR/mJLDEkxXP6CdXs+9oMA0GCSqGSIb3DQEBCwUAMG0x +CzAJBgNVBAYTAlNFMRIwEAYDVQQIDAlTdG9ja2hvbG0xEjAQBgNVBAoMCU15T3Jn +TmFtZTEZMBcGA1UECwwQTXlJbnRlcm1lZGlhdGVDQTEbMBkGA1UEAwwSTXlJbnRl +cm1lZGlhdGVDQS0yMB4XDTIzMDEyMzE0MTYyNVoXDTMzMDEyMDE0MTYyNVowdzEL +MAkGA1UEBhMCU0UxEjAQBgNVBAgMCVN0b2NraG9sbTESMBAGA1UEBwwJU3RvY2to +b2xtMRIwEAYDVQQKDAlNeU9yZ05hbWUxGDAWBgNVBAsMD015U2VydmljZUNsaWVu +dDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEAytXSeNc6WekoWkbsy+gqqglztEep+RZUFSyGmPtCQhAyWxapWfTrSI/e +hoeb5R7sb39rN35ygXadLaaL+XpMGrxJhHk9C94fNBNtR8Bq2lRyC39lrgSs6cCZ +TtyJcL97NKnwA7+3Gij7T94kPBKSzGdqCa5eI3Sf/UrN44Veck6psSYGNoNRwDkL +aV2288p6AQt7bvGm1Xf9JHropj8INLK95mqYcX2yo1EEou3J/zcz/gbq5U8X8vnN +2o3qnRWKBuDNUwaahp9cW+4Jqt+0xcVxb345luScfcjbce/Ha6YIDiR3EWF1sxe/ +IBxBH/lWzDAQYkOgefbDmMer59pzOwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQB/ +Y2uaU1KQOQILzLog0i5cUh76j3TegFaQhqQTNH1GDnPmkPaphWSb3Pm8jEODBT/+ +UpoGG7gfHEii6u+BkDiQLDOU5yS9pG98xbyTQL8E/o4l+I3CiM/YYRjGE20J8lP6 +BHgx/8GPAzV2hfIOqfJ1rb+BZ1LVvUh2brLVvyPZYyjpTiJcK0kwFL4KDOq0A3r1 +/rSs5wMOAhQIWy8vWw7TaDOgBkTJMqJUhpAfpGCRlVADg+i7fMe84ZsQBY4lpoM3 +OI2ulf03LqsyNRz/hgF2EgCWu3Qo/jRqHmWvhNxy9KQZ46raFc01wMz8j3nucxQS +0QULe+f7knniBqwddXFt +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEODCCAyCgAwIBAgIFFnRIM4UwDQYJKoZIhvcNAQELBQAwbzELMAkGA1UEBhMC +U0UxEjAQBgNVBAgMCVN0b2NraG9sbTESMBAGA1UEBwwJU3RvY2tob2xtMRIwEAYD +VQQKDAlNeU9yZ05hbWUxETAPBgNVBAsMCE15Um9vdENBMREwDwYDVQQDDAhNeVJv +b3RDQTAeFw0yMzAxMjMxNDE2MjVaFw0zMzAxMjAxNDE2MjVaMG0xCzAJBgNVBAYT +AlNFMRIwEAYDVQQIDAlTdG9ja2hvbG0xEjAQBgNVBAoMCU15T3JnTmFtZTEZMBcG +A1UECwwQTXlJbnRlcm1lZGlhdGVDQTEbMBkGA1UEAwwSTXlJbnRlcm1lZGlhdGVD +QS0yMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvjw+/i2D1FOz5tbr +FZds+l2PN7NCfq2hQkoI+7z3iibJwqpTG9FoJl5j+zsJPlAx7oPg3XFXiVyJ0ayg +FxJsfLn033uHvmQ4gsUxH7TuY8tdpIOa3klHAydU7ePHyGg/HLQCCxQw+JjYR/AX +Zje/yqnuvpXB5OHrtf9G16itVJRpovm4QuoO5n//9W/ImEpxnAjiIEAd168kVzIG +c3CKdvB/jRO63GX3o+nIYGtnJpJiB7MmzMLAnGT/UBoAR4//1vrbLQOjPg4PH/FO +YeyUshidVG8DZkwTTiswDvEYLTiGLPgpwAj9dXaY8X45OPicDarM26EKhX/QFCrv +5Vk7MwIDAQABo4HcMIHZMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/ +MB0GA1UdDgQWBBQe8N1/KwZug32rsZgUD1ZENKDfPjCBlgYDVR0jBIGOMIGLoXOk +cTBvMQswCQYDVQQGEwJTRTESMBAGA1UECAwJU3RvY2tob2xtMRIwEAYDVQQHDAlT +dG9ja2hvbG0xEjAQBgNVBAoMCU15T3JnTmFtZTERMA8GA1UECwwITXlSb290Q0Ex +ETAPBgNVBAMMCE15Um9vdENBghRfGvHfslLmWduK/DoD5zdiKKaPsDANBgkqhkiG +9w0BAQsFAAOCAQEA09xjxwue/Sx4nPVp01FwNF0PEq04u8wc108TvTExjsPxaeKR +hAdbUf4UlrD/dwJVExI3jNhis9KP9OG7PCxKMkvOjmsxAK8tsY8MnPtzcUfRjxqN +i3UmF1hZjQ3MDm89Gqmg58muBiQJt4apFSQSD0hQjGjEzzTAmjN41vgbTRVdpqLM +Uf3b7cb26xtloEu2FALs5MLOJ1DTbRsjzJ8fmjGlEWw2h2HapnqRTqQSOUx2Nema +Ms/rDPsIt0Yavlp9VBKg213zRZRMqlFEg0GI5XoPouloUZQoxEgW13+iUD63e6ag +EfuWhS+zhXYrFrGDzg+ckx1/rWYLL3QrfK6+9Q== +-----END CERTIFICATE----- diff --git a/scripts/test/influx/certs/server.pem b/scripts/test/influx/certs/server.pem new file mode 100644 index 000000000..4c9201188 --- /dev/null +++ b/scripts/test/influx/certs/server.pem @@ -0,0 +1,48 @@ +-----BEGIN CERTIFICATE----- +MIIDvTCCAqWgAwIBAgIFFnRIM4UwDQYJKoZIhvcNAQELBQAwbTELMAkGA1UEBhMC +U0UxEjAQBgNVBAgMCVN0b2NraG9sbTESMBAGA1UECgwJTXlPcmdOYW1lMRkwFwYD +VQQLDBBNeUludGVybWVkaWF0ZUNBMRswGQYDVQQDDBJNeUludGVybWVkaWF0ZUNB +LTEwHhcNMjMwMTIzMTQxNjI1WhcNMzMwMTIwMTQxNjI1WjBrMQswCQYDVQQGEwJT +RTESMBAGA1UECAwJU3RvY2tob2xtMRIwEAYDVQQKDAlNeU9yZ05hbWUxEjAQBgNV +BAsMCU15U2VydmljZTEgMB4GA1UEAwwXaW5mbHV4ZGIuZW1xeC5pbmZsdXguaW8w +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDstiij2PRYssNxP5k+0YlW +Lgx57xOz5zpzjyjavDvFUmboEJH2VG2A7ITQtXlTqrqB3rxbGEDYjltmRG/tjm4g +5GQ3RgfupZdsNommgzE4/bH0PT6PwYTlt83596Vu9hFqUF5a0rtYF8RJgXHXzDer +az25JoLcuNgGlSrpgioryx6NZB/oBkbuHbGL7fmYuRO8zQSxknHtg/JrK4YqQaw6 +6VFQoydyzcGp9b8KjMYjyBVjNvt7xbvvCLrMZ6XMAhPYyquRhQC00ydJOHavzcau +ARAQH20noHpKU8/W8HSwBOKMsYdh0vxLnFwnrP9qOjMoB1G6DtpL8yGcdsWSkXJT +AgMBAAGjZjBkMCIGA1UdEQQbMBmCF2luZmx1eGRiLmVtcXguaW5mbHV4LmlvMB0G +A1UdDgQWBBSz+wgHJ2TZNGasVR+BSMqRpg3pSTAfBgNVHSMEGDAWgBRfr50a+u+U +2sPlCD7ft2gOjNIdOjANBgkqhkiG9w0BAQsFAAOCAQEAAInxCAHkEVbPQhijpl+C +vblGHxS4aPGGAmIU7rS2CsZQaXRLZNC++syMZQzOKtLOIzLT8WSgs026/tLsFQk0 +5VCLbgnQqS2wblFylGribUiT/dSMmiMvGylc6MHJnbEyiNKtlB88paTLuDC81lIZ +KDBEYSQTfaykm3MXjeZGB5sHBcKeBWCkDY9O1YvsP/5a9zyZiC1c+XTTlB5Lnzgo +aL7SWB88J+mF0cn5iwitwTg0ryZtO5fG8qmqaAs+G8u9oVytnxi+WBmjukjnFdzC +SlRFYGY0KZmg1ZGlLCzBS3Bi3q+ck8yfv2rj2WBqklOISzSnp5VaRC6gcUDNE0XY +1g== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEODCCAyCgAwIBAgIFFnRIM4UwDQYJKoZIhvcNAQELBQAwbzELMAkGA1UEBhMC +U0UxEjAQBgNVBAgMCVN0b2NraG9sbTESMBAGA1UEBwwJU3RvY2tob2xtMRIwEAYD +VQQKDAlNeU9yZ05hbWUxETAPBgNVBAsMCE15Um9vdENBMREwDwYDVQQDDAhNeVJv +b3RDQTAeFw0yMzAxMjMxNDE2MjVaFw0zMzAxMjAxNDE2MjVaMG0xCzAJBgNVBAYT +AlNFMRIwEAYDVQQIDAlTdG9ja2hvbG0xEjAQBgNVBAoMCU15T3JnTmFtZTEZMBcG +A1UECwwQTXlJbnRlcm1lZGlhdGVDQTEbMBkGA1UEAwwSTXlJbnRlcm1lZGlhdGVD +QS0xMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApYFU7RYTvI8eH6Y6 +yVgjwWWMWSqU3Q97G8l2zHcgOCrph8xQRM8tp0wsoPpvj6SYvohzPJyIxK6tFiQg +FvPrk4JQdQWNEK7ogRwqbeFX017vVsQIDMAsO5V5cqoRjOghV1QvHkz5+oVQt09w +whwk47IrsKhwiXtQrFuGi3w7WNTkwh1FX3V8ZrqCVr02/f1GmODYxPqvuXVtf42B +iewWOvnHs58vyMPvts/Ngh4HBzQncSudeI9GDsPscAn6I5YmjBQkGsQQb7cygd4p +u3OrMw/Sw2e85N6fBM6+b4ZTUEaz6EC2ymBNiJtkLDzkM3ZaP19GluYy17PBi8ZB +UO+rowIDAQABo4HcMIHZMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/ +MB0GA1UdDgQWBBRfr50a+u+U2sPlCD7ft2gOjNIdOjCBlgYDVR0jBIGOMIGLoXOk +cTBvMQswCQYDVQQGEwJTRTESMBAGA1UECAwJU3RvY2tob2xtMRIwEAYDVQQHDAlT +dG9ja2hvbG0xEjAQBgNVBAoMCU15T3JnTmFtZTERMA8GA1UECwwITXlSb290Q0Ex +ETAPBgNVBAMMCE15Um9vdENBghRfGvHfslLmWduK/DoD5zdiKKaPsDANBgkqhkiG +9w0BAQsFAAOCAQEAKoEj6H7tBDiD/sqPuxABGJI4dT6jqSFskF2An95FlwTHLu4b +kBxfb1r3emaqM6t+dgEjupqa6Mte9/rQmynGn+U9IFn3dOo95FPmf8igQGGcO7Tz +Wl1pTm2S7m5+fQAgC94jmBk8JUXO+fGJe33Mx+os1ZaE26KvbhQzTk8NpYfK+cp2 ++K4HsaVogr+mR139YNbKD00xexGQdxPzdBO4EHEsCI+6W7ELFz/iaj7S6XTe9YW2 +Uj858vOWhL6bg2zWhb/ER2MqbB05gOQhf8Rw+4iL+45AiGFrdyD4jXaqMy7lR4dv +fBlPwX60owxqgtKc4QC8/rSKLteSey01WV1SXA== +-----END CERTIFICATE----- From 28718edbfd7f7bad68bcdc6bc3e8ae25071be862 Mon Sep 17 00:00:00 2001 From: Erik Timan Date: Tue, 24 Jan 2023 14:12:34 +0100 Subject: [PATCH 0299/1802] chore: bump application VSNs --- apps/emqx_bridge/src/emqx_bridge.app.src | 2 +- apps/emqx_connector/src/emqx_connector.app.src | 2 +- apps/emqx_resource/src/emqx_resource.app.src | 2 +- lib-ee/emqx_ee_bridge/src/emqx_ee_bridge.app.src | 2 +- lib-ee/emqx_ee_connector/src/emqx_ee_connector.app.src | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/emqx_bridge/src/emqx_bridge.app.src b/apps/emqx_bridge/src/emqx_bridge.app.src index 39cb1b18b..ca0001319 100644 --- a/apps/emqx_bridge/src/emqx_bridge.app.src +++ b/apps/emqx_bridge/src/emqx_bridge.app.src @@ -1,7 +1,7 @@ %% -*- mode: erlang -*- {application, emqx_bridge, [ {description, "EMQX bridges"}, - {vsn, "0.1.9"}, + {vsn, "0.1.10"}, {registered, []}, {mod, {emqx_bridge_app, []}}, {applications, [ diff --git a/apps/emqx_connector/src/emqx_connector.app.src b/apps/emqx_connector/src/emqx_connector.app.src index 9a82bda27..2e955a5e9 100644 --- a/apps/emqx_connector/src/emqx_connector.app.src +++ b/apps/emqx_connector/src/emqx_connector.app.src @@ -1,7 +1,7 @@ %% -*- mode: erlang -*- {application, emqx_connector, [ {description, "EMQX Data Integration Connectors"}, - {vsn, "0.1.12"}, + {vsn, "0.1.13"}, {registered, []}, {mod, {emqx_connector_app, []}}, {applications, [ diff --git a/apps/emqx_resource/src/emqx_resource.app.src b/apps/emqx_resource/src/emqx_resource.app.src index e3a37fd10..6bd9fd213 100644 --- a/apps/emqx_resource/src/emqx_resource.app.src +++ b/apps/emqx_resource/src/emqx_resource.app.src @@ -1,7 +1,7 @@ %% -*- mode: erlang -*- {application, emqx_resource, [ {description, "Manager for all external resources"}, - {vsn, "0.1.5"}, + {vsn, "0.1.6"}, {registered, []}, {mod, {emqx_resource_app, []}}, {applications, [ diff --git a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge.app.src b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge.app.src index 11831ab06..b2a3c80c6 100644 --- a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge.app.src +++ b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge.app.src @@ -1,6 +1,6 @@ {application, emqx_ee_bridge, [ {description, "EMQX Enterprise data bridges"}, - {vsn, "0.1.3"}, + {vsn, "0.1.4"}, {registered, []}, {applications, [ kernel, diff --git a/lib-ee/emqx_ee_connector/src/emqx_ee_connector.app.src b/lib-ee/emqx_ee_connector/src/emqx_ee_connector.app.src index 15cafa6a4..56d128601 100644 --- a/lib-ee/emqx_ee_connector/src/emqx_ee_connector.app.src +++ b/lib-ee/emqx_ee_connector/src/emqx_ee_connector.app.src @@ -1,6 +1,6 @@ {application, emqx_ee_connector, [ {description, "EMQX Enterprise connectors"}, - {vsn, "0.1.3"}, + {vsn, "0.1.4"}, {registered, []}, {applications, [ kernel, From fbfd4a25651db6c2f1a87e4b020dd906511676f5 Mon Sep 17 00:00:00 2001 From: Erik Timan Date: Tue, 24 Jan 2023 14:13:03 +0100 Subject: [PATCH 0300/1802] test: use EMQX_IMAGE_TAG for influx test script --- scripts/test/influx/docker-compose.yaml | 4 ++-- scripts/test/influx/start.sh | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/test/influx/docker-compose.yaml b/scripts/test/influx/docker-compose.yaml index 60b4c3421..4ff7ec6bf 100644 --- a/scripts/test/influx/docker-compose.yaml +++ b/scripts/test/influx/docker-compose.yaml @@ -34,7 +34,7 @@ services: emqx_1: container_name: emqx_1 - image: emqx/emqx-enterprise:${EMQX_TAG} + image: emqx/emqx-enterprise:${EMQX_IMAGE_TAG} hostname: emqx_1.emqx.influx.io ports: - "1883:1883" @@ -50,7 +50,7 @@ services: emqx_2: container_name: emqx_2 - image: emqx/emqx-enterprise:${EMQX_TAG} + image: emqx/emqx-enterprise:${EMQX_IMAGE_TAG} hostname: emqx_1.emqx.influx.io environment: EMQX_LOG__CONSOLE_HANDLER__LEVEL: warning diff --git a/scripts/test/influx/start.sh b/scripts/test/influx/start.sh index 7ce9c399c..d06a2d44a 100755 --- a/scripts/test/influx/start.sh +++ b/scripts/test/influx/start.sh @@ -3,7 +3,7 @@ NET='emqx.influx.io' NODE1="emqx@emqx_1.$NET" NODE2="emqx@emqx_2.$NET" -export EMQX_TAG='5.0.0-rc.1-g92797d72' +export EMQX_IMAGE_TAG="${EMQX_IMAGE_TAG:-latest}" cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")" From 9d20431257a45af08e0ed4e43df1c4675d00b5b1 Mon Sep 17 00:00:00 2001 From: Erik Timan Date: Tue, 24 Jan 2023 14:13:35 +0100 Subject: [PATCH 0301/1802] fix(emqx_resource): fix crash while flushing queue We used next_event for flushing the queue in emqx_resource, but this leads to a crash. We now call flush_worker/1 instead. --- .../src/emqx_resource_buffer_worker.erl | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/apps/emqx_resource/src/emqx_resource_buffer_worker.erl b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl index 669b8e474..634b0e954 100644 --- a/apps/emqx_resource/src/emqx_resource_buffer_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl @@ -583,10 +583,11 @@ do_flush( ), case queue_count(Q1) > 0 of true -> - {keep_state, Data1, [{next_event, internal, flush}]}; + flush_worker(self()); false -> - {keep_state, Data1} - end + ok + end, + {keep_state, Data1} end; do_flush(Data0, #{ is_batch := true, @@ -659,15 +660,17 @@ do_flush(Data0, #{ } ), CurrentCount = queue_count(Q1), - case {CurrentCount > 0, CurrentCount >= BatchSize} of - {false, _} -> - {keep_state, Data1}; - {true, true} -> - {keep_state, Data1, [{next_event, internal, flush}]}; - {true, false} -> - Data2 = ensure_flush_timer(Data1), - {keep_state, Data2} - end + Data2 = + case {CurrentCount > 0, CurrentCount >= BatchSize} of + {false, _} -> + Data1; + {true, true} -> + flush_worker(self()), + Data1; + {true, false} -> + ensure_flush_timer(Data1) + end, + {keep_state, Data2} end. batch_reply_caller(Id, BatchResult, Batch, QueryOpts) -> From 7603ab490b2e01a0f65612437edeccc7cbdd4f31 Mon Sep 17 00:00:00 2001 From: Erik Timan Date: Mon, 23 Jan 2023 15:02:23 +0100 Subject: [PATCH 0302/1802] fix(emqx_ee_connector): fix passing of influxdb ssl opts --- lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl b/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl index 1ae4d9874..824233a6d 100644 --- a/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl +++ b/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl @@ -324,8 +324,9 @@ ssl_config(#{enable := false}) -> ssl_config(SSL = #{enable := true}) -> [ {https_enabled, true}, - {transport, ssl} - ] ++ maps:to_list(maps:remove(enable, SSL)). + {transport, ssl}, + {transport_opts, maps:to_list(maps:remove(enable, SSL))} + ]. username(#{username := Username}) -> [{username, str(Username)}]; From 21e933e9bb20b92280569b578f348560d79f2c2d Mon Sep 17 00:00:00 2001 From: Erik Timan Date: Tue, 24 Jan 2023 14:37:15 +0100 Subject: [PATCH 0303/1802] test: add rule to influx-bridge.conf and add newlines --- scripts/test/influx/emqx.conf | 2 +- scripts/test/influx/influx-bridge.conf | 19 ++++++++++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/scripts/test/influx/emqx.conf b/scripts/test/influx/emqx.conf index 8fd114f50..cd8571f98 100644 --- a/scripts/test/influx/emqx.conf +++ b/scripts/test/influx/emqx.conf @@ -91,4 +91,4 @@ authorization { } include emqx-enterprise.conf -include influx-bridge.conf \ No newline at end of file +include influx-bridge.conf diff --git a/scripts/test/influx/influx-bridge.conf b/scripts/test/influx/influx-bridge.conf index b513b962f..31ddeaf79 100644 --- a/scripts/test/influx/influx-bridge.conf +++ b/scripts/test/influx/influx-bridge.conf @@ -22,11 +22,11 @@ bridges { ssl { ciphers = [] depth = 10 - enable = false + enable = true reuse_sessions = true secure_renegotiate = true user_lookup_fun = "emqx_tls_psk:lookup" - verify = "verify_peer" + verify = "verify_none" versions = ["tlsv1.3", "tlsv1.2", "tlsv1.1", "tlsv1"] } token = "abcdefg" @@ -43,4 +43,17 @@ log { level = "warning" } } -} \ No newline at end of file +} +rule_engine { + ignore_sys_message = true + jq_function_default_timeout = "10s" + jq_implementation_module = "jq_nif" + rules { + "rule_o75y" { + actions = ["influxdb_api_v2:test"] + description = "" + metadata {created_at = 1674565304418} + sql = "SELECT\n *\nFROM\n \"t/#\"" + } + } +} From 424d6ed34b9d1e76a11dfea68f3ea9e3e7a1298b Mon Sep 17 00:00:00 2001 From: Erik Timan Date: Tue, 24 Jan 2023 14:42:52 +0100 Subject: [PATCH 0304/1802] test: improve start.sh script for influxdb tests --- scripts/test/influx/start.sh | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/scripts/test/influx/start.sh b/scripts/test/influx/start.sh index d06a2d44a..f430d353a 100755 --- a/scripts/test/influx/start.sh +++ b/scripts/test/influx/start.sh @@ -1,13 +1,15 @@ #!/usr/bin/env bash NET='emqx.influx.io' -NODE1="emqx@emqx_1.$NET" -NODE2="emqx@emqx_2.$NET" +NODE2_FQDN="emqx@emqx_2.$NET" +NODE1_CONTAINER_NAME="emqx_1" +NODE2_CONTAINER_NAME="emqx_2" +INFLUXDB_CONTAINER_NAME="influxdb_server" export EMQX_IMAGE_TAG="${EMQX_IMAGE_TAG:-latest}" -cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")" +cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")" || exit -docker rm -f emqx_1 emqx_2 influxdb_server +docker rm -f "$NODE1_CONTAINER_NAME" "$NODE2_CONTAINER_NAME" "$INFLUXDB_CONTAINER_NAME" docker-compose up -d wait_limit=60 @@ -26,9 +28,9 @@ wait_for_emqx() { done } -wait_for_emqx emqx_1 30 -wait_for_emqx emqx_2 30 +wait_for_emqx "$NODE1_CONTAINER_NAME" 30 +wait_for_emqx "$NODE2_CONTAINER_NAME" 30 echo -docker exec emqx_1 emqx_ctl cluster join "$NODE2" +docker exec "$NODE1_CONTAINER_NAME" emqx_ctl cluster join "$NODE2_FQDN" From 2416aeebc76447b7efa661f06daf236592e29cbf Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Mon, 23 Jan 2023 19:55:14 +0100 Subject: [PATCH 0305/1802] build: allow user-specified image tag for make docker build --- build | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/build b/build index 87c719a53..06b1d29e2 100755 --- a/build +++ b/build @@ -332,13 +332,15 @@ make_docker() { if [[ "$PROFILE" = *-elixir ]]; then PKG_VSN="$PKG_VSN-elixir" fi + local default_tag="emqx/${PROFILE%%-elixir}:${PKG_VSN}" + EMQX_IMAGE_TAG="${EMQX_IMAGE_TAG:-$default_tag}" set -x docker build --no-cache --pull \ --build-arg BUILD_FROM="${EMQX_BUILDER}" \ --build-arg RUN_FROM="${EMQX_RUNNER}" \ --build-arg EMQX_NAME="$PROFILE" \ - --tag "emqx/${PROFILE%%-elixir}:${PKG_VSN}" \ + --tag "${EMQX_IMAGE_TAG}" \ -f "${EMQX_DOCKERFILE}" . } From feb02389118fe3d5196e45d9a9e3c48512170f00 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Tue, 24 Jan 2023 12:54:42 +0100 Subject: [PATCH 0306/1802] fix(influxdb): ensure client is stopped before returning error --- lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl b/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl index 824233a6d..0ee27c5c4 100644 --- a/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl +++ b/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl @@ -248,6 +248,8 @@ do_start_client( client => Client, reason => "client is not alive" }), + %% no leak + _ = influxdb:stop_client(Client), {error, influxdb_client_not_alive} end; {error, {already_started, Client0}} -> From 5fdf7fd24c0fc9053b7ad99debbb197c9887b447 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Thu, 19 Jan 2023 12:39:42 +0100 Subject: [PATCH 0307/1802] fix(kafka): use async callback to bump success counters some telemetry events from wolff are discarded: * dropped: this is double counted in wolff, we now only subscribe to the dropped_queue_full event * retried_failed: it has different meanings in wolff, in wolff, it means it's the 2nd (or onward) produce attempt in EMQX, it means it's eventually failed after some retries * retried_success since we are going to handle the success counters in callbac this having this reported from wolff will only make things harder to understand * failed wolff never fails (unelss drop which is a different counter) --- apps/emqx_bridge/src/emqx_bridge.erl | 16 ++- apps/emqx_resource/include/emqx_resource.hrl | 2 + apps/emqx_resource/src/emqx_resource.erl | 5 +- .../src/emqx_resource_buffer_worker.erl | 70 ++++++++----- .../src/kafka/emqx_bridge_impl_kafka.erl | 8 +- .../kafka/emqx_bridge_impl_kafka_producer.erl | 98 ++++++++----------- .../emqx_bridge_impl_kafka_producer_SUITE.erl | 84 +++++++++++----- scripts/ct/run.sh | 2 +- 8 files changed, 172 insertions(+), 113 deletions(-) diff --git a/apps/emqx_bridge/src/emqx_bridge.erl b/apps/emqx_bridge/src/emqx_bridge.erl index 5b3fe796b..fb199522d 100644 --- a/apps/emqx_bridge/src/emqx_bridge.erl +++ b/apps/emqx_bridge/src/emqx_bridge.erl @@ -171,14 +171,22 @@ send_message(BridgeId, Message) -> not_found -> {error, {bridge_not_found, BridgeId}}; #{enable := true} = Config -> - Timeout = emqx_map_lib:deep_get( - [resource_opts, request_timeout], Config, timer:seconds(15) - ), - emqx_resource:query(ResId, {send_message, Message}, #{timeout => Timeout}); + QueryOpts = query_opts(Config), + emqx_resource:query(ResId, {send_message, Message}, QueryOpts); #{enable := false} -> {error, {bridge_stopped, BridgeId}} end. +query_opts(Config) -> + case emqx_map_lib:deep_get([resource_opts, request_timeout], Config, false) of + Timeout when is_integer(Timeout) -> + %% request_timeout is configured + #{timeout => Timeout}; + _ -> + %% emqx_resource has a default value (15s) + #{} + end. + config_key_path() -> [bridges]. diff --git a/apps/emqx_resource/include/emqx_resource.hrl b/apps/emqx_resource/include/emqx_resource.hrl index 03be8fae8..7464eb4f8 100644 --- a/apps/emqx_resource/include/emqx_resource.hrl +++ b/apps/emqx_resource/include/emqx_resource.hrl @@ -89,6 +89,8 @@ -define(DEFAULT_QUEUE_SIZE, 100 * 1024 * 1024). -define(DEFAULT_QUEUE_SIZE_RAW, <<"100MB">>). +-define(DEFAULT_REQUEST_TIMEOUT, timer:seconds(15)). + %% count -define(DEFAULT_BATCH_SIZE, 1). diff --git a/apps/emqx_resource/src/emqx_resource.erl b/apps/emqx_resource/src/emqx_resource.erl index bb27b6acd..ad7f30b47 100644 --- a/apps/emqx_resource/src/emqx_resource.erl +++ b/apps/emqx_resource/src/emqx_resource.erl @@ -255,7 +255,7 @@ reset_metrics(ResId) -> query(ResId, Request) -> query(ResId, Request, #{}). --spec query(resource_id(), Request :: term(), emqx_resource_buffer_worker:query_opts()) -> +-spec query(resource_id(), Request :: term(), query_opts()) -> Result :: term(). query(ResId, Request, Opts) -> case emqx_resource_manager:ets_lookup(ResId) of @@ -263,7 +263,8 @@ query(ResId, Request, Opts) -> IsBufferSupported = is_buffer_supported(Module), case {IsBufferSupported, QM} of {true, _} -> - emqx_resource_buffer_worker:simple_sync_query(ResId, Request); + %% only Kafka so far + emqx_resource_buffer_worker:simple_async_query(ResId, Request); {false, sync} -> emqx_resource_buffer_worker:sync_query(ResId, Request, Opts); {false, async} -> diff --git a/apps/emqx_resource/src/emqx_resource_buffer_worker.erl b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl index 634b0e954..11d3753f0 100644 --- a/apps/emqx_resource/src/emqx_resource_buffer_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl @@ -38,7 +38,8 @@ ]). -export([ - simple_sync_query/2 + simple_sync_query/2, + simple_async_query/2 ]). -export([ @@ -61,6 +62,7 @@ -define(COLLECT_REQ_LIMIT, 1000). -define(SEND_REQ(FROM, REQUEST), {'$send_req', FROM, REQUEST}). -define(QUERY(FROM, REQUEST, SENT, EXPIRE_AT), {query, FROM, REQUEST, SENT, EXPIRE_AT}). +-define(SIMPLE_QUERY(REQUEST), ?QUERY(undefined, REQUEST, false, infinity)). -define(REPLY(FROM, REQUEST, SENT, RESULT), {reply, FROM, REQUEST, SENT, RESULT}). -define(EXPAND(RESULT, BATCH), [ ?REPLY(FROM, REQUEST, SENT, RESULT) @@ -116,8 +118,8 @@ async_query(Id, Request, Opts0) -> emqx_resource_metrics:matched_inc(Id), pick_cast(Id, PickKey, {query, Request, Opts}). -%% simple query the resource without batching and queuing messages. --spec simple_sync_query(id(), request()) -> Result :: term(). +%% simple query the resource without batching and queuing. +-spec simple_sync_query(id(), request()) -> term(). simple_sync_query(Id, Request) -> %% Note: since calling this function implies in bypassing the %% buffer workers, and each buffer worker index is used when @@ -126,18 +128,27 @@ simple_sync_query(Id, Request) -> %% would mess up the metrics anyway. `undefined' is ignored by %% `emqx_resource_metrics:*_shift/3'. Index = undefined, - QueryOpts0 = #{simple_query => true, timeout => infinity}, - QueryOpts = #{expire_at := ExpireAt} = ensure_expire_at(QueryOpts0), + QueryOpts = simple_query_opts(), emqx_resource_metrics:matched_inc(Id), Ref = make_message_ref(), - HasBeenSent = false, - From = self(), - Result = call_query( - sync, Id, Index, Ref, ?QUERY(From, Request, HasBeenSent, ExpireAt), QueryOpts - ), - _ = handle_query_result(Id, Result, HasBeenSent), + Result = call_query(sync, Id, Index, Ref, ?SIMPLE_QUERY(Request), QueryOpts), + _ = handle_query_result(Id, Result, _HasBeenSent = false), Result. +%% simple async-query the resource without batching and queuing. +-spec simple_async_query(id(), request()) -> term(). +simple_async_query(Id, Request) -> + Index = undefined, + QueryOpts = simple_query_opts(), + emqx_resource_metrics:matched_inc(Id), + Ref = make_message_ref(), + Result = call_query(async, Id, Index, Ref, ?SIMPLE_QUERY(Request), QueryOpts), + _ = handle_query_result(Id, Result, _HasBeenSent = false), + Result. + +simple_query_opts() -> + ensure_expire_at(#{simple_query => true, timeout => infinity}). + -spec block(pid()) -> ok. block(ServerRef) -> gen_statem:cast(ServerRef, block). @@ -848,9 +859,9 @@ call_query(QM0, Id, Index, Ref, Query, QueryOpts) -> case emqx_resource_manager:ets_lookup(Id) of {ok, _Group, #{mod := Mod, state := ResSt, status := connected} = Data} -> QM = - case QM0 of - configured -> maps:get(query_mode, Data); - _ -> QM0 + case QM0 =:= configured of + true -> maps:get(query_mode, Data); + false -> QM0 end, CBM = maps:get(callback_mode, Data), CallMode = call_mode(QM, CBM), @@ -991,11 +1002,7 @@ do_reply_after_query( ref => Ref, result => Result }), - IsFullBefore = is_inflight_full(InflightTID), - IsAcked = ack_inflight(InflightTID, Ref, Id, Index), - IsAcked andalso PostFn(), - IsFullBefore andalso ?MODULE:flush_worker(Pid), - ok + do_ack(InflightTID, Ref, Id, Index, PostFn, Pid, QueryOpts) end. batch_reply_after_query(Pid, Id, Index, InflightTID, Ref, Batch, QueryOpts, Result) -> @@ -1049,13 +1056,23 @@ do_batch_reply_after_query(Pid, Id, Index, InflightTID, Ref, Batch, QueryOpts, R ref => Ref, result => Result }), - IsFullBefore = is_inflight_full(InflightTID), - IsAcked = ack_inflight(InflightTID, Ref, Id, Index), - IsAcked andalso PostFn(), - IsFullBefore andalso ?MODULE:flush_worker(Pid), - ok + do_ack(InflightTID, Ref, Id, Index, PostFn, Pid, QueryOpts) end. +do_ack(InflightTID, Ref, Id, Index, PostFn, WorkerPid, QueryOpts) -> + IsFullBefore = is_inflight_full(InflightTID), + IsKnownRef = ack_inflight(InflightTID, Ref, Id, Index), + case maps:get(simple_query, QueryOpts, false) of + true -> + PostFn(); + false when IsKnownRef -> + PostFn(); + false -> + ok + end, + IsFullBefore andalso ?MODULE:flush_worker(WorkerPid), + ok. + %%============================================================================== %% operations for queue queue_item_marshaller(Bin) when is_binary(Bin) -> @@ -1113,7 +1130,7 @@ inflight_new(InfltWinSZ, Id, Index) -> inflight_append(TableId, {?SIZE_REF, 0}, Id, Index), inflight_append(TableId, {?INITIAL_TIME_REF, erlang:system_time()}, Id, Index), inflight_append( - TableId, {?INITIAL_MONOTONIC_TIME_REF, erlang:monotonic_time(nanosecond)}, Id, Index + TableId, {?INITIAL_MONOTONIC_TIME_REF, make_message_ref()}, Id, Index ), TableId. @@ -1426,8 +1443,7 @@ now_() -> ensure_timeout_query_opts(#{timeout := _} = Opts, _SyncOrAsync) -> Opts; ensure_timeout_query_opts(#{} = Opts0, sync) -> - TimeoutMS = timer:seconds(15), - Opts0#{timeout => TimeoutMS}; + Opts0#{timeout => ?DEFAULT_REQUEST_TIMEOUT}; ensure_timeout_query_opts(#{} = Opts0, async) -> Opts0#{timeout => infinity}. diff --git a/lib-ee/emqx_ee_bridge/src/kafka/emqx_bridge_impl_kafka.erl b/lib-ee/emqx_ee_bridge/src/kafka/emqx_bridge_impl_kafka.erl index 747cd187d..49ca9fb86 100644 --- a/lib-ee/emqx_ee_bridge/src/kafka/emqx_bridge_impl_kafka.erl +++ b/lib-ee/emqx_ee_bridge/src/kafka/emqx_bridge_impl_kafka.erl @@ -12,6 +12,7 @@ on_start/2, on_stop/2, on_query/3, + on_query_async/4, on_get_status/2, is_buffer_supported/0 ]). @@ -26,8 +27,11 @@ on_start(InstId, Config) -> on_stop(InstId, State) -> emqx_bridge_impl_kafka_producer:on_stop(InstId, State). -on_query(InstId, Msg, State) -> - emqx_bridge_impl_kafka_producer:on_query(InstId, Msg, State). +on_query(InstId, Req, State) -> + emqx_bridge_impl_kafka_producer:on_query(InstId, Req, State). + +on_query_async(InstId, Req, ReplyFn, State) -> + emqx_bridge_impl_kafka_producer:on_query_async(InstId, Req, ReplyFn, State). on_get_status(InstId, State) -> emqx_bridge_impl_kafka_producer:on_get_status(InstId, State). diff --git a/lib-ee/emqx_ee_bridge/src/kafka/emqx_bridge_impl_kafka_producer.erl b/lib-ee/emqx_ee_bridge/src/kafka/emqx_bridge_impl_kafka_producer.erl index 25741b6cd..18e27b775 100644 --- a/lib-ee/emqx_ee_bridge/src/kafka/emqx_bridge_impl_kafka_producer.erl +++ b/lib-ee/emqx_ee_bridge/src/kafka/emqx_bridge_impl_kafka_producer.erl @@ -11,6 +11,7 @@ on_start/2, on_stop/2, on_query/3, + on_query_async/4, on_get_status/2 ]). @@ -140,19 +141,48 @@ on_stop(_InstanceID, #{client_id := ClientID, producers := Producers, resource_i } ). +on_query( + _InstId, + {send_message, Message}, + #{message_template := Template, producers := Producers} +) -> + KafkaMessage = render_message(Template, Message), + %% TODO: this function is not used so far, + %% timeout should be configurable + %% or the on_query/3 should be on_query/4 instead. + try + {_Partition, _Offset} = wolff:send_sync(Producers, [KafkaMessage], 5000), + ok + catch + error:{producer_down, _} = Reason -> + {error, Reason}; + error:timeout -> + {error, timeout} + end. + %% @doc The callback API for rule-engine (or bridge without rules) %% The input argument `Message' is an enriched format (as a map()) %% of the original #message{} record. %% The enrichment is done by rule-engine or by the data bridge framework. %% E.g. the output of rule-engine process chain %% or the direct mapping from an MQTT message. -on_query(_InstId, {send_message, Message}, #{message_template := Template, producers := Producers}) -> +on_query_async( + _InstId, + {send_message, Message}, + AsyncReplyFn, + #{message_template := Template, producers := Producers} +) -> KafkaMessage = render_message(Template, Message), + %% * Must be a batch because wolff:send and wolff:send_sync are batch APIs + %% * Must be a single element batch because wolff books calls, but not batch sizes + %% for counters and gauges. + Batch = [KafkaMessage], %% The retuned information is discarded here. %% If the producer process is down when sending, this function would %% raise an error exception which is to be caught by the caller of this callback - {_Partition, _Pid} = wolff:send(Producers, [KafkaMessage], {fun ?MODULE:on_kafka_ack/3, [#{}]}), - {async_return, ok}. + {_Partition, Pid} = wolff:send(Producers, Batch, {fun ?MODULE:on_kafka_ack/3, [AsyncReplyFn]}), + %% this Pid is so far never used because Kafka producer is by-passing the buffer worker + {ok, Pid}. compile_message_template(T) -> KeyTemplate = maps:get(key, T, <<"${.clientid}">>), @@ -194,9 +224,14 @@ render_timestamp(Template, Message) -> erlang:system_time(millisecond) end. -on_kafka_ack(_Partition, _Offset, _Extra) -> - %% Do nothing so far. - %% Maybe need to bump some counters? +%% Wolff producer never gives up retrying +%% so there can only be 'ok' results. +on_kafka_ack(_Partition, Offset, {ReplyFn, Args}) when is_integer(Offset) -> + %% the ReplyFn is emqx_resource_worker:reply_after_query/8 + apply(ReplyFn, Args ++ [ok]); +on_kafka_ack(_Partition, buffer_overflow_discarded, _Callback) -> + %% wolff should bump the dropped_queue_full counter + %% do not apply the callback (which is basically to bump success or fail counter) ok. on_get_status(_InstId, _State) -> @@ -345,27 +380,13 @@ get_required(Field, Config, Throw) -> %% we *must* match the bridge id in the event metadata with that in %% the handler config; otherwise, multiple kafka producer bridges will %% install multiple handlers to the same wolff events, multiplying the -handle_telemetry_event( - [wolff, dropped], - #{counter_inc := Val}, - #{bridge_id := ID}, - #{bridge_id := ID} -) when is_integer(Val) -> - emqx_resource_metrics:dropped_inc(ID, Val); handle_telemetry_event( [wolff, dropped_queue_full], #{counter_inc := Val}, #{bridge_id := ID}, #{bridge_id := ID} ) when is_integer(Val) -> - %% When wolff emits a `dropped_queue_full' event due to replayq - %% overflow, it also emits a `dropped' event (at the time of - %% writing, wolff is 1.7.4). Since we already bump `dropped' when - %% `dropped.queue_full' occurs, we have to correct it here. This - %% correction will have to be dropped if wolff stops also emitting - %% `dropped'. - emqx_resource_metrics:dropped_queue_full_inc(ID, Val), - emqx_resource_metrics:dropped_inc(ID, -Val); + emqx_resource_metrics:dropped_queue_full_inc(ID, Val); handle_telemetry_event( [wolff, queuing], #{gauge_set := Val}, @@ -380,13 +401,6 @@ handle_telemetry_event( #{bridge_id := ID} ) when is_integer(Val) -> emqx_resource_metrics:retried_inc(ID, Val); -handle_telemetry_event( - [wolff, failed], - #{counter_inc := Val}, - #{bridge_id := ID}, - #{bridge_id := ID} -) when is_integer(Val) -> - emqx_resource_metrics:failed_inc(ID, Val); handle_telemetry_event( [wolff, inflight], #{gauge_set := Val}, @@ -394,27 +408,6 @@ handle_telemetry_event( #{bridge_id := ID} ) when is_integer(Val) -> emqx_resource_metrics:inflight_set(ID, PartitionID, Val); -handle_telemetry_event( - [wolff, retried_failed], - #{counter_inc := Val}, - #{bridge_id := ID}, - #{bridge_id := ID} -) when is_integer(Val) -> - emqx_resource_metrics:retried_failed_inc(ID, Val); -handle_telemetry_event( - [wolff, retried_success], - #{counter_inc := Val}, - #{bridge_id := ID}, - #{bridge_id := ID} -) when is_integer(Val) -> - emqx_resource_metrics:retried_success_inc(ID, Val); -handle_telemetry_event( - [wolff, success], - #{counter_inc := Val}, - #{bridge_id := ID}, - #{bridge_id := ID} -) when is_integer(Val) -> - emqx_resource_metrics:success_inc(ID, Val); handle_telemetry_event(_EventId, _Metrics, _MetaData, _HandlerConfig) -> %% Event that we do not handle ok. @@ -437,15 +430,10 @@ maybe_install_wolff_telemetry_handlers(ResourceID) -> %% unique handler id telemetry_handler_id(ResourceID), [ - [wolff, dropped], [wolff, dropped_queue_full], [wolff, queuing], [wolff, retried], - [wolff, failed], - [wolff, inflight], - [wolff, retried_failed], - [wolff, retried_success], - [wolff, success] + [wolff, inflight] ], fun ?MODULE:handle_telemetry_event/4, %% we *must* keep track of the same id that is handed down to diff --git a/lib-ee/emqx_ee_bridge/test/emqx_bridge_impl_kafka_producer_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_bridge_impl_kafka_producer_SUITE.erl index 14567dd39..10a6d8d9a 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_bridge_impl_kafka_producer_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_bridge_impl_kafka_producer_SUITE.erl @@ -46,7 +46,14 @@ %%------------------------------------------------------------------------------ all() -> - emqx_common_test_helpers:all(?MODULE). + [ + {group, on_query}, + {group, on_query_async} + ]. + +groups() -> + All = emqx_common_test_helpers:all(?MODULE), + [{on_query, All}, {on_query_async, All}]. wait_until_kafka_is_up() -> wait_until_kafka_is_up(0). @@ -89,6 +96,12 @@ end_per_suite(_Config) -> _ = application:stop(emqx_connector), ok. +init_per_group(GroupName, Config) -> + [{query_api, GroupName} | Config]. + +end_per_group(_, _) -> + ok. + set_special_configs(emqx_management) -> Listeners = #{http => #{port => 8081}}, Config = #{ @@ -106,23 +119,23 @@ set_special_configs(_) -> %% Test cases for all combinations of SSL, no SSL and authentication types %%------------------------------------------------------------------------------ -t_publish_no_auth(_CtConfig) -> - publish_with_and_without_ssl("none"). +t_publish_no_auth(CtConfig) -> + publish_with_and_without_ssl(CtConfig, "none"). -t_publish_no_auth_key_dispatch(_CtConfig) -> - publish_with_and_without_ssl("none", #{"partition_strategy" => "key_dispatch"}). +t_publish_no_auth_key_dispatch(CtConfig) -> + publish_with_and_without_ssl(CtConfig, "none", #{"partition_strategy" => "key_dispatch"}). -t_publish_sasl_plain(_CtConfig) -> - publish_with_and_without_ssl(valid_sasl_plain_settings()). +t_publish_sasl_plain(CtConfig) -> + publish_with_and_without_ssl(CtConfig, valid_sasl_plain_settings()). -t_publish_sasl_scram256(_CtConfig) -> - publish_with_and_without_ssl(valid_sasl_scram256_settings()). +t_publish_sasl_scram256(CtConfig) -> + publish_with_and_without_ssl(CtConfig, valid_sasl_scram256_settings()). -t_publish_sasl_scram512(_CtConfig) -> - publish_with_and_without_ssl(valid_sasl_scram512_settings()). +t_publish_sasl_scram512(CtConfig) -> + publish_with_and_without_ssl(CtConfig, valid_sasl_scram512_settings()). -t_publish_sasl_kerberos(_CtConfig) -> - publish_with_and_without_ssl(valid_sasl_kerberos_settings()). +t_publish_sasl_kerberos(CtConfig) -> + publish_with_and_without_ssl(CtConfig, valid_sasl_kerberos_settings()). %%------------------------------------------------------------------------------ %% Test cases for REST api @@ -350,7 +363,7 @@ kafka_bridge_rest_api_helper(Config) -> %% exists and it will. This is specially bad if the %% original crash was due to misconfiguration and we are %% trying to fix it... -t_failed_creation_then_fix(_Config) -> +t_failed_creation_then_fix(Config) -> HostsString = kafka_hosts_string_sasl(), ValidAuthSettings = valid_sasl_plain_settings(), WrongAuthSettings = ValidAuthSettings#{"password" := "wrong"}, @@ -394,7 +407,7 @@ t_failed_creation_then_fix(_Config) -> }, {ok, Offset} = resolve_kafka_offset(kafka_hosts(), KafkaTopic, 0), ct:pal("base offset before testing ~p", [Offset]), - ?assertEqual({async_return, ok}, ?PRODUCER:on_query(ResourceId, {send_message, Msg}, State)), + ok = send(Config, ResourceId, Msg, State), {ok, {_, [KafkaMsg]}} = brod:fetch(kafka_hosts(), KafkaTopic, 0, Offset), ?assertMatch(#kafka_message{key = BinTime}, KafkaMsg), %% TODO: refactor those into init/end per testcase @@ -406,11 +419,37 @@ t_failed_creation_then_fix(_Config) -> %% Helper functions %%------------------------------------------------------------------------------ -publish_with_and_without_ssl(AuthSettings) -> - publish_with_and_without_ssl(AuthSettings, #{}). +send(Config, ResourceId, Msg, State) when is_list(Config) -> + Ref = make_ref(), + ok = do_send(Ref, Config, ResourceId, Msg, State), + receive + {ack, Ref} -> + ok + after 10000 -> + error(timeout) + end. -publish_with_and_without_ssl(AuthSettings, Config) -> +do_send(Ref, Config, ResourceId, Msg, State) when is_list(Config) -> + Caller = self(), + F = fun(ok) -> + Caller ! {ack, Ref}, + ok + end, + case proplists:get_value(query_api, Config) of + on_query -> + ok = ?PRODUCER:on_query(ResourceId, {send_message, Msg}, State), + F(ok); + on_query_async -> + {ok, _} = ?PRODUCER:on_query_async(ResourceId, {send_message, Msg}, {F, []}, State), + ok + end. + +publish_with_and_without_ssl(CtConfig, AuthSettings) -> + publish_with_and_without_ssl(CtConfig, AuthSettings, #{}). + +publish_with_and_without_ssl(CtConfig, AuthSettings, Config) -> publish_helper( + CtConfig, #{ auth_settings => AuthSettings, ssl_settings => #{} @@ -418,6 +457,7 @@ publish_with_and_without_ssl(AuthSettings, Config) -> Config ), publish_helper( + CtConfig, #{ auth_settings => AuthSettings, ssl_settings => valid_ssl_settings() @@ -426,10 +466,11 @@ publish_with_and_without_ssl(AuthSettings, Config) -> ), ok. -publish_helper(AuthSettings) -> - publish_helper(AuthSettings, #{}). +publish_helper(CtConfig, AuthSettings) -> + publish_helper(CtConfig, AuthSettings, #{}). publish_helper( + CtConfig, #{ auth_settings := AuthSettings, ssl_settings := SSLSettings @@ -477,8 +518,7 @@ publish_helper( ct:pal("base offset before testing ~p", [Offset]), StartRes = ?PRODUCER:on_start(InstId, Conf), {ok, State} = StartRes, - OnQueryRes = ?PRODUCER:on_query(InstId, {send_message, Msg}, State), - {async_return, ok} = OnQueryRes, + ok = send(CtConfig, InstId, Msg, State), {ok, {_, [KafkaMsg]}} = brod:fetch(kafka_hosts(), KafkaTopic, 0, Offset), ?assertMatch(#kafka_message{key = BinTime}, KafkaMsg), ok = ?PRODUCER:on_stop(InstId, State), diff --git a/scripts/ct/run.sh b/scripts/ct/run.sh index 07d45efe1..69427c7c3 100755 --- a/scripts/ct/run.sh +++ b/scripts/ct/run.sh @@ -195,7 +195,7 @@ fi echo "Fixing file owners and permissions for $UID_GID" # rebar and hex cache directory need to be writable by $UID -docker exec -i $TTY -u root:root "$ERLANG_CONTAINER" bash -c "mkdir -p /.cache && chown $UID_GID /.cache && chown -R $UID_GID /emqx" +docker exec -i $TTY -u root:root "$ERLANG_CONTAINER" bash -c "mkdir -p /.cache && chown $UID_GID /.cache && chown -R $UID_GID /emqx/.git /emqx/.ci /emqx/_build/default/lib" # need to initialize .erlang.cookie manually here because / is not writable by $UID docker exec -i $TTY -u root:root "$ERLANG_CONTAINER" bash -c "openssl rand -base64 16 > /.erlang.cookie && chown $UID_GID /.erlang.cookie && chmod 0400 /.erlang.cookie" From 903a77b47188ce2e8c3db22982aace6c1d2233d0 Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Wed, 25 Jan 2023 15:33:05 +0300 Subject: [PATCH 0308/1802] test(redis): ensure batch query hit different cluster shards This will inevitably fail: it's not generally possible to update different keys through the same cluster connection, one or more update will fail with `MOVED` status. This testcase should serve as a regression test later. --- .../test/emqx_ee_bridge_redis_SUITE.erl | 43 ++++++++++++------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_redis_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_redis_SUITE.erl index 2b67787b2..8435204fd 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_redis_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_redis_SUITE.erl @@ -16,6 +16,9 @@ %% CT boilerplate %%------------------------------------------------------------------------------ +-define(KEYSHARDS, 3). +-define(KEYPREFIX, "MSGS"). + -define(REDIS_TOXYPROXY_CONNECT_CONFIG, #{ <<"server">> => <<"toxiproxy:6379">>, <<"redis_type">> => <<"single">> @@ -23,7 +26,7 @@ -define(COMMON_REDIS_OPTS, #{ <<"password">> => <<"public">>, - <<"command_template">> => [<<"RPUSH">>, <<"MSGS">>, <<"${payload}">>], + <<"command_template">> => [<<"RPUSH">>, <>, <<"${payload}">>], <<"local_topic">> => <<"local_topic/#">> }). @@ -47,7 +50,7 @@ ) ). -all() -> [{group, transport_types}, {group, rest}]. +all() -> [{group, transports}, {group, rest}]. groups() -> ResourceSpecificTCs = [t_create_delete_bridge], @@ -63,7 +66,7 @@ groups() -> ], [ {rest, TCs}, - {transport_types, [ + {transports, [ {group, tcp}, {group, tls} ]}, @@ -79,7 +82,7 @@ groups() -> init_per_group(Group, Config) when Group =:= redis_single; Group =:= redis_sentinel; Group =:= redis_cluster -> - [{transport_type, Group} | Config]; + [{connector_type, Group} | Config]; init_per_group(Group, Config) when Group =:= tcp; Group =:= tls -> @@ -139,7 +142,7 @@ end_per_suite(_Config) -> init_per_testcase(_Testcase, Config) -> ok = delete_all_rules(), ok = delete_all_bridges(), - case ?config(transport_type, Config) of + case ?config(connector_type, Config) of undefined -> Config; RedisType -> @@ -162,7 +165,7 @@ end_per_testcase(_Testcase, Config) -> t_create_delete_bridge(Config) -> Name = <<"mybridge">>, - Type = ?config(transport_type, Config), + Type = ?config(connector_type, Config), BridgeConfig = ?config(bridge_config, Config), IsBatch = ?config(is_batch, Config), ?assertMatch( @@ -350,9 +353,7 @@ check_resource_queries(ResourceId, BaseTopic, IsBatch) -> ?wait_async_action( lists:foreach( fun(I) -> - IBin = integer_to_binary(I), - Topic = <>, - _ = publish_message(Topic, RandomPayload) + _ = publish_message(format_topic(BaseTopic, I), RandomPayload) end, lists:seq(1, N) ), @@ -360,7 +361,7 @@ check_resource_queries(ResourceId, BaseTopic, IsBatch) -> 5000 ), fun(Trace) -> - AddedMsgCount = length(added_msgs(ResourceId, RandomPayload)), + AddedMsgCount = length(added_msgs(ResourceId, BaseTopic, RandomPayload)), case IsBatch of true -> ?assertMatch( @@ -378,11 +379,23 @@ check_resource_queries(ResourceId, BaseTopic, IsBatch) -> end ). -added_msgs(ResourceId, Payload) -> - {ok, Results} = emqx_resource:simple_sync_query( - ResourceId, {cmd, [<<"LRANGE">>, <<"MSGS">>, <<"0">>, <<"-1">>]} - ), - [El || El <- Results, El =:= Payload]. +added_msgs(ResourceId, BaseTopic, Payload) -> + lists:flatmap( + fun(K) -> + {ok, Results} = emqx_resource:simple_sync_query( + ResourceId, + {cmd, [<<"LRANGE">>, K, <<"0">>, <<"-1">>]} + ), + [El || El <- Results, El =:= Payload] + end, + [format_redis_key(BaseTopic, S) || S <- lists:seq(0, ?KEYSHARDS - 1)] + ). + +format_topic(Base, I) -> + iolist_to_binary(io_lib:format("~s/~2..0B", [Base, I rem ?KEYSHARDS])). + +format_redis_key(Base, I) -> + iolist_to_binary([?KEYPREFIX, "/", format_topic(Base, I)]). conf_schema(StructName) -> #{ From 88364945429a7835427c16d38c1d330dd4a16d45 Mon Sep 17 00:00:00 2001 From: Erik Timan Date: Wed, 25 Jan 2023 11:50:12 +0100 Subject: [PATCH 0309/1802] fix: redact influxdb tokens in a few logs --- apps/emqx_bridge/src/emqx_bridge.erl | 2 +- apps/emqx_bridge/src/emqx_bridge_resource.erl | 6 +++--- apps/emqx_conf/src/emqx_cluster_rpc.erl | 8 ++++---- apps/emqx_conf/src/emqx_conf.app.src | 2 +- .../src/emqx_ee_connector_influxdb.erl | 19 +++++++++++++------ 5 files changed, 22 insertions(+), 15 deletions(-) diff --git a/apps/emqx_bridge/src/emqx_bridge.erl b/apps/emqx_bridge/src/emqx_bridge.erl index 5b3fe796b..f6677bd09 100644 --- a/apps/emqx_bridge/src/emqx_bridge.erl +++ b/apps/emqx_bridge/src/emqx_bridge.erl @@ -255,7 +255,7 @@ create(BridgeType, BridgeName, RawConf) -> brige_action => create, bridge_type => BridgeType, bridge_name => BridgeName, - bridge_raw_config => RawConf + bridge_raw_config => emqx_misc:redact(RawConf) }), emqx_conf:update( emqx_bridge:config_key_path() ++ [BridgeType, BridgeName], diff --git a/apps/emqx_bridge/src/emqx_bridge_resource.erl b/apps/emqx_bridge/src/emqx_bridge_resource.erl index cbff85df3..d228f2281 100644 --- a/apps/emqx_bridge/src/emqx_bridge_resource.erl +++ b/apps/emqx_bridge/src/emqx_bridge_resource.erl @@ -137,7 +137,7 @@ create(Type, Name, Conf, Opts0) -> msg => "create bridge", type => Type, name => Name, - config => Conf + config => emqx_misc:redact(Conf) }), Opts = override_start_after_created(Conf, Opts0), {ok, _Data} = emqx_resource:create_local( @@ -172,7 +172,7 @@ update(Type, Name, {OldConf, Conf}, Opts0) -> msg => "update bridge", type => Type, name => Name, - config => Conf + config => emqx_misc:redact(Conf) }), case recreate(Type, Name, Conf, Opts) of {ok, _} -> @@ -182,7 +182,7 @@ update(Type, Name, {OldConf, Conf}, Opts0) -> msg => "updating_a_non_existing_bridge", type => Type, name => Name, - config => Conf + config => emqx_misc:redact(Conf) }), create(Type, Name, Conf, Opts); {error, Reason} -> diff --git a/apps/emqx_conf/src/emqx_cluster_rpc.erl b/apps/emqx_conf/src/emqx_cluster_rpc.erl index fe701049c..c285e09b8 100644 --- a/apps/emqx_conf/src/emqx_cluster_rpc.erl +++ b/apps/emqx_conf/src/emqx_cluster_rpc.erl @@ -495,15 +495,15 @@ log_and_alarm(IsSuccess, Res, #{kind := ?APPLY_KIND_INITIATE} = Meta) -> %% because nothing is committed case IsSuccess of true -> - ?SLOG(debug, Meta#{msg => "cluster_rpc_apply_result", result => Res}); + ?SLOG(debug, Meta#{msg => "cluster_rpc_apply_result", result => emqx_misc:redact(Res)}); false -> - ?SLOG(warning, Meta#{msg => "cluster_rpc_apply_result", result => Res}) + ?SLOG(warning, Meta#{msg => "cluster_rpc_apply_result", result => emqx_misc:redact(Res)}) end; log_and_alarm(true, Res, Meta) -> - ?SLOG(debug, Meta#{msg => "cluster_rpc_apply_ok", result => Res}), + ?SLOG(debug, Meta#{msg => "cluster_rpc_apply_ok", result => emqx_misc:redact(Res)}), do_alarm(deactivate, Res, Meta); log_and_alarm(false, Res, Meta) -> - ?SLOG(error, Meta#{msg => "cluster_rpc_apply_failed", result => Res}), + ?SLOG(error, Meta#{msg => "cluster_rpc_apply_failed", result => emqx_misc:redact(Res)}), do_alarm(activate, Res, Meta). do_alarm(Fun, Res, #{tnx_id := Id} = Meta) -> diff --git a/apps/emqx_conf/src/emqx_conf.app.src b/apps/emqx_conf/src/emqx_conf.app.src index b13c0d055..f7fd33e3b 100644 --- a/apps/emqx_conf/src/emqx_conf.app.src +++ b/apps/emqx_conf/src/emqx_conf.app.src @@ -1,6 +1,6 @@ {application, emqx_conf, [ {description, "EMQX configuration management"}, - {vsn, "0.1.10"}, + {vsn, "0.1.11"}, {registered, []}, {mod, {emqx_conf_app, []}}, {applications, [kernel, stdlib]}, diff --git a/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl b/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl index 824233a6d..ef1b2edc9 100644 --- a/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl +++ b/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl @@ -200,8 +200,8 @@ start_client(InstId, Config) -> ?SLOG(info, #{ msg => "starting influxdb connector", connector => InstId, - config => Config, - client_config => ClientConfig + config => emqx_misc:redact(Config), + client_config => emqx_misc:redact(ClientConfig) }), try do_start_client(InstId, ClientConfig, Config) @@ -236,8 +236,8 @@ do_start_client( ?SLOG(info, #{ msg => "starting influxdb connector success", connector => InstId, - client => Client, - state => State + client => redact_auth(Client), + state => redact_auth(State) }), {ok, State}; false -> @@ -245,7 +245,7 @@ do_start_client( ?SLOG(error, #{ msg => "starting influxdb connector failed", connector => InstId, - client => Client, + client => redact_auth(Client), reason => "client is not alive" }), {error, influxdb_client_not_alive} @@ -255,7 +255,7 @@ do_start_client( ?SLOG(info, #{ msg => "restarting influxdb connector, found already started client", connector => InstId, - old_client => Client0 + old_client => redact_auth(Client0) }), _ = influxdb:stop_client(Client0), do_start_client(InstId, ClientConfig, Config); @@ -338,6 +338,13 @@ password(#{password := Password}) -> password(_) -> []. +redact_auth(Term) -> + emqx_misc:redact(Term, fun is_auth_key/1). + +is_auth_key(<<"Authorization">>) -> true; +is_auth_key(<<"authorization">>) -> true; +is_auth_key(_) -> false. + %% ------------------------------------------------------------------------------------------------- %% Query do_query(InstId, Client, Points) -> From 805d08e823364f02f1d7ab7c7958994f68f8e661 Mon Sep 17 00:00:00 2001 From: Erik Timan Date: Wed, 25 Jan 2023 11:56:27 +0100 Subject: [PATCH 0310/1802] fix: reduce log level from error to warning in several places This reduces the log level from error to warning in places that are connected to the influxdb bridge. Transient errors for external resources should not render an error log. --- apps/emqx_resource/src/emqx_resource_manager.erl | 4 ++-- lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/emqx_resource/src/emqx_resource_manager.erl b/apps/emqx_resource/src/emqx_resource_manager.erl index 8098dbe42..47e0cf658 100644 --- a/apps/emqx_resource/src/emqx_resource_manager.erl +++ b/apps/emqx_resource/src/emqx_resource_manager.erl @@ -487,7 +487,7 @@ start_resource(Data, From) -> Actions = maybe_reply([{state_timeout, 0, health_check}], From, ok), {next_state, connecting, UpdatedData, Actions}; {error, Reason} = Err -> - ?SLOG(error, #{ + ?SLOG(warning, #{ msg => start_resource_failed, id => Data#data.id, reason => Reason @@ -546,7 +546,7 @@ handle_connected_health_check(Data) -> Actions = [{state_timeout, health_check_interval(Data#data.opts), health_check}], {keep_state, UpdatedData, Actions}; (Status, UpdatedData) -> - ?SLOG(error, #{ + ?SLOG(warning, #{ msg => health_check_failed, id => Data#data.id, status => Status diff --git a/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl b/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl index ef1b2edc9..1370ed2c2 100644 --- a/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl +++ b/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl @@ -208,7 +208,7 @@ start_client(InstId, Config) -> catch E:R:S -> ?tp(influxdb_connector_start_exception, #{error => {E, R}}), - ?SLOG(error, #{ + ?SLOG(warning, #{ msg => "start influxdb connector error", connector => InstId, error => E, @@ -242,7 +242,7 @@ do_start_client( {ok, State}; false -> ?tp(influxdb_connector_start_failed, #{error => influxdb_client_not_alive}), - ?SLOG(error, #{ + ?SLOG(warning, #{ msg => "starting influxdb connector failed", connector => InstId, client => redact_auth(Client), @@ -261,7 +261,7 @@ do_start_client( do_start_client(InstId, ClientConfig, Config); {error, Reason} -> ?tp(influxdb_connector_start_failed, #{error => Reason}), - ?SLOG(error, #{ + ?SLOG(warning, #{ msg => "starting influxdb connector failed", connector => InstId, reason => Reason From 26fcaecad78c52637c06b445d82b2aee63033c88 Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Wed, 25 Jan 2023 15:41:52 +0300 Subject: [PATCH 0311/1802] fix(redis): disable batching in `redis_cluster` bridges Through configuration subsystem. --- .../src/emqx_ee_bridge_redis.erl | 56 +++++++++++++++---- .../test/emqx_ee_bridge_redis_SUITE.erl | 11 ++-- 2 files changed, 49 insertions(+), 18 deletions(-) diff --git a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_redis.erl b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_redis.erl index 3a3963786..6b7239a76 100644 --- a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_redis.erl +++ b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_redis.erl @@ -7,7 +7,7 @@ -include_lib("typerefl/include/types.hrl"). -include_lib("hocon/include/hoconsc.hrl"). --import(hoconsc, [mk/2, enum/1, ref/2]). +-import(hoconsc, [mk/2, enum/1, ref/1, ref/2]). -export([ conn_bridge_examples/1 @@ -80,13 +80,20 @@ values(common, RedisType, SpecificOpts) -> pool_size => 8, password => <<"secret">>, command_template => [<<"LPUSH">>, <<"MSGS">>, <<"${payload}">>], - resource_opts => #{ + resource_opts => values(resource_opts, RedisType, #{}), + ssl => #{enable => false} + }, + maps:merge(Config, SpecificOpts); +values(resource_opts, "cluster", SpecificOpts) -> + SpecificOpts; +values(resource_opts, _RedisType, SpecificOpts) -> + maps:merge( + #{ batch_size => 1, batch_time => <<"20ms">> }, - ssl => #{enable => false} - }, - maps:merge(Config, SpecificOpts). + SpecificOpts + ). %% ------------------------------------------------------------------------------------------------- %% Hocon Schema Definitions @@ -115,29 +122,31 @@ fields("get_cluster") -> fields(Type) when Type == redis_single orelse Type == redis_sentinel orelse Type == redis_cluster -> - redis_bridge_common_fields() ++ - connector_fields(Type). + redis_bridge_common_fields(Type) ++ + connector_fields(Type); +fields("creation_opts_" ++ Type) -> + resource_creation_fields(Type). method_fileds(post, ConnectorType) -> - redis_bridge_common_fields() ++ + redis_bridge_common_fields(ConnectorType) ++ connector_fields(ConnectorType) ++ type_name_fields(ConnectorType); method_fileds(get, ConnectorType) -> - redis_bridge_common_fields() ++ + redis_bridge_common_fields(ConnectorType) ++ connector_fields(ConnectorType) ++ type_name_fields(ConnectorType) ++ emqx_bridge_schema:status_fields(); method_fileds(put, ConnectorType) -> - redis_bridge_common_fields() ++ + redis_bridge_common_fields(ConnectorType) ++ connector_fields(ConnectorType). -redis_bridge_common_fields() -> +redis_bridge_common_fields(Type) -> emqx_bridge_schema:common_bridge_fields() ++ [ {local_topic, mk(binary(), #{desc => ?DESC("local_topic")})}, {command_template, fun command_template/1} ] ++ - emqx_resource_schema:fields("resource_opts"). + resource_fields(Type). connector_fields(Type) -> RedisType = bridge_type_to_redis_conn_type(Type), @@ -156,6 +165,27 @@ type_name_fields(Type) -> {name, mk(binary(), #{required => true, desc => ?DESC("desc_name")})} ]. +resource_fields(Type) -> + [ + {resource_opts, + mk( + ref("creation_opts_" ++ atom_to_list(Type)), + #{ + required => false, + default => #{}, + desc => ?DESC(emqx_resource_schema, <<"resource_opts">>) + } + )} + ]. + +resource_creation_fields("redis_cluster") -> + % TODO + % Cluster bridge is currently incompatible with batching. + Fields = emqx_resource_schema:fields("creation_opts"), + lists:foldl(fun proplists:delete/2, Fields, [batch_size, batch_time]); +resource_creation_fields(_) -> + emqx_resource_schema:fields("creation_opts"). + desc("config") -> ?DESC("desc_config"); desc(Method) when Method =:= "get"; Method =:= "put"; Method =:= "post" -> @@ -166,6 +196,8 @@ desc(redis_sentinel) -> ?DESC(emqx_connector_redis, "sentinel"); desc(redis_cluster) -> ?DESC(emqx_connector_redis, "cluster"); +desc("creation_opts_" ++ _Type) -> + ?DESC(emqx_resource_schema, "creation_opts"); desc(_) -> undefined. diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_redis_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_redis_SUITE.erl index 8435204fd..31c75ede4 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_redis_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_redis_SUITE.erl @@ -142,12 +142,13 @@ end_per_suite(_Config) -> init_per_testcase(_Testcase, Config) -> ok = delete_all_rules(), ok = delete_all_bridges(), - case ?config(connector_type, Config) of - undefined -> + case {?config(connector_type, Config), ?config(batch_mode, Config)} of + {undefined, _} -> Config; - RedisType -> + {redis_cluster, batch_on} -> + {skip, "Batching is not supported by 'redis_cluster' bridge type"}; + {RedisType, BatchMode} -> Transport = ?config(transport, Config), - BatchMode = ?config(batch_mode, Config), #{RedisType := #{Transport := RedisConnConfig}} = redis_connect_configs(), #{BatchMode := ResourceConfig} = resource_configs(), IsBatch = (BatchMode =:= batch_on), @@ -522,7 +523,6 @@ invalid_command_bridge_config() -> Conf1#{ <<"resource_opts">> => #{ <<"query_mode">> => <<"sync">>, - <<"batch_size">> => <<"1">>, <<"worker_pool_size">> => <<"1">>, <<"start_timeout">> => <<"15s">> }, @@ -533,7 +533,6 @@ resource_configs() -> #{ batch_off => #{ <<"query_mode">> => <<"sync">>, - <<"batch_size">> => <<"1">>, <<"start_timeout">> => <<"15s">> }, batch_on => #{ From 2ee00b75a70bf957ad708ae71aeadfb61379fd12 Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Wed, 25 Jan 2023 16:49:55 +0300 Subject: [PATCH 0312/1802] fix(redis): unwrap pipeline queries against redis cluster This is an additional safety measure in addition to the disabled batching on the bridge level. --- apps/emqx_connector/src/emqx_connector_redis.erl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/emqx_connector/src/emqx_connector_redis.erl b/apps/emqx_connector/src/emqx_connector_redis.erl index 4bb46bca3..286f7dea6 100644 --- a/apps/emqx_connector/src/emqx_connector_redis.erl +++ b/apps/emqx_connector/src/emqx_connector_redis.erl @@ -222,6 +222,8 @@ is_unrecoverable_error(Results) when is_list(Results) -> lists:any(fun is_unrecoverable_error/1, Results); is_unrecoverable_error({error, <<"ERR unknown command ", _/binary>>}) -> true; +is_unrecoverable_error({error, invalid_cluster_command}) -> + true; is_unrecoverable_error(_) -> false. @@ -267,7 +269,9 @@ do_cmd(PoolName, cluster, {cmd, Command}) -> do_cmd(Conn, _Type, {cmd, Command}) -> eredis:q(Conn, Command); do_cmd(PoolName, cluster, {cmds, Commands}) -> - wrap_qp_result(eredis_cluster:qp(PoolName, Commands)); + % TODO + % Cluster mode is currently incompatible with batching. + wrap_qp_result([eredis_cluster:q(PoolName, Command) || Command <- Commands]); do_cmd(Conn, _Type, {cmds, Commands}) -> wrap_qp_result(eredis:qp(Conn, Commands)). From 9492b40a374ef317441fbfc99c0733ca7034dbb8 Mon Sep 17 00:00:00 2001 From: Ivan Dyachkov Date: Tue, 24 Jan 2023 13:48:46 +0100 Subject: [PATCH 0313/1802] ci: automatically push packages to packagecloud.io --- .github/workflows/build_packages.yaml | 32 +++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/.github/workflows/build_packages.yaml b/.github/workflows/build_packages.yaml index 2ec7dac3e..a1d8dfee4 100644 --- a/.github/workflows/build_packages.yaml +++ b/.github/workflows/build_packages.yaml @@ -27,6 +27,7 @@ jobs: outputs: BUILD_PROFILE: ${{ steps.get_profile.outputs.BUILD_PROFILE }} IS_EXACT_TAG: ${{ steps.get_profile.outputs.IS_EXACT_TAG }} + VERSION: ${{ steps.get_profile.outputs.VERSION }} steps: - uses: actions/checkout@v3 @@ -75,6 +76,7 @@ jobs: ;; esac echo "BUILD_PROFILE=$PROFILE" >> $GITHUB_OUTPUT + echo "VERSION=$(./pkg-vsn.sh $PROFILE)" >> $GITHUB_OUTPUT - name: get_all_deps run: | make -C source deps-all @@ -349,3 +351,33 @@ jobs: fi aws s3 cp --recursive packages/$PROFILE s3://${{ secrets.AWS_S3_BUCKET }}/$s3dir/${{ github.ref_name }} aws cloudfront create-invalidation --distribution-id ${{ secrets.AWS_CLOUDFRONT_ID }} --paths "/$s3dir/${{ github.ref_name }}/*" + - name: Push to packagecloud.io + env: + PROFILE: ${{ matrix.profile }} + VERSION: ${{ needs.prepare.outputs.VERSION }} + PACKAGECLOUD_TOKEN: ${{ secrets.PACKAGECLOUD_TOKEN }} + run: | + set -eu + REPO=$PROFILE + if [ $PROFILE = 'emqx-enterprise' ]; then + REPO='emqx-enterprise5' + fi + function push() { + docker run -t --rm -e PACKAGECLOUD_TOKEN=$PACKAGECLOUD_TOKEN -v $(pwd)/$2:/w/$2 -w /w ghcr.io/emqx/package_cloud push emqx/$REPO/$1 $2 + } + push "debian/buster" "packages/$PROFILE/$PROFILE-$VERSION-debian10-amd64.deb" + push "debian/buster" "packages/$PROFILE/$PROFILE-$VERSION-debian10-arm64.deb" + push "debian/bullseye" "packages/$PROFILE/$PROFILE-$VERSION-debian11-amd64.deb" + push "debian/bullseye" packages/$PROFILE/$PROFILE-$VERSION-debian11-arm64.deb + push "ubuntu/bionic" "packages/$PROFILE/$PROFILE-$VERSION-ubuntu18.04-amd64.deb" + push "ubuntu/bionic" "packages/$PROFILE/$PROFILE-$VERSION-ubuntu18.04-arm64.deb" + push "ubuntu/focal" "packages/$PROFILE/$PROFILE-$VERSION-ubuntu20.04-amd64.deb" + push "ubuntu/focal" "packages/$PROFILE/$PROFILE-$VERSION-ubuntu20.04-arm64.deb" + push "ubuntu/jammy" "packages/$PROFILE/$PROFILE-$VERSION-ubuntu22.04-amd64.deb" + push "ubuntu/jammy" "packages/$PROFILE/$PROFILE-$VERSION-ubuntu22.04-arm64.deb" + push "el/7" "packages/$PROFILE/$PROFILE-$VERSION-el7-amd64.rpm" + push "el/7" "packages/$PROFILE/$PROFILE-$VERSION-el7-arm64.rpm" + push "el/8" "packages/$PROFILE/$PROFILE-$VERSION-el8-amd64.rpm" + push "el/8" "packages/$PROFILE/$PROFILE-$VERSION-el8-arm64.rpm" + push "el/6" "packages/$PROFILE/$PROFILE-$VERSION-amzn2-amd64.rpm" + push "el/6" "packages/$PROFILE/$PROFILE-$VERSION-amzn2-arm64.rpm" From afab2d5e85853ec602349c28a646f1d4a227bdac Mon Sep 17 00:00:00 2001 From: Ivan Dyachkov Date: Tue, 24 Jan 2023 13:49:09 +0100 Subject: [PATCH 0314/1802] ci: delete homebrew step --- .github/workflows/release.yaml | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 3c157cf31..df26778b4 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -79,14 +79,3 @@ jobs: -X POST \ -d "{\"repo\":\"emqx/emqx\", \"tag\": \"${{ github.ref_name }}\" }" \ ${{ secrets.EMQX_IO_RELEASE_API }} - - name: update homebrew packages - if: steps.profile.outputs.profile == 'emqx' && (github.event_name == 'release' || inputs.publish_release_artefacts) - run: | - if [ -z $(echo $version | grep -oE "(alpha|beta|rc)\.[0-9]") ]; then - curl --silent --show-error \ - -H "Authorization: token ${{ secrets.CI_GIT_TOKEN }}" \ - -H "Accept: application/vnd.github.v3+json" \ - -X POST \ - -d "{\"ref\":\"v1.0.4\",\"inputs\":{\"version\": \"${{ github.ref_name }}\"}}" \ - "https://api.github.com/repos/emqx/emqx-ci-helper/actions/workflows/update_emqx_homebrew.yaml/dispatches" - fi From 908ac8d94806fb3912490b1404172db095be9d99 Mon Sep 17 00:00:00 2001 From: firest Date: Thu, 26 Jan 2023 12:29:24 +0800 Subject: [PATCH 0315/1802] fix: speed up banned test cases --- apps/emqx_modules/test/emqx_delayed_SUITE.erl | 11 +++++++++-- apps/emqx_retainer/test/emqx_retainer_SUITE.erl | 16 ++++++++++++---- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/apps/emqx_modules/test/emqx_delayed_SUITE.erl b/apps/emqx_modules/test/emqx_delayed_SUITE.erl index ab35e37dc..ffea436bb 100644 --- a/apps/emqx_modules/test/emqx_delayed_SUITE.erl +++ b/apps/emqx_modules/test/emqx_delayed_SUITE.erl @@ -229,6 +229,14 @@ t_banned_delayed(_) -> }), snabbkaffe:start_trace(), + {ok, SubRef} = + snabbkaffe:subscribe( + ?match_event(#{?snk_kind := ignore_delayed_message_publish}), + _NEvents = 2, + _Timeout = 10000, + 0 + ), + lists:foreach( fun(ClientId) -> Msg = emqx_message:make(ClientId, <<"$delayed/1/bc">>, <<"payload">>), @@ -237,8 +245,7 @@ t_banned_delayed(_) -> [ClientId1, ClientId1, ClientId1, ClientId2, ClientId2] ), - timer:sleep(2000), - Trace = snabbkaffe:collect_trace(), + {ok, Trace} = snabbkaffe:receive_events(SubRef), snabbkaffe:stop(), emqx_banned:delete(Who), mnesia:clear_table(emqx_delayed), diff --git a/apps/emqx_retainer/test/emqx_retainer_SUITE.erl b/apps/emqx_retainer/test/emqx_retainer_SUITE.erl index 845f07802..09d1f77da 100644 --- a/apps/emqx_retainer/test/emqx_retainer_SUITE.erl +++ b/apps/emqx_retainer/test/emqx_retainer_SUITE.erl @@ -687,11 +687,19 @@ t_deliver_when_banned(_) -> }), timer:sleep(100), - snabbkaffe:start_trace(), - {ok, #{}, [0]} = emqtt:subscribe(C1, <<"retained/+">>, [{qos, 0}, {rh, 0}]), - timer:sleep(500), - Trace = snabbkaffe:collect_trace(), + snabbkaffe:start_trace(), + {ok, SubRef} = + snabbkaffe:subscribe( + ?match_event(#{?snk_kind := ignore_retained_message_deliver}), + _NEvents = 3, + _Timeout = 10000, + 0 + ), + + {ok, #{}, [0]} = emqtt:subscribe(C1, <<"retained/+">>, [{qos, 0}, {rh, 0}]), + + {ok, Trace} = snabbkaffe:receive_events(SubRef), ?assertEqual(3, length(?of_kind(ignore_retained_message_deliver, Trace))), snabbkaffe:stop(), emqx_banned:delete(Who), From 1f235ffee935696e15fb06c2da8aa7c7e31c9810 Mon Sep 17 00:00:00 2001 From: Erik Timan Date: Thu, 26 Jan 2023 10:30:13 +0100 Subject: [PATCH 0316/1802] refactor(emqx_ee_connector): redo readact key function --- .../src/emqx_ee_connector_influxdb.erl | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl b/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl index 1370ed2c2..402202b11 100644 --- a/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl +++ b/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl @@ -341,9 +341,10 @@ password(_) -> redact_auth(Term) -> emqx_misc:redact(Term, fun is_auth_key/1). -is_auth_key(<<"Authorization">>) -> true; -is_auth_key(<<"authorization">>) -> true; -is_auth_key(_) -> false. +is_auth_key(Key) when is_binary(Key) -> + string:equal("authorization", Key, true); +is_auth_key(_) -> + false. %% ------------------------------------------------------------------------------------------------- %% Query @@ -628,6 +629,13 @@ is_unrecoverable_error(_) -> -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). +is_auth_key_test_() -> + [ + ?_assert(is_auth_key(<<"Authorization">>)), + ?_assertNot(is_auth_key(<<"Something">>)), + ?_assertNot(is_auth_key(89)) + ]. + %% for coverage desc_test_() -> [ From 86cfbfb43c6f81027617ce59a4791e5d39a30a31 Mon Sep 17 00:00:00 2001 From: Kjell Winblad Date: Wed, 25 Jan 2023 14:07:56 +0100 Subject: [PATCH 0317/1802] fix: Authorization header leak in log entries for webhook There might be another possibility for leakage. If the resource mangager for the webhook resource crashes, OTP might log the spec for the resource manager which contains the Config and thus the Authorization header. This is probably an issue for other resources as well and should be fixed in another commit. The following issue has been created for that: https://emqx.atlassian.net/browse/EMQX-8794 Fixes: https://emqx.atlassian.net/browse/EMQX-8791 --- apps/emqx/src/emqx_misc.erl | 6 ++- .../src/emqx_connector_http.erl | 52 ++++++++++++++++--- 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/apps/emqx/src/emqx_misc.erl b/apps/emqx/src/emqx_misc.erl index c20227c07..fbeec8724 100644 --- a/apps/emqx/src/emqx_misc.erl +++ b/apps/emqx/src/emqx_misc.erl @@ -609,7 +609,11 @@ do_redact(K, V, Checker) -> -define(REDACT_VAL, "******"). redact_v(V) when is_binary(V) -> <>; -redact_v(_V) -> ?REDACT_VAL. +%% The HOCON schema system may generate sensitive values with this format +redact_v([{str, Bin}]) when is_binary(Bin) -> + [{str, <>}]; +redact_v(_V) -> + ?REDACT_VAL. is_redacted(K, V) -> do_is_redacted(K, V, fun is_sensitive_key/1). diff --git a/apps/emqx_connector/src/emqx_connector_http.erl b/apps/emqx_connector/src/emqx_connector_http.erl index 7f84c665a..40df52d45 100644 --- a/apps/emqx_connector/src/emqx_connector_http.erl +++ b/apps/emqx_connector/src/emqx_connector_http.erl @@ -209,7 +209,7 @@ on_start( ?SLOG(info, #{ msg => "starting_http_connector", connector => InstId, - config => emqx_misc:redact(Config) + config => redact(Config) }), {Transport, TransportOpts} = case Scheme of @@ -285,7 +285,11 @@ on_query( ?TRACE( "QUERY", "http_connector_received", - #{request => Request, connector => InstId, state => State} + #{ + request => redact(Request), + connector => InstId, + state => redact(State) + } ), NRequest = formalize_request(Method, BasePath, Request), case @@ -310,7 +314,7 @@ on_query( {error, Reason} = Result -> ?SLOG(error, #{ msg => "http_connector_do_request_failed", - request => NRequest, + request => redact(NRequest), reason => Reason, connector => InstId }), @@ -322,7 +326,7 @@ on_query( {ok, StatusCode, Headers} -> ?SLOG(error, #{ msg => "http connector do request, received error response", - request => NRequest, + request => redact(NRequest), connector => InstId, status_code => StatusCode }), @@ -330,7 +334,7 @@ on_query( {ok, StatusCode, Headers, Body} -> ?SLOG(error, #{ msg => "http connector do request, received error response", - request => NRequest, + request => redact(NRequest), connector => InstId, status_code => StatusCode }), @@ -366,7 +370,11 @@ on_query_async( ?TRACE( "QUERY_ASYNC", "http_connector_received", - #{request => Request, connector => InstId, state => State} + #{ + request => redact(Request), + connector => InstId, + state => redact(State) + } ), NRequest = formalize_request(Method, BasePath, Request), Worker = @@ -401,7 +409,7 @@ do_get_status(PoolName, Timeout) -> {error, Reason} = Error -> ?SLOG(error, #{ msg => "http_connector_get_status_failed", - reason => Reason, + reason => redact(Reason), worker => Worker }), Error @@ -554,3 +562,33 @@ reply_delegator(ReplyFunAndArgs, Result) -> _ -> emqx_resource:apply_reply_fun(ReplyFunAndArgs, Result) end. + +%% The HOCON schema system may generate sensitive keys with this format +is_sensitive_key([{str, StringKey}]) -> + is_sensitive_key(StringKey); +is_sensitive_key(Atom) when is_atom(Atom) -> + is_sensitive_key(erlang:atom_to_binary(Atom)); +is_sensitive_key(Bin) when is_binary(Bin), (size(Bin) =:= 19 orelse size(Bin) =:= 13) -> + try + %% This is wrapped in a try-catch since we don't know that Bin is a + %% valid string so string:lowercase/1 might throw an exception. + %% + %% We want to convert this to lowercase since the http header fields + %% are case insensitive, which means that a user of the Webhook bridge + %% can write this field name in many different ways. + LowercaseBin = iolist_to_binary(string:lowercase(Bin)), + case LowercaseBin of + <<"authorization">> -> true; + <<"proxy-authorization">> -> true; + _ -> false + end + catch + _:_ -> false + end; +is_sensitive_key(_) -> + false. + +%% Function that will do a deep traversal of Data and remove sensitive +%% information (i.e., passwords) +redact(Data) -> + emqx_misc:redact(Data, fun is_sensitive_key/1). From f6b3b930b0716124aa514bd3ab4859db762eb3f4 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Thu, 19 Jan 2023 11:54:56 +0100 Subject: [PATCH 0318/1802] chore: improve a error log --- apps/emqx/src/emqx_config.erl | 8 +++++++- apps/emqx_authz/test/emqx_authz_redis_SUITE.erl | 3 +-- .../test/emqx_ee_bridge_gcp_pubsub_SUITE.erl | 3 --- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/apps/emqx/src/emqx_config.erl b/apps/emqx/src/emqx_config.erl index ba4095daa..204e32a2b 100644 --- a/apps/emqx/src/emqx_config.erl +++ b/apps/emqx/src/emqx_config.erl @@ -424,7 +424,13 @@ check_config(SchemaMod, RawConf, Opts0) -> %% it's maybe too much when reporting to the user -spec compact_errors(any(), any()) -> no_return(). compact_errors(Schema, [Error0 | More]) when is_map(Error0) -> - Error1 = Error0#{discarded_errors_count => length(More)}, + Error1 = + case length(More) of + 0 -> + Error0; + _ -> + Error0#{unshown_errors => length(More)} + end, Error = case is_atom(Schema) of true -> diff --git a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl index 15b180c96..c07d920ad 100644 --- a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl @@ -188,8 +188,7 @@ t_create_invalid_config(_Config) -> ?assertMatch( {error, #{ kind := validation_error, - path := "authorization.sources.1", - discarded_errors_count := 0 + path := "authorization.sources.1" }}, emqx_authz:update(?CMD_REPLACE, [C]) ). diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_gcp_pubsub_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_gcp_pubsub_SUITE.erl index 247b7799b..222acb77b 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_gcp_pubsub_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_gcp_pubsub_SUITE.erl @@ -850,7 +850,6 @@ test_publish_success_batch(Config) -> t_not_a_json(Config) -> ?assertMatch( {error, #{ - discarded_errors_count := 0, kind := validation_error, reason := #{exception := {error, {badmap, "not a json"}}}, %% should be censored as it contains secrets @@ -868,7 +867,6 @@ t_not_a_json(Config) -> t_not_of_service_account_type(Config) -> ?assertMatch( {error, #{ - discarded_errors_count := 0, kind := validation_error, reason := {wrong_type, <<"not a service account">>}, %% should be censored as it contains secrets @@ -887,7 +885,6 @@ t_json_missing_fields(Config) -> GCPPubSubConfig0 = ?config(gcp_pubsub_config, Config), ?assertMatch( {error, #{ - discarded_errors_count := 0, kind := validation_error, reason := {missing_keys, [ From bb26632c8a3d808a554302735dd77cd4d135fadb Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Thu, 26 Jan 2023 14:33:16 +0100 Subject: [PATCH 0319/1802] fix(buffer_worker): fix a wrong assertion the assertion is to ensure queue items are not binary but should not assert the queue itself --- apps/emqx_resource/src/emqx_resource_buffer_worker.erl | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/emqx_resource/src/emqx_resource_buffer_worker.erl b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl index 11d3753f0..eaae64dd8 100644 --- a/apps/emqx_resource/src/emqx_resource_buffer_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl @@ -1084,9 +1084,10 @@ estimate_size(QItem) -> erlang:external_size(QItem). -spec append_queue(id(), index(), replayq:q(), [queue_query()]) -> replayq:q(). -append_queue(Id, Index, Q, Queries) when not is_binary(Q) -> - %% we must not append a raw binary because the marshaller will get - %% lost. +append_queue(Id, Index, Q, Queries) -> + %% this assertion is to ensure that we never append a raw binary + %% because the marshaller will get lost. + false = is_binary(hd(Queries)), Q0 = replayq:append(Q, Queries), Q2 = case replayq:overflow(Q0) of From 25b4821adc53b885b0a8641799b2e37834c1de1a Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Thu, 26 Jan 2023 14:40:41 +0100 Subject: [PATCH 0320/1802] refactor: move the the per-message overflow log from error to info level --- .../src/emqx_resource_buffer_worker.erl | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/apps/emqx_resource/src/emqx_resource_buffer_worker.erl b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl index eaae64dd8..28e2c785d 100644 --- a/apps/emqx_resource/src/emqx_resource_buffer_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl @@ -437,7 +437,7 @@ collect_and_enqueue_query_requests(Request0, Data0) -> end, Requests ), - NewQ = append_queue(Id, Index, Q, Queries), + {_Overflow, NewQ} = append_queue(Id, Index, Q, Queries), Data = Data0#{queue := NewQ}, {Queries, Data}. @@ -1089,18 +1089,22 @@ append_queue(Id, Index, Q, Queries) -> %% because the marshaller will get lost. false = is_binary(hd(Queries)), Q0 = replayq:append(Q, Queries), - Q2 = + {Overflow, Q2} = case replayq:overflow(Q0) of - Overflow when Overflow =< 0 -> - Q0; - Overflow -> - PopOpts = #{bytes_limit => Overflow, count_limit => 999999999}, + OverflowBytes when OverflowBytes =< 0 -> + {[], Q0}; + OverflowBytes -> + PopOpts = #{bytes_limit => OverflowBytes, count_limit => 999999999}, {Q1, QAckRef, Items2} = replayq:pop(Q0, PopOpts), ok = replayq:ack(Q1, QAckRef), Dropped = length(Items2), emqx_resource_metrics:dropped_queue_full_inc(Id), - ?SLOG(error, #{msg => drop_query, reason => queue_full, dropped => Dropped}), - Q1 + ?SLOG(info, #{ + msg => buffer_worker_overflow, + worker_id => Id, + dropped => Dropped + }), + {Items2, Q1} end, emqx_resource_metrics:queuing_set(Id, Index, queue_count(Q2)), ?tp( @@ -1108,10 +1112,11 @@ append_queue(Id, Index, Q, Queries) -> #{ id => Id, items => Queries, - queue_count => queue_count(Q2) + queue_count => queue_count(Q2), + overflow => length(Overflow) } ), - Q2. + {Overflow, Q2}. %%============================================================================== %% the inflight queue for async query From ed2878916447dca24f03a022ba50014cc70b2f2a Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Thu, 26 Jan 2023 14:50:40 +0100 Subject: [PATCH 0321/1802] refactor(buffer_worker): no need to return after collect into buf queue --- apps/emqx_resource/src/emqx_resource_buffer_worker.erl | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/emqx_resource/src/emqx_resource_buffer_worker.erl b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl index 28e2c785d..63a402daa 100644 --- a/apps/emqx_resource/src/emqx_resource_buffer_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl @@ -243,7 +243,7 @@ blocked(cast, flush, Data) -> blocked(state_timeout, unblock, St) -> resume_from_blocked(St); blocked(info, ?SEND_REQ(_ReqFrom, {query, _Request, _Opts}) = Request0, Data0) -> - {_Queries, Data} = collect_and_enqueue_query_requests(Request0, Data0), + Data = collect_and_enqueue_query_requests(Request0, Data0), {keep_state, Data}; blocked(info, {flush, _Ref}, _Data) -> keep_state_and_data; @@ -412,7 +412,7 @@ retry_inflight_sync(Ref, QueryOrBatch, Data0) -> -spec handle_query_requests(?SEND_REQ(request_from(), request()), data()) -> gen_statem:event_handler_result(state(), data()). handle_query_requests(Request0, Data0) -> - {_Queries, Data} = collect_and_enqueue_query_requests(Request0, Data0), + Data = collect_and_enqueue_query_requests(Request0, Data0), maybe_flush(Data). collect_and_enqueue_query_requests(Request0, Data0) -> @@ -438,8 +438,7 @@ collect_and_enqueue_query_requests(Request0, Data0) -> Requests ), {_Overflow, NewQ} = append_queue(Id, Index, Q, Queries), - Data = Data0#{queue := NewQ}, - {Queries, Data}. + Data0#{queue := NewQ}. maybe_flush(Data0) -> #{ From 1f799dfd59e21b8901cabd31ddaa7570eb09fc98 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Thu, 26 Jan 2023 16:56:32 +0100 Subject: [PATCH 0322/1802] fix: reply with {error, buffer_overflow} when discarded --- .../src/emqx_resource_buffer_worker.erl | 35 ++++++++++++++----- .../test/emqx_resource_SUITE.erl | 4 +-- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/apps/emqx_resource/src/emqx_resource_buffer_worker.erl b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl index 63a402daa..3d08f0289 100644 --- a/apps/emqx_resource/src/emqx_resource_buffer_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl @@ -242,7 +242,7 @@ blocked(cast, flush, Data) -> resume_from_blocked(Data); blocked(state_timeout, unblock, St) -> resume_from_blocked(St); -blocked(info, ?SEND_REQ(_ReqFrom, {query, _Request, _Opts}) = Request0, Data0) -> +blocked(info, ?SEND_REQ(_ReqFrom, _Req) = Request0, Data0) -> Data = collect_and_enqueue_query_requests(Request0, Data0), {keep_state, Data}; blocked(info, {flush, _Ref}, _Data) -> @@ -437,9 +437,25 @@ collect_and_enqueue_query_requests(Request0, Data0) -> end, Requests ), - {_Overflow, NewQ} = append_queue(Id, Index, Q, Queries), + {Overflown, NewQ} = append_queue(Id, Index, Q, Queries), + ok = reply_overflown(Overflown), Data0#{queue := NewQ}. +reply_overflown([]) -> + ok; +reply_overflown([?QUERY(From, _Req, _HasBeenSent, _ExpireAt) | More]) -> + do_reply_caller(From, {error, buffer_overflow}), + reply_overflown(More). + +do_reply_caller(undefined, _Result) -> + ok; +do_reply_caller({F, Args}, Result) when is_function(F) -> + _ = erlang:apply(F, Args ++ [Result]), + ok; +do_reply_caller(From, Result) -> + _ = gen_statem:reply(From, Result), + ok. + maybe_flush(Data0) -> #{ batch_size := BatchSize, @@ -1082,18 +1098,19 @@ queue_item_marshaller(Item) -> estimate_size(QItem) -> erlang:external_size(QItem). --spec append_queue(id(), index(), replayq:q(), [queue_query()]) -> replayq:q(). +-spec append_queue(id(), index(), replayq:q(), [queue_query()]) -> + {[queue_query()], replayq:q()}. append_queue(Id, Index, Q, Queries) -> %% this assertion is to ensure that we never append a raw binary %% because the marshaller will get lost. false = is_binary(hd(Queries)), Q0 = replayq:append(Q, Queries), - {Overflow, Q2} = + {Overflown, Q2} = case replayq:overflow(Q0) of - OverflowBytes when OverflowBytes =< 0 -> + OverflownBytes when OverflownBytes =< 0 -> {[], Q0}; - OverflowBytes -> - PopOpts = #{bytes_limit => OverflowBytes, count_limit => 999999999}, + OverflownBytes -> + PopOpts = #{bytes_limit => OverflownBytes, count_limit => 999999999}, {Q1, QAckRef, Items2} = replayq:pop(Q0, PopOpts), ok = replayq:ack(Q1, QAckRef), Dropped = length(Items2), @@ -1112,10 +1129,10 @@ append_queue(Id, Index, Q, Queries) -> id => Id, items => Queries, queue_count => queue_count(Q2), - overflow => length(Overflow) + overflown => length(Overflown) } ), - {Overflow, Q2}. + {Overflown, Q2}. %%============================================================================== %% the inflight queue for async query diff --git a/apps/emqx_resource/test/emqx_resource_SUITE.erl b/apps/emqx_resource/test/emqx_resource_SUITE.erl index 9b2af74f6..34a92a5a2 100644 --- a/apps/emqx_resource/test/emqx_resource_SUITE.erl +++ b/apps/emqx_resource/test/emqx_resource_SUITE.erl @@ -1226,8 +1226,8 @@ t_always_overflow(_Config) -> Payload = binary:copy(<<"a">>, 100), %% since it's sync and it should never send a request, this %% errors with `timeout'. - ?assertError( - timeout, + ?assertEqual( + {error, buffer_overflow}, emqx_resource:query( ?ID, {big_payload, Payload}, From c95f9794131d8b8e14a6ad92c40131814f7b72d3 Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Thu, 26 Jan 2023 21:12:05 +0300 Subject: [PATCH 0323/1802] fix(mqtt_bridge): use correct gen_statem reply action --- apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl index ba2162993..05b9f7bf1 100644 --- a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl +++ b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl @@ -305,7 +305,7 @@ connected({call, From}, {send_to_remote, Msg}, State) -> {ok, NState} -> {keep_state, NState, [{reply, From, ok}]}; {error, Reason} -> - {keep_state_and_data, [[reply, From, {error, Reason}]]} + {keep_state_and_data, {reply, From, {error, Reason}}} end; connected(cast, {send_to_remote_async, Msg, Callback}, State) -> _ = do_send_async(State, Msg, Callback), From 71f996b9d5f5572ad9e3fe03ad8bbded5d0ab2ec Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Thu, 26 Jan 2023 21:13:18 +0300 Subject: [PATCH 0324/1802] refactor(mqtt-bridge): unwrap single statem actions So that the code would be easier to follow and harder to break. Also drop a couple of unused macrodefs. --- .../src/mqtt/emqx_connector_mqtt_worker.erl | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl index 05b9f7bf1..00b45789e 100644 --- a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl +++ b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl @@ -116,8 +116,6 @@ %% same as default in-flight limit for emqtt -define(DEFAULT_INFLIGHT_SIZE, 32). -define(DEFAULT_RECONNECT_DELAY_MS, timer:seconds(5)). --define(DEFAULT_SEG_BYTES, (1 bsl 20)). --define(DEFAULT_MAX_TOTAL_SIZE, (1 bsl 31)). %% @doc Start a bridge worker. Supported configs: %% start_type: 'manual' (default) or 'auto', when manual, bridge will stay @@ -274,10 +272,10 @@ idle({call, From}, ensure_started, State) -> {ok, State1} -> {next_state, connected, State1, [{reply, From, ok}, {state_timeout, 0, connected}]}; {error, Reason, _State} -> - {keep_state_and_data, [{reply, From, {error, Reason}}]} + {keep_state_and_data, {reply, From, {error, Reason}}} end; idle({call, From}, {send_to_remote, _}, _State) -> - {keep_state_and_data, [{reply, From, {error, {recoverable_error, not_connected}}}]}; + {keep_state_and_data, {reply, From, {error, {recoverable_error, not_connected}}}}; %% @doc Standing by for manual start. idle(info, idle, #{start_type := manual}) -> keep_state_and_data; @@ -303,7 +301,7 @@ connected(state_timeout, connected, State) -> connected({call, From}, {send_to_remote, Msg}, State) -> case do_send(State, Msg) of {ok, NState} -> - {keep_state, NState, [{reply, From, ok}]}; + {keep_state, NState, {reply, From, ok}}; {error, Reason} -> {keep_state_and_data, {reply, From, {error, Reason}}} end; @@ -328,21 +326,21 @@ connected(Type, Content, State) -> %% Common handlers common(StateName, {call, From}, status, _State) -> - {keep_state_and_data, [{reply, From, StateName}]}; + {keep_state_and_data, {reply, From, StateName}}; common(_StateName, {call, From}, ping, #{connection := Conn} = _State) -> Reply = emqx_connector_mqtt_mod:ping(Conn), - {keep_state_and_data, [{reply, From, Reply}]}; + {keep_state_and_data, {reply, From, Reply}}; common(_StateName, {call, From}, ensure_stopped, #{connection := undefined} = _State) -> - {keep_state_and_data, [{reply, From, ok}]}; + {keep_state_and_data, {reply, From, ok}}; common(_StateName, {call, From}, ensure_stopped, #{connection := Conn} = State) -> Reply = emqx_connector_mqtt_mod:stop(Conn), - {next_state, idle, State#{connection => undefined}, [{reply, From, Reply}]}; + {next_state, idle, State#{connection => undefined}, {reply, From, Reply}}; common(_StateName, {call, From}, get_forwards, #{connect_opts := #{forwards := Forwards}}) -> - {keep_state_and_data, [{reply, From, Forwards}]}; + {keep_state_and_data, {reply, From, Forwards}}; common(_StateName, {call, From}, get_subscriptions, #{connection := Connection}) -> - {keep_state_and_data, [{reply, From, maps:get(subscriptions, Connection, #{})}]}; + {keep_state_and_data, {reply, From, maps:get(subscriptions, Connection, #{})}}; common(_StateName, {call, From}, Req, _State) -> - {keep_state_and_data, [{reply, From, {error, {unsupported_request, Req}}}]}; + {keep_state_and_data, {reply, From, {error, {unsupported_request, Req}}}}; common(_StateName, info, {'EXIT', _, _}, State) -> {keep_state, State}; common(StateName, Type, Content, #{name := Name} = State) -> From d4fab92b72237b850b4b7e231eca9e295c48a2b6 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Thu, 26 Jan 2023 18:00:20 +0100 Subject: [PATCH 0325/1802] refactor(buffer_worker): no need to keep request for REPLY macro --- .../src/emqx_resource_buffer_worker.erl | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/apps/emqx_resource/src/emqx_resource_buffer_worker.erl b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl index 3d08f0289..5460a8198 100644 --- a/apps/emqx_resource/src/emqx_resource_buffer_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl @@ -63,11 +63,7 @@ -define(SEND_REQ(FROM, REQUEST), {'$send_req', FROM, REQUEST}). -define(QUERY(FROM, REQUEST, SENT, EXPIRE_AT), {query, FROM, REQUEST, SENT, EXPIRE_AT}). -define(SIMPLE_QUERY(REQUEST), ?QUERY(undefined, REQUEST, false, infinity)). --define(REPLY(FROM, REQUEST, SENT, RESULT), {reply, FROM, REQUEST, SENT, RESULT}). --define(EXPAND(RESULT, BATCH), [ - ?REPLY(FROM, REQUEST, SENT, RESULT) - || ?QUERY(FROM, REQUEST, SENT, _EXPIRE_AT) <- BATCH -]). +-define(REPLY(FROM, SENT, RESULT), {reply, FROM, SENT, RESULT}). -define(INFLIGHT_ITEM(Ref, BatchOrQuery, IsRetriable, WorkerMRef), {Ref, BatchOrQuery, IsRetriable, WorkerMRef} ). @@ -370,8 +366,8 @@ retry_inflight_sync(Ref, QueryOrBatch, Data0) -> Result = call_query(sync, Id, Index, Ref, QueryOrBatch, QueryOpts), ReplyResult = case QueryOrBatch of - ?QUERY(From, CoreReq, HasBeenSent, _ExpireAt) -> - Reply = ?REPLY(From, CoreReq, HasBeenSent, Result), + ?QUERY(From, _, HasBeenSent, _ExpireAt) -> + Reply = ?REPLY(From, HasBeenSent, Result), reply_caller_defer_metrics(Id, Reply, QueryOpts); [?QUERY(_, _, _, _) | _] = Batch -> batch_reply_caller_defer_metrics(Id, Result, Batch, QueryOpts) @@ -548,10 +544,10 @@ do_flush( inflight_tid := InflightTID } = Data0, %% unwrap when not batching (i.e., batch size == 1) - [?QUERY(From, CoreReq, HasBeenSent, _ExpireAt) = Request] = Batch, + [?QUERY(From, _, HasBeenSent, _ExpireAt) = Request] = Batch, QueryOpts = #{inflight_tid => InflightTID, simple_query => false}, Result = call_query(configured, Id, Index, Ref, Request, QueryOpts), - Reply = ?REPLY(From, CoreReq, HasBeenSent, Result), + Reply = ?REPLY(From, HasBeenSent, Result), case reply_caller(Id, Reply, QueryOpts) of %% Failed; remove the request from the queue, as we cannot pop %% from it again, but we'll retry it using the inflight table. @@ -705,6 +701,14 @@ batch_reply_caller(Id, BatchResult, Batch, QueryOpts) -> ShouldBlock. batch_reply_caller_defer_metrics(Id, BatchResult, Batch, QueryOpts) -> + %% the `Mod:on_batch_query/3` returns a single result for a batch, + %% so we need to expand + Replies = lists:map( + fun(?QUERY(FROM, _REQUEST, SENT, _EXPIRE_AT)) -> + ?REPLY(FROM, SENT, BatchResult) + end, + Batch + ), {ShouldAck, PostFns} = lists:foldl( fun(Reply, {_ShouldAck, PostFns}) -> @@ -712,9 +716,7 @@ batch_reply_caller_defer_metrics(Id, BatchResult, Batch, QueryOpts) -> {ShouldAck, [PostFn | PostFns]} end, {ack, []}, - %% the `Mod:on_batch_query/3` returns a single result for a batch, - %% so we need to expand - ?EXPAND(BatchResult, Batch) + Replies ), PostFn = fun() -> lists:foreach(fun(F) -> F() end, PostFns) end, {ShouldAck, PostFn}. @@ -726,9 +728,9 @@ reply_caller(Id, Reply, QueryOpts) -> %% Should only reply to the caller when the decision is final (not %% retriable). See comment on `handle_query_result_pure'. -reply_caller_defer_metrics(Id, ?REPLY(undefined, _, HasBeenSent, Result), _QueryOpts) -> +reply_caller_defer_metrics(Id, ?REPLY(undefined, HasBeenSent, Result), _QueryOpts) -> handle_query_result_pure(Id, Result, HasBeenSent); -reply_caller_defer_metrics(Id, ?REPLY({ReplyFun, Args}, _, HasBeenSent, Result), QueryOpts) when +reply_caller_defer_metrics(Id, ?REPLY({ReplyFun, Args}, HasBeenSent, Result), QueryOpts) when is_function(ReplyFun) -> IsSimpleQuery = maps:get(simple_query, QueryOpts, false), @@ -750,7 +752,7 @@ reply_caller_defer_metrics(Id, ?REPLY({ReplyFun, Args}, _, HasBeenSent, Result), ok end, {ShouldAck, PostFn}; -reply_caller_defer_metrics(Id, ?REPLY(From, _, HasBeenSent, Result), QueryOpts) -> +reply_caller_defer_metrics(Id, ?REPLY(From, HasBeenSent, Result), QueryOpts) -> IsSimpleQuery = maps:get(simple_query, QueryOpts, false), IsUnrecoverableError = is_unrecoverable_error(Result), {ShouldAck, PostFn} = handle_query_result_pure(Id, Result, HasBeenSent), @@ -989,7 +991,7 @@ do_reply_after_query( Index, InflightTID, Ref, - ?QUERY(From, Request, HasBeenSent, _ExpireAt), + ?QUERY(From, _Request, HasBeenSent, _ExpireAt), QueryOpts, Result ) -> @@ -997,14 +999,14 @@ do_reply_after_query( %% but received no ACK, NOT the number of messages queued in the %% inflight window. {Action, PostFn} = reply_caller_defer_metrics( - Id, ?REPLY(From, Request, HasBeenSent, Result), QueryOpts + Id, ?REPLY(From, HasBeenSent, Result), QueryOpts ), case Action of nack -> %% Keep retrying. ?tp(buffer_worker_reply_after_query, #{ action => Action, - batch_or_query => ?QUERY(From, Request, HasBeenSent, _ExpireAt), + batch_or_query => ?QUERY(From, _Request, HasBeenSent, _ExpireAt), ref => Ref, result => Result }), @@ -1013,7 +1015,7 @@ do_reply_after_query( ack -> ?tp(buffer_worker_reply_after_query, #{ action => Action, - batch_or_query => ?QUERY(From, Request, HasBeenSent, _ExpireAt), + batch_or_query => ?QUERY(From, _Request, HasBeenSent, _ExpireAt), ref => Ref, result => Result }), From db2f631a8a49289f30eb59624cd1c220de505c03 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Fri, 27 Jan 2023 11:16:06 +0100 Subject: [PATCH 0326/1802] refactor(buffer_worker): simplify caller reply --- .../src/emqx_resource_buffer_worker.erl | 95 +++++++------------ 1 file changed, 36 insertions(+), 59 deletions(-) diff --git a/apps/emqx_resource/src/emqx_resource_buffer_worker.erl b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl index 5460a8198..c7a061b61 100644 --- a/apps/emqx_resource/src/emqx_resource_buffer_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl @@ -210,7 +210,7 @@ running(cast, flush, Data) -> flush(Data); running(cast, block, St) -> {next_state, blocked, St}; -running(info, ?SEND_REQ(_From, _Req) = Request0, Data) -> +running(info, ?SEND_REQ(_ReplyTo, _Req) = Request0, Data) -> handle_query_requests(Request0, Data); running(info, {flush, Ref}, St = #{tref := {_TRef, Ref}}) -> flush(St#{tref := undefined}); @@ -238,7 +238,7 @@ blocked(cast, flush, Data) -> resume_from_blocked(Data); blocked(state_timeout, unblock, St) -> resume_from_blocked(St); -blocked(info, ?SEND_REQ(_ReqFrom, _Req) = Request0, Data0) -> +blocked(info, ?SEND_REQ(_ReplyTo, _Req) = Request0, Data0) -> Data = collect_and_enqueue_query_requests(Request0, Data0), {keep_state, Data}; blocked(info, {flush, _Ref}, _Data) -> @@ -284,7 +284,8 @@ pick_call(Id, Key, Query, Timeout) -> Caller = self(), MRef = erlang:monitor(process, Pid, [{alias, reply_demonitor}]), From = {Caller, MRef}, - erlang:send(Pid, ?SEND_REQ(From, Query)), + ReplyTo = {fun gen_statem:reply/2, [From]}, + erlang:send(Pid, ?SEND_REQ(ReplyTo, Query)), receive {MRef, Response} -> erlang:demonitor(MRef, [flush]), @@ -304,8 +305,8 @@ pick_call(Id, Key, Query, Timeout) -> pick_cast(Id, Key, Query) -> ?PICK(Id, Key, Pid, begin - From = undefined, - erlang:send(Pid, ?SEND_REQ(From, Query)), + ReplyTo = undefined, + erlang:send(Pid, ?SEND_REQ(ReplyTo, Query)), ok end). @@ -366,8 +367,8 @@ retry_inflight_sync(Ref, QueryOrBatch, Data0) -> Result = call_query(sync, Id, Index, Ref, QueryOrBatch, QueryOpts), ReplyResult = case QueryOrBatch of - ?QUERY(From, _, HasBeenSent, _ExpireAt) -> - Reply = ?REPLY(From, HasBeenSent, Result), + ?QUERY(ReplyTo, _, HasBeenSent, _ExpireAt) -> + Reply = ?REPLY(ReplyTo, HasBeenSent, Result), reply_caller_defer_metrics(Id, Reply, QueryOpts); [?QUERY(_, _, _, _) | _] = Batch -> batch_reply_caller_defer_metrics(Id, Result, Batch, QueryOpts) @@ -421,15 +422,15 @@ collect_and_enqueue_query_requests(Request0, Data0) -> Queries = lists:map( fun - (?SEND_REQ(undefined = _From, {query, Req, Opts})) -> + (?SEND_REQ(undefined = _ReplyTo, {query, Req, Opts})) -> ReplyFun = maps:get(async_reply_fun, Opts, undefined), HasBeenSent = false, ExpireAt = maps:get(expire_at, Opts), ?QUERY(ReplyFun, Req, HasBeenSent, ExpireAt); - (?SEND_REQ(From, {query, Req, Opts})) -> + (?SEND_REQ(ReplyTo, {query, Req, Opts})) -> HasBeenSent = false, ExpireAt = maps:get(expire_at, Opts), - ?QUERY(From, Req, HasBeenSent, ExpireAt) + ?QUERY(ReplyTo, Req, HasBeenSent, ExpireAt) end, Requests ), @@ -439,17 +440,18 @@ collect_and_enqueue_query_requests(Request0, Data0) -> reply_overflown([]) -> ok; -reply_overflown([?QUERY(From, _Req, _HasBeenSent, _ExpireAt) | More]) -> - do_reply_caller(From, {error, buffer_overflow}), +reply_overflown([?QUERY(ReplyTo, _Req, _HasBeenSent, _ExpireAt) | More]) -> + do_reply_caller(ReplyTo, {error, buffer_overflow}), reply_overflown(More). do_reply_caller(undefined, _Result) -> ok; +do_reply_caller({F, Args}, {async_return, Result}) -> + %% this is an early return to async caller, the retry + %% decision has to be made by the caller + do_reply_caller({F, Args}, Result); do_reply_caller({F, Args}, Result) when is_function(F) -> _ = erlang:apply(F, Args ++ [Result]), - ok; -do_reply_caller(From, Result) -> - _ = gen_statem:reply(From, Result), ok. maybe_flush(Data0) -> @@ -544,10 +546,10 @@ do_flush( inflight_tid := InflightTID } = Data0, %% unwrap when not batching (i.e., batch size == 1) - [?QUERY(From, _, HasBeenSent, _ExpireAt) = Request] = Batch, + [?QUERY(ReplyTo, _, HasBeenSent, _ExpireAt) = Request] = Batch, QueryOpts = #{inflight_tid => InflightTID, simple_query => false}, Result = call_query(configured, Id, Index, Ref, Request, QueryOpts), - Reply = ?REPLY(From, HasBeenSent, Result), + Reply = ?REPLY(ReplyTo, HasBeenSent, Result), case reply_caller(Id, Reply, QueryOpts) of %% Failed; remove the request from the queue, as we cannot pop %% from it again, but we'll retry it using the inflight table. @@ -730,46 +732,21 @@ reply_caller(Id, Reply, QueryOpts) -> %% retriable). See comment on `handle_query_result_pure'. reply_caller_defer_metrics(Id, ?REPLY(undefined, HasBeenSent, Result), _QueryOpts) -> handle_query_result_pure(Id, Result, HasBeenSent); -reply_caller_defer_metrics(Id, ?REPLY({ReplyFun, Args}, HasBeenSent, Result), QueryOpts) when - is_function(ReplyFun) --> +reply_caller_defer_metrics(Id, ?REPLY(ReplyTo, HasBeenSent, Result), QueryOpts) -> IsSimpleQuery = maps:get(simple_query, QueryOpts, false), IsUnrecoverableError = is_unrecoverable_error(Result), {ShouldAck, PostFn} = handle_query_result_pure(Id, Result, HasBeenSent), case {ShouldAck, Result, IsUnrecoverableError, IsSimpleQuery} of {ack, {async_return, _}, true, _} -> - apply(ReplyFun, Args ++ [Result]), - ok; + ok = do_reply_caller(ReplyTo, Result); {ack, {async_return, _}, false, _} -> ok; {_, _, _, true} -> - apply(ReplyFun, Args ++ [Result]), - ok; + ok = do_reply_caller(ReplyTo, Result); {nack, _, _, _} -> ok; {ack, _, _, _} -> - apply(ReplyFun, Args ++ [Result]), - ok - end, - {ShouldAck, PostFn}; -reply_caller_defer_metrics(Id, ?REPLY(From, HasBeenSent, Result), QueryOpts) -> - IsSimpleQuery = maps:get(simple_query, QueryOpts, false), - IsUnrecoverableError = is_unrecoverable_error(Result), - {ShouldAck, PostFn} = handle_query_result_pure(Id, Result, HasBeenSent), - case {ShouldAck, Result, IsUnrecoverableError, IsSimpleQuery} of - {ack, {async_return, _}, true, _} -> - gen_statem:reply(From, Result), - ok; - {ack, {async_return, _}, false, _} -> - ok; - {_, _, _, true} -> - gen_statem:reply(From, Result), - ok; - {nack, _, _, _} -> - ok; - {ack, _, _, _} -> - gen_statem:reply(From, Result), - ok + ok = do_reply_caller(ReplyTo, Result) end, {ShouldAck, PostFn}. @@ -935,7 +912,7 @@ apply_query_fun(sync, Mod, Id, _Index, _Ref, [?QUERY(_, _, _, _) | _] = Batch, R ?tp(call_batch_query, #{ id => Id, mod => Mod, batch => Batch, res_st => ResSt, call_mode => sync }), - Requests = [Request || ?QUERY(_From, Request, _, _ExpireAt) <- Batch], + Requests = [Request || ?QUERY(_ReplyTo, Request, _, _ExpireAt) <- Batch], ?APPLY_RESOURCE(call_batch_query, Mod:on_batch_query(Id, Requests, ResSt), Batch); apply_query_fun(async, Mod, Id, Index, Ref, [?QUERY(_, _, _, _) | _] = Batch, ResSt, QueryOpts) -> ?tp(call_batch_query_async, #{ @@ -947,7 +924,7 @@ apply_query_fun(async, Mod, Id, Index, Ref, [?QUERY(_, _, _, _) | _] = Batch, Re begin ReplyFun = fun ?MODULE:batch_reply_after_query/8, ReplyFunAndArgs = {ReplyFun, [self(), Id, Index, InflightTID, Ref, Batch, QueryOpts]}, - Requests = [Request || ?QUERY(_From, Request, _, _ExpireAt) <- Batch], + Requests = [Request || ?QUERY(_ReplyTo, Request, _, _ExpireAt) <- Batch], IsRetriable = false, WorkerMRef = undefined, InflightItem = ?INFLIGHT_ITEM(Ref, Batch, IsRetriable, WorkerMRef), @@ -964,7 +941,7 @@ reply_after_query( Index, InflightTID, Ref, - ?QUERY(_From, _Request, _HasBeenSent, ExpireAt) = Query, + ?QUERY(_ReplyTo, _Request, _HasBeenSent, ExpireAt) = Query, QueryOpts, Result ) -> @@ -991,7 +968,7 @@ do_reply_after_query( Index, InflightTID, Ref, - ?QUERY(From, _Request, HasBeenSent, _ExpireAt), + ?QUERY(ReplyTo, _Request, HasBeenSent, _ExpireAt), QueryOpts, Result ) -> @@ -999,14 +976,14 @@ do_reply_after_query( %% but received no ACK, NOT the number of messages queued in the %% inflight window. {Action, PostFn} = reply_caller_defer_metrics( - Id, ?REPLY(From, HasBeenSent, Result), QueryOpts + Id, ?REPLY(ReplyTo, HasBeenSent, Result), QueryOpts ), case Action of nack -> %% Keep retrying. ?tp(buffer_worker_reply_after_query, #{ action => Action, - batch_or_query => ?QUERY(From, _Request, HasBeenSent, _ExpireAt), + batch_or_query => ?QUERY(ReplyTo, _Request, HasBeenSent, _ExpireAt), ref => Ref, result => Result }), @@ -1015,7 +992,7 @@ do_reply_after_query( ack -> ?tp(buffer_worker_reply_after_query, #{ action => Action, - batch_or_query => ?QUERY(From, _Request, HasBeenSent, _ExpireAt), + batch_or_query => ?QUERY(ReplyTo, _Request, HasBeenSent, _ExpireAt), ref => Ref, result => Result }), @@ -1175,7 +1152,7 @@ inflight_get_first_retriable(InflightTID, Now) -> case ets:select(InflightTID, MatchSpec, _Limit = 1) of '$end_of_table' -> none; - {[{Ref, Query = ?QUERY(_From, _CoreReq, _HasBeenSent, ExpireAt)}], _Continuation} -> + {[{Ref, Query = ?QUERY(_ReplyTo, _CoreReq, _HasBeenSent, ExpireAt)}], _Continuation} -> case is_expired(ExpireAt, Now) of true -> {expired, Ref, [Query]}; @@ -1234,7 +1211,7 @@ inflight_append( inflight_append( InflightTID, ?INFLIGHT_ITEM( - Ref, ?QUERY(_From, _Req, _HasBeenSent, _ExpireAt) = Query0, IsRetriable, WorkerMRef + Ref, ?QUERY(_ReplyTo, _Req, _HasBeenSent, _ExpireAt) = Query0, IsRetriable, WorkerMRef ), Id, Index @@ -1405,7 +1382,7 @@ do_collect_requests(Acc, Count, Limit) when Count >= Limit -> lists:reverse(Acc); do_collect_requests(Acc, Count, Limit) -> receive - ?SEND_REQ(_From, _Req) = Request -> + ?SEND_REQ(_ReplyTo, _Req) = Request -> do_collect_requests([Request | Acc], Count + 1, Limit) after 0 -> lists:reverse(Acc) @@ -1413,9 +1390,9 @@ do_collect_requests(Acc, Count, Limit) -> mark_as_sent(Batch) when is_list(Batch) -> lists:map(fun mark_as_sent/1, Batch); -mark_as_sent(?QUERY(From, Req, _HasBeenSent, ExpireAt)) -> +mark_as_sent(?QUERY(ReplyTo, Req, _HasBeenSent, ExpireAt)) -> HasBeenSent = true, - ?QUERY(From, Req, HasBeenSent, ExpireAt). + ?QUERY(ReplyTo, Req, HasBeenSent, ExpireAt). is_unrecoverable_error({error, {unrecoverable_error, _}}) -> true; @@ -1439,7 +1416,7 @@ is_async_return(_) -> sieve_expired_requests(Batch, Now) -> {Expired, NotExpired} = lists:partition( - fun(?QUERY(_From, _CoreReq, _HasBeenSent, ExpireAt)) -> + fun(?QUERY(_ReplyTo, _CoreReq, _HasBeenSent, ExpireAt)) -> is_expired(ExpireAt, Now) end, Batch From d5f62d917e5ff996517676b13d8bc26681c812aa Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Fri, 27 Jan 2023 11:40:05 +0100 Subject: [PATCH 0327/1802] chore: upgrade ehttpc and ecpool --- mix.exs | 4 ++-- rebar.config | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mix.exs b/mix.exs index 315212fbe..623321f29 100644 --- a/mix.exs +++ b/mix.exs @@ -47,7 +47,7 @@ defmodule EMQXUmbrella.MixProject do {:lc, github: "emqx/lc", tag: "0.3.2", override: true}, {:redbug, "2.0.8"}, {:typerefl, github: "ieQu1/typerefl", tag: "0.9.1", override: true}, - {:ehttpc, github: "emqx/ehttpc", tag: "0.4.4", override: true}, + {:ehttpc, github: "emqx/ehttpc", tag: "0.4.5", override: true}, {:gproc, github: "uwiger/gproc", tag: "0.8.0", override: true}, {:jiffy, github: "emqx/jiffy", tag: "1.0.5", override: true}, {:cowboy, github: "emqx/cowboy", tag: "2.9.0", override: true}, @@ -57,7 +57,7 @@ defmodule EMQXUmbrella.MixProject do {:gen_rpc, github: "emqx/gen_rpc", tag: "2.8.1", override: true}, {:grpc, github: "emqx/grpc-erl", tag: "0.6.7", override: true}, {:minirest, github: "emqx/minirest", tag: "1.3.7", override: true}, - {:ecpool, github: "emqx/ecpool", tag: "0.5.2", override: true}, + {:ecpool, github: "emqx/ecpool", tag: "0.5.3", override: true}, {:replayq, github: "emqx/replayq", tag: "0.3.6", override: true}, {:pbkdf2, github: "emqx/erlang-pbkdf2", tag: "2.0.4", override: true}, {:emqtt, github: "emqx/emqtt", tag: "1.7.0-rc.2", override: true}, diff --git a/rebar.config b/rebar.config index a1b4df520..71a54a03d 100644 --- a/rebar.config +++ b/rebar.config @@ -49,7 +49,7 @@ , {gpb, "4.19.5"} %% gpb only used to build, but not for release, pin it here to avoid fetching a wrong version due to rebar plugins scattered in all the deps , {typerefl, {git, "https://github.com/ieQu1/typerefl", {tag, "0.9.1"}}} , {gun, {git, "https://github.com/emqx/gun", {tag, "1.3.9"}}} - , {ehttpc, {git, "https://github.com/emqx/ehttpc", {tag, "0.4.4"}}} + , {ehttpc, {git, "https://github.com/emqx/ehttpc", {tag, "0.4.5"}}} , {gproc, {git, "https://github.com/uwiger/gproc", {tag, "0.8.0"}}} , {jiffy, {git, "https://github.com/emqx/jiffy", {tag, "1.0.5"}}} , {cowboy, {git, "https://github.com/emqx/cowboy", {tag, "2.9.0"}}} @@ -59,7 +59,7 @@ , {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.8.1"}}} , {grpc, {git, "https://github.com/emqx/grpc-erl", {tag, "0.6.7"}}} , {minirest, {git, "https://github.com/emqx/minirest", {tag, "1.3.7"}}} - , {ecpool, {git, "https://github.com/emqx/ecpool", {tag, "0.5.2"}}} + , {ecpool, {git, "https://github.com/emqx/ecpool", {tag, "0.5.3"}}} , {replayq, {git, "https://github.com/emqx/replayq.git", {tag, "0.3.6"}}} , {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}} , {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.7.0-rc.2"}}} From b7e3f9d5a6b539d3a09e7886b579e3b51d32e543 Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Mon, 23 Jan 2023 13:57:49 +0100 Subject: [PATCH 0328/1802] fix: try-case-of rather than try-of try-of catches only what happens within but not after --- .../src/emqx_resource_buffer_worker.erl | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/apps/emqx_resource/src/emqx_resource_buffer_worker.erl b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl index 5460a8198..5b8524f39 100644 --- a/apps/emqx_resource/src/emqx_resource_buffer_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl @@ -266,11 +266,13 @@ code_change(_OldVsn, State, _Extra) -> %%============================================================================== -define(PICK(ID, KEY, PID, EXPR), - try gproc_pool:pick_worker(ID, KEY) of - PID when is_pid(PID) -> - EXPR; - _ -> - ?RESOURCE_ERROR(worker_not_created, "resource not created") + try + case gproc_pool:pick_worker(ID, KEY) of + PID when is_pid(PID) -> + EXPR; + _ -> + ?RESOURCE_ERROR(worker_not_created, "resource not created") + end catch error:badarg -> ?RESOURCE_ERROR(worker_not_created, "resource not created"); From a180bd9aa5197b93aaaa87c266bdc4c7a55d99d3 Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Mon, 23 Jan 2023 13:56:33 +0100 Subject: [PATCH 0329/1802] fix: catch error, not exit --- apps/emqx_resource/src/emqx_resource_buffer_worker.erl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/emqx_resource/src/emqx_resource_buffer_worker.erl b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl index 5b8524f39..0b0bf94aa 100644 --- a/apps/emqx_resource/src/emqx_resource_buffer_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl @@ -20,7 +20,6 @@ -module(emqx_resource_buffer_worker). -include("emqx_resource.hrl"). --include("emqx_resource_utils.hrl"). -include("emqx_resource_errors.hrl"). -include_lib("emqx/include/logger.hrl"). -include_lib("stdlib/include/ms_transform.hrl"). @@ -276,7 +275,7 @@ code_change(_OldVsn, State, _Extra) -> catch error:badarg -> ?RESOURCE_ERROR(worker_not_created, "resource not created"); - exit:{timeout, _} -> + error:timeout -> ?RESOURCE_ERROR(timeout, "call resource timeout") end ). From 8f3b1f87444d4b9d77d400e9f341a6cdc54dca1f Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Mon, 23 Jan 2023 16:37:24 +0100 Subject: [PATCH 0330/1802] chore: add changelog --- changes/v5.0.16/fix-9832.en.md | 1 + changes/v5.0.16/fix-9832.zh.md | 1 + 2 files changed, 2 insertions(+) create mode 100644 changes/v5.0.16/fix-9832.en.md create mode 100644 changes/v5.0.16/fix-9832.zh.md diff --git a/changes/v5.0.16/fix-9832.en.md b/changes/v5.0.16/fix-9832.en.md new file mode 100644 index 000000000..84178b63c --- /dev/null +++ b/changes/v5.0.16/fix-9832.en.md @@ -0,0 +1 @@ +Improve error log when bridge in 'sync' mode timed out to get response. diff --git a/changes/v5.0.16/fix-9832.zh.md b/changes/v5.0.16/fix-9832.zh.md new file mode 100644 index 000000000..e7fd33b6b --- /dev/null +++ b/changes/v5.0.16/fix-9832.zh.md @@ -0,0 +1 @@ +优化桥接同步资源调用超时情况下的一个错误日志。 From 2d62de518802e26695c53cc54d1f39e385f5c50a Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Mon, 23 Jan 2023 16:42:23 +0100 Subject: [PATCH 0331/1802] test: fix expected result from timeout error --- .../test/emqx_resource_SUITE.erl | 27 ++++++++++--------- .../test/emqx_ee_bridge_influxdb_SUITE.erl | 8 +++--- .../test/emqx_ee_bridge_mysql_SUITE.erl | 9 ++++--- .../test/emqx_ee_bridge_pgsql_SUITE.erl | 12 ++++----- 4 files changed, 28 insertions(+), 28 deletions(-) diff --git a/apps/emqx_resource/test/emqx_resource_SUITE.erl b/apps/emqx_resource/test/emqx_resource_SUITE.erl index 34a92a5a2..227b6fedc 100644 --- a/apps/emqx_resource/test/emqx_resource_SUITE.erl +++ b/apps/emqx_resource/test/emqx_resource_SUITE.erl @@ -19,8 +19,6 @@ -compile(export_all). -include_lib("eunit/include/eunit.hrl"). --include_lib("common_test/include/ct.hrl"). --include("emqx_resource.hrl"). -include_lib("stdlib/include/ms_transform.hrl"). -include_lib("snabbkaffe/include/snabbkaffe.hrl"). @@ -772,7 +770,10 @@ t_healthy_timeout(_) -> %% the ?TEST_RESOURCE always returns the `Mod:on_get_status/2` 300ms later. #{health_check_interval => 200} ), - ?assertError(timeout, emqx_resource:query(?ID, get_state, #{timeout => 1_000})), + ?assertMatch( + {error, {resource_error, #{reason := timeout}}}, + emqx_resource:query(?ID, get_state, #{timeout => 1_000}) + ), ?assertMatch({ok, _Group, #{status := disconnected}}, emqx_resource_manager:ets_lookup(?ID)), ok = emqx_resource:remove_local(?ID). @@ -1583,8 +1584,8 @@ do_t_expiration_before_sending(QueryMode) -> spawn_link(fun() -> case QueryMode of sync -> - ?assertError( - timeout, + ?assertMatch( + {error, {resource_error, #{reason := timeout}}}, emqx_resource:query(?ID, {inc_counter, 99}, #{timeout => TimeoutMS}) ); async -> @@ -1690,8 +1691,8 @@ do_t_expiration_before_sending_partial_batch(QueryMode) -> spawn_link(fun() -> case QueryMode of sync -> - ?assertError( - timeout, + ?assertMatch( + {error, {resource_error, #{reason := timeout}}}, emqx_resource:query(?ID, {inc_counter, 199}, #{timeout => TimeoutMS}) ); async -> @@ -2043,8 +2044,8 @@ do_t_expiration_retry(IsBatch) -> ResumeInterval * 2 ), spawn_link(fun() -> - ?assertError( - timeout, + ?assertMatch( + {error, {resource_error, #{reason := timeout}}}, emqx_resource:query( ?ID, {inc_counter, 1}, @@ -2127,8 +2128,8 @@ t_expiration_retry_batch_multiple_times(_Config) -> ), TimeoutMS = 100, spawn_link(fun() -> - ?assertError( - timeout, + ?assertMatch( + {error, {resource_error, #{reason := timeout}}}, emqx_resource:query( ?ID, {inc_counter, 1}, @@ -2137,8 +2138,8 @@ t_expiration_retry_batch_multiple_times(_Config) -> ) end), spawn_link(fun() -> - ?assertError( - timeout, + ?assertMatch( + {error, {resource_error, #{reason := timeout}}}, emqx_resource:query( ?ID, {inc_counter, 2}, diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_influxdb_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_influxdb_SUITE.erl index e1899b1b2..2bac20bce 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_influxdb_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_influxdb_SUITE.erl @@ -910,12 +910,10 @@ t_write_failure(Config) -> sync -> {_, {ok, _}} = ?wait_async_action( - try + ?assertMatch( + {error, {resource_error, #{reason := timeout}}}, send_message(Config, SentData) - catch - error:timeout -> - {error, timeout} - end, + ), #{?snk_kind := buffer_worker_flush_nack}, 1_000 ); diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_mysql_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_mysql_SUITE.erl index 57792b366..fec85c874 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_mysql_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_mysql_SUITE.erl @@ -406,7 +406,10 @@ t_write_failure(Config) -> emqx_common_test_helpers:with_failure(down, ProxyName, ProxyHost, ProxyPort, fun() -> case QueryMode of sync -> - ?assertError(timeout, send_message(Config, SentData)); + ?assertMatch( + {error, {resource_error, #{reason := timeout}}}, + send_message(Config, SentData) + ); async -> send_message(Config, SentData) end @@ -439,8 +442,8 @@ t_write_timeout(Config) -> SentData = #{payload => Val, timestamp => 1668602148000}, Timeout = 1000, emqx_common_test_helpers:with_failure(timeout, ProxyName, ProxyHost, ProxyPort, fun() -> - ?assertError( - timeout, + ?assertMatch( + {error, {resource_error, #{reason := timeout}}}, query_resource(Config, {send_message, SentData, [], Timeout}) ) end), diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_pgsql_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_pgsql_SUITE.erl index 25752f685..6fbb9689f 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_pgsql_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_pgsql_SUITE.erl @@ -426,12 +426,7 @@ t_write_failure(Config) -> ?wait_async_action( case QueryMode of sync -> - try - send_message(Config, SentData) - catch - error:timeout -> - {error, timeout} - end; + ?assertMatch({error, _}, send_message(Config, SentData)); async -> send_message(Config, SentData) end, @@ -467,7 +462,10 @@ t_write_timeout(Config) -> SentData = #{payload => Val, timestamp => 1668602148000}, Timeout = 1000, emqx_common_test_helpers:with_failure(timeout, ProxyName, ProxyHost, ProxyPort, fun() -> - ?assertError(timeout, query_resource(Config, {send_message, SentData, [], Timeout})) + ?assertMatch( + {error, {resource_error, #{reason := timeout}}}, + query_resource(Config, {send_message, SentData, [], Timeout}) + ) end), ok. From 7005b71ddfca5ee1c13a7902984c56b081f0faf7 Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Tue, 24 Jan 2023 16:57:06 +0100 Subject: [PATCH 0332/1802] style: fix typo in comment --- apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl b/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl index a99f06f20..cd5a17184 100644 --- a/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl +++ b/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl @@ -899,7 +899,7 @@ t_mqtt_conn_bridge_egress_reconnect(_) -> ), Payload1 = <<"hello2">>, Payload2 = <<"hello3">>, - %% we need to to it in other processes because it'll block due to + %% We need to do it in other processes because it'll block due to %% the long timeout spawn(fun() -> emqx:publish(emqx_message:make(LocalTopic, Payload1)) end), spawn(fun() -> emqx:publish(emqx_message:make(LocalTopic, Payload2)) end), From 7d18128ba9dbae71b2af3fc9a55fdbc2b812ddca Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Wed, 25 Jan 2023 11:46:52 +0100 Subject: [PATCH 0333/1802] test: async write can return noproc --- lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_influxdb_SUITE.erl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_influxdb_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_influxdb_SUITE.erl index 2bac20bce..cd7f848c2 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_influxdb_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_influxdb_SUITE.erl @@ -945,7 +945,8 @@ t_write_failure(Config) -> {error, {recoverable_error, {closed, "The connection was lost."}}} =:= Result orelse {error, {error, closed}} =:= Result orelse - {error, {recoverable_error, econnrefused}} =:= Result, + {error, {recoverable_error, econnrefused}} =:= Result orelse + {error, {recoverable_error, noproc}} =:= Result, #{got => Result} ) end, From 30a8a436b4019719764ccfeb3c237624f2ced0dd Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Fri, 27 Jan 2023 11:41:24 +0100 Subject: [PATCH 0334/1802] fix(influxdb_bridge): no need to create atom for pool name --- lib-ee/emqx_ee_connector/rebar.config | 2 +- lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib-ee/emqx_ee_connector/rebar.config b/lib-ee/emqx_ee_connector/rebar.config index 3af1868c7..00421e4f6 100644 --- a/lib-ee/emqx_ee_connector/rebar.config +++ b/lib-ee/emqx_ee_connector/rebar.config @@ -1,7 +1,7 @@ {erl_opts, [debug_info]}. {deps, [ {hstreamdb_erl, {git, "https://github.com/hstreamdb/hstreamdb_erl.git", {tag, "0.2.5"}}}, - {influxdb, {git, "https://github.com/emqx/influxdb-client-erl", {tag, "1.1.7"}}}, + {influxdb, {git, "https://github.com/emqx/influxdb-client-erl", {tag, "1.1.8"}}}, {emqx, {path, "../../apps/emqx"}} ]}. diff --git a/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl b/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl index 0ee27c5c4..fa1ce1090 100644 --- a/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl +++ b/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl @@ -282,7 +282,7 @@ client_config( {host, str(Host)}, {port, Port}, {pool_size, erlang:system_info(schedulers)}, - {pool, binary_to_atom(InstId, utf8)}, + {pool, InstId}, {precision, atom_to_binary(maps:get(precision, Config, ms), utf8)} ] ++ protocol_config(Config). From d53106145f9422c2cbd966ddbb3fdb4bc9451b7c Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Fri, 27 Jan 2023 12:15:07 +0100 Subject: [PATCH 0335/1802] fix: stop resource when resource manager terminates --- apps/emqx_resource/src/emqx_resource_manager.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/emqx_resource/src/emqx_resource_manager.erl b/apps/emqx_resource/src/emqx_resource_manager.erl index 8098dbe42..d2cb74437 100644 --- a/apps/emqx_resource/src/emqx_resource_manager.erl +++ b/apps/emqx_resource/src/emqx_resource_manager.erl @@ -309,6 +309,7 @@ init({Data, Opts}) -> end. terminate(_Reason, _State, Data) -> + _ = stop_resource(Data), _ = maybe_clear_alarm(Data#data.id), delete_cache(Data#data.id, Data#data.manager_id), ok. @@ -334,8 +335,7 @@ handle_event({call, From}, start, _State, _Data) -> % Called when the resource received a `quit` message handle_event(info, quit, stopped, _Data) -> {stop, {shutdown, quit}}; -handle_event(info, quit, _State, Data) -> - _ = stop_resource(Data), +handle_event(info, quit, _State, _Data) -> {stop, {shutdown, quit}}; % Called when the resource is to be stopped handle_event({call, From}, stop, stopped, _Data) -> From c47be57c596ae7e1d65bc22af4485f485545ee6e Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Fri, 27 Jan 2023 12:38:00 +0100 Subject: [PATCH 0336/1802] fix(bridge): ensure all bridge resources are stopped before app stop --- apps/emqx_bridge/src/emqx_bridge.erl | 16 ++++++++++++++++ apps/emqx_bridge/src/emqx_bridge_app.erl | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/apps/emqx_bridge/src/emqx_bridge.erl b/apps/emqx_bridge/src/emqx_bridge.erl index fb199522d..a25963190 100644 --- a/apps/emqx_bridge/src/emqx_bridge.erl +++ b/apps/emqx_bridge/src/emqx_bridge.erl @@ -31,6 +31,7 @@ -export([ load/0, + unload/0, lookup/1, lookup/2, lookup/3, @@ -75,6 +76,21 @@ load() -> maps:to_list(Bridges) ). +unload() -> + unload_hook(), + Bridges = emqx:get_config([bridges], #{}), + lists:foreach( + fun({Type, NamedConf}) -> + lists:foreach( + fun({Name, _Conf}) -> + _ = emqx_bridge_resource:stop(Type, Name) + end, + maps:to_list(NamedConf) + ) + end, + maps:to_list(Bridges) + ). + safe_load_bridge(Type, Name, Conf, Opts) -> try _Res = emqx_bridge_resource:create(Type, Name, Conf, Opts), diff --git a/apps/emqx_bridge/src/emqx_bridge_app.erl b/apps/emqx_bridge/src/emqx_bridge_app.erl index e10034bae..daae15a17 100644 --- a/apps/emqx_bridge/src/emqx_bridge_app.erl +++ b/apps/emqx_bridge/src/emqx_bridge_app.erl @@ -39,7 +39,7 @@ start(_StartType, _StartArgs) -> stop(_State) -> emqx_conf:remove_handler(?LEAF_NODE_HDLR_PATH), emqx_conf:remove_handler(?TOP_LELVE_HDLR_PATH), - ok = emqx_bridge:unload_hook(), + ok = emqx_bridge:unload(), ok. -if(?EMQX_RELEASE_EDITION == ee). From 6a58bafcb02f6671e3001abf6bfeea48157dbb6c Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Fri, 27 Jan 2023 14:38:21 +0100 Subject: [PATCH 0337/1802] chore: bump release version to e5.0.0-rc.2 --- apps/emqx/include/emqx_release.hrl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx/include/emqx_release.hrl b/apps/emqx/include/emqx_release.hrl index 7437bc299..d3e0cd3b0 100644 --- a/apps/emqx/include/emqx_release.hrl +++ b/apps/emqx/include/emqx_release.hrl @@ -35,7 +35,7 @@ -define(EMQX_RELEASE_CE, "5.0.15"). %% Enterprise edition --define(EMQX_RELEASE_EE, "5.0.0-rc.1"). +-define(EMQX_RELEASE_EE, "5.0.0-rc.2"). %% the HTTP API version -define(EMQX_API_VERSION, "5.0"). From 262c3a286954ca20923ff0db97b64c68f06cf7ba Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Fri, 27 Jan 2023 15:03:18 +0100 Subject: [PATCH 0338/1802] refactor(buffer_worker): rename function from reply_after_query to handle_async_reply --- apps/emqx_resource/src/emqx_resource_buffer_worker.erl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/emqx_resource/src/emqx_resource_buffer_worker.erl b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl index 50534df4f..58c835533 100644 --- a/apps/emqx_resource/src/emqx_resource_buffer_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl @@ -52,7 +52,7 @@ -export([queue_item_marshaller/1, estimate_size/1]). --export([reply_after_query/8, batch_reply_after_query/8]). +-export([handle_async_reply/8, batch_reply_after_query/8]). -export([clear_disk_queue_dir/2]). @@ -898,7 +898,7 @@ apply_query_fun(async, Mod, Id, Index, Ref, ?QUERY(_, Request, _, _) = Query, Re ?APPLY_RESOURCE( call_query_async, begin - ReplyFun = fun ?MODULE:reply_after_query/8, + ReplyFun = fun ?MODULE:handle_async_reply/8, Args = [self(), Id, Index, InflightTID, Ref, Query, QueryOpts], IsRetriable = false, WorkerMRef = undefined, @@ -936,7 +936,7 @@ apply_query_fun(async, Mod, Id, Index, Ref, [?QUERY(_, _, _, _) | _] = Batch, Re Batch ). -reply_after_query( +handle_async_reply( Pid, Id, Index, From f793807bc1fd9e0216ee09f61bd863e5c8306086 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Fri, 27 Jan 2023 15:04:28 +0100 Subject: [PATCH 0339/1802] refactor(buffer_worker): rename function batch_reply_after_query to handle_async_batch_reply --- apps/emqx_resource/src/emqx_resource_buffer_worker.erl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/emqx_resource/src/emqx_resource_buffer_worker.erl b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl index 58c835533..1b837880e 100644 --- a/apps/emqx_resource/src/emqx_resource_buffer_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl @@ -52,7 +52,7 @@ -export([queue_item_marshaller/1, estimate_size/1]). --export([handle_async_reply/8, batch_reply_after_query/8]). +-export([handle_async_reply/8, handle_async_batch_reply/8]). -export([clear_disk_queue_dir/2]). @@ -923,7 +923,7 @@ apply_query_fun(async, Mod, Id, Index, Ref, [?QUERY(_, _, _, _) | _] = Batch, Re ?APPLY_RESOURCE( call_batch_query_async, begin - ReplyFun = fun ?MODULE:batch_reply_after_query/8, + ReplyFun = fun ?MODULE:handle_async_batch_reply/8, ReplyFunAndArgs = {ReplyFun, [self(), Id, Index, InflightTID, Ref, Batch, QueryOpts]}, Requests = [Request || ?QUERY(_ReplyTo, Request, _, _ExpireAt) <- Batch], IsRetriable = false, @@ -1000,7 +1000,7 @@ do_reply_after_query( do_ack(InflightTID, Ref, Id, Index, PostFn, Pid, QueryOpts) end. -batch_reply_after_query(Pid, Id, Index, InflightTID, Ref, Batch, QueryOpts, Result) -> +handle_async_batch_reply(Pid, Id, Index, InflightTID, Ref, Batch, QueryOpts, Result) -> ?tp( buffer_worker_reply_after_query_enter, #{batch_or_query => Batch, ref => Ref} From 578271ea3d8a3b3b3230cd0bbbe2a4b7294232b6 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Fri, 27 Jan 2023 15:15:46 +0100 Subject: [PATCH 0340/1802] refactor: use lists:map instead of lc for safty --- apps/emqx_resource/src/emqx_resource_buffer_worker.erl | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/emqx_resource/src/emqx_resource_buffer_worker.erl b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl index 1b837880e..026effcf8 100644 --- a/apps/emqx_resource/src/emqx_resource_buffer_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl @@ -73,9 +73,8 @@ -type id() :: binary(). -type index() :: pos_integer(). -type expire_at() :: infinity | integer(). --type queue_query() :: ?QUERY(from(), request(), HasBeenSent :: boolean(), expire_at()). +-type queue_query() :: ?QUERY(reply_fun(), request(), HasBeenSent :: boolean(), expire_at()). -type request() :: term(). --type from() :: pid() | reply_fun() | request_from(). -type request_from() :: undefined | gen_statem:from(). -type state() :: blocked | running. -type inflight_key() :: integer(). @@ -913,7 +912,7 @@ apply_query_fun(sync, Mod, Id, _Index, _Ref, [?QUERY(_, _, _, _) | _] = Batch, R ?tp(call_batch_query, #{ id => Id, mod => Mod, batch => Batch, res_st => ResSt, call_mode => sync }), - Requests = [Request || ?QUERY(_ReplyTo, Request, _, _ExpireAt) <- Batch], + Requests = lists:map(fun(?QUERY(_ReplyTo, Request, _, _ExpireAt)) -> Request end, Batch), ?APPLY_RESOURCE(call_batch_query, Mod:on_batch_query(Id, Requests, ResSt), Batch); apply_query_fun(async, Mod, Id, Index, Ref, [?QUERY(_, _, _, _) | _] = Batch, ResSt, QueryOpts) -> ?tp(call_batch_query_async, #{ @@ -925,7 +924,9 @@ apply_query_fun(async, Mod, Id, Index, Ref, [?QUERY(_, _, _, _) | _] = Batch, Re begin ReplyFun = fun ?MODULE:handle_async_batch_reply/8, ReplyFunAndArgs = {ReplyFun, [self(), Id, Index, InflightTID, Ref, Batch, QueryOpts]}, - Requests = [Request || ?QUERY(_ReplyTo, Request, _, _ExpireAt) <- Batch], + Requests = lists:map( + fun(?QUERY(_ReplyTo, Request, _, _ExpireAt)) -> Request end, Batch + ), IsRetriable = false, WorkerMRef = undefined, InflightItem = ?INFLIGHT_ITEM(Ref, Batch, IsRetriable, WorkerMRef), From 72f39b9b728bd8dbd4ddba8279039cf8a29c5b3d Mon Sep 17 00:00:00 2001 From: Ilya Averyanov Date: Fri, 27 Jan 2023 17:39:16 +0200 Subject: [PATCH 0341/1802] fix(docs): correct Redis conf field description --- apps/emqx_connector/i18n/emqx_connector_redis.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx_connector/i18n/emqx_connector_redis.conf b/apps/emqx_connector/i18n/emqx_connector_redis.conf index e8e05d08f..e42e73498 100644 --- a/apps/emqx_connector/i18n/emqx_connector_redis.conf +++ b/apps/emqx_connector/i18n/emqx_connector_redis.conf @@ -69,7 +69,7 @@ The Redis default port 6379 is used if `[:Port]` is not specified. A Node list for Cluster to connect to. The nodes should be separated with commas, such as: `Node[,Node].` For each Node should be: The IPv4 or IPv6 address or the hostname to connect to. A host entry has the following form: `Host[:Port]`. -The MongoDB default port 27017 is used if `[:Port]` is not specified. +The Redis default port 6379 is used if `[:Port]` is not specified. """ zh: """ From fc38ea9571337c9ba7bf3e72c423f4f7bd1e1c6f Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Fri, 27 Jan 2023 17:12:55 +0100 Subject: [PATCH 0342/1802] refactor(buffer_worker): do not keep request body in reply context the request body can be potentially very large the reply context is sent to the async call handler and kept in its memory until the async reply is received from bridge target service. this commit tries to minimize the size of the reply context by replacing the request body with `[]`. --- .../src/emqx_resource_buffer_worker.erl | 161 +++++++++++------- .../test/emqx_resource_SUITE.erl | 2 +- 2 files changed, 102 insertions(+), 61 deletions(-) diff --git a/apps/emqx_resource/src/emqx_resource_buffer_worker.erl b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl index 026effcf8..355fb276c 100644 --- a/apps/emqx_resource/src/emqx_resource_buffer_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl @@ -52,7 +52,7 @@ -export([queue_item_marshaller/1, estimate_size/1]). --export([handle_async_reply/8, handle_async_batch_reply/8]). +-export([handle_async_reply/2, handle_async_batch_reply/2]). -export([clear_disk_queue_dir/2]). @@ -124,7 +124,7 @@ simple_sync_query(Id, Request) -> Index = undefined, QueryOpts = simple_query_opts(), emqx_resource_metrics:matched_inc(Id), - Ref = make_message_ref(), + Ref = make_request_ref(), Result = call_query(sync, Id, Index, Ref, ?SIMPLE_QUERY(Request), QueryOpts), _ = handle_query_result(Id, Result, _HasBeenSent = false), Result. @@ -135,7 +135,7 @@ simple_async_query(Id, Request) -> Index = undefined, QueryOpts = simple_query_opts(), emqx_resource_metrics:matched_inc(Id), - Ref = make_message_ref(), + Ref = make_request_ref(), Result = call_query(async, Id, Index, Ref, ?SIMPLE_QUERY(Request), QueryOpts), _ = handle_query_result(Id, Result, _HasBeenSent = false), Result. @@ -511,7 +511,7 @@ flush(Data0) -> buffer_worker_flush_potentially_partial, #{expired => Expired, not_expired => NotExpired} ), - Ref = make_message_ref(), + Ref = make_request_ref(), do_flush(Data2, #{ new_queue => Q1, is_batch => IsBatch, @@ -897,13 +897,21 @@ apply_query_fun(async, Mod, Id, Index, Ref, ?QUERY(_, Request, _, _) = Query, Re ?APPLY_RESOURCE( call_query_async, begin - ReplyFun = fun ?MODULE:handle_async_reply/8, - Args = [self(), Id, Index, InflightTID, Ref, Query, QueryOpts], + ReplyFun = fun ?MODULE:handle_async_reply/2, + ReplyContext = #{ + buffer_worker => self(), + resource_id => Id, + worker_index => Index, + inflight_tid => InflightTID, + request_ref => Ref, + query_opts => QueryOpts, + query => minimize(Query) + }, IsRetriable = false, WorkerMRef = undefined, InflightItem = ?INFLIGHT_ITEM(Ref, Query, IsRetriable, WorkerMRef), ok = inflight_append(InflightTID, InflightItem, Id, Index), - Result = Mod:on_query_async(Id, Request, {ReplyFun, Args}, ResSt), + Result = Mod:on_query_async(Id, Request, {ReplyFun, [ReplyContext]}, ResSt), {async_return, Result} end, Request @@ -922,8 +930,16 @@ apply_query_fun(async, Mod, Id, Index, Ref, [?QUERY(_, _, _, _) | _] = Batch, Re ?APPLY_RESOURCE( call_batch_query_async, begin - ReplyFun = fun ?MODULE:handle_async_batch_reply/8, - ReplyFunAndArgs = {ReplyFun, [self(), Id, Index, InflightTID, Ref, Batch, QueryOpts]}, + ReplyFun = fun ?MODULE:handle_async_batch_reply/2, + ReplyContext = #{ + buffer_worker => self(), + resource_id => Id, + worker_index => Index, + inflight_tid => InflightTID, + request_ref => Ref, + query_opts => QueryOpts, + batch => minimize(Batch) + }, Requests = lists:map( fun(?QUERY(_ReplyTo, Request, _, _ExpireAt)) -> Request end, Batch ), @@ -931,20 +947,21 @@ apply_query_fun(async, Mod, Id, Index, Ref, [?QUERY(_, _, _, _) | _] = Batch, Re WorkerMRef = undefined, InflightItem = ?INFLIGHT_ITEM(Ref, Batch, IsRetriable, WorkerMRef), ok = inflight_append(InflightTID, InflightItem, Id, Index), - Result = Mod:on_batch_query_async(Id, Requests, ReplyFunAndArgs, ResSt), + Result = Mod:on_batch_query_async(Id, Requests, {ReplyFun, [ReplyContext]}, ResSt), {async_return, Result} end, Batch ). handle_async_reply( - Pid, - Id, - Index, - InflightTID, - Ref, - ?QUERY(_ReplyTo, _Request, _HasBeenSent, ExpireAt) = Query, - QueryOpts, + #{ + request_ref := Ref, + inflight_tid := InflightTID, + resource_id := Id, + worker_index := Index, + buffer_worker := Pid, + query := ?QUERY(_, _, _, ExpireAt) = Query + } = ReplyContext, Result ) -> ?tp( @@ -961,47 +978,55 @@ handle_async_reply( ?tp(buffer_worker_reply_after_query_expired, #{expired => [Query]}), ok; false -> - do_reply_after_query(Pid, Id, Index, InflightTID, Ref, Query, QueryOpts, Result) + do_handle_async_reply(ReplyContext, Result) end. -do_reply_after_query( - Pid, - Id, - Index, - InflightTID, - Ref, - ?QUERY(ReplyTo, _Request, HasBeenSent, _ExpireAt), - QueryOpts, +do_handle_async_reply( + #{ + query_opts := QueryOpts, + resource_id := Id, + request_ref := Ref, + worker_index := Index, + buffer_worker := Pid, + inflight_tid := InflightTID, + query := ?QUERY(ReplyTo, _, Sent, _ExpireAt) = Query + }, Result ) -> %% NOTE: 'inflight' is the count of messages that were sent async %% but received no ACK, NOT the number of messages queued in the %% inflight window. {Action, PostFn} = reply_caller_defer_metrics( - Id, ?REPLY(ReplyTo, HasBeenSent, Result), QueryOpts + Id, ?REPLY(ReplyTo, Sent, Result), QueryOpts ), + + ?tp(buffer_worker_reply_after_query, #{ + action => Action, + batch_or_query => [Query], + ref => Ref, + result => Result + }), + case Action of nack -> %% Keep retrying. - ?tp(buffer_worker_reply_after_query, #{ - action => Action, - batch_or_query => ?QUERY(ReplyTo, _Request, HasBeenSent, _ExpireAt), - ref => Ref, - result => Result - }), mark_inflight_as_retriable(InflightTID, Ref), ?MODULE:block(Pid); ack -> - ?tp(buffer_worker_reply_after_query, #{ - action => Action, - batch_or_query => ?QUERY(ReplyTo, _Request, HasBeenSent, _ExpireAt), - ref => Ref, - result => Result - }), do_ack(InflightTID, Ref, Id, Index, PostFn, Pid, QueryOpts) end. -handle_async_batch_reply(Pid, Id, Index, InflightTID, Ref, Batch, QueryOpts, Result) -> +handle_async_batch_reply( + #{ + buffer_worker := Pid, + resource_id := Id, + worker_index := Index, + inflight_tid := InflightTID, + request_ref := Ref, + batch := Batch + } = ReplyContext, + Result +) -> ?tp( buffer_worker_reply_after_query_enter, #{batch_or_query => Batch, ref => Ref} @@ -1020,12 +1045,21 @@ handle_async_batch_reply(Pid, Id, Index, InflightTID, Ref, Batch, QueryOpts, Res emqx_resource_metrics:late_reply_inc(Id, NumExpired), NumExpired > 0 andalso ?tp(buffer_worker_reply_after_query_expired, #{expired => Expired}), - do_batch_reply_after_query( - Pid, Id, Index, InflightTID, Ref, NotExpired, QueryOpts, Result - ) + do_handle_async_batch_reply(ReplyContext#{batch := NotExpired}, Result) end. -do_batch_reply_after_query(Pid, Id, Index, InflightTID, Ref, Batch, QueryOpts, Result) -> +do_handle_async_batch_reply( + #{ + buffer_worker := Pid, + resource_id := Id, + worker_index := Index, + inflight_tid := InflightTID, + request_ref := Ref, + batch := Batch, + query_opts := QueryOpts + }, + Result +) -> ?tp( buffer_worker_reply_after_query_enter, #{batch_or_query => Batch, ref => Ref} @@ -1034,24 +1068,18 @@ do_batch_reply_after_query(Pid, Id, Index, InflightTID, Ref, Batch, QueryOpts, R %% but received no ACK, NOT the number of messages queued in the %% inflight window. {Action, PostFn} = batch_reply_caller_defer_metrics(Id, Result, Batch, QueryOpts), + ?tp(buffer_worker_reply_after_query, #{ + action => Action, + batch_or_query => Batch, + ref => Ref, + result => Result + }), case Action of nack -> %% Keep retrying. - ?tp(buffer_worker_reply_after_query, #{ - action => nack, - batch_or_query => Batch, - ref => Ref, - result => Result - }), mark_inflight_as_retriable(InflightTID, Ref), ?MODULE:block(Pid); ack -> - ?tp(buffer_worker_reply_after_query, #{ - action => ack, - batch_or_query => Batch, - ref => Ref, - result => Result - }), do_ack(InflightTID, Ref, Id, Index, PostFn, Pid, QueryOpts) end. @@ -1098,7 +1126,8 @@ append_queue(Id, Index, Q, Queries) -> emqx_resource_metrics:dropped_queue_full_inc(Id), ?SLOG(info, #{ msg => buffer_worker_overflow, - worker_id => Id, + resource_id => Id, + worker_index => Index, dropped => Dropped }), {Items2, Q1} @@ -1133,7 +1162,7 @@ inflight_new(InfltWinSZ, Id, Index) -> inflight_append(TableId, {?SIZE_REF, 0}, Id, Index), inflight_append(TableId, {?INITIAL_TIME_REF, erlang:system_time()}, Id, Index), inflight_append( - TableId, {?INITIAL_MONOTONIC_TIME_REF, make_message_ref()}, Id, Index + TableId, {?INITIAL_MONOTONIC_TIME_REF, make_request_ref()}, Id, Index ), TableId. @@ -1372,8 +1401,8 @@ cancel_flush_timer(St = #{tref := {TRef, _Ref}}) -> _ = erlang:cancel_timer(TRef), St#{tref => undefined}. --spec make_message_ref() -> inflight_key(). -make_message_ref() -> +-spec make_request_ref() -> inflight_key(). +make_request_ref() -> now_(). collect_requests(Acc, Limit) -> @@ -1459,3 +1488,15 @@ ensure_expire_at(#{timeout := TimeoutMS} = Opts) -> TimeoutNS = erlang:convert_time_unit(TimeoutMS, millisecond, nanosecond), ExpireAt = now_() + TimeoutNS, Opts#{expire_at => ExpireAt}. + +%% no need to keep the request for async reply handler +minimize(?QUERY(_, _, _, _) = Q) -> + do_minimize(Q); +minimize(L) when is_list(L) -> + lists:map(fun do_minimize/1, L). + +-ifdef(TEST). +do_minimize(?QUERY(_ReplyTo, _Req, _Sent, _ExpireAt) = Query) -> Query. +-else. +do_minimize(?QUERY(ReplyTo, _Req, Sent, ExpireAt)) -> ?QUERY(ReplyTo, [], Sent, ExpireAt). +-endif. diff --git a/apps/emqx_resource/test/emqx_resource_SUITE.erl b/apps/emqx_resource/test/emqx_resource_SUITE.erl index 227b6fedc..fc201e048 100644 --- a/apps/emqx_resource/test/emqx_resource_SUITE.erl +++ b/apps/emqx_resource/test/emqx_resource_SUITE.erl @@ -2335,7 +2335,7 @@ assert_async_retry_fail_then_succeed_inflight(Trace) -> ct:pal(" ~p", [Trace]), ?assert( ?strict_causality( - #{?snk_kind := buffer_worker_reply_after_query, action := nack, ref := _Ref}, + #{?snk_kind := buffer_worker_reply_after_query, action := nack}, #{?snk_kind := buffer_worker_retry_inflight_failed, ref := _Ref}, Trace ) From 940b238ad9da3dfa0e012c4bad98fdaf62ce958b Mon Sep 17 00:00:00 2001 From: Antoine Bertin Date: Sun, 22 Jan 2023 18:00:15 +0100 Subject: [PATCH 0343/1802] fix: add clusterDomain in values --- deploy/charts/emqx/templates/configmap.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deploy/charts/emqx/templates/configmap.yaml b/deploy/charts/emqx/templates/configmap.yaml index 5086f85f6..6bd815ca0 100644 --- a/deploy/charts/emqx/templates/configmap.yaml +++ b/deploy/charts/emqx/templates/configmap.yaml @@ -16,9 +16,9 @@ data: EMQX_CLUSTER__K8S__SERVICE_NAME: {{ include "emqx.fullname" . }}-headless EMQX_CLUSTER__K8S__NAMESPACE: {{ .Release.Namespace }} EMQX_CLUSTER__K8S__ADDRESS_TYPE: "hostname" - EMQX_CLUSTER__K8S__SUFFIX: "svc.cluster.local" + EMQX_CLUSTER__K8S__SUFFIX: "svc.{{ .Values.clusterDomain }}" {{- else if eq (.Values.emqxConfig.EMQX_CLUSTER__DISCOVERY_STRATEGY) "dns" }} - EMQX_CLUSTER__DNS__NAME: "{{ include "emqx.fullname" . }}-headless.{{ .Release.Namespace }}.svc.cluster.local" + EMQX_CLUSTER__DNS__NAME: "{{ include "emqx.fullname" . }}-headless.{{ .Release.Namespace }}.svc.{{ .Values.clusterDomain }}" EMQX_CLUSTER__DNS__RECORD_TYPE: "srv" {{- end -}} {{- range $index, $value := .Values.emqxConfig }} From 297d33dbad4a2a8dded59b1633f0bf79de6a38eb Mon Sep 17 00:00:00 2001 From: Antoine Bertin Date: Sun, 22 Jan 2023 18:05:12 +0100 Subject: [PATCH 0344/1802] feat: add clusterDomain value --- deploy/charts/emqx/values.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/deploy/charts/emqx/values.yaml b/deploy/charts/emqx/values.yaml index 0423c8cdf..c737c8808 100644 --- a/deploy/charts/emqx/values.yaml +++ b/deploy/charts/emqx/values.yaml @@ -35,6 +35,8 @@ serviceAccount: ## Forces the recreation of pods during helm upgrades. This can be useful to update configuration values even if the container image did not change. recreatePods: false +clusterDomain: cluster.local + podAnnotations: {} # Pod deployment policy From 81e7f26ec6173bc1b42cdbf5213c32e5adabd8f0 Mon Sep 17 00:00:00 2001 From: Antoine Bertin Date: Fri, 27 Jan 2023 23:44:58 +0100 Subject: [PATCH 0345/1802] fix: add clusterDomain to enterprise chart --- deploy/charts/emqx-enterprise/templates/configmap.yaml | 4 ++-- deploy/charts/emqx-enterprise/values.yaml | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/deploy/charts/emqx-enterprise/templates/configmap.yaml b/deploy/charts/emqx-enterprise/templates/configmap.yaml index 5086f85f6..6bd815ca0 100644 --- a/deploy/charts/emqx-enterprise/templates/configmap.yaml +++ b/deploy/charts/emqx-enterprise/templates/configmap.yaml @@ -16,9 +16,9 @@ data: EMQX_CLUSTER__K8S__SERVICE_NAME: {{ include "emqx.fullname" . }}-headless EMQX_CLUSTER__K8S__NAMESPACE: {{ .Release.Namespace }} EMQX_CLUSTER__K8S__ADDRESS_TYPE: "hostname" - EMQX_CLUSTER__K8S__SUFFIX: "svc.cluster.local" + EMQX_CLUSTER__K8S__SUFFIX: "svc.{{ .Values.clusterDomain }}" {{- else if eq (.Values.emqxConfig.EMQX_CLUSTER__DISCOVERY_STRATEGY) "dns" }} - EMQX_CLUSTER__DNS__NAME: "{{ include "emqx.fullname" . }}-headless.{{ .Release.Namespace }}.svc.cluster.local" + EMQX_CLUSTER__DNS__NAME: "{{ include "emqx.fullname" . }}-headless.{{ .Release.Namespace }}.svc.{{ .Values.clusterDomain }}" EMQX_CLUSTER__DNS__RECORD_TYPE: "srv" {{- end -}} {{- range $index, $value := .Values.emqxConfig }} diff --git a/deploy/charts/emqx-enterprise/values.yaml b/deploy/charts/emqx-enterprise/values.yaml index b9507c5a0..3a607a71e 100644 --- a/deploy/charts/emqx-enterprise/values.yaml +++ b/deploy/charts/emqx-enterprise/values.yaml @@ -35,6 +35,8 @@ serviceAccount: ## Forces the recreation of pods during helm upgrades. This can be useful to update configuration values even if the container image did not change. recreatePods: false +clusterDomain: cluster.local + podAnnotations: {} # Pod deployment policy From d47941601d1d78cc4fe345299cf9a9df60c2fd83 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Fri, 27 Jan 2023 17:27:27 +0100 Subject: [PATCH 0346/1802] refactor(buffer_worker): rename trace points --- .../src/emqx_resource_buffer_worker.erl | 29 +++++++------------ .../test/emqx_resource_SUITE.erl | 16 +++++----- .../kafka/emqx_bridge_impl_kafka_producer.erl | 2 +- .../test/emqx_ee_bridge_influxdb_SUITE.erl | 4 +-- 4 files changed, 22 insertions(+), 29 deletions(-) diff --git a/apps/emqx_resource/src/emqx_resource_buffer_worker.erl b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl index 355fb276c..0ac627d29 100644 --- a/apps/emqx_resource/src/emqx_resource_buffer_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl @@ -960,13 +960,13 @@ handle_async_reply( resource_id := Id, worker_index := Index, buffer_worker := Pid, - query := ?QUERY(_, _, _, ExpireAt) = Query + query := ?QUERY(_, _, _, ExpireAt) = _Query } = ReplyContext, Result ) -> ?tp( - buffer_worker_reply_after_query_enter, - #{batch_or_query => [Query], ref => Ref} + handle_async_reply_enter, + #{batch_or_query => [_Query], ref => Ref} ), Now = now_(), case is_expired(ExpireAt, Now) of @@ -975,7 +975,7 @@ handle_async_reply( IsAcked = ack_inflight(InflightTID, Ref, Id, Index), IsAcked andalso emqx_resource_metrics:late_reply_inc(Id), IsFullBefore andalso ?MODULE:flush_worker(Pid), - ?tp(buffer_worker_reply_after_query_expired, #{expired => [Query]}), + ?tp(handle_async_reply_expired, #{expired => [_Query]}), ok; false -> do_handle_async_reply(ReplyContext, Result) @@ -989,7 +989,7 @@ do_handle_async_reply( worker_index := Index, buffer_worker := Pid, inflight_tid := InflightTID, - query := ?QUERY(ReplyTo, _, Sent, _ExpireAt) = Query + query := ?QUERY(ReplyTo, _, Sent, _ExpireAt) = _Query }, Result ) -> @@ -1000,9 +1000,9 @@ do_handle_async_reply( Id, ?REPLY(ReplyTo, Sent, Result), QueryOpts ), - ?tp(buffer_worker_reply_after_query, #{ + ?tp(handle_async_reply, #{ action => Action, - batch_or_query => [Query], + batch_or_query => [_Query], ref => Ref, result => Result }), @@ -1028,7 +1028,7 @@ handle_async_batch_reply( Result ) -> ?tp( - buffer_worker_reply_after_query_enter, + handle_async_reply_enter, #{batch_or_query => Batch, ref => Ref} ), Now = now_(), @@ -1038,13 +1038,13 @@ handle_async_batch_reply( IsAcked = ack_inflight(InflightTID, Ref, Id, Index), IsAcked andalso emqx_resource_metrics:late_reply_inc(Id), IsFullBefore andalso ?MODULE:flush_worker(Pid), - ?tp(buffer_worker_reply_after_query_expired, #{expired => Batch}), + ?tp(handle_async_reply_expired, #{expired => Batch}), ok; {NotExpired, Expired} -> NumExpired = length(Expired), emqx_resource_metrics:late_reply_inc(Id, NumExpired), NumExpired > 0 andalso - ?tp(buffer_worker_reply_after_query_expired, #{expired => Expired}), + ?tp(handle_async_reply_expired, #{expired => Expired}), do_handle_async_batch_reply(ReplyContext#{batch := NotExpired}, Result) end. @@ -1060,15 +1060,8 @@ do_handle_async_batch_reply( }, Result ) -> - ?tp( - buffer_worker_reply_after_query_enter, - #{batch_or_query => Batch, ref => Ref} - ), - %% NOTE: 'inflight' is the count of messages that were sent async - %% but received no ACK, NOT the number of messages queued in the - %% inflight window. {Action, PostFn} = batch_reply_caller_defer_metrics(Id, Result, Batch, QueryOpts), - ?tp(buffer_worker_reply_after_query, #{ + ?tp(handle_async_reply, #{ action => Action, batch_or_query => Batch, ref => Ref, diff --git a/apps/emqx_resource/test/emqx_resource_SUITE.erl b/apps/emqx_resource/test/emqx_resource_SUITE.erl index fc201e048..86336a38f 100644 --- a/apps/emqx_resource/test/emqx_resource_SUITE.erl +++ b/apps/emqx_resource/test/emqx_resource_SUITE.erl @@ -1718,7 +1718,7 @@ do_t_expiration_before_sending_partial_batch(QueryMode) -> async -> {ok, _} = ?block_until( #{ - ?snk_kind := buffer_worker_reply_after_query, + ?snk_kind := handle_async_reply, action := ack, batch_or_query := [{query, _, {inc_counter, 99}, _, _}] }, @@ -1849,7 +1849,7 @@ do_t_expiration_async_after_reply(IsBatch) -> ?force_ordering( #{?snk_kind := delay}, #{ - ?snk_kind := buffer_worker_reply_after_query_enter, + ?snk_kind := handle_async_reply_enter, batch_or_query := [{query, _, {inc_counter, 199}, _, _} | _] } ), @@ -1874,7 +1874,7 @@ do_t_expiration_async_after_reply(IsBatch) -> #{?snk_kind := buffer_worker_flush_potentially_partial}, 4 * TimeoutMS ), {ok, _} = ?block_until( - #{?snk_kind := buffer_worker_reply_after_query_expired}, 10 * TimeoutMS + #{?snk_kind := handle_async_reply_expired}, 10 * TimeoutMS ), unlink(Pid0), @@ -1888,7 +1888,7 @@ do_t_expiration_async_after_reply(IsBatch) -> expired := [{query, _, {inc_counter, 199}, _, _}] } ], - ?of_kind(buffer_worker_reply_after_query_expired, Trace) + ?of_kind(handle_async_reply_expired, Trace) ), wait_telemetry_event(success, #{n_events => 1, timeout => 4_000}), Metrics = tap_metrics(?LINE), @@ -1936,7 +1936,7 @@ t_expiration_batch_all_expired_after_reply(_Config) -> ?force_ordering( #{?snk_kind := delay}, #{ - ?snk_kind := buffer_worker_reply_after_query_enter, + ?snk_kind := handle_async_reply_enter, batch_or_query := [{query, _, {inc_counter, 199}, _, _} | _] } ), @@ -1955,7 +1955,7 @@ t_expiration_batch_all_expired_after_reply(_Config) -> end), {ok, _} = ?block_until( - #{?snk_kind := buffer_worker_reply_after_query_expired}, 10 * TimeoutMS + #{?snk_kind := handle_async_reply_expired}, 10 * TimeoutMS ), unlink(Pid0), @@ -1969,7 +1969,7 @@ t_expiration_batch_all_expired_after_reply(_Config) -> expired := [{query, _, {inc_counter, 199}, _, _}] } ], - ?of_kind(buffer_worker_reply_after_query_expired, Trace) + ?of_kind(handle_async_reply_expired, Trace) ), Metrics = tap_metrics(?LINE), ?assertMatch( @@ -2335,7 +2335,7 @@ assert_async_retry_fail_then_succeed_inflight(Trace) -> ct:pal(" ~p", [Trace]), ?assert( ?strict_causality( - #{?snk_kind := buffer_worker_reply_after_query, action := nack}, + #{?snk_kind := handle_async_reply, action := nack}, #{?snk_kind := buffer_worker_retry_inflight_failed, ref := _Ref}, Trace ) diff --git a/lib-ee/emqx_ee_bridge/src/kafka/emqx_bridge_impl_kafka_producer.erl b/lib-ee/emqx_ee_bridge/src/kafka/emqx_bridge_impl_kafka_producer.erl index 18e27b775..1ac619626 100644 --- a/lib-ee/emqx_ee_bridge/src/kafka/emqx_bridge_impl_kafka_producer.erl +++ b/lib-ee/emqx_ee_bridge/src/kafka/emqx_bridge_impl_kafka_producer.erl @@ -227,7 +227,7 @@ render_timestamp(Template, Message) -> %% Wolff producer never gives up retrying %% so there can only be 'ok' results. on_kafka_ack(_Partition, Offset, {ReplyFn, Args}) when is_integer(Offset) -> - %% the ReplyFn is emqx_resource_worker:reply_after_query/8 + %% the ReplyFn is emqx_resource_worker:handle_async_reply/2 apply(ReplyFn, Args ++ [ok]); on_kafka_ack(_Partition, buffer_overflow_discarded, _Callback) -> %% wolff should bump the dropped_queue_full counter diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_influxdb_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_influxdb_SUITE.erl index cd7f848c2..bbde88cc7 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_influxdb_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_influxdb_SUITE.erl @@ -920,7 +920,7 @@ t_write_failure(Config) -> async -> ?wait_async_action( ?assertEqual(ok, send_message(Config, SentData)), - #{?snk_kind := buffer_worker_reply_after_query}, + #{?snk_kind := handle_async_reply}, 1_000 ) end @@ -938,7 +938,7 @@ t_write_failure(Config) -> #{got => Result} ); async -> - Trace = ?of_kind(buffer_worker_reply_after_query, Trace0), + Trace = ?of_kind(handle_async_reply, Trace0), ?assertMatch([#{action := nack} | _], Trace), [#{result := Result} | _] = Trace, ?assert( From 6cbad047cd1283f0c4b50975c28aaf1e063abde2 Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Mon, 30 Jan 2023 12:16:17 +0800 Subject: [PATCH 0347/1802] fix: don't log CONNECT twice when debug --- apps/emqx/src/emqx_connection.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx/src/emqx_connection.erl b/apps/emqx/src/emqx_connection.erl index 5ed302a6f..714b077ca 100644 --- a/apps/emqx/src/emqx_connection.erl +++ b/apps/emqx/src/emqx_connection.erl @@ -550,6 +550,7 @@ handle_msg( }, handle_incoming(Packet, NState); handle_msg({incoming, Packet}, State) -> + ?TRACE("MQTT", "mqtt_packet_received", #{packet => Packet}), handle_incoming(Packet, State); handle_msg({outgoing, Packets}, State) -> handle_outgoing(Packets, State); @@ -783,7 +784,6 @@ parse_incoming(Data, Packets, State = #state{parse_state = ParseState}) -> handle_incoming(Packet, State) when is_record(Packet, mqtt_packet) -> ok = inc_incoming_stats(Packet), - ?TRACE("MQTT", "mqtt_packet_received", #{packet => Packet}), with_channel(handle_in, [Packet], State); handle_incoming(FrameError, State) -> with_channel(handle_in, [FrameError], State). From ce32ea7334da3cdb925d0c556f3e7d1a6dca98dc Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Mon, 30 Jan 2023 12:18:22 +0800 Subject: [PATCH 0348/1802] feat: Make the log output format order fixed --- apps/emqx/include/logger.hrl | 4 +- apps/emqx/src/emqx_cm.erl | 2 +- apps/emqx/src/emqx_logger_textfmt.erl | 65 ++++++++++++++------------- apps/emqx/test/emqx_mqtt_SUITE.erl | 2 +- 4 files changed, 39 insertions(+), 34 deletions(-) diff --git a/apps/emqx/include/logger.hrl b/apps/emqx/include/logger.hrl index e93aa46f4..27ffc6cc0 100644 --- a/apps/emqx/include/logger.hrl +++ b/apps/emqx/include/logger.hrl @@ -48,9 +48,9 @@ -define(TRACE(Level, Tag, Msg, Meta), begin case persistent_term:get(?TRACE_FILTER, []) of [] -> ok; - %% We can't bind filter list to a variablebecause we pollute the calling scope with it. + %% We can't bind filter list to a variable because we pollute the calling scope with it. %% We also don't want to wrap the macro body in a fun - %% beacause this adds overhead to the happy path. + %% because this adds overhead to the happy path. %% So evaluate `persistent_term:get` twice. _ -> emqx_trace:log(persistent_term:get(?TRACE_FILTER, []), Msg, (Meta)#{trace_tag => Tag}) end, diff --git a/apps/emqx/src/emqx_cm.erl b/apps/emqx/src/emqx_cm.erl index 66e9a2aee..77bc44eeb 100644 --- a/apps/emqx/src/emqx_cm.erl +++ b/apps/emqx/src/emqx_cm.erl @@ -152,7 +152,7 @@ start_link() -> insert_channel_info(ClientId, Info, Stats) -> Chan = {ClientId, self()}, true = ets:insert(?CHAN_INFO_TAB, {Chan, Info, Stats}), - ?tp(debug, insert_channel_info, #{client_id => ClientId}), + ?tp(debug, insert_channel_info, #{clientid => ClientId}), ok. %% @private diff --git a/apps/emqx/src/emqx_logger_textfmt.erl b/apps/emqx/src/emqx_logger_textfmt.erl index 3695929d9..c1d85f341 100644 --- a/apps/emqx/src/emqx_logger_textfmt.erl +++ b/apps/emqx/src/emqx_logger_textfmt.erl @@ -22,20 +22,49 @@ check_config(X) -> logger_formatter:check_config(X). -format(#{msg := {report, Report0}, meta := Meta} = Event, Config) when is_map(Report0) -> - Report1 = enrich_report_mfa(Report0, Meta), - Report2 = enrich_report_clientid(Report1, Meta), - Report3 = enrich_report_peername(Report2, Meta), - Report4 = enrich_report_topic(Report3, Meta), - logger_formatter:format(Event#{msg := {report, Report4}}, Config); +format(#{msg := {report, ReportMap}, meta := Meta} = Event, Config) when is_map(ReportMap) -> + Report = enrich_report(ReportMap, Meta), + logger_formatter:format(Event#{msg := {report, Report}}, Config); format(#{msg := {string, String}} = Event, Config) -> format(Event#{msg => {"~ts ", [String]}}, Config); +%% trace format(#{msg := Msg0, meta := Meta} = Event, Config) -> Msg1 = enrich_client_info(Msg0, Meta), Msg2 = enrich_mfa(Msg1, Meta), Msg3 = enrich_topic(Msg2, Meta), logger_formatter:format(Event#{msg := Msg3}, Config). +enrich_report(ReportRaw, Meta) -> + %% clientid and peername always in emqx_conn's process metadata. + %% topic can be put in meta using ?SLOG/3, or put in msg's report by ?SLOG/2 + Topic = + case maps:get(topic, Meta, undefined) of + undefined -> maps:get(topic, ReportRaw, undefined); + Topic0 -> Topic0 + end, + ClientId = maps:get(clientid, Meta, undefined), + Peer = maps:get(peername, Meta, undefined), + MFA = maps:get(mfa, Meta, undefined), + Line = maps:get(line, Meta, undefined), + Msg = maps:get(msg, ReportRaw, undefined), + lists:foldl( + fun + ({_, undefined}, Acc) -> Acc; + (Item, Acc) -> [Item | Acc] + end, + maps:to_list(maps:without([topic, msg, clientid], ReportRaw)), + [ + {topic, try_format_unicode(Topic)}, + {clientid, try_format_unicode(ClientId)}, + {peername, Peer}, + {line, Line}, + {mfa, mfa(MFA)}, + {msg, Msg} + ] + ). + +try_format_unicode(undefined) -> + undefined; try_format_unicode(Char) -> List = try @@ -53,30 +82,6 @@ try_format_unicode(Char) -> _ -> List end. -enrich_report_mfa(Report, #{mfa := Mfa, line := Line}) -> - Report#{mfa => mfa(Mfa), line => Line}; -enrich_report_mfa(Report, _) -> - Report. - -enrich_report_clientid(Report, #{clientid := ClientId}) -> - Report#{clientid => try_format_unicode(ClientId)}; -enrich_report_clientid(Report, _) -> - Report. - -enrich_report_peername(Report, #{peername := Peername}) -> - Report#{peername => Peername}; -enrich_report_peername(Report, _) -> - Report. - -%% clientid and peername always in emqx_conn's process metadata. -%% topic can be put in meta using ?SLOG/3, or put in msg's report by ?SLOG/2 -enrich_report_topic(Report, #{topic := Topic}) -> - Report#{topic => try_format_unicode(Topic)}; -enrich_report_topic(Report = #{topic := Topic}, _) -> - Report#{topic => try_format_unicode(Topic)}; -enrich_report_topic(Report, _) -> - Report. - enrich_mfa({Fmt, Args}, #{mfa := Mfa, line := Line}) when is_list(Fmt) -> {Fmt ++ " mfa: ~ts line: ~w", Args ++ [mfa(Mfa), Line]}; enrich_mfa(Msg, _) -> diff --git a/apps/emqx/test/emqx_mqtt_SUITE.erl b/apps/emqx/test/emqx_mqtt_SUITE.erl index 287d7fdba..d0162b34b 100644 --- a/apps/emqx/test/emqx_mqtt_SUITE.erl +++ b/apps/emqx/test/emqx_mqtt_SUITE.erl @@ -237,7 +237,7 @@ do_async_set_keepalive() -> {ok, _} = ?block_until( #{ ?snk_kind := insert_channel_info, - client_id := ClientID + clientid := ClientID }, 2000, 100 From 25090563afe5715e70c0b730c0bcfd915ba5a399 Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Mon, 30 Jan 2023 12:28:51 +0800 Subject: [PATCH 0349/1802] chore: use brackets to wrap the mqtt packet when logging --- apps/emqx/src/emqx_packet.erl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/emqx/src/emqx_packet.erl b/apps/emqx/src/emqx_packet.erl index c247419f0..9fa0b00da 100644 --- a/apps/emqx/src/emqx_packet.erl +++ b/apps/emqx/src/emqx_packet.erl @@ -477,8 +477,8 @@ format(Packet) -> format(Packet, emqx_trace_handler:payload_encode()). format(#mqtt_packet{header = Header, variable = Variable, payload = Payload}, PayloadEncode) -> HeaderIO = format_header(Header), case format_variable(Variable, Payload, PayloadEncode) of - "" -> HeaderIO; - VarIO -> [HeaderIO, ",", VarIO] + "" -> [HeaderIO, ")"]; + VarIO -> [HeaderIO, ", ", VarIO, ")"] end. format_header(#mqtt_packet_header{ @@ -487,14 +487,14 @@ format_header(#mqtt_packet_header{ qos = QoS, retain = Retain }) -> - io_lib:format("~ts(Q~p, R~p, D~p)", [type_name(Type), QoS, i(Retain), i(Dup)]). + io_lib:format("~ts(Q~p, R~p, D~p", [type_name(Type), QoS, i(Retain), i(Dup)]). format_variable(undefined, _, _) -> ""; format_variable(Variable, undefined, PayloadEncode) -> format_variable(Variable, PayloadEncode); format_variable(Variable, Payload, PayloadEncode) -> - [format_variable(Variable, PayloadEncode), ",", format_payload(Payload, PayloadEncode)]. + [format_variable(Variable, PayloadEncode), ", ", format_payload(Payload, PayloadEncode)]. format_variable( #mqtt_packet_connect{ From 3d07271ea50bab0fdbfffa8193a2d41e512fec14 Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Mon, 30 Jan 2023 14:30:20 +0800 Subject: [PATCH 0350/1802] fix: crash when mfa not found --- apps/emqx/src/emqx_logger_textfmt.erl | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/emqx/src/emqx_logger_textfmt.erl b/apps/emqx/src/emqx_logger_textfmt.erl index c1d85f341..fe67153ec 100644 --- a/apps/emqx/src/emqx_logger_textfmt.erl +++ b/apps/emqx/src/emqx_logger_textfmt.erl @@ -101,4 +101,5 @@ enrich_topic({Fmt, Args}, #{topic := Topic}) when is_list(Fmt) -> enrich_topic(Msg, _) -> Msg. +mfa(undefined) -> undefined; mfa({M, F, A}) -> atom_to_list(M) ++ ":" ++ atom_to_list(F) ++ "/" ++ integer_to_list(A). From b73d11675e754ea082d208e17a567541792b633e Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Mon, 30 Jan 2023 15:05:49 +0800 Subject: [PATCH 0351/1802] chore: log the bad mqtt packet(frame error) --- apps/emqx/src/emqx_logger_textfmt.erl | 2 +- apps/emqx/src/emqx_packet.erl | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/emqx/src/emqx_logger_textfmt.erl b/apps/emqx/src/emqx_logger_textfmt.erl index fe67153ec..fb27681b8 100644 --- a/apps/emqx/src/emqx_logger_textfmt.erl +++ b/apps/emqx/src/emqx_logger_textfmt.erl @@ -102,4 +102,4 @@ enrich_topic(Msg, _) -> Msg. mfa(undefined) -> undefined; -mfa({M, F, A}) -> atom_to_list(M) ++ ":" ++ atom_to_list(F) ++ "/" ++ integer_to_list(A). +mfa({M, F, A}) -> [atom_to_list(M), ":", atom_to_list(F), "/" ++ integer_to_list(A)]. diff --git a/apps/emqx/src/emqx_packet.erl b/apps/emqx/src/emqx_packet.erl index 9fa0b00da..32bd3df53 100644 --- a/apps/emqx/src/emqx_packet.erl +++ b/apps/emqx/src/emqx_packet.erl @@ -479,7 +479,11 @@ format(#mqtt_packet{header = Header, variable = Variable, payload = Payload}, Pa case format_variable(Variable, Payload, PayloadEncode) of "" -> [HeaderIO, ")"]; VarIO -> [HeaderIO, ", ", VarIO, ")"] - end. + end; +%% receive a frame error packet, such as {frame_error,frame_too_large} or +%% {frame_error,#{expected => <<"'MQTT' or 'MQIsdp'">>,hint => invalid_proto_name,received => <<"bad_name">>}} +format(FrameError, _PayloadEncode) -> + lists:flatten(io_lib:format("~tp", [FrameError])). format_header(#mqtt_packet_header{ type = Type, From bb636394e19f9b3a86a7c335caa52e449a996ab1 Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Mon, 30 Jan 2023 16:35:06 +0800 Subject: [PATCH 0352/1802] chore: add debug log for raw data --- apps/emqx/src/emqx_connection.erl | 6 ++++++ apps/emqx/src/emqx_ws_connection.erl | 8 +++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/apps/emqx/src/emqx_connection.erl b/apps/emqx/src/emqx_connection.erl index 714b077ca..5b783f2fe 100644 --- a/apps/emqx/src/emqx_connection.erl +++ b/apps/emqx/src/emqx_connection.erl @@ -732,6 +732,12 @@ handle_timeout(TRef, Msg, State) -> %% Parse incoming data -compile({inline, [when_bytes_in/3]}). when_bytes_in(Oct, Data, State) -> + ?SLOG(debug, #{ + msg => "raw_bin_received", + size => Oct, + bin => binary_to_list(binary:encode_hex(Data)), + type => "hex" + }), {Packets, NState} = parse_incoming(Data, [], State), Len = erlang:length(Packets), check_limiter( diff --git a/apps/emqx/src/emqx_ws_connection.erl b/apps/emqx/src/emqx_ws_connection.erl index 817c4b505..ead609ed8 100644 --- a/apps/emqx/src/emqx_ws_connection.erl +++ b/apps/emqx/src/emqx_ws_connection.erl @@ -399,6 +399,12 @@ get_peer_info(Type, Listener, Req, Opts) -> websocket_handle({binary, Data}, State) when is_list(Data) -> websocket_handle({binary, iolist_to_binary(Data)}, State); websocket_handle({binary, Data}, State) -> + ?SLOG(debug, #{ + msg => "raw_bin_received", + size => iolist_size(Data), + bin => binary_to_list(binary:encode_hex(Data)), + type => "hex" + }), State2 = ensure_stats_timer(State), {Packets, State3} = parse_incoming(Data, [], State2), LenMsg = erlang:length(Packets), @@ -437,6 +443,7 @@ websocket_info({incoming, Packet = ?CONNECT_PACKET(ConnPkt)}, State) -> NState = State#state{serialize = Serialize}, handle_incoming(Packet, cancel_idle_timer(NState)); websocket_info({incoming, Packet}, State) -> + ?TRACE("WS-MQTT", "mqtt_packet_received", #{packet => Packet}), handle_incoming(Packet, State); websocket_info({outgoing, Packets}, State) -> return(enqueue(Packets, State)); @@ -719,7 +726,6 @@ parse_incoming(Data, Packets, State = #state{parse_state = ParseState}) -> handle_incoming(Packet, State = #state{listener = {Type, Listener}}) when is_record(Packet, mqtt_packet) -> - ?TRACE("WS-MQTT", "mqtt_packet_received", #{packet => Packet}), ok = inc_incoming_stats(Packet), NState = case From 3bdffca4888cf6739600f9f595ec286d9785d1f7 Mon Sep 17 00:00:00 2001 From: firest Date: Mon, 30 Jan 2023 18:17:00 +0800 Subject: [PATCH 0353/1802] fix: the exclusive topics aren't removed when the session has already been cleaned --- apps/emqx/src/emqx_broker.erl | 13 +- apps/emqx/src/emqx_exclusive_subscription.erl | 8 +- apps/emqx/test/emqx_exclusive_sub_SUITE.erl | 159 ++++++++++++++++++ 3 files changed, 168 insertions(+), 12 deletions(-) create mode 100644 apps/emqx/test/emqx_exclusive_sub_SUITE.erl diff --git a/apps/emqx/src/emqx_broker.erl b/apps/emqx/src/emqx_broker.erl index 1c31d86c2..d56620123 100644 --- a/apps/emqx/src/emqx_broker.erl +++ b/apps/emqx/src/emqx_broker.erl @@ -196,13 +196,13 @@ do_unsubscribe(Topic, SubPid, SubOpts) -> true = ets:delete(?SUBOPTION, {Topic, SubPid}), true = ets:delete_object(?SUBSCRIPTION, {SubPid, Topic}), Group = maps:get(share, SubOpts, undefined), - do_unsubscribe(Group, Topic, SubPid, SubOpts), - emqx_exclusive_subscription:unsubscribe(Topic, SubOpts). + do_unsubscribe(Group, Topic, SubPid, SubOpts). do_unsubscribe(undefined, Topic, SubPid, SubOpts) -> case maps:get(shard, SubOpts, 0) of 0 -> true = ets:delete_object(?SUBSCRIBER, {Topic, SubPid}), + emqx_exclusive_subscription:unsubscribe(Topic, SubOpts), cast(pick(Topic), {unsubscribed, Topic}); I -> true = ets:delete_object(?SUBSCRIBER, {{shard, Topic, I}, SubPid}), @@ -366,14 +366,7 @@ subscriber_down(SubPid) -> SubOpts when is_map(SubOpts) -> _ = emqx_broker_helper:reclaim_seq(Topic), true = ets:delete(?SUBOPTION, {Topic, SubPid}), - case maps:get(shard, SubOpts, 0) of - 0 -> - true = ets:delete_object(?SUBSCRIBER, {Topic, SubPid}), - ok = cast(pick(Topic), {unsubscribed, Topic}); - I -> - true = ets:delete_object(?SUBSCRIBER, {{shard, Topic, I}, SubPid}), - ok = cast(pick({Topic, I}), {unsubscribed, Topic, I}) - end; + do_unsubscribe(undefined, Topic, SubPid, SubOpts); undefined -> ok end diff --git a/apps/emqx/src/emqx_exclusive_subscription.erl b/apps/emqx/src/emqx_exclusive_subscription.erl index afb6317b7..a1f7f76ae 100644 --- a/apps/emqx/src/emqx_exclusive_subscription.erl +++ b/apps/emqx/src/emqx_exclusive_subscription.erl @@ -32,7 +32,8 @@ -export([ check_subscribe/2, - unsubscribe/2 + unsubscribe/2, + clear/0 ]). %% Internal exports (RPC) @@ -77,7 +78,7 @@ on_add_module() -> mnesia(boot). on_delete_module() -> - mria:clear_table(?EXCLUSIVE_SHARD). + clear(). %%-------------------------------------------------------------------- %% APIs @@ -101,6 +102,9 @@ unsubscribe(Topic, #{is_exclusive := true}) -> unsubscribe(_Topic, _SubOpts) -> ok. +clear() -> + mria:clear_table(?TAB). + %%-------------------------------------------------------------------- %% Internal functions %%-------------------------------------------------------------------- diff --git a/apps/emqx/test/emqx_exclusive_sub_SUITE.erl b/apps/emqx/test/emqx_exclusive_sub_SUITE.erl new file mode 100644 index 000000000..79dfc9de6 --- /dev/null +++ b/apps/emqx/test/emqx_exclusive_sub_SUITE.erl @@ -0,0 +1,159 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2018-2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_exclusive_sub_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("emqx/include/emqx.hrl"). +-include_lib("emqx/include/emqx_mqtt.hrl"). +-include_lib("eunit/include/eunit.hrl"). + +-define(EXCLUSIVE_TOPIC, <<"$exclusive/t/1">>). +-define(NORMAL_TOPIC, <<"t/1">>). + +-define(CHECK_SUB(Client, Code), ?CHECK_SUB(Client, ?EXCLUSIVE_TOPIC, Code)). +-define(CHECK_SUB(Client, Topic, Code), + {ok, _, [Code]} = emqtt:subscribe(Client, Topic, []) +). + +all() -> emqx_common_test_helpers:all(?MODULE). + +init_per_suite(Config) -> + emqx_common_test_helpers:start_apps([]), + ok = ekka:start(), + OldConf = emqx:get_config([zones], #{}), + emqx_config:put_zone_conf(default, [mqtt, exclusive_subscription], true), + timer:sleep(50), + [{old_conf, OldConf} | Config]. + +end_per_suite(Config) -> + emqx_config:put([zones], proplists:get_value(old_conf, Config)), + ekka:stop(), + mria:stop(), + mria_mnesia:delete_schema(), + emqx_common_test_helpers:stop_apps([]). + +end_per_testcase(_TestCase, _Config) -> + emqx_exclusive_subscription:clear(). + +t_exclusive_sub(_) -> + {ok, C1} = emqtt:start_link([ + {clientid, <<"client1">>}, + {clean_start, false}, + {proto_ver, v5}, + {properties, #{'Session-Expiry-Interval' => 100}} + ]), + {ok, _} = emqtt:connect(C1), + ?CHECK_SUB(C1, 0), + + {ok, C2} = emqtt:start_link([ + {clientid, <<"client2">>}, + {clean_start, false}, + {proto_ver, v5} + ]), + {ok, _} = emqtt:connect(C2), + ?CHECK_SUB(C2, ?RC_QUOTA_EXCEEDED), + + %% keep exclusive even disconnected + ok = emqtt:disconnect(C1), + timer:sleep(1000), + + ?CHECK_SUB(C2, ?RC_QUOTA_EXCEEDED), + + ok = emqtt:disconnect(C2). + +t_allow_normal_sub(_) -> + {ok, C1} = emqtt:start_link([ + {clientid, <<"client1">>}, + {proto_ver, v5} + ]), + {ok, _} = emqtt:connect(C1), + ?CHECK_SUB(C1, 0), + + {ok, C2} = emqtt:start_link([ + {clientid, <<"client2">>}, + {proto_ver, v5} + ]), + {ok, _} = emqtt:connect(C2), + ?CHECK_SUB(C2, ?NORMAL_TOPIC, 0), + + ok = emqtt:disconnect(C1), + ok = emqtt:disconnect(C2). + +t_unsub(_) -> + {ok, C1} = emqtt:start_link([ + {clientid, <<"client1">>}, + {proto_ver, v5} + ]), + {ok, _} = emqtt:connect(C1), + ?CHECK_SUB(C1, 0), + + {ok, C2} = emqtt:start_link([ + {clientid, <<"client2">>}, + {proto_ver, v5} + ]), + {ok, _} = emqtt:connect(C2), + ?CHECK_SUB(C2, ?RC_QUOTA_EXCEEDED), + + {ok, #{}, [0]} = emqtt:unsubscribe(C1, ?EXCLUSIVE_TOPIC), + + ?CHECK_SUB(C2, 0), + + ok = emqtt:disconnect(C1), + ok = emqtt:disconnect(C2). + +t_clean_session(_) -> + erlang:process_flag(trap_exit, true), + {ok, C1} = emqtt:start_link([ + {clientid, <<"client1">>}, + {clean_start, true}, + {proto_ver, v5}, + {properties, #{'Session-Expiry-Interval' => 0}} + ]), + {ok, _} = emqtt:connect(C1), + ?CHECK_SUB(C1, 0), + + {ok, C2} = emqtt:start_link([ + {clientid, <<"client2">>}, + {proto_ver, v5} + ]), + {ok, _} = emqtt:connect(C2), + ?CHECK_SUB(C2, ?RC_QUOTA_EXCEEDED), + + %% auto clean when session was cleand + ok = emqtt:disconnect(C1), + + timer:sleep(1000), + + ?CHECK_SUB(C2, 0), + + ok = emqtt:disconnect(C2). + +t_feat_disabled(_) -> + OldConf = emqx:get_config([zones], #{}), + emqx_config:put_zone_conf(default, [mqtt, exclusive_subscription], false), + + {ok, C1} = emqtt:start_link([ + {clientid, <<"client1">>}, + {proto_ver, v5} + ]), + {ok, _} = emqtt:connect(C1), + ?CHECK_SUB(C1, ?RC_TOPIC_FILTER_INVALID), + ok = emqtt:disconnect(C1), + + emqx_config:put([zones], OldConf). From 63adbe31a0d6bcf163dd5cf9333b5a7d55411d98 Mon Sep 17 00:00:00 2001 From: firest Date: Mon, 30 Jan 2023 18:28:08 +0800 Subject: [PATCH 0354/1802] chore: update changes --- changes/v5.0.16/fix-9864.en.md | 1 + changes/v5.0.16/fix-9864.zh.md | 1 + 2 files changed, 2 insertions(+) create mode 100644 changes/v5.0.16/fix-9864.en.md create mode 100644 changes/v5.0.16/fix-9864.zh.md diff --git a/changes/v5.0.16/fix-9864.en.md b/changes/v5.0.16/fix-9864.en.md new file mode 100644 index 000000000..ad5cd53c9 --- /dev/null +++ b/changes/v5.0.16/fix-9864.en.md @@ -0,0 +1 @@ +Fix the exclusive topics aren't removed when the session has already been cleaned. diff --git a/changes/v5.0.16/fix-9864.zh.md b/changes/v5.0.16/fix-9864.zh.md new file mode 100644 index 000000000..b79d81988 --- /dev/null +++ b/changes/v5.0.16/fix-9864.zh.md @@ -0,0 +1 @@ +修复会话清除后相关的排他订阅主题没有被清理的问题。 From 27881064dc8b8e77765435e1afa571f48c09f6bf Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Mon, 30 Jan 2023 11:34:04 +0100 Subject: [PATCH 0355/1802] fix: increase dropped.queue_full by number of messages --- apps/emqx_resource/src/emqx_resource_buffer_worker.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx_resource/src/emqx_resource_buffer_worker.erl b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl index 50534df4f..56aff94f6 100644 --- a/apps/emqx_resource/src/emqx_resource_buffer_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl @@ -1094,7 +1094,7 @@ append_queue(Id, Index, Q, Queries) -> {Q1, QAckRef, Items2} = replayq:pop(Q0, PopOpts), ok = replayq:ack(Q1, QAckRef), Dropped = length(Items2), - emqx_resource_metrics:dropped_queue_full_inc(Id), + emqx_resource_metrics:dropped_queue_full_inc(Id, Dropped), ?SLOG(info, #{ msg => buffer_worker_overflow, worker_id => Id, From 0c4134c4231992a8915bfb2d5f054c8a1a09126f Mon Sep 17 00:00:00 2001 From: Kjell Winblad Date: Thu, 26 Jan 2023 12:30:48 +0100 Subject: [PATCH 0356/1802] test: add unit test case for redact function in http connector --- .../src/emqx_connector_http.erl | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/apps/emqx_connector/src/emqx_connector_http.erl b/apps/emqx_connector/src/emqx_connector_http.erl index 40df52d45..c99067bdc 100644 --- a/apps/emqx_connector/src/emqx_connector_http.erl +++ b/apps/emqx_connector/src/emqx_connector_http.erl @@ -592,3 +592,33 @@ is_sensitive_key(_) -> %% information (i.e., passwords) redact(Data) -> emqx_misc:redact(Data, fun is_sensitive_key/1). + +-ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). + +redact_test_() -> + TestData1 = [ + {<<"content-type">>, <<"application/json">>}, + {<<"Authorization">>, <<"Basic YWxhZGRpbjpvcGVuc2VzYW1l">>} + ], + + TestData2 = #{ + headers => + [ + {[{str, <<"content-type">>}], [{str, <<"application/json">>}]}, + {[{str, <<"Authorization">>}], [{str, <<"Basic YWxhZGRpbjpvcGVuc2VzYW1l">>}]} + ] + }, + [ + ?_assert(is_sensitive_key(<<"Authorization">>)), + ?_assert(is_sensitive_key(<<"AuthoriZation">>)), + ?_assert(is_sensitive_key('AuthoriZation')), + ?_assert(is_sensitive_key(<<"PrOxy-authoRizaTion">>)), + ?_assert(is_sensitive_key('PrOxy-authoRizaTion')), + ?_assertNot(is_sensitive_key(<<"Something">>)), + ?_assertNot(is_sensitive_key(89)), + ?_assertNotEqual(TestData1, redact(TestData1)), + ?_assertNotEqual(TestData2, redact(TestData2)) + ]. + +-endif. From 0b1483040a12056d86cf5b633ccdb7eec1ecafc6 Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Mon, 30 Jan 2023 18:07:06 +0100 Subject: [PATCH 0357/1802] test: add actual test for haproxy --- .../test/emqx_dashboard_haproxy_SUITE.erl | 97 +++++++++++-------- 1 file changed, 57 insertions(+), 40 deletions(-) diff --git a/apps/emqx_dashboard/test/emqx_dashboard_haproxy_SUITE.erl b/apps/emqx_dashboard/test/emqx_dashboard_haproxy_SUITE.erl index 2fceeee6e..a05de339b 100644 --- a/apps/emqx_dashboard/test/emqx_dashboard_haproxy_SUITE.erl +++ b/apps/emqx_dashboard/test/emqx_dashboard_haproxy_SUITE.erl @@ -19,31 +19,12 @@ -compile(nowarn_export_all). -compile(export_all). --import( - emqx_common_test_http, - [ - request_api/3 - ] -). - -include_lib("eunit/include/eunit.hrl"). -include("emqx_dashboard.hrl"). --define(HOST, "http://127.0.0.1:18083"). - --define(BASE_PATH, "/"). - all() -> emqx_common_test_helpers:all(?MODULE). -end_suite() -> - end_suite([]). - -end_suite(Apps) -> - application:unload(emqx_management), - mnesia:clear_table(?ADMIN), - emqx_common_test_helpers:stop_apps(Apps ++ [emqx_dashboard]). - init_per_suite(Config) -> emqx_common_test_helpers:start_apps( [emqx_management, emqx_dashboard], @@ -51,33 +32,69 @@ init_per_suite(Config) -> ), Config. -end_per_suite(_Config) -> - emqx_common_test_helpers:stop_apps([emqx_dashboard, emqx_management]), - mria:stop(). - set_special_configs(emqx_dashboard) -> emqx_dashboard_api_test_helpers:set_default_config(<<"admin">>, true), ok; set_special_configs(_) -> ok. -disabled_t_status(_) -> - %% no easy way since httpc doesn't support emulating the haproxy protocol - {ok, 200, _Res} = http_get(["status"]), +end_per_suite(Config) -> + application:unload(emqx_management), + mnesia:clear_table(?ADMIN), + emqx_common_test_helpers:stop_apps([emqx_dashboard, emqx_management]), + mria:stop(), + Config. + +t_status(_Config) -> + ProxyInfo = #{ + version => 1, + command => proxy, + transport_family => ipv4, + transport_protocol => stream, + src_address => {127, 0, 0, 1}, + src_port => 444, + dest_address => {192, 168, 0, 1}, + dest_port => 443 + }, + {ok, Socket} = gen_tcp:connect( + "localhost", + 18083, + [binary, {active, false}, {packet, raw}] + ), + ok = gen_tcp:send(Socket, ranch_proxy_header:header(ProxyInfo)), + {ok, Token} = emqx_dashboard_admin:sign_token(<<"admin">>, <<"public">>), + ok = gen_tcp:send( + Socket, + "GET /status HTTP/1.1\r\n" + "Host: localhost\r\n" + "Authorization: Bearer " ++ binary_to_list(Token) ++ + "\r\n" + "\r\n" + ), + {_, 200, _, Rest0} = cow_http:parse_status_line(raw_recv_head(Socket)), + {Headers, Body0} = cow_http:parse_headers(Rest0), + {_, LenBin} = lists:keyfind(<<"content-length">>, 1, Headers), + Len = binary_to_integer(LenBin), + Body = + if + byte_size(Body0) =:= Len -> + Body0; + true -> + {ok, Body1} = gen_tcp:recv(Socket, Len - byte_size(Body0), 5000), + <> + end, + ?assertMatch({match, _}, re:run(Body, "Node .+ is started\nemqx is running")), ok. -%%------------------------------------------------------------------------------ -%% Internal functions -%%------------------------------------------------------------------------------ -http_get(Parts) -> - request_api(get, api_path(Parts), auth_header_()). +raw_recv_head(Socket) -> + {ok, Data} = gen_tcp:recv(Socket, 0, 10000), + raw_recv_head(Socket, Data). -auth_header_() -> - auth_header_(<<"admin">>, <<"public">>). - -auth_header_(Username, Password) -> - {ok, Token} = emqx_dashboard_admin:sign_token(Username, Password), - {"Authorization", "Bearer " ++ binary_to_list(Token)}. - -api_path(Parts) -> - ?HOST ++ filename:join([?BASE_PATH | Parts]). +raw_recv_head(Socket, Buffer) -> + case binary:match(Buffer, <<"\r\n\r\n">>) of + nomatch -> + {ok, Data} = gen_tcp:recv(Socket, 0, 10000), + raw_recv_head(Socket, <>); + {_, _} -> + Buffer + end. From 3ce6cbcd6a7c280552e659e1a2d78f1c1cca924e Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Mon, 30 Jan 2023 18:09:10 +0100 Subject: [PATCH 0358/1802] chore: bump vsn --- apps/emqx_dashboard/src/emqx_dashboard.app.src | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx_dashboard/src/emqx_dashboard.app.src b/apps/emqx_dashboard/src/emqx_dashboard.app.src index 57d63247b..b6c95ca97 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard.app.src +++ b/apps/emqx_dashboard/src/emqx_dashboard.app.src @@ -2,7 +2,7 @@ {application, emqx_dashboard, [ {description, "EMQX Web Dashboard"}, % strict semver, bump manually! - {vsn, "5.0.12"}, + {vsn, "5.0.13"}, {modules, []}, {registered, [emqx_dashboard_sup]}, {applications, [kernel, stdlib, mnesia, minirest, emqx]}, From 33e011aff551b9e9efb187e590dd2fbb1537b0dc Mon Sep 17 00:00:00 2001 From: Erik Timan Date: Fri, 20 Jan 2023 13:48:35 +0100 Subject: [PATCH 0359/1802] fix(emqx_management): handle multiple routes in topics/{topic} API The topics/{topic} API endpoint would return 500 - Internal Error if a topic had multiple routes. This is now fixed by returning a list of routes. --- .../emqx_management/src/emqx_mgmt_api_topics.erl | 7 ++++--- .../test/emqx_mgmt_api_topics_SUITE.erl | 16 +++++++++++++--- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/apps/emqx_management/src/emqx_mgmt_api_topics.erl b/apps/emqx_management/src/emqx_mgmt_api_topics.erl index a64badd3a..4100269e5 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_topics.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_topics.erl @@ -75,7 +75,7 @@ schema("/topics/:topic") -> tags => ?TAGS, parameters => [topic_param(path)], responses => #{ - 200 => hoconsc:mk(hoconsc:ref(topic), #{}), + 200 => hoconsc:mk(hoconsc:array(hoconsc:ref(topic)), #{}), 404 => emqx_dashboard_swagger:error_codes(['TOPIC_NOT_FOUND'], <<"Topic not found">>) } @@ -130,8 +130,9 @@ lookup(#{topic := Topic}) -> case emqx_router:lookup_routes(Topic) of [] -> {404, #{code => ?TOPIC_NOT_FOUND, message => <<"Topic not found">>}}; - [Route] -> - {200, format(Route)} + Routes when is_list(Routes) -> + Formatted = [format(Route) || Route <- Routes], + {200, Formatted} end. %%%============================================================================================== diff --git a/apps/emqx_management/test/emqx_mgmt_api_topics_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_api_topics_SUITE.erl index dcea88d59..70bf1a780 100644 --- a/apps/emqx_management/test/emqx_mgmt_api_topics_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_api_topics_SUITE.erl @@ -72,8 +72,18 @@ t_nodes_api(_) -> ), %% get topics/:topic + %% We add another route here to ensure that the response handles + %% multiple routes for a single topic + DummyNode = 'dummy-node-name', + ok = emqx_router:add_route(Topic, DummyNode), RoutePath = emqx_mgmt_api_test_util:api_path(["topics", Topic]), {ok, RouteResponse} = emqx_mgmt_api_test_util:request_api(get, RoutePath), - RouteData = emqx_json:decode(RouteResponse, [return_maps]), - ?assertEqual(Topic, maps:get(<<"topic">>, RouteData)), - ?assertEqual(Node, maps:get(<<"node">>, RouteData)). + ok = emqx_router:delete_route(Topic, DummyNode), + + [ + #{<<"topic">> := Topic, <<"node">> := Node1}, + #{<<"topic">> := Topic, <<"node">> := Node2} + ] = emqx_json:decode(RouteResponse, [return_maps]), + + DummyNodeBin = atom_to_binary(DummyNode), + ?assertEqual(lists:usort([Node, DummyNodeBin]), lists:usort([Node1, Node2])). From 85d3c5cfd83ee980924c91a42edf2c388104298c Mon Sep 17 00:00:00 2001 From: Erik Timan Date: Fri, 20 Jan 2023 14:42:08 +0100 Subject: [PATCH 0360/1802] chore: update changes --- changes/v5.0.16/fix-9824.en.md | 1 + changes/v5.0.16/fix-9824.zh.md | 1 + 2 files changed, 2 insertions(+) create mode 100644 changes/v5.0.16/fix-9824.en.md create mode 100644 changes/v5.0.16/fix-9824.zh.md diff --git a/changes/v5.0.16/fix-9824.en.md b/changes/v5.0.16/fix-9824.en.md new file mode 100644 index 000000000..29aa93264 --- /dev/null +++ b/changes/v5.0.16/fix-9824.en.md @@ -0,0 +1 @@ +The `topics/{topic}` API endpoint would return `500 - Internal Error` if a topic had multiple routes. This is fixed by returning a list of routes. diff --git a/changes/v5.0.16/fix-9824.zh.md b/changes/v5.0.16/fix-9824.zh.md new file mode 100644 index 000000000..143a39c16 --- /dev/null +++ b/changes/v5.0.16/fix-9824.zh.md @@ -0,0 +1 @@ +修复:当存在多个路由信息时,topics/{topic} 将会返回 500 - Internal Error 的问题,现在将会正确的返回路由信息列表。 From 03cabf6b26c786a1d643c025d2261e99773bd837 Mon Sep 17 00:00:00 2001 From: Erik Timan Date: Mon, 30 Jan 2023 08:51:40 +0100 Subject: [PATCH 0361/1802] chore: bump app VSN --- apps/emqx_management/src/emqx_management.app.src | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx_management/src/emqx_management.app.src b/apps/emqx_management/src/emqx_management.app.src index ccb53dac4..158d65b6b 100644 --- a/apps/emqx_management/src/emqx_management.app.src +++ b/apps/emqx_management/src/emqx_management.app.src @@ -2,7 +2,7 @@ {application, emqx_management, [ {description, "EMQX Management API and CLI"}, % strict semver, bump manually! - {vsn, "5.0.12"}, + {vsn, "5.0.13"}, {modules, []}, {registered, [emqx_management_sup]}, {applications, [kernel, stdlib, emqx_plugins, minirest, emqx]}, From 5b3a77e3c7f44a54e2a7a17ca8764eede76dde11 Mon Sep 17 00:00:00 2001 From: Erik Timan Date: Mon, 30 Jan 2023 14:02:27 +0100 Subject: [PATCH 0362/1802] test(emqx_management): fix flaky route handling in get topic test This reworks a test case to use a second slave node. This ensures that an added route is permanently in the routing table. The old version reverted the routing table quickly since the node name given wasn't a real node. --- .../test/emqx_mgmt_api_topics_SUITE.erl | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/apps/emqx_management/test/emqx_mgmt_api_topics_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_api_topics_SUITE.erl index 70bf1a780..8f9b224ef 100644 --- a/apps/emqx_management/test/emqx_mgmt_api_topics_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_api_topics_SUITE.erl @@ -19,18 +19,25 @@ -compile(nowarn_export_all). -include_lib("eunit/include/eunit.hrl"). +-include_lib("common_test/include/ct.hrl"). + +-define(ROUTE_TAB, emqx_route). all() -> emqx_common_test_helpers:all(?MODULE). init_per_suite(Config) -> emqx_mgmt_api_test_util:init_suite(), - Config. + Slave = emqx_common_test_helpers:start_slave(some_node, []), + [{slave, Slave} | Config]. -end_per_suite(_) -> +end_per_suite(Config) -> + Slave = ?config(slave, Config), + emqx_common_test_helpers:stop_slave(Slave), + mria:clear_table(?ROUTE_TAB), emqx_mgmt_api_test_util:end_suite(). -t_nodes_api(_) -> +t_nodes_api(Config) -> Node = atom_to_binary(node(), utf8), Topic = <<"test_topic">>, {ok, Client} = emqtt:start_link(#{ @@ -74,16 +81,15 @@ t_nodes_api(_) -> %% get topics/:topic %% We add another route here to ensure that the response handles %% multiple routes for a single topic - DummyNode = 'dummy-node-name', - ok = emqx_router:add_route(Topic, DummyNode), + Slave = ?config(slave, Config), + ok = emqx_router:add_route(Topic, Slave), RoutePath = emqx_mgmt_api_test_util:api_path(["topics", Topic]), {ok, RouteResponse} = emqx_mgmt_api_test_util:request_api(get, RoutePath), - ok = emqx_router:delete_route(Topic, DummyNode), + ok = emqx_router:delete_route(Topic, Slave), [ #{<<"topic">> := Topic, <<"node">> := Node1}, #{<<"topic">> := Topic, <<"node">> := Node2} ] = emqx_json:decode(RouteResponse, [return_maps]), - DummyNodeBin = atom_to_binary(DummyNode), - ?assertEqual(lists:usort([Node, DummyNodeBin]), lists:usort([Node1, Node2])). + ?assertEqual(lists:usort([Node, atom_to_binary(Slave)]), lists:usort([Node1, Node2])). From 1bbae31268569e14c765594212455c070adbe4d4 Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Tue, 31 Jan 2023 08:59:46 +0100 Subject: [PATCH 0363/1802] chore: mv for v5.0.16 release --- changes/{v5.0.15 => v5.0.16}/feat-9802.en.md | 0 changes/{v5.0.15 => v5.0.16}/feat-9802.zh.md | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename changes/{v5.0.15 => v5.0.16}/feat-9802.en.md (100%) rename changes/{v5.0.15 => v5.0.16}/feat-9802.zh.md (100%) diff --git a/changes/v5.0.15/feat-9802.en.md b/changes/v5.0.16/feat-9802.en.md similarity index 100% rename from changes/v5.0.15/feat-9802.en.md rename to changes/v5.0.16/feat-9802.en.md diff --git a/changes/v5.0.15/feat-9802.zh.md b/changes/v5.0.16/feat-9802.zh.md similarity index 100% rename from changes/v5.0.15/feat-9802.zh.md rename to changes/v5.0.16/feat-9802.zh.md From 35c429ef1d43c02ac5315fc3e453db5abbc567e9 Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Mon, 30 Jan 2023 14:49:56 +0300 Subject: [PATCH 0364/1802] refactor: drop a couple of unused macros / includes --- apps/emqx_connector/src/mqtt/emqx_connector_mqtt_mod.erl | 6 ------ 1 file changed, 6 deletions(-) diff --git a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_mod.erl b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_mod.erl index 870f9acfc..afe173985 100644 --- a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_mod.erl +++ b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_mod.erl @@ -38,12 +38,6 @@ ]). -include_lib("emqx/include/logger.hrl"). --include_lib("emqx/include/emqx_mqtt.hrl"). - --define(ACK_REF(ClientPid, PktId), {ClientPid, PktId}). - -%% Messages towards ack collector process --define(REF_IDS(Ref, Ids), {Ref, Ids}). %%-------------------------------------------------------------------- %% emqx_bridge_connect callbacks From 4d146c521b76a284a672a5605f18dd89e5554572 Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Mon, 30 Jan 2023 14:51:09 +0300 Subject: [PATCH 0365/1802] fix(mqtt-bridge): ensure proper feedback on async forwards So that buffer worker would notice a connection loss in time, and recycle inflight messages subsequently. --- .../src/emqx_connector_mqtt.erl | 11 +--------- .../src/mqtt/emqx_connector_mqtt_mod.erl | 5 +++++ .../src/mqtt/emqx_connector_mqtt_worker.erl | 21 +++++++++++-------- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/apps/emqx_connector/src/emqx_connector_mqtt.erl b/apps/emqx_connector/src/emqx_connector_mqtt.erl index 71ed81dda..585122539 100644 --- a/apps/emqx_connector/src/emqx_connector_mqtt.erl +++ b/apps/emqx_connector/src/emqx_connector_mqtt.erl @@ -198,10 +198,7 @@ on_query_async( #{name := InstanceId} ) -> ?TRACE("QUERY", "async_send_msg_to_remote_node", #{message => Msg, connector => InstanceId}), - %% this is a cast, currently. - ok = emqx_connector_mqtt_worker:send_to_remote_async(InstanceId, Msg, {ReplyFun, Args}), - WorkerPid = get_worker_pid(InstanceId), - {ok, WorkerPid}. + emqx_connector_mqtt_worker:send_to_remote_async(InstanceId, Msg, {ReplyFun, Args}). on_get_status(_InstId, #{name := InstanceId}) -> case emqx_connector_mqtt_worker:status(InstanceId) of @@ -215,12 +212,6 @@ ensure_mqtt_worker_started(InstanceId, BridgeConf) -> {error, Reason} -> {error, Reason} end. -%% mqtt workers, when created and called via bridge callbacks, are -%% registered. --spec get_worker_pid(atom()) -> pid(). -get_worker_pid(InstanceId) -> - whereis(InstanceId). - make_sub_confs(EmptyMap, _Conf, _) when map_size(EmptyMap) == 0 -> undefined; make_sub_confs(undefined, _Conf, _) -> diff --git a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_mod.erl b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_mod.erl index afe173985..6acbe3bb4 100644 --- a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_mod.erl +++ b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_mod.erl @@ -26,6 +26,8 @@ ping/1 ]). +-export([info/2]). + -export([ ensure_subscribed/3, ensure_unsubscribed/2 @@ -90,6 +92,9 @@ ping(undefined) -> ping(#{client_pid := Pid}) -> emqtt:ping(Pid). +info(pid, #{client_pid := Pid}) -> + Pid. + ensure_subscribed(#{client_pid := Pid, subscriptions := Subs} = Conn, Topic, QoS) when is_pid(Pid) -> diff --git a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl index 00b45789e..776d2d8d9 100644 --- a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl +++ b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl @@ -168,9 +168,9 @@ send_to_remote(Name, Msg) -> gen_statem:call(name(Name), {send_to_remote, Msg}). send_to_remote_async(Pid, Msg, Callback) when is_pid(Pid) -> - gen_statem:cast(Pid, {send_to_remote_async, Msg, Callback}); + gen_statem:call(Pid, {send_to_remote_async, Msg, Callback}); send_to_remote_async(Name, Msg, Callback) -> - gen_statem:cast(name(Name), {send_to_remote_async, Msg, Callback}). + gen_statem:call(name(Name), {send_to_remote_async, Msg, Callback}). %% @doc Return all forwards (local subscriptions). -spec get_forwards(id()) -> [topic()]. @@ -270,12 +270,14 @@ maybe_destroy_session(_State) -> idle({call, From}, ensure_started, State) -> case do_connect(State) of {ok, State1} -> - {next_state, connected, State1, [{reply, From, ok}, {state_timeout, 0, connected}]}; + {next_state, connected, State1, {reply, From, ok}}; {error, Reason, _State} -> {keep_state_and_data, {reply, From, {error, Reason}}} end; idle({call, From}, {send_to_remote, _}, _State) -> {keep_state_and_data, {reply, From, {error, {recoverable_error, not_connected}}}}; +idle({call, From}, {send_to_remote_async, _, _}, _State) -> + {keep_state_and_data, {reply, From, {error, {recoverable_error, not_connected}}}}; %% @doc Standing by for manual start. idle(info, idle, #{start_type := manual}) -> keep_state_and_data; @@ -290,14 +292,11 @@ idle(Type, Content, State) -> connecting(#{reconnect_interval := ReconnectDelayMs} = State) -> case do_connect(State) of {ok, State1} -> - {next_state, connected, State1, {state_timeout, 0, connected}}; + {next_state, connected, State1}; _ -> {keep_state_and_data, {state_timeout, ReconnectDelayMs, reconnect}} end. -connected(state_timeout, connected, State) -> - %% nothing to do - {keep_state, State}; connected({call, From}, {send_to_remote, Msg}, State) -> case do_send(State, Msg) of {ok, NState} -> @@ -305,9 +304,13 @@ connected({call, From}, {send_to_remote, Msg}, State) -> {error, Reason} -> {keep_state_and_data, {reply, From, {error, Reason}}} end; -connected(cast, {send_to_remote_async, Msg, Callback}, State) -> +connected( + {call, From}, + {send_to_remote_async, Msg, Callback}, + State = #{connection := Connection} +) -> _ = do_send_async(State, Msg, Callback), - {keep_state, State}; + {keep_state, State, {reply, From, {ok, emqx_connector_mqtt_mod:info(pid, Connection)}}}; connected( info, {disconnected, Conn, Reason}, From e7ef53558055963b28a5d55b88636ee38274437b Mon Sep 17 00:00:00 2001 From: Kjell Winblad Date: Tue, 31 Jan 2023 09:48:56 +0100 Subject: [PATCH 0366/1802] docs: add change log entry for webhook Authorization header leak --- changes/v5.0.16/fix-9839.en.md | 1 + changes/v5.0.16/fix-9839.zh.md | 1 + 2 files changed, 2 insertions(+) create mode 100644 changes/v5.0.16/fix-9839.en.md create mode 100644 changes/v5.0.16/fix-9839.zh.md diff --git a/changes/v5.0.16/fix-9839.en.md b/changes/v5.0.16/fix-9839.en.md new file mode 100644 index 000000000..9962b6338 --- /dev/null +++ b/changes/v5.0.16/fix-9839.en.md @@ -0,0 +1 @@ +Make sure that the content of an Authorization header that users have specified for a webhook bridge is not printed to log files. diff --git a/changes/v5.0.16/fix-9839.zh.md b/changes/v5.0.16/fix-9839.zh.md new file mode 100644 index 000000000..d9e1e0ad8 --- /dev/null +++ b/changes/v5.0.16/fix-9839.zh.md @@ -0,0 +1 @@ +确保用户为webhook-bridge指定的Authorization-HTTP-header的内容不会被打印到日志文件。 From e7b8df6ba9591abd15d89debe703c072af2e939c Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Tue, 31 Jan 2023 12:46:37 +0100 Subject: [PATCH 0367/1802] chore(bin/emqx): hide upgrade/downgrade from usage info --- bin/emqx | 12 ++++++------ bin/install_upgrade.escript | 21 ++++++--------------- 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/bin/emqx b/bin/emqx index ebf10bada..2c2609b36 100755 --- a/bin/emqx +++ b/bin/emqx @@ -159,7 +159,7 @@ usage() { echo "Evaluate an Erlang expression in the EMQX node, even on Elixir node" ;; versions) - echo "List installed EMQX versions and their status" + echo "List installed EMQX release versions and their status" ;; unpack) echo "Usage: $REL_NAME unpack [VERSION]" @@ -217,12 +217,12 @@ usage() { echo " ctl: Administration commands, execute '$REL_NAME ctl help' for more details" echo '' echo "More:" - echo " Shell attach: remote_console | attach" - echo " Up/Down-grade: upgrade | downgrade | install | uninstall" - echo " Install info: ertspath | root_dir" - echo " Runtime info: pid | ping | versions" + echo " Shell attach: remote_console | attach" +# echo " Up/Down-grade: upgrade | downgrade | install | uninstall | versions" # TODO enable when supported + echo " Install Info: ertspath | root_dir" + echo " Runtime Status: pid | ping" echo " Validate Config: check_config" - echo " Advanced: console_clean | escript | rpc | rpcterms | eval | eval-erl" + echo " Advanced: console_clean | escript | rpc | rpcterms | eval | eval-erl" echo '' echo "Execute '$REL_NAME COMMAND help' for more information" ;; diff --git a/bin/install_upgrade.escript b/bin/install_upgrade.escript index f7f340f31..3e39c787b 100755 --- a/bin/install_upgrade.escript +++ b/bin/install_upgrade.escript @@ -18,27 +18,18 @@ main([Command0, DistInfoStr | CommandArgs]) -> Opts = parse_arguments(CommandArgs), %% invoke the command passed as argument F = case Command0 of - %% "install" -> fun(A, B) -> install(A, B) end; - %% "unpack" -> fun(A, B) -> unpack(A, B) end; - %% "upgrade" -> fun(A, B) -> upgrade(A, B) end; - %% "downgrade" -> fun(A, B) -> downgrade(A, B) end; - %% "uninstall" -> fun(A, B) -> uninstall(A, B) end; - "versions" -> fun(A, B) -> versions(A, B) end; - _ -> fun fail_upgrade/2 + "install" -> fun(A, B) -> install(A, B) end; + "unpack" -> fun(A, B) -> unpack(A, B) end; + "upgrade" -> fun(A, B) -> upgrade(A, B) end; + "downgrade" -> fun(A, B) -> downgrade(A, B) end; + "uninstall" -> fun(A, B) -> uninstall(A, B) end; + "versions" -> fun(A, B) -> versions(A, B) end end, F(DistInfo, Opts); main(Args) -> ?INFO("unknown args: ~p", [Args]), erlang:halt(1). -%% temporary block for hot-upgrades; next release will just remove -%% this and the new script version shall be used instead of this -%% current version. -%% TODO: always deny relup for macos (unsupported) -fail_upgrade(_DistInfo, _Opts) -> - ?ERROR("Unsupported upgrade path", []), - erlang:halt(1). - unpack({RelName, NameTypeArg, NodeName, Cookie}, Opts) -> TargetNode = start_distribution(NodeName, NameTypeArg, Cookie), Version = proplists:get_value(version, Opts), From f6dafc20eaa8c462e74712168ee38a7b608deff4 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Tue, 31 Jan 2023 14:20:34 +0100 Subject: [PATCH 0368/1802] feat: export API to read cluster and local overrides --- apps/emqx/src/emqx_config.erl | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/apps/emqx/src/emqx_config.erl b/apps/emqx/src/emqx_config.erl index 204e32a2b..117043911 100644 --- a/apps/emqx/src/emqx_config.erl +++ b/apps/emqx/src/emqx_config.erl @@ -24,6 +24,7 @@ init_load/2, init_load/3, read_override_conf/1, + read_override_confs/0, delete_override_conf_files/0, check_config/2, fill_defaults/1, @@ -326,9 +327,7 @@ init_load(SchemaMod, RawConf, Opts) when is_map(RawConf) -> ok = save_schema_mod_and_names(SchemaMod), %% Merge environment variable overrides on top RawConfWithEnvs = merge_envs(SchemaMod, RawConf), - ClusterOverrides = read_override_conf(#{override_to => cluster}), - LocalOverrides = read_override_conf(#{override_to => local}), - Overrides = hocon:deep_merge(ClusterOverrides, LocalOverrides), + Overrides = read_override_confs(), RawConfWithOverrides = hocon:deep_merge(RawConfWithEnvs, Overrides), RootNames = get_root_names(), RawConfAll = raw_conf_with_default(SchemaMod, RootNames, RawConfWithOverrides, Opts), @@ -337,6 +336,12 @@ init_load(SchemaMod, RawConf, Opts) when is_map(RawConf) -> save_to_app_env(AppEnvs), ok = save_to_config_map(CheckedConf, RawConfAll). +%% @doc Read merged cluster + local overrides. +read_override_confs() -> + ClusterOverrides = read_override_conf(#{override_to => cluster}), + LocalOverrides = read_override_conf(#{override_to => local}), + hocon:deep_merge(ClusterOverrides, LocalOverrides). + %% keep the raw and non-raw conf has the same keys to make update raw conf easier. raw_conf_with_default(SchemaMod, RootNames, RawConf, #{raw_with_default := true}) -> Fun = fun(Name, Acc) -> @@ -599,8 +604,16 @@ load_hocon_file(FileName, LoadType) -> case filelib:is_regular(FileName) of true -> Opts = #{include_dirs => include_dirs(), format => LoadType}, - {ok, Raw0} = hocon:load(FileName, Opts), - Raw0; + case hocon:load(FileName, Opts) of + {ok, Raw0} -> + Raw0; + {error, Reason} -> + throw(#{ + msg => failed_to_load_conf, + reason => Reason, + file => FileName + }) + end; false -> #{} end. From fce1e74c3d45e2561b8bafebc3aaee225609b1b3 Mon Sep 17 00:00:00 2001 From: Ilya Averyanov Date: Thu, 26 Jan 2023 00:37:12 +0200 Subject: [PATCH 0369/1802] fix(connector): fix redis cluster resource recovery --- .ci/docker-compose-file/.env | 2 +- .ci/docker-compose-file/Makefile.local | 6 +- .../docker-compose-redis-cluster-tcp.yaml | 56 +++++++- .../docker-compose-redis-cluster-tls.yaml | 61 +++++++-- .../docker-compose-redis-sentinel-tcp.yaml | 38 +++++- .../docker-compose-redis-sentinel-tls.yaml | 44 +++++- .ci/docker-compose-file/redis/.gitignore | 3 - .../redis/cluster-tcp/redis.conf | 18 +++ .../redis/cluster-tls/redis.conf | 28 ++++ .ci/docker-compose-file/redis/redis-tls.conf | 12 -- .ci/docker-compose-file/redis/redis.conf | 6 - .ci/docker-compose-file/redis/redis.sh | 126 ------------------ .../redis/sentinel-tcp/master.conf | 14 ++ .../redis/sentinel-tcp/sentinel-base.conf | 7 + .../redis/sentinel-tcp/slave.conf | 17 +++ .../redis/sentinel-tls/master.conf | 20 +++ .../redis/sentinel-tls/sentinel-base.conf | 14 ++ .../redis/sentinel-tls/slave.conf | 24 ++++ .github/workflows/run_test_cases.yaml | 10 +- apps/emqx_connector/docker-ct | 1 + apps/emqx_connector/rebar.config | 12 +- .../src/emqx_connector_redis.erl | 20 +-- .../test/emqx_connector_redis_SUITE.erl | 6 +- .../test/emqx_ee_bridge_redis_SUITE.erl | 5 +- mix.exs | 4 +- rebar.config | 2 +- 26 files changed, 340 insertions(+), 216 deletions(-) delete mode 100644 .ci/docker-compose-file/redis/.gitignore create mode 100644 .ci/docker-compose-file/redis/cluster-tcp/redis.conf create mode 100644 .ci/docker-compose-file/redis/cluster-tls/redis.conf delete mode 100644 .ci/docker-compose-file/redis/redis-tls.conf delete mode 100644 .ci/docker-compose-file/redis/redis.conf delete mode 100755 .ci/docker-compose-file/redis/redis.sh create mode 100644 .ci/docker-compose-file/redis/sentinel-tcp/master.conf create mode 100644 .ci/docker-compose-file/redis/sentinel-tcp/sentinel-base.conf create mode 100644 .ci/docker-compose-file/redis/sentinel-tcp/slave.conf create mode 100644 .ci/docker-compose-file/redis/sentinel-tls/master.conf create mode 100644 .ci/docker-compose-file/redis/sentinel-tls/sentinel-base.conf create mode 100644 .ci/docker-compose-file/redis/sentinel-tls/slave.conf diff --git a/.ci/docker-compose-file/.env b/.ci/docker-compose-file/.env index bd925e224..e5564efb7 100644 --- a/.ci/docker-compose-file/.env +++ b/.ci/docker-compose-file/.env @@ -1,5 +1,5 @@ MYSQL_TAG=8 -REDIS_TAG=6 +REDIS_TAG=7.0 MONGO_TAG=5 PGSQL_TAG=13 LDAP_TAG=2.4.50 diff --git a/.ci/docker-compose-file/Makefile.local b/.ci/docker-compose-file/Makefile.local index 2cf0802ce..9c12255e4 100644 --- a/.ci/docker-compose-file/Makefile.local +++ b/.ci/docker-compose-file/Makefile.local @@ -13,10 +13,10 @@ help: up: env \ MYSQL_TAG=8 \ - REDIS_TAG=6 \ + REDIS_TAG=7.0 \ MONGO_TAG=5 \ PGSQL_TAG=13 \ - docker compose \ + docker-compose \ -f .ci/docker-compose-file/docker-compose.yaml \ -f .ci/docker-compose-file/docker-compose-mongo-single-tcp.yaml \ -f .ci/docker-compose-file/docker-compose-mongo-single-tls.yaml \ @@ -34,7 +34,7 @@ up: up -d --build --remove-orphans down: - docker compose \ + docker-compose \ -f .ci/docker-compose-file/docker-compose.yaml \ -f .ci/docker-compose-file/docker-compose-mongo-single-tcp.yaml \ -f .ci/docker-compose-file/docker-compose-mongo-single-tls.yaml \ diff --git a/.ci/docker-compose-file/docker-compose-redis-cluster-tcp.yaml b/.ci/docker-compose-file/docker-compose-redis-cluster-tcp.yaml index 9c03fc65e..f44a71e14 100644 --- a/.ci/docker-compose-file/docker-compose-redis-cluster-tcp.yaml +++ b/.ci/docker-compose-file/docker-compose-redis-cluster-tcp.yaml @@ -1,11 +1,57 @@ version: '3.9' - services: - redis_cluster: + + redis-cluster-1: &redis-node + container_name: redis-cluster-1 image: redis:${REDIS_TAG} - container_name: redis-cluster volumes: - - ./redis/:/data/conf - command: bash -c "/bin/bash /data/conf/redis.sh --node cluster && tail -f /var/log/redis-server.log" + - ./redis/cluster-tcp:/usr/local/etc/redis + command: redis-server /usr/local/etc/redis/redis.conf networks: - emqx_bridge + + + redis-cluster-2: + <<: *redis-node + container_name: redis-cluster-2 + + redis-cluster-3: + <<: *redis-node + container_name: redis-cluster-3 + + redis-cluster-4: + <<: *redis-node + container_name: redis-cluster-4 + + redis-cluster-5: + <<: *redis-node + container_name: redis-cluster-5 + + redis-cluster-6: + <<: *redis-node + container_name: redis-cluster-6 + + redis-cluster-create: + <<: *redis-node + container_name: redis-cluster-create + command: > + redis-cli + --cluster create + redis-cluster-1:6379 + redis-cluster-2:6379 + redis-cluster-3:6379 + redis-cluster-4:6379 + redis-cluster-5:6379 + redis-cluster-6:6379 + --cluster-replicas 1 + --cluster-yes + --pass "public" + --no-auth-warning + depends_on: + - redis-cluster-1 + - redis-cluster-2 + - redis-cluster-3 + - redis-cluster-4 + - redis-cluster-5 + - redis-cluster-6 + diff --git a/.ci/docker-compose-file/docker-compose-redis-cluster-tls.yaml b/.ci/docker-compose-file/docker-compose-redis-cluster-tls.yaml index bfbf1a4a3..988620acb 100644 --- a/.ci/docker-compose-file/docker-compose-redis-cluster-tls.yaml +++ b/.ci/docker-compose-file/docker-compose-redis-cluster-tls.yaml @@ -1,14 +1,59 @@ version: '3.9' - services: - redis_cluster_tls: - container_name: redis-cluster-tls + + redis-cluster-tls-1: &redis-node + container_name: redis-cluster-tls-1 image: redis:${REDIS_TAG} volumes: - - ../../apps/emqx/etc/certs/cacert.pem:/etc/certs/ca.crt - - ../../apps/emqx/etc/certs/cert.pem:/etc/certs/redis.crt - - ../../apps/emqx/etc/certs/key.pem:/etc/certs/redis.key - - ./redis/:/data/conf - command: bash -c "/bin/bash /data/conf/redis.sh --node cluster --tls-enabled && tail -f /var/log/redis-server.log" + - ./redis/cluster-tls:/usr/local/etc/redis + - ../../apps/emqx/etc/certs:/etc/certs + command: redis-server /usr/local/etc/redis/redis.conf networks: - emqx_bridge + + redis-cluster-tls-2: + <<: *redis-node + container_name: redis-cluster-tls-2 + + redis-cluster-tls-3: + <<: *redis-node + container_name: redis-cluster-tls-3 + + redis-cluster-tls-4: + <<: *redis-node + container_name: redis-cluster-tls-4 + + redis-cluster-tls-5: + <<: *redis-node + container_name: redis-cluster-tls-5 + + redis-cluster-tls-6: + <<: *redis-node + container_name: redis-cluster-tls-6 + + redis-cluster-tls-create: + <<: *redis-node + container_name: redis-cluster-tls-create + command: > + redis-cli + --cluster create + redis-cluster-tls-1:6389 + redis-cluster-tls-2:6389 + redis-cluster-tls-3:6389 + redis-cluster-tls-4:6389 + redis-cluster-tls-5:6389 + redis-cluster-tls-6:6389 + --cluster-replicas 1 + --cluster-yes + --pass "public" + --no-auth-warning + --tls + --insecure + depends_on: + - redis-cluster-tls-1 + - redis-cluster-tls-2 + - redis-cluster-tls-3 + - redis-cluster-tls-4 + - redis-cluster-tls-5 + - redis-cluster-tls-6 + diff --git a/.ci/docker-compose-file/docker-compose-redis-sentinel-tcp.yaml b/.ci/docker-compose-file/docker-compose-redis-sentinel-tcp.yaml index 07c6cfb0a..d395edd2b 100644 --- a/.ci/docker-compose-file/docker-compose-redis-sentinel-tcp.yaml +++ b/.ci/docker-compose-file/docker-compose-redis-sentinel-tcp.yaml @@ -1,11 +1,41 @@ -version: '3.9' +version: "3" services: - redis_sentinel_server: + + redis-sentinel-master: + container_name: redis-sentinel-master + image: redis:${REDIS_TAG} + volumes: + - ./redis/sentinel-tcp:/usr/local/etc/redis + command: redis-server /usr/local/etc/redis/master.conf + networks: + - emqx_bridge + + redis-sentinel-slave: + container_name: redis-sentinel-slave + image: redis:${REDIS_TAG} + volumes: + - ./redis/sentinel-tcp:/usr/local/etc/redis + command: redis-server /usr/local/etc/redis/slave.conf + networks: + - emqx_bridge + depends_on: + - redis-sentinel-master + + redis-sentinel: container_name: redis-sentinel image: redis:${REDIS_TAG} volumes: - - ./redis/:/data/conf - command: bash -c "/bin/bash /data/conf/redis.sh --node sentinel && tail -f /var/log/redis-server.log" + - ./redis/sentinel-tcp/sentinel-base.conf:/usr/local/etc/redis/sentinel-base.conf + depends_on: + - redis-sentinel-master + - redis-sentinel-slave + command: > + bash -c "cp -f /usr/local/etc/redis/sentinel-base.conf /usr/local/etc/redis/sentinel.conf && + redis-sentinel /usr/local/etc/redis/sentinel.conf" networks: - emqx_bridge + + + + diff --git a/.ci/docker-compose-file/docker-compose-redis-sentinel-tls.yaml b/.ci/docker-compose-file/docker-compose-redis-sentinel-tls.yaml index b9eaefa9c..d883e2992 100644 --- a/.ci/docker-compose-file/docker-compose-redis-sentinel-tls.yaml +++ b/.ci/docker-compose-file/docker-compose-redis-sentinel-tls.yaml @@ -1,14 +1,44 @@ -version: '3.9' +version: "3" services: - redis_sentinel_server_tls: + + redis-sentinel-tls-master: + container_name: redis-sentinel-tls-master + image: redis:${REDIS_TAG} + volumes: + - ./redis/sentinel-tls:/usr/local/etc/redis + - ../../apps/emqx/etc/certs:/etc/certs + command: redis-server /usr/local/etc/redis/master.conf + networks: + - emqx_bridge + + redis-sentinel-tls-slave: + container_name: redis-sentinel-tls-slave + image: redis:${REDIS_TAG} + volumes: + - ./redis/sentinel-tls:/usr/local/etc/redis + - ../../apps/emqx/etc/certs:/etc/certs + command: redis-server /usr/local/etc/redis/slave.conf + networks: + - emqx_bridge + depends_on: + - redis-sentinel-tls-master + + redis-sentinel-tls: container_name: redis-sentinel-tls image: redis:${REDIS_TAG} volumes: - - ../../apps/emqx/etc/certs/cacert.pem:/etc/certs/ca.crt - - ../../apps/emqx/etc/certs/cert.pem:/etc/certs/redis.crt - - ../../apps/emqx/etc/certs/key.pem:/etc/certs/redis.key - - ./redis/:/data/conf - command: bash -c "/bin/bash /data/conf/redis.sh --node sentinel --tls-enabled && tail -f /var/log/redis-server.log" + - ./redis/sentinel-tls/sentinel-base.conf:/usr/local/etc/redis/sentinel-base.conf + - ../../apps/emqx/etc/certs:/etc/certs + depends_on: + - redis-sentinel-tls-master + - redis-sentinel-tls-slave + command: > + bash -c "cp -f /usr/local/etc/redis/sentinel-base.conf /usr/local/etc/redis/sentinel.conf && + redis-sentinel /usr/local/etc/redis/sentinel.conf" networks: - emqx_bridge + + + + diff --git a/.ci/docker-compose-file/redis/.gitignore b/.ci/docker-compose-file/redis/.gitignore deleted file mode 100644 index 23ffe8469..000000000 --- a/.ci/docker-compose-file/redis/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -r700?i.log -nodes.700?.conf -*.rdb diff --git a/.ci/docker-compose-file/redis/cluster-tcp/redis.conf b/.ci/docker-compose-file/redis/cluster-tcp/redis.conf new file mode 100644 index 000000000..79a0d8a73 --- /dev/null +++ b/.ci/docker-compose-file/redis/cluster-tcp/redis.conf @@ -0,0 +1,18 @@ +bind :: 0.0.0.0 +port 6379 +requirepass public + +cluster-enabled yes + +masterauth public + +protected-mode no +daemonize no + +loglevel notice +logfile "" + +always-show-logo no +save "" +appendonly no + diff --git a/.ci/docker-compose-file/redis/cluster-tls/redis.conf b/.ci/docker-compose-file/redis/cluster-tls/redis.conf new file mode 100644 index 000000000..3020f46a7 --- /dev/null +++ b/.ci/docker-compose-file/redis/cluster-tls/redis.conf @@ -0,0 +1,28 @@ +bind :: 0.0.0.0 +port 6379 +requirepass public + +cluster-enabled yes + +masterauth public + +tls-port 6389 +tls-cert-file /etc/certs/cert.pem +tls-key-file /etc/certs/key.pem +tls-ca-cert-file /etc/certs/cacert.pem +tls-auth-clients no + +tls-replication yes +tls-cluster yes + + +protected-mode no +daemonize no + +loglevel notice +logfile "" + +always-show-logo no +save "" +appendonly no + diff --git a/.ci/docker-compose-file/redis/redis-tls.conf b/.ci/docker-compose-file/redis/redis-tls.conf deleted file mode 100644 index c503dc2e8..000000000 --- a/.ci/docker-compose-file/redis/redis-tls.conf +++ /dev/null @@ -1,12 +0,0 @@ -daemonize yes -bind 0.0.0.0 :: -logfile /var/log/redis-server.log -protected-mode no -requirepass public -masterauth public - -tls-cert-file /etc/certs/redis.crt -tls-key-file /etc/certs/redis.key -tls-ca-cert-file /etc/certs/ca.crt -tls-replication yes -tls-cluster yes diff --git a/.ci/docker-compose-file/redis/redis.conf b/.ci/docker-compose-file/redis/redis.conf deleted file mode 100644 index 484d9abf9..000000000 --- a/.ci/docker-compose-file/redis/redis.conf +++ /dev/null @@ -1,6 +0,0 @@ -daemonize yes -bind 0.0.0.0 :: -logfile /var/log/redis-server.log -protected-mode no -requirepass public -masterauth public diff --git a/.ci/docker-compose-file/redis/redis.sh b/.ci/docker-compose-file/redis/redis.sh deleted file mode 100755 index be6462249..000000000 --- a/.ci/docker-compose-file/redis/redis.sh +++ /dev/null @@ -1,126 +0,0 @@ -#!/bin/bash - -set -x - -LOCAL_IP=$(hostname -i | grep -oE '((25[0-5]|(2[0-4]|1[0-9]|[1-9]|)[0-9])\.){3}(25[0-5]|(2[0-4]|1[0-9]|[1-9]|)[0-9])' | head -n 1) - -node=single -tls=false -while [[ $# -gt 0 ]] -do -key="$1" - -case $key in - -n|--node) - node="$2" - shift # past argument - shift # past value - ;; - --tls-enabled) - tls=true - shift # past argument - ;; - *) - shift # past argument - ;; -esac -done - -rm -f \ - /data/conf/r7000i.log \ - /data/conf/r7001i.log \ - /data/conf/r7002i.log \ - /data/conf/nodes.7000.conf \ - /data/conf/nodes.7001.conf \ - /data/conf/nodes.7002.conf - -if [ "$node" = "cluster" ]; then - if $tls; then - redis-server /data/conf/redis-tls.conf --port 7000 --cluster-config-file /data/conf/nodes.7000.conf \ - --tls-port 8000 --cluster-enabled yes - redis-server /data/conf/redis-tls.conf --port 7001 --cluster-config-file /data/conf/nodes.7001.conf \ - --tls-port 8001 --cluster-enabled yes - redis-server /data/conf/redis-tls.conf --port 7002 --cluster-config-file /data/conf/nodes.7002.conf \ - --tls-port 8002 --cluster-enabled yes - else - redis-server /data/conf/redis.conf --port 7000 --cluster-config-file /data/conf/nodes.7000.conf \ - --cluster-enabled yes - redis-server /data/conf/redis.conf --port 7001 --cluster-config-file /data/conf/nodes.7001.conf \ - --cluster-enabled yes - redis-server /data/conf/redis.conf --port 7002 --cluster-config-file /data/conf/nodes.7002.conf \ - --cluster-enabled yes - fi -elif [ "$node" = "sentinel" ]; then - if $tls; then - redis-server /data/conf/redis-tls.conf --port 7000 --cluster-config-file /data/conf/nodes.7000.conf \ - --tls-port 8000 --cluster-enabled no - redis-server /data/conf/redis-tls.conf --port 7001 --cluster-config-file /data/conf/nodes.7001.conf \ - --tls-port 8001 --cluster-enabled no --slaveof "$LOCAL_IP" 8000 - redis-server /data/conf/redis-tls.conf --port 7002 --cluster-config-file /data/conf/nodes.7002.conf \ - --tls-port 8002 --cluster-enabled no --slaveof "$LOCAL_IP" 8000 - - else - redis-server /data/conf/redis.conf --port 7000 --cluster-config-file /data/conf/nodes.7000.conf \ - --cluster-enabled no - redis-server /data/conf/redis.conf --port 7001 --cluster-config-file /data/conf/nodes.7001.conf \ - --cluster-enabled no --slaveof "$LOCAL_IP" 7000 - redis-server /data/conf/redis.conf --port 7002 --cluster-config-file /data/conf/nodes.7002.conf \ - --cluster-enabled no --slaveof "$LOCAL_IP" 7000 - fi -fi - -REDIS_LOAD_FLG=true - -while $REDIS_LOAD_FLG; -do - sleep 1 - redis-cli --pass public --no-auth-warning -p 7000 info 1> /data/conf/r7000i.log 2> /dev/null - if ! [ -s /data/conf/r7000i.log ]; then - continue - fi - redis-cli --pass public --no-auth-warning -p 7001 info 1> /data/conf/r7001i.log 2> /dev/null - if ! [ -s /data/conf/r7001i.log ]; then - continue - fi - redis-cli --pass public --no-auth-warning -p 7002 info 1> /data/conf/r7002i.log 2> /dev/null; - if ! [ -s /data/conf/r7002i.log ]; then - continue - fi - if [ "$node" = "cluster" ] ; then - if $tls; then - yes "yes" | redis-cli --cluster create "$LOCAL_IP:8000" "$LOCAL_IP:8001" "$LOCAL_IP:8002" \ - --pass public --no-auth-warning \ - --tls true --cacert /etc/certs/ca.crt \ - --cert /etc/certs/redis.crt --key /etc/certs/redis.key - else - yes "yes" | redis-cli --cluster create "$LOCAL_IP:7000" "$LOCAL_IP:7001" "$LOCAL_IP:7002" \ - --pass public --no-auth-warning - fi - elif [ "$node" = "sentinel" ]; then - tee /_sentinel.conf>/dev/null << EOF -port 26379 -bind 0.0.0.0 :: -daemonize yes -logfile /var/log/redis-server.log -dir /tmp -EOF - if $tls; then - cat >>/_sentinel.conf<>/_sentinel.conf< [{ssl, false}] end ++ [{sentinel, maps:get(sentinel, Config, undefined)}], - PoolName = emqx_plugin_libs_pool:pool_name(InstId), + PoolName = InstId, State = #{poolname => PoolName, type => Type}, case Type of cluster -> @@ -225,26 +225,10 @@ is_unrecoverable_error({error, <<"ERR unknown command ", _/binary>>}) -> is_unrecoverable_error(_) -> false. -extract_eredis_cluster_workers(PoolName) -> - lists:flatten([ - gen_server:call(PoolPid, get_all_workers) - || PoolPid <- eredis_cluster_monitor:get_all_pools(PoolName) - ]). - -eredis_cluster_workers_exist_and_are_connected(Workers) -> - length(Workers) > 0 andalso - lists:all( - fun({_, Pid, _, _}) -> - eredis_cluster_pool_worker:is_connected(Pid) =:= true - end, - Workers - ). - on_get_status(_InstId, #{type := cluster, poolname := PoolName}) -> case eredis_cluster:pool_exists(PoolName) of true -> - Workers = extract_eredis_cluster_workers(PoolName), - Health = eredis_cluster_workers_exist_and_are_connected(Workers), + Health = eredis_cluster:ping_all(PoolName), status_result(Health); false -> disconnected diff --git a/apps/emqx_connector/test/emqx_connector_redis_SUITE.erl b/apps/emqx_connector/test/emqx_connector_redis_SUITE.erl index 87d2b8e21..3a134ad35 100644 --- a/apps/emqx_connector/test/emqx_connector_redis_SUITE.erl +++ b/apps/emqx_connector/test/emqx_connector_redis_SUITE.erl @@ -27,6 +27,8 @@ -define(REDIS_SINGLE_PORT, 6379). -define(REDIS_SENTINEL_HOST, "redis-sentinel"). -define(REDIS_SENTINEL_PORT, 26379). +-define(REDIS_CLUSTER_HOST, "redis-cluster-1"). +-define(REDIS_CLUSTER_PORT, 6379). -define(REDIS_RESOURCE_MOD, emqx_connector_redis). all() -> @@ -203,8 +205,8 @@ redis_config_base(Type, ServerKey) -> MaybeSentinel = "", MaybeDatabase = " database = 1\n"; "cluster" -> - Host = ?REDIS_SINGLE_HOST, - Port = ?REDIS_SINGLE_PORT, + Host = ?REDIS_CLUSTER_HOST, + Port = ?REDIS_CLUSTER_PORT, MaybeSentinel = "", MaybeDatabase = "" end, diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_redis_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_redis_SUITE.erl index 2b67787b2..901e74d10 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_redis_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_redis_SUITE.erl @@ -479,12 +479,13 @@ redis_connect_configs() -> }, redis_cluster => #{ tcp => #{ - <<"servers">> => <<"redis-cluster:7000,redis-cluster:7001,redis-cluster:7002">>, + <<"servers">> => + <<"redis-cluster-1:6379,redis-cluster-2:6379,redis-cluster-3:6379">>, <<"redis_type">> => <<"cluster">> }, tls => #{ <<"servers">> => - <<"redis-cluster-tls:8000,redis-cluster-tls:8001,redis-cluster-tls:8002">>, + <<"redis-cluster-tls-1:6389,redis-cluster-tls-2:6389,redis-cluster-tls-3:6389">>, <<"redis_type">> => <<"cluster">>, <<"ssl">> => redis_connect_ssl_opts(redis_cluster) } diff --git a/mix.exs b/mix.exs index 315212fbe..8e0844aaa 100644 --- a/mix.exs +++ b/mix.exs @@ -57,7 +57,7 @@ defmodule EMQXUmbrella.MixProject do {:gen_rpc, github: "emqx/gen_rpc", tag: "2.8.1", override: true}, {:grpc, github: "emqx/grpc-erl", tag: "0.6.7", override: true}, {:minirest, github: "emqx/minirest", tag: "1.3.7", override: true}, - {:ecpool, github: "emqx/ecpool", tag: "0.5.2", override: true}, + {:ecpool, github: "emqx/ecpool", tag: "0.5.3", override: true}, {:replayq, github: "emqx/replayq", tag: "0.3.6", override: true}, {:pbkdf2, github: "emqx/erlang-pbkdf2", tag: "2.0.4", override: true}, {:emqtt, github: "emqx/emqtt", tag: "1.7.0-rc.2", override: true}, @@ -76,8 +76,6 @@ defmodule EMQXUmbrella.MixProject do {:gun, github: "emqx/gun", tag: "1.3.9", override: true}, # in conflict by emqx_connector and system_monitor {:epgsql, github: "emqx/epgsql", tag: "4.7.0.1", override: true}, - # in conflict by mongodb and eredis_cluster - {:poolboy, github: "emqx/poolboy", tag: "1.5.2", override: true}, # in conflict by emqx and observer_cli {:recon, github: "ferd/recon", tag: "2.5.1", override: true}, {:jsx, github: "talentdeficit/jsx", tag: "v3.1.0", override: true}, diff --git a/rebar.config b/rebar.config index a1b4df520..7e0b50653 100644 --- a/rebar.config +++ b/rebar.config @@ -59,7 +59,7 @@ , {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.8.1"}}} , {grpc, {git, "https://github.com/emqx/grpc-erl", {tag, "0.6.7"}}} , {minirest, {git, "https://github.com/emqx/minirest", {tag, "1.3.7"}}} - , {ecpool, {git, "https://github.com/emqx/ecpool", {tag, "0.5.2"}}} + , {ecpool, {git, "https://github.com/emqx/ecpool", {tag, "0.5.3"}}} , {replayq, {git, "https://github.com/emqx/replayq.git", {tag, "0.3.6"}}} , {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}} , {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.7.0-rc.2"}}} From d0c10b59aa1527dfa537cef34559232f97ceaaa1 Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Mon, 30 Jan 2023 18:35:17 +0300 Subject: [PATCH 0370/1802] feat(mqtt-bridge): avoid middleman process Instead, supervise `emqtt` client process directly. --- .../test/emqx_bridge_api_SUITE.erl | 2 +- .../test/emqx_bridge_mqtt_SUITE.erl | 8 +- .../src/emqx_connector_mqtt.erl | 78 ++- .../src/mqtt/emqx_connector_mqtt_mod.erl | 235 ------- .../src/mqtt/emqx_connector_mqtt_worker.erl | 627 ++++++++---------- .../test/emqx_connector_mqtt_tests.erl | 60 -- .../test/emqx_connector_mqtt_worker_tests.erl | 101 --- 7 files changed, 338 insertions(+), 773 deletions(-) delete mode 100644 apps/emqx_connector/src/mqtt/emqx_connector_mqtt_mod.erl delete mode 100644 apps/emqx_connector/test/emqx_connector_mqtt_tests.erl delete mode 100644 apps/emqx_connector/test/emqx_connector_mqtt_worker_tests.erl diff --git a/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl b/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl index 6b0b3a941..5cb78d3ba 100644 --- a/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl +++ b/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl @@ -640,7 +640,7 @@ t_bridges_probe(Config) -> ?assertMatch( #{ <<"code">> := <<"TEST_FAILED">>, - <<"message">> := <<"#{reason => econnrefused", _/binary>> + <<"message">> := <<"econnrefused">> }, jsx:decode(ConnRefused) ), diff --git a/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl b/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl index cd5a17184..6e3bf77ee 100644 --- a/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl +++ b/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl @@ -825,15 +825,15 @@ t_mqtt_conn_bridge_egress_reconnect(_) -> <<"type">> => ?TYPE_MQTT, <<"name">> => ?BRIDGE_NAME_EGRESS, <<"egress">> => ?EGRESS_CONF, - %% to make it reconnect quickly - <<"reconnect_interval">> => <<"1s">>, <<"resource_opts">> => #{ <<"worker_pool_size">> => 2, <<"query_mode">> => <<"sync">>, %% using a long time so we can test recovery <<"request_timeout">> => <<"15s">>, %% to make it check the healthy quickly - <<"health_check_interval">> => <<"0.5s">> + <<"health_check_interval">> => <<"0.5s">>, + %% to make it reconnect quickly + <<"auto_restart_interval">> => <<"1s">> } } ), @@ -911,7 +911,7 @@ t_mqtt_conn_bridge_egress_reconnect(_) -> Decoded1 = jsx:decode(BridgeStr1), DecodedMetrics1 = jsx:decode(BridgeMetricsStr1), ?assertMatch( - Status when (Status == <<"connected">> orelse Status == <<"connecting">>), + Status when (Status == <<"connecting">> orelse Status == <<"disconnected">>), maps:get(<<"status">>, Decoded1) ), %% matched >= 3 because of possible retries. diff --git a/apps/emqx_connector/src/emqx_connector_mqtt.erl b/apps/emqx_connector/src/emqx_connector_mqtt.erl index 585122539..462bac0b8 100644 --- a/apps/emqx_connector/src/emqx_connector_mqtt.erl +++ b/apps/emqx_connector/src/emqx_connector_mqtt.erl @@ -105,16 +105,15 @@ init([]) -> {ok, {SupFlag, []}}. bridge_spec(Config) -> + {Name, NConfig} = maps:take(name, Config), #{ - id => maps:get(name, Config), - start => {emqx_connector_mqtt_worker, start_link, [Config]}, - restart => permanent, - shutdown => 5000, - type => worker, - modules => [emqx_connector_mqtt_worker] + id => Name, + start => {emqx_connector_mqtt_worker, start_link, [Name, NConfig]}, + restart => temporary, + shutdown => 5000 }. --spec bridges() -> [{node(), map()}]. +-spec bridges() -> [{_Name, _Status}]. bridges() -> [ {Name, emqx_connector_mqtt_worker:status(Name)} @@ -144,8 +143,7 @@ on_message_received(Msg, HookPoint, ResId) -> %% =================================================================== callback_mode() -> async_if_possible. -on_start(InstId, Conf) -> - InstanceId = binary_to_atom(InstId, utf8), +on_start(InstanceId, Conf) -> ?SLOG(info, #{ msg => "starting_mqtt_connector", connector => InstanceId, @@ -154,8 +152,8 @@ on_start(InstId, Conf) -> BasicConf = basic_config(Conf), BridgeConf = BasicConf#{ name => InstanceId, - clientid => clientid(InstId, Conf), - subscriptions => make_sub_confs(maps:get(ingress, Conf, undefined), Conf, InstId), + clientid => clientid(InstanceId, Conf), + subscriptions => make_sub_confs(maps:get(ingress, Conf, undefined), Conf, InstanceId), forwards => make_forward_confs(maps:get(egress, Conf, undefined)) }, case ?MODULE:create_bridge(BridgeConf) of @@ -189,35 +187,49 @@ on_stop(_InstId, #{name := InstanceId}) -> on_query(_InstId, {send_message, Msg}, #{name := InstanceId}) -> ?TRACE("QUERY", "send_msg_to_remote_node", #{message => Msg, connector => InstanceId}), - emqx_connector_mqtt_worker:send_to_remote(InstanceId, Msg). - -on_query_async( - _InstId, - {send_message, Msg}, - {ReplyFun, Args}, - #{name := InstanceId} -) -> - ?TRACE("QUERY", "async_send_msg_to_remote_node", #{message => Msg, connector => InstanceId}), - emqx_connector_mqtt_worker:send_to_remote_async(InstanceId, Msg, {ReplyFun, Args}). - -on_get_status(_InstId, #{name := InstanceId}) -> - case emqx_connector_mqtt_worker:status(InstanceId) of - connected -> connected; - _ -> connecting + case emqx_connector_mqtt_worker:send_to_remote(InstanceId, Msg) of + ok -> + ok; + {error, Reason} -> + classify_error(Reason) end. +on_query_async(_InstId, {send_message, Msg}, Callback, #{name := InstanceId}) -> + ?TRACE("QUERY", "async_send_msg_to_remote_node", #{message => Msg, connector => InstanceId}), + case emqx_connector_mqtt_worker:send_to_remote_async(InstanceId, Msg, Callback) of + ok -> + % TODO this is racy + {ok, emqx_connector_mqtt_worker:pid(InstanceId)}; + {error, Reason} -> + classify_error(Reason) + end. + +on_get_status(_InstId, #{name := InstanceId}) -> + emqx_connector_mqtt_worker:status(InstanceId). + +classify_error(disconnected = Reason) -> + {error, {recoverable_error, Reason}}; +classify_error({disconnected, _RC, _} = Reason) -> + {error, {recoverable_error, Reason}}; +classify_error({shutdown, _} = Reason) -> + {error, {recoverable_error, Reason}}; +classify_error(Reason) -> + {error, {unrecoverable_error, Reason}}. + ensure_mqtt_worker_started(InstanceId, BridgeConf) -> - case emqx_connector_mqtt_worker:ensure_started(InstanceId) of - ok -> {ok, #{name => InstanceId, bridge_conf => BridgeConf}}; - {error, Reason} -> {error, Reason} + case emqx_connector_mqtt_worker:connect(InstanceId) of + {ok, Properties} -> + {ok, #{name => InstanceId, config => BridgeConf, props => Properties}}; + {error, Reason} -> + {error, Reason} end. make_sub_confs(EmptyMap, _Conf, _) when map_size(EmptyMap) == 0 -> undefined; make_sub_confs(undefined, _Conf, _) -> undefined; -make_sub_confs(SubRemoteConf, Conf, InstId) -> - ResId = emqx_resource_manager:manager_id_to_resource_id(InstId), +make_sub_confs(SubRemoteConf, Conf, InstanceId) -> + ResId = emqx_resource_manager:manager_id_to_resource_id(InstanceId), case maps:find(hookpoint, Conf) of error -> error({no_hookpoint_provided, Conf}); @@ -251,7 +263,6 @@ basic_config( %% 30s connect_timeout => 30, auto_reconnect => true, - reconnect_interval => ?AUTO_RECONNECT_INTERVAL, proto_ver => ProtoVer, %% Opening bridge_mode will form a non-standard mqtt connection message. %% A load balancing server (such as haproxy) is often set up before the emqx broker server. @@ -264,8 +275,7 @@ basic_config( retry_interval => RetryIntv, max_inflight => MaxInflight, ssl => EnableSsl, - ssl_opts => maps:to_list(maps:remove(enable, Ssl)), - if_record_metrics => true + ssl_opts => maps:to_list(maps:remove(enable, Ssl)) }, maybe_put_fields([username, password], Conf, BasicConf). diff --git a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_mod.erl b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_mod.erl deleted file mode 100644 index 6acbe3bb4..000000000 --- a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_mod.erl +++ /dev/null @@ -1,235 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2023 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - -%% @doc This module implements EMQX Bridge transport layer on top of MQTT protocol - --module(emqx_connector_mqtt_mod). - --export([ - start/1, - send/2, - send_async/3, - stop/1, - ping/1 -]). - --export([info/2]). - --export([ - ensure_subscribed/3, - ensure_unsubscribed/2 -]). - -%% callbacks for emqtt --export([ - handle_publish/3, - handle_disconnected/2 -]). - --include_lib("emqx/include/logger.hrl"). - -%%-------------------------------------------------------------------- -%% emqx_bridge_connect callbacks -%%-------------------------------------------------------------------- - -start(Config) -> - Parent = self(), - ServerStr = iolist_to_binary(maps:get(server, Config)), - {Server, Port} = emqx_connector_mqtt_schema:parse_server(ServerStr), - Mountpoint = maps:get(receive_mountpoint, Config, undefined), - Subscriptions = maps:get(subscriptions, Config, undefined), - Vars = emqx_connector_mqtt_msg:make_pub_vars(Mountpoint, Subscriptions), - Handlers = make_hdlr(Parent, Vars, #{server => ServerStr}), - Config1 = Config#{ - msg_handler => Handlers, - host => Server, - port => Port, - force_ping => true, - proto_ver => maps:get(proto_ver, Config, v4) - }, - case emqtt:start_link(process_config(Config1)) of - {ok, Pid} -> - case emqtt:connect(Pid) of - {ok, _} -> - try - ok = sub_remote_topics(Pid, Subscriptions), - {ok, #{client_pid => Pid, subscriptions => Subscriptions}} - catch - throw:Reason -> - ok = stop(#{client_pid => Pid}), - {error, error_reason(Reason, ServerStr)} - end; - {error, Reason} -> - ok = stop(#{client_pid => Pid}), - {error, error_reason(Reason, ServerStr)} - end; - {error, Reason} -> - {error, error_reason(Reason, ServerStr)} - end. - -error_reason(Reason, ServerStr) -> - #{reason => Reason, server => ServerStr}. - -stop(#{client_pid := Pid}) -> - safe_stop(Pid, fun() -> emqtt:stop(Pid) end, 1000), - ok. - -ping(undefined) -> - pang; -ping(#{client_pid := Pid}) -> - emqtt:ping(Pid). - -info(pid, #{client_pid := Pid}) -> - Pid. - -ensure_subscribed(#{client_pid := Pid, subscriptions := Subs} = Conn, Topic, QoS) when - is_pid(Pid) --> - case emqtt:subscribe(Pid, Topic, QoS) of - {ok, _, _} -> Conn#{subscriptions => [{Topic, QoS} | Subs]}; - Error -> {error, Error} - end; -ensure_subscribed(_Conn, _Topic, _QoS) -> - %% return ok for now - %% next re-connect should should call start with new topic added to config - ok. - -ensure_unsubscribed(#{client_pid := Pid, subscriptions := Subs} = Conn, Topic) when is_pid(Pid) -> - case emqtt:unsubscribe(Pid, Topic) of - {ok, _, _} -> Conn#{subscriptions => lists:keydelete(Topic, 1, Subs)}; - Error -> {error, Error} - end; -ensure_unsubscribed(Conn, _) -> - %% return ok for now - %% next re-connect should should call start with this topic deleted from config - Conn. - -safe_stop(Pid, StopF, Timeout) -> - MRef = monitor(process, Pid), - unlink(Pid), - try - StopF() - catch - _:_ -> - ok - end, - receive - {'DOWN', MRef, _, _, _} -> - ok - after Timeout -> - exit(Pid, kill) - end. - -send(#{client_pid := ClientPid}, Msg) -> - emqtt:publish(ClientPid, Msg). - -send_async(#{client_pid := ClientPid}, Msg, Callback) -> - emqtt:publish_async(ClientPid, Msg, infinity, Callback). - -handle_publish(Msg, undefined, _Opts) -> - ?SLOG(error, #{ - msg => - "cannot_publish_to_local_broker_as" - "_'ingress'_is_not_configured", - message => Msg - }); -handle_publish(#{properties := Props} = Msg0, Vars, Opts) -> - Msg = format_msg_received(Msg0, Opts), - ?SLOG(debug, #{ - msg => "publish_to_local_broker", - message => Msg, - vars => Vars - }), - case Vars of - #{on_message_received := {Mod, Func, Args}} -> - _ = erlang:apply(Mod, Func, [Msg | Args]); - _ -> - ok - end, - maybe_publish_to_local_broker(Msg, Vars, Props). - -handle_disconnected(Reason, Parent) -> - Parent ! {disconnected, self(), Reason}. - -make_hdlr(Parent, Vars, Opts) -> - #{ - publish => {fun ?MODULE:handle_publish/3, [Vars, Opts]}, - disconnected => {fun ?MODULE:handle_disconnected/2, [Parent]} - }. - -sub_remote_topics(_ClientPid, undefined) -> - ok; -sub_remote_topics(ClientPid, #{remote := #{topic := FromTopic, qos := QoS}}) -> - case emqtt:subscribe(ClientPid, FromTopic, QoS) of - {ok, _, _} -> ok; - Error -> throw(Error) - end. - -process_config(Config) -> - maps:without([conn_type, address, receive_mountpoint, subscriptions, name], Config). - -maybe_publish_to_local_broker(Msg, Vars, Props) -> - case emqx_map_lib:deep_get([local, topic], Vars, undefined) of - %% local topic is not set, discard it - undefined -> ok; - _ -> emqx_broker:publish(emqx_connector_mqtt_msg:to_broker_msg(Msg, Vars, Props)) - end. - -format_msg_received( - #{ - dup := Dup, - payload := Payload, - properties := Props, - qos := QoS, - retain := Retain, - topic := Topic - }, - #{server := Server} -) -> - #{ - id => emqx_guid:to_hexstr(emqx_guid:gen()), - server => Server, - payload => Payload, - topic => Topic, - qos => QoS, - dup => Dup, - retain => Retain, - pub_props => printable_maps(Props), - message_received_at => erlang:system_time(millisecond) - }. - -printable_maps(undefined) -> - #{}; -printable_maps(Headers) -> - maps:fold( - fun - ('User-Property', V0, AccIn) when is_list(V0) -> - AccIn#{ - 'User-Property' => maps:from_list(V0), - 'User-Property-Pairs' => [ - #{ - key => Key, - value => Value - } - || {Key, Value} <- V0 - ] - }; - (K, V0, AccIn) -> - AccIn#{K => V0} - end, - #{}, - Headers - ). diff --git a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl index 776d2d8d9..85261a063 100644 --- a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl +++ b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl @@ -60,172 +60,252 @@ %% * Local messages are all normalised to QoS-1 when exporting to remote -module(emqx_connector_mqtt_worker). --behaviour(gen_statem). -include_lib("snabbkaffe/include/snabbkaffe.hrl"). -include_lib("emqx/include/logger.hrl"). %% APIs -export([ - start_link/1, - stop/1 -]). - -%% gen_statem callbacks --export([ - terminate/3, - code_change/4, - init/1, - callback_mode/0 -]). - -%% state functions --export([ - idle/3, - connected/3 + start_link/2, + stop/1, + pid/1 ]). %% management APIs -export([ - ensure_started/1, - ensure_stopped/1, + connect/1, status/1, ping/1, send_to_remote/2, send_to_remote_async/3 ]). --export([get_forwards/1]). - --export([get_subscriptions/1]). +-export([handle_publish/3]). +-export([handle_disconnect/1]). -export_type([ config/0, ack_ref/0 ]). --type id() :: atom() | string() | pid(). --type qos() :: emqx_types:qos(). +-type name() :: term(). +% -type qos() :: emqx_types:qos(). -type config() :: map(). -type ack_ref() :: term(). --type topic() :: emqx_types:topic(). +% -type topic() :: emqx_types:topic(). -include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/emqx_mqtt.hrl"). -%% same as default in-flight limit for emqtt --define(DEFAULT_INFLIGHT_SIZE, 32). --define(DEFAULT_RECONNECT_DELAY_MS, timer:seconds(5)). +-define(REF(Name), {via, gproc, ?NAME(Name)}). +-define(NAME(Name), {n, l, Name}). %% @doc Start a bridge worker. Supported configs: -%% start_type: 'manual' (default) or 'auto', when manual, bridge will stay -%% at 'idle' state until a manual call to start it. -%% connect_module: The module which implements emqx_bridge_connect behaviour -%% and work as message batch transport layer -%% reconnect_interval: Delay in milli-seconds for the bridge worker to retry -%% in case of transportation failure. -%% max_inflight: Max number of batches allowed to send-ahead before receiving -%% confirmation from remote node/cluster %% mountpoint: The topic mount point for messages sent to remote node/cluster %% `undefined', `<<>>' or `""' to disable %% forwards: Local topics to subscribe. %% %% Find more connection specific configs in the callback modules %% of emqx_bridge_connect behaviour. -start_link(Opts) when is_list(Opts) -> - start_link(maps:from_list(Opts)); -start_link(Opts) -> - case maps:get(name, Opts, undefined) of - undefined -> - gen_statem:start_link(?MODULE, Opts, []); - Name -> - Name1 = name(Name), - gen_statem:start_link({local, Name1}, ?MODULE, Opts#{name => Name1}, []) +-spec start_link(name(), map()) -> + {ok, pid()} | {error, _Reason}. +start_link(Name, BridgeOpts) -> + ?SLOG(debug, #{ + msg => "client_starting", + name => Name, + options => BridgeOpts + }), + Conf = init_config(BridgeOpts), + Options = mk_client_options(Conf, BridgeOpts), + case emqtt:start_link(Options) of + {ok, Pid} -> + true = gproc:reg_other(?NAME(Name), Pid, Conf), + {ok, Pid}; + {error, Reason} = Error -> + ?SLOG(error, #{ + msg => "client_start_failed", + config => emqx_misc:redact(BridgeOpts), + reason => Reason + }), + Error end. -ensure_started(Name) -> - gen_statem:call(name(Name), ensure_started). - -%% @doc Manually stop bridge worker. State idempotency ensured. -ensure_stopped(Name) -> - gen_statem:call(name(Name), ensure_stopped, 5000). - -stop(Pid) -> gen_statem:stop(Pid). - -status(Pid) when is_pid(Pid) -> - gen_statem:call(Pid, status); -status(Name) -> - gen_statem:call(name(Name), status). - -ping(Pid) when is_pid(Pid) -> - gen_statem:call(Pid, ping); -ping(Name) -> - gen_statem:call(name(Name), ping). - -send_to_remote(Pid, Msg) when is_pid(Pid) -> - gen_statem:call(Pid, {send_to_remote, Msg}); -send_to_remote(Name, Msg) -> - gen_statem:call(name(Name), {send_to_remote, Msg}). - -send_to_remote_async(Pid, Msg, Callback) when is_pid(Pid) -> - gen_statem:call(Pid, {send_to_remote_async, Msg, Callback}); -send_to_remote_async(Name, Msg, Callback) -> - gen_statem:call(name(Name), {send_to_remote_async, Msg, Callback}). - -%% @doc Return all forwards (local subscriptions). --spec get_forwards(id()) -> [topic()]. -get_forwards(Name) -> gen_statem:call(name(Name), get_forwards, timer:seconds(1000)). - -%% @doc Return all subscriptions (subscription over mqtt connection to remote broker). --spec get_subscriptions(id()) -> [{emqx_types:topic(), qos()}]. -get_subscriptions(Name) -> gen_statem:call(name(Name), get_subscriptions). - -callback_mode() -> [state_functions]. - -%% @doc Config should be a map(). -init(#{name := Name} = ConnectOpts) -> - ?SLOG(debug, #{ - msg => "starting_bridge_worker", - name => Name - }), - erlang:process_flag(trap_exit, true), - State = init_state(ConnectOpts), - self() ! idle, - {ok, idle, State#{ - connect_opts => pre_process_opts(ConnectOpts) - }}. - -init_state(Opts) -> - ReconnDelayMs = maps:get(reconnect_interval, Opts, ?DEFAULT_RECONNECT_DELAY_MS), - StartType = maps:get(start_type, Opts, manual), +init_config(Opts) -> Mountpoint = maps:get(forward_mountpoint, Opts, undefined), - MaxInflightSize = maps:get(max_inflight, Opts, ?DEFAULT_INFLIGHT_SIZE), - Name = maps:get(name, Opts, undefined), + Subscriptions = maps:get(subscriptions, Opts, undefined), + Forwards = maps:get(forwards, Opts, undefined), #{ - start_type => StartType, - reconnect_interval => ReconnDelayMs, mountpoint => format_mountpoint(Mountpoint), - max_inflight => MaxInflightSize, - connection => undefined, - name => Name + subscriptions => pre_process_subscriptions(Subscriptions), + forwards => pre_process_forwards(Forwards) }. -pre_process_opts(#{subscriptions := InConf, forwards := OutConf} = ConnectOpts) -> - ConnectOpts#{ - subscriptions => pre_process_in_out(in, InConf), - forwards => pre_process_in_out(out, OutConf) +mk_client_options(Conf, BridgeOpts) -> + Server = iolist_to_binary(maps:get(server, BridgeOpts)), + HostPort = emqx_connector_mqtt_schema:parse_server(Server), + Mountpoint = maps:get(receive_mountpoint, BridgeOpts, undefined), + Subscriptions = maps:get(subscriptions, Conf), + Vars = emqx_connector_mqtt_msg:make_pub_vars(Mountpoint, Subscriptions), + Opts = maps:without( + [ + address, + auto_reconnect, + conn_type, + mountpoint, + forwards, + receive_mountpoint, + subscriptions + ], + BridgeOpts + ), + Opts#{ + msg_handler => mk_client_event_handler(Vars, #{server => Server}), + hosts => [HostPort], + force_ping => true, + proto_ver => maps:get(proto_ver, BridgeOpts, v4) }. -pre_process_in_out(_, undefined) -> +mk_client_event_handler(Vars, Opts) when Vars /= undefined -> + #{ + publish => {fun ?MODULE:handle_publish/3, [Vars, Opts]}, + disconnected => {fun ?MODULE:handle_disconnect/1, []} + }; +mk_client_event_handler(undefined, _Opts) -> + undefined. + +connect(Name) -> + #{subscriptions := Subscriptions} = get_config(Name), + case emqtt:connect(pid(Name)) of + {ok, Properties} -> + case subscribe_remote_topics(Name, Subscriptions) of + ok -> + {ok, Properties}; + {ok, _, _RCs} -> + {ok, Properties}; + {error, Reason} = Error -> + ?SLOG(error, #{ + msg => "client_subscribe_failed", + subscriptions => Subscriptions, + reason => Reason + }), + Error + end; + {error, Reason} = Error -> + ?SLOG(error, #{ + msg => "client_connect_failed", + reason => Reason + }), + Error + end. + +subscribe_remote_topics(Ref, #{remote := #{topic := FromTopic, qos := QoS}}) -> + emqtt:subscribe(ref(Ref), FromTopic, QoS); +subscribe_remote_topics(_Ref, undefined) -> + ok. + +stop(Ref) -> + emqtt:stop(ref(Ref)). + +pid(Name) -> + gproc:lookup_pid(?NAME(Name)). + +status(Ref) -> + trycall( + fun() -> + Info = emqtt:info(ref(Ref)), + case proplists:get_value(socket, Info) of + Socket when Socket /= undefined -> + connected; + undefined -> + connecting + end + end, + #{noproc => disconnected} + ). + +ping(Ref) -> + emqtt:ping(ref(Ref)). + +send_to_remote(Name, MsgIn) -> + trycall( + fun() -> do_send(Name, export_msg(Name, MsgIn)) end, + #{ + badarg => {error, disconnected}, + noproc => {error, disconnected} + } + ). + +do_send(Name, {true, Msg}) -> + case emqtt:publish(pid(Name), Msg) of + ok -> + ok; + {ok, #{reason_code := RC}} when + RC =:= ?RC_SUCCESS; + RC =:= ?RC_NO_MATCHING_SUBSCRIBERS + -> + ok; + {ok, #{reason_code := RC, reason_code_name := Reason}} -> + ?SLOG(warning, #{ + msg => "remote_publish_failed", + message => Msg, + reason_code => RC, + reason_code_name => Reason + }), + {error, Reason}; + {error, Reason} -> + ?SLOG(info, #{ + msg => "client_failed", + reason => Reason + }), + {error, Reason} + end; +do_send(_Name, false) -> + ok. + +send_to_remote_async(Name, MsgIn, Callback) -> + trycall( + fun() -> do_send_async(Name, export_msg(Name, MsgIn), Callback) end, + #{badarg => {error, disconnected}} + ). + +do_send_async(Name, {true, Msg}, Callback) -> + emqtt:publish_async(pid(Name), Msg, _Timeout = infinity, Callback); +do_send_async(_Name, false, _Callback) -> + ok. + +ref(Pid) when is_pid(Pid) -> + Pid; +ref(Term) -> + ?REF(Term). + +trycall(Fun, Else) -> + try + Fun() + catch + error:badarg -> + maps:get(badarg, Else); + exit:{noproc, _} -> + maps:get(noproc, Else) + end. + +format_mountpoint(undefined) -> undefined; -pre_process_in_out(in, #{local := LC} = Conf) when is_map(Conf) -> +format_mountpoint(Prefix) -> + binary:replace(iolist_to_binary(Prefix), <<"${node}">>, atom_to_binary(node(), utf8)). + +pre_process_subscriptions(undefined) -> + undefined; +pre_process_subscriptions(#{local := LC} = Conf) when is_map(Conf) -> Conf#{local => pre_process_in_out_common(LC)}; -pre_process_in_out(in, Conf) when is_map(Conf) -> +pre_process_subscriptions(Conf) when is_map(Conf) -> %% have no 'local' field in the config + undefined. + +pre_process_forwards(undefined) -> undefined; -pre_process_in_out(out, #{remote := RC} = Conf) when is_map(Conf) -> +pre_process_forwards(#{remote := RC} = Conf) when is_map(Conf) -> Conf#{remote => pre_process_in_out_common(RC)}; -pre_process_in_out(out, Conf) when is_map(Conf) -> +pre_process_forwards(Conf) when is_map(Conf) -> %% have no 'remote' field in the config undefined. @@ -245,241 +325,112 @@ pre_process_conf(Key, Conf) -> Conf#{Key => Val} end. -code_change(_Vsn, State, Data, _Extra) -> - {ok, State, Data}. +get_config(Name) -> + gproc:lookup_value(?NAME(Name)). -terminate(_Reason, _StateName, State) -> - _ = disconnect(State), - maybe_destroy_session(State). +export_msg(Name, Msg) -> + case get_config(Name) of + #{forwards := Forwards = #{}, mountpoint := Mountpoint} -> + {true, export_msg(Mountpoint, Forwards, Msg)}; + #{forwards := undefined} -> + ?SLOG(error, #{ + msg => "forwarding_unavailable", + message => Msg, + reason => "egress is not configured" + }), + false + end. -maybe_destroy_session(#{connect_opts := ConnectOpts = #{clean_start := false}} = State) -> - try - %% Destroy session if clean_start is not set. - %% Ignore any crashes, just refresh the clean_start = true. - _ = do_connect(State#{connect_opts => ConnectOpts#{clean_start => true}}), - _ = disconnect(State), - ok - catch - _:_ -> +export_msg(Mountpoint, Forwards, Msg) -> + Vars = emqx_connector_mqtt_msg:make_pub_vars(Mountpoint, Forwards), + emqx_connector_mqtt_msg:to_remote_msg(Msg, Vars). + +%% + +handle_publish(#{properties := Props} = MsgIn, Vars, Opts) -> + Msg = import_msg(MsgIn, Opts), + ?SLOG(debug, #{ + msg => "publish_local", + message => Msg, + vars => Vars + }), + case Vars of + #{on_message_received := {Mod, Func, Args}} -> + _ = erlang:apply(Mod, Func, [Msg | Args]); + _ -> ok - end; -maybe_destroy_session(_State) -> + end, + maybe_publish_local(Msg, Vars, Props). + +handle_disconnect(_Reason) -> ok. -%% ensure_started will be deprecated in the future -idle({call, From}, ensure_started, State) -> - case do_connect(State) of - {ok, State1} -> - {next_state, connected, State1, {reply, From, ok}}; - {error, Reason, _State} -> - {keep_state_and_data, {reply, From, {error, Reason}}} - end; -idle({call, From}, {send_to_remote, _}, _State) -> - {keep_state_and_data, {reply, From, {error, {recoverable_error, not_connected}}}}; -idle({call, From}, {send_to_remote_async, _, _}, _State) -> - {keep_state_and_data, {reply, From, {error, {recoverable_error, not_connected}}}}; -%% @doc Standing by for manual start. -idle(info, idle, #{start_type := manual}) -> - keep_state_and_data; -%% @doc Standing by for auto start. -idle(info, idle, #{start_type := auto} = State) -> - connecting(State); -idle(state_timeout, reconnect, State) -> - connecting(State); -idle(Type, Content, State) -> - common(idle, Type, Content, State). - -connecting(#{reconnect_interval := ReconnectDelayMs} = State) -> - case do_connect(State) of - {ok, State1} -> - {next_state, connected, State1}; +maybe_publish_local(Msg, Vars, Props) -> + case emqx_map_lib:deep_get([local, topic], Vars, undefined) of + %% local topic is not set, discard it + undefined -> + ok; _ -> - {keep_state_and_data, {state_timeout, ReconnectDelayMs, reconnect}} + emqx_broker:publish(emqx_connector_mqtt_msg:to_broker_msg(Msg, Vars, Props)) end. -connected({call, From}, {send_to_remote, Msg}, State) -> - case do_send(State, Msg) of - {ok, NState} -> - {keep_state, NState, {reply, From, ok}}; - {error, Reason} -> - {keep_state_and_data, {reply, From, {error, Reason}}} - end; -connected( - {call, From}, - {send_to_remote_async, Msg, Callback}, - State = #{connection := Connection} -) -> - _ = do_send_async(State, Msg, Callback), - {keep_state, State, {reply, From, {ok, emqx_connector_mqtt_mod:info(pid, Connection)}}}; -connected( - info, - {disconnected, Conn, Reason}, - #{connection := Connection, name := Name, reconnect_interval := ReconnectDelayMs} = State -) -> - ?tp(info, disconnected, #{name => Name, reason => Reason}), - case Conn =:= maps:get(client_pid, Connection, undefined) of - true -> - {next_state, idle, State#{connection => undefined}, - {state_timeout, ReconnectDelayMs, reconnect}}; - false -> - keep_state_and_data - end; -connected(Type, Content, State) -> - common(connected, Type, Content, State). - -%% Common handlers -common(StateName, {call, From}, status, _State) -> - {keep_state_and_data, {reply, From, StateName}}; -common(_StateName, {call, From}, ping, #{connection := Conn} = _State) -> - Reply = emqx_connector_mqtt_mod:ping(Conn), - {keep_state_and_data, {reply, From, Reply}}; -common(_StateName, {call, From}, ensure_stopped, #{connection := undefined} = _State) -> - {keep_state_and_data, {reply, From, ok}}; -common(_StateName, {call, From}, ensure_stopped, #{connection := Conn} = State) -> - Reply = emqx_connector_mqtt_mod:stop(Conn), - {next_state, idle, State#{connection => undefined}, {reply, From, Reply}}; -common(_StateName, {call, From}, get_forwards, #{connect_opts := #{forwards := Forwards}}) -> - {keep_state_and_data, {reply, From, Forwards}}; -common(_StateName, {call, From}, get_subscriptions, #{connection := Connection}) -> - {keep_state_and_data, {reply, From, maps:get(subscriptions, Connection, #{})}}; -common(_StateName, {call, From}, Req, _State) -> - {keep_state_and_data, {reply, From, {error, {unsupported_request, Req}}}}; -common(_StateName, info, {'EXIT', _, _}, State) -> - {keep_state, State}; -common(StateName, Type, Content, #{name := Name} = State) -> - ?SLOG(error, #{ - msg => "bridge_discarded_event", - name => Name, - type => Type, - state_name => StateName, - content => Content - }), - {keep_state, State}. - -do_connect( +import_msg( #{ - connect_opts := ConnectOpts, - name := Name - } = State -) -> - case emqx_connector_mqtt_mod:start(ConnectOpts) of - {ok, Conn} -> - ?tp(info, connected, #{name => Name}), - {ok, State#{connection => Conn}}; - {error, Reason} -> - ConnectOpts1 = obfuscate(ConnectOpts), - ?SLOG(error, #{ - msg => "failed_to_connect", - config => ConnectOpts1, - reason => Reason - }), - {error, Reason, State} - end. - -do_send(#{connect_opts := #{forwards := undefined}}, Msg) -> - ?SLOG(error, #{ - msg => - "cannot_forward_messages_to_remote_broker" - "_as_'egress'_is_not_configured", - messages => Msg - }); -do_send( - #{ - connection := Connection, - mountpoint := Mountpoint, - connect_opts := #{forwards := Forwards} - } = State, - Msg -) -> - Vars = emqx_connector_mqtt_msg:make_pub_vars(Mountpoint, Forwards), - ExportMsg = emqx_connector_mqtt_msg:to_remote_msg(Msg, Vars), - ?SLOG(debug, #{ - msg => "publish_to_remote_broker", - message => Msg, - vars => Vars - }), - case emqx_connector_mqtt_mod:send(Connection, ExportMsg) of - ok -> - {ok, State}; - {ok, #{reason_code := RC}} when - RC =:= ?RC_SUCCESS; - RC =:= ?RC_NO_MATCHING_SUBSCRIBERS - -> - {ok, State}; - {ok, #{reason_code := RC, reason_code_name := RCN}} -> - ?SLOG(warning, #{ - msg => "publish_to_remote_node_falied", - message => Msg, - reason_code => RC, - reason_code_name => RCN - }), - {error, RCN}; - {error, Reason} -> - ?SLOG(info, #{ - msg => "mqtt_bridge_produce_failed", - reason => Reason - }), - {error, Reason} - end. - -do_send_async(#{connect_opts := #{forwards := undefined}}, Msg, _Callback) -> - %% TODO: eval callback with undefined error - ?SLOG(error, #{ - msg => - "cannot_forward_messages_to_remote_broker" - "_as_'egress'_is_not_configured", - messages => Msg - }); -do_send_async( - #{ - connection := Connection, - mountpoint := Mountpoint, - connect_opts := #{forwards := Forwards} + dup := Dup, + payload := Payload, + properties := Props, + qos := QoS, + retain := Retain, + topic := Topic }, - Msg, - Callback + #{server := Server} ) -> - Vars = emqx_connector_mqtt_msg:make_pub_vars(Mountpoint, Forwards), - ExportMsg = emqx_connector_mqtt_msg:to_remote_msg(Msg, Vars), - ?SLOG(debug, #{ - msg => "publish_to_remote_broker", - message => Msg, - vars => Vars - }), - emqx_connector_mqtt_mod:send_async(Connection, ExportMsg, Callback). + #{ + id => emqx_guid:to_hexstr(emqx_guid:gen()), + server => Server, + payload => Payload, + topic => Topic, + qos => QoS, + dup => Dup, + retain => Retain, + pub_props => printable_maps(Props), + message_received_at => erlang:system_time(millisecond) + }. -disconnect(#{connection := Conn} = State) when Conn =/= undefined -> - emqx_connector_mqtt_mod:stop(Conn), - State#{connection => undefined}; -disconnect(State) -> - State. - -format_mountpoint(undefined) -> - undefined; -format_mountpoint(Prefix) -> - binary:replace(iolist_to_binary(Prefix), <<"${node}">>, atom_to_binary(node(), utf8)). - -name(Id) -> list_to_atom(str(Id)). - -obfuscate(Map) -> +printable_maps(undefined) -> + #{}; +printable_maps(Headers) -> maps:fold( - fun(K, V, Acc) -> - case is_sensitive(K) of - true -> [{K, '***'} | Acc]; - false -> [{K, V} | Acc] - end + fun + ('User-Property', V0, AccIn) when is_list(V0) -> + AccIn#{ + 'User-Property' => maps:from_list(V0), + 'User-Property-Pairs' => [ + #{ + key => Key, + value => Value + } + || {Key, Value} <- V0 + ] + }; + (K, V0, AccIn) -> + AccIn#{K => V0} end, - [], - Map + #{}, + Headers ). -is_sensitive(password) -> true; -is_sensitive(ssl_opts) -> true; -is_sensitive(_) -> false. - -str(A) when is_atom(A) -> - atom_to_list(A); -str(B) when is_binary(B) -> - binary_to_list(B); -str(S) when is_list(S) -> - S. +%% TODO +% maybe_destroy_session(#{connect_opts := ConnectOpts = #{clean_start := false}} = State) -> +% try +% %% Destroy session if clean_start is not set. +% %% Ignore any crashes, just refresh the clean_start = true. +% _ = do_connect(State#{connect_opts => ConnectOpts#{clean_start => true}}), +% _ = disconnect(State), +% ok +% catch +% _:_ -> +% ok +% end; +% maybe_destroy_session(_State) -> +% ok. diff --git a/apps/emqx_connector/test/emqx_connector_mqtt_tests.erl b/apps/emqx_connector/test/emqx_connector_mqtt_tests.erl deleted file mode 100644 index 88c8b5218..000000000 --- a/apps/emqx_connector/test/emqx_connector_mqtt_tests.erl +++ /dev/null @@ -1,60 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2023 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_connector_mqtt_tests). - --include_lib("eunit/include/eunit.hrl"). --include_lib("emqx/include/emqx_mqtt.hrl"). - -send_and_ack_test() -> - %% delegate from gen_rpc to rpc for unit test - meck:new(emqtt, [passthrough, no_history]), - meck:expect( - emqtt, - start_link, - 1, - fun(_) -> - {ok, spawn_link(fun() -> ok end)} - end - ), - meck:expect(emqtt, connect, 1, {ok, dummy}), - meck:expect( - emqtt, - stop, - 1, - fun(Pid) -> Pid ! stop end - ), - meck:expect( - emqtt, - publish, - 2, - fun(Client, Msg) -> - Client ! {publish, Msg}, - %% as packet id - {ok, Msg} - end - ), - try - Max = 1, - Batch = lists:seq(1, Max), - {ok, Conn} = emqx_connector_mqtt_mod:start(#{server => "127.0.0.1:1883"}), - %% return last packet id as batch reference - {ok, _AckRef} = emqx_connector_mqtt_mod:send(Conn, Batch), - - ok = emqx_connector_mqtt_mod:stop(Conn) - after - meck:unload(emqtt) - end. diff --git a/apps/emqx_connector/test/emqx_connector_mqtt_worker_tests.erl b/apps/emqx_connector/test/emqx_connector_mqtt_worker_tests.erl deleted file mode 100644 index 49bff7bbc..000000000 --- a/apps/emqx_connector/test/emqx_connector_mqtt_worker_tests.erl +++ /dev/null @@ -1,101 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2023 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_connector_mqtt_worker_tests). - --include_lib("eunit/include/eunit.hrl"). --include_lib("emqx/include/emqx.hrl"). --include_lib("emqx/include/emqx_mqtt.hrl"). - --define(BRIDGE_NAME, test). --define(BRIDGE_REG_NAME, emqx_connector_mqtt_worker_test). --define(WAIT(PATTERN, TIMEOUT), - receive - PATTERN -> - ok - after TIMEOUT -> - error(timeout) - end -). - --export([start/1, send/2, stop/1]). - -start(#{connect_result := Result, test_pid := Pid, test_ref := Ref}) -> - case is_pid(Pid) of - true -> Pid ! {connection_start_attempt, Ref}; - false -> ok - end, - Result. - -send(SendFun, Batch) when is_function(SendFun, 2) -> - SendFun(Batch). - -stop(_Pid) -> ok. - -%% connect first, disconnect, then connect again -disturbance_test() -> - meck:new(emqx_connector_mqtt_mod, [passthrough, no_history]), - meck:expect(emqx_connector_mqtt_mod, start, 1, fun(Conf) -> start(Conf) end), - meck:expect(emqx_connector_mqtt_mod, send, 2, fun(SendFun, Batch) -> send(SendFun, Batch) end), - meck:expect(emqx_connector_mqtt_mod, stop, 1, fun(Pid) -> stop(Pid) end), - try - emqx_metrics:start_link(), - Ref = make_ref(), - TestPid = self(), - Config = make_config(Ref, TestPid, {ok, #{client_pid => TestPid}}), - {ok, Pid} = emqx_connector_mqtt_worker:start_link(Config#{name => bridge_disturbance}), - ?assertEqual(Pid, whereis(bridge_disturbance)), - ?WAIT({connection_start_attempt, Ref}, 1000), - Pid ! {disconnected, TestPid, test}, - ?WAIT({connection_start_attempt, Ref}, 1000), - emqx_metrics:stop(), - ok = emqx_connector_mqtt_worker:stop(Pid) - after - meck:unload(emqx_connector_mqtt_mod) - end. - -manual_start_stop_test() -> - meck:new(emqx_connector_mqtt_mod, [passthrough, no_history]), - meck:expect(emqx_connector_mqtt_mod, start, 1, fun(Conf) -> start(Conf) end), - meck:expect(emqx_connector_mqtt_mod, send, 2, fun(SendFun, Batch) -> send(SendFun, Batch) end), - meck:expect(emqx_connector_mqtt_mod, stop, 1, fun(Pid) -> stop(Pid) end), - try - emqx_metrics:start_link(), - Ref = make_ref(), - TestPid = self(), - BridgeName = manual_start_stop, - Config0 = make_config(Ref, TestPid, {ok, #{client_pid => TestPid}}), - Config = Config0#{start_type := manual}, - {ok, Pid} = emqx_connector_mqtt_worker:start_link(Config#{name => BridgeName}), - %% call ensure_started again should yield the same result - ok = emqx_connector_mqtt_worker:ensure_started(BridgeName), - emqx_connector_mqtt_worker:ensure_stopped(BridgeName), - emqx_metrics:stop(), - ok = emqx_connector_mqtt_worker:stop(Pid) - after - meck:unload(emqx_connector_mqtt_mod) - end. - -make_config(Ref, TestPid, Result) -> - #{ - start_type => auto, - subscriptions => undefined, - forwards => undefined, - reconnect_interval => 50, - test_pid => TestPid, - test_ref => Ref, - connect_result => Result - }. From c034cbf6de312d3023e3f8d8a12133e2133085ff Mon Sep 17 00:00:00 2001 From: firest Date: Tue, 31 Jan 2023 17:48:43 +0800 Subject: [PATCH 0371/1802] feat(authz): allow the placeholder to be anywhere in the topic for authz rules --- apps/emqx_authz/src/emqx_authz_rule.erl | 35 ++++++------------- .../emqx_authz/test/emqx_authz_rule_SUITE.erl | 34 ++++++++++++++++-- 2 files changed, 43 insertions(+), 26 deletions(-) diff --git a/apps/emqx_authz/src/emqx_authz_rule.erl b/apps/emqx_authz/src/emqx_authz_rule.erl index 4aa0983e6..306ca9433 100644 --- a/apps/emqx_authz/src/emqx_authz_rule.erl +++ b/apps/emqx_authz/src/emqx_authz_rule.erl @@ -100,15 +100,17 @@ compile_topic(<<"eq ", Topic/binary>>) -> compile_topic({eq, Topic}) -> {eq, emqx_topic:words(bin(Topic))}; compile_topic(Topic) -> - Words = emqx_topic:words(bin(Topic)), - case pattern(Words) of - true -> {pattern, Words}; - false -> Words + TopicBin = bin(Topic), + case + emqx_placeholder:preproc_tmpl( + TopicBin, + #{placeholders => [?PH_USERNAME, ?PH_CLIENTID]} + ) + of + [{str, _}] -> emqx_topic:words(TopicBin); + Tokens -> {pattern, Tokens} end. -pattern(Words) -> - lists:member(?PH_USERNAME, Words) orelse lists:member(?PH_CLIENTID, Words). - atom(B) when is_binary(B) -> try binary_to_existing_atom(B, utf8) @@ -202,8 +204,8 @@ match_who(_, _) -> match_topics(_ClientInfo, _Topic, []) -> false; match_topics(ClientInfo, Topic, [{pattern, PatternFilter} | Filters]) -> - TopicFilter = feed_var(ClientInfo, PatternFilter), - match_topic(emqx_topic:words(Topic), TopicFilter) orelse + TopicFilter = emqx_placeholder:proc_tmpl(PatternFilter, ClientInfo), + match_topic(emqx_topic:words(Topic), emqx_topic:words(TopicFilter)) orelse match_topics(ClientInfo, Topic, Filters); match_topics(ClientInfo, Topic, [TopicFilter | Filters]) -> match_topic(emqx_topic:words(Topic), TopicFilter) orelse @@ -213,18 +215,3 @@ match_topic(Topic, {'eq', TopicFilter}) -> Topic =:= TopicFilter; match_topic(Topic, TopicFilter) -> emqx_topic:match(Topic, TopicFilter). - -feed_var(ClientInfo, Pattern) -> - feed_var(ClientInfo, Pattern, []). -feed_var(_ClientInfo, [], Acc) -> - lists:reverse(Acc); -feed_var(ClientInfo = #{clientid := undefined}, [?PH_CLIENTID | Words], Acc) -> - feed_var(ClientInfo, Words, [?PH_CLIENTID | Acc]); -feed_var(ClientInfo = #{clientid := ClientId}, [?PH_CLIENTID | Words], Acc) -> - feed_var(ClientInfo, Words, [ClientId | Acc]); -feed_var(ClientInfo = #{username := undefined}, [?PH_USERNAME | Words], Acc) -> - feed_var(ClientInfo, Words, [?PH_USERNAME | Acc]); -feed_var(ClientInfo = #{username := Username}, [?PH_USERNAME | Words], Acc) -> - feed_var(ClientInfo, Words, [Username | Acc]); -feed_var(ClientInfo, [W | Words], Acc) -> - feed_var(ClientInfo, Words, [W | Acc]). diff --git a/apps/emqx_authz/test/emqx_authz_rule_SUITE.erl b/apps/emqx_authz/test/emqx_authz_rule_SUITE.erl index 77f8617ee..76e5677ce 100644 --- a/apps/emqx_authz/test/emqx_authz_rule_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_rule_SUITE.erl @@ -35,6 +35,7 @@ ]}, publish, [?PH_S_USERNAME, ?PH_S_CLIENTID]} ). +-define(SOURCE6, {allow, {username, "test"}, publish, ["t/foo${username}boo"]}). all() -> emqx_common_test_helpers:all(?MODULE). @@ -80,7 +81,7 @@ t_compile(_) -> {{127, 0, 0, 1}, {127, 0, 0, 1}, 32}, {{192, 168, 1, 0}, {192, 168, 1, 255}, 24} ]}, - subscribe, [{pattern, [?PH_CLIENTID]}]}, + subscribe, [{pattern, [{var, {var, <<"clientid">>}}]}]}, emqx_authz_rule:compile(?SOURCE3) ), @@ -97,9 +98,18 @@ t_compile(_) -> {username, {re_pattern, _, _, _, _}}, {clientid, {re_pattern, _, _, _, _}} ]}, - publish, [{pattern, [?PH_USERNAME]}, {pattern, [?PH_CLIENTID]}]}, + publish, [ + {pattern, [{var, {var, <<"username">>}}]}, {pattern, [{var, {var, <<"clientid">>}}]} + ]}, emqx_authz_rule:compile(?SOURCE5) ), + + ?assertEqual( + {allow, {username, {eq, <<"test">>}}, publish, [ + {pattern, [{str, <<"t/foo">>}, {var, {var, <<"username">>}}, {str, <<"boo">>}]} + ]}, + emqx_authz_rule:compile(?SOURCE6) + ), ok. t_match(_) -> @@ -307,4 +317,24 @@ t_match(_) -> emqx_authz_rule:compile(?SOURCE5) ) ), + + ?assertEqual( + nomatch, + emqx_authz_rule:match( + ClientInfo1, + publish, + <<"t/foo${username}boo">>, + emqx_authz_rule:compile(?SOURCE6) + ) + ), + + ?assertEqual( + {matched, allow}, + emqx_authz_rule:match( + ClientInfo4, + publish, + <<"t/footestboo">>, + emqx_authz_rule:compile(?SOURCE6) + ) + ), ok. From 9f4c36ecbc4e72f04937bd7c234bf7ffd351b22e Mon Sep 17 00:00:00 2001 From: firest Date: Tue, 31 Jan 2023 18:04:45 +0800 Subject: [PATCH 0372/1802] chore: bump version && update changes --- apps/emqx_authz/src/emqx_authz.app.src | 2 +- changes/v5.0.16/feat-9871.en.md | 3 +++ changes/v5.0.16/feat-9871.zh.md | 3 +++ 3 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 changes/v5.0.16/feat-9871.en.md create mode 100644 changes/v5.0.16/feat-9871.zh.md diff --git a/apps/emqx_authz/src/emqx_authz.app.src b/apps/emqx_authz/src/emqx_authz.app.src index 6a4b721e9..3fea50147 100644 --- a/apps/emqx_authz/src/emqx_authz.app.src +++ b/apps/emqx_authz/src/emqx_authz.app.src @@ -1,7 +1,7 @@ %% -*- mode: erlang -*- {application, emqx_authz, [ {description, "An OTP application"}, - {vsn, "0.1.12"}, + {vsn, "0.1.13"}, {registered, []}, {mod, {emqx_authz_app, []}}, {applications, [ diff --git a/changes/v5.0.16/feat-9871.en.md b/changes/v5.0.16/feat-9871.en.md new file mode 100644 index 000000000..b907aa3f1 --- /dev/null +++ b/changes/v5.0.16/feat-9871.en.md @@ -0,0 +1,3 @@ +Allow the placeholder to be anywhere in the topic for `authz` rules. +e.g: +`{allow, {username, "who"}, publish, ["t/foo${username}boo/${clientid}xxx"]}.` diff --git a/changes/v5.0.16/feat-9871.zh.md b/changes/v5.0.16/feat-9871.zh.md new file mode 100644 index 000000000..ecd526a93 --- /dev/null +++ b/changes/v5.0.16/feat-9871.zh.md @@ -0,0 +1,3 @@ +允许占位符出现在 `authz` 规则中的主题里的任意位置。 +例如: +`{allow, {username, "who"}, publish, ["t/foo${username}boo/${clientid}xxx"]}.` From c76311c9c306c8ede80b54691caedb52a9d5442c Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Tue, 31 Jan 2023 14:17:45 +0300 Subject: [PATCH 0373/1802] fix(buffer): count inflight batches properly --- .../src/emqx_resource_buffer_worker.erl | 9 +++--- .../test/emqx_resource_SUITE.erl | 32 +++++++++++-------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/apps/emqx_resource/src/emqx_resource_buffer_worker.erl b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl index 50534df4f..c5395c8df 100644 --- a/apps/emqx_resource/src/emqx_resource_buffer_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl @@ -1121,6 +1121,10 @@ append_queue(Id, Index, Q, Queries) -> -define(INITIAL_TIME_REF, initial_time). -define(INITIAL_MONOTONIC_TIME_REF, initial_monotonic_time). +%% NOTE +%% There are 4 metadata rows in an inflight table, keyed by atoms declared above. ☝ +-define(INFLIGHT_META_ROWS, 4). + inflight_new(InfltWinSZ, Id, Index) -> TableId = ets:new( emqx_resource_buffer_worker_inflight_tab, @@ -1181,12 +1185,9 @@ is_inflight_full(InflightTID) -> Size >= MaxSize. inflight_num_batches(InflightTID) -> - %% Note: we subtract 2 because there're 2 metadata rows that hold - %% the maximum size value and the number of messages. - MetadataRowCount = 2, case ets:info(InflightTID, size) of undefined -> 0; - Size -> max(0, Size - MetadataRowCount) + Size -> max(0, Size - ?INFLIGHT_META_ROWS) end. inflight_num_msgs(InflightTID) -> diff --git a/apps/emqx_resource/test/emqx_resource_SUITE.erl b/apps/emqx_resource/test/emqx_resource_SUITE.erl index 227b6fedc..27101d1cc 100644 --- a/apps/emqx_resource/test/emqx_resource_SUITE.erl +++ b/apps/emqx_resource/test/emqx_resource_SUITE.erl @@ -411,7 +411,8 @@ t_query_counter_async_inflight(_) -> ?check_trace( {_, {ok, _}} = ?wait_async_action( - inc_counter_in_parallel(WindowSize, ReqOpts), + %% one more so that inflight would be already full upon last query + inc_counter_in_parallel(WindowSize + 1, ReqOpts), #{?snk_kind := buffer_worker_flush_but_inflight_full}, 1_000 ), @@ -445,9 +446,9 @@ t_query_counter_async_inflight(_) -> %% all responses should be received after the resource is resumed. {ok, SRef0} = snabbkaffe:subscribe( ?match_event(#{?snk_kind := connector_demo_inc_counter_async}), - %% +1 because the tmp_query above will be retried and succeed + %% +2 because the tmp_query above will be retried and succeed %% this time. - WindowSize + 1, + WindowSize + 2, _Timeout0 = 10_000 ), ?assertMatch(ok, emqx_resource:simple_sync_query(?ID, resume)), @@ -475,7 +476,7 @@ t_query_counter_async_inflight(_) -> fun(Trace) -> QueryTrace = ?of_kind(call_query_async, Trace), ?assertMatch([#{query := {query, _, {inc_counter, _}, _, _}} | _], QueryTrace), - ?assertEqual(WindowSize + Num, ets:info(Tab0, size), #{tab => ets:tab2list(Tab0)}), + ?assertEqual(WindowSize + Num + 1, ets:info(Tab0, size), #{tab => ets:tab2list(Tab0)}), tap_metrics(?LINE), ok end @@ -487,7 +488,8 @@ t_query_counter_async_inflight(_) -> ?check_trace( {_, {ok, _}} = ?wait_async_action( - inc_counter_in_parallel(WindowSize, ReqOpts), + %% one more so that inflight would be already full upon last query + inc_counter_in_parallel(WindowSize + 1, ReqOpts), #{?snk_kind := buffer_worker_flush_but_inflight_full}, 1_000 ), @@ -500,10 +502,10 @@ t_query_counter_async_inflight(_) -> %% this will block the resource_worker ok = emqx_resource:query(?ID, {inc_counter, 4}), - Sent = WindowSize + Num + WindowSize, + Sent = WindowSize + 1 + Num + WindowSize + 1, {ok, SRef1} = snabbkaffe:subscribe( ?match_event(#{?snk_kind := connector_demo_inc_counter_async}), - WindowSize, + WindowSize + 1, _Timeout0 = 10_000 ), ?assertMatch(ok, emqx_resource:simple_sync_query(?ID, resume)), @@ -593,7 +595,8 @@ t_query_counter_async_inflight_batch(_) -> ?check_trace( {_, {ok, _}} = ?wait_async_action( - inc_counter_in_parallel(NumMsgs, ReqOpts), + %% a batch more so that inflight would be already full upon last query + inc_counter_in_parallel(NumMsgs + BatchSize, ReqOpts), #{?snk_kind := buffer_worker_flush_but_inflight_full}, 5_000 ), @@ -652,9 +655,9 @@ t_query_counter_async_inflight_batch(_) -> %% all responses should be received after the resource is resumed. {ok, SRef0} = snabbkaffe:subscribe( ?match_event(#{?snk_kind := connector_demo_inc_counter_async}), - %% +1 because the tmp_query above will be retried and succeed + %% +2 because the tmp_query above will be retried and succeed %% this time. - WindowSize + 1, + WindowSize + 2, 10_000 ), ?assertMatch(ok, emqx_resource:simple_sync_query(?ID, resume)), @@ -664,7 +667,7 @@ t_query_counter_async_inflight_batch(_) -> %% take it again from the table; this time, it should have %% succeeded. ?assertMatch([{tmp_query, ok}], ets:take(Tab0, tmp_query)), - ?assertEqual(NumMsgs, ets:info(Tab0, size), #{tab => ets:tab2list(Tab0)}), + ?assertEqual(NumMsgs + BatchSize, ets:info(Tab0, size), #{tab => ets:tab2list(Tab0)}), tap_metrics(?LINE), %% send async query, this time everything should be ok. @@ -691,7 +694,7 @@ t_query_counter_async_inflight_batch(_) -> end ), ?assertEqual( - NumMsgs + NumMsgs1, + NumMsgs + BatchSize + NumMsgs1, ets:info(Tab0, size), #{tab => ets:tab2list(Tab0)} ), @@ -703,7 +706,8 @@ t_query_counter_async_inflight_batch(_) -> ?check_trace( {_, {ok, _}} = ?wait_async_action( - inc_counter_in_parallel(NumMsgs, ReqOpts), + %% a batch more so that inflight would be already full upon last query + inc_counter_in_parallel(NumMsgs + BatchSize, ReqOpts), #{?snk_kind := buffer_worker_flush_but_inflight_full}, 5_000 ), @@ -719,7 +723,7 @@ t_query_counter_async_inflight_batch(_) -> %% this will block the resource_worker ok = emqx_resource:query(?ID, {inc_counter, 1}), - Sent = NumMsgs + NumMsgs1 + NumMsgs, + Sent = NumMsgs + BatchSize + NumMsgs1 + NumMsgs, {ok, SRef1} = snabbkaffe:subscribe( ?match_event(#{?snk_kind := connector_demo_inc_counter_async}), WindowSize, From c5a7cd5acd38440d1be63dd5690ea688bbe5232f Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Tue, 31 Jan 2023 15:20:46 +0300 Subject: [PATCH 0374/1802] fix(mqtt-bridge): drop unused configuration parameter --- .../src/schema/emqx_bridge_compatible_config.erl | 1 - .../test/emqx_bridge_compatible_config_tests.erl | 2 -- apps/emqx_connector/src/mqtt/emqx_connector_mqtt_schema.erl | 6 ------ 3 files changed, 9 deletions(-) diff --git a/apps/emqx_bridge/src/schema/emqx_bridge_compatible_config.erl b/apps/emqx_bridge/src/schema/emqx_bridge_compatible_config.erl index 862b5e188..1e55d0c0e 100644 --- a/apps/emqx_bridge/src/schema/emqx_bridge_compatible_config.erl +++ b/apps/emqx_bridge/src/schema/emqx_bridge_compatible_config.erl @@ -72,7 +72,6 @@ up(#{<<"connector">> := Connector} = Config) -> Cn(proto_ver, <<"v4">>), Cn(server, undefined), Cn(retry_interval, <<"15s">>), - Cn(reconnect_interval, <<"15s">>), Cn(ssl, default_ssl()), {enable, Enable}, {resource_opts, default_resource_opts()}, diff --git a/apps/emqx_bridge/test/emqx_bridge_compatible_config_tests.erl b/apps/emqx_bridge/test/emqx_bridge_compatible_config_tests.erl index a2671a40e..36dd6324a 100644 --- a/apps/emqx_bridge/test/emqx_bridge_compatible_config_tests.erl +++ b/apps/emqx_bridge/test/emqx_bridge_compatible_config_tests.erl @@ -224,7 +224,6 @@ bridges { mode = \"cluster_shareload\" password = \"\" proto_ver = \"v5\" - reconnect_interval = \"15s\" replayq {offload = false, seg_bytes = \"100MB\"} retry_interval = \"12s\" server = \"localhost:1883\" @@ -257,7 +256,6 @@ bridges { mode = \"cluster_shareload\" password = \"\" proto_ver = \"v4\" - reconnect_interval = \"15s\" replayq {offload = false, seg_bytes = \"100MB\"} retry_interval = \"44s\" server = \"localhost:1883\" diff --git a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_schema.erl b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_schema.erl index be462fcc1..6ea609cc6 100644 --- a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_schema.erl +++ b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_schema.erl @@ -72,12 +72,6 @@ fields("server_configs") -> )}, {server, emqx_schema:servers_sc(#{desc => ?DESC("server")}, ?MQTT_HOST_OPTS)}, {clientid_prefix, mk(binary(), #{required => false, desc => ?DESC("clientid_prefix")})}, - {reconnect_interval, - mk_duration( - "Reconnect interval. Delay for the MQTT bridge to retry establishing the connection " - "in case of transportation failure.", - #{default => "15s"} - )}, {proto_ver, mk( hoconsc:enum([v3, v4, v5]), From b0ac924ca91b9fe5801d73406d5a15f33276b316 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Tue, 31 Jan 2023 14:40:06 +0100 Subject: [PATCH 0375/1802] refactor: less copy-paste --- apps/emqx/src/emqx_schema.erl | 12 ++++-------- apps/emqx_conf/src/emqx_conf_schema.erl | 15 ++------------- 2 files changed, 6 insertions(+), 21 deletions(-) diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 8f016f684..48bd206c9 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -1815,16 +1815,12 @@ desc(_) -> %% utils -spec conf_get(string() | [string()], hocon:config()) -> term(). conf_get(Key, Conf) -> - V = hocon_maps:get(Key, Conf), - case is_binary(V) of - true -> - binary_to_list(V); - false -> - V - end. + ensure_list(hocon_maps:get(Key, Conf)). conf_get(Key, Conf, Default) -> - V = hocon_maps:get(Key, Conf, Default), + ensure_list(hocon_maps:get(Key, Conf, Default)). + +ensure_list(V) -> case is_binary(V) of true -> binary_to_list(V); diff --git a/apps/emqx_conf/src/emqx_conf_schema.erl b/apps/emqx_conf/src/emqx_conf_schema.erl index bc9154933..12f5a00ec 100644 --- a/apps/emqx_conf/src/emqx_conf_schema.erl +++ b/apps/emqx_conf/src/emqx_conf_schema.erl @@ -1308,11 +1308,9 @@ crash_dump_file_default() -> %% utils -spec conf_get(string() | [string()], hocon:config()) -> term(). -conf_get(Key, Conf) -> - ensure_list(hocon_maps:get(Key, Conf)). +conf_get(Key, Conf) -> emqx_schema:conf_get(Key, Conf). -conf_get(Key, Conf, Default) -> - ensure_list(hocon_maps:get(Key, Conf, Default)). +conf_get(Key, Conf, Default) -> emqx_schema:conf_get(Key, Conf, Default). filter(Opts) -> [{K, V} || {K, V} <- Opts, V =/= undefined]. @@ -1376,15 +1374,6 @@ to_atom(Str) when is_list(Str) -> to_atom(Bin) when is_binary(Bin) -> binary_to_atom(Bin, utf8). --spec ensure_list(binary() | list(char())) -> list(char()). -ensure_list(V) -> - case is_binary(V) of - true -> - binary_to_list(V); - false -> - V - end. - roots(Module) -> lists:map(fun({_BinName, Root}) -> Root end, hocon_schema:roots(Module)). From 605d9972e467b5b341ca283731bbee781196fc2a Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Tue, 31 Jan 2023 18:13:13 +0100 Subject: [PATCH 0376/1802] fix(config): avoid calling emqx:update_config/2 for logger refresh Prior to this fix, whatever configured for loggers are persisted to cluster-override.conf. This may cause troulbe for users who changes boot mode. For example if the node is once started in console mode, the logging config is persisted, with console enabled, but file disabled. Then if the user decide to start in daemon mode, all the logs will silently go to erlang.log.N and emqx.log.N will be empty. After this fix, only changes really made into cluter-override.conf will take effect. --- apps/emqx/src/config/emqx_config_logger.erl | 241 +++++++++++++++++--- apps/emqx_conf/src/emqx_conf_schema.erl | 140 +----------- 2 files changed, 218 insertions(+), 163 deletions(-) diff --git a/apps/emqx/src/config/emqx_config_logger.erl b/apps/emqx/src/config/emqx_config_logger.erl index 4b46e43d6..4d787ea0c 100644 --- a/apps/emqx/src/config/emqx_config_logger.erl +++ b/apps/emqx/src/config/emqx_config_logger.erl @@ -18,6 +18,7 @@ -behaviour(emqx_config_handler). %% API +-export([tr_handlers/1, tr_level/1]). -export([add_handler/0, remove_handler/0, refresh_config/0]). -export([post_config_update/5]). @@ -37,38 +38,224 @@ remove_handler() -> %% so we need to refresh the logger config after this node starts. %% It will not affect the logger config when cluster-override.conf is unchanged. refresh_config() -> - case emqx:get_raw_config(?LOG, undefined) of - %% no logger config when CT is running. - undefined -> - ok; - Log -> - {ok, _} = emqx:update_config(?LOG, Log), - ok - end. + Overrides = emqx_config:read_override_confs(), + refresh_config(Overrides). -post_config_update(?LOG, _Req, _NewConf, _OldConf, AppEnvs) -> - Kernel = proplists:get_value(kernel, AppEnvs), - NewHandlers = proplists:get_value(logger, Kernel, []), - Level = proplists:get_value(logger_level, Kernel, warning), - ok = update_log_handlers(NewHandlers), - ok = emqx_logger:set_primary_log_level(Level), - application:set_env(kernel, logger_level, Level), - ok; +refresh_config(#{<<"log">> := _}) -> + %% read the checked config + LogConfig = emqx:get_config(?LOG, undefined), + Conf = #{log => LogConfig}, + ok = do_refresh_config(Conf); +refresh_config(_) -> + %% No config override found for 'log', do nothing + %% because the 'kernel' app should already be configured + %% from the base configs. i.e. emqx.conf + env vars + ok. + +%% this call is shared between initial config refresh at boot +%% and dynamic config update from HTTP API +do_refresh_config(Conf) -> + Handlers = tr_handlers(Conf), + ok = update_log_handlers(Handlers), + Level = tr_level(Conf), + ok = maybe_update_log_level(Level), + ok. + +post_config_update(?LOG, _Req, NewConf, _OldConf, _AppEnvs) -> + ok = do_refresh_config(#{log => NewConf}); post_config_update(_ConfPath, _Req, _NewConf, _OldConf, _AppEnvs) -> ok. +maybe_update_log_level(NewLevel) -> + OldLevel = application:get_env(kernel, logger_level, warning), + case OldLevel =:= NewLevel of + true -> + %% no change + ok; + false -> + log_to_console("Config override: log level is set to '~p'~n", [NewLevel]) + end. + +log_to_console(Fmt, Args) -> + io:format(standard_error, Fmt, Args). + update_log_handlers(NewHandlers) -> OldHandlers = application:get_env(kernel, logger, []), - lists:foreach( - fun({handler, HandlerId, _Mod, _Conf}) -> - logger:remove_handler(HandlerId) + NewHandlersIds = lists:map(fun({handler, Id, _Mod, _Conf}) -> Id end, NewHandlers), + OldHandlersIds = lists:map(fun({handler, Id, _Mod, _Conf}) -> Id end, OldHandlers), + Removes = lists:map(fun(Id) -> {removed, Id} end, OldHandlersIds -- NewHandlersIds), + MapFn = fun({handler, Id, Mod, Conf} = Handler) -> + case lists:keyfind(Id, 2, OldHandlers) of + {handler, Id, Mod, Conf} -> + %% no change + false; + {handler, Id, _Mod, _Conf} -> + {true, {updated, Handler}}; + false -> + {true, {enabled, Handler}} + end + end, + AddsAndUpdates = lists:filtermap(MapFn, NewHandlers), + lists:foreach(fun update_log_handler/1, Removes ++ AddsAndUpdates), + _ = application:set_env(kernel, logger, NewHandlers), + ok. + +update_log_handler({removed, Id}) -> + log_to_console("Config override: ~s is removed~n", [id_for_log(Id)]), + logger:remove_handler(Id); +update_log_handler({Action, {handler, Id, Mod, Conf}}) -> + log_to_console("Config override: ~s is ~p~n", [id_for_log(Id), Action]), + % may return {error, {not_found, Id}} + _ = logger:remove_handler(Id), + ok = logger:add_handler(Id, Mod, Conf). + +id_for_log(console) -> "log.console_handler"; +id_for_log(Other) -> "log.file_handlers." ++ atom_to_list(Other). + +atom(Id) when is_binary(Id) -> binary_to_atom(Id, utf8); +atom(Id) when is_atom(Id) -> Id. + +%% @doc Translate raw config to app-env conpatible log handler configs list. +tr_handlers(Conf) -> + %% mute the default handler + tr_console_handler(Conf) ++ + tr_file_handlers(Conf). + +%% For the default logger that outputs to console +tr_console_handler(Conf) -> + case conf_get("log.console_handler.enable", Conf) of + true -> + ConsoleConf = conf_get("log.console_handler", Conf), + [ + {handler, console, logger_std_h, #{ + level => conf_get("log.console_handler.level", Conf), + config => (log_handler_conf(ConsoleConf))#{type => standard_io}, + formatter => log_formatter(ConsoleConf), + filters => log_filter(ConsoleConf) + }} + ]; + false -> + [] + end. + +%% For the file logger +tr_file_handlers(Conf) -> + Handlers = logger_file_handlers(Conf), + lists:map(fun tr_file_handler/1, Handlers). + +tr_file_handler({HandlerName, SubConf}) -> + {handler, atom(HandlerName), logger_disk_log_h, #{ + level => conf_get("level", SubConf), + config => (log_handler_conf(SubConf))#{ + type => + case conf_get("rotation.enable", SubConf) of + true -> wrap; + _ -> halt + end, + file => conf_get("file", SubConf), + max_no_files => conf_get("rotation.count", SubConf), + max_no_bytes => conf_get("max_size", SubConf) + }, + formatter => log_formatter(SubConf), + filters => log_filter(SubConf), + filesync_repeat_interval => no_repeat + }}. + +logger_file_handlers(Conf) -> + Handlers = maps:to_list(conf_get("log.file_handlers", Conf, #{})), + lists:filter( + fun({_Name, Opts}) -> + B = conf_get("enable", Opts), + true = is_boolean(B), + B end, - OldHandlers -- NewHandlers - ), - lists:foreach( - fun({handler, HandlerId, Mod, Conf}) -> - logger:add_handler(HandlerId, Mod, Conf) + Handlers + ). + +conf_get(Key, Conf) -> emqx_schema:conf_get(Key, Conf). +conf_get(Key, Conf, Default) -> emqx_schema:conf_get(Key, Conf, Default). + +log_handler_conf(Conf) -> + SycModeQlen = conf_get("sync_mode_qlen", Conf), + DropModeQlen = conf_get("drop_mode_qlen", Conf), + FlushQlen = conf_get("flush_qlen", Conf), + Overkill = conf_get("overload_kill", Conf), + BurstLimit = conf_get("burst_limit", Conf), + #{ + sync_mode_qlen => SycModeQlen, + drop_mode_qlen => DropModeQlen, + flush_qlen => FlushQlen, + overload_kill_enable => conf_get("enable", Overkill), + overload_kill_qlen => conf_get("qlen", Overkill), + overload_kill_mem_size => conf_get("mem_size", Overkill), + overload_kill_restart_after => conf_get("restart_after", Overkill), + burst_limit_enable => conf_get("enable", BurstLimit), + burst_limit_max_count => conf_get("max_count", BurstLimit), + burst_limit_window_time => conf_get("window_time", BurstLimit) + }. + +log_formatter(Conf) -> + CharsLimit = + case conf_get("chars_limit", Conf) of + unlimited -> unlimited; + V when V > 0 -> V end, - NewHandlers -- OldHandlers - ), - application:set_env(kernel, logger, NewHandlers). + TimeOffSet = + case conf_get("time_offset", Conf) of + "system" -> ""; + "utc" -> 0; + OffSetStr -> OffSetStr + end, + SingleLine = conf_get("single_line", Conf), + Depth = conf_get("max_depth", Conf), + do_formatter(conf_get("formatter", Conf), CharsLimit, SingleLine, TimeOffSet, Depth). + +%% helpers +do_formatter(json, CharsLimit, SingleLine, TimeOffSet, Depth) -> + {emqx_logger_jsonfmt, #{ + chars_limit => CharsLimit, + single_line => SingleLine, + time_offset => TimeOffSet, + depth => Depth + }}; +do_formatter(text, CharsLimit, SingleLine, TimeOffSet, Depth) -> + {emqx_logger_textfmt, #{ + template => [time, " [", level, "] ", msg, "\n"], + chars_limit => CharsLimit, + single_line => SingleLine, + time_offset => TimeOffSet, + depth => Depth + }}. + +log_filter(Conf) -> + case conf_get("supervisor_reports", Conf) of + error -> [{drop_progress_reports, {fun logger_filters:progress/2, stop}}]; + progress -> [] + end. + +tr_level(Conf) -> + ConsoleLevel = conf_get("log.console_handler.level", Conf, undefined), + FileLevels = [ + conf_get("level", SubConf) + || {_, SubConf} <- + logger_file_handlers(Conf) + ], + case FileLevels ++ [ConsoleLevel || ConsoleLevel =/= undefined] of + %% warning is the default level we should use + [] -> warning; + Levels -> least_severe_log_level(Levels) + end. + +least_severe_log_level(Levels) -> + hd(sort_log_levels(Levels)). + +sort_log_levels(Levels) -> + lists:sort( + fun(A, B) -> + case logger:compare_levels(A, B) of + R when R == lt; R == eq -> true; + gt -> false + end + end, + Levels + ). diff --git a/apps/emqx_conf/src/emqx_conf_schema.erl b/apps/emqx_conf/src/emqx_conf_schema.erl index 12f5a00ec..9793e00d0 100644 --- a/apps/emqx_conf/src/emqx_conf_schema.erl +++ b/apps/emqx_conf/src/emqx_conf_schema.erl @@ -993,7 +993,7 @@ translation("ekka") -> translation("kernel") -> [ {"logger_level", fun tr_logger_level/1}, - {"logger", fun tr_logger/1}, + {"logger", fun tr_logger_handlers/1}, {"error_logger", fun(_) -> silent end} ]; translation("emqx") -> @@ -1065,70 +1065,10 @@ tr_cluster_discovery(Conf) -> -spec tr_logger_level(hocon:config()) -> logger:level(). tr_logger_level(Conf) -> - ConsoleLevel = conf_get("log.console_handler.level", Conf, undefined), - FileLevels = [ - conf_get("level", SubConf) - || {_, SubConf} <- - logger_file_handlers(Conf) - ], - case FileLevels ++ [ConsoleLevel || ConsoleLevel =/= undefined] of - %% warning is the default level we should use - [] -> warning; - Levels -> least_severe_log_level(Levels) - end. + emqx_config_logger:tr_level(Conf). -logger_file_handlers(Conf) -> - Handlers = maps:to_list(conf_get("log.file_handlers", Conf, #{})), - lists:filter( - fun({_Name, Opts}) -> - B = conf_get("enable", Opts), - true = is_boolean(B), - B - end, - Handlers - ). - -tr_logger(Conf) -> - %% For the default logger that outputs to console - ConsoleHandler = - case conf_get("log.console_handler.enable", Conf) of - true -> - ConsoleConf = conf_get("log.console_handler", Conf), - [ - {handler, console, logger_std_h, #{ - level => conf_get("log.console_handler.level", Conf), - config => (log_handler_conf(ConsoleConf))#{type => standard_io}, - formatter => log_formatter(ConsoleConf), - filters => log_filter(ConsoleConf) - }} - ]; - false -> - [] - end, - %% For the file logger - FileHandlers = - [ - begin - {handler, to_atom(HandlerName), logger_disk_log_h, #{ - level => conf_get("level", SubConf), - config => (log_handler_conf(SubConf))#{ - type => - case conf_get("rotation.enable", SubConf) of - true -> wrap; - _ -> halt - end, - file => conf_get("file", SubConf), - max_no_files => conf_get("rotation.count", SubConf), - max_no_bytes => conf_get("max_size", SubConf) - }, - formatter => log_formatter(SubConf), - filters => log_filter(SubConf), - filesync_repeat_interval => no_repeat - }} - end - || {HandlerName, SubConf} <- logger_file_handlers(Conf) - ], - [{handler, default, undefined}] ++ ConsoleHandler ++ FileHandlers. +tr_logger_handlers(Conf) -> + emqx_config_logger:tr_handlers(Conf). log_handler_common_confs(Enable) -> [ @@ -1225,78 +1165,6 @@ log_handler_common_confs(Enable) -> )} ]. -log_handler_conf(Conf) -> - SycModeQlen = conf_get("sync_mode_qlen", Conf), - DropModeQlen = conf_get("drop_mode_qlen", Conf), - FlushQlen = conf_get("flush_qlen", Conf), - Overkill = conf_get("overload_kill", Conf), - BurstLimit = conf_get("burst_limit", Conf), - #{ - sync_mode_qlen => SycModeQlen, - drop_mode_qlen => DropModeQlen, - flush_qlen => FlushQlen, - overload_kill_enable => conf_get("enable", Overkill), - overload_kill_qlen => conf_get("qlen", Overkill), - overload_kill_mem_size => conf_get("mem_size", Overkill), - overload_kill_restart_after => conf_get("restart_after", Overkill), - burst_limit_enable => conf_get("enable", BurstLimit), - burst_limit_max_count => conf_get("max_count", BurstLimit), - burst_limit_window_time => conf_get("window_time", BurstLimit) - }. - -log_formatter(Conf) -> - CharsLimit = - case conf_get("chars_limit", Conf) of - unlimited -> unlimited; - V when V > 0 -> V - end, - TimeOffSet = - case conf_get("time_offset", Conf) of - "system" -> ""; - "utc" -> 0; - OffSetStr -> OffSetStr - end, - SingleLine = conf_get("single_line", Conf), - Depth = conf_get("max_depth", Conf), - do_formatter(conf_get("formatter", Conf), CharsLimit, SingleLine, TimeOffSet, Depth). - -%% helpers -do_formatter(json, CharsLimit, SingleLine, TimeOffSet, Depth) -> - {emqx_logger_jsonfmt, #{ - chars_limit => CharsLimit, - single_line => SingleLine, - time_offset => TimeOffSet, - depth => Depth - }}; -do_formatter(text, CharsLimit, SingleLine, TimeOffSet, Depth) -> - {emqx_logger_textfmt, #{ - template => [time, " [", level, "] ", msg, "\n"], - chars_limit => CharsLimit, - single_line => SingleLine, - time_offset => TimeOffSet, - depth => Depth - }}. - -log_filter(Conf) -> - case conf_get("supervisor_reports", Conf) of - error -> [{drop_progress_reports, {fun logger_filters:progress/2, stop}}]; - progress -> [] - end. - -least_severe_log_level(Levels) -> - hd(sort_log_levels(Levels)). - -sort_log_levels(Levels) -> - lists:sort( - fun(A, B) -> - case logger:compare_levels(A, B) of - R when R == lt; R == eq -> true; - gt -> false - end - end, - Levels - ). - crash_dump_file_default() -> case os:getenv("RUNNER_LOG_DIR") of false -> From ddfbc0a19f489219902f26af228c5f2de0799db9 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Tue, 31 Jan 2023 18:33:10 +0100 Subject: [PATCH 0377/1802] refactor: refresh logger config before starting listeners --- apps/emqx/src/emqx_app.erl | 1 + apps/emqx/src/emqx_config.erl | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx/src/emqx_app.erl b/apps/emqx/src/emqx_app.erl index 6188d8030..c56cdb520 100644 --- a/apps/emqx/src/emqx_app.erl +++ b/apps/emqx/src/emqx_app.erl @@ -42,6 +42,7 @@ start(_Type, _Args) -> ok = maybe_load_config(), ok = emqx_persistent_session:init_db_backend(), + ok = emqx_config_logger:refresh_config(), ok = maybe_start_quicer(), ok = emqx_bpapi:start(), wait_boot_shards(), diff --git a/apps/emqx/src/emqx_config.erl b/apps/emqx/src/emqx_config.erl index 117043911..6d706316c 100644 --- a/apps/emqx/src/emqx_config.erl +++ b/apps/emqx/src/emqx_config.erl @@ -592,7 +592,6 @@ save_to_override_conf(RawConf, Opts) -> add_handlers() -> ok = emqx_config_logger:add_handler(), emqx_sys_mon:add_handler(), - emqx_config_logger:refresh_config(), ok. remove_handlers() -> From 0c80c31c9ecf4d37b2c207129df9665ef7acac59 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Tue, 31 Jan 2023 20:43:59 +0100 Subject: [PATCH 0378/1802] test: delete log override from influxdb bridge test script --- scripts/test/influx/influx-bridge.conf | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/scripts/test/influx/influx-bridge.conf b/scripts/test/influx/influx-bridge.conf index 31ddeaf79..3b5bb9f9f 100644 --- a/scripts/test/influx/influx-bridge.conf +++ b/scripts/test/influx/influx-bridge.conf @@ -34,16 +34,6 @@ bridges { } } } -log { - console_handler {enable = true, level = "warning"} - file_handlers { - default { - enable = false - file = "log/emqx.log" - level = "warning" - } - } -} rule_engine { ignore_sys_message = true jq_function_default_timeout = "10s" From 7e8253e3af5a0c1dc97252ce11f4bd65dfcaa614 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Tue, 31 Jan 2023 20:45:02 +0100 Subject: [PATCH 0379/1802] chore: bump version to e5.0.0-rc.3 --- apps/emqx/include/emqx_release.hrl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx/include/emqx_release.hrl b/apps/emqx/include/emqx_release.hrl index d3e0cd3b0..e3429aef1 100644 --- a/apps/emqx/include/emqx_release.hrl +++ b/apps/emqx/include/emqx_release.hrl @@ -35,7 +35,7 @@ -define(EMQX_RELEASE_CE, "5.0.15"). %% Enterprise edition --define(EMQX_RELEASE_EE, "5.0.0-rc.2"). +-define(EMQX_RELEASE_EE, "5.0.0-rc.3"). %% the HTTP API version -define(EMQX_API_VERSION, "5.0"). From 2d67bb3fb67cb8544dd401699d93ad01d67f59b2 Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Sun, 29 Jan 2023 10:25:28 +0800 Subject: [PATCH 0380/1802] fix: /api/nodes is timeout if emqx in high load --- apps/emqx/src/emqx_os_mon.erl | 8 +- apps/emqx/src/emqx_vm.erl | 11 ++- apps/emqx/src/emqx_vm_mon.erl | 7 +- apps/emqx_management/src/emqx_mgmt.erl | 2 +- apps/emqx_management/src/emqx_mgmt_sup.erl | 13 ++- .../src/emqx_mgmt_sys_memory.erl | 79 +++++++++++++++++++ 6 files changed, 109 insertions(+), 11 deletions(-) create mode 100644 apps/emqx_management/src/emqx_mgmt_sys_memory.erl diff --git a/apps/emqx/src/emqx_os_mon.erl b/apps/emqx/src/emqx_os_mon.erl index a06f56a4c..5c6987ea0 100644 --- a/apps/emqx/src/emqx_os_mon.erl +++ b/apps/emqx/src/emqx_os_mon.erl @@ -130,8 +130,10 @@ handle_info({timeout, _Timer, mem_check}, #{sysmem_high_watermark := HWM} = Stat handle_info({timeout, _Timer, cpu_check}, State) -> CPUHighWatermark = emqx:get_config([sysmon, os, cpu_high_watermark]) * 100, CPULowWatermark = emqx:get_config([sysmon, os, cpu_low_watermark]) * 100, - case emqx_vm:cpu_util() of - 0 -> + CPUVal = emqx_vm:cpu_util(), + case CPUVal of + %% 0 or 0.0 + Busy when Busy == 0 -> ok; Busy when Busy > CPUHighWatermark -> _ = emqx_alarm:activate( @@ -236,5 +238,5 @@ do_update_mem_alarm_status(HWM0) -> ok. usage_msg(Usage, What) -> - %% devide by 1.0 to ensure float point number + %% divide by 1.0 to ensure float point number iolist_to_binary(io_lib:format("~.2f% ~p usage", [Usage / 1.0, What])). diff --git a/apps/emqx/src/emqx_vm.erl b/apps/emqx/src/emqx_vm.erl index cf1a9dc08..fc94a461a 100644 --- a/apps/emqx/src/emqx_vm.erl +++ b/apps/emqx/src/emqx_vm.erl @@ -232,8 +232,10 @@ mem_info() -> Free = proplists:get_value(free_memory, Dataset), [{total_memory, Total}, {used_memory, Total - Free}]. -ftos(F) -> - io_lib:format("~.2f", [F / 1.0]). +ftos(F) when is_float(F) -> + float_to_binary(F, [{decimals, 2}]); +ftos(F) when is_integer(F) -> + ftos(F / 1.0). %%%% erlang vm scheduler_usage fun copied from recon scheduler_usage(Interval) when is_integer(Interval) -> @@ -391,11 +393,12 @@ cpu_util() -> compat_windows(Fun) -> case os:type() of {win32, nt} -> - 0; + 0.0; _Type -> case catch Fun() of + Val when is_float(Val) -> floor(Val * 100) / 100; Val when is_number(Val) -> Val; - _Error -> 0 + _Error -> 0.0 end end. diff --git a/apps/emqx/src/emqx_vm_mon.erl b/apps/emqx/src/emqx_vm_mon.erl index 5447e94e9..1327a1bb0 100644 --- a/apps/emqx/src/emqx_vm_mon.erl +++ b/apps/emqx/src/emqx_vm_mon.erl @@ -63,7 +63,7 @@ handle_info({timeout, _Timer, check}, State) -> ProcessCount = erlang:system_info(process_count), case ProcessCount / erlang:system_info(process_limit) of Percent when Percent > ProcHighWatermark -> - Usage = io_lib:format("~p%", [Percent * 100]), + Usage = usage(Percent), Message = [Usage, " process usage"], emqx_alarm:activate( too_many_processes, @@ -75,7 +75,7 @@ handle_info({timeout, _Timer, check}, State) -> Message ); Percent when Percent < ProcLowWatermark -> - Usage = io_lib:format("~p%", [Percent * 100]), + Usage = usage(Percent), Message = [Usage, " process usage"], emqx_alarm:ensure_deactivated( too_many_processes, @@ -108,3 +108,6 @@ code_change(_OldVsn, State, _Extra) -> start_check_timer() -> Interval = emqx:get_config([sysmon, vm, process_check_interval]), emqx_misc:start_timer(Interval, check). + +usage(Percent) -> + integer_to_list(floor(Percent * 100)) ++ "%". diff --git a/apps/emqx_management/src/emqx_mgmt.erl b/apps/emqx_management/src/emqx_mgmt.erl index 6b38e8ca0..09adde8bd 100644 --- a/apps/emqx_management/src/emqx_mgmt.erl +++ b/apps/emqx_management/src/emqx_mgmt.erl @@ -150,7 +150,7 @@ node_info() -> get_sys_memory() -> case os:type() of {unix, linux} -> - load_ctl:get_sys_memory(); + emqx_mgmt_sys_memory:get_sys_memory(); _ -> {0, 0} end. diff --git a/apps/emqx_management/src/emqx_mgmt_sup.erl b/apps/emqx_management/src/emqx_mgmt_sup.erl index 329532fa1..fa49c02a6 100644 --- a/apps/emqx_management/src/emqx_mgmt_sup.erl +++ b/apps/emqx_management/src/emqx_mgmt_sup.erl @@ -26,4 +26,15 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). init([]) -> - {ok, {{one_for_one, 1, 5}, []}}. + LC = child_spec(emqx_mgmt_sys_memory, 5000, worker), + {ok, {{one_for_one, 1, 5}, [LC]}}. + +child_spec(Mod, Shutdown, Type) -> + #{ + id => Mod, + start => {Mod, start_link, []}, + restart => permanent, + shutdown => Shutdown, + type => Type, + modules => [Mod] + }. diff --git a/apps/emqx_management/src/emqx_mgmt_sys_memory.erl b/apps/emqx_management/src/emqx_mgmt_sys_memory.erl new file mode 100644 index 000000000..d393caabe --- /dev/null +++ b/apps/emqx_management/src/emqx_mgmt_sys_memory.erl @@ -0,0 +1,79 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- +-module(emqx_mgmt_sys_memory). + +-behaviour(gen_server). +-define(SYS_MEMORY_CACHE_KEY, ?MODULE). +-define(TIMEOUT, 3000). + +-export([start_link/0, get_sys_memory/0, get_sys_memory/1]). +-export([ + init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3 +]). + +get_sys_memory() -> + get_sys_memory(?TIMEOUT). + +get_sys_memory(Timeout) -> + try + gen_server:call(?MODULE, get_sys_memory, Timeout) + catch + exit:{timeout, _} -> + get_memory_from_cache() + end. + +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +init([]) -> + {ok, #{last_time => 0}}. + +handle_call(get_sys_memory, _From, State = #{last_time := LastTime}) -> + Now = erlang:system_time(millisecond), + case Now - LastTime >= ?TIMEOUT of + true -> + Memory = load_ctl:get_sys_memory(), + persistent_term:put(?SYS_MEMORY_CACHE_KEY, Memory), + {reply, Memory, State#{last_time => Now}}; + false -> + {reply, get_memory_from_cache(), State} + end; +handle_call(_Request, _From, State = #{}) -> + {reply, ok, State}. + +handle_cast(_Request, State = #{}) -> + {noreply, State}. + +handle_info(_Info, State = #{}) -> + {noreply, State}. + +terminate(_Reason, _State = #{}) -> + ok. + +code_change(_OldVsn, State = #{}, _Extra) -> + {ok, State}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== + +get_memory_from_cache() -> + persistent_term:get(?SYS_MEMORY_CACHE_KEY, {0, 0}). From 5783127c3017da8a5855daf8ad37763aae3f52e7 Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Sun, 29 Jan 2023 11:01:43 +0800 Subject: [PATCH 0381/1802] test: cpu_sup:load mock test --- apps/emqx/src/emqx_vm.erl | 4 +--- apps/emqx/test/emqx_vm_SUITE.erl | 16 +++++++++++++++- .../test/emqx_mgmt_api_alarms_SUITE.erl | 3 +++ 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/apps/emqx/src/emqx_vm.erl b/apps/emqx/src/emqx_vm.erl index fc94a461a..c1096f611 100644 --- a/apps/emqx/src/emqx_vm.erl +++ b/apps/emqx/src/emqx_vm.erl @@ -233,9 +233,7 @@ mem_info() -> [{total_memory, Total}, {used_memory, Total - Free}]. ftos(F) when is_float(F) -> - float_to_binary(F, [{decimals, 2}]); -ftos(F) when is_integer(F) -> - ftos(F / 1.0). + float_to_binary(F, [{decimals, 2}]). %%%% erlang vm scheduler_usage fun copied from recon scheduler_usage(Interval) when is_integer(Interval) -> diff --git a/apps/emqx/test/emqx_vm_SUITE.erl b/apps/emqx/test/emqx_vm_SUITE.erl index f9809361b..9115c5ab4 100644 --- a/apps/emqx/test/emqx_vm_SUITE.erl +++ b/apps/emqx/test/emqx_vm_SUITE.erl @@ -24,7 +24,21 @@ all() -> emqx_common_test_helpers:all(?MODULE). t_load(_Config) -> - ?assertMatch([{load1, _}, {load5, _}, {load15, _}], emqx_vm:loads()). + lists:foreach( + fun(Avg, Int) -> + emqx_common_test_helpers:with_mock( + cpu_sup, + Avg, + fun() -> Int end, + fun() -> + Load = proplists:get_value(Avg, emqx_vm:loads()), + ?assertEqual(Int / 1.0, Load) + end + ), + ?assertMatch([{load1, _}, {load5, _}, {load15, _}], emqx_vm:loads()) + end, + [{load1, 1}, {load5, 5}, {load15, 15}] + ). t_systeminfo(_Config) -> ?assertEqual( diff --git a/apps/emqx_management/test/emqx_mgmt_api_alarms_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_api_alarms_SUITE.erl index adff41214..2c61651bf 100644 --- a/apps/emqx_management/test/emqx_mgmt_api_alarms_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_api_alarms_SUITE.erl @@ -40,6 +40,9 @@ t_alarms_api(_) -> get_alarms(1, true), get_alarms(1, false). +t_alarm_cpu(_) -> + ok. + t_delete_alarms_api(_) -> Path = emqx_mgmt_api_test_util:api_path(["alarms"]), {ok, _} = emqx_mgmt_api_test_util:request_api(delete, Path), From 6162f90610758c14e3d280869660e70e2857ccdd Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Sat, 28 Jan 2023 11:53:54 +0800 Subject: [PATCH 0382/1802] fix: don't crash when OTP_VERSION file is missing --- apps/emqx/src/emqx_vm.erl | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/emqx/src/emqx_vm.erl b/apps/emqx/src/emqx_vm.erl index c1096f611..50582a2cc 100644 --- a/apps/emqx/src/emqx_vm.erl +++ b/apps/emqx/src/emqx_vm.erl @@ -400,7 +400,7 @@ compat_windows(Fun) -> end end. -%% @doc Return on which Eralng/OTP the current vm is running. +%% @doc Return on which Erlang/OTP the current vm is running. %% NOTE: This API reads a file, do not use it in critical code paths. get_otp_version() -> read_otp_version(). @@ -417,6 +417,8 @@ read_otp_version() -> %% running tests etc. OtpMajor = erlang:system_info(otp_release), OtpVsnFile = filename:join([ReleasesDir, OtpMajor, "OTP_VERSION"]), - {ok, Vsn} = file:read_file(OtpVsnFile), - Vsn + case file:read_file(OtpVsnFile) of + {ok, Vsn} -> Vsn; + {error, enoent} -> list_to_binary(OtpMajor) + end end. From 0b19be074c7856c9f52a53364f05145b65eda5ed Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Sat, 28 Jan 2023 12:07:50 +0800 Subject: [PATCH 0383/1802] feat: cache OTP_VERSION in persistent_term --- apps/emqx/src/emqx_vm.erl | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/apps/emqx/src/emqx_vm.erl b/apps/emqx/src/emqx_vm.erl index 50582a2cc..7da49016d 100644 --- a/apps/emqx/src/emqx_vm.erl +++ b/apps/emqx/src/emqx_vm.erl @@ -401,9 +401,19 @@ compat_windows(Fun) -> end. %% @doc Return on which Erlang/OTP the current vm is running. -%% NOTE: This API reads a file, do not use it in critical code paths. +%% The dashboard's /api/nodes endpoint will call this function frequently. +%% we should avoid reading file every time. +%% The OTP version never changes at runtime expect upgrade erts, +%% so we cache it in a persistent term for performance. get_otp_version() -> - read_otp_version(). + case persistent_term:get(emqx_otp_version, undefined) of + undefined -> + OtpVsn = read_otp_version(), + persistent_term:put(emqx_otp_version, OtpVsn), + OtpVsn; + OtpVsn when is_binary(OtpVsn) -> + OtpVsn + end. read_otp_version() -> ReleasesDir = filename:join([code:root_dir(), "releases"]), From b6e6315b5076c39a2c394623651fdfc44dc3b1a9 Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Mon, 30 Jan 2023 11:53:49 +0800 Subject: [PATCH 0384/1802] feat: change loads from string to float --- apps/emqx/src/emqx_alarm.erl | 5 +- apps/emqx/src/emqx_os_mon.erl | 34 +++-- apps/emqx/src/emqx_vm.erl | 12 +- apps/emqx/test/emqx_os_mon_SUITE.erl | 142 ++++++++++++++++-- apps/emqx/test/emqx_vm_SUITE.erl | 15 +- apps/emqx/test/emqx_vm_mon_SUITE.erl | 26 +++- apps/emqx_management/src/emqx_mgmt.erl | 2 +- .../src/emqx_mgmt_api_nodes.erl | 12 +- .../src/emqx_mgmt_sys_memory.erl | 2 +- .../test/emqx_mgmt_api_nodes_SUITE.erl | 4 +- 10 files changed, 199 insertions(+), 55 deletions(-) diff --git a/apps/emqx/src/emqx_alarm.erl b/apps/emqx/src/emqx_alarm.erl index 209715a85..84c40ef2a 100644 --- a/apps/emqx/src/emqx_alarm.erl +++ b/apps/emqx/src/emqx_alarm.erl @@ -325,19 +325,20 @@ deactivate_alarm( false -> ok end, + Now = erlang:system_time(microsecond), HistoryAlarm = make_deactivated_alarm( ActivateAt, Name, Details0, Msg0, - erlang:system_time(microsecond) + Now ), DeActAlarm = make_deactivated_alarm( ActivateAt, Name, Details, normalize_message(Name, iolist_to_binary(Message)), - erlang:system_time(microsecond) + Now ), mria:dirty_write(?DEACTIVATED_ALARM, HistoryAlarm), mria:dirty_delete(?ACTIVATED_ALARM, Name), diff --git a/apps/emqx/src/emqx_os_mon.erl b/apps/emqx/src/emqx_os_mon.erl index 5c6987ea0..c5ce35bf9 100644 --- a/apps/emqx/src/emqx_os_mon.erl +++ b/apps/emqx/src/emqx_os_mon.erl @@ -93,9 +93,9 @@ init([]) -> %% memsup is not reliable, ignore memsup:set_sysmem_high_watermark(1.0), SysHW = init_os_monitor(), - _ = start_mem_check_timer(), - _ = start_cpu_check_timer(), - {ok, #{sysmem_high_watermark => SysHW}}. + MemRef = start_mem_check_timer(), + CpuRef = start_cpu_check_timer(), + {ok, #{sysmem_high_watermark => SysHW, mem_time_ref => MemRef, cpu_time_ref => CpuRef}}. init_os_monitor() -> init_os_monitor(emqx:get_config([sysmon, os])). @@ -125,8 +125,8 @@ handle_cast(Msg, State) -> handle_info({timeout, _Timer, mem_check}, #{sysmem_high_watermark := HWM} = State) -> ok = update_mem_alarm_status(HWM), - ok = start_mem_check_timer(), - {noreply, State}; + Ref = start_mem_check_timer(), + {noreply, State#{mem_time_ref => Ref}}; handle_info({timeout, _Timer, cpu_check}, State) -> CPUHighWatermark = emqx:get_config([sysmon, os, cpu_high_watermark]) * 100, CPULowWatermark = emqx:get_config([sysmon, os, cpu_low_watermark]) * 100, @@ -158,11 +158,14 @@ handle_info({timeout, _Timer, cpu_check}, State) -> _Busy -> ok end, - ok = start_cpu_check_timer(), - {noreply, State}; -handle_info({monitor_conf_update, OS}, _State) -> + Ref = start_cpu_check_timer(), + {noreply, State#{cpu_time_ref => Ref}}; +handle_info({monitor_conf_update, OS}, State) -> + cancel_outdated_timer(State), SysHW = init_os_monitor(OS), - {noreply, #{sysmem_high_watermark => SysHW}}; + MemRef = start_mem_check_timer(), + CpuRef = start_cpu_check_timer(), + {noreply, #{sysmem_high_watermark => SysHW, mem_time_ref => MemRef, cpu_time_ref => CpuRef}}; handle_info(Info, State) -> ?SLOG(error, #{msg => "unexpected_info", info => Info}), {noreply, State}. @@ -176,11 +179,15 @@ code_change(_OldVsn, State, _Extra) -> %%-------------------------------------------------------------------- %% Internal functions %%-------------------------------------------------------------------- +cancel_outdated_timer(#{mem_time_ref := MemRef, cpu_time_ref := CpuRef}) -> + emqx_misc:cancel_timer(MemRef), + emqx_misc:cancel_timer(CpuRef), + ok. start_cpu_check_timer() -> Interval = emqx:get_config([sysmon, os, cpu_check_interval]), case erlang:system_info(system_architecture) of - "x86_64-pc-linux-musl" -> ok; + "x86_64-pc-linux-musl" -> undefined; _ -> start_timer(Interval, cpu_check) end. @@ -193,12 +200,11 @@ start_mem_check_timer() -> true -> start_timer(Interval, mem_check); false -> - ok + undefined end. start_timer(Interval, Msg) -> - _ = emqx_misc:start_timer(Interval, Msg), - ok. + emqx_misc:start_timer(Interval, Msg). update_mem_alarm_status(HWM) when HWM > 1.0 orelse HWM < 0.0 -> ?SLOG(warning, #{msg => "discarded_out_of_range_mem_alarm_threshold", value => HWM}), @@ -225,7 +231,7 @@ do_update_mem_alarm_status(HWM0) -> }, usage_msg(Usage, mem) ); - _ -> + false -> ok = emqx_alarm:ensure_deactivated( high_system_memory_usage, #{ diff --git a/apps/emqx/src/emqx_vm.erl b/apps/emqx/src/emqx_vm.erl index 7da49016d..f80d18a3a 100644 --- a/apps/emqx/src/emqx_vm.erl +++ b/apps/emqx/src/emqx_vm.erl @@ -175,9 +175,9 @@ schedulers() -> loads() -> [ - {load1, ftos(avg1() / 256)}, - {load5, ftos(avg5() / 256)}, - {load15, ftos(avg15() / 256)} + {load1, load(avg1())}, + {load5, load(avg5())}, + {load15, load(avg15())} ]. system_info_keys() -> ?SYSTEM_INFO_KEYS. @@ -232,9 +232,6 @@ mem_info() -> Free = proplists:get_value(free_memory, Dataset), [{total_memory, Total}, {used_memory, Total - Free}]. -ftos(F) when is_float(F) -> - float_to_binary(F, [{decimals, 2}]). - %%%% erlang vm scheduler_usage fun copied from recon scheduler_usage(Interval) when is_integer(Interval) -> %% We start and stop the scheduler_wall_time system flag @@ -400,6 +397,9 @@ compat_windows(Fun) -> end end. +load(Avg) -> + floor((Avg / 256) * 100) / 100. + %% @doc Return on which Erlang/OTP the current vm is running. %% The dashboard's /api/nodes endpoint will call this function frequently. %% we should avoid reading file every time. diff --git a/apps/emqx/test/emqx_os_mon_SUITE.erl b/apps/emqx/test/emqx_os_mon_SUITE.erl index 8729bbdb6..0c5a1f261 100644 --- a/apps/emqx/test/emqx_os_mon_SUITE.erl +++ b/apps/emqx/test/emqx_os_mon_SUITE.erl @@ -25,25 +25,44 @@ all() -> emqx_common_test_helpers:all(?MODULE). init_per_suite(Config) -> emqx_common_test_helpers:boot_modules(all), - emqx_common_test_helpers:start_apps( - [], - fun - (emqx) -> - application:set_env(emqx, os_mon, [ - {cpu_check_interval, 1}, - {cpu_high_watermark, 5}, - {cpu_low_watermark, 80}, - {procmem_high_watermark, 5} - ]); - (_) -> - ok - end - ), + emqx_common_test_helpers:start_apps([]), Config. end_per_suite(_Config) -> emqx_common_test_helpers:stop_apps([]). +init_per_testcase(t_cpu_check_alarm, Config) -> + emqx_common_test_helpers:boot_modules(all), + emqx_common_test_helpers:start_apps([]), + SysMon = emqx_config:get([sysmon, os], #{}), + emqx_config:put([sysmon, os], SysMon#{ + cpu_high_watermark => 0.9, + cpu_low_watermark => 0, + %% 200ms + cpu_check_interval => 200 + }), + ok = supervisor:terminate_child(emqx_sys_sup, emqx_os_mon), + {ok, _} = supervisor:restart_child(emqx_sys_sup, emqx_os_mon), + Config; +init_per_testcase(t_sys_mem_check_alarm, Config) -> + emqx_common_test_helpers:boot_modules(all), + emqx_common_test_helpers:start_apps([]), + SysMon = emqx_config:get([sysmon, os], #{}), + emqx_config:put([sysmon, os], SysMon#{ + sysmem_high_watermark => 0.51, + %% 200ms + mem_check_interval => 200 + }), + ok = meck:new(os, [non_strict, no_link, no_history, passthrough, unstick]), + ok = meck:expect(os, type, fun() -> {unix, linux} end), + ok = supervisor:terminate_child(emqx_sys_sup, emqx_os_mon), + {ok, _} = supervisor:restart_child(emqx_sys_sup, emqx_os_mon), + Config; +init_per_testcase(_, Config) -> + emqx_common_test_helpers:boot_modules(all), + emqx_common_test_helpers:start_apps([]), + Config. + t_api(_) -> ?assertEqual(60000, emqx_os_mon:get_mem_check_interval()), ?assertEqual(ok, emqx_os_mon:set_mem_check_interval(30000)), @@ -67,3 +86,98 @@ t_api(_) -> emqx_os_mon ! ignored, gen_server:stop(emqx_os_mon), ok. + +t_sys_mem_check_alarm(_) -> + emqx_config:put([sysmon, os, mem_check_interval], 200), + emqx_os_mon:update(emqx_config:get([sysmon, os])), + Mem = 0.52345, + Usage = floor(Mem * 10000) / 100, + emqx_common_test_helpers:with_mock( + load_ctl, + get_memory_usage, + fun() -> Mem end, + fun() -> + timer:sleep(500), + Alarms = emqx_alarm:get_alarms(activated), + ?assert( + emqx_vm_mon_SUITE:is_existing( + high_system_memory_usage, emqx_alarm:get_alarms(activated) + ), + #{ + load_ctl_memory => load_ctl:get_memory_usage(), + config => emqx_config:get([sysmon, os]), + process => sys:get_state(emqx_os_mon), + alarms => Alarms + } + ), + [ + #{ + activate_at := _, + activated := true, + deactivate_at := infinity, + details := #{high_watermark := 51.0, usage := RealUsage}, + message := Msg, + name := high_system_memory_usage + } + ] = + lists:filter( + fun + (#{name := high_system_memory_usage}) -> true; + (_) -> false + end, + Alarms + ), + ?assert(RealUsage >= Usage, {RealUsage, Usage}), + ?assert(is_binary(Msg)), + emqx_config:put([sysmon, os, sysmem_high_watermark], 0.99999), + ok = supervisor:terminate_child(emqx_sys_sup, emqx_os_mon), + {ok, _} = supervisor:restart_child(emqx_sys_sup, emqx_os_mon), + timer:sleep(600), + Activated = emqx_alarm:get_alarms(activated), + ?assertNot( + emqx_vm_mon_SUITE:is_existing(high_system_memory_usage, Activated), + #{activated => Activated, process_state => sys:get_state(emqx_os_mon)} + ) + end + ). + +t_cpu_check_alarm(_) -> + CpuUtil = 90.12345, + Usage = floor(CpuUtil * 100) / 100, + emqx_common_test_helpers:with_mock( + cpu_sup, + util, + fun() -> CpuUtil end, + fun() -> + timer:sleep(500), + Alarms = emqx_alarm:get_alarms(activated), + ?assert( + emqx_vm_mon_SUITE:is_existing(high_cpu_usage, emqx_alarm:get_alarms(activated)) + ), + [ + #{ + activate_at := _, + activated := true, + deactivate_at := infinity, + details := #{high_watermark := 90.0, low_watermark := 0, usage := RealUsage}, + message := Msg, + name := high_cpu_usage + } + ] = + lists:filter( + fun + (#{name := high_cpu_usage}) -> true; + (_) -> false + end, + Alarms + ), + ?assert(RealUsage >= Usage, {RealUsage, Usage}), + ?assert(is_binary(Msg)), + emqx_config:put([sysmon, os, cpu_high_watermark], 1), + emqx_config:put([sysmon, os, cpu_low_watermark], 0.96), + timer:sleep(500), + ?assertNot( + emqx_vm_mon_SUITE:is_existing(high_cpu_usage, emqx_alarm:get_alarms(activated)) + ) + end + ). diff --git a/apps/emqx/test/emqx_vm_SUITE.erl b/apps/emqx/test/emqx_vm_SUITE.erl index 9115c5ab4..35f37a41e 100644 --- a/apps/emqx/test/emqx_vm_SUITE.erl +++ b/apps/emqx/test/emqx_vm_SUITE.erl @@ -25,19 +25,22 @@ all() -> emqx_common_test_helpers:all(?MODULE). t_load(_Config) -> lists:foreach( - fun(Avg, Int) -> + fun({Avg, LoadKey, Int}) -> emqx_common_test_helpers:with_mock( cpu_sup, Avg, fun() -> Int end, fun() -> - Load = proplists:get_value(Avg, emqx_vm:loads()), - ?assertEqual(Int / 1.0, Load) + Load = proplists:get_value(LoadKey, emqx_vm:loads()), + ?assertEqual(Int / 256, Load) end - ), - ?assertMatch([{load1, _}, {load5, _}, {load15, _}], emqx_vm:loads()) + ) end, - [{load1, 1}, {load5, 5}, {load15, 15}] + [{avg1, load1, 0}, {avg5, load5, 128}, {avg15, load15, 256}] + ), + ?assertMatch( + [{load1, _}, {load5, _}, {load15, _}], + emqx_vm:loads() ). t_systeminfo(_Config) -> diff --git a/apps/emqx/test/emqx_vm_mon_SUITE.erl b/apps/emqx/test/emqx_vm_mon_SUITE.erl index 140a00010..ceeffafb5 100644 --- a/apps/emqx/test/emqx_vm_mon_SUITE.erl +++ b/apps/emqx/test/emqx_vm_mon_SUITE.erl @@ -23,13 +23,13 @@ all() -> emqx_common_test_helpers:all(?MODULE). -init_per_testcase(t_alarms, Config) -> +init_per_testcase(t_too_many_processes_alarm, Config) -> emqx_common_test_helpers:boot_modules(all), emqx_common_test_helpers:start_apps([]), emqx_config:put([sysmon, vm], #{ process_high_watermark => 0, process_low_watermark => 0, - %% 1s + %% 100ms process_check_interval => 100 }), ok = supervisor:terminate_child(emqx_sys_sup, emqx_vm_mon), @@ -43,9 +43,29 @@ init_per_testcase(_, Config) -> end_per_testcase(_, _Config) -> emqx_common_test_helpers:stop_apps([]). -t_alarms(_) -> +t_too_many_processes_alarm(_) -> timer:sleep(500), + Alarms = emqx_alarm:get_alarms(activated), ?assert(is_existing(too_many_processes, emqx_alarm:get_alarms(activated))), + ?assertMatch( + [ + #{ + activate_at := _, + activated := true, + deactivate_at := infinity, + details := #{high_watermark := 0, low_watermark := 0, usage := "0%"}, + message := <<"0% process usage">>, + name := too_many_processes + } + ], + lists:filter( + fun + (#{name := too_many_processes}) -> true; + (_) -> false + end, + Alarms + ) + ), emqx_config:put([sysmon, vm, process_high_watermark], 70), emqx_config:put([sysmon, vm, process_low_watermark], 60), timer:sleep(500), diff --git a/apps/emqx_management/src/emqx_mgmt.erl b/apps/emqx_management/src/emqx_mgmt.erl index 09adde8bd..f794ef01d 100644 --- a/apps/emqx_management/src/emqx_mgmt.erl +++ b/apps/emqx_management/src/emqx_mgmt.erl @@ -126,7 +126,7 @@ lookup_node(Node) -> node_info() -> {UsedRatio, Total} = get_sys_memory(), - Info = maps:from_list([{K, list_to_binary(V)} || {K, V} <- emqx_vm:loads()]), + Info = maps:from_list(emqx_vm:loads()), BrokerInfo = emqx_sys:info(), Info#{ node => node(), diff --git a/apps/emqx_management/src/emqx_mgmt_api_nodes.erl b/apps/emqx_management/src/emqx_mgmt_api_nodes.erl index 64ef3c1ef..cb8d37609 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_nodes.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_nodes.erl @@ -159,18 +159,18 @@ fields(node_info) -> )}, {load1, mk( - string(), - #{desc => <<"CPU average load in 1 minute">>, example => "2.66"} + float(), + #{desc => <<"CPU average load in 1 minute">>, example => 2.66} )}, {load5, mk( - string(), - #{desc => <<"CPU average load in 5 minute">>, example => "2.66"} + float(), + #{desc => <<"CPU average load in 5 minute">>, example => 2.66} )}, {load15, mk( - string(), - #{desc => <<"CPU average load in 15 minute">>, example => "2.66"} + float(), + #{desc => <<"CPU average load in 15 minute">>, example => 2.66} )}, {max_fds, mk( diff --git a/apps/emqx_management/src/emqx_mgmt_sys_memory.erl b/apps/emqx_management/src/emqx_mgmt_sys_memory.erl index d393caabe..cc4f987b5 100644 --- a/apps/emqx_management/src/emqx_mgmt_sys_memory.erl +++ b/apps/emqx_management/src/emqx_mgmt_sys_memory.erl @@ -17,7 +17,7 @@ -behaviour(gen_server). -define(SYS_MEMORY_CACHE_KEY, ?MODULE). --define(TIMEOUT, 3000). +-define(TIMEOUT, 2200). -export([start_link/0, get_sys_memory/0, get_sys_memory/1]). -export([ diff --git a/apps/emqx_management/test/emqx_mgmt_api_nodes_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_api_nodes_SUITE.erl index 2bbdf938d..a0dbb9314 100644 --- a/apps/emqx_management/test/emqx_mgmt_api_nodes_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_api_nodes_SUITE.erl @@ -24,11 +24,11 @@ all() -> emqx_common_test_helpers:all(?MODULE). init_per_suite(Config) -> - emqx_mgmt_api_test_util:init_suite([emqx_conf]), + emqx_mgmt_api_test_util:init_suite([emqx_conf, emqx_management]), Config. end_per_suite(_) -> - emqx_mgmt_api_test_util:end_suite([emqx_conf]). + emqx_mgmt_api_test_util:end_suite([emqx_management, emqx_conf]). init_per_testcase(t_log_path, Config) -> emqx_config_logger:add_handler(), From c2bdb9faa7d5b10e0d00de48685ecf0e2b57920b Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Mon, 30 Jan 2023 12:48:28 +0800 Subject: [PATCH 0385/1802] test: multiple_nodes case failed --- apps/emqx_management/test/emqx_mgmt_api_nodes_SUITE.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx_management/test/emqx_mgmt_api_nodes_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_api_nodes_SUITE.erl index a0dbb9314..03b0ea2d9 100644 --- a/apps/emqx_management/test/emqx_mgmt_api_nodes_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_api_nodes_SUITE.erl @@ -152,7 +152,7 @@ cluster(Specs) -> Env = [{emqx, boot_modules, []}], emqx_common_test_helpers:emqx_cluster(Specs, [ {env, Env}, - {apps, [emqx_conf]}, + {apps, [emqx_conf, emqx_management]}, {load_schema, false}, {join_to, true}, {env_handler, fun From 7a9f47726718175249bc86f77a140e96b95a9075 Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Wed, 1 Feb 2023 00:51:34 +0800 Subject: [PATCH 0386/1802] feat: use emqx_mgmt_cache to cache sys_memory --- apps/emqx_management/src/emqx_mgmt_cache.erl | 104 ++++++++++++++++++ apps/emqx_management/src/emqx_mgmt_sup.erl | 2 +- .../src/emqx_mgmt_sys_memory.erl | 79 ------------- 3 files changed, 105 insertions(+), 80 deletions(-) create mode 100644 apps/emqx_management/src/emqx_mgmt_cache.erl delete mode 100644 apps/emqx_management/src/emqx_mgmt_sys_memory.erl diff --git a/apps/emqx_management/src/emqx_mgmt_cache.erl b/apps/emqx_management/src/emqx_mgmt_cache.erl new file mode 100644 index 000000000..37f8e1367 --- /dev/null +++ b/apps/emqx_management/src/emqx_mgmt_cache.erl @@ -0,0 +1,104 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- +-module(emqx_mgmt_cache). + +-behaviour(gen_server). + +-define(SYS_MEMORY_KEY, sys_memory). +-define(EXPIRED_MS, 3000). +%% -100ms to early update cache +-define(REFRESH_MS, ?EXPIRED_MS - 100). +-define(DEFAULT_BAD_MEMORY, {0, 0}). + +-export([start_link/0, get_sys_memory/0]). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). + +get_sys_memory() -> + Now = now_millisecond(), + {CacheMem, ExpiredAt} = get_memory_from_cache(), + case Now > ExpiredAt of + true -> + erlang:send(?MODULE, fresh_sys_memory), + CacheMem; + %% stale cache value, try to recalculate + false -> + get_sys_memory_sync() + end. + +get_sys_memory_sync() -> + try + gen_server:call(?MODULE, get_sys_memory, ?EXPIRED_MS) + catch + exit:{timeout, _} -> + ?DEFAULT_BAD_MEMORY + end. + +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +init([]) -> + ets:new(?MODULE, [set, named_table, public, {keypos, 1}]), + {ok, #{fresh_at => 0}}. + +handle_call(get_sys_memory, _From, State) -> + {Mem, NewState} = fresh_sys_memory(State), + {reply, Mem, NewState}; +handle_call(_Request, _From, State) -> + {reply, ok, State}. + +handle_cast(_Request, State) -> + {noreply, State}. + +handle_info(fresh_sys_memory, State) -> + {_, NewState} = fresh_sys_memory(State), + {noreply, NewState}; +handle_info(_Info, State) -> + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== + +fresh_sys_memory(State = #{fresh_at := LastFreshAt}) -> + Now = now_millisecond(), + {Mem, ExpiredAt} = get_memory_from_cache(), + case Now >= ExpiredAt orelse Now - LastFreshAt >= ?REFRESH_MS of + true -> + %% NOTE: Now /= UpdateAt, because + %% load_ctl:get_sys_memory/0 maybe a heavy operation, + %% so record update_at timestamp after get_sys_memory/0. + NewMem = load_ctl:get_sys_memory(), + NewExpiredAt = now_millisecond() + ?EXPIRED_MS, + ets:insert(?MODULE, {?SYS_MEMORY_KEY, {NewMem, NewExpiredAt}}), + {NewMem, State#{fresh_at => Now}}; + false -> + {Mem, State} + end. + +get_memory_from_cache() -> + case ets:lookup(?MODULE, ?SYS_MEMORY_KEY) of + [] -> {?DEFAULT_BAD_MEMORY, 0}; + [{_, CacheVal}] -> CacheVal + end. + +now_millisecond() -> + erlang:system_time(millisecond). diff --git a/apps/emqx_management/src/emqx_mgmt_sup.erl b/apps/emqx_management/src/emqx_mgmt_sup.erl index fa49c02a6..2d9a9ba8a 100644 --- a/apps/emqx_management/src/emqx_mgmt_sup.erl +++ b/apps/emqx_management/src/emqx_mgmt_sup.erl @@ -26,7 +26,7 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). init([]) -> - LC = child_spec(emqx_mgmt_sys_memory, 5000, worker), + LC = child_spec(emqx_mgmt_cache, 5000, worker), {ok, {{one_for_one, 1, 5}, [LC]}}. child_spec(Mod, Shutdown, Type) -> diff --git a/apps/emqx_management/src/emqx_mgmt_sys_memory.erl b/apps/emqx_management/src/emqx_mgmt_sys_memory.erl deleted file mode 100644 index cc4f987b5..000000000 --- a/apps/emqx_management/src/emqx_mgmt_sys_memory.erl +++ /dev/null @@ -1,79 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2023 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- --module(emqx_mgmt_sys_memory). - --behaviour(gen_server). --define(SYS_MEMORY_CACHE_KEY, ?MODULE). --define(TIMEOUT, 2200). - --export([start_link/0, get_sys_memory/0, get_sys_memory/1]). --export([ - init/1, - handle_call/3, - handle_cast/2, - handle_info/2, - terminate/2, - code_change/3 -]). - -get_sys_memory() -> - get_sys_memory(?TIMEOUT). - -get_sys_memory(Timeout) -> - try - gen_server:call(?MODULE, get_sys_memory, Timeout) - catch - exit:{timeout, _} -> - get_memory_from_cache() - end. - -start_link() -> - gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). - -init([]) -> - {ok, #{last_time => 0}}. - -handle_call(get_sys_memory, _From, State = #{last_time := LastTime}) -> - Now = erlang:system_time(millisecond), - case Now - LastTime >= ?TIMEOUT of - true -> - Memory = load_ctl:get_sys_memory(), - persistent_term:put(?SYS_MEMORY_CACHE_KEY, Memory), - {reply, Memory, State#{last_time => Now}}; - false -> - {reply, get_memory_from_cache(), State} - end; -handle_call(_Request, _From, State = #{}) -> - {reply, ok, State}. - -handle_cast(_Request, State = #{}) -> - {noreply, State}. - -handle_info(_Info, State = #{}) -> - {noreply, State}. - -terminate(_Reason, _State = #{}) -> - ok. - -code_change(_OldVsn, State = #{}, _Extra) -> - {ok, State}. - -%%%=================================================================== -%%% Internal functions -%%%=================================================================== - -get_memory_from_cache() -> - persistent_term:get(?SYS_MEMORY_CACHE_KEY, {0, 0}). From 71f00f2962a3cbd6c7bd50f2503b3c8cbf41932c Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Wed, 1 Feb 2023 10:07:09 +0800 Subject: [PATCH 0387/1802] test: ctl suite failed --- apps/emqx_management/src/emqx_mgmt_cli.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx_management/src/emqx_mgmt_cli.erl b/apps/emqx_management/src/emqx_mgmt_cli.erl index 0e7506a0b..442d5c7de 100644 --- a/apps/emqx_management/src/emqx_mgmt_cli.erl +++ b/apps/emqx_management/src/emqx_mgmt_cli.erl @@ -315,7 +315,7 @@ vm([]) -> vm(["all"]) -> [vm([Name]) || Name <- ["load", "memory", "process", "io", "ports"]]; vm(["load"]) -> - [emqx_ctl:print("cpu/~-20s: ~ts~n", [L, V]) || {L, V} <- emqx_vm:loads()]; + [emqx_ctl:print("cpu/~-20s: ~w~n", [L, V]) || {L, V} <- emqx_vm:loads()]; vm(["memory"]) -> [emqx_ctl:print("memory/~-17s: ~w~n", [Cat, Val]) || {Cat, Val} <- erlang:memory()]; vm(["process"]) -> From 9cbad5297c8ed03fb3eb4ec306d1718788d94095 Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Wed, 1 Feb 2023 11:16:30 +0800 Subject: [PATCH 0388/1802] fix: primary logger level not update --- apps/emqx/src/config/emqx_config_logger.erl | 10 +++++++--- apps/emqx/src/emqx_app.erl | 1 - apps/emqx_conf/src/emqx_conf_app.erl | 1 + deploy/packages/emqx.service | 2 +- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/apps/emqx/src/config/emqx_config_logger.erl b/apps/emqx/src/config/emqx_config_logger.erl index 4d787ea0c..625a498e2 100644 --- a/apps/emqx/src/config/emqx_config_logger.erl +++ b/apps/emqx/src/config/emqx_config_logger.erl @@ -67,12 +67,16 @@ post_config_update(_ConfPath, _Req, _NewConf, _OldConf, _AppEnvs) -> ok. maybe_update_log_level(NewLevel) -> - OldLevel = application:get_env(kernel, logger_level, warning), + OldLevel = emqx_logger:get_primary_log_level(), case OldLevel =:= NewLevel of true -> %% no change ok; false -> + ok = emqx_logger:set_primary_log_level(NewLevel), + %% also update kernel's logger_level for troubleshooting + %% what is actually in effect is the logger's primary log level + ok = application:set_env(kernel, logger_level, NewLevel), log_to_console("Config override: log level is set to '~p'~n", [NewLevel]) end. @@ -97,7 +101,7 @@ update_log_handlers(NewHandlers) -> end, AddsAndUpdates = lists:filtermap(MapFn, NewHandlers), lists:foreach(fun update_log_handler/1, Removes ++ AddsAndUpdates), - _ = application:set_env(kernel, logger, NewHandlers), + ok = application:set_env(kernel, logger, NewHandlers), ok. update_log_handler({removed, Id}) -> @@ -115,7 +119,7 @@ id_for_log(Other) -> "log.file_handlers." ++ atom_to_list(Other). atom(Id) when is_binary(Id) -> binary_to_atom(Id, utf8); atom(Id) when is_atom(Id) -> Id. -%% @doc Translate raw config to app-env conpatible log handler configs list. +%% @doc Translate raw config to app-env compatible log handler configs list. tr_handlers(Conf) -> %% mute the default handler tr_console_handler(Conf) ++ diff --git a/apps/emqx/src/emqx_app.erl b/apps/emqx/src/emqx_app.erl index c56cdb520..6188d8030 100644 --- a/apps/emqx/src/emqx_app.erl +++ b/apps/emqx/src/emqx_app.erl @@ -42,7 +42,6 @@ start(_Type, _Args) -> ok = maybe_load_config(), ok = emqx_persistent_session:init_db_backend(), - ok = emqx_config_logger:refresh_config(), ok = maybe_start_quicer(), ok = emqx_bpapi:start(), wait_boot_shards(), diff --git a/apps/emqx_conf/src/emqx_conf_app.erl b/apps/emqx_conf/src/emqx_conf_app.erl index f2e4f6f56..34224c3f2 100644 --- a/apps/emqx_conf/src/emqx_conf_app.erl +++ b/apps/emqx_conf/src/emqx_conf_app.erl @@ -29,6 +29,7 @@ start(_StartType, _StartArgs) -> init_conf(), + ok = emqx_config_logger:refresh_config(), emqx_conf_sup:start_link(). stop(_State) -> diff --git a/deploy/packages/emqx.service b/deploy/packages/emqx.service index d505cc519..d826e358b 100644 --- a/deploy/packages/emqx.service +++ b/deploy/packages/emqx.service @@ -13,7 +13,7 @@ Environment=HOME=/var/lib/emqx # Enable logging to file Environment=EMQX_LOG__TO=default -# Start 'foregroun' but not 'start' (daemon) mode. +# Start 'foreground' but not 'start' (daemon) mode. # Because systemd monitor/restarts 'simple' services ExecStart=/bin/bash /usr/bin/emqx foreground From 95ef1e806c293d2b376dbf69e709e9a64f48c9f9 Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Wed, 1 Feb 2023 11:24:27 +0800 Subject: [PATCH 0389/1802] chore: don't crash when add_handler failed --- apps/emqx/src/config/emqx_config_logger.erl | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/apps/emqx/src/config/emqx_config_logger.erl b/apps/emqx/src/config/emqx_config_logger.erl index 625a498e2..15e4d3959 100644 --- a/apps/emqx/src/config/emqx_config_logger.erl +++ b/apps/emqx/src/config/emqx_config_logger.erl @@ -111,7 +111,17 @@ update_log_handler({Action, {handler, Id, Mod, Conf}}) -> log_to_console("Config override: ~s is ~p~n", [id_for_log(Id), Action]), % may return {error, {not_found, Id}} _ = logger:remove_handler(Id), - ok = logger:add_handler(Id, Mod, Conf). + case logger:add_handler(Id, Mod, Conf) of + ok -> + ok; + %% Don't crash here, otherwise the cluster rpc will retry the wrong handler forever. + {error, Reason} -> + log_to_console( + "Config override: ~s is ~p, but failed to add handler: ~p~n", + [id_for_log(Id), Action, Reason] + ) + end, + ok. id_for_log(console) -> "log.console_handler"; id_for_log(Other) -> "log.file_handlers." ++ atom_to_list(Other). From 56b9238645bf39072fec1f36e5cf69d4799b3538 Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Wed, 1 Feb 2023 12:21:34 +0800 Subject: [PATCH 0390/1802] fix: only cache sys_memory in linux --- apps/emqx_management/src/emqx_mgmt.erl | 2 +- apps/emqx_management/src/emqx_mgmt_sup.erl | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/apps/emqx_management/src/emqx_mgmt.erl b/apps/emqx_management/src/emqx_mgmt.erl index f794ef01d..814b39cdc 100644 --- a/apps/emqx_management/src/emqx_mgmt.erl +++ b/apps/emqx_management/src/emqx_mgmt.erl @@ -150,7 +150,7 @@ node_info() -> get_sys_memory() -> case os:type() of {unix, linux} -> - emqx_mgmt_sys_memory:get_sys_memory(); + emqx_mgmt_cache:get_sys_memory(); _ -> {0, 0} end. diff --git a/apps/emqx_management/src/emqx_mgmt_sup.erl b/apps/emqx_management/src/emqx_mgmt_sup.erl index 2d9a9ba8a..713ff87dc 100644 --- a/apps/emqx_management/src/emqx_mgmt_sup.erl +++ b/apps/emqx_management/src/emqx_mgmt_sup.erl @@ -26,8 +26,14 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). init([]) -> - LC = child_spec(emqx_mgmt_cache, 5000, worker), - {ok, {{one_for_one, 1, 5}, [LC]}}. + Workers = + case os:type() of + {unix, linux} -> + [child_spec(emqx_mgmt_cache, 5000, worker)]; + _ -> + [] + end, + {ok, {{one_for_one, 1, 5}, Workers}}. child_spec(Mod, Shutdown, Type) -> #{ From ced55719ef2fd8b93fe021c6434fcfe219a0e58d Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Wed, 1 Feb 2023 14:47:41 +0800 Subject: [PATCH 0391/1802] chore: only run t_sys_mem_check ct in linux --- apps/emqx/test/emqx_os_mon_SUITE.erl | 39 ++++++++++++++++------------ 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/apps/emqx/test/emqx_os_mon_SUITE.erl b/apps/emqx/test/emqx_os_mon_SUITE.erl index 0c5a1f261..0538d949a 100644 --- a/apps/emqx/test/emqx_os_mon_SUITE.erl +++ b/apps/emqx/test/emqx_os_mon_SUITE.erl @@ -32,8 +32,6 @@ end_per_suite(_Config) -> emqx_common_test_helpers:stop_apps([]). init_per_testcase(t_cpu_check_alarm, Config) -> - emqx_common_test_helpers:boot_modules(all), - emqx_common_test_helpers:start_apps([]), SysMon = emqx_config:get([sysmon, os], #{}), emqx_config:put([sysmon, os], SysMon#{ cpu_high_watermark => 0.9, @@ -45,19 +43,20 @@ init_per_testcase(t_cpu_check_alarm, Config) -> {ok, _} = supervisor:restart_child(emqx_sys_sup, emqx_os_mon), Config; init_per_testcase(t_sys_mem_check_alarm, Config) -> - emqx_common_test_helpers:boot_modules(all), - emqx_common_test_helpers:start_apps([]), - SysMon = emqx_config:get([sysmon, os], #{}), - emqx_config:put([sysmon, os], SysMon#{ - sysmem_high_watermark => 0.51, - %% 200ms - mem_check_interval => 200 - }), - ok = meck:new(os, [non_strict, no_link, no_history, passthrough, unstick]), - ok = meck:expect(os, type, fun() -> {unix, linux} end), - ok = supervisor:terminate_child(emqx_sys_sup, emqx_os_mon), - {ok, _} = supervisor:restart_child(emqx_sys_sup, emqx_os_mon), - Config; + case os:type() of + {unix, linux} -> + SysMon = emqx_config:get([sysmon, os], #{}), + emqx_config:put([sysmon, os], SysMon#{ + sysmem_high_watermark => 0.51, + %% 200ms + mem_check_interval => 200 + }), + ok = supervisor:terminate_child(emqx_sys_sup, emqx_os_mon), + {ok, _} = supervisor:restart_child(emqx_sys_sup, emqx_os_mon), + Config; + _ -> + Config + end; init_per_testcase(_, Config) -> emqx_common_test_helpers:boot_modules(all), emqx_common_test_helpers:start_apps([]), @@ -87,7 +86,15 @@ t_api(_) -> gen_server:stop(emqx_os_mon), ok. -t_sys_mem_check_alarm(_) -> +t_sys_mem_check_alarm(Config) -> + case os:type() of + {unix, linux} -> + do_sys_mem_check_alarm(Config); + _ -> + skip + end. + +do_sys_mem_check_alarm(_Config) -> emqx_config:put([sysmon, os, mem_check_interval], 200), emqx_os_mon:update(emqx_config:get([sysmon, os])), Mem = 0.52345, From bc7feeed0b78b57e62108ffe09cc27cf0efb131a Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Wed, 1 Feb 2023 08:45:04 +0100 Subject: [PATCH 0392/1802] fix(ehttpc): upgrade to version 0.4.6 --- mix.exs | 2 +- rebar.config | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mix.exs b/mix.exs index 623321f29..fdc7a3663 100644 --- a/mix.exs +++ b/mix.exs @@ -47,7 +47,7 @@ defmodule EMQXUmbrella.MixProject do {:lc, github: "emqx/lc", tag: "0.3.2", override: true}, {:redbug, "2.0.8"}, {:typerefl, github: "ieQu1/typerefl", tag: "0.9.1", override: true}, - {:ehttpc, github: "emqx/ehttpc", tag: "0.4.5", override: true}, + {:ehttpc, github: "emqx/ehttpc", tag: "0.4.6", override: true}, {:gproc, github: "uwiger/gproc", tag: "0.8.0", override: true}, {:jiffy, github: "emqx/jiffy", tag: "1.0.5", override: true}, {:cowboy, github: "emqx/cowboy", tag: "2.9.0", override: true}, diff --git a/rebar.config b/rebar.config index 71a54a03d..4ee7840ff 100644 --- a/rebar.config +++ b/rebar.config @@ -49,7 +49,7 @@ , {gpb, "4.19.5"} %% gpb only used to build, but not for release, pin it here to avoid fetching a wrong version due to rebar plugins scattered in all the deps , {typerefl, {git, "https://github.com/ieQu1/typerefl", {tag, "0.9.1"}}} , {gun, {git, "https://github.com/emqx/gun", {tag, "1.3.9"}}} - , {ehttpc, {git, "https://github.com/emqx/ehttpc", {tag, "0.4.5"}}} + , {ehttpc, {git, "https://github.com/emqx/ehttpc", {tag, "0.4.6"}}} , {gproc, {git, "https://github.com/uwiger/gproc", {tag, "0.8.0"}}} , {jiffy, {git, "https://github.com/emqx/jiffy", {tag, "1.0.5"}}} , {cowboy, {git, "https://github.com/emqx/cowboy", {tag, "2.9.0"}}} From 3c4d09a752cb4debbf72929511f47e56a6720bb3 Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Wed, 1 Feb 2023 16:00:55 +0800 Subject: [PATCH 0393/1802] fix: get_memory_cache return {ok, Mem} | stale --- apps/emqx_management/src/emqx_mgmt_cache.erl | 40 +++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/apps/emqx_management/src/emqx_mgmt_cache.erl b/apps/emqx_management/src/emqx_mgmt_cache.erl index 37f8e1367..05736a33c 100644 --- a/apps/emqx_management/src/emqx_mgmt_cache.erl +++ b/apps/emqx_management/src/emqx_mgmt_cache.erl @@ -27,14 +27,11 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). get_sys_memory() -> - Now = now_millisecond(), - {CacheMem, ExpiredAt} = get_memory_from_cache(), - case Now > ExpiredAt of - true -> + case get_memory_from_cache() of + {ok, CacheMem} -> erlang:send(?MODULE, fresh_sys_memory), CacheMem; - %% stale cache value, try to recalculate - false -> + stale -> get_sys_memory_sync() end. @@ -80,24 +77,31 @@ code_change(_OldVsn, State, _Extra) -> fresh_sys_memory(State = #{fresh_at := LastFreshAt}) -> Now = now_millisecond(), - {Mem, ExpiredAt} = get_memory_from_cache(), - case Now >= ExpiredAt orelse Now - LastFreshAt >= ?REFRESH_MS of + case Now - LastFreshAt >= ?REFRESH_MS of true -> - %% NOTE: Now /= UpdateAt, because - %% load_ctl:get_sys_memory/0 maybe a heavy operation, - %% so record update_at timestamp after get_sys_memory/0. - NewMem = load_ctl:get_sys_memory(), - NewExpiredAt = now_millisecond() + ?EXPIRED_MS, - ets:insert(?MODULE, {?SYS_MEMORY_KEY, {NewMem, NewExpiredAt}}), - {NewMem, State#{fresh_at => Now}}; + do_fresh_sys_memory(Now, State); false -> - {Mem, State} + case get_memory_from_cache() of + stale -> do_fresh_sys_memory(Now, State); + {ok, Mem} -> {Mem, State} + end end. +do_fresh_sys_memory(FreshAt, State) -> + NewMem = load_ctl:get_sys_memory(), + NewExpiredAt = now_millisecond() + ?EXPIRED_MS, + ets:insert(?MODULE, {?SYS_MEMORY_KEY, {NewMem, NewExpiredAt}}), + {NewMem, State#{fresh_at => FreshAt}}. + get_memory_from_cache() -> case ets:lookup(?MODULE, ?SYS_MEMORY_KEY) of - [] -> {?DEFAULT_BAD_MEMORY, 0}; - [{_, CacheVal}] -> CacheVal + [] -> + stale; + [{_, {Mem, ExpiredAt}}] -> + case now_millisecond() < ExpiredAt of + true -> {ok, Mem}; + false -> stale + end end. now_millisecond() -> From 6ca816f7a4d7dd80ea3b8e01ef0f88e8722f6fe3 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Wed, 1 Feb 2023 09:30:10 +0100 Subject: [PATCH 0394/1802] docs: remove impl details from user doc --- apps/emqx_connector/i18n/emqx_connector_http.conf | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/apps/emqx_connector/i18n/emqx_connector_http.conf b/apps/emqx_connector/i18n/emqx_connector_http.conf index 7583a38ed..da886191b 100644 --- a/apps/emqx_connector/i18n/emqx_connector_http.conf +++ b/apps/emqx_connector/i18n/emqx_connector_http.conf @@ -4,12 +4,12 @@ emqx_connector_http { en: """ The base URL is the URL includes only the scheme, host and port.
When send an HTTP request, the real URL to be used is the concatenation of the base URL and the -path parameter (passed by the emqx_resource:query/2,3 or provided by the request parameter).
+path parameter
For example: `http://localhost:9901/` """ zh: """ base URL 只包含host和port。
-发送HTTP请求时,真实的URL是由base URL 和 path parameter连接而成(通过emqx_resource:query/2,3传递,或者通过请求参数提供)。
+发送HTTP请求时,真实的URL是由base URL 和 path parameter连接而成。
示例:`http://localhost:9901/` """ } @@ -76,14 +76,8 @@ base URL 只包含host和port。
request { desc { - en: """ -If the request is provided, the caller can send HTTP requests via -emqx_resource:query(ResourceId, {send_message, BridgeId, Message}) -""" - zh: """ -如果提供了请求,调用者可以通过以下方式发送 HTTP 请求 -emqx_resource:query(ResourceId, {send_message, BridgeId, Message}) -""" + en: """Configure HTTP request parameters.""" + zh: """设置 HTTP 请求的参数。""" } label: { en: "Request" From f4381d90ca2095e211ca6972a2d4d7ec8ed37f8d Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Wed, 1 Feb 2023 09:30:30 +0100 Subject: [PATCH 0395/1802] refactor: http cliet should not need to retry HTTP requests for bridge --- apps/emqx_connector/src/emqx_connector_http.erl | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/emqx_connector/src/emqx_connector_http.erl b/apps/emqx_connector/src/emqx_connector_http.erl index 7f84c665a..33407f17c 100644 --- a/apps/emqx_connector/src/emqx_connector_http.erl +++ b/apps/emqx_connector/src/emqx_connector_http.erl @@ -264,9 +264,10 @@ on_query(InstId, {send_message, Msg}, State) -> path := Path, body := Body, headers := Headers, - request_timeout := Timeout, - max_retries := Retry + request_timeout := Timeout } = process_request(Request, Msg), + %% bridge buffer worker has retry, do not let ehttpc retry + Retry = 0, on_query( InstId, {undefined, Method, {Path, Headers, Body}, Timeout, Retry}, @@ -274,9 +275,11 @@ on_query(InstId, {send_message, Msg}, State) -> ) end; on_query(InstId, {Method, Request}, State) -> - on_query(InstId, {undefined, Method, Request, 5000, 2}, State); + %% TODO: Get retry from State + on_query(InstId, {undefined, Method, Request, 5000, _Retry = 2}, State); on_query(InstId, {Method, Request, Timeout}, State) -> - on_query(InstId, {undefined, Method, Request, Timeout, 2}, State); + %% TODO: Get retry from State + on_query(InstId, {undefined, Method, Request, Timeout, _Retry = 2}, State); on_query( InstId, {KeyOrNum, Method, Request, Timeout, Retry}, From 9f6b6cedc670f64ced528421e97eeba8e227e56d Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Wed, 1 Feb 2023 09:53:25 +0100 Subject: [PATCH 0396/1802] fix(webhook): pick worker according to pool type --- .../src/emqx_connector_http.erl | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/apps/emqx_connector/src/emqx_connector_http.erl b/apps/emqx_connector/src/emqx_connector_http.erl index 33407f17c..bd129f1a8 100644 --- a/apps/emqx_connector/src/emqx_connector_http.erl +++ b/apps/emqx_connector/src/emqx_connector_http.erl @@ -234,6 +234,7 @@ on_start( PoolName = emqx_plugin_libs_pool:pool_name(InstId), State = #{ pool_name => PoolName, + pool_type => PoolType, host => Host, port => Port, connect_timeout => ConnectTimeout, @@ -283,7 +284,7 @@ on_query(InstId, {Method, Request, Timeout}, State) -> on_query( InstId, {KeyOrNum, Method, Request, Timeout, Retry}, - #{pool_name := PoolName, base_path := BasePath} = State + #{base_path := BasePath} = State ) -> ?TRACE( "QUERY", @@ -291,12 +292,10 @@ on_query( #{request => Request, connector => InstId, state => State} ), NRequest = formalize_request(Method, BasePath, Request), + Worker = resolve_pool_worker(State, KeyOrNum), case ehttpc:request( - case KeyOrNum of - undefined -> PoolName; - _ -> {PoolName, KeyOrNum} - end, + Worker, Method, NRequest, Timeout, @@ -364,19 +363,15 @@ on_query_async( InstId, {KeyOrNum, Method, Request, Timeout}, ReplyFunAndArgs, - #{pool_name := PoolName, base_path := BasePath} = State + #{base_path := BasePath} = State ) -> + Worker = resolve_pool_worker(State, KeyOrNum), ?TRACE( "QUERY_ASYNC", "http_connector_received", #{request => Request, connector => InstId, state => State} ), NRequest = formalize_request(Method, BasePath, Request), - Worker = - case KeyOrNum of - undefined -> ehttpc_pool:pick_worker(PoolName); - _ -> ehttpc_pool:pick_worker(PoolName, KeyOrNum) - end, ok = ehttpc:request_async( Worker, Method, @@ -386,6 +381,16 @@ on_query_async( ), {ok, Worker}. +resolve_pool_worker(State, undefined) -> + resolve_pool_worker(State, self()); +resolve_pool_worker(#{pool_name := PoolName} = State, Key) -> + case maps:get(pool_type, State, random) of + random -> + ehttpc_pool:pick_worker(PoolName); + hash -> + ehttpc_pool:pick_worker(PoolName, Key) + end. + on_get_status(_InstId, #{pool_name := PoolName, connect_timeout := Timeout} = State) -> case do_get_status(PoolName, Timeout) of ok -> From 638291503675fc1fa33a06f55790e23bdd2fa826 Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Wed, 1 Feb 2023 17:36:30 +0800 Subject: [PATCH 0397/1802] chore: repalce fresh by refresh --- apps/emqx_management/src/emqx_mgmt_cache.erl | 22 ++++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/emqx_management/src/emqx_mgmt_cache.erl b/apps/emqx_management/src/emqx_mgmt_cache.erl index 05736a33c..9b3cd4f56 100644 --- a/apps/emqx_management/src/emqx_mgmt_cache.erl +++ b/apps/emqx_management/src/emqx_mgmt_cache.erl @@ -29,7 +29,7 @@ get_sys_memory() -> case get_memory_from_cache() of {ok, CacheMem} -> - erlang:send(?MODULE, fresh_sys_memory), + erlang:send(?MODULE, refresh_sys_memory), CacheMem; stale -> get_sys_memory_sync() @@ -48,10 +48,10 @@ start_link() -> init([]) -> ets:new(?MODULE, [set, named_table, public, {keypos, 1}]), - {ok, #{fresh_at => 0}}. + {ok, #{latest_refresh => 0}}. handle_call(get_sys_memory, _From, State) -> - {Mem, NewState} = fresh_sys_memory(State), + {Mem, NewState} = refresh_sys_memory(State), {reply, Mem, NewState}; handle_call(_Request, _From, State) -> {reply, ok, State}. @@ -59,8 +59,8 @@ handle_call(_Request, _From, State) -> handle_cast(_Request, State) -> {noreply, State}. -handle_info(fresh_sys_memory, State) -> - {_, NewState} = fresh_sys_memory(State), +handle_info(refresh_sys_memory, State) -> + {_, NewState} = refresh_sys_memory(State), {noreply, NewState}; handle_info(_Info, State) -> {noreply, State}. @@ -75,23 +75,23 @@ code_change(_OldVsn, State, _Extra) -> %%% Internal functions %%%=================================================================== -fresh_sys_memory(State = #{fresh_at := LastFreshAt}) -> +refresh_sys_memory(State = #{latest_refresh := LatestRefresh}) -> Now = now_millisecond(), - case Now - LastFreshAt >= ?REFRESH_MS of + case Now - LatestRefresh >= ?REFRESH_MS of true -> - do_fresh_sys_memory(Now, State); + do_refresh_sys_memory(Now, State); false -> case get_memory_from_cache() of - stale -> do_fresh_sys_memory(Now, State); + stale -> do_refresh_sys_memory(Now, State); {ok, Mem} -> {Mem, State} end end. -do_fresh_sys_memory(FreshAt, State) -> +do_refresh_sys_memory(RefreshAt, State) -> NewMem = load_ctl:get_sys_memory(), NewExpiredAt = now_millisecond() + ?EXPIRED_MS, ets:insert(?MODULE, {?SYS_MEMORY_KEY, {NewMem, NewExpiredAt}}), - {NewMem, State#{fresh_at => FreshAt}}. + {NewMem, State#{latest_refresh => RefreshAt}}. get_memory_from_cache() -> case ets:lookup(?MODULE, ?SYS_MEMORY_KEY) of From 96a18e7105598e5b2665dc47d9e1931b49e154f4 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Wed, 1 Feb 2023 09:58:21 +0100 Subject: [PATCH 0398/1802] chore: upgrade to hocon 0.35.3 --- apps/emqx/rebar.config | 2 +- apps/emqx_authn/test/emqx_authn_pgsql_SUITE.erl | 3 +-- apps/emqx_authz/test/emqx_authz_redis_SUITE.erl | 2 +- mix.exs | 2 +- rebar.config | 2 +- 5 files changed, 5 insertions(+), 6 deletions(-) diff --git a/apps/emqx/rebar.config b/apps/emqx/rebar.config index 0ea42c0fb..cad599436 100644 --- a/apps/emqx/rebar.config +++ b/apps/emqx/rebar.config @@ -29,7 +29,7 @@ {esockd, {git, "https://github.com/emqx/esockd", {tag, "5.9.4"}}}, {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.13.9"}}}, {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.8.1"}}}, - {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.35.0"}}}, + {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.35.3"}}}, {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}}, {recon, {git, "https://github.com/ferd/recon", {tag, "2.5.1"}}}, {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "1.0.0"}}} diff --git a/apps/emqx_authn/test/emqx_authn_pgsql_SUITE.erl b/apps/emqx_authn/test/emqx_authn_pgsql_SUITE.erl index ff0bfaea7..fa0658f6a 100644 --- a/apps/emqx_authn/test/emqx_authn_pgsql_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_pgsql_SUITE.erl @@ -112,8 +112,7 @@ t_update_with_invalid_config(_Config) -> #{ kind := validation_error, path := "authentication.server", - reason := required_field, - value := undefined + reason := required_field } ]} }}}, diff --git a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl index c07d920ad..b480e0262 100644 --- a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl @@ -188,7 +188,7 @@ t_create_invalid_config(_Config) -> ?assertMatch( {error, #{ kind := validation_error, - path := "authorization.sources.1" + path := "authorization.sources.1.server" }}, emqx_authz:update(?CMD_REPLACE, [C]) ). diff --git a/mix.exs b/mix.exs index fdc7a3663..cb7d9aa3b 100644 --- a/mix.exs +++ b/mix.exs @@ -68,7 +68,7 @@ defmodule EMQXUmbrella.MixProject do # in conflict by emqtt and hocon {:getopt, "1.0.2", override: true}, {:snabbkaffe, github: "kafka4beam/snabbkaffe", tag: "1.0.0", override: true}, - {:hocon, github: "emqx/hocon", tag: "0.35.0", override: true}, + {:hocon, github: "emqx/hocon", tag: "0.35.3", override: true}, {:emqx_http_lib, github: "emqx/emqx_http_lib", tag: "0.5.2", override: true}, {:esasl, github: "emqx/esasl", tag: "0.2.0"}, {:jose, github: "potatosalad/erlang-jose", tag: "1.11.2"}, diff --git a/rebar.config b/rebar.config index 4ee7840ff..4a35641c4 100644 --- a/rebar.config +++ b/rebar.config @@ -68,7 +68,7 @@ , {system_monitor, {git, "https://github.com/ieQu1/system_monitor", {tag, "3.0.3"}}} , {getopt, "1.0.2"} , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "1.0.0"}}} - , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.35.0"}}} + , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.35.3"}}} , {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.5.2"}}} , {esasl, {git, "https://github.com/emqx/esasl", {tag, "0.2.0"}}} , {jose, {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.11.2"}}} From ff473e0f1bd899e9dbec726dfdaa5a8b29085ec0 Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Wed, 1 Feb 2023 12:55:13 +0300 Subject: [PATCH 0399/1802] test(bufworker): fix testcase flapping due to data races --- .../test/emqx_resource_SUITE.erl | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/apps/emqx_resource/test/emqx_resource_SUITE.erl b/apps/emqx_resource/test/emqx_resource_SUITE.erl index 1516fc870..620516a88 100644 --- a/apps/emqx_resource/test/emqx_resource_SUITE.erl +++ b/apps/emqx_resource/test/emqx_resource_SUITE.erl @@ -618,6 +618,8 @@ t_query_counter_async_inflight_batch(_) -> ), tap_metrics(?LINE), + Sent1 = NumMsgs + BatchSize, + ?check_trace( begin %% this will block the resource_worker as the inflight window is full now @@ -633,6 +635,12 @@ t_query_counter_async_inflight_batch(_) -> [] ), + %% NOTE + %% The query above won't affect the size of the results table for some reason, + %% it's not clear if this is expected behaviour. Only the `async_reply_fun` + %% defined below will be called for the whole batch consisting of 2 increments. + Sent2 = Sent1 + 0, + tap_metrics(?LINE), %% send query now will fail because the resource is blocked. Insert = fun(Tab, Ref, Result) -> @@ -658,7 +666,7 @@ t_query_counter_async_inflight_batch(_) -> %% +2 because the tmp_query above will be retried and succeed %% this time. WindowSize + 2, - 10_000 + 5_000 ), ?assertMatch(ok, emqx_resource:simple_sync_query(?ID, resume)), tap_metrics(?LINE), @@ -666,8 +674,8 @@ t_query_counter_async_inflight_batch(_) -> %% since the previous tmp_query was enqueued to be retried, we %% take it again from the table; this time, it should have %% succeeded. - ?assertMatch([{tmp_query, ok}], ets:take(Tab0, tmp_query)), - ?assertEqual(NumMsgs + BatchSize, ets:info(Tab0, size), #{tab => ets:tab2list(Tab0)}), + ?assertEqual([{tmp_query, ok}], ets:take(Tab0, tmp_query)), + ?assertEqual(Sent2, ets:info(Tab0, size), #{tab => ets:tab2list(Tab0)}), tap_metrics(?LINE), %% send async query, this time everything should be ok. @@ -679,7 +687,7 @@ t_query_counter_async_inflight_batch(_) -> {ok, SRef} = snabbkaffe:subscribe( ?match_event(#{?snk_kind := connector_demo_inc_counter_async}), NumBatches1, - 10_000 + 5_000 ), inc_counter_in_parallel(NumMsgs1, ReqOpts), {ok, _} = snabbkaffe:receive_events(SRef), @@ -693,11 +701,10 @@ t_query_counter_async_inflight_batch(_) -> ) end ), - ?assertEqual( - NumMsgs + BatchSize + NumMsgs1, - ets:info(Tab0, size), - #{tab => ets:tab2list(Tab0)} - ), + + Sent3 = Sent2 + NumMsgs1, + + ?assertEqual(Sent3, ets:info(Tab0, size), #{tab => ets:tab2list(Tab0)}), tap_metrics(?LINE), %% block the resource @@ -720,22 +727,23 @@ t_query_counter_async_inflight_batch(_) -> end ), + Sent4 = Sent3 + NumMsgs + BatchSize, + %% this will block the resource_worker ok = emqx_resource:query(?ID, {inc_counter, 1}), - Sent = NumMsgs + BatchSize + NumMsgs1 + NumMsgs, {ok, SRef1} = snabbkaffe:subscribe( ?match_event(#{?snk_kind := connector_demo_inc_counter_async}), - WindowSize, - 10_000 + WindowSize + 1, + 5_000 ), ?assertMatch(ok, emqx_resource:simple_sync_query(?ID, resume)), {ok, _} = snabbkaffe:receive_events(SRef1), - ?assertEqual(Sent, ets:info(Tab0, size), #{tab => ets:tab2list(Tab0)}), + ?assertEqual(Sent4, ets:info(Tab0, size), #{tab => ets:tab2list(Tab0)}), {ok, Counter} = emqx_resource:simple_sync_query(?ID, get_counter), - ct:pal("get_counter: ~p, sent: ~p", [Counter, Sent]), - ?assert(Sent =< Counter), + ct:pal("get_counter: ~p, sent: ~p", [Counter, Sent4]), + ?assert(Sent4 =< Counter), %% give the metrics some time to stabilize. ct:sleep(1000), From f0395be3830a0baeeba43c7674e17733f875b47b Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Tue, 31 Jan 2023 23:18:46 +0300 Subject: [PATCH 0400/1802] refactor(mqtt-worker): avoid unnecessary abstraction So the code is easier to follow. --- .../src/emqx_connector_mqtt.erl | 5 +- .../src/mqtt/emqx_connector_mqtt_worker.erl | 78 ++++++++++--------- 2 files changed, 45 insertions(+), 38 deletions(-) diff --git a/apps/emqx_connector/src/emqx_connector_mqtt.erl b/apps/emqx_connector/src/emqx_connector_mqtt.erl index 462bac0b8..c1a051836 100644 --- a/apps/emqx_connector/src/emqx_connector_mqtt.erl +++ b/apps/emqx_connector/src/emqx_connector_mqtt.erl @@ -198,8 +198,9 @@ on_query_async(_InstId, {send_message, Msg}, Callback, #{name := InstanceId}) -> ?TRACE("QUERY", "async_send_msg_to_remote_node", #{message => Msg, connector => InstanceId}), case emqx_connector_mqtt_worker:send_to_remote_async(InstanceId, Msg, Callback) of ok -> - % TODO this is racy - {ok, emqx_connector_mqtt_worker:pid(InstanceId)}; + ok; + {ok, Pid} -> + {ok, Pid}; {error, Reason} -> classify_error(Reason) end. diff --git a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl index 85261a063..9fac20153 100644 --- a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl +++ b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl @@ -67,8 +67,7 @@ %% APIs -export([ start_link/2, - stop/1, - pid/1 + stop/1 ]). %% management APIs @@ -175,7 +174,7 @@ mk_client_event_handler(undefined, _Opts) -> connect(Name) -> #{subscriptions := Subscriptions} = get_config(Name), - case emqtt:connect(pid(Name)) of + case emqtt:connect(get_pid(Name)) of {ok, Properties} -> case subscribe_remote_topics(Name, Subscriptions) of ok -> @@ -206,37 +205,28 @@ subscribe_remote_topics(_Ref, undefined) -> stop(Ref) -> emqtt:stop(ref(Ref)). -pid(Name) -> - gproc:lookup_pid(?NAME(Name)). - status(Ref) -> - trycall( - fun() -> - Info = emqtt:info(ref(Ref)), - case proplists:get_value(socket, Info) of - Socket when Socket /= undefined -> - connected; - undefined -> - connecting - end - end, - #{noproc => disconnected} - ). + try + Info = emqtt:info(ref(Ref)), + case proplists:get_value(socket, Info) of + Socket when Socket /= undefined -> + connected; + undefined -> + connecting + end + catch + exit:{noproc, _} -> + disconnected + end. ping(Ref) -> emqtt:ping(ref(Ref)). send_to_remote(Name, MsgIn) -> - trycall( - fun() -> do_send(Name, export_msg(Name, MsgIn)) end, - #{ - badarg => {error, disconnected}, - noproc => {error, disconnected} - } - ). + trycall(fun() -> do_send(Name, export_msg(Name, MsgIn)) end). do_send(Name, {true, Msg}) -> - case emqtt:publish(pid(Name), Msg) of + case emqtt:publish(get_pid(Name), Msg) of ok -> ok; {ok, #{reason_code := RC}} when @@ -263,13 +253,16 @@ do_send(_Name, false) -> ok. send_to_remote_async(Name, MsgIn, Callback) -> - trycall( - fun() -> do_send_async(Name, export_msg(Name, MsgIn), Callback) end, - #{badarg => {error, disconnected}} - ). + trycall(fun() -> do_send_async(Name, export_msg(Name, MsgIn), Callback) end). do_send_async(Name, {true, Msg}, Callback) -> - emqtt:publish_async(pid(Name), Msg, _Timeout = infinity, Callback); + Pid = get_pid(Name), + case emqtt:publish_async(Pid, Msg, _Timeout = infinity, Callback) of + ok -> + {ok, Pid}; + {error, _} = Error -> + Error + end; do_send_async(_Name, false, _Callback) -> ok. @@ -278,14 +271,14 @@ ref(Pid) when is_pid(Pid) -> ref(Term) -> ?REF(Term). -trycall(Fun, Else) -> +trycall(Fun) -> try Fun() catch - error:badarg -> - maps:get(badarg, Else); + throw:noproc -> + {error, disconnected}; exit:{noproc, _} -> - maps:get(noproc, Else) + {error, disconnected} end. format_mountpoint(undefined) -> @@ -325,8 +318,21 @@ pre_process_conf(Key, Conf) -> Conf#{Key => Val} end. +get_pid(Name) -> + case gproc:where(?NAME(Name)) of + Pid when is_pid(Pid) -> + Pid; + undefined -> + throw(noproc) + end. + get_config(Name) -> - gproc:lookup_value(?NAME(Name)). + try + gproc:lookup_value(?NAME(Name)) + catch + error:badarg -> + throw(noproc) + end. export_msg(Name, Msg) -> case get_config(Name) of From ad88938d34f322eaa298b217587a0ed243cfe73b Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Wed, 1 Feb 2023 16:22:19 +0300 Subject: [PATCH 0401/1802] refactor: reuse some parts of test code for brewity --- .../test/emqx_bridge_mqtt_SUITE.erl | 456 +++++------------- 1 file changed, 131 insertions(+), 325 deletions(-) diff --git a/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl b/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl index 6e3bf77ee..52084196a 100644 --- a/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl +++ b/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl @@ -32,7 +32,6 @@ -define(BRIDGE_CONF_DEFAULT, <<"bridges: {}">>). -define(TYPE_MQTT, <<"mqtt">>). --define(NAME_MQTT, <<"my_mqtt_bridge">>). -define(BRIDGE_NAME_INGRESS, <<"ingress_mqtt_bridge">>). -define(BRIDGE_NAME_EGRESS, <<"egress_mqtt_bridge">>). @@ -98,6 +97,24 @@ } }). +-define(assertMetrics(Pat, BridgeID), + ?assertMetrics(Pat, true, BridgeID) +). +-define(assertMetrics(Pat, Guard, BridgeID), + ?assertMatch( + #{ + <<"metrics">> := Pat, + <<"node_metrics">> := [ + #{ + <<"node">> := _, + <<"metrics">> := Pat + } + ] + } when Guard, + request_bridge_metrics(BridgeID) + ) +). + inspect(Selected, _Envs, _Args) -> persistent_term:put(?MODULE, #{inspect => Selected}). @@ -176,7 +193,7 @@ t_mqtt_conn_bridge_ingress(_) -> {ok, 201, Bridge} = request( post, uri(["bridges"]), - ?SERVER_CONF(User1)#{ + ServerConf = ?SERVER_CONF(User1)#{ <<"type">> => ?TYPE_MQTT, <<"name">> => ?BRIDGE_NAME_INGRESS, <<"ingress">> => ?INGRESS_CONF @@ -186,6 +203,7 @@ t_mqtt_conn_bridge_ingress(_) -> <<"type">> := ?TYPE_MQTT, <<"name">> := ?BRIDGE_NAME_INGRESS } = jsx:decode(Bridge), + BridgeIDIngress = emqx_bridge_resource:bridge_id(?TYPE_MQTT, ?BRIDGE_NAME_INGRESS), %% we now test if the bridge works as expected @@ -198,34 +216,12 @@ t_mqtt_conn_bridge_ingress(_) -> %% the remote broker is also the local one. emqx:publish(emqx_message:make(RemoteTopic, Payload)), %% we should receive a message on the local broker, with specified topic - ?assert( - receive - {deliver, LocalTopic, #message{payload = Payload}} -> - ct:pal("local broker got message: ~p on topic ~p", [Payload, LocalTopic]), - true; - Msg -> - ct:pal("Msg: ~p", [Msg]), - false - after 100 -> - false - end - ), + assert_mqtt_msg_received(LocalTopic, Payload), %% verify the metrics of the bridge - {ok, 200, BridgeMetricsStr} = request(get, uri(["bridges", BridgeIDIngress, "metrics"]), []), - ?assertMatch( - #{ - <<"metrics">> := #{<<"matched">> := 0, <<"received">> := 1}, - <<"node_metrics">> := - [ - #{ - <<"node">> := _, - <<"metrics">> := - #{<<"matched">> := 0, <<"received">> := 1} - } - ] - }, - jsx:decode(BridgeMetricsStr) + ?assertMetrics( + #{<<"matched">> := 0, <<"received">> := 1}, + BridgeIDIngress ), %% delete the bridge @@ -236,21 +232,13 @@ t_mqtt_conn_bridge_ingress(_) -> t_mqtt_conn_bridge_ingress_no_payload_template(_) -> User1 = <<"user1">>, - %% create an MQTT bridge, using POST - {ok, 201, Bridge} = request( - post, - uri(["bridges"]), + BridgeIDIngress = create_bridge( ?SERVER_CONF(User1)#{ <<"type">> => ?TYPE_MQTT, <<"name">> => ?BRIDGE_NAME_INGRESS, <<"ingress">> => ?INGRESS_CONF_NO_PAYLOAD_TEMPLATE } ), - #{ - <<"type">> := ?TYPE_MQTT, - <<"name">> := ?BRIDGE_NAME_INGRESS - } = jsx:decode(Bridge), - BridgeIDIngress = emqx_bridge_resource:bridge_id(?TYPE_MQTT, ?BRIDGE_NAME_INGRESS), %% we now test if the bridge works as expected RemoteTopic = <>, @@ -262,40 +250,13 @@ t_mqtt_conn_bridge_ingress_no_payload_template(_) -> %% the remote broker is also the local one. emqx:publish(emqx_message:make(RemoteTopic, Payload)), %% we should receive a message on the local broker, with specified topic - ?assert( - receive - {deliver, LocalTopic, #message{payload = MapMsg}} -> - ct:pal("local broker got message: ~p on topic ~p", [MapMsg, LocalTopic]), - %% the MapMsg is all fields outputed by Rule-Engine. it's a binary coded json here. - case jsx:decode(MapMsg) of - #{<<"payload">> := Payload} -> - true; - _ -> - false - end; - Msg -> - ct:pal("Msg: ~p", [Msg]), - false - after 100 -> - false - end - ), + Msg = assert_mqtt_msg_received(LocalTopic), + ?assertMatch(#{<<"payload">> := Payload}, jsx:decode(Msg#message.payload)), %% verify the metrics of the bridge - {ok, 200, BridgeStr} = request(get, uri(["bridges", BridgeIDIngress, "metrics"]), []), - ?assertMatch( - #{ - <<"metrics">> := #{<<"matched">> := 0, <<"received">> := 1}, - <<"node_metrics">> := - [ - #{ - <<"node">> := _, - <<"metrics">> := - #{<<"matched">> := 0, <<"received">> := 1} - } - ] - }, - jsx:decode(BridgeStr) + ?assertMetrics( + #{<<"matched">> := 0, <<"received">> := 1}, + BridgeIDIngress ), %% delete the bridge @@ -307,22 +268,15 @@ t_mqtt_conn_bridge_ingress_no_payload_template(_) -> t_mqtt_conn_bridge_egress(_) -> %% then we add a mqtt connector, using POST User1 = <<"user1">>, - - {ok, 201, Bridge} = request( - post, - uri(["bridges"]), + BridgeIDEgress = create_bridge( ?SERVER_CONF(User1)#{ <<"type">> => ?TYPE_MQTT, <<"name">> => ?BRIDGE_NAME_EGRESS, <<"egress">> => ?EGRESS_CONF } ), - #{ - <<"type">> := ?TYPE_MQTT, - <<"name">> := ?BRIDGE_NAME_EGRESS - } = jsx:decode(Bridge), - BridgeIDEgress = emqx_bridge_resource:bridge_id(?TYPE_MQTT, ?BRIDGE_NAME_EGRESS), ResourceID = emqx_bridge_resource:resource_id(?TYPE_MQTT, ?BRIDGE_NAME_EGRESS), + %% we now test if the bridge works as expected LocalTopic = <>, RemoteTopic = <>, @@ -334,36 +288,14 @@ t_mqtt_conn_bridge_egress(_) -> emqx:publish(emqx_message:make(LocalTopic, Payload)), %% we should receive a message on the "remote" broker, with specified topic - ?assert( - receive - {deliver, RemoteTopic, #message{payload = Payload, from = From}} -> - ct:pal("local broker got message: ~p on topic ~p", [Payload, RemoteTopic]), - Size = byte_size(ResourceID), - ?assertMatch(<>, From), - true; - Msg -> - ct:pal("Msg: ~p", [Msg]), - false - after 100 -> - false - end - ), + Msg = assert_mqtt_msg_received(RemoteTopic, Payload), + Size = byte_size(ResourceID), + ?assertMatch(<>, Msg#message.from), %% verify the metrics of the bridge - {ok, 200, BridgeMetricsStr} = request(get, uri(["bridges", BridgeIDEgress, "metrics"]), []), - ?assertMatch( - #{ - <<"metrics">> := #{<<"matched">> := 1, <<"success">> := 1, <<"failed">> := 0}, - <<"node_metrics">> := - [ - #{ - <<"node">> := _, - <<"metrics">> := - #{<<"matched">> := 1, <<"success">> := 1, <<"failed">> := 0} - } - ] - }, - jsx:decode(BridgeMetricsStr) + ?assertMetrics( + #{<<"matched">> := 1, <<"success">> := 1, <<"failed">> := 0}, + BridgeIDEgress ), %% delete the bridge @@ -375,21 +307,15 @@ t_mqtt_conn_bridge_egress_no_payload_template(_) -> %% then we add a mqtt connector, using POST User1 = <<"user1">>, - {ok, 201, Bridge} = request( - post, - uri(["bridges"]), + BridgeIDEgress = create_bridge( ?SERVER_CONF(User1)#{ <<"type">> => ?TYPE_MQTT, <<"name">> => ?BRIDGE_NAME_EGRESS, <<"egress">> => ?EGRESS_CONF_NO_PAYLOAD_TEMPLATE } ), - #{ - <<"type">> := ?TYPE_MQTT, - <<"name">> := ?BRIDGE_NAME_EGRESS - } = jsx:decode(Bridge), - BridgeIDEgress = emqx_bridge_resource:bridge_id(?TYPE_MQTT, ?BRIDGE_NAME_EGRESS), ResourceID = emqx_bridge_resource:resource_id(?TYPE_MQTT, ?BRIDGE_NAME_EGRESS), + %% we now test if the bridge works as expected LocalTopic = <>, RemoteTopic = <>, @@ -401,42 +327,15 @@ t_mqtt_conn_bridge_egress_no_payload_template(_) -> emqx:publish(emqx_message:make(LocalTopic, Payload)), %% we should receive a message on the "remote" broker, with specified topic - ?assert( - receive - {deliver, RemoteTopic, #message{payload = MapMsg, from = From}} -> - ct:pal("local broker got message: ~p on topic ~p", [MapMsg, RemoteTopic]), - %% the MapMsg is all fields outputed by Rule-Engine. it's a binary coded json here. - Size = byte_size(ResourceID), - ?assertMatch(<>, From), - case jsx:decode(MapMsg) of - #{<<"payload">> := Payload} -> - true; - _ -> - false - end; - Msg -> - ct:pal("Msg: ~p", [Msg]), - false - after 100 -> - false - end - ), + Msg = assert_mqtt_msg_received(RemoteTopic), + %% the MapMsg is all fields outputed by Rule-Engine. it's a binary coded json here. + ?assertMatch(<>, Msg#message.from), + ?assertMatch(#{<<"payload">> := Payload}, jsx:decode(Msg#message.payload)), %% verify the metrics of the bridge - {ok, 200, BridgeStr} = request(get, uri(["bridges", BridgeIDEgress, "metrics"]), []), - ?assertMatch( - #{ - <<"metrics">> := #{<<"matched">> := 1, <<"success">> := 1, <<"failed">> := 0}, - <<"node_metrics">> := - [ - #{ - <<"node">> := _, - <<"metrics">> := - #{<<"matched">> := 1, <<"success">> := 1, <<"failed">> := 0} - } - ] - }, - jsx:decode(BridgeStr) + ?assertMetrics( + #{<<"matched">> := 1, <<"success">> := 1, <<"failed">> := 0}, + BridgeIDEgress ), %% delete the bridge @@ -447,9 +346,7 @@ t_mqtt_conn_bridge_egress_no_payload_template(_) -> t_egress_custom_clientid_prefix(_Config) -> User1 = <<"user1">>, - {ok, 201, Bridge} = request( - post, - uri(["bridges"]), + BridgeIDEgress = create_bridge( ?SERVER_CONF(User1)#{ <<"clientid_prefix">> => <<"my-custom-prefix">>, <<"type">> => ?TYPE_MQTT, @@ -457,11 +354,6 @@ t_egress_custom_clientid_prefix(_Config) -> <<"egress">> => ?EGRESS_CONF } ), - #{ - <<"type">> := ?TYPE_MQTT, - <<"name">> := ?BRIDGE_NAME_EGRESS - } = jsx:decode(Bridge), - BridgeIDEgress = emqx_bridge_resource:bridge_id(?TYPE_MQTT, ?BRIDGE_NAME_EGRESS), ResourceID = emqx_bridge_resource:resource_id(?TYPE_MQTT, ?BRIDGE_NAME_EGRESS), LocalTopic = <>, RemoteTopic = <>, @@ -470,58 +362,36 @@ t_egress_custom_clientid_prefix(_Config) -> timer:sleep(100), emqx:publish(emqx_message:make(LocalTopic, Payload)), - receive - {deliver, RemoteTopic, #message{from = From}} -> - Size = byte_size(ResourceID), - ?assertMatch(<<"my-custom-prefix:", _ResouceID:Size/binary, _/binary>>, From), - ok - after 1000 -> - ct:fail("should have published message") - end, + Msg = assert_mqtt_msg_received(RemoteTopic, Payload), + Size = byte_size(ResourceID), + ?assertMatch(<<"my-custom-prefix:", _ResouceID:Size/binary, _/binary>>, Msg#message.from), {ok, 204, <<>>} = request(delete, uri(["bridges", BridgeIDEgress]), []), ok. t_mqtt_conn_bridge_ingress_and_egress(_) -> User1 = <<"user1">>, - %% create an MQTT bridge, using POST - {ok, 201, Bridge} = request( - post, - uri(["bridges"]), + BridgeIDIngress = create_bridge( ?SERVER_CONF(User1)#{ <<"type">> => ?TYPE_MQTT, <<"name">> => ?BRIDGE_NAME_INGRESS, <<"ingress">> => ?INGRESS_CONF } ), - - #{ - <<"type">> := ?TYPE_MQTT, - <<"name">> := ?BRIDGE_NAME_INGRESS - } = jsx:decode(Bridge), - BridgeIDIngress = emqx_bridge_resource:bridge_id(?TYPE_MQTT, ?BRIDGE_NAME_INGRESS), - {ok, 201, Bridge2} = request( - post, - uri(["bridges"]), + BridgeIDEgress = create_bridge( ?SERVER_CONF(User1)#{ <<"type">> => ?TYPE_MQTT, <<"name">> => ?BRIDGE_NAME_EGRESS, <<"egress">> => ?EGRESS_CONF } ), - #{ - <<"type">> := ?TYPE_MQTT, - <<"name">> := ?BRIDGE_NAME_EGRESS - } = jsx:decode(Bridge2), - BridgeIDEgress = emqx_bridge_resource:bridge_id(?TYPE_MQTT, ?BRIDGE_NAME_EGRESS), %% we now test if the bridge works as expected LocalTopic = <>, RemoteTopic = <>, Payload = <<"hello">>, emqx:subscribe(RemoteTopic), - {ok, 200, BridgeMetricsStr1} = request(get, uri(["bridges", BridgeIDEgress, "metrics"]), []), #{ <<"metrics">> := #{ <<"matched">> := CntMatched1, <<"success">> := CntSuccess1, <<"failed">> := 0 @@ -538,29 +408,17 @@ t_mqtt_conn_bridge_ingress_and_egress(_) -> } } ] - } = jsx:decode(BridgeMetricsStr1), + } = request_bridge_metrics(BridgeIDEgress), timer:sleep(100), %% PUBLISH a message to the 'local' broker, as we have only one broker, %% the remote broker is also the local one. emqx:publish(emqx_message:make(LocalTopic, Payload)), %% we should receive a message on the "remote" broker, with specified topic - ?assert( - receive - {deliver, RemoteTopic, #message{payload = Payload}} -> - ct:pal("local broker got message: ~p on topic ~p", [Payload, RemoteTopic]), - true; - Msg -> - ct:pal("Msg: ~p", [Msg]), - false - after 100 -> - false - end - ), + assert_mqtt_msg_received(RemoteTopic, Payload), %% verify the metrics of the bridge timer:sleep(1000), - {ok, 200, BridgeMetricsStr2} = request(get, uri(["bridges", BridgeIDEgress, "metrics"]), []), #{ <<"metrics">> := #{ <<"matched">> := CntMatched2, <<"success">> := CntSuccess2, <<"failed">> := 0 @@ -577,7 +435,7 @@ t_mqtt_conn_bridge_ingress_and_egress(_) -> } } ] - } = jsx:decode(BridgeMetricsStr2), + } = request_bridge_metrics(BridgeIDEgress), ?assertEqual(CntMatched2, CntMatched1 + 1), ?assertEqual(CntSuccess2, CntSuccess1 + 1), ?assertEqual(NodeCntMatched2, NodeCntMatched1 + 1), @@ -590,16 +448,13 @@ t_mqtt_conn_bridge_ingress_and_egress(_) -> ok. t_ingress_mqtt_bridge_with_rules(_) -> - {ok, 201, _} = request( - post, - uri(["bridges"]), + BridgeIDIngress = create_bridge( ?SERVER_CONF(<<"user1">>)#{ <<"type">> => ?TYPE_MQTT, <<"name">> => ?BRIDGE_NAME_INGRESS, <<"ingress">> => ?INGRESS_CONF } ), - BridgeIDIngress = emqx_bridge_resource:bridge_id(?TYPE_MQTT, ?BRIDGE_NAME_INGRESS), {ok, 201, Rule} = request( post, @@ -624,18 +479,7 @@ t_ingress_mqtt_bridge_with_rules(_) -> %% the remote broker is also the local one. emqx:publish(emqx_message:make(RemoteTopic, Payload)), %% we should receive a message on the local broker, with specified topic - ?assert( - receive - {deliver, LocalTopic, #message{payload = Payload}} -> - ct:pal("local broker got message: ~p on topic ~p", [Payload, LocalTopic]), - true; - Msg -> - ct:pal("Msg: ~p", [Msg]), - false - after 100 -> - false - end - ), + assert_mqtt_msg_received(LocalTopic, Payload), %% and also the rule should be matched, with matched + 1: {ok, 200, Rule1} = request(get, uri(["rules", RuleId]), []), {ok, 200, Metrics} = request(get, uri(["rules", RuleId, "metrics"]), []), @@ -680,37 +524,22 @@ t_ingress_mqtt_bridge_with_rules(_) -> ), %% verify the metrics of the bridge - {ok, 200, BridgeMetricsStr} = request(get, uri(["bridges", BridgeIDIngress, "metrics"]), []), - ?assertMatch( - #{ - <<"metrics">> := #{<<"matched">> := 0, <<"received">> := 1}, - <<"node_metrics">> := - [ - #{ - <<"node">> := _, - <<"metrics">> := - #{<<"matched">> := 0, <<"received">> := 1} - } - ] - }, - jsx:decode(BridgeMetricsStr) + ?assertMetrics( + #{<<"matched">> := 0, <<"received">> := 1}, + BridgeIDIngress ), {ok, 204, <<>>} = request(delete, uri(["rules", RuleId]), []), {ok, 204, <<>>} = request(delete, uri(["bridges", BridgeIDIngress]), []). t_egress_mqtt_bridge_with_rules(_) -> - {ok, 201, Bridge} = request( - post, - uri(["bridges"]), + BridgeIDEgress = create_bridge( ?SERVER_CONF(<<"user1">>)#{ <<"type">> => ?TYPE_MQTT, <<"name">> => ?BRIDGE_NAME_EGRESS, <<"egress">> => ?EGRESS_CONF } ), - #{<<"type">> := ?TYPE_MQTT, <<"name">> := ?BRIDGE_NAME_EGRESS} = jsx:decode(Bridge), - BridgeIDEgress = emqx_bridge_resource:bridge_id(?TYPE_MQTT, ?BRIDGE_NAME_EGRESS), {ok, 201, Rule} = request( post, @@ -734,18 +563,7 @@ t_egress_mqtt_bridge_with_rules(_) -> %% the remote broker is also the local one. emqx:publish(emqx_message:make(LocalTopic, Payload)), %% we should receive a message on the "remote" broker, with specified topic - ?assert( - receive - {deliver, RemoteTopic, #message{payload = Payload}} -> - ct:pal("remote broker got message: ~p on topic ~p", [Payload, RemoteTopic]), - true; - Msg -> - ct:pal("Msg: ~p", [Msg]), - false - after 100 -> - false - end - ), + assert_mqtt_msg_received(RemoteTopic, Payload), emqx:unsubscribe(RemoteTopic), %% PUBLISH a message to the rule. @@ -780,35 +598,12 @@ t_egress_mqtt_bridge_with_rules(_) -> ), %% we should receive a message on the "remote" broker, with specified topic - ?assert( - receive - {deliver, RemoteTopic2, #message{payload = Payload2}} -> - ct:pal("remote broker got message: ~p on topic ~p", [Payload2, RemoteTopic2]), - true; - Msg -> - ct:pal("Msg: ~p", [Msg]), - false - after 100 -> - false - end - ), + assert_mqtt_msg_received(RemoteTopic2, Payload2), %% verify the metrics of the bridge - {ok, 200, BridgeMetricsStr} = request(get, uri(["bridges", BridgeIDEgress, "metrics"]), []), - ?assertMatch( - #{ - <<"metrics">> := #{<<"matched">> := 2, <<"success">> := 2, <<"failed">> := 0}, - <<"node_metrics">> := - [ - #{ - <<"node">> := _, - <<"metrics">> := #{ - <<"matched">> := 2, <<"success">> := 2, <<"failed">> := 0 - } - } - ] - }, - jsx:decode(BridgeMetricsStr) + ?assertMetrics( + #{<<"matched">> := 2, <<"success">> := 2, <<"failed">> := 0}, + BridgeIDEgress ), {ok, 204, <<>>} = request(delete, uri(["rules", RuleId]), []), @@ -817,10 +612,7 @@ t_egress_mqtt_bridge_with_rules(_) -> t_mqtt_conn_bridge_egress_reconnect(_) -> %% then we add a mqtt connector, using POST User1 = <<"user1">>, - - {ok, 201, Bridge} = request( - post, - uri(["bridges"]), + BridgeIDEgress = create_bridge( ?SERVER_CONF(User1)#{ <<"type">> => ?TYPE_MQTT, <<"name">> => ?BRIDGE_NAME_EGRESS, @@ -837,17 +629,14 @@ t_mqtt_conn_bridge_egress_reconnect(_) -> } } ), - #{ - <<"type">> := ?TYPE_MQTT, - <<"name">> := ?BRIDGE_NAME_EGRESS - } = jsx:decode(Bridge), - BridgeIDEgress = emqx_bridge_resource:bridge_id(?TYPE_MQTT, ?BRIDGE_NAME_EGRESS), + on_exit(fun() -> %% delete the bridge {ok, 204, <<>>} = request(delete, uri(["bridges", BridgeIDEgress]), []), {ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []), ok end), + %% we now test if the bridge works as expected LocalTopic = <>, RemoteTopic = <>, @@ -862,20 +651,9 @@ t_mqtt_conn_bridge_egress_reconnect(_) -> assert_mqtt_msg_received(RemoteTopic, Payload0), %% verify the metrics of the bridge - {ok, 200, BridgeMetricsStr} = request(get, uri(["bridges", BridgeIDEgress, "metrics"]), []), - ?assertMatch( - #{ - <<"metrics">> := #{<<"matched">> := 1, <<"success">> := 1, <<"failed">> := 0}, - <<"node_metrics">> := - [ - #{ - <<"node">> := _, - <<"metrics">> := - #{<<"matched">> := 1, <<"success">> := 1, <<"failed">> := 0} - } - ] - }, - jsx:decode(BridgeMetricsStr) + ?assertMetrics( + #{<<"matched">> := 1, <<"success">> := 1, <<"failed">> := 0}, + BridgeIDEgress ), %% stop the listener 1883 to make the bridge disconnected @@ -906,63 +684,91 @@ t_mqtt_conn_bridge_egress_reconnect(_) -> {ok, _} = snabbkaffe:receive_events(SRef), %% verify the metrics of the bridge, the message should be queued - {ok, 200, BridgeStr1} = request(get, uri(["bridges", BridgeIDEgress]), []), - {ok, 200, BridgeMetricsStr1} = request(get, uri(["bridges", BridgeIDEgress, "metrics"]), []), - Decoded1 = jsx:decode(BridgeStr1), - DecodedMetrics1 = jsx:decode(BridgeMetricsStr1), ?assertMatch( - Status when (Status == <<"connecting">> orelse Status == <<"disconnected">>), - maps:get(<<"status">>, Decoded1) + #{<<"status">> := Status} when Status == <<"connecting">>; Status == <<"disconnected">>, + request_bridge(BridgeIDEgress) ), %% matched >= 3 because of possible retries. - ?assertMatch( + ?assertMetrics( #{ <<"matched">> := Matched, <<"success">> := 1, <<"failed">> := 0, <<"queuing">> := Queuing, <<"inflight">> := Inflight - } when Matched >= 3 andalso Inflight + Queuing == 2, - maps:get(<<"metrics">>, DecodedMetrics1) + }, + Matched >= 3 andalso Inflight + Queuing == 2, + BridgeIDEgress ), %% start the listener 1883 to make the bridge reconnected ok = emqx_listeners:start_listener('tcp:default'), timer:sleep(1500), %% verify the metrics of the bridge, the 2 queued messages should have been sent - {ok, 200, BridgeStr2} = request(get, uri(["bridges", BridgeIDEgress]), []), - {ok, 200, BridgeMetricsStr2} = request(get, uri(["bridges", BridgeIDEgress, "metrics"]), []), - Decoded2 = jsx:decode(BridgeStr2), - ?assertEqual(<<"connected">>, maps:get(<<"status">>, Decoded2)), + ?assertMatch(#{<<"status">> := <<"connected">>}, request_bridge(BridgeIDEgress)), %% matched >= 3 because of possible retries. - ?assertMatch( + ?assertMetrics( #{ - <<"metrics">> := #{ - <<"matched">> := Matched, - <<"success">> := 3, - <<"failed">> := 0, - <<"queuing">> := 0, - <<"retried">> := _ - } - } when Matched >= 3, - jsx:decode(BridgeMetricsStr2) + <<"matched">> := Matched, + <<"success">> := 3, + <<"failed">> := 0, + <<"queuing">> := 0, + <<"retried">> := _ + }, + Matched >= 3, + BridgeIDEgress ), %% also verify the 2 messages have been sent to the remote broker assert_mqtt_msg_received(RemoteTopic, Payload1), assert_mqtt_msg_received(RemoteTopic, Payload2), ok. +assert_mqtt_msg_received(Topic) -> + assert_mqtt_msg_received(Topic, '_', 200). + assert_mqtt_msg_received(Topic, Payload) -> - ct:pal("checking if ~p has been received on ~p", [Payload, Topic]), + assert_mqtt_msg_received(Topic, Payload, 200). + +assert_mqtt_msg_received(Topic, Payload, Timeout) -> receive - {deliver, Topic, #message{payload = Payload}} -> - ct:pal("Got mqtt message: ~p on topic ~p", [Payload, Topic]), - ok - after 300 -> + {deliver, Topic, Msg = #message{}} when Payload == '_' -> + ct:pal("received mqtt ~p on topic ~p", [Msg, Topic]), + Msg; + {deliver, Topic, Msg = #message{payload = Payload}} -> + ct:pal("received mqtt ~p on topic ~p", [Msg, Topic]), + Msg + after Timeout -> {messages, Messages} = process_info(self(), messages), - Msg = io_lib:format("timeout waiting for ~p on topic ~p", [Payload, Topic]), - error({Msg, #{messages => Messages}}) + ct:fail("timeout waiting ~p ms for ~p on topic '~s', messages = ~0p", [ + Timeout, + Payload, + Topic, + Messages + ]) end. +create_bridge(Config = #{<<"type">> := Type, <<"name">> := Name}) -> + {ok, 201, Bridge} = request( + post, + uri(["bridges"]), + Config + ), + ?assertMatch( + #{ + <<"type">> := Type, + <<"name">> := Name + }, + jsx:decode(Bridge) + ), + emqx_bridge_resource:bridge_id(Type, Name). + +request_bridge(BridgeID) -> + {ok, 200, Bridge} = request(get, uri(["bridges", BridgeID]), []), + jsx:decode(Bridge). + +request_bridge_metrics(BridgeID) -> + {ok, 200, BridgeMetrics} = request(get, uri(["bridges", BridgeID, "metrics"]), []), + jsx:decode(BridgeMetrics). + request(Method, Url, Body) -> request(<<"connector_admin">>, Method, Url, Body). From 5ebceb20d2b32ba92522ecf24c5734c4dbf3908e Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Wed, 1 Feb 2023 16:23:58 +0300 Subject: [PATCH 0402/1802] test(mqtt-bridge): also test reconfiguration --- apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl b/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl index 52084196a..7e1d08497 100644 --- a/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl +++ b/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl @@ -206,6 +206,18 @@ t_mqtt_conn_bridge_ingress(_) -> BridgeIDIngress = emqx_bridge_resource:bridge_id(?TYPE_MQTT, ?BRIDGE_NAME_INGRESS), + %% try to create the bridge again + ?assertMatch( + {ok, 400, _}, + request(post, uri(["bridges"]), ServerConf) + ), + + %% try to reconfigure the bridge + ?assertMatch( + {ok, 200, _}, + request(put, uri(["bridges", BridgeIDIngress]), ServerConf) + ), + %% we now test if the bridge works as expected RemoteTopic = <>, LocalTopic = <>, From 8a46cb974e9a45ca1f3e023c3f6e11edd8868b25 Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Wed, 1 Feb 2023 16:24:40 +0300 Subject: [PATCH 0403/1802] test(mqtt-bridge): test async bridge reconnects seamlessly --- .../test/emqx_bridge_mqtt_SUITE.erl | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl b/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl index 7e1d08497..841ed885f 100644 --- a/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl +++ b/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl @@ -735,6 +735,89 @@ t_mqtt_conn_bridge_egress_reconnect(_) -> assert_mqtt_msg_received(RemoteTopic, Payload2), ok. +t_mqtt_conn_bridge_egress_async_reconnect(_) -> + User1 = <<"user1">>, + BridgeIDEgress = create_bridge( + ?SERVER_CONF(User1)#{ + <<"type">> => ?TYPE_MQTT, + <<"name">> => ?BRIDGE_NAME_EGRESS, + <<"egress">> => ?EGRESS_CONF, + <<"resource_opts">> => #{ + <<"worker_pool_size">> => 2, + <<"query_mode">> => <<"async">>, + %% using a long time so we can test recovery + <<"request_timeout">> => <<"15s">>, + %% to make it check the healthy quickly + <<"health_check_interval">> => <<"0.5s">>, + %% to make it reconnect quickly + <<"auto_restart_interval">> => <<"1s">> + } + } + ), + + on_exit(fun() -> + %% delete the bridge + {ok, 204, <<>>} = request(delete, uri(["bridges", BridgeIDEgress]), []), + {ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []), + ok + end), + + Self = self(), + LocalTopic = <>, + RemoteTopic = <>, + emqx:subscribe(RemoteTopic), + + Publisher = start_publisher(LocalTopic, 200, Self), + ct:sleep(1000), + + %% stop the listener 1883 to make the bridge disconnected + ok = emqx_listeners:stop_listener('tcp:default'), + ct:sleep(1500), + ?assertMatch( + #{<<"status">> := Status} when Status == <<"connecting">>; Status == <<"disconnected">>, + request_bridge(BridgeIDEgress) + ), + + %% start the listener 1883 to make the bridge reconnected + ok = emqx_listeners:start_listener('tcp:default'), + timer:sleep(1500), + ?assertMatch( + #{<<"status">> := <<"connected">>}, + request_bridge(BridgeIDEgress) + ), + + N = stop_publisher(Publisher), + + %% all those messages should eventually be delivered + [ + assert_mqtt_msg_received(RemoteTopic, Payload) + || I <- lists:seq(1, N), + Payload <- [integer_to_binary(I)] + ], + + ok. + +start_publisher(Topic, Interval, CtrlPid) -> + spawn_link(fun() -> publisher(Topic, 1, Interval, CtrlPid) end). + +stop_publisher(Pid) -> + _ = Pid ! {self(), stop}, + receive + {Pid, N} -> N + after 1_000 -> ct:fail("publisher ~p did not stop", [Pid]) + end. + +publisher(Topic, N, Delay, CtrlPid) -> + _ = emqx:publish(emqx_message:make(Topic, integer_to_binary(N))), + receive + {CtrlPid, stop} -> + CtrlPid ! {self(), N} + after Delay -> + publisher(Topic, N + 1, Delay, CtrlPid) + end. + +%% + assert_mqtt_msg_received(Topic) -> assert_mqtt_msg_received(Topic, '_', 200). From 4059d8ed8b965642a8de963b8cb21d126591dc47 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Wed, 1 Feb 2023 14:51:40 +0100 Subject: [PATCH 0404/1802] chore: upgrade to enterprise dashboard e1.0.2 --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 4be33b567..6da9231ed 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ export EMQX_DEFAULT_RUNNER = debian:11-slim export OTP_VSN ?= $(shell $(CURDIR)/scripts/get-otp-vsn.sh) export ELIXIR_VSN ?= $(shell $(CURDIR)/scripts/get-elixir-vsn.sh) export EMQX_DASHBOARD_VERSION ?= v1.1.6 -export EMQX_EE_DASHBOARD_VERSION ?= e1.0.1 +export EMQX_EE_DASHBOARD_VERSION ?= e1.0.2 export EMQX_REL_FORM ?= tgz export QUICER_DOWNLOAD_FROM_RELEASE = 1 ifeq ($(OS),Windows_NT) From 38ef37d109517b5ca6b6c3ac00fd29955eae3bcc Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Wed, 1 Feb 2023 14:52:08 +0100 Subject: [PATCH 0405/1802] chore: bump release version to e5.0.0-rc.4 --- apps/emqx/include/emqx_release.hrl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx/include/emqx_release.hrl b/apps/emqx/include/emqx_release.hrl index e3429aef1..72244acaa 100644 --- a/apps/emqx/include/emqx_release.hrl +++ b/apps/emqx/include/emqx_release.hrl @@ -35,7 +35,7 @@ -define(EMQX_RELEASE_CE, "5.0.15"). %% Enterprise edition --define(EMQX_RELEASE_EE, "5.0.0-rc.3"). +-define(EMQX_RELEASE_EE, "5.0.0-rc.4"). %% the HTTP API version -define(EMQX_API_VERSION, "5.0"). From 5fd7f65a1f09637f4f3c7080f1e9da34d71aebd7 Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Wed, 1 Feb 2023 16:52:47 +0300 Subject: [PATCH 0406/1802] test(bufworker): make testcase simpler to follow The confusion was due to the fact that subsequent query was missing `async_reply_fun` and thus, was not accumulating in the results. --- apps/emqx_resource/test/emqx_resource_SUITE.erl | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/apps/emqx_resource/test/emqx_resource_SUITE.erl b/apps/emqx_resource/test/emqx_resource_SUITE.erl index 620516a88..3ae69a47d 100644 --- a/apps/emqx_resource/test/emqx_resource_SUITE.erl +++ b/apps/emqx_resource/test/emqx_resource_SUITE.erl @@ -625,7 +625,7 @@ t_query_counter_async_inflight_batch(_) -> %% this will block the resource_worker as the inflight window is full now {ok, {ok, _}} = ?wait_async_action( - emqx_resource:query(?ID, {inc_counter, 2}), + emqx_resource:query(?ID, {inc_counter, 2}, ReqOpts()), #{?snk_kind := buffer_worker_flush_but_inflight_full}, 5_000 ), @@ -635,11 +635,7 @@ t_query_counter_async_inflight_batch(_) -> [] ), - %% NOTE - %% The query above won't affect the size of the results table for some reason, - %% it's not clear if this is expected behaviour. Only the `async_reply_fun` - %% defined below will be called for the whole batch consisting of 2 increments. - Sent2 = Sent1 + 0, + Sent2 = Sent1 + 1, tap_metrics(?LINE), %% send query now will fail because the resource is blocked. From 14f528cc86f3030ddff469c63ad3c6a5d7cfc251 Mon Sep 17 00:00:00 2001 From: Ilya Averyanov Date: Wed, 1 Feb 2023 14:22:49 +0200 Subject: [PATCH 0407/1802] fix(resources): fix resource lifecycle * do not resume all buffer workers on successful healthcheck * do not pass undefined state to resource healthcheck callback --- .../src/emqx_resource_buffer_worker.erl | 2 +- .../src/emqx_resource_buffer_worker_sup.erl | 10 +++- .../src/emqx_resource_manager.erl | 14 ++--- .../test/emqx_resource_SUITE.erl | 58 +++++++++++++++++++ changes/v5.0.16/fix-9884.en.md | 5 ++ changes/v5.0.16/fix-9884.zh.md | 5 ++ 6 files changed, 85 insertions(+), 9 deletions(-) create mode 100644 changes/v5.0.16/fix-9884.en.md create mode 100644 changes/v5.0.16/fix-9884.zh.md diff --git a/apps/emqx_resource/src/emqx_resource_buffer_worker.erl b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl index f91a994c7..4ef384da6 100644 --- a/apps/emqx_resource/src/emqx_resource_buffer_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl @@ -195,7 +195,7 @@ init({Id, Index, Opts}) -> {ok, running, Data}. running(enter, _, Data) -> - ?tp(buffer_worker_enter_running, #{}), + ?tp(buffer_worker_enter_running, #{id => maps:get(id, Data)}), %% According to `gen_statem' laws, we mustn't call `maybe_flush' %% directly because it may decide to return `{next_state, blocked, _}', %% and that's an invalid response for a state enter call. diff --git a/apps/emqx_resource/src/emqx_resource_buffer_worker_sup.erl b/apps/emqx_resource/src/emqx_resource_buffer_worker_sup.erl index 4987946c9..a00dcdcd2 100644 --- a/apps/emqx_resource/src/emqx_resource_buffer_worker_sup.erl +++ b/apps/emqx_resource/src/emqx_resource_buffer_worker_sup.erl @@ -23,7 +23,7 @@ %% External API -export([start_link/0]). --export([start_workers/2, stop_workers/2]). +-export([start_workers/2, stop_workers/2, worker_pids/1]). %% Callbacks -export([init/1]). @@ -75,6 +75,14 @@ stop_workers(ResId, Opts) -> ensure_worker_pool_removed(ResId), ok. +worker_pids(ResId) -> + lists:map( + fun({_Name, Pid}) -> + Pid + end, + gproc_pool:active_workers(ResId) + ). + %%%============================================================================= %%% Internal %%%============================================================================= diff --git a/apps/emqx_resource/src/emqx_resource_manager.erl b/apps/emqx_resource/src/emqx_resource_manager.erl index 232b17ce7..5de55fc4f 100644 --- a/apps/emqx_resource/src/emqx_resource_manager.erl +++ b/apps/emqx_resource/src/emqx_resource_manager.erl @@ -555,12 +555,14 @@ handle_connected_health_check(Data) -> end ). +with_health_check(#data{state = undefined} = Data, Func) -> + Func(disconnected, Data); with_health_check(Data, Func) -> ResId = Data#data.id, HCRes = emqx_resource:call_health_check(Data#data.manager_id, Data#data.mod, Data#data.state), {Status, NewState, Err} = parse_health_check_result(HCRes, Data), _ = maybe_alarm(Status, ResId), - ok = maybe_resume_resource_workers(Status), + ok = maybe_resume_resource_workers(ResId, Status), UpdatedData = Data#data{ state = NewState, status = Status, error = Err }, @@ -581,14 +583,12 @@ maybe_alarm(_Status, ResId) -> <<"resource down: ", ResId/binary>> ). -maybe_resume_resource_workers(connected) -> +maybe_resume_resource_workers(ResId, connected) -> lists:foreach( - fun({_, Pid, _, _}) -> - emqx_resource_buffer_worker:resume(Pid) - end, - supervisor:which_children(emqx_resource_buffer_worker_sup) + fun emqx_resource_buffer_worker:resume/1, + emqx_resource_buffer_worker_sup:worker_pids(ResId) ); -maybe_resume_resource_workers(_) -> +maybe_resume_resource_workers(_, _) -> ok. maybe_clear_alarm(<>) -> diff --git a/apps/emqx_resource/test/emqx_resource_SUITE.erl b/apps/emqx_resource/test/emqx_resource_SUITE.erl index 620516a88..0d3822ec2 100644 --- a/apps/emqx_resource/test/emqx_resource_SUITE.erl +++ b/apps/emqx_resource/test/emqx_resource_SUITE.erl @@ -24,6 +24,7 @@ -define(TEST_RESOURCE, emqx_connector_demo). -define(ID, <<"id">>). +-define(ID1, <<"id1">>). -define(DEFAULT_RESOURCE_GROUP, <<"default">>). -define(RESOURCE_ERROR(REASON), {error, {resource_error, #{reason := REASON}}}). -define(TRACE_OPTS, #{timetrap => 10000, timeout => 1000}). @@ -1033,6 +1034,63 @@ t_auto_retry(_) -> ), ?assertEqual(ok, Res). +t_health_check_disconnected(_) -> + _ = emqx_resource:create_local( + ?ID, + ?DEFAULT_RESOURCE_GROUP, + ?TEST_RESOURCE, + #{name => test_resource, create_error => true}, + #{auto_retry_interval => 100} + ), + ?assertEqual( + {ok, disconnected}, + emqx_resource:health_check(?ID) + ). + +t_unblock_only_required_buffer_workers(_) -> + {ok, _} = emqx_resource:create( + ?ID, + ?DEFAULT_RESOURCE_GROUP, + ?TEST_RESOURCE, + #{name => test_resource}, + #{ + query_mode => async, + batch_size => 5 + } + ), + lists:foreach( + fun emqx_resource_buffer_worker:block/1, + emqx_resource_buffer_worker_sup:worker_pids(?ID) + ), + emqx_resource:create( + ?ID1, + ?DEFAULT_RESOURCE_GROUP, + ?TEST_RESOURCE, + #{name => test_resource}, + #{ + query_mode => async, + batch_size => 5 + } + ), + %% creation of `?ID1` should not have unblocked `?ID`'s buffer workers + %% so we should see resumes now (`buffer_worker_enter_running`). + ?check_trace( + ?wait_async_action( + lists:foreach( + fun emqx_resource_buffer_worker:resume/1, + emqx_resource_buffer_worker_sup:worker_pids(?ID) + ), + #{?snk_kind := buffer_worker_enter_running}, + 5000 + ), + fun(Trace) -> + ?assertMatch( + [#{id := ?ID} | _], + ?of_kind(buffer_worker_enter_running, Trace) + ) + end + ). + t_retry_batch(_Config) -> {ok, _} = emqx_resource:create( ?ID, diff --git a/changes/v5.0.16/fix-9884.en.md b/changes/v5.0.16/fix-9884.en.md new file mode 100644 index 000000000..7676b1213 --- /dev/null +++ b/changes/v5.0.16/fix-9884.en.md @@ -0,0 +1,5 @@ +Fix resource health check process + +* Do not resume all buffer workers on successful health check. Previously after a successful healthcheck all buffer workers (for all resources) were resumed + +* Do not pass undefined state to resource health check callback. If `on_start` callback never succeeded, the state of the resource is undefined. There is no sense to pass it to `on_get_status` callback. diff --git a/changes/v5.0.16/fix-9884.zh.md b/changes/v5.0.16/fix-9884.zh.md new file mode 100644 index 000000000..6d6894242 --- /dev/null +++ b/changes/v5.0.16/fix-9884.zh.md @@ -0,0 +1,5 @@ +修复资源健康检查流程 + +* 不要在健康检查成功时恢复所有缓冲区工作者。 之前,在成功进行健康检查后,所有缓冲区工作人员(针对所有资源)都已恢复 + +* 不要将未定义的状态传递给资源健康检查回调。 如果 `on_start` 回调从未成功,资源的状态是未定义的。 将它传递给 `on_get_status` 回调是没有意义的。 From 6fd30a7e81a87359e3223bfd2bd4e43841727987 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Tue, 31 Jan 2023 12:35:20 +0100 Subject: [PATCH 0408/1802] docs: improve docs for system monitoring config --- apps/emqx/i18n/emqx_schema_i18n.conf | 42 +++++++++++++++++++++------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/apps/emqx/i18n/emqx_schema_i18n.conf b/apps/emqx/i18n/emqx_schema_i18n.conf index 0ab781280..67a6bf4c7 100644 --- a/apps/emqx/i18n/emqx_schema_i18n.conf +++ b/apps/emqx/i18n/emqx_schema_i18n.conf @@ -188,8 +188,12 @@ emqx_schema { sysmon_vm_long_gc { desc { - en: "Enable Long GC monitoring." - zh: "启用长垃圾回收监控。" + en: """When an Erlang process spends long time to perform garbage collection, a warning level long_gc log is emitted, +and an MQTT message is published to the system topic $SYS/sysmon/long_gc. +""" + zh: """当系统检测到某个 Erlang 进程垃圾回收占用过长时间,会触发一条带有 long_gc 关键字的日志。 +同时还会发布一条主题为 $SYS/sysmon/long_gc 的 MQTT 系统消息。 +""" } label { en: "Enable Long GC monitoring." @@ -199,8 +203,12 @@ emqx_schema { sysmon_vm_long_schedule { desc { - en: "Enable Long Schedule monitoring." - zh: "启用长调度监控。" + en: """When the Erlang VM detect a task scheduled for too long, a warning level 'long_schedule' log is emitted, +and an MQTT message is published to the system topic $SYS/sysmon/long_schedule. +""" + zh: """启用后,如果 Erlang VM 调度器出现某个任务占用时间过长时,会触发一条带有 'long_schedule' 关键字的日志。 +同时还会发布一条主题为 $SYS/sysmon/long_schedule 的 MQTT 系统消息。 +""" } label { en: "Enable Long Schedule monitoring." @@ -210,8 +218,13 @@ emqx_schema { sysmon_vm_large_heap { desc { - en: "Enable Large Heap monitoring." - zh: "启用大 heap 监控。" + en: """When an Erlang process consumed a large amount of memory for its heap space, +the system will write a warning level large_heap log, and an MQTT message is published to +the system topic $SYS/sysmon/large_heap. +""" + zh: """启用后,当一个 Erlang 进程申请了大量内存,系统会触发一条带有 large_heap 关键字的 +warning 级别日志。同时还会发布一条主题为 $SYS/sysmon/busy_dist_port 的 MQTT 系统消息。 +""" } label { en: "Enable Large Heap monitoring." @@ -221,8 +234,13 @@ emqx_schema { sysmon_vm_busy_dist_port { desc { - en: "Enable Busy Distribution Port monitoring." - zh: "启用分布式端口过忙监控。" + en: """When the RPC connection used to communicate with other nodes in the cluster is overloaded, +there will be a busy_dist_port warning log, +and an MQTT message is published to system topic $SYS/sysmon/busy_dist_port. +""" + zh: """启用后,当用于集群接点之间 RPC 的连接过忙时,会触发一条带有 busy_dist_port 关键字的 warning 级别日志。 +同时还会发布一条主题为 $SYS/sysmon/busy_dist_port 的 MQTT 系统消息。 +""" } label { en: "Enable Busy Distribution Port monitoring." @@ -232,8 +250,12 @@ emqx_schema { sysmon_vm_busy_port { desc { - en: "Enable Busy Port monitoring." - zh: "启用端口过忙监控。" + en: """When a port (e.g. TCP socket) is overloaded, there will be a busy_port warning log, +and an MQTT message is published to the system topic $SYS/sysmon/busy_port. +""" + zh: """当一个系统接口(例如 TCP socket)过忙,会触发一条带有 busy_port 关键字的 warning 级别的日志。 +同时还会发布一条主题为 $SYS/sysmon/busy_port 的 MQTT 系统消息。 +""" } label { en: "Enable Busy Port monitoring." From 1a90c1654cc565b4fb38d5a953240eb99e5dcd90 Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Thu, 2 Feb 2023 11:43:04 +0800 Subject: [PATCH 0409/1802] chore: bad typo --- apps/emqx_resource/i18n/emqx_resource_schema_i18n.conf | 6 +++--- lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_kafka.conf | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/emqx_resource/i18n/emqx_resource_schema_i18n.conf b/apps/emqx_resource/i18n/emqx_resource_schema_i18n.conf index 7e9fcd9e7..8e349f4aa 100644 --- a/apps/emqx_resource/i18n/emqx_resource_schema_i18n.conf +++ b/apps/emqx_resource/i18n/emqx_resource_schema_i18n.conf @@ -59,7 +59,7 @@ For bridges only have ingress direction data flow, it can be set to 0 otherwise start_timeout { desc { en: """If 'start_after_created' enabled, how long time do we wait for the resource get started, in milliseconds.""" - zh: """如果选择了创建后立即启动资源,此选项用来设置等待资源启动的超时时间,单位毫秒。""" + zh: """如果选择了创建后立即启动资源,此选项用来设置等待资源启动的超时时间,单位秒。""" } label { en: """Start Timeout""" @@ -80,8 +80,8 @@ For bridges only have ingress direction data flow, it can be set to 0 otherwise query_mode { desc { - en: """Query mode. Optional 'sync/async', default 'sync'.""" - zh: """请求模式。可选 '同步/异步',默认为'同步'模式。""" + en: """Query mode. Optional 'sync/async', default 'async'.""" + zh: """请求模式。可选 '同步/异步',默认为'异步'模式。""" } label { en: """Query mode""" diff --git a/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_kafka.conf b/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_kafka.conf index be1e581bd..cb3d6618d 100644 --- a/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_kafka.conf +++ b/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_kafka.conf @@ -303,7 +303,7 @@ emqx_ee_bridge_kafka { "When a single message is over the limit, it is still sent (as a single element batch)." zh: "最大消息批量字节数。" "大多数 Kafka 环境的默认最低值是 1 MB,EMQX 的默认值比 1 MB 更小是因为需要" - "补偿 Kafka 消息编码索需要的额外字节(尤其是当每条消息都很小的情况下)。" + "补偿 Kafka 消息编码所需要的额外字节(尤其是当每条消息都很小的情况下)。" "当单个消息的大小超过该限制时,它仍然会被发送,(相当于该批量中只有单个消息)。" } label { @@ -371,7 +371,7 @@ emqx_ee_bridge_kafka { "Greater value typically means better throughput. However, there can be a risk of message reordering when this " "value is greater than 1." zh: "设置 Kafka 生产者(每个分区一个)在收到 Kafka 的确认前最多发送多少个请求(批量)。" - "调大这个值通常可以增加吞吐量,但是,当该值设置大于 1 是存在消息乱序的风险。" + "调大这个值通常可以增加吞吐量,但是,当该值设置大于 1 时存在消息乱序的风险。" } label { en: "Max Inflight" @@ -395,14 +395,14 @@ emqx_ee_bridge_kafka { desc { en: "Message buffer mode.\n\n" "memory: Buffer all messages in memory. The messages will be lost in case of EMQX node restart\n" - "disc: Buffer all messages on disk. The messages on disk are able to survive EMQX node restart.\n" + "disk: Buffer all messages on disk. The messages on disk are able to survive EMQX node restart.\n" "hybrid: Buffer message in memory first, when up to certain limit " "(see segment_bytes config for more information), then start offloading " "messages to disk, Like memory mode, the messages will be lost in case of " "EMQX node restart." zh: "消息缓存模式。\n" "memory: 所有的消息都缓存在内存里。如果 EMQX 服务重启,缓存的消息会丢失。\n" - "disc: 缓存到磁盘上。EMQX 重启后会继续发送重启前未发送完成的消息。\n" + "disk: 缓存到磁盘上。EMQX 重启后会继续发送重启前未发送完成的消息。\n" "hybrid: 先将消息缓存在内存中,当内存中的消息堆积超过一定限制" "(配置项 segment_bytes 描述了该限制)后,后续的消息会缓存到磁盘上。" "与 memory 模式一样,如果 EMQX 服务重启,缓存的消息会丢失。" From f81a488010b29a9957282b35f27ac0e4fa8e1102 Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Thu, 2 Feb 2023 15:17:51 +0800 Subject: [PATCH 0410/1802] chore: logwarn if dynlibs is missing --- bin/emqx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/emqx b/bin/emqx index 2c2609b36..132d8cba7 100755 --- a/bin/emqx +++ b/bin/emqx @@ -361,7 +361,7 @@ if [ "$IS_BOOT_COMMAND" = 'yes' ]; then logerr "$COMPATIBILITY_INFO" exit 2 fi - logerr "Using libs from '${DYNLIBS_DIR}' due to missing from the OS." + logwarn "Using libs from '${DYNLIBS_DIR}' due to missing from the OS." fi [ "$DEBUG" -eq 1 ] && set -x fi From 44b7624c10f05093060712ec97ab9b853c1b5a89 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Thu, 2 Feb 2023 09:13:18 +0100 Subject: [PATCH 0411/1802] docs: update changelog --- changes/v5.0.16/fix-9884.en.md | 7 ++----- changes/v5.0.16/fix-9884.zh.md | 7 ++----- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/changes/v5.0.16/fix-9884.en.md b/changes/v5.0.16/fix-9884.en.md index 7676b1213..28eacfc86 100644 --- a/changes/v5.0.16/fix-9884.en.md +++ b/changes/v5.0.16/fix-9884.en.md @@ -1,5 +1,2 @@ -Fix resource health check process - -* Do not resume all buffer workers on successful health check. Previously after a successful healthcheck all buffer workers (for all resources) were resumed - -* Do not pass undefined state to resource health check callback. If `on_start` callback never succeeded, the state of the resource is undefined. There is no sense to pass it to `on_get_status` callback. +Do not resume all buffer workers on successful health check of any individual resource. +Previously after any successful healthcheck, all buffer workers (for all resources) were resumed diff --git a/changes/v5.0.16/fix-9884.zh.md b/changes/v5.0.16/fix-9884.zh.md index 6d6894242..08f6e7188 100644 --- a/changes/v5.0.16/fix-9884.zh.md +++ b/changes/v5.0.16/fix-9884.zh.md @@ -1,5 +1,2 @@ -修复资源健康检查流程 - -* 不要在健康检查成功时恢复所有缓冲区工作者。 之前,在成功进行健康检查后,所有缓冲区工作人员(针对所有资源)都已恢复 - -* 不要将未定义的状态传递给资源健康检查回调。 如果 `on_start` 回调从未成功,资源的状态是未定义的。 将它传递给 `on_get_status` 回调是没有意义的。 +不在任意一个资源健康检查成功时恢复所有资源发送缓存。 +在此修复之前,在任意一个资源成功进行健康检查后,所有资源的缓存都会尝试恢复。 From dcc6bd9c21974ad5c39aa621b9b478c2ad955f93 Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Thu, 2 Feb 2023 14:37:03 +0800 Subject: [PATCH 0412/1802] fix: mongodb bridge'd default connect_opt should be async, not sync --- lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_mongodb.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_mongodb.erl b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_mongodb.erl index b62871299..64fe82163 100644 --- a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_mongodb.erl +++ b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_mongodb.erl @@ -79,7 +79,7 @@ fields("creation_opts") -> enum([sync, async]), #{ desc => ?DESC(emqx_resource_schema, "query_mode"), - default => sync + default => async } )}; (Field) -> From 22c3f5002051ee387c4034c2c4135327a35dc4db Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Thu, 2 Feb 2023 16:52:52 +0800 Subject: [PATCH 0413/1802] fix: add query_mode_sync_only for mysql pgsql redis mongodb bridge --- .../i18n/emqx_resource_schema_i18n.conf | 11 ++++++ .../src/schema/emqx_resource_schema.erl | 34 ++++++++++++++++--- .../src/emqx_ee_bridge_mongodb.erl | 29 ++-------------- .../src/emqx_ee_bridge_mysql.erl | 7 +--- .../src/emqx_ee_bridge_pgsql.erl | 8 +---- .../src/emqx_ee_bridge_redis.erl | 4 +-- 6 files changed, 46 insertions(+), 47 deletions(-) diff --git a/apps/emqx_resource/i18n/emqx_resource_schema_i18n.conf b/apps/emqx_resource/i18n/emqx_resource_schema_i18n.conf index 8e349f4aa..b5f245df4 100644 --- a/apps/emqx_resource/i18n/emqx_resource_schema_i18n.conf +++ b/apps/emqx_resource/i18n/emqx_resource_schema_i18n.conf @@ -89,6 +89,17 @@ For bridges only have ingress direction data flow, it can be set to 0 otherwise } } + query_mode_sync_only { + desc { + en: """Query mode. Currently only support 'sync'.""" + zh: """请求模式。目前只支持同步模式。""" + } + label { + en: """Query mode""" + zh: """请求模式""" + } + } + request_timeout { desc { en: """Timeout for requests. If query_mode is sync, calls to the resource will be blocked for this amount of time before timing out.""" diff --git a/apps/emqx_resource/src/schema/emqx_resource_schema.erl b/apps/emqx_resource/src/schema/emqx_resource_schema.erl index 39513e28c..36b4d7d2e 100644 --- a/apps/emqx_resource/src/schema/emqx_resource_schema.erl +++ b/apps/emqx_resource/src/schema/emqx_resource_schema.erl @@ -30,16 +30,25 @@ namespace() -> "resource_schema". roots() -> []. +fields("resource_opts_sync_only") -> + [ + {resource_opts, + mk( + ref(?MODULE, "creation_opts_sync_only"), + resource_opts_meta() + )} + ]; +fields("creation_opts_sync_only") -> + Fields0 = fields("creation_opts"), + Fields1 = lists:keydelete(async_inflight_window, 1, Fields0), + QueryMod = {query_mode, fun query_mode_sync_only/1}, + lists:keyreplace(query_mode, 1, Fields1, QueryMod); fields("resource_opts") -> [ {resource_opts, mk( ref(?MODULE, "creation_opts"), - #{ - required => false, - default => #{}, - desc => ?DESC(<<"resource_opts">>) - } + resource_opts_meta() )} ]; fields("creation_opts") -> @@ -59,6 +68,13 @@ fields("creation_opts") -> {max_queue_bytes, fun max_queue_bytes/1} ]. +resource_opts_meta() -> + #{ + required => false, + default => #{}, + desc => ?DESC(<<"resource_opts">>) + }. + worker_pool_size(type) -> non_neg_integer(); worker_pool_size(desc) -> ?DESC("worker_pool_size"); worker_pool_size(default) -> ?WORKER_POOL_SIZE; @@ -95,6 +111,12 @@ query_mode(default) -> async; query_mode(required) -> false; query_mode(_) -> undefined. +query_mode_sync_only(type) -> enum([sync]); +query_mode_sync_only(desc) -> ?DESC("query_mode_sync_only"); +query_mode_sync_only(default) -> async; +query_mode_sync_only(required) -> false; +query_mode_sync_only(_) -> undefined. + request_timeout(type) -> hoconsc:union([infinity, emqx_schema:duration_ms()]); request_timeout(desc) -> ?DESC("request_timeout"); request_timeout(default) -> <<"15s">>; @@ -139,4 +161,6 @@ max_queue_bytes(required) -> false; max_queue_bytes(_) -> undefined. desc("creation_opts") -> + ?DESC("creation_opts"); +desc("creation_opts_sync_only") -> ?DESC("creation_opts"). diff --git a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_mongodb.erl b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_mongodb.erl index 64fe82163..8312c081c 100644 --- a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_mongodb.erl +++ b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_mongodb.erl @@ -39,7 +39,7 @@ fields("config") -> {enable, mk(boolean(), #{desc => ?DESC("enable"), default => true})}, {collection, mk(binary(), #{desc => ?DESC("collection"), default => <<"mqtt">>})}, {payload_template, mk(binary(), #{required => false, desc => ?DESC("payload_template")})} - ] ++ fields("resource_opts"); + ] ++ emqx_resource_schema:fields("resource_opts_sync_only"); fields(mongodb_rs) -> emqx_connector_mongo:fields(rs) ++ fields("config"); fields(mongodb_sharded) -> @@ -69,32 +69,7 @@ fields("get_sharded") -> fields("get_single") -> emqx_bridge_schema:status_fields() ++ fields(mongodb_single) ++ - type_and_name_fields(mongodb_single); -fields("creation_opts") -> - lists:map( - fun - ({query_mode, _FieldSchema}) -> - {query_mode, - mk( - enum([sync, async]), - #{ - desc => ?DESC(emqx_resource_schema, "query_mode"), - default => async - } - )}; - (Field) -> - Field - end, - emqx_resource_schema:fields("creation_opts") - ); -fields("resource_opts") -> - [ - {resource_opts, - mk( - ref(?MODULE, "creation_opts"), - #{default => #{}, desc => ?DESC(emqx_resource_schema, "resource_opts")} - )} - ]. + type_and_name_fields(mongodb_single). conn_bridge_examples(Method) -> [ diff --git a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_mysql.erl b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_mysql.erl index fadf05848..fd4d9bdd9 100644 --- a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_mysql.erl +++ b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_mysql.erl @@ -98,8 +98,7 @@ fields("config") -> (emqx_connector_mysql:fields(config) -- emqx_connector_schema_lib:prepare_statement_fields()); fields("creation_opts") -> - Opts = emqx_resource_schema:fields("creation_opts"), - [O || {Field, _} = O <- Opts, not is_hidden_opts(Field)]; + emqx_resource_schema:fields("creation_opts_sync_only"); fields("post") -> [type_field(), name_field() | fields("config")]; fields("put") -> @@ -118,10 +117,6 @@ desc(_) -> %% ------------------------------------------------------------------------------------------------- %% internal -is_hidden_opts(Field) -> - lists:member(Field, [ - async_inflight_window - ]). type_field() -> {type, mk(enum([mysql]), #{required => true, desc => ?DESC("desc_type")})}. diff --git a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_pgsql.erl b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_pgsql.erl index 8bf7b1969..b592197f9 100644 --- a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_pgsql.erl +++ b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_pgsql.erl @@ -100,8 +100,7 @@ fields("config") -> (emqx_connector_pgsql:fields(config) -- emqx_connector_schema_lib:prepare_statement_fields()); fields("creation_opts") -> - Opts = emqx_resource_schema:fields("creation_opts"), - [O || {Field, _} = O <- Opts, not is_hidden_opts(Field)]; + emqx_resource_schema:fields("creation_opts_sync_only"); fields("post") -> fields("post", pgsql); fields("put") -> @@ -122,11 +121,6 @@ desc(_) -> undefined. %% ------------------------------------------------------------------------------------------------- -%% internal -is_hidden_opts(Field) -> - lists:member(Field, [ - async_inflight_window - ]). type_field(Type) -> {type, mk(enum([Type]), #{required => true, desc => ?DESC("desc_type")})}. diff --git a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_redis.erl b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_redis.erl index 6b7239a76..861ef243e 100644 --- a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_redis.erl +++ b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_redis.erl @@ -181,10 +181,10 @@ resource_fields(Type) -> resource_creation_fields("redis_cluster") -> % TODO % Cluster bridge is currently incompatible with batching. - Fields = emqx_resource_schema:fields("creation_opts"), + Fields = emqx_resource_schema:fields("creation_opts_sync_only"), lists:foldl(fun proplists:delete/2, Fields, [batch_size, batch_time]); resource_creation_fields(_) -> - emqx_resource_schema:fields("creation_opts"). + emqx_resource_schema:fields("creation_opts_sync_only"). desc("config") -> ?DESC("desc_config"); From e77675186036dfa1432b2088de64482db4b9ba72 Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Thu, 2 Feb 2023 16:55:11 +0800 Subject: [PATCH 0414/1802] fix: delete enable_batch from redis bridge --- lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_redis.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_redis.erl b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_redis.erl index 861ef243e..18822ba11 100644 --- a/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_redis.erl +++ b/lib-ee/emqx_ee_bridge/src/emqx_ee_bridge_redis.erl @@ -182,7 +182,7 @@ resource_creation_fields("redis_cluster") -> % TODO % Cluster bridge is currently incompatible with batching. Fields = emqx_resource_schema:fields("creation_opts_sync_only"), - lists:foldl(fun proplists:delete/2, Fields, [batch_size, batch_time]); + lists:foldl(fun proplists:delete/2, Fields, [batch_size, batch_time, enable_batch]); resource_creation_fields(_) -> emqx_resource_schema:fields("creation_opts_sync_only"). From f8936013b79a4904ee705fee8ea0bb946cae8e01 Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Thu, 2 Feb 2023 16:58:40 +0800 Subject: [PATCH 0415/1802] chore: replace async with sync --- apps/emqx_resource/src/schema/emqx_resource_schema.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx_resource/src/schema/emqx_resource_schema.erl b/apps/emqx_resource/src/schema/emqx_resource_schema.erl index 36b4d7d2e..fdd65bc3c 100644 --- a/apps/emqx_resource/src/schema/emqx_resource_schema.erl +++ b/apps/emqx_resource/src/schema/emqx_resource_schema.erl @@ -113,7 +113,7 @@ query_mode(_) -> undefined. query_mode_sync_only(type) -> enum([sync]); query_mode_sync_only(desc) -> ?DESC("query_mode_sync_only"); -query_mode_sync_only(default) -> async; +query_mode_sync_only(default) -> sync; query_mode_sync_only(required) -> false; query_mode_sync_only(_) -> undefined. From 22cc1cc745bde3982db5d75123b2db01dc691ff7 Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Thu, 2 Feb 2023 17:33:31 +0800 Subject: [PATCH 0416/1802] fix: make spell_check happy --- apps/emqx_resource/i18n/emqx_resource_schema_i18n.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx_resource/i18n/emqx_resource_schema_i18n.conf b/apps/emqx_resource/i18n/emqx_resource_schema_i18n.conf index b5f245df4..3a18827f2 100644 --- a/apps/emqx_resource/i18n/emqx_resource_schema_i18n.conf +++ b/apps/emqx_resource/i18n/emqx_resource_schema_i18n.conf @@ -91,7 +91,7 @@ For bridges only have ingress direction data flow, it can be set to 0 otherwise query_mode_sync_only { desc { - en: """Query mode. Currently only support 'sync'.""" + en: """Query mode. only support 'sync'.""" zh: """请求模式。目前只支持同步模式。""" } label { From 1c9035d24cff320fdf99ff1c6407d0b1d98de3cf Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Thu, 2 Feb 2023 17:34:09 +0800 Subject: [PATCH 0417/1802] test: remove async from redis ct --- apps/emqx_resource/i18n/emqx_resource_schema_i18n.conf | 2 +- lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_redis_SUITE.erl | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/emqx_resource/i18n/emqx_resource_schema_i18n.conf b/apps/emqx_resource/i18n/emqx_resource_schema_i18n.conf index 3a18827f2..d274d4ba2 100644 --- a/apps/emqx_resource/i18n/emqx_resource_schema_i18n.conf +++ b/apps/emqx_resource/i18n/emqx_resource_schema_i18n.conf @@ -91,7 +91,7 @@ For bridges only have ingress direction data flow, it can be set to 0 otherwise query_mode_sync_only { desc { - en: """Query mode. only support 'sync'.""" + en: """Query mode. Only support 'sync'.""" zh: """请求模式。目前只支持同步模式。""" } label { diff --git a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_redis_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_redis_SUITE.erl index 1d4fa5da4..67a9b4a05 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_redis_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_ee_bridge_redis_SUITE.erl @@ -509,7 +509,7 @@ redis_connect_configs() -> toxiproxy_redis_bridge_config() -> Conf0 = ?REDIS_TOXYPROXY_CONNECT_CONFIG#{ <<"resource_opts">> => #{ - <<"query_mode">> => <<"async">>, + <<"query_mode">> => <<"sync">>, <<"worker_pool_size">> => <<"1">>, <<"batch_size">> => integer_to_binary(?BATCH_SIZE), <<"health_check_interval">> => <<"1s">>, @@ -537,7 +537,7 @@ resource_configs() -> <<"start_timeout">> => <<"15s">> }, batch_on => #{ - <<"query_mode">> => <<"async">>, + <<"query_mode">> => <<"sync">>, <<"worker_pool_size">> => <<"1">>, <<"batch_size">> => integer_to_binary(?BATCH_SIZE), <<"start_timeout">> => <<"15s">> From 0912f13c1f0026dec4a5185171fd7862d50a06c4 Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Thu, 2 Feb 2023 12:55:10 +0300 Subject: [PATCH 0418/1802] fix(mqtt-bridge): stop respecting `clean_start` config parameter We are ignoring the user configuration because there's currently no reliable way to ensure proper session recovery according to the MQTT spec. --- .../test/emqx_bridge_mqtt_SUITE.erl | 23 +++++++++++++++++++ .../src/emqx_connector_mqtt.erl | 6 +++-- .../src/mqtt/emqx_connector_mqtt_schema.erl | 4 +++- .../src/mqtt/emqx_connector_mqtt_worker.erl | 15 ------------ 4 files changed, 30 insertions(+), 18 deletions(-) diff --git a/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl b/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl index 841ed885f..8e8f45dbc 100644 --- a/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl +++ b/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl @@ -242,6 +242,29 @@ t_mqtt_conn_bridge_ingress(_) -> ok. +t_mqtt_conn_bridge_ignores_clean_start(_) -> + BridgeName = atom_to_binary(?FUNCTION_NAME), + BridgeID = create_bridge( + ?SERVER_CONF(<<"user1">>)#{ + <<"type">> => ?TYPE_MQTT, + <<"name">> => BridgeName, + <<"ingress">> => ?INGRESS_CONF, + <<"clean_start">> => false + } + ), + + {ok, 200, BridgeJSON} = request(get, uri(["bridges", BridgeID]), []), + Bridge = jsx:decode(BridgeJSON), + + %% verify that there's no `clean_start` in response + ?assertEqual(#{}, maps:with([<<"clean_start">>], Bridge)), + + %% delete the bridge + {ok, 204, <<>>} = request(delete, uri(["bridges", BridgeID]), []), + {ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []), + + ok. + t_mqtt_conn_bridge_ingress_no_payload_template(_) -> User1 = <<"user1">>, BridgeIDIngress = create_bridge( diff --git a/apps/emqx_connector/src/emqx_connector_mqtt.erl b/apps/emqx_connector/src/emqx_connector_mqtt.erl index c1a051836..cffd138b5 100644 --- a/apps/emqx_connector/src/emqx_connector_mqtt.erl +++ b/apps/emqx_connector/src/emqx_connector_mqtt.erl @@ -251,7 +251,6 @@ basic_config( server := Server, proto_ver := ProtoVer, bridge_mode := BridgeMode, - clean_start := CleanStart, keepalive := KeepAlive, retry_interval := RetryIntv, max_inflight := MaxInflight, @@ -271,7 +270,10 @@ basic_config( %% non-standard mqtt connection packets will be filtered out by LB. %% So let's disable bridge_mode. bridge_mode => BridgeMode, - clean_start => CleanStart, + %% NOTE + %% We are ignoring the user configuration here because there's currently no reliable way + %% to ensure proper session recovery according to the MQTT spec. + clean_start => true, keepalive => ms_to_s(KeepAlive), retry_interval => RetryIntv, max_inflight => MaxInflight, diff --git a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_schema.erl b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_schema.erl index 6ea609cc6..6fabc95e8 100644 --- a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_schema.erl +++ b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_schema.erl @@ -110,7 +110,9 @@ fields("server_configs") -> boolean(), #{ default => true, - desc => ?DESC("clean_start") + desc => ?DESC("clean_start"), + hidden => true, + deprecated => {since, "v5.0.16"} } )}, {keepalive, mk_duration("MQTT Keepalive.", #{default => "300s"})}, diff --git a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl index 9fac20153..631ac9350 100644 --- a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl +++ b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl @@ -425,18 +425,3 @@ printable_maps(Headers) -> #{}, Headers ). - -%% TODO -% maybe_destroy_session(#{connect_opts := ConnectOpts = #{clean_start := false}} = State) -> -% try -% %% Destroy session if clean_start is not set. -% %% Ignore any crashes, just refresh the clean_start = true. -% _ = do_connect(State#{connect_opts => ConnectOpts#{clean_start => true}}), -% _ = disconnect(State), -% ok -% catch -% _:_ -> -% ok -% end; -% maybe_destroy_session(_State) -> -% ok. From 0eb554a62e9f987779dcb5600ad05daf558dff4b Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Thu, 2 Feb 2023 11:20:21 +0100 Subject: [PATCH 0419/1802] fix(kafka): check Kafka partition leader connectivity --- .../kafka/emqx_bridge_impl_kafka_producer.erl | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/lib-ee/emqx_ee_bridge/src/kafka/emqx_bridge_impl_kafka_producer.erl b/lib-ee/emqx_ee_bridge/src/kafka/emqx_bridge_impl_kafka_producer.erl index 1ac619626..ac98209ed 100644 --- a/lib-ee/emqx_ee_bridge/src/kafka/emqx_bridge_impl_kafka_producer.erl +++ b/lib-ee/emqx_ee_bridge/src/kafka/emqx_bridge_impl_kafka_producer.erl @@ -91,6 +91,7 @@ on_start(InstId, Config) -> {ok, #{ message_template => compile_message_template(MessageTemplate), client_id => ClientId, + kafka_topic => KafkaTopic, producers => Producers, resource_id => ResourceID }}; @@ -234,8 +235,35 @@ on_kafka_ack(_Partition, buffer_overflow_discarded, _Callback) -> %% do not apply the callback (which is basically to bump success or fail counter) ok. -on_get_status(_InstId, _State) -> - connected. +on_get_status(_InstId, #{client_id := ClientId, kafka_topic := KafkaTopic}) -> + case wolff_client_sup:find_client(ClientId) of + {ok, Pid} -> + do_get_status(Pid, KafkaTopic); + {error, _Reason} -> + disconnected + end. + +do_get_status(Client, KafkaTopic) -> + %% TODO: add a wolff_producers:check_connectivity + case wolff_client:get_leader_connections(Client, KafkaTopic) of + {ok, Leaders} -> + %% Kafka is considered healthy as long as any of the partition leader is reachable + case + lists:any( + fun({_Partition, Pid}) -> + is_pid(Pid) andalso erlang:is_process_alive(Pid) + end, + Leaders + ) + of + true -> + connected; + false -> + disconnected + end; + {error, _} -> + disconnected + end. %% Parse comma separated host:port list into a [{Host,Port}] list hosts(Hosts) when is_binary(Hosts) -> From 9864587389ac7d8c457e8f84d1a29a2c06260a80 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Thu, 2 Feb 2023 12:03:02 +0100 Subject: [PATCH 0420/1802] fix: send to buffer-supported connector even when disconnected --- apps/emqx_resource/src/emqx_resource.erl | 3 +- .../src/emqx_resource_buffer_worker.erl | 40 ++++++++++++------- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/apps/emqx_resource/src/emqx_resource.erl b/apps/emqx_resource/src/emqx_resource.erl index ad7f30b47..1c5eecfbb 100644 --- a/apps/emqx_resource/src/emqx_resource.erl +++ b/apps/emqx_resource/src/emqx_resource.erl @@ -264,7 +264,8 @@ query(ResId, Request, Opts) -> case {IsBufferSupported, QM} of {true, _} -> %% only Kafka so far - emqx_resource_buffer_worker:simple_async_query(ResId, Request); + Opts1 = Opts#{is_buffer_supported => true}, + emqx_resource_buffer_worker:simple_async_query(ResId, Request, Opts1); {false, sync} -> emqx_resource_buffer_worker:sync_query(ResId, Request, Opts); {false, async} -> diff --git a/apps/emqx_resource/src/emqx_resource_buffer_worker.erl b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl index 4ef384da6..c7b143381 100644 --- a/apps/emqx_resource/src/emqx_resource_buffer_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl @@ -38,7 +38,7 @@ -export([ simple_sync_query/2, - simple_async_query/2 + simple_async_query/3 ]). -export([ @@ -130,10 +130,10 @@ simple_sync_query(Id, Request) -> Result. %% simple async-query the resource without batching and queuing. --spec simple_async_query(id(), request()) -> term(). -simple_async_query(Id, Request) -> +-spec simple_async_query(id(), request(), query_opts()) -> term(). +simple_async_query(Id, Request, QueryOpts0) -> Index = undefined, - QueryOpts = simple_query_opts(), + QueryOpts = maps:merge(simple_query_opts(), QueryOpts0), emqx_resource_metrics:matched_inc(Id), Ref = make_request_ref(), Result = call_query(async, Id, Index, Ref, ?SIMPLE_QUERY(Request), QueryOpts), @@ -851,23 +851,33 @@ handle_async_worker_down(Data0, Pid) -> call_query(QM0, Id, Index, Ref, Query, QueryOpts) -> ?tp(call_query_enter, #{id => Id, query => Query}), case emqx_resource_manager:ets_lookup(Id) of - {ok, _Group, #{mod := Mod, state := ResSt, status := connected} = Data} -> - QM = - case QM0 =:= configured of - true -> maps:get(query_mode, Data); - false -> QM0 - end, - CBM = maps:get(callback_mode, Data), - CallMode = call_mode(QM, CBM), - apply_query_fun(CallMode, Mod, Id, Index, Ref, Query, ResSt, QueryOpts); {ok, _Group, #{status := stopped}} -> ?RESOURCE_ERROR(stopped, "resource stopped or disabled"); - {ok, _Group, #{status := S}} when S == connecting; S == disconnected -> - ?RESOURCE_ERROR(not_connected, "resource not connected"); + {ok, _Group, Resource} -> + QM = + case QM0 =:= configured of + true -> maps:get(query_mode, Resource); + false -> QM0 + end, + do_call_query(QM, Id, Index, Ref, Query, QueryOpts, Resource); {error, not_found} -> ?RESOURCE_ERROR(not_found, "resource not found") end. +do_call_query(QM, Id, Index, Ref, Query, #{is_buffer_supported := true} = QueryOpts, Resource) -> + %% The connector supprots buffer, send even in disconnected state + #{mod := Mod, state := ResSt, callback_mode := CBM} = Resource, + CallMode = call_mode(QM, CBM), + apply_query_fun(CallMode, Mod, Id, Index, Ref, Query, ResSt, QueryOpts); +do_call_query(QM, Id, Index, Ref, Query, QueryOpts, #{status := connected} = Resource) -> + %% when calling from the buffer worker or other simple queries, + %% only apply the query fun when it's at connected status + #{mod := Mod, state := ResSt, callback_mode := CBM} = Resource, + CallMode = call_mode(QM, CBM), + apply_query_fun(CallMode, Mod, Id, Index, Ref, Query, ResSt, QueryOpts); +do_call_query(_QM, _Id, _Index, _Ref, _Query, _QueryOpts, _Data) -> + ?RESOURCE_ERROR(not_connected, "resource not connected"). + -define(APPLY_RESOURCE(NAME, EXPR, REQ), try %% if the callback module (connector) wants to return an error that From 7ad80d012ca381968c0611c67c4bde7866edd6cb Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Thu, 2 Feb 2023 12:23:15 +0100 Subject: [PATCH 0421/1802] chore(kafka): upgrade to woff 1.7.5 comparing to 1.7.4, a counter bump bug was fixed but it does not affect EMQX as the bridge does not make use of the counter which had issue. --- lib-ee/emqx_ee_bridge/rebar.config | 2 +- mix.exs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib-ee/emqx_ee_bridge/rebar.config b/lib-ee/emqx_ee_bridge/rebar.config index 6ca554c72..fa6dd560e 100644 --- a/lib-ee/emqx_ee_bridge/rebar.config +++ b/lib-ee/emqx_ee_bridge/rebar.config @@ -1,5 +1,5 @@ {erl_opts, [debug_info]}. -{deps, [ {wolff, {git, "https://github.com/kafka4beam/wolff.git", {tag, "1.7.4"}}} +{deps, [ {wolff, {git, "https://github.com/kafka4beam/wolff.git", {tag, "1.7.5"}}} , {kafka_protocol, {git, "https://github.com/kafka4beam/kafka_protocol.git", {tag, "4.1.2"}}} , {brod_gssapi, {git, "https://github.com/kafka4beam/brod_gssapi.git", {tag, "v0.1.0-rc1"}}} , {brod, {git, "https://github.com/kafka4beam/brod.git", {tag, "3.16.7"}}} diff --git a/mix.exs b/mix.exs index 8e271390f..c6e497d37 100644 --- a/mix.exs +++ b/mix.exs @@ -130,7 +130,7 @@ defmodule EMQXUmbrella.MixProject do [ {:hstreamdb_erl, github: "hstreamdb/hstreamdb_erl", tag: "0.2.5"}, {:influxdb, github: "emqx/influxdb-client-erl", tag: "1.1.7", override: true}, - {:wolff, github: "kafka4beam/wolff", tag: "1.7.4"}, + {:wolff, github: "kafka4beam/wolff", tag: "1.7.5"}, {:kafka_protocol, github: "kafka4beam/kafka_protocol", tag: "4.1.2", override: true}, {:brod_gssapi, github: "kafka4beam/brod_gssapi", tag: "v0.1.0-rc1"}, {:brod, github: "kafka4beam/brod", tag: "3.16.7"}, From 031b36da32594fa26181478bbeb484d39d29aa29 Mon Sep 17 00:00:00 2001 From: firest Date: Thu, 2 Feb 2023 20:20:52 +0800 Subject: [PATCH 0422/1802] feat(banned): filter out banned messages from mqueue --- apps/emqx/src/emqx_cm.erl | 20 ++++++-- apps/emqx/src/emqx_mqueue.erl | 16 ++++++- apps/emqx/src/emqx_session.erl | 4 ++ apps/emqx/test/emqx_banned_SUITE.erl | 70 ++++++++++++++++++++++++++++ 4 files changed, 105 insertions(+), 5 deletions(-) diff --git a/apps/emqx/src/emqx_cm.erl b/apps/emqx/src/emqx_cm.erl index 66e9a2aee..65f1069a6 100644 --- a/apps/emqx/src/emqx_cm.erl +++ b/apps/emqx/src/emqx_cm.erl @@ -19,6 +19,7 @@ -behaviour(gen_server). +-include("emqx.hrl"). -include("logger.hrl"). -include("types.hrl"). -include_lib("snabbkaffe/include/snabbkaffe.hrl"). @@ -296,9 +297,9 @@ open_session(false, ClientInfo = #{clientid := ClientId}, ConnInfo) -> register_channel(ClientId, Self, ConnInfo), {ok, #{ - session => Session1, + session => clean_session(Session1), present => true, - pendings => Pendings + pendings => clean_pendings(Pendings) }}; {living, ConnMod, ChanPid, Session} -> ok = emqx_session:resume(ClientInfo, Session), @@ -315,9 +316,9 @@ open_session(false, ClientInfo = #{clientid := ClientId}, ConnInfo) -> ), register_channel(ClientId, Self, ConnInfo), {ok, #{ - session => Session1, + session => clean_session(Session1), present => true, - pendings => Pendings + pendings => clean_pendings(Pendings) }}; {error, _} -> CreateSess() @@ -730,3 +731,14 @@ get_connected_client_count() -> undefined -> 0; Size -> Size end. + +clean_session(Session) -> + emqx_session:filter_queue(fun is_banned_msg/1, Session). + +clean_pendings(Pendings) -> + lists:filter(fun is_banned_msg/1, Pendings). + +is_banned_msg(#message{from = ClientId}) -> + [] =:= emqx_banned:look_up({clientid, ClientId}); +is_banned_msg({deliver, _Topic, Msg}) -> + is_banned_msg(Msg). diff --git a/apps/emqx/src/emqx_mqueue.erl b/apps/emqx/src/emqx_mqueue.erl index 494e2b33e..fbf29d754 100644 --- a/apps/emqx/src/emqx_mqueue.erl +++ b/apps/emqx/src/emqx_mqueue.erl @@ -67,7 +67,8 @@ out/1, stats/1, dropped/1, - to_list/1 + to_list/1, + filter/2 ]). -define(NO_PRIORITY_TABLE, disabled). @@ -158,6 +159,19 @@ max_len(#mqueue{max_len = MaxLen}) -> MaxLen. to_list(MQ) -> to_list(MQ, []). +-spec filter(fun((any()) -> boolean()), mqueue()) -> mqueue(). +filter(_Pred, #mqueue{len = 0} = MQ) -> + MQ; +filter(Pred, #mqueue{q = Q, len = Len, dropped = Droppend} = MQ) -> + Q2 = ?PQUEUE:filter(Pred, Q), + case ?PQUEUE:len(Q2) of + Len -> + MQ; + Len2 -> + Diff = Len - Len2, + MQ#mqueue{q = Q2, len = Len2, dropped = Droppend + Diff} + end. + to_list(MQ, Acc) -> case out(MQ) of {empty, _MQ} -> diff --git a/apps/emqx/src/emqx_session.erl b/apps/emqx/src/emqx_session.erl index 2e17190e2..54801f2de 100644 --- a/apps/emqx/src/emqx_session.erl +++ b/apps/emqx/src/emqx_session.erl @@ -82,6 +82,7 @@ deliver/3, enqueue/3, dequeue/2, + filter_queue/2, ignore_local/4, retry/2, terminate/3 @@ -529,6 +530,9 @@ dequeue(ClientInfo, Cnt, Msgs, Q) -> end end. +filter_queue(Pred, #session{mqueue = Q} = Session) -> + Session#session{mqueue = emqx_mqueue:filter(Pred, Q)}. + acc_cnt(#message{qos = ?QOS_0}, Cnt) -> Cnt; acc_cnt(_Msg, Cnt) -> Cnt - 1. diff --git a/apps/emqx/test/emqx_banned_SUITE.erl b/apps/emqx/test/emqx_banned_SUITE.erl index ed22a019a..605c1de6d 100644 --- a/apps/emqx/test/emqx_banned_SUITE.erl +++ b/apps/emqx/test/emqx_banned_SUITE.erl @@ -141,3 +141,73 @@ t_kick(_) -> snabbkaffe:stop(), emqx_banned:delete(Who), ?assertEqual(1, length(?of_kind(kick_session_due_to_banned, Trace))). + +t_session_taken(_) -> + erlang:process_flag(trap_exit, true), + Topic = <<"t/banned">>, + ClientId2 = <<"t_session_taken">>, + MsgNum = 3, + Connect = fun() -> + {ok, C} = emqtt:start_link([ + {clientid, <<"client1">>}, + {proto_ver, v5}, + {clean_start, false}, + {properties, #{'Session-Expiry-Interval' => 120}} + ]), + {ok, _} = emqtt:connect(C), + {ok, _, [0]} = emqtt:subscribe(C, Topic, []), + C + end, + + Publish = fun() -> + lists:foreach( + fun(_) -> + Msg = emqx_message:make(ClientId2, Topic, <<"payload">>), + emqx_broker:safe_publish(Msg) + end, + lists:seq(1, MsgNum) + ) + end, + + C1 = Connect(), + ok = emqtt:disconnect(C1), + + Publish(), + + C2 = Connect(), + ?assertEqual(MsgNum, length(receive_messages(MsgNum + 1))), + ok = emqtt:disconnect(C2), + + Publish(), + + Now = erlang:system_time(second), + Who = {clientid, ClientId2}, + emqx_banned:create(#{ + who => Who, + by => <<"test">>, + reason => <<"test">>, + at => Now, + until => Now + 120 + }), + + C3 = Connect(), + ?assertEqual(0, length(receive_messages(MsgNum + 1))), + emqx_banned:delete(Who), + {ok, #{}, [0]} = emqtt:unsubscribe(C3, Topic), + ok = emqtt:disconnect(C3). + +receive_messages(Count) -> + receive_messages(Count, []). +receive_messages(0, Msgs) -> + Msgs; +receive_messages(Count, Msgs) -> + receive + {publish, Msg} -> + ct:log("Msg: ~p ~n", [Msg]), + receive_messages(Count - 1, [Msg | Msgs]); + Other -> + ct:log("Other Msg: ~p~n", [Other]), + receive_messages(Count, Msgs) + after 1200 -> + Msgs + end. From 080e2b10d4dd14048d13250727c9fba4f79e1b76 Mon Sep 17 00:00:00 2001 From: firest Date: Thu, 2 Feb 2023 20:29:45 +0800 Subject: [PATCH 0423/1802] chore: update changes --- changes/v5.0.16/feat-9893.en.md | 1 + changes/v5.0.16/feat-9893.zh.md | 1 + 2 files changed, 2 insertions(+) create mode 100644 changes/v5.0.16/feat-9893.en.md create mode 100644 changes/v5.0.16/feat-9893.zh.md diff --git a/changes/v5.0.16/feat-9893.en.md b/changes/v5.0.16/feat-9893.en.md new file mode 100644 index 000000000..590d82a0f --- /dev/null +++ b/changes/v5.0.16/feat-9893.en.md @@ -0,0 +1 @@ +When connecting with the flag `clean_start=false`, the new session will filter out banned messages from the `mqueue` before deliver. diff --git a/changes/v5.0.16/feat-9893.zh.md b/changes/v5.0.16/feat-9893.zh.md new file mode 100644 index 000000000..30286a679 --- /dev/null +++ b/changes/v5.0.16/feat-9893.zh.md @@ -0,0 +1 @@ +当使用 `clean_start=false` 标志连接时,新会话将在传递之前从 `mqueue` 中过滤掉被封禁的消息。 From d5c482b0b0724a168556d016559c050038782c7e Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Thu, 2 Feb 2023 12:45:27 +0100 Subject: [PATCH 0424/1802] docs: remove timer unit from description the user input has time unit. e.g. "5s" for 5 seconds etc. --- apps/emqx/i18n/emqx_schema_i18n.conf | 4 ++-- apps/emqx_conf/i18n/emqx_conf_schema.conf | 2 +- apps/emqx_management/i18n/emqx_mgmt_api_alarms_i18n.conf | 2 +- apps/emqx_resource/i18n/emqx_resource_schema_i18n.conf | 4 ++-- apps/emqx_resource/src/emqx_resource_buffer_worker.erl | 2 +- lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_kafka.conf | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/emqx/i18n/emqx_schema_i18n.conf b/apps/emqx/i18n/emqx_schema_i18n.conf index 67a6bf4c7..95790fae7 100644 --- a/apps/emqx/i18n/emqx_schema_i18n.conf +++ b/apps/emqx/i18n/emqx_schema_i18n.conf @@ -46,8 +46,8 @@ emqx_schema { overload_protection_backoff_delay { desc { - en: "When at high load, some unimportant tasks could be delayed for execution, here set the duration in milliseconds precision." - zh: "高负载时,一些不重要的任务可能会延迟执行,在这里设置允许延迟的时间。单位为毫秒。" + en: "When at high load, some unimportant tasks could be delayed for execution, here set the duration." + zh: "高负载时,一些不重要的任务可能会延迟执行,在这里设置允许延迟的时间。" } label { en: "Delay Time" diff --git a/apps/emqx_conf/i18n/emqx_conf_schema.conf b/apps/emqx_conf/i18n/emqx_conf_schema.conf index 131118f5b..522e8cf6e 100644 --- a/apps/emqx_conf/i18n/emqx_conf_schema.conf +++ b/apps/emqx_conf/i18n/emqx_conf_schema.conf @@ -1255,7 +1255,7 @@ Supervisor 报告的类型。默认为 error 类型。 log_overload_kill_restart_after { desc { - en: """If the handler is terminated, it restarts automatically after a delay specified in milliseconds. The value `infinity` prevents restarts.""" + en: """If the handler is terminated, it restarts automatically after a delay. The value `infinity` prevents restarts.""" zh: """如果处理进程终止,它会在以指定的时间后后自动重新启动。 `infinity` 不自动重启。""" } label { diff --git a/apps/emqx_management/i18n/emqx_mgmt_api_alarms_i18n.conf b/apps/emqx_management/i18n/emqx_mgmt_api_alarms_i18n.conf index 0ab09520e..ca8bf0769 100644 --- a/apps/emqx_management/i18n/emqx_mgmt_api_alarms_i18n.conf +++ b/apps/emqx_management/i18n/emqx_mgmt_api_alarms_i18n.conf @@ -62,7 +62,7 @@ The default is false.""" duration { desc { - en: """Indicates how long the alarm has lasted, in milliseconds.""" + en: """Indicates how long the alarm has been active in milliseconds.""" zh: """表明告警已经持续了多久,单位:毫秒。""" } } diff --git a/apps/emqx_resource/i18n/emqx_resource_schema_i18n.conf b/apps/emqx_resource/i18n/emqx_resource_schema_i18n.conf index 8e349f4aa..c46de294b 100644 --- a/apps/emqx_resource/i18n/emqx_resource_schema_i18n.conf +++ b/apps/emqx_resource/i18n/emqx_resource_schema_i18n.conf @@ -58,8 +58,8 @@ For bridges only have ingress direction data flow, it can be set to 0 otherwise start_timeout { desc { - en: """If 'start_after_created' enabled, how long time do we wait for the resource get started, in milliseconds.""" - zh: """如果选择了创建后立即启动资源,此选项用来设置等待资源启动的超时时间,单位秒。""" + en: """Time interval to wait for an auto-started resource to become healthy before responding resource creation requests.""" + zh: """在回复资源创建请求前等待资源进入健康状态的时间。""" } label { en: """Start Timeout""" diff --git a/apps/emqx_resource/src/emqx_resource_buffer_worker.erl b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl index c7b143381..29fe79d09 100644 --- a/apps/emqx_resource/src/emqx_resource_buffer_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl @@ -865,7 +865,7 @@ call_query(QM0, Id, Index, Ref, Query, QueryOpts) -> end. do_call_query(QM, Id, Index, Ref, Query, #{is_buffer_supported := true} = QueryOpts, Resource) -> - %% The connector supprots buffer, send even in disconnected state + %% The connector supports buffer, send even in disconnected state #{mod := Mod, state := ResSt, callback_mode := CBM} = Resource, CallMode = call_mode(QM, CBM), apply_query_fun(CallMode, Mod, Id, Index, Ref, Query, ResSt, QueryOpts); diff --git a/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_kafka.conf b/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_kafka.conf index cb3d6618d..fff798e19 100644 --- a/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_kafka.conf +++ b/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_kafka.conf @@ -220,7 +220,7 @@ emqx_ee_bridge_kafka { desc { en: "When set to 'true', TCP buffer sent as soon as possible. " "Otherwise, the OS kernel may buffer small TCP packets for a while (40 ms by default)." - zh: "设置 ‘true' 让系统内核立即发送。否则当需要发送当内容很少时,可能会有一定延迟(默认 40 毫秒)。" + zh: "设置‘true’让系统内核立即发送。否则当需要发送的内容很少时,可能会有一定延迟(默认 40 毫秒)。" } label { en: "No Delay" From cf8432227abbbdfba44f3c2e96f2b153dcb69ee3 Mon Sep 17 00:00:00 2001 From: Ivan Dyachkov Date: Thu, 2 Feb 2023 13:40:29 +0100 Subject: [PATCH 0425/1802] ci: always run static_checks --- .github/workflows/build_slim_packages.yaml | 4 ---- .github/workflows/run_test_cases.yaml | 1 + 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/build_slim_packages.yaml b/.github/workflows/build_slim_packages.yaml index bfdeb1fb8..88e454860 100644 --- a/.github/workflows/build_slim_packages.yaml +++ b/.github/workflows/build_slim_packages.yaml @@ -55,10 +55,6 @@ jobs: run: | make ${EMQX_NAME}-tgz ./scripts/pkg-tests.sh ${EMQX_NAME}-tgz - - name: run static checks - if: contains(matrix.os, 'ubuntu') - run: | - make static_checks - name: build and test deb/rpm packages run: | make ${EMQX_NAME}-pkg diff --git a/.github/workflows/run_test_cases.yaml b/.github/workflows/run_test_cases.yaml index 2d9d2f01e..ac0edef13 100644 --- a/.github/workflows/run_test_cases.yaml +++ b/.github/workflows/run_test_cases.yaml @@ -77,6 +77,7 @@ jobs: make ensure-rebar3 # fetch all deps and compile make ${{ matrix.profile }} + make static_checks make test-compile cd .. zip -ryq source.zip source/* source/.[^.]* From 5b1e9764126f371ea3d2a94b2801947e0306d6e3 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Thu, 2 Feb 2023 13:56:09 +0100 Subject: [PATCH 0426/1802] chore: upgrade replayq from 0.3.6 to 0.3.7 only included a dialyzer fix --- mix.exs | 2 +- rebar.config | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mix.exs b/mix.exs index c6e497d37..6fbddb28b 100644 --- a/mix.exs +++ b/mix.exs @@ -58,7 +58,7 @@ defmodule EMQXUmbrella.MixProject do {:grpc, github: "emqx/grpc-erl", tag: "0.6.7", override: true}, {:minirest, github: "emqx/minirest", tag: "1.3.7", override: true}, {:ecpool, github: "emqx/ecpool", tag: "0.5.3", override: true}, - {:replayq, github: "emqx/replayq", tag: "0.3.6", override: true}, + {:replayq, github: "emqx/replayq", tag: "0.3.7", override: true}, {:pbkdf2, github: "emqx/erlang-pbkdf2", tag: "2.0.4", override: true}, {:emqtt, github: "emqx/emqtt", tag: "1.7.0-rc.2", override: true}, {:rulesql, github: "emqx/rulesql", tag: "0.1.4"}, diff --git a/rebar.config b/rebar.config index 4a35641c4..e61aac544 100644 --- a/rebar.config +++ b/rebar.config @@ -60,7 +60,7 @@ , {grpc, {git, "https://github.com/emqx/grpc-erl", {tag, "0.6.7"}}} , {minirest, {git, "https://github.com/emqx/minirest", {tag, "1.3.7"}}} , {ecpool, {git, "https://github.com/emqx/ecpool", {tag, "0.5.3"}}} - , {replayq, {git, "https://github.com/emqx/replayq.git", {tag, "0.3.6"}}} + , {replayq, {git, "https://github.com/emqx/replayq.git", {tag, "0.3.7"}}} , {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}} , {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.7.0-rc.2"}}} , {rulesql, {git, "https://github.com/emqx/rulesql", {tag, "0.1.4"}}} From c0d478bd414f125d4eb10a76f82b1f083a2965de Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Thu, 2 Feb 2023 13:59:28 +0100 Subject: [PATCH 0427/1802] fix(buffer_worker): type spec --- apps/emqx_resource/include/emqx_resource.hrl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/emqx_resource/include/emqx_resource.hrl b/apps/emqx_resource/include/emqx_resource.hrl index 7464eb4f8..ce4e02c2a 100644 --- a/apps/emqx_resource/include/emqx_resource.hrl +++ b/apps/emqx_resource/include/emqx_resource.hrl @@ -31,7 +31,9 @@ pick_key => term(), timeout => timeout(), expire_at => infinity | integer(), - async_reply_fun => reply_fun() + async_reply_fun => reply_fun(), + simple_query => boolean(), + is_buffer_supported => boolean() }. -type resource_data() :: #{ id := resource_id(), From 1c748070c115cc668b59f06c959d26a83cd28c1b Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Thu, 2 Feb 2023 14:01:27 +0100 Subject: [PATCH 0428/1802] fix(emqx_connector_mqtt_worker): publish_async can only return ok # Please enter the commit message for your changes. Lines starting --- .../src/mqtt/emqx_connector_mqtt_worker.erl | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl index 631ac9350..6da63f99a 100644 --- a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl +++ b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl @@ -257,12 +257,8 @@ send_to_remote_async(Name, MsgIn, Callback) -> do_send_async(Name, {true, Msg}, Callback) -> Pid = get_pid(Name), - case emqtt:publish_async(Pid, Msg, _Timeout = infinity, Callback) of - ok -> - {ok, Pid}; - {error, _} = Error -> - Error - end; + ok = emqtt:publish_async(Pid, Msg, _Timeout = infinity, Callback), + {ok, Pid}; do_send_async(_Name, false, _Callback) -> ok. From 9a9943d35db63e0f9802dc3c9e81270819940a6d Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Thu, 2 Feb 2023 14:02:34 +0100 Subject: [PATCH 0429/1802] chore(emqx_mgmt_cache): ignore unused return value for dialyer --- apps/emqx_management/src/emqx_mgmt_cache.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx_management/src/emqx_mgmt_cache.erl b/apps/emqx_management/src/emqx_mgmt_cache.erl index 9b3cd4f56..e7f9ac0b1 100644 --- a/apps/emqx_management/src/emqx_mgmt_cache.erl +++ b/apps/emqx_management/src/emqx_mgmt_cache.erl @@ -47,7 +47,7 @@ start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). init([]) -> - ets:new(?MODULE, [set, named_table, public, {keypos, 1}]), + _ = ets:new(?MODULE, [set, named_table, public, {keypos, 1}]), {ok, #{latest_refresh => 0}}. handle_call(get_sys_memory, _From, State) -> From 58627b7958b03ca2f4ee087878fd22d9eb1db45d Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Thu, 2 Feb 2023 14:08:02 +0100 Subject: [PATCH 0430/1802] chore(emqx_resource_manager): ignore unused return value for dialyzer --- apps/emqx_resource/src/emqx_resource_manager.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/emqx_resource/src/emqx_resource_manager.erl b/apps/emqx_resource/src/emqx_resource_manager.erl index 5de55fc4f..db4441d88 100644 --- a/apps/emqx_resource/src/emqx_resource_manager.erl +++ b/apps/emqx_resource/src/emqx_resource_manager.erl @@ -194,7 +194,7 @@ remove(ResId, ClearMetrics) when is_binary(ResId) -> restart(ResId, Opts) when is_binary(ResId) -> case safe_call(ResId, restart, ?T_OPERATION) of ok -> - wait_for_ready(ResId, maps:get(start_timeout, Opts, 5000)), + _ = wait_for_ready(ResId, maps:get(start_timeout, Opts, 5000)), ok; {error, _Reason} = Error -> Error @@ -205,7 +205,7 @@ restart(ResId, Opts) when is_binary(ResId) -> start(ResId, Opts) -> case safe_call(ResId, start, ?T_OPERATION) of ok -> - wait_for_ready(ResId, maps:get(start_timeout, Opts, 5000)), + _ = wait_for_ready(ResId, maps:get(start_timeout, Opts, 5000)), ok; {error, _Reason} = Error -> Error From 0e299642cbf3aa2c803e3b860f2ee1e72d86dbe2 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Thu, 2 Feb 2023 14:16:23 +0100 Subject: [PATCH 0431/1802] chore: function renamed but forgot to update xref excemption --- apps/emqx/test/emqx_bpapi_static_checks.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx/test/emqx_bpapi_static_checks.erl b/apps/emqx/test/emqx_bpapi_static_checks.erl index f218739fc..142750cac 100644 --- a/apps/emqx/test/emqx_bpapi_static_checks.erl +++ b/apps/emqx/test/emqx_bpapi_static_checks.erl @@ -65,7 +65,7 @@ % Reason: legacy code. A fun and a QC query are % passed in the args, it's futile to try to statically % check it - "emqx_mgmt_api:do_query/2, emqx_mgmt_api:collect_total_from_tail_nodes/3" + "emqx_mgmt_api:do_query/2, emqx_mgmt_api:collect_total_from_tail_nodes/2" ). -define(XREF, myxref). From 980ba2fd35affcb995adf39a5f0ec61ceee4dbce Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Thu, 2 Feb 2023 14:19:37 +0100 Subject: [PATCH 0432/1802] docs: Update apps/emqx/i18n/emqx_schema_i18n.conf Co-authored-by: Ivan Dyachkov --- apps/emqx/i18n/emqx_schema_i18n.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx/i18n/emqx_schema_i18n.conf b/apps/emqx/i18n/emqx_schema_i18n.conf index 95790fae7..88ff373be 100644 --- a/apps/emqx/i18n/emqx_schema_i18n.conf +++ b/apps/emqx/i18n/emqx_schema_i18n.conf @@ -46,7 +46,7 @@ emqx_schema { overload_protection_backoff_delay { desc { - en: "When at high load, some unimportant tasks could be delayed for execution, here set the duration." + en: "The maximum duration of delay for background task execution during high load conditions." zh: "高负载时,一些不重要的任务可能会延迟执行,在这里设置允许延迟的时间。" } label { From 0d7cd2f0c55b77577c1aa40225eb260f282203f6 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Thu, 2 Feb 2023 14:20:07 +0100 Subject: [PATCH 0433/1802] docs: Update apps/emqx_conf/i18n/emqx_conf_schema.conf Co-authored-by: Ivan Dyachkov --- apps/emqx_conf/i18n/emqx_conf_schema.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx_conf/i18n/emqx_conf_schema.conf b/apps/emqx_conf/i18n/emqx_conf_schema.conf index 522e8cf6e..56da34621 100644 --- a/apps/emqx_conf/i18n/emqx_conf_schema.conf +++ b/apps/emqx_conf/i18n/emqx_conf_schema.conf @@ -1255,7 +1255,7 @@ Supervisor 报告的类型。默认为 error 类型。 log_overload_kill_restart_after { desc { - en: """If the handler is terminated, it restarts automatically after a delay. The value `infinity` prevents restarts.""" + en: """The handler restarts automatically after a delay in the event of termination, unless the value `infinity` is set, which blocks any subsequent restarts.""" zh: """如果处理进程终止,它会在以指定的时间后后自动重新启动。 `infinity` 不自动重启。""" } label { From 85fb53dcf0099a1c03e47d1d827ad8486e46f249 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Thu, 2 Feb 2023 14:58:50 +0100 Subject: [PATCH 0434/1802] refactor: use compile switch to diff functions between ce and ee --- apps/emqx_bridge/src/emqx_bridge_api.erl | 10 +++--- .../src/schema/emqx_bridge_schema.erl | 36 ++++++++++--------- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/apps/emqx_bridge/src/emqx_bridge_api.erl b/apps/emqx_bridge/src/emqx_bridge_api.erl index 2c43ce5d7..2e94f0719 100644 --- a/apps/emqx_bridge/src/emqx_bridge_api.erl +++ b/apps/emqx_bridge/src/emqx_bridge_api.erl @@ -171,12 +171,12 @@ bridge_info_examples(Method, WithMetrics) -> ee_bridge_examples(Method) ). +-if(?EMQX_RELEASE_EDITION == ee). ee_bridge_examples(Method) -> - try - emqx_ee_bridge:examples(Method) - catch - _:_ -> #{} - end. + emqx_ee_bridge:examples(Method). +-else. +ee_bridge_examples(_Method) -> #{}. +-endif. info_example(Type, Method, WithMetrics) -> maps:merge( diff --git a/apps/emqx_bridge/src/schema/emqx_bridge_schema.erl b/apps/emqx_bridge/src/schema/emqx_bridge_schema.erl index 4aecfac5d..c490294eb 100644 --- a/apps/emqx_bridge/src/schema/emqx_bridge_schema.erl +++ b/apps/emqx_bridge/src/schema/emqx_bridge_schema.erl @@ -56,8 +56,8 @@ api_schema(Method) -> EE = ee_api_schemas(Method), hoconsc:union(Broker ++ EE). +-if(?EMQX_RELEASE_EDITION == ee). ee_api_schemas(Method) -> - %% must ensure the app is loaded before checking if fn is defined. ensure_loaded(emqx_ee_bridge, emqx_ee_bridge), case erlang:function_exported(emqx_ee_bridge, api_schemas, 1) of true -> emqx_ee_bridge:api_schemas(Method); @@ -65,13 +65,31 @@ ee_api_schemas(Method) -> end. ee_fields_bridges() -> - %% must ensure the app is loaded before checking if fn is defined. ensure_loaded(emqx_ee_bridge, emqx_ee_bridge), case erlang:function_exported(emqx_ee_bridge, fields, 1) of true -> emqx_ee_bridge:fields(bridges); false -> [] end. +%% must ensure the app is loaded before checking if fn is defined. +ensure_loaded(App, Mod) -> + try + _ = application:load(App), + _ = Mod:module_info(), + ok + catch + _:_ -> + ok + end. + +-else. + +ee_api_schemas(_) -> []. + +ee_fields_bridges() -> []. + +-endif. + common_bridge_fields() -> [ {enable, @@ -194,17 +212,3 @@ status() -> node_name() -> {"node", mk(binary(), #{desc => ?DESC("desc_node_name"), example => "emqx@127.0.0.1"})}. - -%%================================================================================================= -%% Internal fns -%%================================================================================================= - -ensure_loaded(App, Mod) -> - try - _ = application:load(App), - _ = Mod:module_info(), - ok - catch - _:_ -> - ok - end. From 7df50032c192b7401f1e66ce80b2f79a9a1a95b7 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Thu, 2 Feb 2023 15:00:36 +0100 Subject: [PATCH 0435/1802] ci: only run static check for emqx-enterprise profile --- Makefile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 6da9231ed..15bc0c79c 100644 --- a/Makefile +++ b/Makefile @@ -79,7 +79,8 @@ ct: $(REBAR) merge-config .PHONY: static_checks static_checks: - @$(REBAR) as check do dialyzer, xref, ct --suite apps/emqx/test/emqx_static_checks --readable $(CT_READABLE) + @$(REBAR) as check do dialyzer, xref + @if [ "$${PROFILE}" = 'emqx-enterprise' ]; then $(REBAR) ct --suite apps/emqx/test/emqx_static_checks --readable $(CT_READABLE); fi APPS=$(shell $(SCRIPTS)/find-apps.sh) From 36ff3d75960087f51ddd060a53e564bd4800f491 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Thu, 2 Feb 2023 16:11:17 +0100 Subject: [PATCH 0436/1802] chore: add a comment to explain why bpapi is only checked for enterprise --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index 15bc0c79c..07160b7b0 100644 --- a/Makefile +++ b/Makefile @@ -77,6 +77,7 @@ test-compile: $(REBAR) merge-config ct: $(REBAR) merge-config @ENABLE_COVER_COMPILE=1 $(REBAR) ct --name $(CT_NODE_NAME) -c -v --cover_export_name $(CT_COVER_EXPORT_PREFIX)-ct +## only check bpapi for enterprise profile because it's a super-set. .PHONY: static_checks static_checks: @$(REBAR) as check do dialyzer, xref From a71d983ff886b22a94d23057bea4571643ad9622 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Thu, 2 Feb 2023 16:10:41 +0100 Subject: [PATCH 0437/1802] test: fix assertMatch patter to make rebar3 fmt work --- apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl b/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl index 8e8f45dbc..c4afa4db2 100644 --- a/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl +++ b/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl @@ -720,7 +720,8 @@ t_mqtt_conn_bridge_egress_reconnect(_) -> %% verify the metrics of the bridge, the message should be queued ?assertMatch( - #{<<"status">> := Status} when Status == <<"connecting">>; Status == <<"disconnected">>, + #{<<"status">> := Status} when + Status == <<"connecting">> orelse Status == <<"disconnected">>, request_bridge(BridgeIDEgress) ), %% matched >= 3 because of possible retries. @@ -797,7 +798,8 @@ t_mqtt_conn_bridge_egress_async_reconnect(_) -> ok = emqx_listeners:stop_listener('tcp:default'), ct:sleep(1500), ?assertMatch( - #{<<"status">> := Status} when Status == <<"connecting">>; Status == <<"disconnected">>, + #{<<"status">> := Status} when + Status == <<"connecting">> orelse Status == <<"disconnected">>, request_bridge(BridgeIDEgress) ), From 3d6756cbe800e43d45bb0c8bf0b1e2d0152b522f Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Thu, 2 Feb 2023 16:37:56 +0100 Subject: [PATCH 0438/1802] test: fix addrinuse errors in listeners API test SUITE --- .../test/emqx_mgmt_api_listeners_SUITE.erl | 67 ++++++++++++++----- 1 file changed, 49 insertions(+), 18 deletions(-) diff --git a/apps/emqx_management/test/emqx_mgmt_api_listeners_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_api_listeners_SUITE.erl index 9a8824e7d..0e212d52f 100644 --- a/apps/emqx_management/test/emqx_mgmt_api_listeners_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_api_listeners_SUITE.erl @@ -20,6 +20,8 @@ -include_lib("eunit/include/eunit.hrl"). +-define(PORT, (20000 + ?LINE)). + all() -> emqx_common_test_helpers:all(?MODULE). @@ -32,13 +34,38 @@ end_per_suite(_) -> emqx_conf:remove([listeners, tcp, new1], #{override_to => local}), emqx_mgmt_api_test_util:end_suite([emqx_conf]). -t_max_connection_default(_Config) -> +init_per_testcase(Case, Config) -> + try + ?MODULE:Case({init, Config}) + catch + error:function_clause -> + Config + end. + +end_per_testcase(Case, Config) -> + try + ?MODULE:Case({'end', Config}) + catch + error:function_clause -> + ok + end. + +t_max_connection_default({init, Config}) -> emqx_mgmt_api_test_util:end_suite([emqx_conf]), Etc = filename:join(["etc", "emqx.conf.all"]), + TmpConfName = atom_to_list(?FUNCTION_NAME) ++ ".conf", + Inc = filename:join(["etc", TmpConfName]), ConfFile = emqx_common_test_helpers:app_path(emqx_conf, Etc), - Bin = <<"listeners.tcp.max_connection_test {bind = \"0.0.0.0:3883\"}">>, - ok = file:write_file(ConfFile, Bin, [append]), + IncFile = emqx_common_test_helpers:app_path(emqx_conf, Inc), + Port = integer_to_binary(?PORT), + Bin = <<"listeners.tcp.max_connection_test {bind = \"0.0.0.0:", Port/binary, "\"}">>, + ok = file:write_file(IncFile, Bin), + ok = file:write_file(ConfFile, ["include \"", TmpConfName, "\""], [append]), emqx_mgmt_api_test_util:init_suite([emqx_conf]), + [{tmp_config_file, IncFile} | Config]; +t_max_connection_default({'end', Config}) -> + ok = file:delete(proplists:get_value(tmp_config_file, Config)); +t_max_connection_default(Config) when is_list(Config) -> %% Check infinity is binary not atom. #{<<"listeners">> := Listeners} = emqx_mgmt_api_listeners:do_list_listeners(), Target = lists:filter( @@ -51,7 +78,7 @@ t_max_connection_default(_Config) -> emqx_conf:remove([listeners, tcp, max_connection_test], #{override_to => cluster}), ok. -t_list_listeners(_) -> +t_list_listeners(Config) when is_list(Config) -> Path = emqx_mgmt_api_test_util:api_path(["listeners"]), Res = request(get, Path, [], []), #{<<"listeners">> := Expect} = emqx_mgmt_api_listeners:do_list_listeners(), @@ -71,9 +98,10 @@ t_list_listeners(_) -> ?assertMatch({error, {"HTTP/1.1", 404, _}}, request(get, NewPath, [], [])), OriginListener2 = maps:remove(<<"id">>, OriginListener), + Port = integer_to_binary(?PORT), NewConf = OriginListener2#{ <<"name">> => <<"new">>, - <<"bind">> => <<"0.0.0.0:2883">>, + <<"bind">> => <<"0.0.0.0:", Port/binary>>, <<"max_connections">> := <<"infinity">> }, Create = request(post, Path, [], NewConf), @@ -89,7 +117,7 @@ t_list_listeners(_) -> ?assertMatch({error, {"HTTP/1.1", 404, _}}, request(get, NewPath, [], [])), ok. -t_tcp_crud_listeners_by_id(_) -> +t_tcp_crud_listeners_by_id(Config) when is_list(Config) -> ListenerId = <<"tcp:default">>, NewListenerId = <<"tcp:new">>, MinListenerId = <<"tcp:min">>, @@ -97,7 +125,7 @@ t_tcp_crud_listeners_by_id(_) -> Type = <<"tcp">>, crud_listeners_by_id(ListenerId, NewListenerId, MinListenerId, BadId, Type). -t_ssl_crud_listeners_by_id(_) -> +t_ssl_crud_listeners_by_id(Config) when is_list(Config) -> ListenerId = <<"ssl:default">>, NewListenerId = <<"ssl:new">>, MinListenerId = <<"ssl:min">>, @@ -105,7 +133,7 @@ t_ssl_crud_listeners_by_id(_) -> Type = <<"ssl">>, crud_listeners_by_id(ListenerId, NewListenerId, MinListenerId, BadId, Type). -t_ws_crud_listeners_by_id(_) -> +t_ws_crud_listeners_by_id(Config) when is_list(Config) -> ListenerId = <<"ws:default">>, NewListenerId = <<"ws:new">>, MinListenerId = <<"ws:min">>, @@ -113,7 +141,7 @@ t_ws_crud_listeners_by_id(_) -> Type = <<"ws">>, crud_listeners_by_id(ListenerId, NewListenerId, MinListenerId, BadId, Type). -t_wss_crud_listeners_by_id(_) -> +t_wss_crud_listeners_by_id(Config) when is_list(Config) -> ListenerId = <<"wss:default">>, NewListenerId = <<"wss:new">>, MinListenerId = <<"wss:min">>, @@ -121,7 +149,7 @@ t_wss_crud_listeners_by_id(_) -> Type = <<"wss">>, crud_listeners_by_id(ListenerId, NewListenerId, MinListenerId, BadId, Type). -t_api_listeners_list_not_ready(_Config) -> +t_api_listeners_list_not_ready(Config) when is_list(Config) -> net_kernel:start(['listeners@127.0.0.1', longnames]), ct:timetrap({seconds, 120}), snabbkaffe:fix_ct_logging(), @@ -151,16 +179,17 @@ t_api_listeners_list_not_ready(_Config) -> emqx_common_test_helpers:stop_slave(Node2) end. -t_clear_certs(_) -> +t_clear_certs(Config) when is_list(Config) -> ListenerId = <<"ssl:default">>, NewListenerId = <<"ssl:clear">>, OriginPath = emqx_mgmt_api_test_util:api_path(["listeners", ListenerId]), NewPath = emqx_mgmt_api_test_util:api_path(["listeners", NewListenerId]), ConfTempT = request(get, OriginPath, [], []), + Port = integer_to_binary(?PORT), ConfTemp = ConfTempT#{ <<"id">> => NewListenerId, - <<"bind">> => <<"0.0.0.0:2883">> + <<"bind">> => <<"0.0.0.0:", Port/binary>> }, %% create, make sure the cert files are created @@ -245,9 +274,11 @@ crud_listeners_by_id(ListenerId, NewListenerId, MinListenerId, BadId, Type) -> %% create with full options ?assertEqual({error, not_found}, is_running(NewListenerId)), ?assertMatch({error, {"HTTP/1.1", 404, _}}, request(get, NewPath, [], [])), + Port1 = integer_to_binary(?PORT), + Port2 = integer_to_binary(?PORT), NewConf = OriginListener#{ <<"id">> => NewListenerId, - <<"bind">> => <<"0.0.0.0:2883">> + <<"bind">> => <<"0.0.0.0:", Port1/binary>> }, Create = request(post, NewPath, [], NewConf), ?assertEqual(lists:sort(maps:keys(OriginListener)), lists:sort(maps:keys(Create))), @@ -271,7 +302,7 @@ crud_listeners_by_id(ListenerId, NewListenerId, MinListenerId, BadId, Type) -> } -> #{ <<"id">> => MinListenerId, - <<"bind">> => <<"0.0.0.0:3883">>, + <<"bind">> => <<"0.0.0.0:", Port2/binary>>, <<"type">> => Type, <<"ssl_options">> => #{ <<"cacertfile">> => CaCertFile, @@ -282,7 +313,7 @@ crud_listeners_by_id(ListenerId, NewListenerId, MinListenerId, BadId, Type) -> _ -> #{ <<"id">> => MinListenerId, - <<"bind">> => <<"0.0.0.0:3883">>, + <<"bind">> => <<"0.0.0.0:", Port2/binary>>, <<"type">> => Type } end, @@ -296,7 +327,7 @@ crud_listeners_by_id(ListenerId, NewListenerId, MinListenerId, BadId, Type) -> BadPath = emqx_mgmt_api_test_util:api_path(["listeners", BadId]), BadConf = OriginListener#{ <<"id">> => BadId, - <<"bind">> => <<"0.0.0.0:2883">> + <<"bind">> => <<"0.0.0.0:", Port1/binary>> }, ?assertMatch({error, {"HTTP/1.1", 400, _}}, request(post, BadPath, [], BadConf)), @@ -332,12 +363,12 @@ crud_listeners_by_id(ListenerId, NewListenerId, MinListenerId, BadId, Type) -> ?assertEqual([], delete(NewPath)), ok. -t_delete_nonexistent_listener(_) -> +t_delete_nonexistent_listener(Config) when is_list(Config) -> NonExist = emqx_mgmt_api_test_util:api_path(["listeners", "tcp:nonexistent"]), ?assertEqual([], delete(NonExist)), ok. -t_action_listeners(_) -> +t_action_listeners(Config) when is_list(Config) -> ID = "tcp:default", action_listener(ID, "stop", false), action_listener(ID, "start", true), From 611701406f68d50f31481c35ad2c6482b38e059c Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Thu, 2 Feb 2023 17:24:14 +0100 Subject: [PATCH 0439/1802] chore: upgrade dashboard to v1.1.7 for ce and e1.0.3 for ee --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 6da9231ed..f9be4401a 100644 --- a/Makefile +++ b/Makefile @@ -6,8 +6,8 @@ export EMQX_DEFAULT_BUILDER = ghcr.io/emqx/emqx-builder/5.0-26:1.13.4-24.3.4.2-1 export EMQX_DEFAULT_RUNNER = debian:11-slim export OTP_VSN ?= $(shell $(CURDIR)/scripts/get-otp-vsn.sh) export ELIXIR_VSN ?= $(shell $(CURDIR)/scripts/get-elixir-vsn.sh) -export EMQX_DASHBOARD_VERSION ?= v1.1.6 -export EMQX_EE_DASHBOARD_VERSION ?= e1.0.2 +export EMQX_DASHBOARD_VERSION ?= v1.1.7 +export EMQX_EE_DASHBOARD_VERSION ?= e1.0.3 export EMQX_REL_FORM ?= tgz export QUICER_DOWNLOAD_FROM_RELEASE = 1 ifeq ($(OS),Windows_NT) From 42462db8b8185b65f7ab95b1bbcc03112d32114a Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Thu, 2 Feb 2023 17:26:00 +0100 Subject: [PATCH 0440/1802] chore: bump version to v5.0.16 and e5.0.0 --- apps/emqx/include/emqx_release.hrl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/emqx/include/emqx_release.hrl b/apps/emqx/include/emqx_release.hrl index 72244acaa..4e4066eef 100644 --- a/apps/emqx/include/emqx_release.hrl +++ b/apps/emqx/include/emqx_release.hrl @@ -32,10 +32,10 @@ %% `apps/emqx/src/bpapi/README.md' %% Community edition --define(EMQX_RELEASE_CE, "5.0.15"). +-define(EMQX_RELEASE_CE, "5.0.16"). %% Enterprise edition --define(EMQX_RELEASE_EE, "5.0.0-rc.4"). +-define(EMQX_RELEASE_EE, "5.0.0"). %% the HTTP API version -define(EMQX_API_VERSION, "5.0"). From c5085ab235182c21bb5dc0f61fa7ee6d99318049 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Thu, 2 Feb 2023 17:42:40 +0100 Subject: [PATCH 0441/1802] chore: bump chart version to v5.0.16 --- deploy/charts/emqx/Chart.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deploy/charts/emqx/Chart.yaml b/deploy/charts/emqx/Chart.yaml index fb689839c..2208f3019 100644 --- a/deploy/charts/emqx/Chart.yaml +++ b/deploy/charts/emqx/Chart.yaml @@ -14,8 +14,8 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. -version: 5.0.15 +version: 5.0.16 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. -appVersion: 5.0.15 +appVersion: 5.0.16 From 0c1df8109c1284b10f3a4d60c0e23fb222c0f1cf Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Thu, 2 Feb 2023 17:43:01 +0100 Subject: [PATCH 0442/1802] chore: Generate changelog for v5.0.16 --- changes/v5.0.16-en.md | 18 ++++++++++++++++++ changes/v5.0.16-zh.md | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 changes/v5.0.16-en.md create mode 100644 changes/v5.0.16-zh.md diff --git a/changes/v5.0.16-en.md b/changes/v5.0.16-en.md new file mode 100644 index 000000000..8995d6d10 --- /dev/null +++ b/changes/v5.0.16-en.md @@ -0,0 +1,18 @@ +# v5.0.16 + +## Enhancements + + + +## Bug fixes + +- [#9824](https://github.com/emqx/emqx/pull/9824) The `topics/{topic}` API endpoint would return `500 - Internal Error` if a topic had multiple routes. This is fixed by returning a list of routes. + +- [#9832](https://github.com/emqx/emqx/pull/9832) Improve error log when bridge in 'sync' mode timed out to get response. + +- [#9834](https://github.com/emqx/emqx/pull/9834) Allow `mqtt.idle_timeout` to be set to `infinity` + +- [#9839](https://github.com/emqx/emqx/pull/9839) Make sure that the content of an Authorization header that users have specified for a webhook bridge is not printed to log files. + +- [#9884](https://github.com/emqx/emqx/pull/9884) Do not resume all buffer workers on successful health check of any individual resource. + Previously after any successful healthcheck, all buffer workers (for all resources) were resumed diff --git a/changes/v5.0.16-zh.md b/changes/v5.0.16-zh.md new file mode 100644 index 000000000..e8912efbb --- /dev/null +++ b/changes/v5.0.16-zh.md @@ -0,0 +1,18 @@ +# v5.0.16 + +## 增强 + + + +## 修复 + +- [#9824](https://github.com/emqx/emqx/pull/9824) 修复:当存在多个路由信息时,topics/{topic} 将会返回 500 - Internal Error 的问题,现在将会正确的返回路由信息列表。 + +- [#9832](https://github.com/emqx/emqx/pull/9832) 优化桥接同步资源调用超时情况下的一个错误日志。 + +- [#9834](https://github.com/emqx/emqx/pull/9834) 允许配置项 `mqtt.idle_timeout` 设置成 `infinity` + +- [#9839](https://github.com/emqx/emqx/pull/9839) 确保用户为webhook-bridge指定的Authorization-HTTP-header的内容不会被打印到日志文件。 + +- [#9884](https://github.com/emqx/emqx/pull/9884) 不在任意一个资源健康检查成功时恢复所有资源发送缓存。 + 在此修复之前,在任意一个资源成功进行健康检查后,所有资源的缓存都会尝试恢复。 From f3d433dee5209b2abda9d6a13ef1f65335feffaf Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Thu, 2 Feb 2023 20:29:04 +0100 Subject: [PATCH 0443/1802] docs: move changes missed v5.0.16 release to v5.0.17 --- changes/{v5.0.16 => v5.0.17}/feat-9802.en.md | 0 changes/{v5.0.16 => v5.0.17}/feat-9802.zh.md | 0 changes/{v5.0.16 => v5.0.17}/feat-9871.en.md | 0 changes/{v5.0.16 => v5.0.17}/feat-9871.zh.md | 0 changes/{v5.0.16 => v5.0.17}/fix-9864.en.md | 0 changes/{v5.0.16 => v5.0.17}/fix-9864.zh.md | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename changes/{v5.0.16 => v5.0.17}/feat-9802.en.md (100%) rename changes/{v5.0.16 => v5.0.17}/feat-9802.zh.md (100%) rename changes/{v5.0.16 => v5.0.17}/feat-9871.en.md (100%) rename changes/{v5.0.16 => v5.0.17}/feat-9871.zh.md (100%) rename changes/{v5.0.16 => v5.0.17}/fix-9864.en.md (100%) rename changes/{v5.0.16 => v5.0.17}/fix-9864.zh.md (100%) diff --git a/changes/v5.0.16/feat-9802.en.md b/changes/v5.0.17/feat-9802.en.md similarity index 100% rename from changes/v5.0.16/feat-9802.en.md rename to changes/v5.0.17/feat-9802.en.md diff --git a/changes/v5.0.16/feat-9802.zh.md b/changes/v5.0.17/feat-9802.zh.md similarity index 100% rename from changes/v5.0.16/feat-9802.zh.md rename to changes/v5.0.17/feat-9802.zh.md diff --git a/changes/v5.0.16/feat-9871.en.md b/changes/v5.0.17/feat-9871.en.md similarity index 100% rename from changes/v5.0.16/feat-9871.en.md rename to changes/v5.0.17/feat-9871.en.md diff --git a/changes/v5.0.16/feat-9871.zh.md b/changes/v5.0.17/feat-9871.zh.md similarity index 100% rename from changes/v5.0.16/feat-9871.zh.md rename to changes/v5.0.17/feat-9871.zh.md diff --git a/changes/v5.0.16/fix-9864.en.md b/changes/v5.0.17/fix-9864.en.md similarity index 100% rename from changes/v5.0.16/fix-9864.en.md rename to changes/v5.0.17/fix-9864.en.md diff --git a/changes/v5.0.16/fix-9864.zh.md b/changes/v5.0.17/fix-9864.zh.md similarity index 100% rename from changes/v5.0.16/fix-9864.zh.md rename to changes/v5.0.17/fix-9864.zh.md From c44a836d24d2662b862ff7a0309969d622e632a2 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Thu, 2 Feb 2023 20:30:16 +0100 Subject: [PATCH 0444/1802] docs: delete merged changelog files --- changes/v5.0.15/feat-9569.en.md | 1 - changes/v5.0.15/feat-9569.zh.md | 1 - changes/v5.0.15/feat-9585.en.md | 1 - changes/v5.0.15/feat-9585.zh.md | 1 - changes/v5.0.15/feat-9586.en.md | 1 - changes/v5.0.15/feat-9586.zh.md | 1 - changes/v5.0.15/feat-9628.en.md | 1 - changes/v5.0.15/feat-9628.zh.md | 1 - changes/v5.0.15/feat-9722.en.md | 3 --- changes/v5.0.15/feat-9722.zh.md | 3 --- changes/v5.0.15/feat-9725.en.md | 11 ----------- changes/v5.0.15/feat-9725.zh.md | 8 -------- changes/v5.0.15/feat-9736.en.md | 5 ----- changes/v5.0.15/feat-9736.zh.md | 5 ----- changes/v5.0.15/feat-9774.en.md | 3 --- changes/v5.0.15/feat-9774.zh.md | 2 -- changes/v5.0.15/fix-9626.en.md | 2 -- changes/v5.0.15/fix-9626.zh.md | 3 --- changes/v5.0.15/fix-9680.en.md | 1 - changes/v5.0.15/fix-9680.zh.md | 1 - changes/v5.0.15/fix-9726.en.md | 1 - changes/v5.0.15/fix-9726.zh.md | 1 - changes/v5.0.15/fix-9735.en.md | 1 - changes/v5.0.15/fix-9735.zh.md | 1 - changes/v5.0.15/fix-9748.en.md | 1 - changes/v5.0.15/fix-9748.zh.md | 1 - changes/v5.0.15/fix-9749.en.md | 1 - changes/v5.0.15/fix-9749.zh.md | 1 - changes/v5.0.15/fix-9750.en.md | 5 ----- changes/v5.0.15/fix-9750.zh.md | 4 ---- changes/v5.0.15/fix-9751.en.md | 1 - changes/v5.0.15/fix-9751.zh.md | 1 - changes/v5.0.15/fix-9763.en.md | 1 - changes/v5.0.15/fix-9763.zh.md | 1 - changes/v5.0.15/fix-9765.en.md | 6 ------ changes/v5.0.15/fix-9765.zh.md | 7 ------- changes/v5.0.15/fix-9769.en.md | 1 - changes/v5.0.15/fix-9769.zh.md | 1 - changes/v5.0.15/fix-9780.en.md | 1 - changes/v5.0.15/fix-9780.zh.md | 1 - changes/v5.0.15/fix-9781.en.md | 1 - changes/v5.0.15/fix-9781.zh.md | 1 - changes/v5.0.15/fix-9785.en.md | 1 - changes/v5.0.15/fix-9785.zh.md | 1 - changes/v5.0.15/fix-9787.en.md | 1 - changes/v5.0.15/fix-9787.zh.md | 1 - changes/v5.0.16/fix-9824.en.md | 1 - changes/v5.0.16/fix-9824.zh.md | 1 - changes/v5.0.16/fix-9832.en.md | 1 - changes/v5.0.16/fix-9832.zh.md | 1 - changes/v5.0.16/fix-9834.en.md | 1 - changes/v5.0.16/fix-9834.zh.md | 1 - changes/v5.0.16/fix-9839.en.md | 1 - changes/v5.0.16/fix-9839.zh.md | 1 - changes/v5.0.16/fix-9884.en.md | 2 -- changes/v5.0.16/fix-9884.zh.md | 2 -- 56 files changed, 111 deletions(-) delete mode 100644 changes/v5.0.15/feat-9569.en.md delete mode 100644 changes/v5.0.15/feat-9569.zh.md delete mode 100644 changes/v5.0.15/feat-9585.en.md delete mode 100644 changes/v5.0.15/feat-9585.zh.md delete mode 100644 changes/v5.0.15/feat-9586.en.md delete mode 100644 changes/v5.0.15/feat-9586.zh.md delete mode 100644 changes/v5.0.15/feat-9628.en.md delete mode 100644 changes/v5.0.15/feat-9628.zh.md delete mode 100644 changes/v5.0.15/feat-9722.en.md delete mode 100644 changes/v5.0.15/feat-9722.zh.md delete mode 100644 changes/v5.0.15/feat-9725.en.md delete mode 100644 changes/v5.0.15/feat-9725.zh.md delete mode 100644 changes/v5.0.15/feat-9736.en.md delete mode 100644 changes/v5.0.15/feat-9736.zh.md delete mode 100644 changes/v5.0.15/feat-9774.en.md delete mode 100644 changes/v5.0.15/feat-9774.zh.md delete mode 100644 changes/v5.0.15/fix-9626.en.md delete mode 100644 changes/v5.0.15/fix-9626.zh.md delete mode 100644 changes/v5.0.15/fix-9680.en.md delete mode 100644 changes/v5.0.15/fix-9680.zh.md delete mode 100644 changes/v5.0.15/fix-9726.en.md delete mode 100644 changes/v5.0.15/fix-9726.zh.md delete mode 100644 changes/v5.0.15/fix-9735.en.md delete mode 100644 changes/v5.0.15/fix-9735.zh.md delete mode 100644 changes/v5.0.15/fix-9748.en.md delete mode 100644 changes/v5.0.15/fix-9748.zh.md delete mode 100644 changes/v5.0.15/fix-9749.en.md delete mode 100644 changes/v5.0.15/fix-9749.zh.md delete mode 100644 changes/v5.0.15/fix-9750.en.md delete mode 100644 changes/v5.0.15/fix-9750.zh.md delete mode 100644 changes/v5.0.15/fix-9751.en.md delete mode 100644 changes/v5.0.15/fix-9751.zh.md delete mode 100644 changes/v5.0.15/fix-9763.en.md delete mode 100644 changes/v5.0.15/fix-9763.zh.md delete mode 100644 changes/v5.0.15/fix-9765.en.md delete mode 100644 changes/v5.0.15/fix-9765.zh.md delete mode 100644 changes/v5.0.15/fix-9769.en.md delete mode 100644 changes/v5.0.15/fix-9769.zh.md delete mode 100644 changes/v5.0.15/fix-9780.en.md delete mode 100644 changes/v5.0.15/fix-9780.zh.md delete mode 100644 changes/v5.0.15/fix-9781.en.md delete mode 100644 changes/v5.0.15/fix-9781.zh.md delete mode 100644 changes/v5.0.15/fix-9785.en.md delete mode 100644 changes/v5.0.15/fix-9785.zh.md delete mode 100644 changes/v5.0.15/fix-9787.en.md delete mode 100644 changes/v5.0.15/fix-9787.zh.md delete mode 100644 changes/v5.0.16/fix-9824.en.md delete mode 100644 changes/v5.0.16/fix-9824.zh.md delete mode 100644 changes/v5.0.16/fix-9832.en.md delete mode 100644 changes/v5.0.16/fix-9832.zh.md delete mode 100644 changes/v5.0.16/fix-9834.en.md delete mode 100644 changes/v5.0.16/fix-9834.zh.md delete mode 100644 changes/v5.0.16/fix-9839.en.md delete mode 100644 changes/v5.0.16/fix-9839.zh.md delete mode 100644 changes/v5.0.16/fix-9884.en.md delete mode 100644 changes/v5.0.16/fix-9884.zh.md diff --git a/changes/v5.0.15/feat-9569.en.md b/changes/v5.0.15/feat-9569.en.md deleted file mode 100644 index f3b70ec41..000000000 --- a/changes/v5.0.15/feat-9569.en.md +++ /dev/null @@ -1 +0,0 @@ -Refactor `/authorization/sources/built_in_database/` by adding `rules/` to the path. diff --git a/changes/v5.0.15/feat-9569.zh.md b/changes/v5.0.15/feat-9569.zh.md deleted file mode 100644 index dd2e19c11..000000000 --- a/changes/v5.0.15/feat-9569.zh.md +++ /dev/null @@ -1 +0,0 @@ -重构 `/authorization/sources/built_in_database/` 接口,将 `rules/` 添加到了其路径中。 diff --git a/changes/v5.0.15/feat-9585.en.md b/changes/v5.0.15/feat-9585.en.md deleted file mode 100644 index 986cbb0c3..000000000 --- a/changes/v5.0.15/feat-9585.en.md +++ /dev/null @@ -1 +0,0 @@ -`/bridges_probe` API endpoint to test params for creating a new data bridge. diff --git a/changes/v5.0.15/feat-9585.zh.md b/changes/v5.0.15/feat-9585.zh.md deleted file mode 100644 index 82dd307ae..000000000 --- a/changes/v5.0.15/feat-9585.zh.md +++ /dev/null @@ -1 +0,0 @@ -添加新 API 接口 `/bridges_probe` 用于测试创建桥接的参数是否可用。 diff --git a/changes/v5.0.15/feat-9586.en.md b/changes/v5.0.15/feat-9586.en.md deleted file mode 100644 index 777fb81df..000000000 --- a/changes/v5.0.15/feat-9586.en.md +++ /dev/null @@ -1 +0,0 @@ -Basic auth is no longer allowed for API calls, must use API key instead. diff --git a/changes/v5.0.15/feat-9586.zh.md b/changes/v5.0.15/feat-9586.zh.md deleted file mode 100644 index 102266a46..000000000 --- a/changes/v5.0.15/feat-9586.zh.md +++ /dev/null @@ -1 +0,0 @@ -API 调用不再支持基于 `username:password` 的 `baisc` 认证, 现在 API 必须通过 API Key 才能进行调用。 diff --git a/changes/v5.0.15/feat-9628.en.md b/changes/v5.0.15/feat-9628.en.md deleted file mode 100644 index 6f814dd21..000000000 --- a/changes/v5.0.15/feat-9628.en.md +++ /dev/null @@ -1 +0,0 @@ -Expose additional resource configuration parameters: `start_after_created` and `start_timeout`. diff --git a/changes/v5.0.15/feat-9628.zh.md b/changes/v5.0.15/feat-9628.zh.md deleted file mode 100644 index fee14181b..000000000 --- a/changes/v5.0.15/feat-9628.zh.md +++ /dev/null @@ -1 +0,0 @@ -为桥接资源增加了配置参数:`start_after_created` 和 `start_timeout`。 diff --git a/changes/v5.0.15/feat-9722.en.md b/changes/v5.0.15/feat-9722.en.md deleted file mode 100644 index b86f37b83..000000000 --- a/changes/v5.0.15/feat-9722.en.md +++ /dev/null @@ -1,3 +0,0 @@ -Add the following configuration options for Pushing metrics to Prometheus Push Gateway: -- `headers`: Allows custom HTTP request headers. -- `job_name`: allows to customize the name of the Job pushed to Push Gateway. diff --git a/changes/v5.0.15/feat-9722.zh.md b/changes/v5.0.15/feat-9722.zh.md deleted file mode 100644 index a806cb1de..000000000 --- a/changes/v5.0.15/feat-9722.zh.md +++ /dev/null @@ -1,3 +0,0 @@ -为 Prometheus 推送到 Push Gateway 新增以下配置项: -- `headers`:允许自定义 HTTP 请求头。 -- `job_name`:允许自定义推送到 Push Gateway 的 Job 名称。 diff --git a/changes/v5.0.15/feat-9725.en.md b/changes/v5.0.15/feat-9725.en.md deleted file mode 100644 index 832aa6bf9..000000000 --- a/changes/v5.0.15/feat-9725.en.md +++ /dev/null @@ -1,11 +0,0 @@ -Remove the config `auto_reconnect` from the emqx_authz, emqx_authn and data-bridge componets. -This is because we have another config with similar functions: `resource_opts.auto_restart_interval`。 - -The functions of these two config are difficult to distinguish, which will lead to confusion. -After this change, `auto_reconnect` will not be configurable (always be true), and the underlying -drivers that support this config will automatically reconnect the abnormally disconnected -connection every `2s`. - -And the config `resource_opts.auto_restart_interval` is still available for user. -It is the time interval that emqx restarts the resource when the connection cannot be -established for some reason. diff --git a/changes/v5.0.15/feat-9725.zh.md b/changes/v5.0.15/feat-9725.zh.md deleted file mode 100644 index e7a2412d4..000000000 --- a/changes/v5.0.15/feat-9725.zh.md +++ /dev/null @@ -1,8 +0,0 @@ -从认证、鉴权和数据桥接功能中,删除 `auto_reconnect` 配置项,因为我们还有另一个功能类似的配置项: -`resource_opts.auto_restart_interval`。 - -这两个配置项的功能难以区分,会导致困惑。此修改之后,`auto_reconnect` 将不可配置(永远为 true), -支持此配置的底层驱动将以 `2s` 为周期自动重连异常断开的连接。 - -而 `resource_opts.auto_restart_interval` 配置项仍然开放给用户配置,它是资源因为某些原因 -无法建立连接的时候,emqx 重新启动该资源的时间间隔。 diff --git a/changes/v5.0.15/feat-9736.en.md b/changes/v5.0.15/feat-9736.en.md deleted file mode 100644 index 59d7bd558..000000000 --- a/changes/v5.0.15/feat-9736.en.md +++ /dev/null @@ -1,5 +0,0 @@ -Refactor of /bridges API to make it more consistent with other APIs: -- bridge enable/disable is now done via the endpoint `/bridges/{id}/enable/[true,false]` -- `/bridges/{id}/operation/{operation}` endpoints are now `/bridges/{id}/{operation}` -- metrics are moved out from the GET `/bridges/{id}` response and can now be fetched via `/bridges/{id}/metrics` -- the `bridges/{id}/reset_metrics` endpoint is now `/bridges/{id}/metrics/reset` diff --git a/changes/v5.0.15/feat-9736.zh.md b/changes/v5.0.15/feat-9736.zh.md deleted file mode 100644 index 0107c8ab6..000000000 --- a/changes/v5.0.15/feat-9736.zh.md +++ /dev/null @@ -1,5 +0,0 @@ -重构部分 /bridges 的API 使得其和其他 API 能够更加一致: -- 桥接的启用和禁用现在是通过 `/bridges/{id}/enable/[true,false]` API 来实现的 -- 使用 `/bridges/{id}/{operation}` 替换了旧的 `/bridges/{id}/operation/{operation}` API -- 指标数据从 `/bridges/{id}` 的响应消息中移除,现在可以使用新的 API `/bridges/{id}/metrics` 进行访问 -- 使用 `/bridges/{id}/metrics/reset` 替换了旧的 `bridges/{id}/reset_metrics` API diff --git a/changes/v5.0.15/feat-9774.en.md b/changes/v5.0.15/feat-9774.en.md deleted file mode 100644 index 722c4db6b..000000000 --- a/changes/v5.0.15/feat-9774.en.md +++ /dev/null @@ -1,3 +0,0 @@ -Add a password complexity requirement when adding or modifying Dashboard users via the API. -Now password must contain at least 2 of alphabetic, numeric and special characters, -and must be 8 to 64 characters long. diff --git a/changes/v5.0.15/feat-9774.zh.md b/changes/v5.0.15/feat-9774.zh.md deleted file mode 100644 index 21bfddfaf..000000000 --- a/changes/v5.0.15/feat-9774.zh.md +++ /dev/null @@ -1,2 +0,0 @@ -通过 API 添加、修改 Dashboard 用户时,增加对密码复杂度的要求。 -现在密码必须包含字母、数字以及特殊字符中的至少 2 种,并且长度范围必须是 8~64 个字符。 diff --git a/changes/v5.0.15/fix-9626.en.md b/changes/v5.0.15/fix-9626.en.md deleted file mode 100644 index cc1c86d3e..000000000 --- a/changes/v5.0.15/fix-9626.en.md +++ /dev/null @@ -1,2 +0,0 @@ -Return authorization settings with default values. -The authorization cache is enabled by default, but due to the missing default value in `GET` response of `/authorization/settings`, it seemed to be disabled from the dashboard. diff --git a/changes/v5.0.15/fix-9626.zh.md b/changes/v5.0.15/fix-9626.zh.md deleted file mode 100644 index bc2391f48..000000000 --- a/changes/v5.0.15/fix-9626.zh.md +++ /dev/null @@ -1,3 +0,0 @@ -为授权设置 API 返回默认值。 -授权缓存默认为开启,但是在此修复前,因为默认值在 `/authorization/settings` 这个 API 的返回值中缺失, -使得在仪表盘配置页面中看起来是关闭了。 diff --git a/changes/v5.0.15/fix-9680.en.md b/changes/v5.0.15/fix-9680.en.md deleted file mode 100644 index 2ee3caaa5..000000000 --- a/changes/v5.0.15/fix-9680.en.md +++ /dev/null @@ -1 +0,0 @@ -Fix the problem that username and password authentication is mandatory in Influxdb v1 write API. diff --git a/changes/v5.0.15/fix-9680.zh.md b/changes/v5.0.15/fix-9680.zh.md deleted file mode 100644 index bd1ace306..000000000 --- a/changes/v5.0.15/fix-9680.zh.md +++ /dev/null @@ -1 +0,0 @@ -修复 InfluxDB v1 桥接写入 API 配置中强制需要用户名密码认证的问题。 diff --git a/changes/v5.0.15/fix-9726.en.md b/changes/v5.0.15/fix-9726.en.md deleted file mode 100644 index 9aa522690..000000000 --- a/changes/v5.0.15/fix-9726.en.md +++ /dev/null @@ -1 +0,0 @@ -Client fuzzy search API results were missing information which could tell if more results are available in the next pages, this is now fixed by providing `hasnext` flag in the response. diff --git a/changes/v5.0.15/fix-9726.zh.md b/changes/v5.0.15/fix-9726.zh.md deleted file mode 100644 index 3554d2db7..000000000 --- a/changes/v5.0.15/fix-9726.zh.md +++ /dev/null @@ -1 +0,0 @@ -在此修复前,客户端模糊搜索 API 缺少一些可以用于判断是否可以继续翻页的信息,现在通过在响应中提供 `hasnext` 标志来解决这个问题。 diff --git a/changes/v5.0.15/fix-9735.en.md b/changes/v5.0.15/fix-9735.en.md deleted file mode 100644 index 6085adecd..000000000 --- a/changes/v5.0.15/fix-9735.en.md +++ /dev/null @@ -1 +0,0 @@ -Password information has been removed from information log messages for http, ldap, mongo, mqtt, mysql, pgsql and redis. diff --git a/changes/v5.0.15/fix-9735.zh.md b/changes/v5.0.15/fix-9735.zh.md deleted file mode 100644 index d8aa81fd1..000000000 --- a/changes/v5.0.15/fix-9735.zh.md +++ /dev/null @@ -1 +0,0 @@ -密码信息已从http、ldap、mongo、mqtt、mysql、pgsql和redis的信息日志消息中删除。 diff --git a/changes/v5.0.15/fix-9748.en.md b/changes/v5.0.15/fix-9748.en.md deleted file mode 100644 index 85f5896b2..000000000 --- a/changes/v5.0.15/fix-9748.en.md +++ /dev/null @@ -1 +0,0 @@ -Listeners not configured with `max_connections` will cause the cluster `/listeners` API to return 500 error. diff --git a/changes/v5.0.15/fix-9748.zh.md b/changes/v5.0.15/fix-9748.zh.md deleted file mode 100644 index cab352e79..000000000 --- a/changes/v5.0.15/fix-9748.zh.md +++ /dev/null @@ -1 +0,0 @@ -监听器不配置 `max_connections` 时会导致集群 `/listeners` 接口返回 500 错误。 diff --git a/changes/v5.0.15/fix-9749.en.md b/changes/v5.0.15/fix-9749.en.md deleted file mode 100644 index f079385ce..000000000 --- a/changes/v5.0.15/fix-9749.en.md +++ /dev/null @@ -1 +0,0 @@ -In some cases search APIs could respond with an incorrect `count` value in the metadata, that is usually much bigger than expected, this is now fixed. diff --git a/changes/v5.0.15/fix-9749.zh.md b/changes/v5.0.15/fix-9749.zh.md deleted file mode 100644 index 356cf9475..000000000 --- a/changes/v5.0.15/fix-9749.zh.md +++ /dev/null @@ -1 +0,0 @@ -在某些情况下,搜索 API 可能会在元数据中响应不正确的 `count` 值,这通常比预期的要大得多,现在已经修复了。 diff --git a/changes/v5.0.15/fix-9750.en.md b/changes/v5.0.15/fix-9750.en.md deleted file mode 100644 index 98c07dfb6..000000000 --- a/changes/v5.0.15/fix-9750.en.md +++ /dev/null @@ -1,5 +0,0 @@ -Reload overriding configs after boot. -Prior to this change, two configs were allow to change from dashboard, but will not take effect after reboot: - * Logging (such as level) - * Prometheus configs - diff --git a/changes/v5.0.15/fix-9750.zh.md b/changes/v5.0.15/fix-9750.zh.md deleted file mode 100644 index 605d4c225..000000000 --- a/changes/v5.0.15/fix-9750.zh.md +++ /dev/null @@ -1,4 +0,0 @@ -启动后重新加载一些重载配置项。 -在此修复前,下面两个配置项允许从 Dashboard 控制台修改,但是在重启后无法生效: - * 日志 (例如日志级别) - * Prometheus 配置 diff --git a/changes/v5.0.15/fix-9751.en.md b/changes/v5.0.15/fix-9751.en.md deleted file mode 100644 index f45b99129..000000000 --- a/changes/v5.0.15/fix-9751.en.md +++ /dev/null @@ -1 +0,0 @@ -Fix that obsoleted cert file will not be deleted after the listener is updated/deleted diff --git a/changes/v5.0.15/fix-9751.zh.md b/changes/v5.0.15/fix-9751.zh.md deleted file mode 100644 index 3908e5c20..000000000 --- a/changes/v5.0.15/fix-9751.zh.md +++ /dev/null @@ -1 +0,0 @@ -修复在更新或者删除监听器后,过时的证书文件没有被删除的问题。 diff --git a/changes/v5.0.15/fix-9763.en.md b/changes/v5.0.15/fix-9763.en.md deleted file mode 100644 index 8c07a3d5d..000000000 --- a/changes/v5.0.15/fix-9763.en.md +++ /dev/null @@ -1 +0,0 @@ -Fix an authentication exception when password is not provided diff --git a/changes/v5.0.15/fix-9763.zh.md b/changes/v5.0.15/fix-9763.zh.md deleted file mode 100644 index 8548a363e..000000000 --- a/changes/v5.0.15/fix-9763.zh.md +++ /dev/null @@ -1 +0,0 @@ -修复客户端没有提供密码时的一个异常 diff --git a/changes/v5.0.15/fix-9765.en.md b/changes/v5.0.15/fix-9765.en.md deleted file mode 100644 index 7de7e55f3..000000000 --- a/changes/v5.0.15/fix-9765.en.md +++ /dev/null @@ -1,6 +0,0 @@ -Parse decimals as password from environment variable overrides correctly. -Prior to this change, config values for passwords are not allowed to be decimals. -e.g. `EMQX_FOOBAR__PASSWORD=12344` or `emqx.foobar.password=1234` -would result in a type check error, unless quoted as: -`EMQX_FOOBAR__PASSWORD='"12344"'` or `emqx.foobar.password="1234"`. -After this fix, the value does not have to be quoted. diff --git a/changes/v5.0.15/fix-9765.zh.md b/changes/v5.0.15/fix-9765.zh.md deleted file mode 100644 index dd0b6a79c..000000000 --- a/changes/v5.0.15/fix-9765.zh.md +++ /dev/null @@ -1,7 +0,0 @@ -允许使用纯数字作为密码配置。 -在此修复前,密码的配置必须是字符串,使用纯数字时,会报类型检查错误。 -例如,`EMQX_FOOBAR__PASSWORD=12344` 或 `emqx.foobar.password=1234` 会出错, -必须用引把值括起来才行: -`EMQX_FOOBAR__PASSWORD='"12344"'` 或 `emqx.foobar.password="1234"`。 -修复后可以不使用引号。在环境变量重载中使用更加方便。 - diff --git a/changes/v5.0.15/fix-9769.en.md b/changes/v5.0.15/fix-9769.en.md deleted file mode 100644 index e07397363..000000000 --- a/changes/v5.0.15/fix-9769.en.md +++ /dev/null @@ -1 +0,0 @@ -Fix Erlang shell prompt version prefix. e5.0.15 -> v5.0.15 diff --git a/changes/v5.0.15/fix-9769.zh.md b/changes/v5.0.15/fix-9769.zh.md deleted file mode 100644 index c7e63b862..000000000 --- a/changes/v5.0.15/fix-9769.zh.md +++ /dev/null @@ -1 +0,0 @@ -修复 Eralng 控制台版本号前缀的打印错误 e5.0.15 -> v5.0.15 diff --git a/changes/v5.0.15/fix-9780.en.md b/changes/v5.0.15/fix-9780.en.md deleted file mode 100644 index cf777e6dc..000000000 --- a/changes/v5.0.15/fix-9780.en.md +++ /dev/null @@ -1 +0,0 @@ -When creating disk queue directory for resource worker, substitute ':' with '-' in worker id. diff --git a/changes/v5.0.15/fix-9780.zh.md b/changes/v5.0.15/fix-9780.zh.md deleted file mode 100644 index bc5079e1d..000000000 --- a/changes/v5.0.15/fix-9780.zh.md +++ /dev/null @@ -1 +0,0 @@ -在为资源缓存进程创建磁盘队列目录时,在ID中用 '-' 代替 ':'。 diff --git a/changes/v5.0.15/fix-9781.en.md b/changes/v5.0.15/fix-9781.en.md deleted file mode 100644 index 2b34ddc24..000000000 --- a/changes/v5.0.15/fix-9781.en.md +++ /dev/null @@ -1 +0,0 @@ -Trace files were left on a node when creating a zip file for download. They are now removed when the file is sent. Also, concurrent downloads will no longer interfere with each other. diff --git a/changes/v5.0.15/fix-9781.zh.md b/changes/v5.0.15/fix-9781.zh.md deleted file mode 100644 index 5c4cee0f5..000000000 --- a/changes/v5.0.15/fix-9781.zh.md +++ /dev/null @@ -1 +0,0 @@ -当下载 日志追踪 的日志时,一些中间文件将存留在处理节点上,现在这个问题得到了修复。同时,并发下载日志将不再相互干扰。 diff --git a/changes/v5.0.15/fix-9785.en.md b/changes/v5.0.15/fix-9785.en.md deleted file mode 100644 index 8af14b1ed..000000000 --- a/changes/v5.0.15/fix-9785.en.md +++ /dev/null @@ -1 +0,0 @@ -Stop authentication hook chain if `emqx_authentication` provides a definitive result. diff --git a/changes/v5.0.15/fix-9785.zh.md b/changes/v5.0.15/fix-9785.zh.md deleted file mode 100644 index 6aa84f755..000000000 --- a/changes/v5.0.15/fix-9785.zh.md +++ /dev/null @@ -1 +0,0 @@ -如果 `emqx_authentication` 提供了确定的结果,则停止认证钩子链。 diff --git a/changes/v5.0.15/fix-9787.en.md b/changes/v5.0.15/fix-9787.en.md deleted file mode 100644 index b41180368..000000000 --- a/changes/v5.0.15/fix-9787.en.md +++ /dev/null @@ -1 +0,0 @@ -Fix a compatible problem for the `webhook` bridge configuration which was created before the v5.0.12. diff --git a/changes/v5.0.15/fix-9787.zh.md b/changes/v5.0.15/fix-9787.zh.md deleted file mode 100644 index a9d758de0..000000000 --- a/changes/v5.0.15/fix-9787.zh.md +++ /dev/null @@ -1 +0,0 @@ -修复对在 v5.0.12 之前创建的 `webhook` 桥接配置的兼容问题。 diff --git a/changes/v5.0.16/fix-9824.en.md b/changes/v5.0.16/fix-9824.en.md deleted file mode 100644 index 29aa93264..000000000 --- a/changes/v5.0.16/fix-9824.en.md +++ /dev/null @@ -1 +0,0 @@ -The `topics/{topic}` API endpoint would return `500 - Internal Error` if a topic had multiple routes. This is fixed by returning a list of routes. diff --git a/changes/v5.0.16/fix-9824.zh.md b/changes/v5.0.16/fix-9824.zh.md deleted file mode 100644 index 143a39c16..000000000 --- a/changes/v5.0.16/fix-9824.zh.md +++ /dev/null @@ -1 +0,0 @@ -修复:当存在多个路由信息时,topics/{topic} 将会返回 500 - Internal Error 的问题,现在将会正确的返回路由信息列表。 diff --git a/changes/v5.0.16/fix-9832.en.md b/changes/v5.0.16/fix-9832.en.md deleted file mode 100644 index 84178b63c..000000000 --- a/changes/v5.0.16/fix-9832.en.md +++ /dev/null @@ -1 +0,0 @@ -Improve error log when bridge in 'sync' mode timed out to get response. diff --git a/changes/v5.0.16/fix-9832.zh.md b/changes/v5.0.16/fix-9832.zh.md deleted file mode 100644 index e7fd33b6b..000000000 --- a/changes/v5.0.16/fix-9832.zh.md +++ /dev/null @@ -1 +0,0 @@ -优化桥接同步资源调用超时情况下的一个错误日志。 diff --git a/changes/v5.0.16/fix-9834.en.md b/changes/v5.0.16/fix-9834.en.md deleted file mode 100644 index d5ad1f67a..000000000 --- a/changes/v5.0.16/fix-9834.en.md +++ /dev/null @@ -1 +0,0 @@ -Allow `mqtt.idle_timeout` to be set to `infinity` diff --git a/changes/v5.0.16/fix-9834.zh.md b/changes/v5.0.16/fix-9834.zh.md deleted file mode 100644 index 06eafc1a0..000000000 --- a/changes/v5.0.16/fix-9834.zh.md +++ /dev/null @@ -1 +0,0 @@ -允许配置项 `mqtt.idle_timeout` 设置成 `infinity` diff --git a/changes/v5.0.16/fix-9839.en.md b/changes/v5.0.16/fix-9839.en.md deleted file mode 100644 index 9962b6338..000000000 --- a/changes/v5.0.16/fix-9839.en.md +++ /dev/null @@ -1 +0,0 @@ -Make sure that the content of an Authorization header that users have specified for a webhook bridge is not printed to log files. diff --git a/changes/v5.0.16/fix-9839.zh.md b/changes/v5.0.16/fix-9839.zh.md deleted file mode 100644 index d9e1e0ad8..000000000 --- a/changes/v5.0.16/fix-9839.zh.md +++ /dev/null @@ -1 +0,0 @@ -确保用户为webhook-bridge指定的Authorization-HTTP-header的内容不会被打印到日志文件。 diff --git a/changes/v5.0.16/fix-9884.en.md b/changes/v5.0.16/fix-9884.en.md deleted file mode 100644 index 28eacfc86..000000000 --- a/changes/v5.0.16/fix-9884.en.md +++ /dev/null @@ -1,2 +0,0 @@ -Do not resume all buffer workers on successful health check of any individual resource. -Previously after any successful healthcheck, all buffer workers (for all resources) were resumed diff --git a/changes/v5.0.16/fix-9884.zh.md b/changes/v5.0.16/fix-9884.zh.md deleted file mode 100644 index 08f6e7188..000000000 --- a/changes/v5.0.16/fix-9884.zh.md +++ /dev/null @@ -1,2 +0,0 @@ -不在任意一个资源健康检查成功时恢复所有资源发送缓存。 -在此修复之前,在任意一个资源成功进行健康检查后,所有资源的缓存都会尝试恢复。 From 1e0fe270bca1b5727266889256075606481d50da Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Thu, 2 Feb 2023 20:32:09 +0100 Subject: [PATCH 0445/1802] chore: bump emqx app vsn to 5.0.17 --- apps/emqx/src/emqx.app.src | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx/src/emqx.app.src b/apps/emqx/src/emqx.app.src index 270d36a5e..c812b2217 100644 --- a/apps/emqx/src/emqx.app.src +++ b/apps/emqx/src/emqx.app.src @@ -3,7 +3,7 @@ {id, "emqx"}, {description, "EMQX Core"}, % strict semver, bump manually! - {vsn, "5.0.16"}, + {vsn, "5.0.17"}, {modules, []}, {registered, []}, {applications, [ From 90f23ffc7880369c536c58ee3e18cf65fefacb87 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Thu, 2 Feb 2023 21:43:15 +0100 Subject: [PATCH 0446/1802] ci: no relup support for now --- scripts/relup-build/download-base-packages.sh | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/scripts/relup-build/download-base-packages.sh b/scripts/relup-build/download-base-packages.sh index 1a03f7ef8..fc4511b58 100755 --- a/scripts/relup-build/download-base-packages.sh +++ b/scripts/relup-build/download-base-packages.sh @@ -14,8 +14,10 @@ export PROFILE case $PROFILE in "emqx-enterprise") - DIR='emqx-ee' - EDITION='enterprise' + #S3DIR='emqx-ee' + #EDITION='enterprise' + echo "No relup for now" + exit 0 ;; "emqx") echo "No relup for opensource edition" @@ -51,7 +53,7 @@ mkdir -p _upgrade_base pushd _upgrade_base >/dev/null for tag in ${BASE_VERSIONS}; do filename="$PROFILE-$(fullvsn "${tag#[e|v]}").tar.gz" - url="https://packages.emqx.io/$DIR/$tag/$filename" + url="https://packages.emqx.io/$S3DIR/$tag/$filename" echo "downloading ${filename} ..." ## if the file does not exist (not downloaded yet) ## and there is such a package to downlaod From 636247650854c41645d238f0964f70eccaa64328 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Fri, 3 Feb 2023 09:00:34 +0100 Subject: [PATCH 0447/1802] feat: disable telemetry for ee by default --- lib-ee/emqx_ee_conf/etc/emqx-enterprise.conf | 1 + scripts/merge-config.escript | 33 ++++++++++---------- 2 files changed, 17 insertions(+), 17 deletions(-) create mode 100644 lib-ee/emqx_ee_conf/etc/emqx-enterprise.conf diff --git a/lib-ee/emqx_ee_conf/etc/emqx-enterprise.conf b/lib-ee/emqx_ee_conf/etc/emqx-enterprise.conf new file mode 100644 index 000000000..8da63dad9 --- /dev/null +++ b/lib-ee/emqx_ee_conf/etc/emqx-enterprise.conf @@ -0,0 +1 @@ +telemetry.enable = false diff --git a/scripts/merge-config.escript b/scripts/merge-config.escript index 1b30dbd1d..d30a0ca68 100755 --- a/scripts/merge-config.escript +++ b/scripts/merge-config.escript @@ -30,7 +30,7 @@ main(_) -> case IsEnterprise of true -> EnterpriseCfgs = get_all_cfgs("lib-ee"), - EnterpriseConf = merge("", EnterpriseCfgs), + EnterpriseConf = merge(<<"">>, EnterpriseCfgs), ok = file:write_file("apps/emqx_conf/etc/emqx-enterprise.conf.all", EnterpriseConf); false -> ok @@ -41,22 +41,21 @@ is_enterprise() -> nomatch =/= string:find(Profile, "enterprise"). merge(BaseConf, Cfgs) -> - lists:foldl( - fun(CfgFile, Acc) -> - case filelib:is_regular(CfgFile) of - true -> - {ok, Bin1} = file:read_file(CfgFile), - case string:trim(Bin1, both) of - <<>> -> Acc; - Bin2 -> [Acc, io_lib:nl(), io_lib:nl(), Bin2] - end; - false -> - Acc - end - end, - BaseConf, - Cfgs - ). + Confs = [BaseConf | lists:map(fun read_conf/1, Cfgs)], + infix(lists:filter(fun(I) -> iolist_size(I) > 0 end, Confs), [io_lib:nl(), io_lib:nl()]). + +read_conf(CfgFile) -> + case filelib:is_regular(CfgFile) of + true -> + {ok, Bin1} = file:read_file(CfgFile), + string:trim(Bin1, both); + false -> + <<>> + end. + +infix([], _With) -> []; +infix([One], _With) -> [One]; +infix([H | T], With) -> [H, With, infix(T, With)]. get_all_cfgs(Root) -> Apps0 = filelib:wildcard("*", Root) -- ["emqx_machine", "emqx_conf"], From c95d201331ca59a4ff361a3b53dfb194da821616 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Fri, 3 Feb 2023 09:40:28 +0100 Subject: [PATCH 0448/1802] ci: no relup for now --- scripts/relup-build/download-base-packages.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/relup-build/download-base-packages.sh b/scripts/relup-build/download-base-packages.sh index 1a03f7ef8..8d89e8ab5 100755 --- a/scripts/relup-build/download-base-packages.sh +++ b/scripts/relup-build/download-base-packages.sh @@ -14,8 +14,10 @@ export PROFILE case $PROFILE in "emqx-enterprise") - DIR='emqx-ee' - EDITION='enterprise' + #DIR='emqx-ee' + #EDITION='enterprise' + echo "No relup for now" + exit 0 ;; "emqx") echo "No relup for opensource edition" From d45a0cb2ee929f48113823bfc0b832f10e094408 Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Fri, 3 Feb 2023 15:45:18 +0800 Subject: [PATCH 0449/1802] chore: remove <<>> when logging username --- apps/emqx_dashboard/src/emqx_dashboard_api.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx_dashboard/src/emqx_dashboard_api.erl b/apps/emqx_dashboard/src/emqx_dashboard_api.erl index a4322c696..cc2a1337d 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_api.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_api.erl @@ -325,7 +325,7 @@ is_self_auth_token(Username, Token) -> end. change_pwd(post, #{bindings := #{username := Username}, body := Params}) -> - LogMeta = #{msg => "Dashboard change password", username => Username}, + LogMeta = #{msg => "Dashboard change password", username => binary_to_list(Username)}, OldPwd = maps:get(<<"old_pwd">>, Params), NewPwd = maps:get(<<"new_pwd">>, Params), case ?EMPTY(OldPwd) orelse ?EMPTY(NewPwd) of From 7233f42889d3786575742e8a404b54edded04632 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Fri, 3 Feb 2023 09:58:02 +0100 Subject: [PATCH 0450/1802] ci: skip relup tests for now --- .github/workflows/run_relup_tests.yaml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/run_relup_tests.yaml b/.github/workflows/run_relup_tests.yaml index a6854aa40..29b4fbc45 100644 --- a/.github/workflows/run_relup_tests.yaml +++ b/.github/workflows/run_relup_tests.yaml @@ -4,13 +4,13 @@ concurrency: group: relup-${{ github.event_name }}-${{ github.ref }} cancel-in-progress: true -on: - push: - branches: - - '**' - tags: - - e* - pull_request: +# on: +# push: +# branches: +# - '**' +# tags: +# - e* +# pull_request: jobs: relup_test_plan: From 15035f7eb074185ac88cbc7d2d9da553c0e60932 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Sun, 8 Jan 2023 21:57:53 +0100 Subject: [PATCH 0451/1802] refactor: remove lazy type for authentication The idea of using lazy type for authentication was to make the config check easy to extend at runtime. However the reality is: it's overly complicated. It's more likely that people will just continue to implementing the auth hook callbacks instead of injecting config schema at runtime. --- apps/emqx/src/emqx_config.erl | 14 ++-------- apps/emqx/src/emqx_schema.erl | 27 +++++++------------ apps/emqx_conf/src/emqx_conf.erl | 2 -- .../src/emqx_dashboard_swagger.erl | 2 -- 4 files changed, 12 insertions(+), 33 deletions(-) diff --git a/apps/emqx/src/emqx_config.erl b/apps/emqx/src/emqx_config.erl index 6d706316c..c462b6725 100644 --- a/apps/emqx/src/emqx_config.erl +++ b/apps/emqx/src/emqx_config.erl @@ -366,13 +366,6 @@ schema_default(Schema) -> case hocon_schema:field_schema(Schema, type) of ?ARRAY(_) -> []; - ?LAZY(?ARRAY(_)) -> - []; - ?LAZY(?UNION(Members)) -> - case [A || ?ARRAY(A) <- hoconsc:union_members(Members)] of - [_ | _] -> []; - _ -> #{} - end; _ -> #{} end. @@ -407,8 +400,7 @@ merge_envs(SchemaMod, RawConf) -> Opts = #{ required => false, format => map, - apply_override_envs => true, - check_lazy => true + apply_override_envs => true }, hocon_tconf:merge_env_overrides(SchemaMod, RawConf, all, Opts). @@ -451,9 +443,7 @@ compact_errors(Schema, Errors) -> do_check_config(SchemaMod, RawConf, Opts0) -> Opts1 = #{ return_plain => true, - format => map, - %% Don't check lazy types, such as authenticate - check_lazy => false + format => map }, Opts = maps:merge(Opts0, Opts1), {AppEnvs, CheckedConf} = diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 48bd206c9..77b51f074 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -2352,25 +2352,18 @@ authentication(Which) -> global -> ?DESC(global_authentication); listener -> ?DESC(listener_authentication) end, - %% The runtime module injection - %% from EMQX_AUTHENTICATION_SCHEMA_MODULE_PT_KEY - %% is for now only affecting document generation. - %% maybe in the future, we can find a more straightforward way to support - %% * document generation (at compile time) - %% * type checks before boot (in bin/emqx config generation) - %% * type checks at runtime (when changing configs via management API) - Type0 = + %% poor man's dependency injection + %% this is due to the fact that authn is implemented outside of 'emqx' app. + %% so it can not be a part of emqx_schema since 'emqx' app is supposed to + %% work standalone. + Type = case persistent_term:get(?EMQX_AUTHENTICATION_SCHEMA_MODULE_PT_KEY, undefined) of - undefined -> hoconsc:array(typerefl:map()); - Module -> Module:root_type() + undefined -> + hoconsc:array(typerefl:map()); + Module -> + Module:root_type() end, - %% It is a lazy type because when handling runtime update requests - %% the config is not checked by emqx_schema, but by the injected schema - Type = hoconsc:lazy(Type0), - #{ - type => Type, - desc => Desc - }. + hoconsc:mk(Type, #{desc => Desc}). -spec qos() -> typerefl:type(). qos() -> diff --git a/apps/emqx_conf/src/emqx_conf.erl b/apps/emqx_conf/src/emqx_conf.erl index 8b471a137..00648db31 100644 --- a/apps/emqx_conf/src/emqx_conf.erl +++ b/apps/emqx_conf/src/emqx_conf.erl @@ -296,8 +296,6 @@ hocon_schema_to_spec(Type, LocalModule) when ?IS_TYPEREFL(Type) -> hocon_schema_to_spec(?ARRAY(Item), LocalModule) -> {Schema, Refs} = hocon_schema_to_spec(Item, LocalModule), {#{type => array, items => Schema}, Refs}; -hocon_schema_to_spec(?LAZY(Item), LocalModule) -> - hocon_schema_to_spec(Item, LocalModule); hocon_schema_to_spec(?ENUM(Items), _LocalModule) -> {#{type => enum, symbols => Items}, []}; hocon_schema_to_spec(?MAP(Name, Type), LocalModule) -> diff --git a/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl b/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl index 0afbf5f1e..1d0fa7352 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl @@ -609,8 +609,6 @@ hocon_schema_to_spec(Type, LocalModule) when ?IS_TYPEREFL(Type) -> hocon_schema_to_spec(?ARRAY(Item), LocalModule) -> {Schema, Refs} = hocon_schema_to_spec(Item, LocalModule), {#{type => array, items => Schema}, Refs}; -hocon_schema_to_spec(?LAZY(Item), LocalModule) -> - hocon_schema_to_spec(Item, LocalModule); hocon_schema_to_spec(?ENUM(Items), _LocalModule) -> {#{type => string, enum => Items}, []}; hocon_schema_to_spec(?MAP(Name, Type), LocalModule) -> From c6a78cbfda4ecbbc29e6a58df861b8a5aa159e7d Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Mon, 9 Jan 2023 23:02:32 +0100 Subject: [PATCH 0452/1802] chore: refine a warning message about default erlang cookie --- bin/emqx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/emqx b/bin/emqx index 132d8cba7..0a967a73c 100755 --- a/bin/emqx +++ b/bin/emqx @@ -911,7 +911,7 @@ fi if [ $IS_BOOT_COMMAND = 'yes' ] && [ "$COOKIE" = "$EMQX_DEFAULT_ERLANG_COOKIE" ]; then logwarn "Default (insecure) Erlang cookie is in use." logwarn "Configure node.cookie in $EMQX_ETC_DIR/emqx.conf or override from environment variable EMQX_NODE__COOKIE" - logwarn "Use the same config value for all nodes in the cluster." + logwarn "NOTE: Use the same cookie for all nodes in the cluster." fi ## check if OTP version has mnesia_hook feature; if not, fallback to From 6aaff6211f2f7016c1e73d41850859634b0a468b Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Mon, 9 Jan 2023 09:28:03 +0100 Subject: [PATCH 0453/1802] feat: use union member selector for authn config schema --- apps/emqx_authn/src/emqx_authn_schema.erl | 99 +++++++++++++++++-- .../src/simple_authn/emqx_authn_http.erl | 12 ++- .../src/simple_authn/emqx_authn_jwt.erl | 15 ++- .../src/simple_authn/emqx_authn_mongodb.erl | 10 ++ .../src/simple_authn/emqx_authn_redis.erl | 10 ++ 5 files changed, 134 insertions(+), 12 deletions(-) diff --git a/apps/emqx_authn/src/emqx_authn_schema.erl b/apps/emqx_authn/src/emqx_authn_schema.erl index f40e759f0..9c4c7d89a 100644 --- a/apps/emqx_authn/src/emqx_authn_schema.erl +++ b/apps/emqx_authn/src/emqx_authn_schema.erl @@ -45,24 +45,105 @@ enable(desc) -> ?DESC(?FUNCTION_NAME); enable(_) -> undefined. authenticator_type() -> - hoconsc:union(config_refs([Module || {_AuthnType, Module} <- emqx_authn:providers()])). + hoconsc:union(union_member_selector(emqx_authn:providers())). authenticator_type_without_scram() -> Providers = lists:filtermap( fun - ({{password_based, _Backend}, Mod}) -> - {true, Mod}; - ({jwt, Mod}) -> - {true, Mod}; ({{scram, _Backend}, _Mod}) -> - false + false; + (_) -> + true end, emqx_authn:providers() ), - hoconsc:union(config_refs(Providers)). + hoconsc:union(union_member_selector(Providers)). -config_refs(Modules) -> - lists:append([Module:refs() || Module <- Modules]). +config_refs(Providers) -> + lists:append([Module:refs() || {_, Module} <- Providers]). + +union_member_selector(Providers) -> + Types = config_refs(Providers), + fun + (all_union_members) -> Types; + ({value, Value}) -> select_union_member(Value, Providers) + end. + +select_union_member(#{<<"mechanism">> := _} = Value, Providers) -> + select_union_member(Value, Providers, #{}); +select_union_member(_Value, _) -> + throw(#{hint => "missing 'mechanism' field"}). + +select_union_member(Value, [], ReasonsMap) when ReasonsMap =:= #{} -> + BackendVal = maps:get(<<"backend">>, Value, undefined), + MechanismVal = maps:get(<<"mechanism">>, Value), + throw(#{ + backend => BackendVal, + mechanism => MechanismVal, + hint => "unknown_mechanism_or_backend" + }); +select_union_member(_Value, [], ReasonsMap) -> + throw(ReasonsMap); +select_union_member(Value, [Provider | Providers], ReasonsMap) -> + {Mechanism, Backend, Module} = + case Provider of + {{M, B}, Mod} -> {atom_to_binary(M), atom_to_binary(B), Mod}; + {M, Mod} when is_atom(M) -> {atom_to_binary(M), undefined, Mod} + end, + case do_select_union_member(Mechanism, Backend, Module, Value) of + {ok, Type} -> + [Type]; + {error, nomatch} -> + %% obvious mismatch, do not complain + %% e.g. when 'backend' is "http", but the value is "redis", + %% then there is no need to record the error like + %% "'http' is exepcted but got 'redis'" + select_union_member(Value, Providers, ReasonsMap); + {error, Reason} -> + %% more interesting error message + %% e.g. when 'backend' is "http", but there is no "method" field + %% found so there is no way to tell if it's the 'get' type or 'post' type. + %% hence the error message is like: + %% #{emqx_auth_http => "'http' auth backend must have get|post as 'method'"} + select_union_member(Value, Providers, ReasonsMap#{Module => Reason}) + end. + +do_select_union_member(Mechanism, Backend, Module, Value) -> + BackendVal = maps:get(<<"backend">>, Value, undefined), + MechanismVal = maps:get(<<"mechanism">>, Value), + case MechanismVal =:= Mechanism of + true when Backend =:= undefined -> + case BackendVal =:= undefined of + true -> + %% e.g. jwt has no 'backend' + try_select_union_member(Module, Value); + false -> + {error, "unexpected 'backend' for " ++ binary_to_list(Mechanism)} + end; + true -> + case Backend =:= BackendVal of + true -> + try_select_union_member(Module, Value); + false -> + %% 'backend' not matching + {error, nomatch} + end; + false -> + %% 'mechanism' not matching + {error, nomatch} + end. + +try_select_union_member(Module, Value) -> + try + %% some modules have refs/1 exported to help selectin the sub-types + %% emqx_authn_http, emqx_authn_jwt, emqx_authn_mongodb and emqx_authn_redis + Module:refs(Value) + catch + error:undef -> + %% otherwise expect only one member from this module + [Type] = Module:refs(), + {ok, Type} + end. %% authn is a core functionality however implemented outside of emqx app %% in emqx_schema, 'authentication' is a map() type which is to allow diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl index b6b68eab9..ecd084c08 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl @@ -40,6 +40,7 @@ -export([ refs/0, + refs/1, create/2, update/2, authenticate/2, @@ -66,12 +67,12 @@ roots() -> fields(get) -> [ - {method, #{type => get, required => true, default => get, desc => ?DESC(method)}}, + {method, #{type => get, required => true, desc => ?DESC(method)}}, {headers, fun headers_no_content_type/1} ] ++ common_fields(); fields(post) -> [ - {method, #{type => post, required => true, default => post, desc => ?DESC(method)}}, + {method, #{type => post, required => true, desc => ?DESC(method)}}, {headers, fun headers/1} ] ++ common_fields(). @@ -159,6 +160,13 @@ refs() -> hoconsc:ref(?MODULE, post) ]. +refs(#{<<"method">> := <<"get">>}) -> + {ok, hoconsc:ref(?MODULE, get)}; +refs(#{<<"method">> := <<"post">>}) -> + {ok, hoconsc:ref(?MODULE, post)}; +refs(_) -> + {error, "'http' auth backend must have get|post as 'method'"}. + create(_AuthenticatorID, Config) -> create(Config). diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl index 852139875..f8a278ee5 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl @@ -21,7 +21,6 @@ -include_lib("hocon/include/hoconsc.hrl"). -behaviour(hocon_schema). --behaviour(emqx_authentication). -export([ namespace/0, @@ -33,6 +32,7 @@ -export([ refs/0, + refs/1, create/2, update/2, authenticate/2, @@ -169,6 +169,19 @@ refs() -> hoconsc:ref(?MODULE, 'jwks') ]. +refs(#{<<"mechanism">> := <<"jwt">>} = V) -> + UseJWKS = maps:get(<<"use_jwks">>, V, undefined), + select_ref(UseJWKS, V). + +select_ref(true, _) -> + {ok, hoconsc:ref(?MODULE, 'jwks')}; +select_ref(false, #{<<"public_key">> := _}) -> + {ok, hoconsc:ref(?MODULE, 'public-key')}; +select_ref(false, _) -> + {ok, hoconsc:ref(?MODULE, 'hmac-based')}; +select_ref(_, _) -> + {error, "use_jwks must be set"}. + create(_AuthenticatorID, Config) -> create(Config). diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl index bcdc7b0b1..3820005af 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl @@ -33,6 +33,7 @@ -export([ refs/0, + refs/1, create/2, update/2, authenticate/2, @@ -130,6 +131,15 @@ refs() -> hoconsc:ref(?MODULE, 'sharded-cluster') ]. +refs(#{<<"mongo_type">> := <<"single">>}) -> + {ok, hoconsc:ref(?MODULE, standalone)}; +refs(#{<<"mongo_type">> := <<"rs">>}) -> + {ok, hoconsc:ref(?MODULE, 'replica-set')}; +refs(#{<<"mongo_type">> := <<"sharded">>}) -> + {ok, hoconsc:ref(?MODULE, 'sharded-cluster')}; +refs(_) -> + {error, "unknown 'mongo_type'"}. + create(_AuthenticatorID, Config) -> create(Config). diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl index 8c9cb5333..cafcf1830 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl @@ -33,6 +33,7 @@ -export([ refs/0, + refs/1, create/2, update/2, authenticate/2, @@ -97,6 +98,15 @@ refs() -> hoconsc:ref(?MODULE, sentinel) ]. +refs(#{<<"redis_type">> := <<"single">>}) -> + {ok, hoconsc:ref(?MODULE, standalone)}; +refs(#{<<"redis_type">> := <<"cluster">>}) -> + {ok, hoconsc:ref(?MODULE, cluster)}; +refs(#{<<"redis_type">> := <<"sentinel">>}) -> + {ok, hoconsc:ref(?MODULE, sentinel)}; +refs(_) -> + {error, "unknown 'redis_type'"}. + create(_AuthenticatorID, Config) -> create(Config). From fbc52330f3c42af47f73489ab702cd7909acb4e3 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Wed, 11 Jan 2023 15:20:39 +0100 Subject: [PATCH 0454/1802] refactor: do not re-check config in post_config_update now that the type is not lazy, it should have already been checked when reaching the post_config_update step --- apps/emqx/src/emqx_authentication_config.erl | 52 +------------------- apps/emqx_authn/src/emqx_authn_app.erl | 6 ++- 2 files changed, 5 insertions(+), 53 deletions(-) diff --git a/apps/emqx/src/emqx_authentication_config.erl b/apps/emqx/src/emqx_authentication_config.erl index 300fb4a66..5471b66fc 100644 --- a/apps/emqx/src/emqx_authentication_config.erl +++ b/apps/emqx/src/emqx_authentication_config.erl @@ -136,7 +136,7 @@ do_pre_config_update({move_authenticator, _ChainName, AuthenticatorID, Position} ) -> ok | {ok, map()} | {error, term()}. post_config_update(_, UpdateReq, NewConfig, OldConfig, AppEnvs) -> - do_post_config_update(UpdateReq, check_configs(to_list(NewConfig)), OldConfig, AppEnvs). + do_post_config_update(UpdateReq, to_list(NewConfig), OldConfig, AppEnvs). do_post_config_update({create_authenticator, ChainName, Config}, NewConfig, _OldConfig, _AppEnvs) -> NConfig = get_authenticator_config(authenticator_id(Config), NewConfig), @@ -175,56 +175,6 @@ do_post_config_update( ) -> emqx_authentication:move_authenticator(ChainName, AuthenticatorID, Position). -check_configs(Configs) -> - Providers = emqx_authentication:get_providers(), - lists:map(fun(C) -> do_check_config(C, Providers) end, Configs). - -do_check_config(Config, Providers) -> - Type = authn_type(Config), - case maps:get(Type, Providers, false) of - false -> - ?SLOG(warning, #{ - msg => "unknown_authn_type", - type => Type, - providers => Providers - }), - throw({unknown_authn_type, Type}); - Module -> - do_check_config(Type, Config, Module) - end. - -do_check_config(Type, Config, Module) -> - F = - case erlang:function_exported(Module, check_config, 1) of - true -> - fun Module:check_config/1; - false -> - fun(C) -> - Key = list_to_binary(?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME), - AtomKey = list_to_atom(?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME), - R = hocon_tconf:check_plain( - Module, - #{Key => C}, - #{atom_key => true} - ), - maps:get(AtomKey, R) - end - end, - try - F(Config) - catch - C:E:S -> - ?SLOG(warning, #{ - msg => "failed_to_check_config", - config => Config, - type => Type, - exception => C, - reason => E, - stacktrace => S - }), - throw({bad_authenticator_config, #{type => Type, reason => E}}) - end. - to_list(undefined) -> []; to_list(M) when M =:= #{} -> []; to_list(M) when is_map(M) -> [M]; diff --git a/apps/emqx_authn/src/emqx_authn_app.erl b/apps/emqx_authn/src/emqx_authn_app.erl index 99a141822..ed97c0975 100644 --- a/apps/emqx_authn/src/emqx_authn_app.erl +++ b/apps/emqx_authn/src/emqx_authn_app.erl @@ -35,6 +35,9 @@ %%------------------------------------------------------------------------------ start(_StartType, _StartArgs) -> + %% required by test cases, ensure the injection of + %% EMQX_AUTHENTICATION_SCHEMA_MODULE_PT_KEY + _ = emqx_conf_schema:roots(), ok = mria_rlog:wait_for_shards([?AUTH_SHARD], infinity), {ok, Sup} = emqx_authn_sup:start_link(), case initialize() of @@ -43,8 +46,7 @@ start(_StartType, _StartArgs) -> end. stop(_State) -> - ok = deinitialize(), - ok. + ok = deinitialize(). %%------------------------------------------------------------------------------ %% Internal functions From 65319567cc242701d294425c37589192c2995cdb Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Wed, 11 Jan 2023 20:00:28 +0100 Subject: [PATCH 0455/1802] chore: no type check for 3rd party auth --- apps/emqx/src/emqx_authentication.erl | 5 +- apps/emqx/test/emqx_authentication_SUITE.erl | 54 +++----------------- 2 files changed, 10 insertions(+), 49 deletions(-) diff --git a/apps/emqx/src/emqx_authentication.erl b/apps/emqx/src/emqx_authentication.erl index ec8a1c113..fe93bed68 100644 --- a/apps/emqx/src/emqx_authentication.erl +++ b/apps/emqx/src/emqx_authentication.erl @@ -759,9 +759,10 @@ maybe_unhook(State) -> State. do_create_authenticator(AuthenticatorID, #{enable := Enable} = Config, Providers) -> - case maps:get(authn_type(Config), Providers, undefined) of + Type = authn_type(Config), + case maps:get(Type, Providers, undefined) of undefined -> - {error, no_available_provider}; + {error, {no_available_provider_for, Type}}; Provider -> case Provider:create(AuthenticatorID, Config) of {ok, State} -> diff --git a/apps/emqx/test/emqx_authentication_SUITE.erl b/apps/emqx/test/emqx_authentication_SUITE.erl index 77789dedd..2c83162ed 100644 --- a/apps/emqx/test/emqx_authentication_SUITE.erl +++ b/apps/emqx/test/emqx_authentication_SUITE.erl @@ -52,50 +52,10 @@ ) ). -%%------------------------------------------------------------------------------ -%% Hocon Schema -%%------------------------------------------------------------------------------ - -roots() -> - [ - {config, #{ - type => hoconsc:union([ - hoconsc:ref(?MODULE, type1), - hoconsc:ref(?MODULE, type2) - ]) - }} - ]. - -fields(type1) -> - [ - {mechanism, {enum, [password_based]}}, - {backend, {enum, [built_in_database]}}, - {enable, fun enable/1} - ]; -fields(type2) -> - [ - {mechanism, {enum, [password_based]}}, - {backend, {enum, [mysql]}}, - {enable, fun enable/1} - ]. - -enable(type) -> boolean(); -enable(default) -> true; -enable(_) -> undefined. - %%------------------------------------------------------------------------------ %% Callbacks %%------------------------------------------------------------------------------ -check_config(C) -> - #{config := R} = - hocon_tconf:check_plain( - ?MODULE, - #{<<"config">> => C}, - #{atom_key => true} - ), - R. - create(_AuthenticatorID, _Config) -> {ok, #{mark => 1}}. @@ -200,7 +160,7 @@ t_authenticator(Config) when is_list(Config) -> % Create an authenticator when the provider does not exist ?assertEqual( - {error, no_available_provider}, + {error, {no_available_provider_for, {password_based, built_in_database}}}, ?AUTHN:create_authenticator(ChainName, AuthenticatorConfig1) ), @@ -335,14 +295,14 @@ t_update_config(Config) when is_list(Config) -> ok = register_provider(?config("auth2"), ?MODULE), Global = ?config(global), AuthenticatorConfig1 = #{ - <<"mechanism">> => <<"password_based">>, - <<"backend">> => <<"built_in_database">>, - <<"enable">> => true + mechanism => password_based, + backend => built_in_database, + enable => true }, AuthenticatorConfig2 = #{ - <<"mechanism">> => <<"password_based">>, - <<"backend">> => <<"mysql">>, - <<"enable">> => true + mechanism => password_based, + backend => mysql, + enable => true }, ID1 = <<"password_based:built_in_database">>, ID2 = <<"password_based:mysql">>, From 4f91bf415c68688e4b4f8359b1e7fcd1e8014309 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Thu, 12 Jan 2023 12:58:28 +0100 Subject: [PATCH 0456/1802] fix: schema check error reason pattern --- apps/emqx/src/emqx_schema.erl | 7 +++- apps/emqx_authn/src/emqx_authn_api.erl | 7 +--- .../src/emqx_authn_password_hashing.erl | 34 +++++++++++++------ .../src/simple_authn/emqx_authn_jwt.erl | 11 ++++-- .../test/emqx_authn_mnesia_SUITE.erl | 32 +++++++++++++---- .../test/emqx_authn_pgsql_SUITE.erl | 17 ++++------ .../test/emqx_authn_redis_SUITE.erl | 10 +++--- 7 files changed, 78 insertions(+), 40 deletions(-) diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 77b51f074..fa4b068a1 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -2363,7 +2363,12 @@ authentication(Which) -> Module -> Module:root_type() end, - hoconsc:mk(Type, #{desc => Desc}). + hoconsc:mk(Type, #{desc => Desc, converter => fun ensure_array/1}). + +%% the older version schema allows individual element (instead of a chain) in config +ensure_array(undefined) -> undefined; +ensure_array(L) when is_list(L) -> L; +ensure_array(M) when is_map(M) -> [M]. -spec qos() -> typerefl:type(). qos() -> diff --git a/apps/emqx_authn/src/emqx_authn_api.erl b/apps/emqx_authn/src/emqx_authn_api.erl index 452a7bb90..7e1c613d3 100644 --- a/apps/emqx_authn/src/emqx_authn_api.erl +++ b/apps/emqx_authn/src/emqx_authn_api.erl @@ -1232,15 +1232,10 @@ serialize_error({unknown_authn_type, Type}) -> code => <<"BAD_REQUEST">>, message => binfmt("Unknown type '~p'", [Type]) }}; -serialize_error({bad_authenticator_config, Reason}) -> - {400, #{ - code => <<"BAD_REQUEST">>, - message => binfmt("Bad authenticator config ~p", [Reason]) - }}; serialize_error(Reason) -> {400, #{ code => <<"BAD_REQUEST">>, - message => binfmt("~p", [Reason]) + message => binfmt("~0p", [Reason]) }}. parse_position(<<"front">>) -> diff --git a/apps/emqx_authn/src/emqx_authn_password_hashing.erl b/apps/emqx_authn/src/emqx_authn_password_hashing.erl index b3e90e2cd..4954cd66e 100644 --- a/apps/emqx_authn/src/emqx_authn_password_hashing.erl +++ b/apps/emqx_authn/src/emqx_authn_password_hashing.erl @@ -64,7 +64,7 @@ ]). namespace() -> "authn-hash". -roots() -> [pbkdf2, bcrypt, bcrypt_rw, other_algorithms]. +roots() -> [pbkdf2, bcrypt, bcrypt_rw, simple]. fields(bcrypt_rw) -> fields(bcrypt) ++ @@ -96,7 +96,7 @@ fields(pbkdf2) -> )}, {dk_length, fun dk_length/1} ]; -fields(other_algorithms) -> +fields(simple) -> [ {name, sc( @@ -112,8 +112,8 @@ desc(bcrypt) -> "Settings for bcrypt password hashing algorithm."; desc(pbkdf2) -> "Settings for PBKDF2 password hashing algorithm."; -desc(other_algorithms) -> - "Settings for other password hashing algorithms."; +desc(simple) -> + "Settings for simple algorithms."; desc(_) -> undefined. @@ -231,17 +231,31 @@ check_password(#{name := Other, salt_position := SaltPosition}, Salt, PasswordHa %%------------------------------------------------------------------------------ rw_refs() -> - [ + All = [ hoconsc:ref(?MODULE, bcrypt_rw), hoconsc:ref(?MODULE, pbkdf2), - hoconsc:ref(?MODULE, other_algorithms) - ]. + hoconsc:ref(?MODULE, simple) + ], + fun + (all_union_members) -> All; + ({value, #{<<"name">> := <<"bcrypt">>}}) -> [hoconsc:ref(?MODULE, bcrypt_rw)]; + ({value, #{<<"name">> := <<"pbkdf2">>}}) -> [hoconsc:ref(?MODULE, pbkdf2)]; + ({value, #{<<"name">> := _}}) -> [hoconsc:ref(?MODULE, simple)]; + ({value, _}) -> throw(#{reason => "algorithm_name_missing"}) + end. ro_refs() -> - [ + All = [ hoconsc:ref(?MODULE, bcrypt), hoconsc:ref(?MODULE, pbkdf2), - hoconsc:ref(?MODULE, other_algorithms) - ]. + hoconsc:ref(?MODULE, simple) + ], + fun + (all_union_members) -> All; + ({value, #{<<"name">> := <<"bcrypt">>}}) -> [hoconsc:ref(?MODULE, bcrypt)]; + ({value, #{<<"name">> := <<"pbkdf2">>}}) -> [hoconsc:ref(?MODULE, pbkdf2)]; + ({value, #{<<"name">> := _}}) -> [hoconsc:ref(?MODULE, simple)]; + ({value, _}) -> throw(#{reason => "algorithm_name_missing"}) + end. sc(Type, Meta) -> hoconsc:mk(Type, Meta). diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl index f8a278ee5..e32786b04 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl @@ -171,7 +171,14 @@ refs() -> refs(#{<<"mechanism">> := <<"jwt">>} = V) -> UseJWKS = maps:get(<<"use_jwks">>, V, undefined), - select_ref(UseJWKS, V). + select_ref(boolean(UseJWKS), V). + +%% this field is technically a boolean type, +%% but union member selection is done before type casting (by typrefl), +%% so we have to allow strings too +boolean(<<"true">>) -> true; +boolean(<<"false">>) -> false; +boolean(Other) -> Other. select_ref(true, _) -> {ok, hoconsc:ref(?MODULE, 'jwks')}; @@ -180,7 +187,7 @@ select_ref(false, #{<<"public_key">> := _}) -> select_ref(false, _) -> {ok, hoconsc:ref(?MODULE, 'hmac-based')}; select_ref(_, _) -> - {error, "use_jwks must be set"}. + {error, "use_jwks must be set to true or false"}. create(_AuthenticatorID, Config) -> create(Config). diff --git a/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl b/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl index cd97a15d9..740e7387a 100644 --- a/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl @@ -52,6 +52,7 @@ end_per_testcase(_Case, Config) -> -define(CONF(Conf), #{?CONF_NS_BINARY => Conf}). t_check_schema(_Config) -> + Check = fun(C) -> emqx_config:check_config(emqx_schema, ?CONF(C)) end, ConfigOk = #{ <<"mechanism">> => <<"password_based">>, <<"backend">> => <<"built_in_database">>, @@ -61,8 +62,7 @@ t_check_schema(_Config) -> <<"salt_rounds">> => <<"6">> } }, - - hocon_tconf:check_plain(emqx_authn_mnesia, ?CONF(ConfigOk)), + _ = Check(ConfigOk), ConfigNotOk = #{ <<"mechanism">> => <<"password_based">>, @@ -72,11 +72,31 @@ t_check_schema(_Config) -> <<"name">> => <<"md6">> } }, + ?assertThrow( + #{ + path := "authentication.1.password_hash_algorithm.name", + matched_type := "authn-builtin_db:authentication/authn-hash:simple", + reason := unable_to_convert_to_enum_symbol + }, + Check(ConfigNotOk) + ), - ?assertException( - throw, - {emqx_authn_mnesia, _}, - hocon_tconf:check_plain(emqx_authn_mnesia, ?CONF(ConfigNotOk)) + ConfigMissingAlgoName = #{ + <<"mechanism">> => <<"password_based">>, + <<"backend">> => <<"built_in_database">>, + <<"user_id_type">> => <<"username">>, + <<"password_hash_algorithm">> => #{ + <<"foo">> => <<"bar">> + } + }, + + ?assertThrow( + #{ + path := "authentication.1.password_hash_algorithm", + reason := "algorithm_name_missing", + matched_type := "authn-builtin_db:authentication" + }, + Check(ConfigMissingAlgoName) ). t_create(_) -> diff --git a/apps/emqx_authn/test/emqx_authn_pgsql_SUITE.erl b/apps/emqx_authn/test/emqx_authn_pgsql_SUITE.erl index fa0658f6a..65c783298 100644 --- a/apps/emqx_authn/test/emqx_authn_pgsql_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_pgsql_SUITE.erl @@ -105,17 +105,12 @@ t_update_with_invalid_config(_Config) -> AuthConfig = raw_pgsql_auth_config(), BadConfig = maps:without([<<"server">>], AuthConfig), ?assertMatch( - {error, - {bad_authenticator_config, #{ - reason := - {emqx_authn_pgsql, [ - #{ - kind := validation_error, - path := "authentication.server", - reason := required_field - } - ]} - }}}, + {error, #{ + kind := validation_error, + matched_type := "authn-postgresql:authentication", + path := "authentication.1.server", + reason := required_field + }}, emqx:update_config( ?PATH, {create_authenticator, ?GLOBAL, BadConfig} diff --git a/apps/emqx_authn/test/emqx_authn_redis_SUITE.erl b/apps/emqx_authn/test/emqx_authn_redis_SUITE.erl index 4f25c8709..4f95bef93 100644 --- a/apps/emqx_authn/test/emqx_authn_redis_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_redis_SUITE.erl @@ -160,10 +160,12 @@ t_create_invalid_config(_Config) -> Config0 = raw_redis_auth_config(), Config = maps:without([<<"server">>], Config0), ?assertMatch( - {error, - {bad_authenticator_config, #{ - reason := {emqx_authn_redis, [#{kind := validation_error}]} - }}}, + {error, #{ + kind := validation_error, + matched_type := "authn-redis:standalone", + path := "authentication.1.server", + reason := required_field + }}, emqx:update_config(?PATH, {create_authenticator, ?GLOBAL, Config}) ), ?assertMatch([], emqx_config:get_raw([authentication])), From 2ebc89e339d6c4294d62553ebe9f1f499964fdb1 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Fri, 13 Jan 2023 14:05:57 +0100 Subject: [PATCH 0457/1802] refactor: authn schema union selector --- apps/emqx/src/emqx_schema.erl | 8 +- apps/emqx_authn/src/emqx_authn_schema.erl | 107 ++++------ .../src/simple_authn/emqx_authn_http.erl | 5 +- .../src/simple_authn/emqx_authn_jwt.erl | 5 +- .../src/simple_authn/emqx_authn_mongodb.erl | 5 +- .../src/simple_authn/emqx_authn_redis.erl | 5 +- .../test/emqx_authn_mnesia_SUITE.erl | 50 ----- .../test/emqx_authn_schema_SUITE.erl | 190 ++++++++++++++++++ 8 files changed, 250 insertions(+), 125 deletions(-) create mode 100644 apps/emqx_authn/test/emqx_authn_schema_SUITE.erl diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index fa4b068a1..ba813d2c8 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -2363,12 +2363,12 @@ authentication(Which) -> Module -> Module:root_type() end, - hoconsc:mk(Type, #{desc => Desc, converter => fun ensure_array/1}). + hoconsc:mk(Type, #{desc => Desc, converter => fun ensure_array/2}). %% the older version schema allows individual element (instead of a chain) in config -ensure_array(undefined) -> undefined; -ensure_array(L) when is_list(L) -> L; -ensure_array(M) when is_map(M) -> [M]. +ensure_array(undefined, _) -> undefined; +ensure_array(L, _) when is_list(L) -> L; +ensure_array(M, _) -> [M]. -spec qos() -> typerefl:type(). qos() -> diff --git a/apps/emqx_authn/src/emqx_authn_schema.erl b/apps/emqx_authn/src/emqx_authn_schema.erl index 9c4c7d89a..ce90aebe9 100644 --- a/apps/emqx_authn/src/emqx_authn_schema.erl +++ b/apps/emqx_authn/src/emqx_authn_schema.erl @@ -69,80 +69,53 @@ union_member_selector(Providers) -> ({value, Value}) -> select_union_member(Value, Providers) end. -select_union_member(#{<<"mechanism">> := _} = Value, Providers) -> - select_union_member(Value, Providers, #{}); -select_union_member(_Value, _) -> - throw(#{hint => "missing 'mechanism' field"}). - -select_union_member(Value, [], ReasonsMap) when ReasonsMap =:= #{} -> +select_union_member(#{<<"mechanism">> := _} = Value, Providers0) -> BackendVal = maps:get(<<"backend">>, Value, undefined), MechanismVal = maps:get(<<"mechanism">>, Value), - throw(#{ - backend => BackendVal, - mechanism => MechanismVal, - hint => "unknown_mechanism_or_backend" - }); -select_union_member(_Value, [], ReasonsMap) -> - throw(ReasonsMap); -select_union_member(Value, [Provider | Providers], ReasonsMap) -> - {Mechanism, Backend, Module} = - case Provider of - {{M, B}, Mod} -> {atom_to_binary(M), atom_to_binary(B), Mod}; - {M, Mod} when is_atom(M) -> {atom_to_binary(M), undefined, Mod} - end, - case do_select_union_member(Mechanism, Backend, Module, Value) of - {ok, Type} -> - [Type]; - {error, nomatch} -> - %% obvious mismatch, do not complain - %% e.g. when 'backend' is "http", but the value is "redis", - %% then there is no need to record the error like - %% "'http' is exepcted but got 'redis'" - select_union_member(Value, Providers, ReasonsMap); - {error, Reason} -> - %% more interesting error message - %% e.g. when 'backend' is "http", but there is no "method" field - %% found so there is no way to tell if it's the 'get' type or 'post' type. - %% hence the error message is like: - %% #{emqx_auth_http => "'http' auth backend must have get|post as 'method'"} - select_union_member(Value, Providers, ReasonsMap#{Module => Reason}) - end. - -do_select_union_member(Mechanism, Backend, Module, Value) -> - BackendVal = maps:get(<<"backend">>, Value, undefined), - MechanismVal = maps:get(<<"mechanism">>, Value), - case MechanismVal =:= Mechanism of - true when Backend =:= undefined -> - case BackendVal =:= undefined of - true -> - %% e.g. jwt has no 'backend' - try_select_union_member(Module, Value); - false -> - {error, "unexpected 'backend' for " ++ binary_to_list(Mechanism)} - end; - true -> - case Backend =:= BackendVal of - true -> - try_select_union_member(Module, Value); - false -> - %% 'backend' not matching - {error, nomatch} - end; - false -> - %% 'mechanism' not matching - {error, nomatch} - end. + BackendFilterFn = fun + ({{_Mec, Backend}, _Mod}) -> + BackendVal =:= atom_to_binary(Backend); + (_) -> + BackendVal =:= undefined + end, + MechanismFilterFn = fun + ({{Mechanism, _Backend}, _Mod}) -> + MechanismVal =:= atom_to_binary(Mechanism); + ({Mechanism, _Mod}) -> + MechanismVal =:= atom_to_binary(Mechanism) + end, + case lists:filter(BackendFilterFn, Providers0) of + [] -> + throw(#{reason => "unknown_backend", backend => BackendVal}); + Providers1 -> + case lists:filter(MechanismFilterFn, Providers1) of + [] -> + throw(#{ + reason => "unsupported_mechanism", + mechanism => MechanismVal, + backend => BackendVal + }); + [{_, Module}] -> + try_select_union_member(Module, Value) + end + end; +select_union_member(Value, _Providers) when is_map(Value) -> + throw(#{reason => "missing_mechanism_field"}); +select_union_member(Value, _Providers) -> + throw(#{reason => "not_a_struct", value => Value}). try_select_union_member(Module, Value) -> - try - %% some modules have refs/1 exported to help selectin the sub-types - %% emqx_authn_http, emqx_authn_jwt, emqx_authn_mongodb and emqx_authn_redis - Module:refs(Value) + %% some modules have refs/1 exported to help selectin the sub-types + %% emqx_authn_http, emqx_authn_jwt, emqx_authn_mongodb and emqx_authn_redis + try Module:refs(Value) of + {ok, Type} -> + [Type]; + {error, Reason} -> + throw(Reason) catch error:undef -> %% otherwise expect only one member from this module - [Type] = Module:refs(), - {ok, Type} + Module:refs() end. %% authn is a core functionality however implemented outside of emqx app diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl index ecd084c08..0c0ec3b06 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl @@ -165,7 +165,10 @@ refs(#{<<"method">> := <<"get">>}) -> refs(#{<<"method">> := <<"post">>}) -> {ok, hoconsc:ref(?MODULE, post)}; refs(_) -> - {error, "'http' auth backend must have get|post as 'method'"}. + {error, #{ + field_name => method, + expected => "get | post" + }}. create(_AuthenticatorID, Config) -> create(Config). diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl index e32786b04..d96f9073e 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl @@ -187,7 +187,10 @@ select_ref(false, #{<<"public_key">> := _}) -> select_ref(false, _) -> {ok, hoconsc:ref(?MODULE, 'hmac-based')}; select_ref(_, _) -> - {error, "use_jwks must be set to true or false"}. + {error, #{ + field_name => use_jwks, + expected => "true | false" + }}. create(_AuthenticatorID, Config) -> create(Config). diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl index 3820005af..11048528c 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl @@ -138,7 +138,10 @@ refs(#{<<"mongo_type">> := <<"rs">>}) -> refs(#{<<"mongo_type">> := <<"sharded">>}) -> {ok, hoconsc:ref(?MODULE, 'sharded-cluster')}; refs(_) -> - {error, "unknown 'mongo_type'"}. + {error, #{ + field_name => mongo_type, + expected => "single | rs | sharded" + }}. create(_AuthenticatorID, Config) -> create(Config). diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl index cafcf1830..419899bbd 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl @@ -105,7 +105,10 @@ refs(#{<<"redis_type">> := <<"cluster">>}) -> refs(#{<<"redis_type">> := <<"sentinel">>}) -> {ok, hoconsc:ref(?MODULE, sentinel)}; refs(_) -> - {error, "unknown 'redis_type'"}. + {error, #{ + field_name => redis_type, + expected => "single | cluster | sentinel" + }}. create(_AuthenticatorID, Config) -> create(Config). diff --git a/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl b/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl index 740e7387a..599eae92e 100644 --- a/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl @@ -49,56 +49,6 @@ end_per_testcase(_Case, Config) -> %% Tests %%------------------------------------------------------------------------------ --define(CONF(Conf), #{?CONF_NS_BINARY => Conf}). - -t_check_schema(_Config) -> - Check = fun(C) -> emqx_config:check_config(emqx_schema, ?CONF(C)) end, - ConfigOk = #{ - <<"mechanism">> => <<"password_based">>, - <<"backend">> => <<"built_in_database">>, - <<"user_id_type">> => <<"username">>, - <<"password_hash_algorithm">> => #{ - <<"name">> => <<"bcrypt">>, - <<"salt_rounds">> => <<"6">> - } - }, - _ = Check(ConfigOk), - - ConfigNotOk = #{ - <<"mechanism">> => <<"password_based">>, - <<"backend">> => <<"built_in_database">>, - <<"user_id_type">> => <<"username">>, - <<"password_hash_algorithm">> => #{ - <<"name">> => <<"md6">> - } - }, - ?assertThrow( - #{ - path := "authentication.1.password_hash_algorithm.name", - matched_type := "authn-builtin_db:authentication/authn-hash:simple", - reason := unable_to_convert_to_enum_symbol - }, - Check(ConfigNotOk) - ), - - ConfigMissingAlgoName = #{ - <<"mechanism">> => <<"password_based">>, - <<"backend">> => <<"built_in_database">>, - <<"user_id_type">> => <<"username">>, - <<"password_hash_algorithm">> => #{ - <<"foo">> => <<"bar">> - } - }, - - ?assertThrow( - #{ - path := "authentication.1.password_hash_algorithm", - reason := "algorithm_name_missing", - matched_type := "authn-builtin_db:authentication" - }, - Check(ConfigMissingAlgoName) - ). - t_create(_) -> Config0 = config(), diff --git a/apps/emqx_authn/test/emqx_authn_schema_SUITE.erl b/apps/emqx_authn/test/emqx_authn_schema_SUITE.erl new file mode 100644 index 000000000..7e67584ac --- /dev/null +++ b/apps/emqx_authn/test/emqx_authn_schema_SUITE.erl @@ -0,0 +1,190 @@ +-module(emqx_authn_schema_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("eunit/include/eunit.hrl"). + +-include("emqx_authn.hrl"). + +all() -> + emqx_common_test_helpers:all(?MODULE). + +init_per_suite(Config) -> + _ = application:load(emqx_conf), + emqx_common_test_helpers:start_apps([emqx_authn]), + Config. + +end_per_suite(_) -> + emqx_common_test_helpers:stop_apps([emqx_authn]), + ok. + +init_per_testcase(_Case, Config) -> + {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000), + mria:clear_table(emqx_authn_mnesia), + Config. + +end_per_testcase(_Case, Config) -> + Config. + +-define(CONF(Conf), #{?CONF_NS_BINARY => Conf}). + +t_check_schema(_Config) -> + Check = fun(C) -> emqx_config:check_config(emqx_schema, ?CONF(C)) end, + ConfigOk = #{ + <<"mechanism">> => <<"password_based">>, + <<"backend">> => <<"built_in_database">>, + <<"user_id_type">> => <<"username">>, + <<"password_hash_algorithm">> => #{ + <<"name">> => <<"bcrypt">>, + <<"salt_rounds">> => <<"6">> + } + }, + _ = Check(ConfigOk), + + ConfigNotOk = #{ + <<"mechanism">> => <<"password_based">>, + <<"backend">> => <<"built_in_database">>, + <<"user_id_type">> => <<"username">>, + <<"password_hash_algorithm">> => #{ + <<"name">> => <<"md6">> + } + }, + ?assertThrow( + #{ + path := "authentication.1.password_hash_algorithm.name", + matched_type := "authn-builtin_db:authentication/authn-hash:simple", + reason := unable_to_convert_to_enum_symbol + }, + Check(ConfigNotOk) + ), + + ConfigMissingAlgoName = #{ + <<"mechanism">> => <<"password_based">>, + <<"backend">> => <<"built_in_database">>, + <<"user_id_type">> => <<"username">>, + <<"password_hash_algorithm">> => #{ + <<"foo">> => <<"bar">> + } + }, + + ?assertThrow( + #{ + path := "authentication.1.password_hash_algorithm", + reason := "algorithm_name_missing", + matched_type := "authn-builtin_db:authentication" + }, + Check(ConfigMissingAlgoName) + ). + +t_union_member_selector(_) -> + ?assertMatch(#{authentication := undefined}, check(undefined)), + C1 = #{<<"backend">> => <<"built_in_database">>}, + ?assertThrow( + #{ + path := "authentication.1", + reason := "missing_mechanism_field" + }, + check(C1) + ), + C2 = <<"foobar">>, + ?assertThrow( + #{ + path := "authentication.1", + reason := "not_a_struct", + value := <<"foobar">> + }, + check(C2) + ), + Base = #{ + <<"user_id_type">> => <<"username">>, + <<"password_hash_algorithm">> => #{ + <<"name">> => <<"plain">> + } + }, + BadBackend = Base#{<<"mechanism">> => <<"password_based">>, <<"backend">> => <<"bar">>}, + ?assertThrow( + #{ + reason := "unknown_backend", + backend := <<"bar">> + }, + check(BadBackend) + ), + BadMechanism = Base#{<<"mechanism">> => <<"foo">>, <<"backend">> => <<"built_in_database">>}, + ?assertThrow( + #{ + reason := "unsupported_mechanism", + mechanism := <<"foo">>, + backend := <<"built_in_database">> + }, + check(BadMechanism) + ), + BadCombination = Base#{<<"mechanism">> => <<"scram">>, <<"backend">> => <<"http">>}, + ?assertThrow( + #{ + reason := "unsupported_mechanism", + mechanism := <<"scram">>, + backend := <<"http">> + }, + check(BadCombination) + ), + ok. + +t_http_auth_selector(_) -> + C1 = #{ + <<"mechanism">> => <<"password_based">>, + <<"backend">> => <<"http">> + }, + ?assertThrow( + #{ + field_name := method, + expected := "get | post" + }, + check(C1) + ), + ok. + +t_mongo_auth_selector(_) -> + C1 = #{ + <<"mechanism">> => <<"password_based">>, + <<"backend">> => <<"mongodb">> + }, + ?assertThrow( + #{ + field_name := mongo_type, + expected := "single | rs | sharded" + }, + check(C1) + ), + ok. + +t_redis_auth_selector(_) -> + C1 = #{ + <<"mechanism">> => <<"password_based">>, + <<"backend">> => <<"redis">> + }, + ?assertThrow( + #{ + field_name := redis_type, + expected := "single | cluster | sentinel" + }, + check(C1) + ), + ok. + +t_redis_jwt_selector(_) -> + C1 = #{ + <<"mechanism">> => <<"jwt">> + }, + ?assertThrow( + #{ + field_name := use_jwks, + expected := "true | false" + }, + check(C1) + ), + ok. + +check(C) -> + {_Mappings, Checked} = emqx_config:check_config(emqx_schema, ?CONF(C)), + Checked. From a9c650da66a7f02af34bc8d14a73009c3ba33bae Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Wed, 18 Jan 2023 08:05:40 +0100 Subject: [PATCH 0458/1802] refactor: for better test coverage --- apps/emqx/src/emqx_config.erl | 8 +- apps/emqx/src/emqx_hocon.erl | 36 ++++- apps/emqx_authn/src/emqx_authn.erl | 2 +- apps/emqx_authn/src/emqx_authn_schema.erl | 9 +- .../src/simple_authn/emqx_authn_http.erl | 17 ++- .../src/simple_authn/emqx_authn_jwt.erl | 18 +-- .../src/simple_authn/emqx_authn_mongodb.erl | 33 +++-- .../src/simple_authn/emqx_authn_redis.erl | 19 ++- .../test/emqx_authn_schema_tests.erl | 135 ++++++++++++++++++ 9 files changed, 229 insertions(+), 48 deletions(-) create mode 100644 apps/emqx_authn/test/emqx_authn_schema_tests.erl diff --git a/apps/emqx/src/emqx_config.erl b/apps/emqx/src/emqx_config.erl index c462b6725..fb84d64a1 100644 --- a/apps/emqx/src/emqx_config.erl +++ b/apps/emqx/src/emqx_config.erl @@ -413,8 +413,8 @@ check_config(SchemaMod, RawConf, Opts0) -> try do_check_config(SchemaMod, RawConf, Opts0) catch - throw:{Schema, Errors} -> - compact_errors(Schema, Errors) + throw:Errors:Stacktrace -> + throw(emqx_hocon:compact_errors(Errors, Stacktrace)) end. %% HOCON tries to be very informative about all the detailed errors @@ -425,8 +425,8 @@ compact_errors(Schema, [Error0 | More]) when is_map(Error0) -> case length(More) of 0 -> Error0; - _ -> - Error0#{unshown_errors => length(More)} + N -> + Error0#{unshown_errors => N} end, Error = case is_atom(Schema) of diff --git a/apps/emqx/src/emqx_hocon.erl b/apps/emqx/src/emqx_hocon.erl index 4391a9a0b..a039aa8c2 100644 --- a/apps/emqx/src/emqx_hocon.erl +++ b/apps/emqx/src/emqx_hocon.erl @@ -20,6 +20,7 @@ -export([ format_path/1, check/2, + compact_errors/2, format_error/1, format_error/2, make_schema/1 @@ -43,8 +44,8 @@ check(SchemaModule, Conf) when is_map(Conf) -> try {ok, hocon_tconf:check_plain(SchemaModule, Conf, Opts)} catch - throw:Reason -> - {error, Reason} + throw:Errors:Stacktrace -> + compact_errors(Errors, Stacktrace) end; check(SchemaModule, HoconText) -> case hocon:binary(HoconText, #{format => map}) of @@ -90,3 +91,34 @@ iol(L) when is_list(L) -> L. no_stacktrace(Map) -> maps:without([stacktrace], Map). + +%% @doc HOCON tries to be very informative about all the detailed errors +%% it's maybe too much when reporting to the user +-spec compact_errors(any(), Stacktrace :: list()) -> {error, any()}. +compact_errors({SchemaModule, Errors}, Stacktrace) -> + compact_errors(SchemaModule, Errors, Stacktrace). + +compact_errors(SchemaModule, [Error0 | More], _Stacktrace) when is_map(Error0) -> + Error1 = + case length(More) of + 0 -> + Error0; + N -> + Error0#{unshown_errors_count => N} + end, + Error = + case is_atom(SchemaModule) of + true -> + Error1#{schema_module => SchemaModule}; + false -> + Error1 + end, + {error, Error}; +compact_errors(SchemaModule, Error, Stacktrace) -> + %% unexpected, we need the stacktrace reported, hence error + %% if this happens i'ts a bug in hocon_tconf + {error, #{ + schema_module => SchemaModule, + exception => Error, + stacktrace => Stacktrace + }}. diff --git a/apps/emqx_authn/src/emqx_authn.erl b/apps/emqx_authn/src/emqx_authn.erl index 8c8e2efd9..934c57fb1 100644 --- a/apps/emqx_authn/src/emqx_authn.erl +++ b/apps/emqx_authn/src/emqx_authn.erl @@ -69,7 +69,7 @@ do_check_config(#{<<"mechanism">> := Mec0} = Config, Opts) -> false -> throw(#{error => unknown_authn_provider, which => Key}); {_, ProviderModule} -> - hocon_tconf:check_plain( + emqx_hocon:check( ProviderModule, #{?CONF_NS_BINARY => Config}, Opts#{atom_key => true} diff --git a/apps/emqx_authn/src/emqx_authn_schema.erl b/apps/emqx_authn/src/emqx_authn_schema.erl index ce90aebe9..f4a966d89 100644 --- a/apps/emqx_authn/src/emqx_authn_schema.erl +++ b/apps/emqx_authn/src/emqx_authn_schema.erl @@ -105,13 +105,10 @@ select_union_member(Value, _Providers) -> throw(#{reason => "not_a_struct", value => Value}). try_select_union_member(Module, Value) -> - %% some modules have refs/1 exported to help selectin the sub-types + %% some modules have union_member_selector/1 exported to help selectin the sub-types %% emqx_authn_http, emqx_authn_jwt, emqx_authn_mongodb and emqx_authn_redis - try Module:refs(Value) of - {ok, Type} -> - [Type]; - {error, Reason} -> - throw(Reason) + try + Module:union_member_selector({value, Value}) catch error:undef -> %% otherwise expect only one member from this module diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl index 0c0ec3b06..9be1d0a33 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl @@ -40,7 +40,7 @@ -export([ refs/0, - refs/1, + union_member_selector/1, create/2, update/2, authenticate/2, @@ -60,7 +60,7 @@ roots() -> [ {?CONF_NS, hoconsc:mk( - hoconsc:union(refs()), + hoconsc:union(fun union_member_selector/1), #{} )} ]. @@ -160,15 +160,20 @@ refs() -> hoconsc:ref(?MODULE, post) ]. +union_member_selector(all_union_members) -> + refs(); +union_member_selector({value, Value}) -> + refs(Value). + refs(#{<<"method">> := <<"get">>}) -> - {ok, hoconsc:ref(?MODULE, get)}; + [hoconsc:ref(?MODULE, get)]; refs(#{<<"method">> := <<"post">>}) -> - {ok, hoconsc:ref(?MODULE, post)}; + [hoconsc:ref(?MODULE, post)]; refs(_) -> - {error, #{ + throw(#{ field_name => method, expected => "get | post" - }}. + }). create(_AuthenticatorID, Config) -> create(Config). diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl index d96f9073e..4de64dcfe 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl @@ -32,7 +32,7 @@ -export([ refs/0, - refs/1, + union_member_selector/1, create/2, update/2, authenticate/2, @@ -52,7 +52,7 @@ roots() -> [ {?CONF_NS, hoconsc:mk( - hoconsc:union(refs()), + hoconsc:union(fun union_member_selector/1), #{} )} ]. @@ -169,7 +169,9 @@ refs() -> hoconsc:ref(?MODULE, 'jwks') ]. -refs(#{<<"mechanism">> := <<"jwt">>} = V) -> +union_member_selector(all_union_members) -> + refs(); +union_member_selector({value, V}) -> UseJWKS = maps:get(<<"use_jwks">>, V, undefined), select_ref(boolean(UseJWKS), V). @@ -181,16 +183,16 @@ boolean(<<"false">>) -> false; boolean(Other) -> Other. select_ref(true, _) -> - {ok, hoconsc:ref(?MODULE, 'jwks')}; + [hoconsc:ref(?MODULE, 'jwks')]; select_ref(false, #{<<"public_key">> := _}) -> - {ok, hoconsc:ref(?MODULE, 'public-key')}; + [hoconsc:ref(?MODULE, 'public-key')]; select_ref(false, _) -> - {ok, hoconsc:ref(?MODULE, 'hmac-based')}; + [hoconsc:ref(?MODULE, 'hmac-based')]; select_ref(_, _) -> - {error, #{ + throw(#{ field_name => use_jwks, expected => "true | false" - }}. + }). create(_AuthenticatorID, Config) -> create(Config). diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl index 11048528c..22b930485 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl @@ -33,7 +33,7 @@ -export([ refs/0, - refs/1, + union_member_selector/1, create/2, update/2, authenticate/2, @@ -53,7 +53,7 @@ roots() -> [ {?CONF_NS, hoconsc:mk( - hoconsc:union(refs()), + hoconsc:union(fun union_member_selector/1), #{} )} ]. @@ -131,18 +131,6 @@ refs() -> hoconsc:ref(?MODULE, 'sharded-cluster') ]. -refs(#{<<"mongo_type">> := <<"single">>}) -> - {ok, hoconsc:ref(?MODULE, standalone)}; -refs(#{<<"mongo_type">> := <<"rs">>}) -> - {ok, hoconsc:ref(?MODULE, 'replica-set')}; -refs(#{<<"mongo_type">> := <<"sharded">>}) -> - {ok, hoconsc:ref(?MODULE, 'sharded-cluster')}; -refs(_) -> - {error, #{ - field_name => mongo_type, - expected => "single | rs | sharded" - }}. - create(_AuthenticatorID, Config) -> create(Config). @@ -259,3 +247,20 @@ is_superuser(Doc, #{is_superuser_field := IsSuperuserField}) -> emqx_authn_utils:is_superuser(#{<<"is_superuser">> => IsSuperuser}); is_superuser(_, _) -> emqx_authn_utils:is_superuser(#{<<"is_superuser">> => false}). + +union_member_selector(all_union_members) -> + refs(); +union_member_selector({value, Value}) -> + refs(Value). + +refs(#{<<"mongo_type">> := <<"single">>}) -> + [hoconsc:ref(?MODULE, standalone)]; +refs(#{<<"mongo_type">> := <<"rs">>}) -> + [hoconsc:ref(?MODULE, 'replica-set')]; +refs(#{<<"mongo_type">> := <<"sharded">>}) -> + [hoconsc:ref(?MODULE, 'sharded-cluster')]; +refs(_) -> + throw(#{ + field_name => mongo_type, + expected => "single | rs | sharded" + }). diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl index 419899bbd..ff81fd4ca 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl @@ -33,7 +33,7 @@ -export([ refs/0, - refs/1, + union_member_selector/1, create/2, update/2, authenticate/2, @@ -53,7 +53,7 @@ roots() -> [ {?CONF_NS, hoconsc:mk( - hoconsc:union(refs()), + hoconsc:union(fun union_member_selector/1), #{} )} ]. @@ -98,17 +98,22 @@ refs() -> hoconsc:ref(?MODULE, sentinel) ]. +union_member_selector(all_union_members) -> + refs(); +union_member_selector({value, Value}) -> + refs(Value). + refs(#{<<"redis_type">> := <<"single">>}) -> - {ok, hoconsc:ref(?MODULE, standalone)}; + [hoconsc:ref(?MODULE, standalone)]; refs(#{<<"redis_type">> := <<"cluster">>}) -> - {ok, hoconsc:ref(?MODULE, cluster)}; + [hoconsc:ref(?MODULE, cluster)]; refs(#{<<"redis_type">> := <<"sentinel">>}) -> - {ok, hoconsc:ref(?MODULE, sentinel)}; + [hoconsc:ref(?MODULE, sentinel)]; refs(_) -> - {error, #{ + throw(#{ field_name => redis_type, expected => "single | cluster | sentinel" - }}. + }). create(_AuthenticatorID, Config) -> create(Config). diff --git a/apps/emqx_authn/test/emqx_authn_schema_tests.erl b/apps/emqx_authn/test/emqx_authn_schema_tests.erl new file mode 100644 index 000000000..25fcd28e4 --- /dev/null +++ b/apps/emqx_authn/test/emqx_authn_schema_tests.erl @@ -0,0 +1,135 @@ +%%-------------------------------------------------------------------- +%% 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. +%%-------------------------------------------------------------------- + +-module(emqx_authn_schema_tests). + +-include_lib("eunit/include/eunit.hrl"). + +%% schema error +-define(ERR(Reason), {error, Reason}). + +union_member_selector_mongo_test_() -> + Check = fun(Txt) -> check(emqx_authn_mongodb, Txt) end, + [ + {"unknown", fun() -> + ?assertMatch( + ?ERR(#{field_name := mongo_type, expected := _}), + Check("{mongo_type: foobar}") + ) + end}, + {"single", fun() -> + ?assertMatch( + ?ERR(#{matched_type := "authn-mongodb:standalone"}), + Check("{mongo_type: single}") + ) + end}, + {"replica-set", fun() -> + ?assertMatch( + ?ERR(#{matched_type := "authn-mongodb:replica-set"}), + Check("{mongo_type: rs}") + ) + end}, + {"sharded", fun() -> + ?assertMatch( + ?ERR(#{matched_type := "authn-mongodb:sharded-cluster"}), + Check("{mongo_type: sharded}") + ) + end} + ]. + +union_member_selector_jwt_test_() -> + Check = fun(Txt) -> check(emqx_authn_jwt, Txt) end, + [ + {"unknown", fun() -> + ?assertMatch( + ?ERR(#{field_name := use_jwks, expected := "true | false"}), + Check("{use_jwks = 1}") + ) + end}, + {"jwks", fun() -> + ?assertMatch( + ?ERR(#{matched_type := "authn-jwt:jwks"}), + Check("{use_jwks = true}") + ) + end}, + {"publick-key", fun() -> + ?assertMatch( + ?ERR(#{matched_type := "authn-jwt:public-key"}), + Check("{use_jwks = false, public_key = 1}") + ) + end}, + {"hmac-based", fun() -> + ?assertMatch( + ?ERR(#{matched_type := "authn-jwt:hmac-based"}), + Check("{use_jwks = false}") + ) + end} + ]. + +union_member_selector_redis_test_() -> + Check = fun(Txt) -> check(emqx_authn_redis, Txt) end, + [ + {"unknown", fun() -> + ?assertMatch( + ?ERR(#{field_name := redis_type, expected := _}), + Check("{redis_type = 1}") + ) + end}, + {"single", fun() -> + ?assertMatch( + ?ERR(#{matched_type := "authn-redis:standalone"}), + Check("{redis_type = single}") + ) + end}, + {"cluster", fun() -> + ?assertMatch( + ?ERR(#{matched_type := "authn-redis:cluster"}), + Check("{redis_type = cluster}") + ) + end}, + {"sentinel", fun() -> + ?assertMatch( + ?ERR(#{matched_type := "authn-redis:sentinel"}), + Check("{redis_type = sentinel}") + ) + end} + ]. + +union_member_selector_http_test_() -> + Check = fun(Txt) -> check(emqx_authn_http, Txt) end, + [ + {"unknown", fun() -> + ?assertMatch( + ?ERR(#{field_name := method, expected := _}), + Check("{method = 1}") + ) + end}, + {"get", fun() -> + ?assertMatch( + ?ERR(#{matched_type := "authn-http:get"}), + Check("{method = get}") + ) + end}, + {"post", fun() -> + ?assertMatch( + ?ERR(#{matched_type := "authn-http:post"}), + Check("{method = post}") + ) + end} + ]. + +check(Module, HoconConf) -> + emqx_hocon:check(Module, ["authentication= ", HoconConf]). From 618ec043406cccbc44fce44fa9c6862debc91092 Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Tue, 31 Jan 2023 15:02:19 +0100 Subject: [PATCH 0459/1802] style: fix comment --- apps/emqx_plugins/src/emqx_plugins.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/emqx_plugins/src/emqx_plugins.erl b/apps/emqx_plugins/src/emqx_plugins.erl index 613148a24..6815760e9 100644 --- a/apps/emqx_plugins/src/emqx_plugins.erl +++ b/apps/emqx_plugins/src/emqx_plugins.erl @@ -111,8 +111,8 @@ do_ensure_installed(NameVsn) -> }} end. -%% @doc Ensure files and directories for the given plugin are delete. -%% If a plugin is running, or enabled, error is returned. +%% @doc Ensure files and directories for the given plugin are being deleted. +%% If a plugin is running, or enabled, an error is returned. -spec ensure_uninstalled(name_vsn()) -> ok | {error, any()}. ensure_uninstalled(NameVsn) -> case read_plugin(NameVsn, #{}) of From 70c4e12b685b0d8324ce50e6d9e49b5ce1ed8c7c Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Tue, 31 Jan 2023 15:10:24 +0100 Subject: [PATCH 0460/1802] fix: cleanup after upload of broken plugin --- .../src/emqx_mgmt_api_plugins.erl | 36 +++++++-- apps/emqx_plugins/src/emqx_plugins.app.src | 2 +- apps/emqx_plugins/src/emqx_plugins.erl | 78 +++++++++++++++++-- 3 files changed, 101 insertions(+), 15 deletions(-) diff --git a/apps/emqx_management/src/emqx_mgmt_api_plugins.erl b/apps/emqx_management/src/emqx_mgmt_api_plugins.erl index 28deac98d..d1acfee6a 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_plugins.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_plugins.erl @@ -17,7 +17,6 @@ -behaviour(minirest_api). --include_lib("kernel/include/file.hrl"). -include_lib("typerefl/include/types.hrl"). -include_lib("emqx/include/logger.hrl"). %%-include_lib("emqx_plugins/include/emqx_plugins.hrl"). @@ -326,7 +325,8 @@ upload_install(post, #{body := #{<<"plugin">> := Plugin}}) when is_map(Plugin) - %% File bin is too large, we use rpc:multicall instead of cluster_rpc:multicall %% TODO what happens when a new node join in? %% emqx_plugins_monitor should copy plugins from other core node when boot-up. - case emqx_plugins:describe(string:trim(FileName, trailing, ".tar.gz")) of + NameVsn = string:trim(FileName, trailing, ".tar.gz"), + case emqx_plugins:describe(NameVsn) of {error, #{error := "bad_info_file", return := {enoent, _}}} -> case emqx_plugins:parse_name_vsn(FileName) of {ok, AppName, _Vsn} -> @@ -346,6 +346,7 @@ upload_install(post, #{body := #{<<"plugin">> := Plugin}}) when is_map(Plugin) - }} end; {error, Reason} -> + emqx_plugins:delete_package(NameVsn), {400, #{ code => 'BAD_PLUGIN_INFO', message => iolist_to_binary([Reason, ":", FileName]) @@ -367,9 +368,24 @@ upload_install(post, #{}) -> do_install_package(FileName, Bin) -> %% TODO: handle bad nodes {[_ | _] = Res, []} = emqx_mgmt_api_plugins_proto_v1:install_package(FileName, Bin), - %% TODO: handle non-OKs - [] = lists:filter(fun(R) -> R =/= ok end, Res), - {200}. + case lists:filter(fun(R) -> R =/= ok end, Res) of + [] -> + {200}; + Filtered -> + %% crash if we have unexpected errors or results + [] = lists:filter( + fun + ({error, {failed, _}}) -> true; + ({error, _}) -> false + end, + Filtered + ), + {error, #{error := Reason}} = hd(Filtered), + {400, #{ + code => 'BAD_PLUGIN_INFO', + message => iolist_to_binary([Reason, ":", FileName]) + }} + end. plugin(get, #{bindings := #{name := Name}}) -> {Plugins, _} = emqx_mgmt_api_plugins_proto_v1:describe_package(Name), @@ -408,7 +424,15 @@ install_package(FileName, Bin) -> File = filename:join(emqx_plugins:install_dir(), FileName), ok = file:write_file(File, Bin), PackageName = string:trim(FileName, trailing, ".tar.gz"), - emqx_plugins:ensure_installed(PackageName). + case emqx_plugins:ensure_installed(PackageName) of + {error, #{return := not_found}} = NotFound -> + NotFound; + {error, _Reason} = Error -> + _ = file:delete(File), + Error; + Result -> + Result + end. %% For RPC plugin get describe_package(Name) -> diff --git a/apps/emqx_plugins/src/emqx_plugins.app.src b/apps/emqx_plugins/src/emqx_plugins.app.src index 1635bb516..de56099ba 100644 --- a/apps/emqx_plugins/src/emqx_plugins.app.src +++ b/apps/emqx_plugins/src/emqx_plugins.app.src @@ -1,7 +1,7 @@ %% -*- mode: erlang -*- {application, emqx_plugins, [ {description, "EMQX Plugin Management"}, - {vsn, "0.1.0"}, + {vsn, "0.1.1"}, {modules, []}, {mod, {emqx_plugins_app, []}}, {applications, [kernel, stdlib, emqx]}, diff --git a/apps/emqx_plugins/src/emqx_plugins.erl b/apps/emqx_plugins/src/emqx_plugins.erl index 6815760e9..8993404d4 100644 --- a/apps/emqx_plugins/src/emqx_plugins.erl +++ b/apps/emqx_plugins/src/emqx_plugins.erl @@ -16,8 +16,13 @@ -module(emqx_plugins). --include_lib("emqx/include/emqx.hrl"). -include_lib("emqx/include/logger.hrl"). +-include_lib("emqx/include/logger.hrl"). +-include("emqx_plugins.hrl"). + +-ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). +-endif. -export([ ensure_installed/1, @@ -56,10 +61,6 @@ -compile(nowarn_export_all). -endif. --include_lib("emqx/include/emqx.hrl"). --include_lib("emqx/include/logger.hrl"). --include("emqx_plugins.hrl"). - %% "my_plugin-0.1.0" -type name_vsn() :: binary() | string(). %% the parse result of the JSON info file @@ -87,14 +88,15 @@ ensure_installed(NameVsn) -> do_ensure_installed(NameVsn) -> TarGz = pkg_file(NameVsn), - case erl_tar:extract(TarGz, [{cwd, install_dir()}, compressed]) of - ok -> + case erl_tar:extract(TarGz, [compressed, memory]) of + {ok, TarContent} -> + ok = write_tar_file_content(install_dir(), TarContent), case read_plugin(NameVsn, #{}) of {ok, _} -> ok; {error, Reason} -> ?SLOG(warning, Reason#{msg => "failed_to_read_after_install"}), - _ = ensure_uninstalled(NameVsn), + ok = delete_tar_file_content(install_dir(), TarContent), {error, Reason} end; {error, {_, enoent}} -> @@ -111,6 +113,66 @@ do_ensure_installed(NameVsn) -> }} end. +write_tar_file_content(BaseDir, TarContent) -> + lists:foreach( + fun({Name, Bin}) -> + Filename = filename:join(BaseDir, Name), + ok = filelib:ensure_dir(Filename), + ok = file:write_file(Filename, Bin) + end, + TarContent + ). + +delete_tar_file_content(BaseDir, TarContent) -> + lists:foreach( + fun({Name, _}) -> + Filename = filename:join(BaseDir, Name), + case filelib:is_file(Filename) of + true -> + TopDirOrFile = top_dir(BaseDir, Filename), + ok = file:del_dir_r(TopDirOrFile); + false -> + %% probably already deleted + ok + end + end, + TarContent + ). + +top_dir(BaseDir0, DirOrFile) -> + BaseDir = normalize_dir(BaseDir0), + case filename:dirname(DirOrFile) of + RockBottom when RockBottom =:= "/" orelse RockBottom =:= "." -> + throw({out_of_bounds, DirOrFile}); + BaseDir -> + DirOrFile; + Parent -> + top_dir(BaseDir, Parent) + end. + +normalize_dir(Dir) -> + %% Get rid of possible trailing slash + filename:join([Dir, ""]). + +-ifdef(TEST). +normalize_dir_test_() -> + [ + ?_assertEqual("foo", normalize_dir("foo")), + ?_assertEqual("foo", normalize_dir("foo/")), + ?_assertEqual("/foo", normalize_dir("/foo")), + ?_assertEqual("/foo", normalize_dir("/foo/")) + ]. + +top_dir_test_() -> + [ + ?_assertEqual("base/foo", top_dir("base", filename:join(["base", "foo", "bar"]))), + ?_assertEqual("/base/foo", top_dir("/base", filename:join(["/", "base", "foo", "bar"]))), + ?_assertEqual("/base/foo", top_dir("/base/", filename:join(["/", "base", "foo", "bar"]))), + ?_assertThrow({out_of_bounds, _}, top_dir("/base", filename:join(["/", "base"]))), + ?_assertThrow({out_of_bounds, _}, top_dir("/base", filename:join(["/", "foo", "bar"]))) + ]. +-endif. + %% @doc Ensure files and directories for the given plugin are being deleted. %% If a plugin is running, or enabled, an error is returned. -spec ensure_uninstalled(name_vsn()) -> ok | {error, any()}. From a866c995f55e230d1063ea71e276a42324dd076d Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Wed, 1 Feb 2023 16:32:30 +0100 Subject: [PATCH 0461/1802] test: add test for arbitrary content in tar file --- apps/emqx_plugins/test/emqx_plugins_SUITE.erl | 57 +++++++++++++++---- 1 file changed, 47 insertions(+), 10 deletions(-) diff --git a/apps/emqx_plugins/test/emqx_plugins_SUITE.erl b/apps/emqx_plugins/test/emqx_plugins_SUITE.erl index 494b48f7f..9c410a1ee 100644 --- a/apps/emqx_plugins/test/emqx_plugins_SUITE.erl +++ b/apps/emqx_plugins/test/emqx_plugins_SUITE.erl @@ -20,6 +20,7 @@ -compile(nowarn_export_all). -include_lib("eunit/include/eunit.hrl"). +-include_lib("common_test/include/ct.hrl"). -define(EMQX_PLUGIN_TEMPLATE_RELEASE_NAME, "emqx_plugin_template"). -define(EMQX_PLUGIN_TEMPLATE_URL, @@ -325,27 +326,60 @@ t_bad_tar_gz(Config) -> %% idempotent ok = emqx_plugins:delete_package("fake-vsn"). -%% create a corrupted .tar.gz +%% create with incomplete info file %% failed install attempts should not leave behind extracted dir t_bad_tar_gz2({init, Config}) -> - Config; -t_bad_tar_gz2({'end', _Config}) -> - ok; -t_bad_tar_gz2(Config) -> WorkDir = proplists:get_value(data_dir, Config), NameVsn = "foo-0.2", - %% this an invalid info file content + %% this an invalid info file content (description missing) BadInfo = "name=foo, rel_vsn=\"0.2\", rel_apps=[foo]", ok = write_info_file(Config, NameVsn, BadInfo), TarGz = filename:join([WorkDir, NameVsn ++ ".tar.gz"]), ok = make_tar(WorkDir, NameVsn), + [{tar_gz, TarGz}, {name_vsn, NameVsn} | Config]; +t_bad_tar_gz2({'end', Config}) -> + NameVsn = ?config(name_vsn, Config), + ok = emqx_plugins:delete_package(NameVsn), + ok; +t_bad_tar_gz2(Config) -> + TarGz = ?config(tar_gz, Config), + NameVsn = ?config(name_vsn, Config), ?assert(filelib:is_regular(TarGz)), - %% failed to install, it also cleans up the bad .tar.gz file + %% failed to install, it also cleans up the bad content of .tar.gz file ?assertMatch({error, _}, emqx_plugins:ensure_installed(NameVsn)), + ?assertEqual({error, enoent}, file:read_file_info(emqx_plugins:dir(NameVsn))), + %% but the tar.gz file is still around + ?assert(filelib:is_regular(TarGz)), + ok. + +%% test that we even cleanup content that doesn't match the expected name-vsn +%% pattern +t_tar_vsn_content_mismatch({init, Config}) -> + WorkDir = proplists:get_value(data_dir, Config), + NameVsn = "bad_tar-0.2", + %% this an invalid info file content + BadInfo = "name=foo, rel_vsn=\"0.2\", rel_apps=[\"foo-0.2\"], description=\"lorem ipsum\"", + ok = write_info_file(Config, "foo-0.2", BadInfo), + TarGz = filename:join([WorkDir, "bad_tar-0.2.tar.gz"]), + ok = make_tar(WorkDir, "foo-0.2", NameVsn), + file:delete(filename:join([WorkDir, "foo-0.2", "release.json"])), + [{tar_gz, TarGz}, {name_vsn, NameVsn} | Config]; +t_tar_vsn_content_mismatch({'end', Config}) -> + NameVsn = ?config(name_vsn, Config), + ok = emqx_plugins:delete_package(NameVsn), + ok; +t_tar_vsn_content_mismatch(Config) -> + TarGz = ?config(tar_gz, Config), + NameVsn = ?config(name_vsn, Config), + ?assert(filelib:is_regular(TarGz)), + %% failed to install, it also cleans up content of the bad .tar.gz file even + %% if in other directory + ?assertMatch({error, _}, emqx_plugins:ensure_installed(NameVsn)), + ?assertEqual({error, enoent}, file:read_file_info(emqx_plugins:dir(NameVsn))), + ?assertEqual({error, enoent}, file:read_file_info(emqx_plugins:dir("foo-0.2"))), %% the tar.gz file is still around ?assert(filelib:is_regular(TarGz)), - ?assertEqual({error, enoent}, file:read_file_info(emqx_plugins:dir(NameVsn))), - ok = emqx_plugins:delete_package(NameVsn). + ok. t_bad_info_json({init, Config}) -> Config; @@ -446,11 +480,14 @@ t_elixir_plugin(Config) -> ok. make_tar(Cwd, NameWithVsn) -> + make_tar(Cwd, NameWithVsn, NameWithVsn). + +make_tar(Cwd, NameWithVsn, TarfileVsn) -> {ok, OriginalCwd} = file:get_cwd(), ok = file:set_cwd(Cwd), try Files = filelib:wildcard(NameWithVsn ++ "/**"), - TarFile = NameWithVsn ++ ".tar.gz", + TarFile = TarfileVsn ++ ".tar.gz", ok = erl_tar:create(TarFile, Files, [compressed]) after file:set_cwd(OriginalCwd) From e135f8654c9c7e91eb701b40ec97d2b0df5fc1ac Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Thu, 2 Feb 2023 11:09:40 +0100 Subject: [PATCH 0462/1802] test: enable basic plugins API tests --- .../test/emqx_mgmt_api_plugins_SUITE.erl | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/apps/emqx_management/test/emqx_mgmt_api_plugins_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_api_plugins_SUITE.erl index 12193914b..6183dc29d 100644 --- a/apps/emqx_management/test/emqx_mgmt_api_plugins_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_api_plugins_SUITE.erl @@ -20,7 +20,7 @@ -include_lib("eunit/include/eunit.hrl"). --define(EMQX_PLUGIN_TEMPLATE_VSN, "5.0-rc.1"). +-define(EMQX_PLUGIN_TEMPLATE_VSN, "5.0.0"). -define(PACKAGE_SUFFIX, ".tar.gz"). all() -> @@ -30,12 +30,11 @@ init_per_suite(Config) -> WorkDir = proplists:get_value(data_dir, Config), ok = filelib:ensure_dir(WorkDir), DemoShDir1 = string:replace(WorkDir, "emqx_mgmt_api_plugins", "emqx_plugins"), - DemoShDir = string:replace(DemoShDir1, "emqx_management", "emqx_plugins"), + DemoShDir = lists:flatten(string:replace(DemoShDir1, "emqx_management", "emqx_plugins")), OrigInstallDir = emqx_plugins:get_config(install_dir, undefined), ok = filelib:ensure_dir(DemoShDir), emqx_mgmt_api_test_util:init_suite([emqx_conf, emqx_plugins]), emqx_plugins:put_config(install_dir, DemoShDir), - [{demo_sh_dir, DemoShDir}, {orig_install_dir, OrigInstallDir} | Config]. end_per_suite(Config) -> @@ -48,18 +47,20 @@ end_per_suite(Config) -> emqx_mgmt_api_test_util:end_suite([emqx_plugins, emqx_conf]), ok. -todo_t_plugins(Config) -> +t_plugins(Config) -> DemoShDir = proplists:get_value(demo_sh_dir, Config), PackagePath = get_demo_plugin_package(DemoShDir), ct:pal("package_location:~p install dir:~p", [PackagePath, emqx_plugins:install_dir()]), NameVsn = filename:basename(PackagePath, ?PACKAGE_SUFFIX), + ok = emqx_plugins:ensure_uninstalled(NameVsn), ok = emqx_plugins:delete_package(NameVsn), ok = install_plugin(PackagePath), {ok, StopRes} = describe_plugins(NameVsn), + Node = atom_to_binary(node()), ?assertMatch( #{ <<"running_status">> := [ - #{<<"node">> := <<"test@127.0.0.1">>, <<"status">> := <<"stopped">>} + #{<<"node">> := Node, <<"status">> := <<"stopped">>} ] }, StopRes @@ -70,7 +71,7 @@ todo_t_plugins(Config) -> ?assertMatch( #{ <<"running_status">> := [ - #{<<"node">> := <<"test@127.0.0.1">>, <<"status">> := <<"running">>} + #{<<"node">> := Node, <<"status">> := <<"running">>} ] }, StartRes @@ -80,7 +81,7 @@ todo_t_plugins(Config) -> ?assertMatch( #{ <<"running_status">> := [ - #{<<"node">> := <<"test@127.0.0.1">>, <<"status">> := <<"stopped">>} + #{<<"node">> := Node, <<"status">> := <<"stopped">>} ] }, StopRes2 From 847f899fa0e220f5f9812e48285a6ee3a3a89a3e Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Thu, 2 Feb 2023 12:01:30 +0100 Subject: [PATCH 0463/1802] test: ensure bad plugin upload gets deleted --- .../test/emqx_mgmt_api_plugins_SUITE.erl | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/apps/emqx_management/test/emqx_mgmt_api_plugins_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_api_plugins_SUITE.erl index 6183dc29d..0cf15d678 100644 --- a/apps/emqx_management/test/emqx_mgmt_api_plugins_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_api_plugins_SUITE.erl @@ -89,6 +89,28 @@ t_plugins(Config) -> {ok, []} = uninstall_plugin(NameVsn), ok. +t_bad_plugin(Config) -> + DemoShDir = proplists:get_value(demo_sh_dir, Config), + PackagePathOrig = get_demo_plugin_package(DemoShDir), + PackagePath = filename:join([ + filename:dirname(PackagePathOrig), + "bad_plugin-1.0.0.tar.gz" + ]), + ct:pal("package_location:~p orig:~p", [PackagePath, PackagePathOrig]), + %% rename plugin tarball + file:copy(PackagePathOrig, PackagePath), + file:delete(PackagePathOrig), + {ok, {{"HTTP/1.1", 400, "Bad Request"}, _, _}} = install_plugin(PackagePath), + ?assertEqual( + {error, enoent}, + file:delete( + filename:join([ + emqx_plugins:install_dir(), + filename:basename(PackagePath) + ]) + ) + ). + list_plugins() -> Path = emqx_mgmt_api_test_util:api_path(["plugins"]), case emqx_mgmt_api_test_util:request_api(get, Path) of From 0d6f8331d7a81a90f27c0a78a2d3eab1036ed22c Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Thu, 2 Feb 2023 13:05:24 +0100 Subject: [PATCH 0464/1802] chore: add changelog --- changes/v5.0.17/fix-9875.en.md | 1 + changes/v5.0.17/fix-9875.zh.md | 1 + 2 files changed, 2 insertions(+) create mode 100644 changes/v5.0.17/fix-9875.en.md create mode 100644 changes/v5.0.17/fix-9875.zh.md diff --git a/changes/v5.0.17/fix-9875.en.md b/changes/v5.0.17/fix-9875.en.md new file mode 100644 index 000000000..ca2d4094a --- /dev/null +++ b/changes/v5.0.17/fix-9875.en.md @@ -0,0 +1 @@ +Return `400` if a broken plugin package is uploaded from HTTP API. Also cleanup if plugin is not accepted. diff --git a/changes/v5.0.17/fix-9875.zh.md b/changes/v5.0.17/fix-9875.zh.md new file mode 100644 index 000000000..6e1230f3a --- /dev/null +++ b/changes/v5.0.17/fix-9875.zh.md @@ -0,0 +1 @@ +当通过 HTTP API 上传一个损坏的插件包时,返回 `400`,且删除该插件包。 From 4e12a44ee6ab37795517969b871bcbb3db481e91 Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Fri, 3 Feb 2023 15:19:36 +0100 Subject: [PATCH 0465/1802] fix: return 204 instead of 200 if there's no body content --- apps/emqx_bridge/src/emqx_bridge_api.erl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/emqx_bridge/src/emqx_bridge_api.erl b/apps/emqx_bridge/src/emqx_bridge_api.erl index 2e94f0719..dce99646f 100644 --- a/apps/emqx_bridge/src/emqx_bridge_api.erl +++ b/apps/emqx_bridge/src/emqx_bridge_api.erl @@ -378,7 +378,7 @@ schema("/bridges/:id/metrics/reset") -> description => ?DESC("desc_api6"), parameters => [param_path_id()], responses => #{ - 200 => <<"Reset success">>, + 204 => <<"Reset success">>, 400 => error_schema(['BAD_REQUEST'], "RPC Call Failed") } } @@ -412,7 +412,7 @@ schema("/bridges/:id/:operation") -> param_path_operation_cluster() ], responses => #{ - 200 => <<"Operation success">>, + 204 => <<"Operation success">>, 503 => error_schema('SERVICE_UNAVAILABLE', "Service unavailable"), 400 => error_schema('INVALID_ID', "Bad bridge ID") } @@ -431,7 +431,7 @@ schema("/nodes/:node/bridges/:id/:operation") -> param_path_operation_on_node() ], responses => #{ - 200 => <<"Operation success">>, + 204 => <<"Operation success">>, 400 => error_schema('INVALID_ID', "Bad bridge ID"), 403 => error_schema('FORBIDDEN_REQUEST', "forbidden operation"), 503 => error_schema('SERVICE_UNAVAILABLE', "Service unavailable") @@ -535,7 +535,7 @@ schema("/bridges_probe") -> emqx_bridge_resource:resource_id(BridgeType, BridgeName) ) of - ok -> {200, <<"Reset success">>}; + ok -> {204}; Reason -> {400, error_msg('BAD_REQUEST', Reason)} end ). @@ -660,7 +660,7 @@ operation_to_all_nodes(Nodes, OperFunc, BridgeType, BridgeName) -> end, case is_ok(emqx_bridge_proto_v1:RpcFunc(Nodes, BridgeType, BridgeName)) of {ok, _} -> - {200}; + {204}; {error, [timeout | _]} -> {503, error_msg('SERVICE_UNAVAILABLE', <<"request timeout">>)}; {error, ErrL} -> @@ -905,7 +905,7 @@ call_operation(Node, OperFunc, BridgeType, BridgeName) -> ) of ok -> - {200}; + {204}; {error, timeout} -> {503, error_msg('SERVICE_UNAVAILABLE', <<"request timeout">>)}; {error, {start_pool_failed, Name, Reason}} -> From 758d19f1e0d7b4833c586f292f420b8c8245f441 Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Mon, 6 Feb 2023 10:44:06 +0100 Subject: [PATCH 0466/1802] chore: bump vsn --- apps/emqx_management/src/emqx_management.app.src | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx_management/src/emqx_management.app.src b/apps/emqx_management/src/emqx_management.app.src index 158d65b6b..fdd2af9b2 100644 --- a/apps/emqx_management/src/emqx_management.app.src +++ b/apps/emqx_management/src/emqx_management.app.src @@ -2,7 +2,7 @@ {application, emqx_management, [ {description, "EMQX Management API and CLI"}, % strict semver, bump manually! - {vsn, "5.0.13"}, + {vsn, "5.0.14"}, {modules, []}, {registered, [emqx_management_sup]}, {applications, [kernel, stdlib, emqx_plugins, minirest, emqx]}, From 83e66da2aa90c2d2295344fa14d549a3b4f9a1b0 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Wed, 18 Jan 2023 12:40:57 +0100 Subject: [PATCH 0467/1802] fix(authn): throw exception on invalid config from the API --- apps/emqx/src/emqx_config.erl | 26 ++------------------- apps/emqx/src/emqx_hocon.erl | 10 ++++++--- apps/emqx_authn/src/emqx_authn.app.src | 2 +- apps/emqx_authn/src/emqx_authn.erl | 15 ++++++++----- apps/emqx_authn/src/emqx_authn_app.erl | 31 +++++++++----------------- apps/emqx_conf/src/emqx_conf.app.src | 2 +- 6 files changed, 32 insertions(+), 54 deletions(-) diff --git a/apps/emqx/src/emqx_config.erl b/apps/emqx/src/emqx_config.erl index fb84d64a1..b76234e85 100644 --- a/apps/emqx/src/emqx_config.erl +++ b/apps/emqx/src/emqx_config.erl @@ -414,32 +414,10 @@ check_config(SchemaMod, RawConf, Opts0) -> do_check_config(SchemaMod, RawConf, Opts0) catch throw:Errors:Stacktrace -> - throw(emqx_hocon:compact_errors(Errors, Stacktrace)) + {error, Reason} = emqx_hocon:compact_errors(Errors, Stacktrace), + erlang:raise(throw, Reason, Stacktrace) end. -%% HOCON tries to be very informative about all the detailed errors -%% it's maybe too much when reporting to the user --spec compact_errors(any(), any()) -> no_return(). -compact_errors(Schema, [Error0 | More]) when is_map(Error0) -> - Error1 = - case length(More) of - 0 -> - Error0; - N -> - Error0#{unshown_errors => N} - end, - Error = - case is_atom(Schema) of - true -> - Error1#{schema_module => Schema}; - false -> - Error1 - end, - throw(Error); -compact_errors(Schema, Errors) -> - %% unexpected, we need the stacktrace reported, hence error - error({Schema, Errors}). - do_check_config(SchemaMod, RawConf, Opts0) -> Opts1 = #{ return_plain => true, diff --git a/apps/emqx/src/emqx_hocon.erl b/apps/emqx/src/emqx_hocon.erl index a039aa8c2..09503e6fe 100644 --- a/apps/emqx/src/emqx_hocon.erl +++ b/apps/emqx/src/emqx_hocon.erl @@ -20,6 +20,7 @@ -export([ format_path/1, check/2, + check/3, compact_errors/2, format_error/1, format_error/2, @@ -37,20 +38,23 @@ format_path([Name | Rest]) -> [iol(Name), "." | format_path(Rest)]. %% Always return plain map with atom keys. -spec check(module(), hocon:config() | iodata()) -> {ok, hocon:config()} | {error, any()}. -check(SchemaModule, Conf) when is_map(Conf) -> +check(SchemaModule, Conf) -> %% TODO: remove required %% fields should state required or not in their schema Opts = #{atom_key => true, required => false}, + check(SchemaModule, Conf, Opts). + +check(SchemaModule, Conf, Opts) when is_map(Conf) -> try {ok, hocon_tconf:check_plain(SchemaModule, Conf, Opts)} catch throw:Errors:Stacktrace -> compact_errors(Errors, Stacktrace) end; -check(SchemaModule, HoconText) -> +check(SchemaModule, HoconText, Opts) -> case hocon:binary(HoconText, #{format => map}) of {ok, MapConfig} -> - check(SchemaModule, MapConfig); + check(SchemaModule, MapConfig, Opts); {error, Reason} -> {error, Reason} end. diff --git a/apps/emqx_authn/src/emqx_authn.app.src b/apps/emqx_authn/src/emqx_authn.app.src index 7f01d94c0..0b5b0dedc 100644 --- a/apps/emqx_authn/src/emqx_authn.app.src +++ b/apps/emqx_authn/src/emqx_authn.app.src @@ -1,7 +1,7 @@ %% -*- mode: erlang -*- {application, emqx_authn, [ {description, "EMQX Authentication"}, - {vsn, "0.1.12"}, + {vsn, "0.1.13"}, {modules, []}, {registered, [emqx_authn_sup, emqx_authn_registry]}, {applications, [kernel, stdlib, emqx_resource, emqx_connector, ehttpc, epgsql, mysql, jose]}, diff --git a/apps/emqx_authn/src/emqx_authn.erl b/apps/emqx_authn/src/emqx_authn.erl index 934c57fb1..8bda0c3a1 100644 --- a/apps/emqx_authn/src/emqx_authn.erl +++ b/apps/emqx_authn/src/emqx_authn.erl @@ -69,11 +69,7 @@ do_check_config(#{<<"mechanism">> := Mec0} = Config, Opts) -> false -> throw(#{error => unknown_authn_provider, which => Key}); {_, ProviderModule} -> - emqx_hocon:check( - ProviderModule, - #{?CONF_NS_BINARY => Config}, - Opts#{atom_key => true} - ) + do_check_config_maybe_throw(ProviderModule, Config, Opts) end; do_check_config(Config, Opts) when is_map(Config) -> throw(#{ @@ -82,6 +78,15 @@ do_check_config(Config, Opts) when is_map(Config) -> reason => "mechanism_field_required" }). +do_check_config_maybe_throw(ProviderModule, Config0, Opts) -> + Config = #{?CONF_NS_BINARY => Config0}, + case emqx_hocon:check(ProviderModule, Config, Opts#{atom_key => true}) of + {ok, Checked} -> + Checked; + {error, Reason} -> + throw(Reason) + end. + %% The atoms have to be loaded already, %% which might be an issue for plugins which are loaded after node boot %% but they should really manage their own configs in that case. diff --git a/apps/emqx_authn/src/emqx_authn_app.erl b/apps/emqx_authn/src/emqx_authn_app.erl index ed97c0975..c94aec75d 100644 --- a/apps/emqx_authn/src/emqx_authn_app.erl +++ b/apps/emqx_authn/src/emqx_authn_app.erl @@ -53,26 +53,17 @@ stop(_State) -> %%------------------------------------------------------------------------------ initialize() -> - try - ok = ?AUTHN:register_providers(emqx_authn:providers()), - - lists:foreach( - fun({ChainName, RawAuthConfigs}) -> - AuthConfig = emqx_authn:check_configs(RawAuthConfigs), - ?AUTHN:initialize_authentication( - ChainName, - AuthConfig - ) - end, - chain_configs() - ) - of - ok -> ok - catch - throw:Reason -> - ?SLOG(error, #{msg => "failed_to_initialize_authentication", reason => Reason}), - {error, {failed_to_initialize_authentication, Reason}} - end. + ok = ?AUTHN:register_providers(emqx_authn:providers()), + lists:foreach( + fun({ChainName, RawAuthConfigs}) -> + AuthConfig = emqx_authn:check_configs(RawAuthConfigs), + ?AUTHN:initialize_authentication( + ChainName, + AuthConfig + ) + end, + chain_configs() + ). deinitialize() -> ok = ?AUTHN:deregister_providers(provider_types()), diff --git a/apps/emqx_conf/src/emqx_conf.app.src b/apps/emqx_conf/src/emqx_conf.app.src index f7fd33e3b..beac051a1 100644 --- a/apps/emqx_conf/src/emqx_conf.app.src +++ b/apps/emqx_conf/src/emqx_conf.app.src @@ -1,6 +1,6 @@ {application, emqx_conf, [ {description, "EMQX configuration management"}, - {vsn, "0.1.11"}, + {vsn, "0.1.12"}, {registered, []}, {mod, {emqx_conf_app, []}}, {applications, [kernel, stdlib]}, From dc8f0c5101f867bc81eadad0b81490b4df62392c Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Mon, 6 Feb 2023 18:15:01 +0100 Subject: [PATCH 0468/1802] refactor(authn): no need to check config at app boot --- apps/emqx/src/emqx_hocon.erl | 4 +-- apps/emqx_authn/src/emqx_authn.erl | 23 +++++++--------- apps/emqx_authn/src/emqx_authn_app.erl | 7 +++-- apps/emqx_authn/test/emqx_authn_SUITE.erl | 32 +++++++++++++++++++++++ 4 files changed, 46 insertions(+), 20 deletions(-) diff --git a/apps/emqx/src/emqx_hocon.erl b/apps/emqx/src/emqx_hocon.erl index 09503e6fe..08192e9be 100644 --- a/apps/emqx/src/emqx_hocon.erl +++ b/apps/emqx/src/emqx_hocon.erl @@ -119,8 +119,8 @@ compact_errors(SchemaModule, [Error0 | More], _Stacktrace) when is_map(Error0) - end, {error, Error}; compact_errors(SchemaModule, Error, Stacktrace) -> - %% unexpected, we need the stacktrace reported, hence error - %% if this happens i'ts a bug in hocon_tconf + %% unexpected, we need the stacktrace reported + %% if this happens it's a bug in hocon_tconf {error, #{ schema_module => SchemaModule, exception => Error, diff --git a/apps/emqx_authn/src/emqx_authn.erl b/apps/emqx_authn/src/emqx_authn.erl index 8bda0c3a1..15efeb673 100644 --- a/apps/emqx_authn/src/emqx_authn.erl +++ b/apps/emqx_authn/src/emqx_authn.erl @@ -20,7 +20,6 @@ providers/0, check_config/1, check_config/2, - check_configs/1, %% for telemetry information get_enabled_authns/0 ]). @@ -39,16 +38,6 @@ providers() -> {{scram, built_in_database}, emqx_enhanced_authn_scram_mnesia} ]. -check_configs(CM) when is_map(CM) -> - check_configs([CM]); -check_configs(CL) -> - check_configs(CL, 1). - -check_configs([], _Nth) -> - []; -check_configs([Config | Configs], Nth) -> - [check_config(Config, #{id_for_log => Nth}) | check_configs(Configs, Nth + 1)]. - check_config(Config) -> check_config(Config, #{}). @@ -67,14 +56,20 @@ do_check_config(#{<<"mechanism">> := Mec0} = Config, Opts) -> end, case lists:keyfind(Key, 1, providers()) of false -> - throw(#{error => unknown_authn_provider, which => Key}); + Reason = + case Key of + {M, B} -> + #{mechanism => M, backend => B}; + M -> + #{mechanism => M} + end, + throw(Reason#{error => unknown_authn_provider}); {_, ProviderModule} -> do_check_config_maybe_throw(ProviderModule, Config, Opts) end; -do_check_config(Config, Opts) when is_map(Config) -> +do_check_config(Config, _Opts) when is_map(Config) -> throw(#{ error => invalid_config, - which => maps:get(id_for_log, Opts, unknown), reason => "mechanism_field_required" }). diff --git a/apps/emqx_authn/src/emqx_authn_app.erl b/apps/emqx_authn/src/emqx_authn_app.erl index c94aec75d..44fec2363 100644 --- a/apps/emqx_authn/src/emqx_authn_app.erl +++ b/apps/emqx_authn/src/emqx_authn_app.erl @@ -55,8 +55,7 @@ stop(_State) -> initialize() -> ok = ?AUTHN:register_providers(emqx_authn:providers()), lists:foreach( - fun({ChainName, RawAuthConfigs}) -> - AuthConfig = emqx_authn:check_configs(RawAuthConfigs), + fun({ChainName, AuthConfig}) -> ?AUTHN:initialize_authentication( ChainName, AuthConfig @@ -73,12 +72,12 @@ chain_configs() -> [global_chain_config() | listener_chain_configs()]. global_chain_config() -> - {?GLOBAL, emqx:get_raw_config([?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_BINARY], [])}. + {?GLOBAL, emqx:get_config([?EMQX_AUTHENTICATION_CONFIG_ROOT_NAME_BINARY], [])}. listener_chain_configs() -> lists:map( fun({ListenerID, _}) -> - {ListenerID, emqx:get_raw_config(auth_config_path(ListenerID), [])} + {ListenerID, emqx:get_config(auth_config_path(ListenerID), [])} end, emqx_listeners:list() ). diff --git a/apps/emqx_authn/test/emqx_authn_SUITE.erl b/apps/emqx_authn/test/emqx_authn_SUITE.erl index b30d2fcff..4f96ca2dd 100644 --- a/apps/emqx_authn/test/emqx_authn_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_SUITE.erl @@ -168,6 +168,38 @@ t_password_undefined(Config) when is_list(Config) -> end, ok. +t_union_selector_errors({init, Config}) -> + Config; +t_union_selector_errors({'end', _Config}) -> + ok; +t_union_selector_errors(Config) when is_list(Config) -> + Conf0 = #{ + <<"mechanism">> => <<"password_based">>, + <<"backend">> => <<"mysql">> + }, + Conf1 = Conf0#{<<"mechanism">> => <<"unknown-atom-xx">>}, + ?assertThrow(#{error := unknown_mechanism}, emqx_authn:check_config(Conf1)), + Conf2 = Conf0#{<<"backend">> => <<"unknown-atom-xx">>}, + ?assertThrow(#{error := unknown_backend}, emqx_authn:check_config(Conf2)), + Conf3 = Conf0#{<<"backend">> => <<"unknown">>, <<"mechanism">> => <<"unknown">>}, + ?assertThrow( + #{ + error := unknown_authn_provider, + backend := unknown, + mechanism := unknown + }, + emqx_authn:check_config(Conf3) + ), + Res = catch (emqx_authn:check_config(#{<<"mechanism">> => <<"unknown">>})), + ?assertEqual( + #{ + error => unknown_authn_provider, + mechanism => unknown + }, + Res + ), + ok. + parse(Bytes) -> {ok, Frame, <<>>, {none, _}} = emqx_frame:parse(Bytes), Frame. From 4653ca65f787174dc41483621d1c6a8680806502 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Mon, 6 Feb 2023 22:35:32 +0100 Subject: [PATCH 0469/1802] fix(logger): fix REPORT_CB/2 CRASH logs --- apps/emqx/src/emqx_logger_textfmt.erl | 15 ++++++++++++++- changes/v5.0.17/fix-9923.en.md | 1 + changes/v5.0.17/fix-9923.zh.md | 1 + 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 changes/v5.0.17/fix-9923.en.md create mode 100644 changes/v5.0.17/fix-9923.zh.md diff --git a/apps/emqx/src/emqx_logger_textfmt.erl b/apps/emqx/src/emqx_logger_textfmt.erl index fb27681b8..3dce8a2ec 100644 --- a/apps/emqx/src/emqx_logger_textfmt.erl +++ b/apps/emqx/src/emqx_logger_textfmt.erl @@ -23,7 +23,14 @@ check_config(X) -> logger_formatter:check_config(X). format(#{msg := {report, ReportMap}, meta := Meta} = Event, Config) when is_map(ReportMap) -> - Report = enrich_report(ReportMap, Meta), + ReportList = enrich_report(ReportMap, Meta), + Report = + case is_list_report_acceptable(Meta) of + true -> + ReportList; + false -> + maps:from_list(ReportList) + end, logger_formatter:format(Event#{msg := {report, Report}}, Config); format(#{msg := {string, String}} = Event, Config) -> format(Event#{msg => {"~ts ", [String]}}, Config); @@ -34,6 +41,11 @@ format(#{msg := Msg0, meta := Meta} = Event, Config) -> Msg3 = enrich_topic(Msg2, Meta), logger_formatter:format(Event#{msg := Msg3}, Config). +is_list_report_acceptable(#{report_cb := Cb}) -> + Cb =:= fun logger:format_otp_report/1 orelse Cb =:= fun logger:format_report/1; +is_list_report_acceptable(_) -> + false. + enrich_report(ReportRaw, Meta) -> %% clientid and peername always in emqx_conn's process metadata. %% topic can be put in meta using ?SLOG/3, or put in msg's report by ?SLOG/2 @@ -47,6 +59,7 @@ enrich_report(ReportRaw, Meta) -> MFA = maps:get(mfa, Meta, undefined), Line = maps:get(line, Meta, undefined), Msg = maps:get(msg, ReportRaw, undefined), + %% turn it into a list so that the order of the fields is determined lists:foldl( fun ({_, undefined}, Acc) -> Acc; diff --git a/changes/v5.0.17/fix-9923.en.md b/changes/v5.0.17/fix-9923.en.md new file mode 100644 index 000000000..fa6926893 --- /dev/null +++ b/changes/v5.0.17/fix-9923.en.md @@ -0,0 +1 @@ +Fix REPORT_CB/2 CRASH error logs when erros happen during boot-up or shutdown. diff --git a/changes/v5.0.17/fix-9923.zh.md b/changes/v5.0.17/fix-9923.zh.md new file mode 100644 index 000000000..610b14b96 --- /dev/null +++ b/changes/v5.0.17/fix-9923.zh.md @@ -0,0 +1 @@ +修复在启动和关闭过程中发生错误时,日志中的 REPORT_CB/2 CRASH 错误。 From d487299b753c42d415ad160f797651e63ed17cf4 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Mon, 6 Feb 2023 23:00:17 +0100 Subject: [PATCH 0470/1802] refactor: remove dead code --- apps/emqx_authn/i18n/emqx_authn_jwt_i18n.conf | 14 -------------- apps/emqx_authn/src/emqx_authn.app.src | 2 +- .../emqx_authn/src/simple_authn/emqx_authn_jwt.erl | 6 +----- 3 files changed, 2 insertions(+), 20 deletions(-) diff --git a/apps/emqx_authn/i18n/emqx_authn_jwt_i18n.conf b/apps/emqx_authn/i18n/emqx_authn_jwt_i18n.conf index a420dd7d9..eb243c0cb 100644 --- a/apps/emqx_authn/i18n/emqx_authn_jwt_i18n.conf +++ b/apps/emqx_authn/i18n/emqx_authn_jwt_i18n.conf @@ -197,20 +197,6 @@ Authentication will verify that the value of claims in the JWT (taken from the P } } - ssl_disable { - desc { - en: """SSL configuration.""" - zh: """SSL 配置。""" - } - } - - ssl_enable { - desc { - en: """SSL configuration.""" - zh: """SSL 配置。""" - } - } - acl_claim_name { desc { en: """JWT claim name to use for getting ACL rules.""" diff --git a/apps/emqx_authn/src/emqx_authn.app.src b/apps/emqx_authn/src/emqx_authn.app.src index 7f01d94c0..0b5b0dedc 100644 --- a/apps/emqx_authn/src/emqx_authn.app.src +++ b/apps/emqx_authn/src/emqx_authn.app.src @@ -1,7 +1,7 @@ %% -*- mode: erlang -*- {application, emqx_authn, [ {description, "EMQX Authentication"}, - {vsn, "0.1.12"}, + {vsn, "0.1.13"}, {modules, []}, {registered, [emqx_authn_sup, emqx_authn_registry]}, {applications, [kernel, stdlib, emqx_resource, emqx_connector, ehttpc, epgsql, mysql, jose]}, diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl index 852139875..021d4a2d1 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl @@ -91,11 +91,7 @@ desc('public-key') -> ?DESC('public-key'); desc('jwks') -> ?DESC('jwks'); -desc(ssl_disable) -> - ?DESC(ssl_disable); -desc(ssl_enable) -> - ?DESC(ssl_enable); -desc(_) -> +desc(undefined) -> undefined. common_fields() -> From 537ff305505c367b92b60aa9c434bdce5a17b880 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Tue, 7 Feb 2023 11:40:17 +0100 Subject: [PATCH 0471/1802] chore: update code comment --- apps/emqx_authn/src/emqx_authn_schema.erl | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/emqx_authn/src/emqx_authn_schema.erl b/apps/emqx_authn/src/emqx_authn_schema.erl index f4a966d89..112ea2076 100644 --- a/apps/emqx_authn/src/emqx_authn_schema.erl +++ b/apps/emqx_authn/src/emqx_authn_schema.erl @@ -105,8 +105,12 @@ select_union_member(Value, _Providers) -> throw(#{reason => "not_a_struct", value => Value}). try_select_union_member(Module, Value) -> - %% some modules have union_member_selector/1 exported to help selectin the sub-types - %% emqx_authn_http, emqx_authn_jwt, emqx_authn_mongodb and emqx_authn_redis + %% some modules have `union_member_selector/1' exported to help selecting + %% the sub-types, they are: + %% emqx_authn_http + %% emqx_authn_jwt + %% emqx_authn_mongodb + %% emqx_authn_redis try Module:union_member_selector({value, Value}) catch From 86f3f5787fc8b5c84fe3f5c392083842ea44cdd8 Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Fri, 3 Feb 2023 15:20:30 +0100 Subject: [PATCH 0472/1802] feat: allow to manually re-connect disconected bridge --- apps/emqx/priv/bpapi.versions | 1 + apps/emqx_bridge/src/emqx_bridge.app.src | 2 +- apps/emqx_bridge/src/emqx_bridge_api.erl | 41 ++++-- apps/emqx_bridge/src/emqx_bridge_resource.erl | 6 +- .../src/proto/emqx_bridge_proto_v1.erl | 4 + .../src/proto/emqx_bridge_proto_v2.erl | 122 ++++++++++++++++++ .../test/emqx_bridge_api_SUITE.erl | 40 +++++- apps/emqx_resource/src/emqx_resource.app.src | 2 +- .../src/emqx_resource_manager.erl | 8 +- changes/v5.0.17/feat-9910.en.md | 1 + changes/v5.0.17/feat-9910.zh.md | 1 + 11 files changed, 207 insertions(+), 21 deletions(-) create mode 100644 apps/emqx_bridge/src/proto/emqx_bridge_proto_v2.erl create mode 100644 changes/v5.0.17/feat-9910.en.md create mode 100644 changes/v5.0.17/feat-9910.zh.md diff --git a/apps/emqx/priv/bpapi.versions b/apps/emqx/priv/bpapi.versions index 1a1bac140..769145722 100644 --- a/apps/emqx/priv/bpapi.versions +++ b/apps/emqx/priv/bpapi.versions @@ -3,6 +3,7 @@ {emqx_authn,1}. {emqx_authz,1}. {emqx_bridge,1}. +{emqx_bridge,2}. {emqx_broker,1}. {emqx_cm,1}. {emqx_conf,1}. diff --git a/apps/emqx_bridge/src/emqx_bridge.app.src b/apps/emqx_bridge/src/emqx_bridge.app.src index ca0001319..0d4b552ee 100644 --- a/apps/emqx_bridge/src/emqx_bridge.app.src +++ b/apps/emqx_bridge/src/emqx_bridge.app.src @@ -1,7 +1,7 @@ %% -*- mode: erlang -*- {application, emqx_bridge, [ {description, "EMQX bridges"}, - {vsn, "0.1.10"}, + {vsn, "0.1.11"}, {registered, []}, {mod, {emqx_bridge_app, []}}, {applications, [ diff --git a/apps/emqx_bridge/src/emqx_bridge_api.erl b/apps/emqx_bridge/src/emqx_bridge_api.erl index dce99646f..8711535ce 100644 --- a/apps/emqx_bridge/src/emqx_bridge_api.erl +++ b/apps/emqx_bridge/src/emqx_bridge_api.erl @@ -413,8 +413,9 @@ schema("/bridges/:id/:operation") -> ], responses => #{ 204 => <<"Operation success">>, - 503 => error_schema('SERVICE_UNAVAILABLE', "Service unavailable"), - 400 => error_schema('INVALID_ID', "Bad bridge ID") + 400 => error_schema('INVALID_ID', "Bad bridge ID"), + 501 => error_schema('NOT_IMPLEMENTED', "Not Implemented"), + 503 => error_schema('SERVICE_UNAVAILABLE', "Service unavailable") } } }; @@ -434,6 +435,7 @@ schema("/nodes/:node/bridges/:id/:operation") -> 204 => <<"Operation success">>, 400 => error_schema('INVALID_ID', "Bad bridge ID"), 403 => error_schema('FORBIDDEN_REQUEST', "forbidden operation"), + 501 => error_schema('NOT_IMPLEMENTED', "Not Implemented"), 503 => error_schema('SERVICE_UNAVAILABLE', "Service unavailable") } } @@ -640,10 +642,12 @@ lookup_from_local_node(BridgeType, BridgeName) -> end ). +node_operation_func(<<"start">>) -> start_bridge_to_node; node_operation_func(<<"stop">>) -> stop_bridge_to_node; node_operation_func(<<"restart">>) -> restart_bridge_to_node; node_operation_func(_) -> invalid. +operation_func(<<"start">>) -> start; operation_func(<<"stop">>) -> stop; operation_func(<<"restart">>) -> restart; operation_func(_) -> invalid. @@ -656,11 +660,14 @@ operation_to_all_nodes(Nodes, OperFunc, BridgeType, BridgeName) -> RpcFunc = case OperFunc of restart -> restart_bridges_to_all_nodes; + start -> start_bridges_to_all_nodes; stop -> stop_bridges_to_all_nodes end, - case is_ok(emqx_bridge_proto_v1:RpcFunc(Nodes, BridgeType, BridgeName)) of + case is_ok(do_bpapi_call(RpcFunc, [Nodes, BridgeType, BridgeName])) of {ok, _} -> {204}; + {error, not_implemented} -> + {501}; {error, [timeout | _]} -> {503, error_msg('SERVICE_UNAVAILABLE', <<"request timeout">>)}; {error, ErrL} -> @@ -858,6 +865,8 @@ unpack_bridge_conf(Type, PackedConf) -> #{<<"foo">> := RawConf} = maps:get(bin(Type), Bridges), RawConf. +is_ok(Error = {error, _}) -> + Error; is_ok(ResL) -> case lists:filter( @@ -899,13 +908,11 @@ bin(S) when is_binary(S) -> call_operation(Node, OperFunc, BridgeType, BridgeName) -> case emqx_misc:safe_to_existing_atom(Node, utf8) of {ok, TargetNode} -> - case - emqx_bridge_proto_v1:OperFunc( - TargetNode, BridgeType, BridgeName - ) - of + case do_bpapi_call(TargetNode, OperFunc, [TargetNode, BridgeType, BridgeName]) of ok -> {204}; + {error, not_implemented} -> + {501}; {error, timeout} -> {503, error_msg('SERVICE_UNAVAILABLE', <<"request timeout">>)}; {error, {start_pool_failed, Name, Reason}} -> @@ -926,6 +933,24 @@ call_operation(Node, OperFunc, BridgeType, BridgeName) -> {400, error_msg('INVALID_NODE', <<"invalid node">>)} end. +do_bpapi_call(Call, Args) -> + do_bpapi_call_vsn(emqx_bpapi:supported_version(emqx_bridge), Call, Args). + +do_bpapi_call(Node, Call, Args) -> + do_bpapi_call_vsn(emqx_bpapi:supported_version(Node, emqx_bridge), Call, Args). + +do_bpapi_call_vsn(SupportedVersion, Call, Args) -> + case lists:member(SupportedVersion, supported_versions(Call)) of + true -> + apply(emqx_bridge_proto_v2, Call, Args); + false -> + {error, not_implemented} + end. + +supported_versions(start_bridge_to_node) -> [2]; +supported_versions(start_bridges_to_all_nodes) -> [2]; +supported_versions(_Call) -> [1, 2]. + redact(Term) -> emqx_misc:redact(Term). diff --git a/apps/emqx_bridge/src/emqx_bridge_resource.erl b/apps/emqx_bridge/src/emqx_bridge_resource.erl index d228f2281..6cfe40bc3 100644 --- a/apps/emqx_bridge/src/emqx_bridge_resource.erl +++ b/apps/emqx_bridge/src/emqx_bridge_resource.erl @@ -14,7 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- -module(emqx_bridge_resource). --include_lib("emqx/include/emqx.hrl"). + -include_lib("emqx/include/logger.hrl"). -export([ @@ -38,6 +38,7 @@ update/2, update/3, update/4, + start/2, stop/2, restart/2, reset_metrics/1 @@ -121,6 +122,9 @@ reset_metrics(ResourceId) -> stop(Type, Name) -> emqx_resource:stop(resource_id(Type, Name)). +start(Type, Name) -> + emqx_resource:start(resource_id(Type, Name)). + %% we don't provide 'start', as we want an already started bridge to be restarted. restart(Type, Name) -> emqx_resource:restart(resource_id(Type, Name)). diff --git a/apps/emqx_bridge/src/proto/emqx_bridge_proto_v1.erl b/apps/emqx_bridge/src/proto/emqx_bridge_proto_v1.erl index 52790ca42..88554893b 100644 --- a/apps/emqx_bridge/src/proto/emqx_bridge_proto_v1.erl +++ b/apps/emqx_bridge/src/proto/emqx_bridge_proto_v1.erl @@ -20,6 +20,7 @@ -export([ introduced_in/0, + deprecated_since/0, list_bridges/1, restart_bridge_to_node/3, @@ -36,6 +37,9 @@ introduced_in() -> "5.0.0". +deprecated_since() -> + "5.0.17". + -spec list_bridges(node()) -> list() | emqx_rpc:badrpc(). list_bridges(Node) -> rpc:call(Node, emqx_bridge, list, [], ?TIMEOUT). diff --git a/apps/emqx_bridge/src/proto/emqx_bridge_proto_v2.erl b/apps/emqx_bridge/src/proto/emqx_bridge_proto_v2.erl new file mode 100644 index 000000000..0fd733380 --- /dev/null +++ b/apps/emqx_bridge/src/proto/emqx_bridge_proto_v2.erl @@ -0,0 +1,122 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_bridge_proto_v2). + +-behaviour(emqx_bpapi). + +-export([ + introduced_in/0, + + list_bridges/1, + restart_bridge_to_node/3, + start_bridge_to_node/3, + stop_bridge_to_node/3, + lookup_from_all_nodes/3, + restart_bridges_to_all_nodes/3, + start_bridges_to_all_nodes/3, + stop_bridges_to_all_nodes/3 +]). + +-include_lib("emqx/include/bpapi.hrl"). + +-define(TIMEOUT, 15000). + +introduced_in() -> + "5.0.17". + +-spec list_bridges(node()) -> list() | emqx_rpc:badrpc(). +list_bridges(Node) -> + rpc:call(Node, emqx_bridge, list, [], ?TIMEOUT). + +-type key() :: atom() | binary() | [byte()]. + +-spec restart_bridge_to_node(node(), key(), key()) -> + term(). +restart_bridge_to_node(Node, BridgeType, BridgeName) -> + rpc:call( + Node, + emqx_bridge_resource, + restart, + [BridgeType, BridgeName], + ?TIMEOUT + ). + +-spec start_bridge_to_node(node(), key(), key()) -> + term(). +start_bridge_to_node(Node, BridgeType, BridgeName) -> + rpc:call( + Node, + emqx_bridge_resource, + start, + [BridgeType, BridgeName], + ?TIMEOUT + ). + +-spec stop_bridge_to_node(node(), key(), key()) -> + term(). +stop_bridge_to_node(Node, BridgeType, BridgeName) -> + rpc:call( + Node, + emqx_bridge_resource, + stop, + [BridgeType, BridgeName], + ?TIMEOUT + ). + +-spec restart_bridges_to_all_nodes([node()], key(), key()) -> + emqx_rpc:erpc_multicall(). +restart_bridges_to_all_nodes(Nodes, BridgeType, BridgeName) -> + erpc:multicall( + Nodes, + emqx_bridge_resource, + restart, + [BridgeType, BridgeName], + ?TIMEOUT + ). + +-spec start_bridges_to_all_nodes([node()], key(), key()) -> + emqx_rpc:erpc_multicall(). +start_bridges_to_all_nodes(Nodes, BridgeType, BridgeName) -> + erpc:multicall( + Nodes, + emqx_bridge_resource, + start, + [BridgeType, BridgeName], + ?TIMEOUT + ). + +-spec stop_bridges_to_all_nodes([node()], key(), key()) -> + emqx_rpc:erpc_multicall(). +stop_bridges_to_all_nodes(Nodes, BridgeType, BridgeName) -> + erpc:multicall( + Nodes, + emqx_bridge_resource, + stop, + [BridgeType, BridgeName], + ?TIMEOUT + ). + +-spec lookup_from_all_nodes([node()], key(), key()) -> + emqx_rpc:erpc_multicall(). +lookup_from_all_nodes(Nodes, BridgeType, BridgeName) -> + erpc:multicall( + Nodes, + emqx_bridge_api, + lookup_from_local_node, + [BridgeType, BridgeName], + ?TIMEOUT + ). diff --git a/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl b/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl index 5cb78d3ba..bf09cf7f3 100644 --- a/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl +++ b/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl @@ -82,11 +82,19 @@ end_per_suite(_Config) -> emqx_mgmt_api_test_util:end_suite([emqx_rule_engine, emqx_bridge]), ok. +init_per_testcase(t_bad_bpapi_vsn, Config) -> + meck:new(emqx_bpapi, [passthrough]), + meck:expect(emqx_bpapi, supported_version, 1, 1), + meck:expect(emqx_bpapi, supported_version, 2, 1), + init_per_testcase(commong, Config); init_per_testcase(_, Config) -> {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000), {Port, Sock, Acceptor} = start_http_server(fun handle_fun_200_ok/2), [{port, Port}, {sock, Sock}, {acceptor, Acceptor} | Config]. +end_per_testcase(t_bad_bpapi_vsn, Config) -> + meck:unload([emqx_bpapi]), + end_per_testcase(commong, Config); end_per_testcase(_, Config) -> Sock = ?config(sock, Config), Acceptor = ?config(acceptor, Config), @@ -440,6 +448,20 @@ t_start_stop_bridges_node(Config) -> t_start_stop_bridges_cluster(Config) -> do_start_stop_bridges(cluster, Config). +t_bad_bpapi_vsn(Config) -> + Port = ?config(port, Config), + URL1 = ?URL(Port, "abc"), + Name = <<"t_bad_bpapi_vsn">>, + {ok, 201, _Bridge} = request( + post, + uri(["bridges"]), + ?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, Name) + ), + BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE, Name), + {ok, 501, <<>>} = request(post, operation_path(cluster, start, BridgeID), <<"">>), + {ok, 501, <<>>} = request(post, operation_path(node, start, BridgeID), <<"">>), + ok. + do_start_stop_bridges(Type, Config) -> %% assert we there's no bridges at first {ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []), @@ -463,21 +485,25 @@ do_start_stop_bridges(Type, Config) -> } = jsx:decode(Bridge), BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE, Name), %% stop it - {ok, 200, <<>>} = request(post, operation_path(Type, stop, BridgeID), <<"">>), + {ok, 204, <<>>} = request(post, operation_path(Type, stop, BridgeID), <<"">>), {ok, 200, Bridge2} = request(get, uri(["bridges", BridgeID]), []), ?assertMatch(#{<<"status">> := <<"stopped">>}, jsx:decode(Bridge2)), %% start again - {ok, 200, <<>>} = request(post, operation_path(Type, restart, BridgeID), <<"">>), + {ok, 204, <<>>} = request(post, operation_path(Type, start, BridgeID), <<"">>), {ok, 200, Bridge3} = request(get, uri(["bridges", BridgeID]), []), ?assertMatch(#{<<"status">> := <<"connected">>}, jsx:decode(Bridge3)), + %% start a started bridge + {ok, 204, <<>>} = request(post, operation_path(Type, start, BridgeID), <<"">>), + {ok, 200, Bridge3_1} = request(get, uri(["bridges", BridgeID]), []), + ?assertMatch(#{<<"status">> := <<"connected">>}, jsx:decode(Bridge3_1)), %% restart an already started bridge - {ok, 200, <<>>} = request(post, operation_path(Type, restart, BridgeID), <<"">>), + {ok, 204, <<>>} = request(post, operation_path(Type, restart, BridgeID), <<"">>), {ok, 200, Bridge3} = request(get, uri(["bridges", BridgeID]), []), ?assertMatch(#{<<"status">> := <<"connected">>}, jsx:decode(Bridge3)), %% stop it again - {ok, 200, <<>>} = request(post, operation_path(Type, stop, BridgeID), <<"">>), + {ok, 204, <<>>} = request(post, operation_path(Type, stop, BridgeID), <<"">>), %% restart a stopped bridge - {ok, 200, <<>>} = request(post, operation_path(Type, restart, BridgeID), <<"">>), + {ok, 204, <<>>} = request(post, operation_path(Type, restart, BridgeID), <<"">>), {ok, 200, Bridge4} = request(get, uri(["bridges", BridgeID]), []), ?assertMatch(#{<<"status">> := <<"connected">>}, jsx:decode(Bridge4)), %% delete the bridge @@ -536,7 +562,7 @@ t_enable_disable_bridges(Config) -> {ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []). t_reset_bridges(Config) -> - %% assert we there's no bridges at first + %% assert there's no bridges at first {ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []), Name = ?BRIDGE_NAME, @@ -557,7 +583,7 @@ t_reset_bridges(Config) -> <<"url">> := URL1 } = jsx:decode(Bridge), BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE, Name), - {ok, 200, <<"Reset success">>} = request(put, uri(["bridges", BridgeID, "metrics/reset"]), []), + {ok, 204, <<>>} = request(put, uri(["bridges", BridgeID, "metrics/reset"]), []), %% delete the bridge {ok, 204, <<>>} = request(delete, uri(["bridges", BridgeID]), []), diff --git a/apps/emqx_resource/src/emqx_resource.app.src b/apps/emqx_resource/src/emqx_resource.app.src index 6bd9fd213..4618e94a6 100644 --- a/apps/emqx_resource/src/emqx_resource.app.src +++ b/apps/emqx_resource/src/emqx_resource.app.src @@ -1,7 +1,7 @@ %% -*- mode: erlang -*- {application, emqx_resource, [ {description, "Manager for all external resources"}, - {vsn, "0.1.6"}, + {vsn, "0.1.7"}, {registered, []}, {mod, {emqx_resource_app, []}}, {applications, [ diff --git a/apps/emqx_resource/src/emqx_resource_manager.erl b/apps/emqx_resource/src/emqx_resource_manager.erl index db4441d88..4846c8ae6 100644 --- a/apps/emqx_resource/src/emqx_resource_manager.erl +++ b/apps/emqx_resource/src/emqx_resource_manager.erl @@ -17,7 +17,6 @@ -behaviour(gen_statem). -include("emqx_resource.hrl"). --include("emqx_resource_utils.hrl"). -include_lib("emqx/include/logger.hrl"). % API @@ -327,8 +326,11 @@ handle_event({call, From}, set_resource_status_connecting, _State, Data) -> handle_event({call, From}, restart, _State, Data) -> _ = stop_resource(Data), start_resource(Data, From); -% Called when the resource is to be started -handle_event({call, From}, start, stopped, Data) -> +% Called when the resource is to be started (also used for manual reconnect) +handle_event({call, From}, start, State, Data) when + State =:= stopped orelse + State =:= disconnected +-> start_resource(Data, From); handle_event({call, From}, start, _State, _Data) -> {keep_state_and_data, [{reply, From, ok}]}; diff --git a/changes/v5.0.17/feat-9910.en.md b/changes/v5.0.17/feat-9910.en.md new file mode 100644 index 000000000..bf8e0e1b4 --- /dev/null +++ b/changes/v5.0.17/feat-9910.en.md @@ -0,0 +1 @@ +Add `start` operation to bridges API to allow manual reconnect after failure. diff --git a/changes/v5.0.17/feat-9910.zh.md b/changes/v5.0.17/feat-9910.zh.md new file mode 100644 index 000000000..f165a44b9 --- /dev/null +++ b/changes/v5.0.17/feat-9910.zh.md @@ -0,0 +1 @@ +在桥梁 API 中增加 `start` "操作,允许失败后手动重新连接。 From 19c2288a4b3537d654163124292d41ab4ac28f74 Mon Sep 17 00:00:00 2001 From: Erik Timan Date: Tue, 7 Feb 2023 15:42:55 +0100 Subject: [PATCH 0473/1802] feat(emqx_prometheus): expose live_connections stats to prometheus --- apps/emqx_prometheus/src/emqx_prometheus.erl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/emqx_prometheus/src/emqx_prometheus.erl b/apps/emqx_prometheus/src/emqx_prometheus.erl index a66f275f8..4712a43c8 100644 --- a/apps/emqx_prometheus/src/emqx_prometheus.erl +++ b/apps/emqx_prometheus/src/emqx_prometheus.erl @@ -219,6 +219,10 @@ emqx_collect(emqx_connections_count, Stats) -> gauge_metric(?C('connections.count', Stats)); emqx_collect(emqx_connections_max, Stats) -> gauge_metric(?C('connections.max', Stats)); +emqx_collect(emqx_live_connections_count, Stats) -> + gauge_metric(?C('live_connections.count', Stats)); +emqx_collect(emqx_live_connections_max, Stats) -> + gauge_metric(?C('live_connections.max', Stats)); %% sessions emqx_collect(emqx_sessions_count, Stats) -> gauge_metric(?C('sessions.count', Stats)); @@ -460,6 +464,8 @@ emqx_stats() -> [ emqx_connections_count, emqx_connections_max, + emqx_live_connections_count, + emqx_live_connections_max, emqx_sessions_count, emqx_sessions_max, emqx_topics_count, From 784ec0c0a524f76211f31aac60324bc479bf636a Mon Sep 17 00:00:00 2001 From: Erik Timan Date: Tue, 7 Feb 2023 15:53:40 +0100 Subject: [PATCH 0474/1802] chore: bump VSN --- apps/emqx_prometheus/src/emqx_prometheus.app.src | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx_prometheus/src/emqx_prometheus.app.src b/apps/emqx_prometheus/src/emqx_prometheus.app.src index 31f8cbfaf..013de63fb 100644 --- a/apps/emqx_prometheus/src/emqx_prometheus.app.src +++ b/apps/emqx_prometheus/src/emqx_prometheus.app.src @@ -2,7 +2,7 @@ {application, emqx_prometheus, [ {description, "Prometheus for EMQX"}, % strict semver, bump manually! - {vsn, "5.0.4"}, + {vsn, "5.0.5"}, {modules, []}, {registered, [emqx_prometheus_sup]}, {applications, [kernel, stdlib, prometheus, emqx]}, From 6113d467c19ac018a4e36c30f3f66c2d8dc9918b Mon Sep 17 00:00:00 2001 From: William Yang Date: Mon, 6 Feb 2023 09:12:59 +0100 Subject: [PATCH 0475/1802] fix(mqtt-bridge): support verify TLS wildcard cert --- apps/emqx/rebar.config | 2 +- changes/v5.0.17/fix-9916.en.md | 2 ++ changes/v5.0.17/fix-9916.zh.md | 1 + mix.exs | 2 +- rebar.config | 2 +- 5 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 changes/v5.0.17/fix-9916.en.md create mode 100644 changes/v5.0.17/fix-9916.zh.md diff --git a/apps/emqx/rebar.config b/apps/emqx/rebar.config index cad599436..7f494f76c 100644 --- a/apps/emqx/rebar.config +++ b/apps/emqx/rebar.config @@ -43,7 +43,7 @@ {meck, "0.9.2"}, {proper, "1.4.0"}, {bbmustache, "1.10.0"}, - {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.7.0-rc.1"}}} + {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.7.0"}}} ]}, {extra_src_dirs, [{"test", [recursive]}]} ]} diff --git a/changes/v5.0.17/fix-9916.en.md b/changes/v5.0.17/fix-9916.en.md new file mode 100644 index 000000000..dd6de13e5 --- /dev/null +++ b/changes/v5.0.17/fix-9916.en.md @@ -0,0 +1,2 @@ +Fix MQTT bridge fails to verify TLS wildcard server certificate. + diff --git a/changes/v5.0.17/fix-9916.zh.md b/changes/v5.0.17/fix-9916.zh.md new file mode 100644 index 000000000..3bed13e4c --- /dev/null +++ b/changes/v5.0.17/fix-9916.zh.md @@ -0,0 +1 @@ +修复MQTT桥接器无法验证TLS通配符服务器证书的问题。 diff --git a/mix.exs b/mix.exs index 18e11fdd7..11fa83646 100644 --- a/mix.exs +++ b/mix.exs @@ -60,7 +60,7 @@ defmodule EMQXUmbrella.MixProject do {:ecpool, github: "emqx/ecpool", tag: "0.5.3", override: true}, {:replayq, github: "emqx/replayq", tag: "0.3.7", override: true}, {:pbkdf2, github: "emqx/erlang-pbkdf2", tag: "2.0.4", override: true}, - {:emqtt, github: "emqx/emqtt", tag: "1.7.0-rc.2", override: true}, + {:emqtt, github: "emqx/emqtt", tag: "1.7.0", override: true}, {:rulesql, github: "emqx/rulesql", tag: "0.1.4"}, {:observer_cli, "1.7.1"}, {:system_monitor, github: "ieQu1/system_monitor", tag: "3.0.3"}, diff --git a/rebar.config b/rebar.config index 1a9d651dc..1fd795b87 100644 --- a/rebar.config +++ b/rebar.config @@ -62,7 +62,7 @@ , {ecpool, {git, "https://github.com/emqx/ecpool", {tag, "0.5.3"}}} , {replayq, {git, "https://github.com/emqx/replayq.git", {tag, "0.3.7"}}} , {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}} - , {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.7.0-rc.2"}}} + , {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.7.0"}}} , {rulesql, {git, "https://github.com/emqx/rulesql", {tag, "0.1.4"}}} , {observer_cli, "1.7.1"} % NOTE: depends on recon 2.5.x , {system_monitor, {git, "https://github.com/ieQu1/system_monitor", {tag, "3.0.3"}}} From c7535fce566af24b10edc5a72dde2cf2d9fb5ccd Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Wed, 8 Feb 2023 09:14:35 +0100 Subject: [PATCH 0476/1802] test: fix test expecting 200 instead of 204 --- .../test/emqx_bridge_impl_kafka_producer_SUITE.erl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib-ee/emqx_ee_bridge/test/emqx_bridge_impl_kafka_producer_SUITE.erl b/lib-ee/emqx_ee_bridge/test/emqx_bridge_impl_kafka_producer_SUITE.erl index 10a6d8d9a..17484b948 100644 --- a/lib-ee/emqx_ee_bridge/test/emqx_bridge_impl_kafka_producer_SUITE.erl +++ b/lib-ee/emqx_ee_bridge/test/emqx_bridge_impl_kafka_producer_SUITE.erl @@ -346,9 +346,9 @@ kafka_bridge_rest_api_helper(Config) -> {ok, 204, _} = show(http_put(show(BridgesPartsOpDisable), #{})), {ok, 204, _} = show(http_put(show(BridgesPartsOpEnable), #{})), {ok, 204, _} = show(http_put(show(BridgesPartsOpEnable), #{})), - {ok, 200, _} = show(http_post(show(BridgesPartsOpStop), #{})), - {ok, 200, _} = show(http_post(show(BridgesPartsOpStop), #{})), - {ok, 200, _} = show(http_post(show(BridgesPartsOpRestart), #{})), + {ok, 204, _} = show(http_post(show(BridgesPartsOpStop), #{})), + {ok, 204, _} = show(http_post(show(BridgesPartsOpStop), #{})), + {ok, 204, _} = show(http_post(show(BridgesPartsOpRestart), #{})), %% Cleanup {ok, 204, _} = show(http_delete(BridgesPartsIdDeleteAlsoActions)), false = MyKafkaBridgeExists(), From c407ee3c3b10074442b8e3fc763a627bf50d5ce3 Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Wed, 8 Feb 2023 09:19:20 +0100 Subject: [PATCH 0477/1802] style: fix zn changelog --- changes/v5.0.17/feat-9910.zh.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changes/v5.0.17/feat-9910.zh.md b/changes/v5.0.17/feat-9910.zh.md index f165a44b9..3e35fff19 100644 --- a/changes/v5.0.17/feat-9910.zh.md +++ b/changes/v5.0.17/feat-9910.zh.md @@ -1 +1 @@ -在桥梁 API 中增加 `start` "操作,允许失败后手动重新连接。 +在桥接 API 中增加 `start` 操作,允许失败后手动重新连接。 From d8d06a260ff1df921966166736b88bd7436e8274 Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Mon, 6 Feb 2023 19:24:56 +0300 Subject: [PATCH 0478/1802] test(buffer): add test on inflight overflow w/ async queries This testcase should verify that the buffer will retry all inflight queries failed with recoverable errors + flush all outstanding queries. Co-authored-by: ieQu1 <99872536+ieQu1@users.noreply.github.com> --- .../test/emqx_connector_demo.erl | 33 ++++++++ .../test/emqx_resource_SUITE.erl | 84 +++++++++++++++---- 2 files changed, 103 insertions(+), 14 deletions(-) diff --git a/apps/emqx_resource/test/emqx_connector_demo.erl b/apps/emqx_resource/test/emqx_connector_demo.erl index 4e3423808..1d96fa083 100644 --- a/apps/emqx_resource/test/emqx_connector_demo.erl +++ b/apps/emqx_resource/test/emqx_connector_demo.erl @@ -134,6 +134,17 @@ on_query(_InstId, get_counter, #{pid := Pid}) -> {ReqRef, Num} -> {ok, Num} after 1000 -> {error, timeout} + end; +on_query(_InstId, {sleep, For}, #{pid := Pid}) -> + ?tp(connector_demo_sleep, #{mode => sync, for => For}), + ReqRef = make_ref(), + From = {self(), ReqRef}, + Pid ! {From, {sleep, For}}, + receive + {ReqRef, Result} -> + Result + after 1000 -> + {error, timeout} end. on_query_async(_InstId, {inc_counter, N}, ReplyFun, #{pid := Pid}) -> @@ -147,6 +158,10 @@ on_query_async(_InstId, block_now, ReplyFun, #{pid := Pid}) -> {ok, Pid}; on_query_async(_InstId, {big_payload, Payload}, ReplyFun, #{pid := Pid}) -> Pid ! {big_payload, Payload, ReplyFun}, + {ok, Pid}; +on_query_async(_InstId, {sleep, For}, ReplyFun, #{pid := Pid}) -> + ?tp(connector_demo_sleep, #{mode => async, for => For}), + Pid ! {{sleep, For}, ReplyFun}, {ok, Pid}. on_batch_query(InstId, BatchReq, State) -> @@ -283,10 +298,28 @@ counter_loop( State; {{FromPid, ReqRef}, get} -> FromPid ! {ReqRef, Num}, + State; + {{sleep, _} = SleepQ, ReplyFun} -> + apply_reply(ReplyFun, handle_query(async, SleepQ, Status)), + State; + {{FromPid, ReqRef}, {sleep, _} = SleepQ} -> + FromPid ! {ReqRef, handle_query(sync, SleepQ, Status)}, State end, counter_loop(NewState). +handle_query(Mode, {sleep, For} = Query, Status) -> + ok = timer:sleep(For), + Result = + case Status of + running -> ok; + blocked -> {error, {recoverable_error, blocked}} + end, + ?tp(connector_demo_sleep_handled, #{ + mode => Mode, query => Query, slept => For, result => Result + }), + Result. + maybe_register(Name, Pid, true) -> ct:pal("---- Register Name: ~p", [Name]), ct:pal("---- whereis(): ~p", [whereis(Name)]), diff --git a/apps/emqx_resource/test/emqx_resource_SUITE.erl b/apps/emqx_resource/test/emqx_resource_SUITE.erl index bc0331d02..a042bfb67 100644 --- a/apps/emqx_resource/test/emqx_resource_SUITE.erl +++ b/apps/emqx_resource/test/emqx_resource_SUITE.erl @@ -1452,6 +1452,61 @@ t_retry_async_inflight(_Config) -> ), ok. +t_retry_async_inflight_full(_Config) -> + ResumeInterval = 1_000, + AsyncInflightWindow = 5, + emqx_connector_demo:set_callback_mode(async_if_possible), + {ok, _} = emqx_resource:create( + ?ID, + ?DEFAULT_RESOURCE_GROUP, + ?TEST_RESOURCE, + #{name => ?FUNCTION_NAME}, + #{ + query_mode => async, + async_inflight_window => AsyncInflightWindow, + batch_size => 1, + batch_time => 20, + worker_pool_size => 1, + resume_interval => ResumeInterval + } + ), + ?check_trace( + #{timetrap => 15_000}, + begin + %% block + ok = emqx_resource:simple_sync_query(?ID, block), + + {ok, {ok, _}} = + ?wait_async_action( + inc_counter_in_parallel( + AsyncInflightWindow * 2, + fun() -> + For = (ResumeInterval div 4) + rand:uniform(ResumeInterval div 4), + {sleep, For} + end, + #{async_reply_fun => {fun(Res) -> ct:pal("Res = ~p", [Res]) end, []}} + ), + #{?snk_kind := buffer_worker_flush_but_inflight_full}, + ResumeInterval * 2 + ), + + %% will reply with success after the resource is healed + {ok, {ok, _}} = + ?wait_async_action( + emqx_resource:simple_sync_query(?ID, resume), + #{?snk_kind := buffer_worker_enter_running} + ), + ok + end, + [ + fun(Trace) -> + ?assertMatch([#{} | _], ?of_kind(buffer_worker_flush_but_inflight_full, Trace)) + end + ] + ), + ?assertEqual(0, emqx_resource_metrics:inflight_get(?ID)), + ok. + t_retry_async_inflight_batch(_Config) -> ResumeInterval = 1_000, emqx_connector_demo:set_callback_mode(async_if_possible), @@ -2241,18 +2296,16 @@ t_expiration_retry_batch_multiple_times(_Config) -> %%------------------------------------------------------------------------------ inc_counter_in_parallel(N) -> - inc_counter_in_parallel(N, #{}). + inc_counter_in_parallel(N, {inc_counter, 1}, #{}). inc_counter_in_parallel(N, Opts0) -> + inc_counter_in_parallel(N, {inc_counter, 1}, Opts0). + +inc_counter_in_parallel(N, Query, Opts) -> Parent = self(), Pids = [ erlang:spawn(fun() -> - Opts = - case is_function(Opts0) of - true -> Opts0(); - false -> Opts0 - end, - emqx_resource:query(?ID, {inc_counter, 1}, Opts), + emqx_resource:query(?ID, maybe_apply(Query), maybe_apply(Opts)), Parent ! {complete, self()} end) || _ <- lists:seq(1, N) @@ -2267,16 +2320,11 @@ inc_counter_in_parallel(N, Opts0) -> ], ok. -inc_counter_in_parallel_increasing(N, StartN, Opts0) -> +inc_counter_in_parallel_increasing(N, StartN, Opts) -> Parent = self(), Pids = [ erlang:spawn(fun() -> - Opts = - case is_function(Opts0) of - true -> Opts0(); - false -> Opts0 - end, - emqx_resource:query(?ID, {inc_counter, M}, Opts), + emqx_resource:query(?ID, {inc_counter, M}, maybe_apply(Opts)), Parent ! {complete, self()} end) || M <- lists:seq(StartN, StartN + N - 1) @@ -2290,6 +2338,14 @@ inc_counter_in_parallel_increasing(N, StartN, Opts0) -> || Pid <- Pids ]. +maybe_apply(FunOrTerm) -> + maybe_apply(FunOrTerm, []). + +maybe_apply(Fun, Args) when is_function(Fun) -> + erlang:apply(Fun, Args); +maybe_apply(Term, _Args) -> + Term. + bin_config() -> <<"\"name\": \"test_resource\"">>. From c6fc0ec8cd404771d332408605d76e07a0be1ab7 Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Wed, 8 Feb 2023 14:03:40 +0300 Subject: [PATCH 0479/1802] fix(bufworker): do not avoid retry if inflight table is full Otherwise there's no other piece of code that would retry the inflight queries in that case. --- .../src/emqx_resource_buffer_worker.erl | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/apps/emqx_resource/src/emqx_resource_buffer_worker.erl b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl index 29fe79d09..b25725c41 100644 --- a/apps/emqx_resource/src/emqx_resource_buffer_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl @@ -334,12 +334,7 @@ resume_from_blocked(Data) -> {single, Ref, Query} -> %% We retry msgs in inflight window sync, as if we send them %% async, they will be appended to the end of inflight window again. - case is_inflight_full(InflightTID) of - true -> - {keep_state, Data}; - false -> - retry_inflight_sync(Ref, Query, Data) - end; + retry_inflight_sync(Ref, Query, Data); {batch, Ref, NotExpired, Expired} -> update_inflight_item(InflightTID, Ref, NotExpired), NumExpired = length(Expired), @@ -347,12 +342,7 @@ resume_from_blocked(Data) -> NumExpired > 0 andalso ?tp(buffer_worker_retry_expired, #{expired => Expired}), %% We retry msgs in inflight window sync, as if we send them %% async, they will be appended to the end of inflight window again. - case is_inflight_full(InflightTID) of - true -> - {keep_state, Data}; - false -> - retry_inflight_sync(Ref, NotExpired, Data) - end + retry_inflight_sync(Ref, NotExpired, Data) end. retry_inflight_sync(Ref, QueryOrBatch, Data0) -> From 81b1bab11e70595395b6c775841500be841292e7 Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Wed, 8 Feb 2023 14:10:08 +0300 Subject: [PATCH 0480/1802] chore: bump `emqx_resource` version to 0.1.7 Also add the changelog entry. --- apps/emqx_resource/src/emqx_resource.app.src | 2 +- changes/v5.0.17/fix-9922.en.md | 1 + changes/v5.0.17/fix-9922.zh.md | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 changes/v5.0.17/fix-9922.en.md create mode 100644 changes/v5.0.17/fix-9922.zh.md diff --git a/apps/emqx_resource/src/emqx_resource.app.src b/apps/emqx_resource/src/emqx_resource.app.src index 6bd9fd213..4618e94a6 100644 --- a/apps/emqx_resource/src/emqx_resource.app.src +++ b/apps/emqx_resource/src/emqx_resource.app.src @@ -1,7 +1,7 @@ %% -*- mode: erlang -*- {application, emqx_resource, [ {description, "Manager for all external resources"}, - {vsn, "0.1.6"}, + {vsn, "0.1.7"}, {registered, []}, {mod, {emqx_resource_app, []}}, {applications, [ diff --git a/changes/v5.0.17/fix-9922.en.md b/changes/v5.0.17/fix-9922.en.md new file mode 100644 index 000000000..b58a8a766 --- /dev/null +++ b/changes/v5.0.17/fix-9922.en.md @@ -0,0 +1 @@ +Fix the issue with the bridge resource buffer where it might become stuck if enough async queries fill the inflight window full before failing with retryable errors. diff --git a/changes/v5.0.17/fix-9922.zh.md b/changes/v5.0.17/fix-9922.zh.md new file mode 100644 index 000000000..cf50b55e4 --- /dev/null +++ b/changes/v5.0.17/fix-9922.zh.md @@ -0,0 +1 @@ +修复桥接资源缓冲区的问题,如果足够多的异步查询在失败并出现可重试错误之前将机上窗口填满,则可能会卡住。 From 502f62e18dc17597ecae175ceac4a6d9d7572179 Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Wed, 8 Feb 2023 13:19:41 +0100 Subject: [PATCH 0481/1802] fix: try old 'restart' behavior if 'start' is not implemented --- apps/emqx_bridge/src/emqx_bridge_api.erl | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/apps/emqx_bridge/src/emqx_bridge_api.erl b/apps/emqx_bridge/src/emqx_bridge_api.erl index 8711535ce..ebb529904 100644 --- a/apps/emqx_bridge/src/emqx_bridge_api.erl +++ b/apps/emqx_bridge/src/emqx_bridge_api.erl @@ -642,14 +642,14 @@ lookup_from_local_node(BridgeType, BridgeName) -> end ). +node_operation_func(<<"restart">>) -> restart_bridge_to_node; node_operation_func(<<"start">>) -> start_bridge_to_node; node_operation_func(<<"stop">>) -> stop_bridge_to_node; -node_operation_func(<<"restart">>) -> restart_bridge_to_node; node_operation_func(_) -> invalid. +operation_func(<<"restart">>) -> restart; operation_func(<<"start">>) -> start; operation_func(<<"stop">>) -> stop; -operation_func(<<"restart">>) -> restart; operation_func(_) -> invalid. enable_func(<<"true">>) -> enable; @@ -667,13 +667,20 @@ operation_to_all_nodes(Nodes, OperFunc, BridgeType, BridgeName) -> {ok, _} -> {204}; {error, not_implemented} -> - {501}; + %% As of now this can only happen when we call 'start' on nodes + %% that run on an older proto version. + maybe_try_restart(Nodes, OperFunc, BridgeType, BridgeName); {error, [timeout | _]} -> {503, error_msg('SERVICE_UNAVAILABLE', <<"request timeout">>)}; {error, ErrL} -> {500, error_msg('INTERNAL_ERROR', ErrL)} end. +maybe_try_restart(Nodes, start, BridgeType, BridgeName) -> + operation_to_all_nodes(Nodes, restart, BridgeType, BridgeName); +maybe_try_restart(_, _, _, _) -> + {501}. + ensure_bridge_created(BridgeType, BridgeName, Conf) -> case emqx_bridge:create(BridgeType, BridgeName, Conf) of {ok, _} -> ok; @@ -912,7 +919,9 @@ call_operation(Node, OperFunc, BridgeType, BridgeName) -> ok -> {204}; {error, not_implemented} -> - {501}; + %% Should only happen if we call `start` on a node that is + %% still on an older bpapi version that doesn't support it. + maybe_try_restart_node(Node, OperFunc, BridgeType, BridgeName); {error, timeout} -> {503, error_msg('SERVICE_UNAVAILABLE', <<"request timeout">>)}; {error, {start_pool_failed, Name, Reason}} -> @@ -933,6 +942,11 @@ call_operation(Node, OperFunc, BridgeType, BridgeName) -> {400, error_msg('INVALID_NODE', <<"invalid node">>)} end. +maybe_try_restart_node(Node, start_bridge_to_node, BridgeType, BridgeName) -> + call_operation(Node, restart_bridge_to_node, BridgeType, BridgeName); +maybe_try_restart_node(_, _, _, _) -> + {501}. + do_bpapi_call(Call, Args) -> do_bpapi_call_vsn(emqx_bpapi:supported_version(emqx_bridge), Call, Args). From a3fd0897bc23ea1ef06b9d904ffc657f250fd38e Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Wed, 8 Feb 2023 13:39:45 +0100 Subject: [PATCH 0482/1802] refactor: less code duplication --- apps/emqx_bridge/src/emqx_bridge_api.erl | 116 ++++++++---------- .../test/emqx_bridge_api_SUITE.erl | 50 ++++++-- 2 files changed, 89 insertions(+), 77 deletions(-) diff --git a/apps/emqx_bridge/src/emqx_bridge_api.erl b/apps/emqx_bridge/src/emqx_bridge_api.erl index ebb529904..04760f021 100644 --- a/apps/emqx_bridge/src/emqx_bridge_api.erl +++ b/apps/emqx_bridge/src/emqx_bridge_api.erl @@ -609,12 +609,12 @@ lookup_from_local_node(BridgeType, BridgeName) -> }) -> ?TRY_PARSE_ID( Id, - case operation_func(Op) of + case operation_to_all_func(Op) of invalid -> {400, error_msg('BAD_REQUEST', <<"invalid operation">>)}; OperFunc -> Nodes = mria_mnesia:running_nodes(), - operation_to_all_nodes(Nodes, OperFunc, BridgeType, BridgeName) + call_operation(all, OperFunc, [Nodes, BridgeType, BridgeName]) end ). @@ -637,7 +637,14 @@ lookup_from_local_node(BridgeType, BridgeName) -> <<"forbidden operation: bridge disabled">> )}; true -> - call_operation(Node, OperFunc, BridgeType, BridgeName) + case emqx_misc:safe_to_existing_atom(Node, utf8) of + {ok, TargetNode} -> + call_operation(TargetNode, OperFunc, [ + TargetNode, BridgeType, BridgeName + ]); + {error, _} -> + {400, error_msg('INVALID_NODE', <<"invalid node">>)} + end end end ). @@ -647,40 +654,15 @@ node_operation_func(<<"start">>) -> start_bridge_to_node; node_operation_func(<<"stop">>) -> stop_bridge_to_node; node_operation_func(_) -> invalid. -operation_func(<<"restart">>) -> restart; -operation_func(<<"start">>) -> start; -operation_func(<<"stop">>) -> stop; -operation_func(_) -> invalid. +operation_to_all_func(<<"restart">>) -> restart_bridges_to_all_nodes; +operation_to_all_func(<<"start">>) -> start_bridges_to_all_nodes; +operation_to_all_func(<<"stop">>) -> stop_bridges_to_all_nodes; +operation_to_all_func(_) -> invalid. enable_func(<<"true">>) -> enable; enable_func(<<"false">>) -> disable; enable_func(_) -> invalid. -operation_to_all_nodes(Nodes, OperFunc, BridgeType, BridgeName) -> - RpcFunc = - case OperFunc of - restart -> restart_bridges_to_all_nodes; - start -> start_bridges_to_all_nodes; - stop -> stop_bridges_to_all_nodes - end, - case is_ok(do_bpapi_call(RpcFunc, [Nodes, BridgeType, BridgeName])) of - {ok, _} -> - {204}; - {error, not_implemented} -> - %% As of now this can only happen when we call 'start' on nodes - %% that run on an older proto version. - maybe_try_restart(Nodes, OperFunc, BridgeType, BridgeName); - {error, [timeout | _]} -> - {503, error_msg('SERVICE_UNAVAILABLE', <<"request timeout">>)}; - {error, ErrL} -> - {500, error_msg('INTERNAL_ERROR', ErrL)} - end. - -maybe_try_restart(Nodes, start, BridgeType, BridgeName) -> - operation_to_all_nodes(Nodes, restart, BridgeType, BridgeName); -maybe_try_restart(_, _, _, _) -> - {501}. - ensure_bridge_created(BridgeType, BridgeName, Conf) -> case emqx_bridge:create(BridgeType, BridgeName, Conf) of {ok, _} -> ok; @@ -872,6 +854,10 @@ unpack_bridge_conf(Type, PackedConf) -> #{<<"foo">> := RawConf} = maps:get(bin(Type), Bridges), RawConf. +is_ok(ok) -> + ok; +is_ok({ok, _} = OkResult) -> + OkResult; is_ok(Error = {error, _}) -> Error; is_ok(ResL) -> @@ -912,44 +898,42 @@ bin(S) when is_atom(S) -> bin(S) when is_binary(S) -> S. -call_operation(Node, OperFunc, BridgeType, BridgeName) -> - case emqx_misc:safe_to_existing_atom(Node, utf8) of - {ok, TargetNode} -> - case do_bpapi_call(TargetNode, OperFunc, [TargetNode, BridgeType, BridgeName]) of - ok -> - {204}; - {error, not_implemented} -> - %% Should only happen if we call `start` on a node that is - %% still on an older bpapi version that doesn't support it. - maybe_try_restart_node(Node, OperFunc, BridgeType, BridgeName); - {error, timeout} -> - {503, error_msg('SERVICE_UNAVAILABLE', <<"request timeout">>)}; - {error, {start_pool_failed, Name, Reason}} -> - {503, - error_msg( - 'SERVICE_UNAVAILABLE', - bin( - io_lib:format( - "failed to start ~p pool for reason ~p", - [Name, Reason] - ) - ) - )}; - {error, Reason} -> - {500, error_msg('INTERNAL_ERROR', Reason)} - end; - {error, _} -> - {400, error_msg('INVALID_NODE', <<"invalid node">>)} +call_operation(NodeOrAll, OperFunc, Args) -> + case is_ok(do_bpapi_call(NodeOrAll, OperFunc, Args)) of + ok -> + {204}; + {ok, _} -> + {204}; + {error, not_implemented} -> + %% Should only happen if we call `start` on a node that is + %% still on an older bpapi version that doesn't support it. + maybe_try_restart(NodeOrAll, OperFunc, Args); + {error, timeout} -> + {503, error_msg('SERVICE_UNAVAILABLE', <<"request timeout">>)}; + {error, {start_pool_failed, Name, Reason}} -> + {503, + error_msg( + 'SERVICE_UNAVAILABLE', + bin( + io_lib:format( + "failed to start ~p pool for reason ~p", + [Name, Reason] + ) + ) + )}; + {error, Reason} -> + {500, error_msg('INTERNAL_ERROR', Reason)} end. -maybe_try_restart_node(Node, start_bridge_to_node, BridgeType, BridgeName) -> - call_operation(Node, restart_bridge_to_node, BridgeType, BridgeName); -maybe_try_restart_node(_, _, _, _) -> +maybe_try_restart(all, start_bridges_to_all_nodes, Args) -> + call_operation(all, restart_bridges_to_all_nodes, Args); +maybe_try_restart(Node, start_bridge_to_node, Args) -> + call_operation(Node, restart_bridge_to_node, Args); +maybe_try_restart(_, _, _) -> {501}. -do_bpapi_call(Call, Args) -> - do_bpapi_call_vsn(emqx_bpapi:supported_version(emqx_bridge), Call, Args). - +do_bpapi_call(all, Call, Args) -> + do_bpapi_call_vsn(emqx_bpapi:supported_version(emqx_bridge), Call, Args); do_bpapi_call(Node, Call, Args) -> do_bpapi_call_vsn(emqx_bpapi:supported_version(Node, emqx_bridge), Call, Args). diff --git a/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl b/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl index bf09cf7f3..d6e8708ff 100644 --- a/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl +++ b/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl @@ -82,19 +82,27 @@ end_per_suite(_Config) -> emqx_mgmt_api_test_util:end_suite([emqx_rule_engine, emqx_bridge]), ok. -init_per_testcase(t_bad_bpapi_vsn, Config) -> +init_per_testcase(t_broken_bpapi_vsn, Config) -> + meck:new(emqx_bpapi, [passthrough]), + meck:expect(emqx_bpapi, supported_version, 1, -1), + meck:expect(emqx_bpapi, supported_version, 2, -1), + init_per_testcase(commong, Config); +init_per_testcase(t_old_bpapi_vsn, Config) -> meck:new(emqx_bpapi, [passthrough]), meck:expect(emqx_bpapi, supported_version, 1, 1), meck:expect(emqx_bpapi, supported_version, 2, 1), - init_per_testcase(commong, Config); + init_per_testcase(common, Config); init_per_testcase(_, Config) -> {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000), {Port, Sock, Acceptor} = start_http_server(fun handle_fun_200_ok/2), [{port, Port}, {sock, Sock}, {acceptor, Acceptor} | Config]. -end_per_testcase(t_bad_bpapi_vsn, Config) -> +end_per_testcase(t_broken_bpapi_vsn, Config) -> meck:unload([emqx_bpapi]), - end_per_testcase(commong, Config); + end_per_testcase(common, Config); +end_per_testcase(t_old_bpapi_vsn, Config) -> + meck:unload([emqx_bpapi]), + end_per_testcase(common, Config); end_per_testcase(_, Config) -> Sock = ?config(sock, Config), Acceptor = ?config(acceptor, Config), @@ -442,13 +450,7 @@ t_cascade_delete_actions(Config) -> {ok, 204, <<>>} = request(delete, uri(["rules", RuleId]), []), ok. -t_start_stop_bridges_node(Config) -> - do_start_stop_bridges(node, Config). - -t_start_stop_bridges_cluster(Config) -> - do_start_stop_bridges(cluster, Config). - -t_bad_bpapi_vsn(Config) -> +t_broken_bpapi_vsn(Config) -> Port = ?config(port, Config), URL1 = ?URL(Port, "abc"), Name = <<"t_bad_bpapi_vsn">>, @@ -458,10 +460,36 @@ t_bad_bpapi_vsn(Config) -> ?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, Name) ), BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE, Name), + %% still works since we redirect to 'restart' {ok, 501, <<>>} = request(post, operation_path(cluster, start, BridgeID), <<"">>), {ok, 501, <<>>} = request(post, operation_path(node, start, BridgeID), <<"">>), ok. +t_old_bpapi_vsn(Config) -> + Port = ?config(port, Config), + URL1 = ?URL(Port, "abc"), + Name = <<"t_bad_bpapi_vsn">>, + {ok, 201, _Bridge} = request( + post, + uri(["bridges"]), + ?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, Name) + ), + BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE, Name), + {ok, 204, <<>>} = request(post, operation_path(cluster, stop, BridgeID), <<"">>), + {ok, 204, <<>>} = request(post, operation_path(node, stop, BridgeID), <<"">>), + %% still works since we redirect to 'restart' + {ok, 204, <<>>} = request(post, operation_path(cluster, start, BridgeID), <<"">>), + {ok, 204, <<>>} = request(post, operation_path(node, start, BridgeID), <<"">>), + {ok, 204, <<>>} = request(post, operation_path(cluster, restart, BridgeID), <<"">>), + {ok, 204, <<>>} = request(post, operation_path(node, restart, BridgeID), <<"">>), + ok. + +t_start_stop_bridges_node(Config) -> + do_start_stop_bridges(node, Config). + +t_start_stop_bridges_cluster(Config) -> + do_start_stop_bridges(cluster, Config). + do_start_stop_bridges(Type, Config) -> %% assert we there's no bridges at first {ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []), From f89ac54b1789ceab3b7a7fd44b6483c783902e6a Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Wed, 8 Feb 2023 13:40:17 +0100 Subject: [PATCH 0483/1802] style: remove stale comment --- apps/emqx_bridge/src/emqx_bridge_resource.erl | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/apps/emqx_bridge/src/emqx_bridge_resource.erl b/apps/emqx_bridge/src/emqx_bridge_resource.erl index 6cfe40bc3..d2ce7a9d5 100644 --- a/apps/emqx_bridge/src/emqx_bridge_resource.erl +++ b/apps/emqx_bridge/src/emqx_bridge_resource.erl @@ -29,19 +29,19 @@ create/2, create/3, create/4, + create_dry_run/2, recreate/2, recreate/3, - create_dry_run/2, remove/1, remove/2, remove/4, - update/2, - update/3, - update/4, + reset_metrics/1, + restart/2, start/2, stop/2, - restart/2, - reset_metrics/1 + update/2, + update/3, + update/4 ]). %% bi-directional bridge with producer/consumer or ingress/egress configs @@ -119,16 +119,15 @@ to_type_atom(Type) -> reset_metrics(ResourceId) -> emqx_resource:reset_metrics(ResourceId). +restart(Type, Name) -> + emqx_resource:restart(resource_id(Type, Name)). + stop(Type, Name) -> emqx_resource:stop(resource_id(Type, Name)). start(Type, Name) -> emqx_resource:start(resource_id(Type, Name)). -%% we don't provide 'start', as we want an already started bridge to be restarted. -restart(Type, Name) -> - emqx_resource:restart(resource_id(Type, Name)). - create(BridgeId, Conf) -> {BridgeType, BridgeName} = parse_bridge_id(BridgeId), create(BridgeType, BridgeName, Conf). From fdb802975c17f930ca28a1d413f6fdcda573d4c9 Mon Sep 17 00:00:00 2001 From: Ivan Dyachkov Date: Wed, 8 Feb 2023 15:33:35 +0100 Subject: [PATCH 0484/1802] feat: disable disksup by default --- apps/emqx/etc/vm.args.cloud | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/emqx/etc/vm.args.cloud b/apps/emqx/etc/vm.args.cloud index 9a4530ce5..34fc19777 100644 --- a/apps/emqx/etc/vm.args.cloud +++ b/apps/emqx/etc/vm.args.cloud @@ -113,3 +113,6 @@ ## Mnesia thresholds -mnesia dump_log_write_threshold 5000 -mnesia dump_log_time_threshold 60000 + +## Disable os_mon's disksup by default +-os_mon start_disksup false From 87d8c49080730767f8b634cd9aa8ce741b5d8f65 Mon Sep 17 00:00:00 2001 From: Ivan Dyachkov Date: Wed, 8 Feb 2023 15:42:51 +0100 Subject: [PATCH 0485/1802] chore: update CODEOWNERS to allow anyone to approve changelog also remove redundant overrides --- .github/CODEOWNERS | 28 ++++++++-------------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 32be1bfe1..4de5a83b3 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -2,37 +2,25 @@ * @emqx/emqx-review-board ## apps -/apps/emqx/ @emqx/emqx-review-board @lafirest @thalesmg +/apps/emqx/ @emqx/emqx-review-board @lafirest /apps/emqx_authn/ @emqx/emqx-review-board @JimMoen @savonarola /apps/emqx_authz/ @emqx/emqx-review-board @JimMoen @savonarola -/apps/emqx_auto_subscribe/ @emqx/emqx-review-board @thalesmg -/apps/emqx_bridge/ @emqx/emqx-review-board @thalesmg -/apps/emqx_conf/ @emqx/emqx-review-board @thalesmg /apps/emqx_connector/ @emqx/emqx-review-board @JimMoen /apps/emqx_dashboard/ @emqx/emqx-review-board @JimMoen @lafirest /apps/emqx_exhook/ @emqx/emqx-review-board @JimMoen @lafirest /apps/emqx_gateway/ @emqx/emqx-review-board @lafirest -/apps/emqx_machine/ @emqx/emqx-review-board @thalesmg /apps/emqx_management/ @emqx/emqx-review-board @lafirest @sstrigler -/apps/emqx_modules/ @emqx/emqx-review-board @thalesmg /apps/emqx_plugin_libs/ @emqx/emqx-review-board @lafirest -/apps/emqx_plugins/ @emqx/emqx-review-board @JimMoen @thalesmg +/apps/emqx_plugins/ @emqx/emqx-review-board @JimMoen /apps/emqx_prometheus/ @emqx/emqx-review-board @JimMoen -/apps/emqx_psk/ @emqx/emqx-review-board @lafirest @thalesmg -/apps/emqx_resource/ @emqx/emqx-review-board @thalesmg -/apps/emqx_retainer/ @emqx/emqx-review-board @lafirest @thalesmg +/apps/emqx_psk/ @emqx/emqx-review-board @lafirest +/apps/emqx_retainer/ @emqx/emqx-review-board @lafirest /apps/emqx_rule_engine/ @emqx/emqx-review-board @kjellwinblad /apps/emqx_slow_subs/ @emqx/emqx-review-board @lafirest /apps/emqx_statsd/ @emqx/emqx-review-board @JimMoen -## other -/lib-ee/ @emqx/emqx-review-board @thalesmg -/bin/ @emqx/emqx-review-board @thalesmg @id -/rel/ @emqx/emqx-review-board @thalesmg @id - ## CI -/.github/ @emqx/emqx-review-board @id -/.ci/ @emqx/emqx-review-board @id -/scripts/ @emqx/emqx-review-board @id -/build @emqx/emqx-review-board @id -/deploy/ @emqx/emqx-review-board @id @Rory-Z +/deploy/ @emqx/emqx-review-board @Rory-Z + +## no owner for changelogs, anyone can approve +/changes From 06df3a912632b8644c487ba84ca7a446a5ec57f4 Mon Sep 17 00:00:00 2001 From: Erik Timan Date: Tue, 7 Feb 2023 15:47:41 +0100 Subject: [PATCH 0486/1802] chore: add changes --- changes/v5.0.17/feat-9930.en.md | 1 + changes/v5.0.17/feat-9930.zh.md | 1 + 2 files changed, 2 insertions(+) create mode 100644 changes/v5.0.17/feat-9930.en.md create mode 100644 changes/v5.0.17/feat-9930.zh.md diff --git a/changes/v5.0.17/feat-9930.en.md b/changes/v5.0.17/feat-9930.en.md new file mode 100644 index 000000000..1747bb23e --- /dev/null +++ b/changes/v5.0.17/feat-9930.en.md @@ -0,0 +1 @@ +Expose the stats `live_connections.count` and `live_connections.max` to Prometheus. diff --git a/changes/v5.0.17/feat-9930.zh.md b/changes/v5.0.17/feat-9930.zh.md new file mode 100644 index 000000000..d17c1cf68 --- /dev/null +++ b/changes/v5.0.17/feat-9930.zh.md @@ -0,0 +1 @@ +将统计数据 `live_connections.count` 和 `live_connections.max` 公开给 Prometheus. From 5df667738adaab79460732643070e7ac01d192e2 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Wed, 8 Feb 2023 17:25:22 +0100 Subject: [PATCH 0487/1802] docs: fix bad i18n files --- apps/emqx/i18n/emqx_schema_i18n.conf | 4 ++-- apps/emqx_authn/i18n/emqx_authn_api_i18n.conf | 2 +- apps/emqx_auto_subscribe/i18n/emqx_auto_subscribe_i18n.conf | 2 +- apps/emqx_modules/i18n/emqx_topic_metrics_api_i18n.conf | 2 ++ 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/emqx/i18n/emqx_schema_i18n.conf b/apps/emqx/i18n/emqx_schema_i18n.conf index 88ff373be..31003a30a 100644 --- a/apps/emqx/i18n/emqx_schema_i18n.conf +++ b/apps/emqx/i18n/emqx_schema_i18n.conf @@ -1133,7 +1133,7 @@ This config is a map from shared subscription group name to the strategy name. The group name should be of format `[A-Za-z0-9]`. i.e. no special characters are allowed. """ - cn: """设置共享订阅组为单位的分发策略。该配置是一个从组名到 + zh: """设置共享订阅组为单位的分发策略。该配置是一个从组名到 策略名的一个map,组名不得包含 `[A-Za-z0-9]` 之外的特殊字符。 """ } @@ -1152,7 +1152,7 @@ until the subscriber disconnects. - `local`: send to a random local subscriber. If local subscriber was not found, send to a random subscriber cluster-wide """ - cn: """共享订阅的分发策略名称。 + zh: """共享订阅的分发策略名称。 - `random`:随机选择一个组内成员; - `round_robin`:循环选择下一个成员; - `round_robin_per_group`:在共享组内循环选择下一个成员; diff --git a/apps/emqx_authn/i18n/emqx_authn_api_i18n.conf b/apps/emqx_authn/i18n/emqx_authn_api_i18n.conf index da5d6a1d1..a068edee2 100644 --- a/apps/emqx_authn/i18n/emqx_authn_api_i18n.conf +++ b/apps/emqx_authn/i18n/emqx_authn_api_i18n.conf @@ -192,7 +192,7 @@ emqx_authn_api { param_position { desc { en: """Position of authenticator in chain. Possible values are 'front', 'rear', 'before:{other_authenticator}', 'after:{other_authenticator}'.""" - zn: """认证者在链中的位置。可能的值是 'front', 'rear', 'before:{other_authenticator}', 'after:{other_authenticator}'""" + zh: """认证者在链中的位置。可能的值是 'front', 'rear', 'before:{other_authenticator}', 'after:{other_authenticator}'""" } } diff --git a/apps/emqx_auto_subscribe/i18n/emqx_auto_subscribe_i18n.conf b/apps/emqx_auto_subscribe/i18n/emqx_auto_subscribe_i18n.conf index 57f744e8e..ae8c40303 100644 --- a/apps/emqx_auto_subscribe/i18n/emqx_auto_subscribe_i18n.conf +++ b/apps/emqx_auto_subscribe/i18n/emqx_auto_subscribe_i18n.conf @@ -4,7 +4,7 @@ emqx_auto_subscribe_schema { en: """After the device logs in successfully, the subscription is automatically completed for the device through the pre-defined subscription representation. Supports the use of placeholders.""" zh: """设备登录成功之后,通过预设的订阅表示符,为设备自动完成订阅。支持使用占位符。""" } - lable { + label { en: """Auto Subscribe""" zh: """自动订阅""" } diff --git a/apps/emqx_modules/i18n/emqx_topic_metrics_api_i18n.conf b/apps/emqx_modules/i18n/emqx_topic_metrics_api_i18n.conf index 3725ccd13..623884f31 100644 --- a/apps/emqx_modules/i18n/emqx_topic_metrics_api_i18n.conf +++ b/apps/emqx_modules/i18n/emqx_topic_metrics_api_i18n.conf @@ -56,9 +56,11 @@ emqx_topic_metrics_api { } reset_topic_desc { + desc { en: """Topic Name. If this parameter is not present,all created topic metrics will be reset.""" zh: """主题名称。如果此参数不存在,则所有创建的主题监控数据都将重置。""" } + } topic { desc { From 123fea44de10da095fa5bd80a2f642c60c7ec358 Mon Sep 17 00:00:00 2001 From: ieQu1 <99872536+ieQu1@users.noreply.github.com> Date: Wed, 8 Feb 2023 10:05:58 +0100 Subject: [PATCH 0488/1802] chore(ekka): Bump version to 0.14.0 --- apps/emqx/rebar.config | 2 +- mix.exs | 2 +- rebar.config | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/emqx/rebar.config b/apps/emqx/rebar.config index cad599436..4d29f08dd 100644 --- a/apps/emqx/rebar.config +++ b/apps/emqx/rebar.config @@ -27,7 +27,7 @@ {jiffy, {git, "https://github.com/emqx/jiffy", {tag, "1.0.5"}}}, {cowboy, {git, "https://github.com/emqx/cowboy", {tag, "2.9.0"}}}, {esockd, {git, "https://github.com/emqx/esockd", {tag, "5.9.4"}}}, - {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.13.9"}}}, + {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.14.0"}}}, {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.8.1"}}}, {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.35.3"}}}, {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}}, diff --git a/mix.exs b/mix.exs index 18e11fdd7..4dea0bf32 100644 --- a/mix.exs +++ b/mix.exs @@ -53,7 +53,7 @@ defmodule EMQXUmbrella.MixProject do {:cowboy, github: "emqx/cowboy", tag: "2.9.0", override: true}, {:esockd, github: "emqx/esockd", tag: "5.9.4", override: true}, {:rocksdb, github: "emqx/erlang-rocksdb", tag: "1.7.2-emqx-7", override: true}, - {:ekka, github: "emqx/ekka", tag: "0.13.9", override: true}, + {:ekka, github: "emqx/ekka", tag: "0.14.0", override: true}, {:gen_rpc, github: "emqx/gen_rpc", tag: "2.8.1", override: true}, {:grpc, github: "emqx/grpc-erl", tag: "0.6.7", override: true}, {:minirest, github: "emqx/minirest", tag: "1.3.8", override: true}, diff --git a/rebar.config b/rebar.config index 1a9d651dc..702feb031 100644 --- a/rebar.config +++ b/rebar.config @@ -55,7 +55,7 @@ , {cowboy, {git, "https://github.com/emqx/cowboy", {tag, "2.9.0"}}} , {esockd, {git, "https://github.com/emqx/esockd", {tag, "5.9.4"}}} , {rocksdb, {git, "https://github.com/emqx/erlang-rocksdb", {tag, "1.7.2-emqx-7"}}} - , {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.13.9"}}} + , {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.14.0"}}} , {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.8.1"}}} , {grpc, {git, "https://github.com/emqx/grpc-erl", {tag, "0.6.7"}}} , {minirest, {git, "https://github.com/emqx/minirest", {tag, "1.3.8"}}} From 43bf297b318ec58643cbdfe81a3163a7f5a74063 Mon Sep 17 00:00:00 2001 From: Ivan Dyachkov Date: Fri, 3 Feb 2023 14:07:20 +0100 Subject: [PATCH 0489/1802] ci: do not upload packages to s3 and packagecloud on schedule event --- .github/workflows/build_packages.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_packages.yaml b/.github/workflows/build_packages.yaml index a1d8dfee4..6abb36d86 100644 --- a/.github/workflows/build_packages.yaml +++ b/.github/workflows/build_packages.yaml @@ -308,7 +308,7 @@ jobs: publish_artifacts: runs-on: ubuntu-20.04 needs: [prepare, mac, linux] - if: ${{ needs.prepare.outputs.IS_EXACT_TAG }} + if: needs.prepare.outputs.IS_EXACT_TAG && github.event_name != 'schedule' strategy: fail-fast: false matrix: @@ -368,7 +368,7 @@ jobs: push "debian/buster" "packages/$PROFILE/$PROFILE-$VERSION-debian10-amd64.deb" push "debian/buster" "packages/$PROFILE/$PROFILE-$VERSION-debian10-arm64.deb" push "debian/bullseye" "packages/$PROFILE/$PROFILE-$VERSION-debian11-amd64.deb" - push "debian/bullseye" packages/$PROFILE/$PROFILE-$VERSION-debian11-arm64.deb + push "debian/bullseye" "packages/$PROFILE/$PROFILE-$VERSION-debian11-arm64.deb" push "ubuntu/bionic" "packages/$PROFILE/$PROFILE-$VERSION-ubuntu18.04-amd64.deb" push "ubuntu/bionic" "packages/$PROFILE/$PROFILE-$VERSION-ubuntu18.04-arm64.deb" push "ubuntu/focal" "packages/$PROFILE/$PROFILE-$VERSION-ubuntu20.04-amd64.deb" From 157c919ba175bcde9a56205bf93f2ae80b0d973e Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Wed, 8 Feb 2023 17:39:55 +0100 Subject: [PATCH 0490/1802] ci: add i18n style check script --- Makefile | 1 + apps/emqx/etc/ssl_dist.conf | 10 +- apps/emqx/i18n/emqx_schema_i18n.conf | 670 ++++++------------ apps/emqx_authn/i18n/emqx_authn_jwt_i18n.conf | 12 +- .../i18n/emqx_authn_mongodb_i18n.conf | 12 +- .../i18n/emqx_authz_api_schema_i18n.conf | 14 +- .../i18n/emqx_authz_schema_i18n.conf | 42 +- apps/emqx_bridge/i18n/emqx_bridge_api.conf | 14 +- .../i18n/emqx_bridge_webhook_schema.conf | 59 +- apps/emqx_conf/i18n/emqx_conf_schema.conf | 341 ++++----- .../i18n/emqx_connector_api.conf | 12 +- .../i18n/emqx_connector_http.conf | 12 +- .../i18n/emqx_connector_mongo.conf | 26 +- .../i18n/emqx_connector_mqtt_schema.conf | 132 ++-- .../i18n/emqx_connector_mysql.conf | 12 +- .../i18n/emqx_connector_pgsql.conf | 12 +- .../i18n/emqx_connector_redis.conf | 25 +- .../i18n/emqx_dashboard_i18n.conf | 6 +- .../i18n/emqx_exhook_api_i18n.conf | 23 +- .../i18n/emqx_gateway_api_i18n.conf | 8 +- .../i18n/emqx_gateway_api_listeners_i18n.conf | 2 +- .../i18n/emqx_gateway_schema_i18n.conf | 126 ++-- .../i18n/emqx_mgmt_api_key_i18n.conf | 7 +- .../i18n/emqx_mgmt_api_publish_i18n.conf | 42 +- .../i18n/emqx_plugins_schema.conf | 24 +- .../i18n/emqx_prometheus_schema_i18n.conf | 6 +- apps/emqx_psk/i18n/emqx_psk_i18n.conf | 3 +- .../i18n/emqx_retainer_i18n.conf | 6 +- .../i18n/emqx_rule_api_schema.conf | 4 +- .../i18n/emqx_rule_engine_schema.conf | 114 ++- .../i18n/emqx_ee_bridge_gcp_pubsub.conf | 6 +- .../i18n/emqx_ee_bridge_hstreamdb.conf | 12 +- .../i18n/emqx_ee_bridge_influxdb.conf | 12 +- .../i18n/emqx_ee_bridge_kafka.conf | 10 +- .../i18n/emqx_ee_bridge_mysql.conf | 6 +- .../i18n/emqx_ee_bridge_pgsql.conf | 6 +- .../i18n/emqx_ee_bridge_redis.conf | 12 +- .../i18n/emqx_ee_connector_influxdb.conf | 6 +- .../i18n/emqx_license_schema_i18n.conf | 2 +- scripts/check-i18n-style.escript | 116 +++ scripts/check-i18n-style.sh | 8 + 41 files changed, 788 insertions(+), 1185 deletions(-) create mode 100755 scripts/check-i18n-style.escript create mode 100755 scripts/check-i18n-style.sh diff --git a/Makefile b/Makefile index d91dca137..2bfdffdfb 100644 --- a/Makefile +++ b/Makefile @@ -82,6 +82,7 @@ ct: $(REBAR) merge-config static_checks: @$(REBAR) as check do dialyzer, xref @if [ "$${PROFILE}" = 'emqx-enterprise' ]; then $(REBAR) ct --suite apps/emqx/test/emqx_static_checks --readable $(CT_READABLE); fi + @if [ "$${PROFILE}" = 'emqx-enterprise' ]; then ./scripts/check-i18n-style.sh; fi APPS=$(shell $(SCRIPTS)/find-apps.sh) diff --git a/apps/emqx/etc/ssl_dist.conf b/apps/emqx/etc/ssl_dist.conf index 6048ddf60..af1c7506d 100644 --- a/apps/emqx/etc/ssl_dist.conf +++ b/apps/emqx/etc/ssl_dist.conf @@ -1,7 +1,9 @@ -%% The options in the {server, Opts} tuple are used when calling ssl:ssl_accept/3, -%% and the options in the {client, Opts} tuple are used when calling ssl:connect/4. -%% -%% More information at: http://erlang.org/doc/apps/ssl/ssl_distribution.html +%% This additional config file is used when the config 'cluster.proto_dis' in emqx.conf is set to 'inet_tls'. +%% Which means the EMQX nodes will connect to each other over TLS. +%% For more information about inter-broker security, see: https://docs.emqx.com/en/enterprise/v5.0/deploy/cluster/security.html + +%% For more information in technical details see: http://erlang.org/doc/apps/ssl/ssl_distribution.html + [{server, [ %{log_level, debug}, %% NOTE: debug level logging impacts performance, and need to set EMQX logging level to 'debug' diff --git a/apps/emqx/i18n/emqx_schema_i18n.conf b/apps/emqx/i18n/emqx_schema_i18n.conf index 31003a30a..2ba5e7f52 100644 --- a/apps/emqx/i18n/emqx_schema_i18n.conf +++ b/apps/emqx/i18n/emqx_schema_i18n.conf @@ -189,11 +189,9 @@ emqx_schema { sysmon_vm_long_gc { desc { en: """When an Erlang process spends long time to perform garbage collection, a warning level long_gc log is emitted, -and an MQTT message is published to the system topic $SYS/sysmon/long_gc. -""" +and an MQTT message is published to the system topic $SYS/sysmon/long_gc.""" zh: """当系统检测到某个 Erlang 进程垃圾回收占用过长时间,会触发一条带有 long_gc 关键字的日志。 -同时还会发布一条主题为 $SYS/sysmon/long_gc 的 MQTT 系统消息。 -""" +同时还会发布一条主题为 $SYS/sysmon/long_gc 的 MQTT 系统消息。""" } label { en: "Enable Long GC monitoring." @@ -204,11 +202,9 @@ and an MQTT message is published to the system topic $SYS/sysmon/long_gc$SYS/sysmon/long_schedule. -""" +and an MQTT message is published to the system topic $SYS/sysmon/long_schedule.""" zh: """启用后,如果 Erlang VM 调度器出现某个任务占用时间过长时,会触发一条带有 'long_schedule' 关键字的日志。 -同时还会发布一条主题为 $SYS/sysmon/long_schedule 的 MQTT 系统消息。 -""" +同时还会发布一条主题为 $SYS/sysmon/long_schedule 的 MQTT 系统消息。""" } label { en: "Enable Long Schedule monitoring." @@ -220,11 +216,9 @@ and an MQTT message is published to the system topic $SYS/sysmon/long_sche desc { en: """When an Erlang process consumed a large amount of memory for its heap space, the system will write a warning level large_heap log, and an MQTT message is published to -the system topic $SYS/sysmon/large_heap. -""" +the system topic $SYS/sysmon/large_heap.""" zh: """启用后,当一个 Erlang 进程申请了大量内存,系统会触发一条带有 large_heap 关键字的 -warning 级别日志。同时还会发布一条主题为 $SYS/sysmon/busy_dist_port 的 MQTT 系统消息。 -""" +warning 级别日志。同时还会发布一条主题为 $SYS/sysmon/busy_dist_port 的 MQTT 系统消息。""" } label { en: "Enable Large Heap monitoring." @@ -236,11 +230,9 @@ warning 级别日志。同时还会发布一条主题为 $SYS/sysmon/busy_ desc { en: """When the RPC connection used to communicate with other nodes in the cluster is overloaded, there will be a busy_dist_port warning log, -and an MQTT message is published to system topic $SYS/sysmon/busy_dist_port. -""" +and an MQTT message is published to system topic $SYS/sysmon/busy_dist_port.""" zh: """启用后,当用于集群接点之间 RPC 的连接过忙时,会触发一条带有 busy_dist_port 关键字的 warning 级别日志。 -同时还会发布一条主题为 $SYS/sysmon/busy_dist_port 的 MQTT 系统消息。 -""" +同时还会发布一条主题为 $SYS/sysmon/busy_dist_port 的 MQTT 系统消息。""" } label { en: "Enable Busy Distribution Port monitoring." @@ -251,11 +243,9 @@ and an MQTT message is published to system topic $SYS/sysmon/busy_dist_por sysmon_vm_busy_port { desc { en: """When a port (e.g. TCP socket) is overloaded, there will be a busy_port warning log, -and an MQTT message is published to the system topic $SYS/sysmon/busy_port. -""" +and an MQTT message is published to the system topic $SYS/sysmon/busy_port.""" zh: """当一个系统接口(例如 TCP socket)过忙,会触发一条带有 busy_port 关键字的 warning 级别的日志。 -同时还会发布一条主题为 $SYS/sysmon/busy_port 的 MQTT 系统消息。 -""" +同时还会发布一条主题为 $SYS/sysmon/busy_port 的 MQTT 系统消息。""" } label { en: "Enable Busy Port monitoring." @@ -451,7 +441,7 @@ and an MQTT message is published to the system topic $SYS/sysmon/busy_port desc { en: "The maximum total number of deactivated alarms to keep as history.
" "When this limit is exceeded, the oldest deactivated alarms are " - "deleted to cap the total number.\n" + "deleted to cap the total number." zh: "要保留为历史记录的已停用报警的最大总数。当超过此限制时,将删除最旧的停用报警,以限制总数。" } label: { @@ -463,7 +453,7 @@ and an MQTT message is published to the system topic $SYS/sysmon/busy_port alarm_validity_period { desc { en: "Retention time of deactivated alarms. Alarms are not deleted immediately\n" - "when deactivated, but after the retention time.\n" + "when deactivated, but after the retention time." zh: "停用报警的保留时间。报警在停用时不会立即删除,而是在保留时间之后删除。" } label: { @@ -520,7 +510,7 @@ and an MQTT message is published to the system topic $SYS/sysmon/busy_port desc { en: "Use the database to store information about persistent sessions.\n" "This makes it possible to migrate a client connection to another\n" - "cluster node if a node is stopped.\n" + "cluster node if a node is stopped." zh: "使用数据库存储有关持久会话的信息。\n" "这使得在节点停止时,可以将客户端连接迁移到另一个群集节点。" } @@ -573,7 +563,7 @@ and an MQTT message is published to the system topic $SYS/sysmon/busy_port desc { en: "The time messages that was not delivered to a persistent session\n" "is stored before being garbage collected if the node the previous\n" - "session was handled on restarts of is stopped.\n" + "session was handled on restarts of is stopped." zh: "如果重新启动时处理上一个会话的节点已停止,则未传递到持久会话的消息在垃圾收集之前会被存储。" } label: { @@ -586,7 +576,7 @@ and an MQTT message is published to the system topic $SYS/sysmon/busy_port desc { en: "The starting interval for garbage collection of undelivered messages to\n" "a persistent session. This affects how often the \"max_retain_undelivered\"\n" - "is checked for removal.\n" + "is checked for removal." zh: "将未送达的消息垃圾收集到持久会话的开始间隔。\n" "这会影响检查 \"max_retain_undelivered\"(最大保留未送达)的删除频率。" } @@ -600,9 +590,9 @@ and an MQTT message is published to the system topic $SYS/sysmon/busy_port desc { en: "The starting interval for garbage collection of transient data for\n" "persistent session messages. This does not affect the lifetime length\n" - "of persistent session messages.\n" + "of persistent session messages." zh: "持久会话消息的临时数据垃圾收集的开始间隔。\n" - "这不会影响持久会话消息的生命周期长度。\n" + "这不会影响持久会话消息的生命周期长度。" } label: { en: "Session message GC interval" @@ -658,8 +648,7 @@ and an MQTT message is published to the system topic $SYS/sysmon/busy_port desc { en: """A zone is a set of configs grouped by the zone name. For flexible configuration mapping, the name can be set to a listener's zone config. -NOTE: A built-in zone named default is auto created and can not be deleted. -""" +NOTE: A built-in zone named default is auto created and can not be deleted.""" zh: """zone 是按name 分组的一组配置。 对于灵活的配置映射,可以将 name 设置为侦听器的 zone 配置。 注:名为 default 的内置区域是自动创建的,无法删除。""" @@ -669,8 +658,7 @@ NOTE: A built-in zone named default is auto created and can not be mqtt { desc { en: """Global MQTT configuration. -The configs here work as default values which can be overridden in zone configs -""" +The configs here work as default values which can be overridden in zone configs""" zh: """全局的 MQTT 配置项。 mqtt 下所有的配置作为全局的默认值存在,它可以被 zone 中的配置覆盖。""" } @@ -949,8 +937,7 @@ There's no priority table by default, hence all messages are treated equal. **Examples**: To configure \"topic/1\" > \"topic/2\": -mqueue_priorities: {\"topic/1\": 10, \"topic/2\": 8} -""" +mqueue_priorities: {\"topic/1\": 10, \"topic/2\": 8}""" zh: """主题优先级。取值范围 [1-255] 默认优先级表为空,即所有的主题优先级相同。 @@ -959,8 +946,7 @@ To configure \"topic/1\" > \"topic/2\": 示例: 配置 \"topic/1\" > \"topic/2\": -mqueue_priorities: {\"topic/1\": 10, \"topic/2\": 8} -""" +mqueue_priorities: {\"topic/1\": 10, \"topic/2\": 8}""" } label: { en: """Topic Priorities""" @@ -993,11 +979,9 @@ To configure \"topic/1\" > \"topic/2\": mqtt_use_username_as_clientid { desc { en: """Whether to user Client ID as Username. -This setting takes effect later than Use Peer Certificate as Username (peer_cert_as_username) and Use peer certificate as Client ID (peer_cert_as_clientid). -""" +This setting takes effect later than Use Peer Certificate as Username (peer_cert_as_username) and Use peer certificate as Client ID (peer_cert_as_clientid).""" zh: """是否使用用户名作为客户端 ID。 -此设置的作用时间晚于 使用对端证书作为用户名peer_cert_as_username) 和 使用对端证书作为客户端 IDpeer_cert_as_clientid)。 -""" +此设置的作用时间晚于 使用对端证书作为用户名peer_cert_as_username) 和 使用对端证书作为客户端 IDpeer_cert_as_clientid)。""" } label: { en: """Use Username as Client ID""" @@ -1013,16 +997,14 @@ Supported configurations are the following: - dn: Take the DN field of the certificate as Username - crt: Take the content of the DER or PEM certificate as Username - pem: Convert DER certificate content to PEM format as Username -- md5: Take the MD5 value of the content of the DER or PEM certificate as Username -""" +- md5: Take the MD5 value of the content of the DER or PEM certificate as Username""" zh: """使用对端证书中的 CN、DN 字段或整个证书内容来作为用户名。仅适用于 TLS 连接。 目前支持配置为以下内容: - cn: 取证书的 CN 字段作为 Username - dn: 取证书的 DN 字段作为 Username - crt: 取 DERPEM 证书的内容作为 Username - pem: 将 DER 证书内容转换为 PEM 格式后作为 Username -- md5: 取 DERPEM 证书的内容的 MD5 值作为 Username -""" +- md5: 取 DERPEM 证书的内容的 MD5 值作为 Username""" } label: { en: """Use Peer Certificate as Username""" @@ -1038,16 +1020,14 @@ Supported configurations are the following: - dn: Take the DN field of the certificate as Client ID - crt: Take the content of the DER or PEM certificate as Client ID - pem: Convert DER certificate content to PEM format as Client ID -- md5: Take the MD5 value of the content of the DER or PEM certificate as Client ID -""" +- md5: Take the MD5 value of the content of the DER or PEM certificate as Client ID""" zh: """使用对端证书中的 CN、DN 字段或整个证书内容来作为客户端 ID。仅适用于 TLS 连接。 目前支持配置为以下内容: - cn: 取证书的 CN 字段作为 Client ID - dn: 取证书的 DN 字段作为 Client ID - crt: 取 DERPEM 证书的内容作为 Client ID - pem: 将 DER 证书内容转换为 PEM 格式后作为 Client ID -- md5: 取 DERPEM 证书的内容的 MD5 值作为 Client ID -""" +- md5: 取 DERPEM 证书的内容的 MD5 值作为 Client ID""" } label: { en: """Use Peer Certificate as Client ID""" @@ -1075,15 +1055,13 @@ Supported configurations are the following: - `local`: only lock the session on the current node - `one`: select only one remote node to lock the session - `quorum`: select some nodes to lock the session - - `all`: lock the session on all the nodes in the cluster -""" + - `all`: lock the session on all the nodes in the cluster""" zh: """Session 在集群中的锁策略。 - `loca`:仅锁本节点的 Session; - `one`:任选一个其它节点加锁; - `quorum`:选择集群中半数以上的节点加锁; - - `all`:选择所有节点加锁。 -""" + - `all`:选择所有节点加锁。""" } } @@ -1093,15 +1071,13 @@ Supported configurations are the following: - `random`: dispatch the message to a random selected subscriber - `round_robin`: select the subscribers in a round-robin manner - `sticky`: always use the last selected subscriber to dispatch, until the subscriber disconnects. - - `hash`: select the subscribers by the hash of `clientIds` -""" + - `hash`: select the subscribers by the hash of `clientIds`""" zh: """共享订阅消息派发策略。 - `random`:随机挑选一个共享订阅者派发; - `round_robin`:使用 round-robin 策略派发; - `sticky`:总是使用上次选中的订阅者派发,直到它断开连接; - - `hash`:使用发送者的 Client ID 进行 Hash 来选择订阅者。 -""" + - `hash`:使用发送者的 Client ID 进行 Hash 来选择订阅者。""" } } @@ -1109,13 +1085,11 @@ Supported configurations are the following: desc { en: """Deprecated, will be removed in 5.1. Enable/disable shared dispatch acknowledgement for QoS 1 and QoS 2 messages. -This should allow messages to be dispatched to a different subscriber in the group in case the picked (based on `shared_subscription_strategy`) subscriber is offline. -""" +This should allow messages to be dispatched to a different subscriber in the group in case the picked (based on `shared_subscription_strategy`) subscriber is offline.""" zh: """该配置项已废弃,会在 5.1 中移除。 启用/禁用 QoS 1 和 QoS 2 消息的共享派发确认。 -开启后,允许将消息从未及时回复 ACK 的订阅者 (例如,客户端离线)重新派发给另外一个订阅者。 -""" +开启后,允许将消息从未及时回复 ACK 的订阅者 (例如,客户端离线)重新派发给另外一个订阅者。""" } } @@ -1131,11 +1105,9 @@ This should allow messages to be dispatched to a different subscriber in the gro en: """Per group dispatch strategy for shared subscription. This config is a map from shared subscription group name to the strategy name. The group name should be of format `[A-Za-z0-9]`. i.e. no -special characters are allowed. -""" +special characters are allowed.""" zh: """设置共享订阅组为单位的分发策略。该配置是一个从组名到 -策略名的一个map,组名不得包含 `[A-Za-z0-9]` 之外的特殊字符。 -""" +策略名的一个map,组名不得包含 `[A-Za-z0-9]` 之外的特殊字符。""" } } @@ -1150,16 +1122,14 @@ special characters are allowed. until the subscriber disconnects. - `hash`: select the subscribers by the hash of `clientIds` - `local`: send to a random local subscriber. If local -subscriber was not found, send to a random subscriber cluster-wide -""" +subscriber was not found, send to a random subscriber cluster-wide""" zh: """共享订阅的分发策略名称。 - `random`:随机选择一个组内成员; - `round_robin`:循环选择下一个成员; - `round_robin_per_group`:在共享组内循环选择下一个成员; - `sticky`:使用上一次选中的成员; - `hash`:根据 ClientID 哈希映射到一个成员; -- `local`:随机分发到节点本地成成员,如果本地成员不存在,则随机分发到任意一个成员。 -""" +- `local`:随机分发到节点本地成成员,如果本地成员不存在,则随机分发到任意一个成员。""" } } @@ -1172,16 +1142,14 @@ Change this parameter only when there are many wildcard topics. NOTE: when changing from/to `global` lock, it requires all nodes in the cluster to be stopped before the change. - `key`: mnesia transactional updates with per-key locks. Recommended for a single-node setup. - `tab`: mnesia transactional updates with table lock. Recommended for a cluster setup. - - `global`: updates are protected with a global lock. Recommended for large clusters. -""" + - `global`: updates are protected with a global lock. Recommended for large clusters.""" zh: """通配主题订阅/取消订阅性能调优。 建议仅当通配符主题较多时才更改此参数。 注:当从/更改为 `global` 锁时,它要求集群中的所有节点在更改之前停止。 - `key`:为 Mnesia 事务涉及到的每个 key 上锁,建议单节点时使用。 - `tab`:为 Mnesia 事务涉及到的表上锁,建议在集群中使用。 - - `global`:所以更新操作都被全局的锁保护,仅建议在超大规模集群中使用。 -""" + - `global`:所以更新操作都被全局的锁保护,仅建议在超大规模集群中使用。""" } } @@ -1191,14 +1159,12 @@ NOTE: when changing from/to `global` lock, it requires all nodes in the cluster Enabling it significantly improves wildcard topic subscribe rate, if wildcard topics have unique prefixes like: 'sensor/{{id}}/+/', where ID is unique per subscriber. Topic match performance (when publishing) may degrade if messages are mostly published to topics with large number of levels. -NOTE: This is a cluster-wide configuration. It requires all nodes to be stopped before changing it. -""" +NOTE: This is a cluster-wide configuration. It requires all nodes to be stopped before changing it.""" zh: """是否开启主题表压缩存储。 启用它会显着提高通配符主题订阅率,如果通配符主题具有唯一前缀,例如:'sensor/{{id}}/+/',其中每个订阅者的 ID 是唯一的。 如果消息主要发布到具有大量级别的主题,则主题匹配性能(发布时)可能会降低。 -注意:这是一个集群范围的配置。 它要求在更改之前停止所有节点。 -""" +注意:这是一个集群范围的配置。 它要求在更改之前停止所有节点。""" } } @@ -1220,12 +1186,10 @@ NOTE: This is a cluster-wide configuration. It requires all nodes to be stopped desc { en: """Time interval for publishing following heartbeat messages: - `$SYS/brokers//uptime` - - `$SYS/brokers//datetime` -""" + - `$SYS/brokers//datetime`""" zh: """发送心跳系统消息的间隔时间,它包括: - `$SYS/brokers//uptime` - - `$SYS/brokers//datetime` -""" + - `$SYS/brokers//datetime`""" } } @@ -1267,17 +1231,13 @@ NOTE: This is a cluster-wide configuration. It requires all nodes to be stopped fields_authorization_no_match { desc { - en: """ -Default access control action if the user or client matches no ACL rules, + en: """Default access control action if the user or client matches no ACL rules, or if no such user or client is found by the configurable authorization sources such as built_in_database, an HTTP API, or a query against PostgreSQL. -Find more details in 'authorization.sources' config. -""" - zh: """ -如果用户或客户端不匹配ACL规则,或者从可配置授权源(比如内置数据库、HTTP API 或 PostgreSQL 等。)内未找 +Find more details in 'authorization.sources' config.""" + zh: """如果用户或客户端不匹配ACL规则,或者从可配置授权源(比如内置数据库、HTTP API 或 PostgreSQL 等。)内未找 到此类用户或客户端时,模式的认访问控制操作。 -在“授权”中查找更多详细信息。 -""" +在“授权”中查找更多详细信息。""" } label: { en: "Authorization no match" @@ -1320,7 +1280,7 @@ fields_cache_max_size { fields_cache_ttl { desc { - en: """Time to live for the cached data. """ + en: """Time to live for the cached data.""" zh: """缓存数据的生存时间。""" } label: { @@ -1331,7 +1291,7 @@ fields_cache_ttl { fields_deflate_opts_level { desc { - en: """Compression level. """ + en: """Compression level.""" zh: """压缩级别""" } label: { @@ -1342,14 +1302,10 @@ fields_deflate_opts_level { fields_deflate_opts_mem_level { desc { - en: """ -Specifies the size of the compression state.
-Lower values decrease memory usage per connection. -""" - zh: """ -指定压缩状态的大小
-较低的值会减少每个连接的内存使用。 -""" + en: """Specifies the size of the compression state.
+Lower values decrease memory usage per connection.""" + zh: """指定压缩状态的大小
+较低的值会减少每个连接的内存使用。""" } label: { en: "Size of the compression state" @@ -1370,7 +1326,7 @@ fields_deflate_opts_strategy { fields_deflate_opts_server_context_takeover { desc { - en: """Takeover means the compression state is retained between server messages. """ + en: """Takeover means the compression state is retained between server messages.""" zh: """接管意味着在服务器消息之间保留压缩状态。""" } label: { @@ -1381,7 +1337,7 @@ fields_deflate_opts_server_context_takeover { fields_deflate_opts_client_context_takeover { desc { - en: """Takeover means the compression state is retained between client messages. """ + en: """Takeover means the compression state is retained between client messages.""" zh: """接管意味着在客户端消息之间保留压缩状态。""" } label: { @@ -1414,7 +1370,7 @@ fields_deflate_opts_client_max_window_bits { client_ssl_opts_schema_enable { desc { - en: """Enable TLS. """ + en: """Enable TLS.""" zh: """启用 TLS。""" } label: { @@ -1425,22 +1381,18 @@ client_ssl_opts_schema_enable { common_ssl_opts_schema_cacertfile { desc { - en: """ -Trusted PEM format CA certificates bundle file.
+ en: """Trusted PEM format CA certificates bundle file.
The certificates in this file are used to verify the TLS peer's certificates. Append new certificates to the file if new CAs are to be trusted. There is no need to restart EMQX to have the updated file loaded, because the system regularly checks if file has been updated (and reload).
NOTE: invalidating (deleting) a certificate from the file will not affect -already established connections. -""" - zh: """ -受信任的PEM格式 CA 证书捆绑文件
+already established connections.""" + zh: """受信任的PEM格式 CA 证书捆绑文件
此文件中的证书用于验证TLS对等方的证书。 如果要信任新 CA,请将新证书附加到文件中。 无需重启EMQX即可加载更新的文件,因为系统会定期检查文件是否已更新(并重新加载)
-注意:从文件中失效(删除)证书不会影响已建立的连接。 -""" +注意:从文件中失效(删除)证书不会影响已建立的连接。""" } label: { en: "CACertfile" @@ -1450,20 +1402,16 @@ already established connections. common_ssl_opts_schema_certfile { desc { - en: """ -PEM format certificates chain file.
+ en: """PEM format certificates chain file.
The certificates in this file should be in reversed order of the certificate issue chain. That is, the host's certificate should be placed in the beginning of the file, followed by the immediate issuer certificate and so on. Although the root CA certificate is optional, it should be placed at the end of -the file if it is to be added. -""" - zh: """ -PEM格式证书链文件
+the file if it is to be added.""" + zh: """PEM格式证书链文件
此文件中的证书应与证书颁发链的顺序相反。也就是说,主机的证书应该放在文件的开头, 然后是直接颁发者 CA 证书,依此类推,一直到根 CA 证书。 -根 CA 证书是可选的,如果想要添加,应加到文件到最末端。 -""" +根 CA 证书是可选的,如果想要添加,应加到文件到最末端。""" } label: { en: "Certfile" @@ -1473,7 +1421,7 @@ PEM格式证书链文件
common_ssl_opts_schema_keyfile { desc { - en: """PEM format private key file. """ + en: """PEM format private key file.""" zh: """PEM格式的私钥文件。""" } label: { @@ -1484,7 +1432,7 @@ common_ssl_opts_schema_keyfile { common_ssl_opts_schema_verify { desc { - en: """Enable or disable peer verification. """ + en: """Enable or disable peer verification.""" zh: """启用或禁用对等验证。""" } label: { @@ -1495,7 +1443,7 @@ common_ssl_opts_schema_verify { common_ssl_opts_schema_reuse_sessions { desc { - en: """Enable TLS session reuse. """ + en: """Enable TLS session reuse.""" zh: """启用 TLS 会话重用。""" } label: { @@ -1506,18 +1454,14 @@ common_ssl_opts_schema_reuse_sessions { common_ssl_opts_schema_depth { desc { - en: """ -Maximum number of non-self-issued intermediate certificates that can follow the peer certificate in a valid certification path. + en: """Maximum number of non-self-issued intermediate certificates that can follow the peer certificate in a valid certification path. So, if depth is 0 the PEER must be signed by the trusted ROOT-CA directly;
if 1 the path can be PEER, Intermediate-CA, ROOT-CA;
-if 2 the path can be PEER, Intermediate-CA1, Intermediate-CA2, ROOT-CA.
-""" - zh: """ -在有效的证书路径中,可以跟随对等证书的非自颁发中间证书的最大数量。 +if 2 the path can be PEER, Intermediate-CA1, Intermediate-CA2, ROOT-CA.""" + zh: """在有效的证书路径中,可以跟随对等证书的非自颁发中间证书的最大数量。 因此,如果深度为0,则对等方必须由受信任的根 CA 直接签名;
如果是1,路径可以是 PEER、中间 CA、ROOT-CA;
-如果是2,则路径可以是PEER、中间 CA1、中间 CA2、ROOT-CA。 -""" +如果是2,则路径可以是PEER、中间 CA1、中间 CA2、ROOT-CA。""" } label: { en: "CACert Depth" @@ -1527,14 +1471,8 @@ if 2 the path can be PEER, Intermediate-CA1, Intermediate-CA2, ROOT-CA.
common_ssl_opts_schema_password { desc { - en: """ -String containing the user's password. -Only used if the private key file is password-protected. -""" - zh: """ -包含用户密码的字符串。 -仅在私钥文件受密码保护时使用。 -""" + en: """String containing the user's password. Only used if the private key file is password-protected.""" + zh: """包含用户密码的字符串。仅在私钥文件受密码保护时使用。""" } label: { en: "Keyfile passphrase" @@ -1544,17 +1482,12 @@ Only used if the private key file is password-protected. common_ssl_opts_schema_versions { desc { - en: """ -All TLS/DTLS versions to be supported.
+ en: """All TLS/DTLS versions to be supported.
NOTE: PSK ciphers are suppressed by 'tlsv1.3' version config.
In case PSK cipher suites are intended, make sure to configure -['tlsv1.2', 'tlsv1.1'] here. -""" - zh: """ -支持所有TLS/DTLS版本
- -注:PSK 的 Ciphers 无法在 tlsv1.3 中使用,如果打算使用 PSK 密码套件,请确保这里配置为 ["tlsv1.2","tlsv1.1"]。 -""" +['tlsv1.2', 'tlsv1.1'] here.""" + zh: """支持所有TLS/DTLS版本
+注:PSK 的 Ciphers 无法在 tlsv1.3 中使用,如果打算使用 PSK 密码套件,请确保这里配置为 ["tlsv1.2","tlsv1.1"]。""" } label: { en: "SSL versions" @@ -1564,8 +1497,7 @@ In case PSK cipher suites are intended, make sure to configure ciphers_schema_common { desc { - en: """ -This config holds TLS cipher suite names separated by comma, + en: """This config holds TLS cipher suite names separated by comma, or as an array of strings. e.g. "TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256" or ["TLS_AES_256_GCM_SHA384","TLS_AES_128_GCM_SHA256"]. @@ -1591,10 +1523,8 @@ If PSK cipher suites are intended, 'tlsv1.3' should be disabled from versi PSK cipher suites: "RSA-PSK-AES256-GCM-SHA384,RSA-PSK-AES256-CBC-SHA384, RSA-PSK-AES128-GCM-SHA256,RSA-PSK-AES128-CBC-SHA256, RSA-PSK-AES256-CBC-SHA,RSA-PSK-AES128-CBC-SHA, -RSA-PSK-DES-CBC3-SHA,RSA-PSK-RC4-SHA"
-""" - zh: """ -此配置保存由逗号分隔的 TLS 密码套件名称,或作为字符串数组。例如 +RSA-PSK-DES-CBC3-SHA,RSA-PSK-RC4-SHA"
""" + zh: """此配置保存由逗号分隔的 TLS 密码套件名称,或作为字符串数组。例如 "TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256"["TLS_AES_256_GCM_SHA384","TLS_AES_128_GCM_SHA256"]
@@ -1617,8 +1547,7 @@ PSK 密码套件: "RSA-PSK-AES256-GCM-SHA384,RSA-PSK-AES256-CBC-SHA384, RSA-PSK-AES128-GCM-SHA256,RSA-PSK-AES128-CBC-SHA256, RSA-PSK-AES256-CBC-SHA,RSA-PSK-AES128-CBC-SHA, -RSA-PSK-DES-CBC3-SHA,RSA-PSK-RC4-SHA"
-""" +RSA-PSK-DES-CBC3-SHA,RSA-PSK-RC4-SHA"
""" } label: { en: "" @@ -1628,8 +1557,7 @@ RSA-PSK-DES-CBC3-SHA,RSA-PSK-RC4-SHA"

ciphers_schema_quic { desc { - en: """ -This config holds TLS cipher suite names separated by comma, + en: """This config holds TLS cipher suite names separated by comma, or as an array of strings. e.g. "TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256" or ["TLS_AES_256_GCM_SHA384","TLS_AES_128_GCM_SHA256"]. @@ -1657,10 +1585,9 @@ RSA-PSK-AES128-GCM-SHA256,RSA-PSK-AES128-CBC-SHA256, RSA-PSK-AES256-CBC-SHA,RSA-PSK-AES128-CBC-SHA, RSA-PSK-DES-CBC3-SHA,RSA-PSK-RC4-SHA"

-NOTE: QUIC listener supports only 'tlsv1.3' ciphers
-""" - zh: """ -此配置保存由逗号分隔的 TLS 密码套件名称,或作为字符串数组。例如 +NOTE: QUIC listener supports only 'tlsv1.3' ciphers""" + + zh: """此配置保存由逗号分隔的 TLS 密码套件名称,或作为字符串数组。例如 "TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256"["TLS_AES_256_GCM_SHA384","TLS_AES_128_GCM_SHA256"]
@@ -1685,8 +1612,7 @@ RSA-PSK-AES128-GCM-SHA256,RSA-PSK-AES128-CBC-SHA256, RSA-PSK-AES256-CBC-SHA,RSA-PSK-AES128-CBC-SHA, RSA-PSK-DES-CBC3-SHA,RSA-PSK-RC4-SHA"

-注:QUIC 监听器不支持 tlsv1.3 的 ciphers -""" +注:QUIC 监听器不支持 tlsv1.3 的 ciphers""" } label: { en: "" @@ -1696,7 +1622,7 @@ RSA-PSK-DES-CBC3-SHA,RSA-PSK-RC4-SHA"

common_ssl_opts_schema_user_lookup_fun { desc { - en: """EMQX-internal callback that is used to lookup pre-shared key (PSK) identity. """ + en: """EMQX-internal callback that is used to lookup pre-shared key (PSK) identity.""" zh: """用于查找预共享密钥(PSK)标识的 EMQX 内部回调。""" } label: { @@ -1707,16 +1633,12 @@ common_ssl_opts_schema_user_lookup_fun { common_ssl_opts_schema_secure_renegotiate { desc { - en: """ -SSL parameter renegotiation is a feature that allows a client and a server + en: """SSL parameter renegotiation is a feature that allows a client and a server to renegotiate the parameters of the SSL connection on the fly. RFC 5746 defines a more secure way of doing this. By enabling secure renegotiation, -you drop support for the insecure renegotiation, prone to MitM attacks. -""" - zh: """ -SSL 参数重新协商是一种允许客户端和服务器动态重新协商 SSL 连接参数的功能。 -RFC 5746 定义了一种更安全的方法。通过启用安全的重新协商,您就失去了对不安全的重新协商的支持,从而容易受到 MitM 攻击。 -""" +you drop support for the insecure renegotiation, prone to MitM attacks.""" + zh: """SSL 参数重新协商是一种允许客户端和服务器动态重新协商 SSL 连接参数的功能。 +RFC 5746 定义了一种更安全的方法。通过启用安全的重新协商,您就失去了对不安全的重新协商的支持,从而容易受到 MitM 攻击。""" } label: { en: "SSL renegotiate" @@ -1726,18 +1648,13 @@ RFC 5746 定义了一种更安全的方法。通过启用安全的重新协商 server_ssl_opts_schema_dhfile { desc { - en: """ -Path to a file containing PEM-encoded Diffie-Hellman parameters + en: """Path to a file containing PEM-encoded Diffie-Hellman parameters to be used by the server if a cipher suite using Diffie-Hellman key exchange is negotiated. If not specified, default parameters are used.
-NOTE: The dhfile option is not supported by TLS 1.3. -""" - zh: """ -如果协商使用Diffie-Hellman密钥交换的密码套件,则服务器将使用包含PEM编码的Diffie-Hellman参数的文件的路径。如果未指定,则使用默认参数。
- -注意:TLS 1.3不支持dhfile选项。 -""" +NOTE: The dhfile option is not supported by TLS 1.3.""" + zh: """如果协商使用Diffie-Hellman密钥交换的密码套件,则服务器将使用包含PEM编码的Diffie-Hellman参数的文件的路径。如果未指定,则使用默认参数。
+注意:TLS 1.3不支持dhfile选项。""" } label: { en: "SSL dhfile" @@ -1747,18 +1664,14 @@ NOTE: The dhfile option is not supported by TLS 1.3. server_ssl_opts_schema_fail_if_no_peer_cert { desc { - en: """ -Used together with {verify, verify_peer} by an TLS/DTLS server. + en: """Used together with {verify, verify_peer} by an TLS/DTLS server. If set to true, the server fails if the client does not have a certificate to send, that is, sends an empty certificate. If set to false, it fails only if the client sends an invalid -certificate (an empty certificate is considered valid). -""" - zh: """ -TLS/DTLS 服务器与 {verify,verify_peer} 一起使用。 +certificate (an empty certificate is considered valid).""" + zh: """TLS/DTLS 服务器与 {verify,verify_peer} 一起使用。 如果设置为true,则如果客户端没有要发送的证书,即发送空证书,服务器将失败。 -如果设置为false,则仅当客户端发送无效证书(空证书被视为有效证书)时才会失败。 -""" +如果设置为false,则仅当客户端发送无效证书(空证书被视为有效证书)时才会失败。""" } label: { en: "SSL fail if no peer cert" @@ -1768,15 +1681,11 @@ TLS/DTLS 服务器与 {verify,verify_peer} 一起使用。 server_ssl_opts_schema_honor_cipher_order { desc { - en: """ -An important security setting, it forces the cipher to be set based + en: """An important security setting, it forces the cipher to be set based on the server-specified order instead of the client-specified order, hence enforcing the (usually more properly configured) security - ordering of the server administrator. -""" - zh: """ -一个重要的安全设置,它强制根据服务器指定的顺序而不是客户机指定的顺序设置密码,从而强制服务器管理员执行(通常配置得更正确)安全顺序。 -""" + ordering of the server administrator.""" + zh: """一个重要的安全设置,它强制根据服务器指定的顺序而不是客户机指定的顺序设置密码,从而强制服务器管理员执行(通常配置得更正确)安全顺序。""" } label: { en: "SSL honor cipher order" @@ -1786,22 +1695,18 @@ An important security setting, it forces the cipher to be set based server_ssl_opts_schema_client_renegotiation { desc { - en: """ -In protocols that support client-initiated renegotiation, + en: """In protocols that support client-initiated renegotiation, the cost of resources of such an operation is higher for the server than the client. This can act as a vector for denial of service attacks. The SSL application already takes measures to counter-act such attempts, but client-initiated renegotiation can be strictly disabled by setting this option to false. The default value is true. Note that disabling renegotiation can result in long-lived connections becoming unusable due to limits on -the number of messages the underlying cipher suite can encipher. -""" - zh: """ -在支持客户机发起的重新协商的协议中,这种操作的资源成本对于服务器来说高于客户机。 +the number of messages the underlying cipher suite can encipher.""" + zh: """在支持客户机发起的重新协商的协议中,这种操作的资源成本对于服务器来说高于客户机。 这可能会成为拒绝服务攻击的载体。 SSL 应用程序已经采取措施来反击此类尝试,但通过将此选项设置为 false,可以严格禁用客户端发起的重新协商。 -默认值为 true。请注意,由于基础密码套件可以加密的消息数量有限,禁用重新协商可能会导致长期连接变得不可用。 -""" +默认值为 true。请注意,由于基础密码套件可以加密的消息数量有限,禁用重新协商可能会导致长期连接变得不可用。""" } label: { en: "SSL client renegotiation" @@ -1811,12 +1716,8 @@ SSL 应用程序已经采取措施来反击此类尝试,但通过将此选项 server_ssl_opts_schema_handshake_timeout { desc { - en: """ -Maximum time duration allowed for the handshake to complete -""" - zh: """ -握手完成所允许的最长时间 -""" + en: """Maximum time duration allowed for the handshake to complete""" + zh: """握手完成所允许的最长时间""" } label: { en: "Handshake timeout" @@ -1826,14 +1727,8 @@ Maximum time duration allowed for the handshake to complete server_ssl_opts_schema_gc_after_handshake { desc { - en: """ -Memory usage tuning. If enabled, will immediately perform a garbage collection after -the TLS/SSL handshake. -""" - zh: """ -内存使用调优。如果启用,将在TLS/SSL握手完成后立即执行垃圾回收。 -TLS/SSL握手建立后立即进行GC。 -""" + en: """Memory usage tuning. If enabled, will immediately perform a garbage collection after the TLS/SSL handshake.""" + zh: """内存使用调优。如果启用,将在TLS/SSL握手完成后立即执行垃圾回收。TLS/SSL握手建立后立即进行GC。""" } label: { en: "Perform GC after handshake" @@ -1898,7 +1793,7 @@ fields_listeners_quic { fields_listener_enabled { desc { - en: """Enable listener. """ + en: """Enable listener.""" zh: """启停监听器。""" } label: { @@ -1920,7 +1815,7 @@ fields_mqtt_quic_listener_certfile { fields_mqtt_quic_listener_keyfile { desc { - en: """Path to the secret key file. """ + en: """Path to the secret key file.""" zh: """私钥文件。""" } label: { @@ -1942,7 +1837,7 @@ fields_mqtt_quic_listener_idle_timeout { fields_mqtt_quic_listener_handshake_idle_timeout { desc { - en: """How long a handshake can idle before it is discarded. """ + en: """How long a handshake can idle before it is discarded.""" zh: """一个握手在被丢弃之前可以空闲多长时间。""" } label: { @@ -1953,12 +1848,8 @@ fields_mqtt_quic_listener_handshake_idle_timeout { fields_mqtt_quic_listener_keep_alive_interval { desc { - en: """ -How often to send PING frames to keep a connection alive. 0 means disabled. -""" - zh: """ -发送 PING 帧的频率,以保活连接. 设为 0 表示禁用。 -""" + en: """How often to send PING frames to keep a connection alive. 0 means disabled.""" + zh: """发送 PING 帧的频率,以保活连接. 设为 0 表示禁用。""" } label: { en: "Keep Alive Interval" @@ -1968,12 +1859,8 @@ How often to send PING frames to keep a connection alive. 0 means disabled. base_listener_bind { desc { - en: """ -IP address and port for the listening socket. -""" - zh: """ -监听套接字的 IP 地址和端口。 -""" + en: """IP address and port for the listening socket.""" + zh: """监听套接字的 IP 地址和端口。""" } label: { en: "IP address and port" @@ -1994,7 +1881,7 @@ base_listener_acceptors { base_listener_max_connections { desc { - en: """The maximum number of concurrent connections allowed by the listener. """ + en: """The maximum number of concurrent connections allowed by the listener.""" zh: """监听器允许的最大并发连接数。""" } label: { @@ -2005,8 +1892,7 @@ base_listener_max_connections { base_listener_mountpoint { desc { - en: """ -When publishing or subscribing, prefix all topics with a mountpoint string. + en: """When publishing or subscribing, prefix all topics with a mountpoint string. The prefixed string will be removed from the topic name when the message is delivered to the subscriber. The mountpoint is a way that users can use to implement isolation of message routing between different listeners. @@ -2020,10 +1906,8 @@ Set to `""` to disable the feature.
Variables in mountpoint string: - ${clientid}: clientid - - ${username}: username -""" - zh: """ -发布或订阅时,请在所有主题前面加上 mountpoint 字符串。 + - ${username}: username""" + zh: """发布或订阅时,请在所有主题前面加上 mountpoint 字符串。 将消息传递给订阅者时,将从主题名称中删除带前缀的字符串。挂载点是一种用户可以用来实现不同侦听器之间消息路由隔离的方法。 @@ -2034,8 +1918,7 @@ Variables in mountpoint string: mountpoint 字符串中的变量: - ${clientid}: clientid -- ${username}: username -""" +- ${username}: username""" } label: { en: "mountpoint" @@ -2045,12 +1928,8 @@ mountpoint 字符串中的变量: base_listener_zone { desc { - en: """ -The configuration zone to which the listener belongs. -""" - zh: """ -监听器所属的配置组。 -""" + en: """The configuration zone to which the listener belongs.""" + zh: """监听器所属的配置组。""" } label: { en: "Zone" @@ -2060,12 +1939,8 @@ The configuration zone to which the listener belongs. base_listener_limiter { desc { - en: """ -Type of the rate limit. -""" - zh: """ -速率限制类型 -""" + en: """Type of the rate limit.""" + zh: """速率限制类型""" } label: { en: "Type of the rate limit." @@ -2075,20 +1950,16 @@ Type of the rate limit. base_listener_enable_authn { desc { - en: """ -Set true (default) to enable client authentication on this listener, the authentication + en: """Set true (default) to enable client authentication on this listener, the authentication process goes through the configured authentication chain. When set to false to allow any clients with or without authentication information such as username or password to log in. When set to quick_deny_anonymous, it behaves like when set to true, but clients will be denied immediately without going through any authenticators if username is not provided. This is useful to fence off -anonymous clients early. -""" - zh: """ -配置 true (默认值)启用客户端进行身份认证,通过检查认配置的认认证器链来决定是否允许接入。 +anonymous clients early.""" + zh: """配置 true (默认值)启用客户端进行身份认证,通过检查认配置的认认证器链来决定是否允许接入。 配置 false 时,将不对客户端做任何认证,任何客户端,不论是不是携带用户名等认证信息,都可以接入。 配置 quick_deny_anonymous 时,行为跟 true 类似,但是会对匿名 -客户直接拒绝,不做使用任何认证器对客户端进行身份检查。 -""" +客户直接拒绝,不做使用任何认证器对客户端进行身份检查。""" } label: { en: "Enable authentication" @@ -2098,9 +1969,7 @@ anonymous clients early. mqtt_listener_access_rules { desc { - en: """ -The access control rules for this listener.
See: https://github.com/emqtt/esockd#allowdeny -""" + en: """The access control rules for this listener.
See: https://github.com/emqtt/esockd#allowdeny""" zh: """此监听器的访问控制规则。""" } label: { @@ -2111,14 +1980,10 @@ The access control rules for this listener.
See: https://github.com/emqtt/es mqtt_listener_proxy_protocol { desc { - en: """ -Enable the Proxy Protocol V1/2 if the EMQX cluster is deployed behind HAProxy or Nginx.
-See: https://www.haproxy.com/blog/haproxy/proxy-protocol/ -""" - zh: """ -如果EMQX集群部署在 HAProxy 或 Nginx 之后,请启用代理协议 V1/2
-详情见: https://www.haproxy.com/blog/haproxy/proxy-protocol/ -""" + en: """Enable the Proxy Protocol V1/2 if the EMQX cluster is deployed behind HAProxy or Nginx.
+See: https://www.haproxy.com/blog/haproxy/proxy-protocol/""" + zh: """如果EMQX集群部署在 HAProxy 或 Nginx 之后,请启用代理协议 V1/2
+详情见: https://www.haproxy.com/blog/haproxy/proxy-protocol/""" } label: { en: "Proxy protocol" @@ -2128,12 +1993,8 @@ See: https://www.haproxy.com/blog/haproxy/proxy-protocol/ mqtt_listener_proxy_protocol_timeout { desc { - en: """ -Timeout for proxy protocol. EMQX will close the TCP connection if proxy protocol packet is not received within the timeout. -""" - zh: """ -代理协议超时。如果在超时时间内未收到代理协议数据包,EMQX将关闭TCP连接。 -""" + en: """Timeout for proxy protocol. EMQX will close the TCP connection if proxy protocol packet is not received within the timeout.""" + zh: """代理协议超时。如果在超时时间内未收到代理协议数据包,EMQX将关闭TCP连接。""" } label: { en: "Proxy protocol timeout" @@ -2156,8 +2017,7 @@ This option can be configured with: When a chain is configured, the login credentials are checked against the backends per the configured order, until an 'allow' or 'deny' decision can be made. -If there is no decision after a full chain exhaustion, the login is rejected. -""" +If there is no decision after a full chain exhaustion, the login is rejected.""" zh: """全局 MQTT 监听器的默认认证配置。 为每个监听器配置认证参考监听器器配置中的authentication 配置。 该配置可以被配置为: @@ -2169,25 +2029,18 @@ If there is no decision after a full chain exhaustion, the login is rejected. 当配置为认证链后,登录凭证会按照配置的顺序进行检查,直到做出allowdeny的结果。 -如果在所有的认证器都执行完后,还是没有结果,登录将被拒绝。 -""" +如果在所有的认证器都执行完后,还是没有结果,登录将被拒绝。""" } } listener_authentication { desc { - en: """ -Per-listener authentication override. + en: """Per-listener authentication override. Authentication can be one single authenticator instance or a chain of authenticators as an array. -When authenticating a login (username, client ID, etc.) the authenticators are checked in the configured order.
- -""" - zh: """ -监听器认证重载。 - +When authenticating a login (username, client ID, etc.) the authenticators are checked in the configured order.""" + zh: """监听器认证重载。 认证配置可以是单个认证器实例,也可以是一个认证器数组组成的认证链。 -执行登录验证时(用户名、客户端 ID 等),将按配置的顺序执行。 -""" +执行登录验证时(用户名、客户端 ID 等),将按配置的顺序执行。""" } label: { en: "Per-listener authentication override" @@ -2219,14 +2072,9 @@ fields_rate_limit_conn_messages_in { fields_rate_limit_conn_bytes_in { desc { - en: """ -Limit the rate of receiving packets for a MQTT connection. -The rate is counted by bytes of packets per second. -""" - zh: """ -限制 MQTT 连接接收数据包的速率。 -速率以每秒的数据包字节数计算。 -""" + en: """Limit the rate of receiving packets for a MQTT connection. +The rate is counted by bytes of packets per second.""" + zh: """限制 MQTT 连接接收数据包的速率。 速率以每秒的数据包字节数计算。""" } label: { en: "Connection bytes in" @@ -2236,8 +2084,7 @@ The rate is counted by bytes of packets per second. client_ssl_opts_schema_server_name_indication { desc { - en: """ -Specify the host name to be used in TLS Server Name Indication extension.
+ en: """Specify the host name to be used in TLS Server Name Indication extension.
For instance, when connecting to "server.example.net", the genuine server which accepts the connection and performs TLS handshake may differ from the host the TLS client initially connects to, e.g. when connecting to an IP address @@ -2247,17 +2094,14 @@ to establish the connection, unless it is IP addressed used.
The host name is then also used in the host name verification of the peer certificate.
The special value 'disable' prevents the Server Name Indication extension from being sent and disables the hostname -verification check. -""" - zh: """ -指定要在 TLS 服务器名称指示扩展中使用的主机名。
+verification check.""" + zh: """指定要在 TLS 服务器名称指示扩展中使用的主机名。
例如,当连接到 "server.example.net" 时,接受连接并执行 TLS 握手的真正服务器可能与 TLS 客户端最初连接到的主机不同, 例如,当连接到 IP 地址时,或者当主机具有多个可解析的 DNS 记录时
如果未指定,它将默认为使用的主机名字符串 建立连接,除非使用 IP 地址
然后,主机名也用于对等机的主机名验证证书
-特殊值 disable 阻止发送服务器名称指示扩展,并禁用主机名验证检查。 -""" +特殊值 disable 阻止发送服务器名称指示扩展,并禁用主机名验证检查。""" } label: { en: "Server Name Indication" @@ -2267,14 +2111,10 @@ verification check. fields_tcp_opts_active_n { desc { - en: """ -Specify the {active, N} option for this Socket.
-See: https://erlang.org/doc/man/inet.html#setopts-2 -""" - zh: """ -为此套接字指定{active,N}选项
-See: https://erlang.org/doc/man/inet.html#setopts-2 -""" + en: """Specify the {active, N} option for this Socket.
+See: https://erlang.org/doc/man/inet.html#setopts-2""" + zh: """为此套接字指定{active,N}选项
+See: https://erlang.org/doc/man/inet.html#setopts-2""" } label: { en: "active_n" @@ -2284,13 +2124,9 @@ See: https://erlang.org/doc/man/inet.html#setopts-2 fields_tcp_opts_backlog { desc { - en: """ -TCP backlog defines the maximum length that the queue of - pending connections can grow to. -""" - zh: """ -TCP backlog 定义了挂起连接队列可以增长到的最大长度。 -""" + en: """TCP backlog defines the maximum length that the queue of +pending connections can grow to.""" + zh: """TCP backlog 定义了挂起连接队列可以增长到的最大长度。""" } label: { en: "TCP backlog length" @@ -2300,7 +2136,7 @@ TCP backlog 定义了挂起连接队列可以增长到的最大长度。 fields_tcp_opts_send_timeout { desc { - en: """The TCP send timeout for the connections. """ + en: """The TCP send timeout for the connections.""" zh: """连接的 TCP 发送超时。""" } label: { @@ -2311,12 +2147,8 @@ fields_tcp_opts_send_timeout { fields_tcp_opts_send_timeout_close { desc { - en: """ -Close the connection if send timeout. -""" - zh: """ -如果发送超时,则关闭连接。 -""" + en: """Close the connection if send timeout.""" + zh: """如果发送超时,则关闭连接。""" } label: { en: "TCP send timeout close" @@ -2326,12 +2158,8 @@ Close the connection if send timeout. fields_tcp_opts_recbuf { desc { - en: """ -The TCP receive buffer (OS kernel) for the connections. -""" - zh: """ -连接的 TCP 接收缓冲区(OS 内核)。 -""" + en: """The TCP receive buffer (OS kernel) for the connections.""" + zh: """连接的 TCP 接收缓冲区(OS 内核)。""" } label: { en: "TCP receive buffer" @@ -2341,12 +2169,8 @@ The TCP receive buffer (OS kernel) for the connections. fields_tcp_opts_sndbuf { desc { - en: """ -The TCP send buffer (OS kernel) for the connections. -""" - zh: """ -连接的 TCP 发送缓冲区(OS 内核)。 -""" + en: """The TCP send buffer (OS kernel) for the connections.""" + zh: """连接的 TCP 发送缓冲区(OS 内核)。""" } label: { en: "TCP send buffer" @@ -2356,12 +2180,8 @@ The TCP send buffer (OS kernel) for the connections. fields_tcp_opts_buffer { desc { - en: """ -The size of the user-space buffer used by the driver. -""" - zh: """ -驱动程序使用的用户空间缓冲区的大小。 -""" + en: """The size of the user-space buffer used by the driver.""" + zh: """驱动程序使用的用户空间缓冲区的大小。""" } label: { en: "TCP user-space buffer" @@ -2371,13 +2191,9 @@ The size of the user-space buffer used by the driver. fields_tcp_opts_high_watermark { desc { - en: """ -The socket is set to a busy state when the amount of data queued internally - by the VM socket implementation reaches this limit. -""" - zh: """ -当 VM 套接字实现内部排队的数据量达到此限制时,套接字将设置为忙碌状态。 -""" + en: """The socket is set to a busy state when the amount of data queued internally +by the VM socket implementation reaches this limit.""" + zh: """当 VM 套接字实现内部排队的数据量达到此限制时,套接字将设置为忙碌状态。""" } label: { en: "TCP 高水位线" @@ -2387,12 +2203,8 @@ The socket is set to a busy state when the amount of data queued internally fields_tcp_opts_nodelay { desc { - en: """ -The TCP_NODELAY flag for the connections. -""" - zh: """ -连接的 TCP_NODELAY 标识 -""" + en: """The TCP_NODELAY flag for the connections.""" + zh: """连接的 TCP_NODELAY 标识""" } label: { en: "TCP_NODELAY" @@ -2402,12 +2214,8 @@ The TCP_NODELAY flag for the connections. fields_tcp_opts_reuseaddr { desc { - en: """ -The SO_REUSEADDR flag for the connections. -""" - zh: """ -连接的 SO_REUSEADDR 标识 -""" + en: """The SO_REUSEADDR flag for the connections.""" + zh: """连接的 SO_REUSEADDR 标识。""" } label: { en: "SO_REUSEADDR" @@ -2417,21 +2225,16 @@ The SO_REUSEADDR flag for the connections. fields_trace_payload_encode { desc { - en: """ -Determine the format of the payload format in the trace file.
+ en: """Determine the format of the payload format in the trace file.
`text`: Text-based protocol or plain text protocol. It is recommended when payload is JSON encoded.
`hex`: Binary hexadecimal encode. It is recommended when payload is a custom binary protocol.
-`hidden`: payload is obfuscated as `******` - -""" - zh: """ -确定跟踪文件中有效负载格式的格式。
+`hidden`: payload is obfuscated as `******`""" + zh: """确定跟踪文件中有效负载格式的格式。
`text`:基于文本的协议或纯文本协议。 建议在有效负载为JSON编码时使用
`hex`:二进制十六进制编码。当有效负载是自定义二进制协议时,建议使用此选项
-`hidden`:有效负载被模糊化为 `******` -""" +`hidden`:有效负载被模糊化为 `******`""" } label: { en: "Payload encode" @@ -2441,14 +2244,10 @@ Determine the format of the payload format in the trace file.
fields_ws_opts_mqtt_path { desc { - en: """ -WebSocket's MQTT protocol path. So the address of EMQX Broker's WebSocket is: -ws://{ip}:{port}/mqtt -""" - zh: """ -WebSocket 的 MQTT 协议路径。因此,EMQX Broker的WebSocket地址为: -ws://{ip}:{port}/mqtt -""" + en: """WebSocket's MQTT protocol path. So the address of EMQX Broker's WebSocket is: +ws://{ip}:{port}/mqtt""" + zh: """WebSocket 的 MQTT 协议路径。因此,EMQX Broker的WebSocket地址为: +ws://{ip}:{port}/mqtt""" } label: { en: "WS MQTT Path" @@ -2458,12 +2257,8 @@ WebSocket 的 MQTT 协议路径。因此,EMQX Broker的WebSocket地址为: fields_ws_opts_mqtt_piggyback { desc { - en: """ -Whether a WebSocket message is allowed to contain multiple MQTT packets. -""" - zh: """ -WebSocket消息是否允许包含多个 MQTT 数据包。 -""" + en: """Whether a WebSocket message is allowed to contain multiple MQTT packets.""" + zh: """WebSocket消息是否允许包含多个 MQTT 数据包。""" } label: { en: "MQTT Piggyback" @@ -2473,14 +2268,10 @@ WebSocket消息是否允许包含多个 MQTT 数据包。 fields_ws_opts_compress { desc { - en: """ -If true, compress WebSocket messages using zlib.
-The configuration items under deflate_opts belong to the compression-related parameter configuration. -""" - zh: """ -如果 true,则使用zlib 压缩 WebSocket 消息
-deflate_opts 下的配置项属于压缩相关参数配置。 -""" + en: """If true, compress WebSocket messages using zlib.
+The configuration items under deflate_opts belong to the compression-related parameter configuration.""" + zh: """如果 true,则使用zlib 压缩 WebSocket 消息
+deflate_opts 下的配置项属于压缩相关参数配置。""" } label: { en: "Ws compress" @@ -2490,13 +2281,8 @@ The configuration items under deflate_opts belong to the compressio fields_ws_opts_idle_timeout { desc { - en: """ -Close transport-layer connections from the clients that have not sent MQTT CONNECT -message within this interval. -""" - zh: """ -关闭在此间隔内未发送 MQTT CONNECT 消息的客户端的传输层连接。 -""" + en: """Close transport-layer connections from the clients that have not sent MQTT CONNECT message within this interval.""" + zh: """关闭在此间隔内未发送 MQTT CONNECT 消息的客户端的传输层连接。""" } label: { en: "WS idle timeout" @@ -2506,12 +2292,8 @@ message within this interval. fields_ws_opts_max_frame_size { desc { - en: """ -The maximum length of a single MQTT packet. -""" - zh: """ -单个 MQTT 数据包的最大长度。 -""" + en: """The maximum length of a single MQTT packet.""" + zh: """单个 MQTT 数据包的最大长度。""" } label: { en: "Max frame size" @@ -2521,15 +2303,11 @@ The maximum length of a single MQTT packet. fields_ws_opts_fail_if_no_subprotocol { desc { - en: """ -If true, the server will return an error when + en: """If true, the server will return an error when the client does not carry the Sec-WebSocket-Protocol field. -
Note: WeChat applet needs to disable this verification. -""" - zh: """ -如果true,当客户端未携带Sec WebSocket Protocol字段时,服务器将返回一个错误。 -
注意:微信小程序需要禁用此验证。 -""" +
Note: WeChat applet needs to disable this verification.""" + zh: """如果true,当客户端未携带Sec WebSocket Protocol字段时,服务器将返回一个错误。 +
注意:微信小程序需要禁用此验证。""" } label: { en: "Fail if no subprotocol" @@ -2539,12 +2317,8 @@ If true, the server will return an error when fields_ws_opts_supported_subprotocols { desc { - en: """ -Comma-separated list of supported subprotocols. -""" - zh: """ -逗号分隔的 subprotocols 支持列表。 -""" + en: """Comma-separated list of supported subprotocols.""" + zh: """逗号分隔的 subprotocols 支持列表。""" } label: { en: "Supported subprotocols" @@ -2554,14 +2328,10 @@ Comma-separated list of supported subprotocols. fields_ws_opts_check_origin_enable { desc { - en: """ -If true, origin HTTP header will be + en: """If true, origin HTTP header will be validated against the list of allowed origins configured in check_origins - parameter. -""" - zh: """ -如果trueoriginHTTP 头将根据check_origins参数中配置的允许来源列表进行验证。 -""" + parameter.""" + zh: """如果trueoriginHTTP 头将根据check_origins参数中配置的允许来源列表进行验证。""" } label: { en: "Check origin" @@ -2571,14 +2341,10 @@ If true, origin HTTP header will be fields_ws_opts_allow_origin_absence { desc { - en: """ -If false and check_origin_enable is + en: """If false and check_origin_enable is true, the server will reject requests that don't have origin - HTTP header. -""" - zh: """ -If false and check_origin_enable is true, the server will reject requests that don't have origin HTTP header. -""" + HTTP header.""" + zh: """If false and check_origin_enable is true, the server will reject requests that don't have origin HTTP header.""" } label: { en: "Allow origin absence" @@ -2588,12 +2354,8 @@ If false and check_origin_enable is true, fields_ws_opts_check_origins { desc { - en: """ -List of allowed origins.
See check_origin_enable. -""" - zh: """ -允许的 origins 列表 -""" + en: """List of allowed origins.
See check_origin_enable.""" + zh: """允许的 origins 列表""" } label: { en: "Allowed origins" @@ -2603,14 +2365,10 @@ List of allowed origins.
See check_origin_enable. fields_ws_opts_proxy_address_header { desc { - en: """ -HTTP header used to pass information about the client IP address. -Relevant when the EMQX cluster is deployed behind a load-balancer. -""" - zh: """ -HTTP 头,用于传递有关客户端 IP 地址的信息。 -当 EMQX 集群部署在负载平衡器后面时,这一点非常重要。 -""" + en: """HTTP header used to pass information about the client IP address. +Relevant when the EMQX cluster is deployed behind a load-balancer.""" + zh: """HTTP 头,用于传递有关客户端 IP 地址的信息。 +当 EMQX 集群部署在负载平衡器后面时,这一点非常重要。""" } label: { en: "Proxy address header" @@ -2620,14 +2378,8 @@ HTTP 头,用于传递有关客户端 IP 地址的信息。 fields_ws_opts_proxy_port_header { desc { - en: """ -HTTP header used to pass information about the client port. -Relevant when the EMQX cluster is deployed behind a load-balancer. -""" - zh: """ -HTTP 头,用于传递有关客户端端口的信息。 -当 EMQX 集群部署在负载平衡器后面时,这一点非常重要。 -""" + en: """HTTP header used to pass information about the client port. Relevant when the EMQX cluster is deployed behind a load-balancer.""" + zh: """HTTP 头,用于传递有关客户端端口的信息。当 EMQX 集群部署在负载平衡器后面时,这一点非常重要。""" } label: { en: "Proxy port header" diff --git a/apps/emqx_authn/i18n/emqx_authn_jwt_i18n.conf b/apps/emqx_authn/i18n/emqx_authn_jwt_i18n.conf index eb243c0cb..6a4a1e2d4 100644 --- a/apps/emqx_authn/i18n/emqx_authn_jwt_i18n.conf +++ b/apps/emqx_authn/i18n/emqx_authn_jwt_i18n.conf @@ -133,20 +133,16 @@ emqx_authn_jwt { verify_claims { desc { - en: """ -A list of custom claims to validate, which is a list of name/value pairs. + en: """A list of custom claims to validate, which is a list of name/value pairs. Values can use the following placeholders: - ${username}: Will be replaced at runtime with Username used by the client when connecting - ${clientid}: Will be replaced at runtime with Client ID used by the client when connecting -Authentication will verify that the value of claims in the JWT (taken from the Password field) matches what is required in verify_claims. -""" - zh: """ -需要验证的自定义声明列表,它是一个名称/值对列表。 +Authentication will verify that the value of claims in the JWT (taken from the Password field) matches what is required in verify_claims.""" + zh: """需要验证的自定义声明列表,它是一个名称/值对列表。 值可以使用以下占位符: - ${username}: 将在运行时被替换为客户端连接时使用的用户名 - ${clientid}: 将在运行时被替换为客户端连接时使用的客户端标识符 -认证时将验证 JWT(取自 Password 字段)中 claims 的值是否与 verify_claims 中要求的相匹配。 -""" +认证时将验证 JWT(取自 Password 字段)中 claims 的值是否与 verify_claims 中要求的相匹配。""" } label { en: """Verify Claims""" diff --git a/apps/emqx_authn/i18n/emqx_authn_mongodb_i18n.conf b/apps/emqx_authn/i18n/emqx_authn_mongodb_i18n.conf index 440e6edb1..80d6473ed 100644 --- a/apps/emqx_authn/i18n/emqx_authn_mongodb_i18n.conf +++ b/apps/emqx_authn/i18n/emqx_authn_mongodb_i18n.conf @@ -33,18 +33,14 @@ emqx_authn_mongodb { filter { desc { - en: """ -Conditional expression that defines the filter condition in the query. + en: """Conditional expression that defines the filter condition in the query. Filter supports the following placeholders: - ${username}: Will be replaced at runtime with Username used by the client when connecting -- ${clientid}: Will be replaced at runtime with Client ID used by the client when connecting -""" - zh: """ -在查询中定义过滤条件的条件表达式。 +- ${clientid}: Will be replaced at runtime with Client ID used by the client when connecting""" + zh: """在查询中定义过滤条件的条件表达式。 过滤器支持如下占位符: - ${username}: 将在运行时被替换为客户端连接时使用的用户名 -- ${clientid}: 将在运行时被替换为客户端连接时使用的客户端标识符 -""" +- ${clientid}: 将在运行时被替换为客户端连接时使用的客户端标识符""" } label: { en: """Filter""" diff --git a/apps/emqx_authz/i18n/emqx_authz_api_schema_i18n.conf b/apps/emqx_authz/i18n/emqx_authz_api_schema_i18n.conf index afec5c109..d649dd5a0 100644 --- a/apps/emqx_authz/i18n/emqx_authz_api_schema_i18n.conf +++ b/apps/emqx_authz/i18n/emqx_authz_api_schema_i18n.conf @@ -121,18 +121,14 @@ emqx_authz_api_schema { filter { desc { - en: """ -Conditional expression that defines the filter condition in the query. + en: """Conditional expression that defines the filter condition in the query. Filter supports the following placeholders: - - ${username}: Will be replaced at runtime with Username used by the client when connecting - - ${clientid}: Will be replaced at runtime with Client ID used by the client when connecting -""" - zh: """ -在查询中定义过滤条件的条件表达式。 + - ${username}: Will be replaced at runtime with Username used by the client when connecting; + - ${clientid}: Will be replaced at runtime with Client ID used by the client when connecting.""" + zh: """在查询中定义过滤条件的条件表达式。 过滤器支持如下占位符: - ${username}: 将在运行时被替换为客户端连接时使用的用户名 -- ${clientid}: 将在运行时被替换为客户端连接时使用的客户端标识符 -""" +- ${clientid}: 将在运行时被替换为客户端连接时使用的客户端标识符""" } label { en: """Filter""" diff --git a/apps/emqx_authz/i18n/emqx_authz_schema_i18n.conf b/apps/emqx_authz/i18n/emqx_authz_schema_i18n.conf index a10128592..e48c64b89 100644 --- a/apps/emqx_authz/i18n/emqx_authz_schema_i18n.conf +++ b/apps/emqx_authz/i18n/emqx_authz_schema_i18n.conf @@ -1,8 +1,7 @@ emqx_authz_schema { sources { desc { - en: """ -Authorization data sources.
+ en: """Authorization data sources.
An array of authorization (ACL) data providers. It is designed as an array, not a hash-map, so the sources can be ordered to form a chain of access controls.
@@ -18,10 +17,9 @@ the default action configured in 'authorization.no_match' is applied.
NOTE: The source elements are identified by their 'type'. -It is NOT allowed to configure two or more sources of the same type. -""" - zh: """ -授权数据源。
+It is NOT allowed to configure two or more sources of the same type.""" + + zh: """授权数据源。
授权(ACL)数据源的列表。 它被设计为一个数组,而不是一个散列映射, 所以可以作为链式访问控制。
@@ -36,8 +34,7 @@ It is NOT allowed to configure two or more sources of the same type. 注意: 数据源使用 'type' 进行标识。 -使用同一类型的数据源多于一次不被允许。 -""" +使用同一类型的数据源多于一次不被允许。""" } label { en: """sources""" @@ -93,18 +90,15 @@ It is NOT allowed to configure two or more sources of the same type. path { desc { - en: """ -Path to the file which contains the ACL rules. + en: """Path to the file which contains the ACL rules. If the file provisioned before starting EMQX node, it can be placed anywhere as long as EMQX has read access to it. That is, EMQX will treat it as read only. In case the rule-set is created or updated from EMQX Dashboard or HTTP API, a new file will be created and placed in `authz` subdirectory inside EMQX's `data_dir`, -and the old file will not be used anymore. -""" - zh: """ -包含 ACL 规则的文件路径。 +and the old file will not be used anymore.""" + zh: """包含 ACL 规则的文件路径。 如果在启动 EMQX 节点前预先配置该路径, 那么可以将该文件置于任何 EMQX 可以访问到的位置。 @@ -269,18 +263,14 @@ and the old file will not be used anymore. filter { desc { - en: """ -Conditional expression that defines the filter condition in the query. -Filter supports the following placeholders: - - ${username}: Will be replaced at runtime with Username used by the client when connecting - - ${clientid}: Will be replaced at runtime with Client ID used by the client when connecting -""" - zh: """ -在查询中定义过滤条件的条件表达式。 -过滤器支持如下占位符: -- ${username}:将在运行时被替换为客户端连接时使用的用户名 -- ${clientid}:将在运行时被替换为客户端连接时使用的客户端标识符 -""" + en: """Conditional expression that defines the filter condition in the query. +Filter supports the following placeholders
+ - ${username}: Will be replaced at runtime with Username used by the client when connecting
+ - ${clientid}: Will be replaced at runtime with Client ID used by the client when connecting""" + zh: """在查询中定义过滤条件的条件表达式。 +过滤器支持如下占位符:
+- ${username}:将在运行时被替换为客户端连接时使用的用户名
+- ${clientid}:将在运行时被替换为客户端连接时使用的客户端标识符""" } label { en: """Filter""" diff --git a/apps/emqx_bridge/i18n/emqx_bridge_api.conf b/apps/emqx_bridge/i18n/emqx_bridge_api.conf index 9d1911e1b..73f78609d 100644 --- a/apps/emqx_bridge/i18n/emqx_bridge_api.conf +++ b/apps/emqx_bridge/i18n/emqx_bridge_api.conf @@ -57,7 +57,7 @@ emqx_bridge_api { desc_api1 { desc { en: """List all created bridges""" - zh: """列出所有 Birdge """ + zh: """列出所有 Birdge""" } label: { en: "List All Bridges" @@ -144,14 +144,10 @@ emqx_bridge_api { desc_api9 { desc { - en: """ -Test creating a new bridge by given ID
-The ID must be of format '{type}:{name}' -""" - zh: """ -通过给定的 ID 测试创建一个新的桥接。
-ID 的格式必须为 ’{type}:{name}” -""" + en: """Test creating a new bridge by given ID
+The ID must be of format '{type}:{name}'""" + zh: """通过给定的 ID 测试创建一个新的桥接。
+ID 的格式必须为 ’{type}:{name}”""" } label: { en: "Test Bridge Creation" diff --git a/apps/emqx_bridge/i18n/emqx_bridge_webhook_schema.conf b/apps/emqx_bridge/i18n/emqx_bridge_webhook_schema.conf index a6e75a766..cf86b63e0 100644 --- a/apps/emqx_bridge/i18n/emqx_bridge_webhook_schema.conf +++ b/apps/emqx_bridge/i18n/emqx_bridge_webhook_schema.conf @@ -22,22 +22,18 @@ emqx_bridge_webhook_schema { } config_url { desc { - en: """ -The URL of the HTTP Bridge.
+ en: """The URL of the HTTP Bridge.
Template with variables is allowed in the path, but variables cannot be used in the scheme, host, or port part.
For example, http://localhost:9901/${topic} is allowed, but http://${host}:9901/message or http://localhost:${port}/message -is not allowed. -""" - zh: """ -HTTP Bridge 的 URL。
+is not allowed.""" + zh: """HTTP Bridge 的 URL。
路径中允许使用带变量的模板,但是 host, port 不允许使用变量模板。
例如, http://localhost:9901/${topic} 是允许的, 但是 http://${host}:9901/message http://localhost:${port}/message -不允许。 -""" +不允许。""" } label: { en: "HTTP Bridge" @@ -47,17 +43,13 @@ HTTP Bridge 的 URL。
config_local_topic { desc { - en: """ -The MQTT topic filter to be forwarded to the HTTP server. All MQTT 'PUBLISH' messages with the topic + en: """The MQTT topic filter to be forwarded to the HTTP server. All MQTT 'PUBLISH' messages with the topic matching the local_topic will be forwarded.
NOTE: if this bridge is used as the action of a rule (EMQX rule engine), and also local_topic is configured, then both the data got from the rule and the MQTT messages that match local_topic -will be forwarded. -""" - zh: """ -发送到 'local_topic' 的消息都会转发到 HTTP 服务器。
-注意:如果这个 Bridge 被用作规则(EMQX 规则引擎)的输出,同时也配置了 'local_topic' ,那么这两部分的消息都会被转发到 HTTP 服务器。 -""" +will be forwarded.""" + zh: """发送到 'local_topic' 的消息都会转发到 HTTP 服务器。
+注意:如果这个 Bridge 被用作规则(EMQX 规则引擎)的输出,同时也配置了 'local_topic' ,那么这两部分的消息都会被转发到 HTTP 服务器。""" } label: { en: "Local Topic" @@ -67,13 +59,10 @@ will be forwarded. config_method { desc { - en: """ -The method of the HTTP request. All the available methods are: post, put, get, delete.
-Template with variables is allowed.
-""" - zh: """ -HTTP 请求的方法。 所有可用的方法包括:post、put、get、delete。
-允许使用带有变量的模板。
""" + en: """The method of the HTTP request. All the available methods are: post, put, get, delete.
+Template with variables is allowed.""" + zh: """HTTP 请求的方法。 所有可用的方法包括:post、put、get、delete。
+允许使用带有变量的模板。""" } label: { en: "HTTP Method" @@ -83,14 +72,10 @@ HTTP 请求的方法。 所有可用的方法包括:post、put、get、delete config_headers { desc { - en: """ -The headers of the HTTP request.
-Template with variables is allowed. -""" - zh: """ -HTTP 请求的标头。
-允许使用带有变量的模板。 -""" + en: """The headers of the HTTP request.
+Template with variables is allowed.""" + zh: """HTTP 请求的标头。
+允许使用带有变量的模板。""" } label: { en: "HTTP Header" @@ -100,22 +85,18 @@ HTTP 请求的标头。
config_body { desc { - en: """ -The body of the HTTP request.
+ en: """The body of the HTTP request.
If not provided, the body will be a JSON object of all the available fields.
There, 'all the available fields' means the context of a MQTT message when this webhook is triggered by receiving a MQTT message (the `local_topic` is set), or the context of the event when this webhook is triggered by a rule (i.e. this webhook is used as an action of a rule).
-Template with variables is allowed. -""" - zh: """ -HTTP 请求的正文。
+Template with variables is allowed.""" + zh: """HTTP 请求的正文。
如果没有设置该字段,请求正文将是包含所有可用字段的 JSON object。
如果该 webhook 是由于收到 MQTT 消息触发的,'所有可用字段' 将是 MQTT 消息的 上下文信息;如果该 webhook 是由于规则触发的,'所有可用字段' 则为触发事件的上下文信息。
-允许使用带有变量的模板。 -""" +允许使用带有变量的模板。""" } label: { en: "HTTP Body" diff --git a/apps/emqx_conf/i18n/emqx_conf_schema.conf b/apps/emqx_conf/i18n/emqx_conf_schema.conf index 56da34621..2f0c6b938 100644 --- a/apps/emqx_conf/i18n/emqx_conf_schema.conf +++ b/apps/emqx_conf/i18n/emqx_conf_schema.conf @@ -15,12 +15,11 @@ emqx_conf_schema { desc { en: """Maximum number of simultaneously existing processes for this Erlang system. The actual maximum chosen may be much larger than the Number passed. -For more information, see: https://www.erlang.org/doc/man/erl.html - """ +For more information, see: https://www.erlang.org/doc/man/erl.html""" + zh: """Erlang系统同时存在的最大进程数。 实际选择的最大值可能比设置的数字大得多。 -参考: https://www.erlang.org/doc/man/erl.html - """ +参考: https://www.erlang.org/doc/man/erl.html""" } label { en: "Erlang Process Limit" @@ -32,12 +31,11 @@ For more information, see: https://www.erlang.org/doc/man/erl.html desc { en: """Maximum number of simultaneously existing ports for this Erlang system. The actual maximum chosen may be much larger than the Number passed. -For more information, see: https://www.erlang.org/doc/man/erl.html - """ +For more information, see: https://www.erlang.org/doc/man/erl.html""" + zh: """Erlang系统同时存在的最大端口数。 实际选择的最大值可能比设置的数字大得多。 -参考: https://www.erlang.org/doc/man/erl.html - """ +参考: https://www.erlang.org/doc/man/erl.html""" } label { en: "Erlang Port Limit" @@ -69,15 +67,19 @@ For more information, see: https://www.erlang.org/doc/man/erl.html cluster_discovery_strategy { desc { - en: """Service discovery method for the cluster nodes.""" + en: """Service discovery method for the cluster nodes. Possible values are: +- manual: Use emqx ctl cluster command to manage cluster.
+- static: Configure static nodes list by setting seeds in config file.
+- dns: Use DNS A record to discover peer nodes.
+- etcd: Use etcd to discover peer nodes.
+- k8s: Use Kubernetes API to discover peer pods.""" + zh: """集群节点发现方式。可选值为: -- manual: 手动加入集群
+- manual: 使用 emqx ctl cluster 命令管理集群。
- static: 配置静态节点。配置几个固定的节点,新节点通过连接固定节点中的某一个来加入集群。
-- mcast: 使用 UDP 多播的方式发现节点。
- dns: 使用 DNS A 记录的方式发现节点。
- etcd: 使用 etcd 发现节点。
-- k8s: 使用 Kubernetes 发现节点。
- """ +- k8s: 使用 Kubernetes API 发现节点。""" } label { en: "Cluster Discovery Strategy" @@ -109,12 +111,12 @@ For more information, see: https://www.erlang.org/doc/man/erl.html cluster_proto_dist { desc { - en: """The Erlang distribution protocol for the cluster.""" - zh: """分布式 Erlang 集群协议类型。可选值为: + en: """The Erlang distribution protocol for the cluster.
+- inet_tcp: IPv4 TCP
+- inet_tls: IPv4 TLS, works together with etc/ssl_dist.conf""" + zh: """分布式 Erlang 集群协议类型。可选值为:
- inet_tcp: 使用 IPv4
-- inet6_tcp 使用 IPv6
-- inet_tls: 使用 TLS,需要与 node.ssl_dist_optfile 配置一起使用。
- """ +- inet_tls: 使用 TLS,需要配合 etc/ssl_dist.conf 一起使用。""" } label { en: "Cluster Protocol Distribution" @@ -128,8 +130,7 @@ For more information, see: https://www.erlang.org/doc/man/erl.html zh: """集群中的EMQX节点名称列表, 指定固定的节点列表,多个节点间使用逗号 , 分隔。 当 cluster.discovery_strategy 为 static 时,此配置项才有效。 -适合于节点数量较少且固定的集群。 - """ +适合于节点数量较少且固定的集群。""" } label { en: "Cluster Static Seeds" @@ -141,8 +142,7 @@ For more information, see: https://www.erlang.org/doc/man/erl.html desc { en: """Multicast IPv4 address.""" zh: """指定多播 IPv4 地址。 -当 cluster.discovery_strategy 为 mcast 时,此配置项才有效。 - """ +当 cluster.discovery_strategy 为 mcast 时,此配置项才有效。""" } label { en: "Cluster Multicast Address" @@ -153,11 +153,10 @@ For more information, see: https://www.erlang.org/doc/man/erl.html cluster_mcast_ports { desc { en: """List of UDP ports used for service discovery.
-Note: probe messages are broadcast to all the specified ports. - """ +Note: probe messages are broadcast to all the specified ports.""" + zh: """指定多播端口。如有多个端口使用逗号 , 分隔。 -当 cluster.discovery_strategy 为 mcast 时,此配置项才有效。 - """ +当 cluster.discovery_strategy 为 mcast 时,此配置项才有效。""" } label { en: "Cluster Multicast Ports" @@ -169,8 +168,7 @@ Note: probe messages are broadcast to all the specified ports. desc { en: """Local IP address the node discovery service needs to bind to.""" zh: """指定节点发现服务需要绑定到本地 IP 地址。 -当 cluster.discovery_strategy 为 mcast 时,此配置项才有效。 - """ +当 cluster.discovery_strategy 为 mcast 时,此配置项才有效。""" } label { en: "Cluster Multicast Interface" @@ -182,8 +180,7 @@ Note: probe messages are broadcast to all the specified ports. desc { en: """Time-to-live (TTL) for the outgoing UDP datagrams.""" zh: """指定多播的 Time-To-Live 值。 -当 cluster.discovery_strategy 为 mcast 时,此配置项才有效。 - """ +当 cluster.discovery_strategy 为 mcast 时,此配置项才有效。""" } label { en: "Cluster Multicast TTL" @@ -195,8 +192,7 @@ Note: probe messages are broadcast to all the specified ports. desc { en: """If true, loop UDP datagrams back to the local socket.""" zh: """设置多播的报文是否投递到本地回环地址。 -当 cluster.discovery_strategy 为 mcast 时,此配置项才有效。 - """ +当 cluster.discovery_strategy 为 mcast 时,此配置项才有效。""" } label { en: "Cluster Multicast Loop" @@ -208,8 +204,7 @@ Note: probe messages are broadcast to all the specified ports. desc { en: """Size of the kernel-level buffer for outgoing datagrams.""" zh: """外发数据报的内核级缓冲区的大小。 -当 cluster.discovery_strategy 为 mcast 时,此配置项才有效。 - """ +当 cluster.discovery_strategy 为 mcast 时,此配置项才有效。""" } label { en: "Cluster Muticast Sendbuf" @@ -221,8 +216,7 @@ Note: probe messages are broadcast to all the specified ports. desc { en: """Size of the kernel-level buffer for incoming datagrams.""" zh: """接收数据报的内核级缓冲区的大小。 -当 cluster.discovery_strategy 为 mcast 时,此配置项才有效。 - """ +当 cluster.discovery_strategy 为 mcast 时,此配置项才有效。""" } label { en: "Cluster Muticast Sendbuf" @@ -234,8 +228,7 @@ Note: probe messages are broadcast to all the specified ports. desc { en: """Size of the user-level buffer.""" zh: """用户级缓冲区的大小。 -当 cluster.discovery_strategy 为 mcast 时,此配置项才有效。 - """ +当 cluster.discovery_strategy 为 mcast 时,此配置项才有效。""" } label { en: "Cluster Muticast Buffer" @@ -246,11 +239,9 @@ Note: probe messages are broadcast to all the specified ports. cluster_dns_name { desc { en: """The domain name from which to discover peer EMQX nodes' IP addresses. -Applicable when cluster.discovery_strategy = dns -""" +Applicable when cluster.discovery_strategy = dns""" zh: """指定 DNS A 记录的名字。emqx 会通过访问这个 DNS A 记录来获取 IP 地址列表。 -当cluster.discovery_strategydns 时有效。 -""" +当cluster.discovery_strategydns 时有效。""" } label { en: "Cluster Dns Name" @@ -260,7 +251,7 @@ Applicable when cluster.discovery_strategy = dns cluster_dns_record_type { desc { - en: """DNS record type. """ + en: """DNS record type.""" zh: """DNS 记录类型。""" } label { @@ -273,8 +264,7 @@ Applicable when cluster.discovery_strategy = dns desc { en: """List of endpoint URLs of the etcd cluster""" zh: """指定 etcd 服务的地址。如有多个服务使用逗号 , 分隔。 -当 cluster.discovery_strategy 为 etcd 时,此配置项才有效。 - """ +当 cluster.discovery_strategy 为 etcd 时,此配置项才有效。""" } label { en: "Cluster Etcd Server" @@ -287,8 +277,7 @@ Applicable when cluster.discovery_strategy = dns en: """Key prefix used for EMQX service discovery.""" zh: """指定 etcd 路径的前缀。每个节点在 etcd 中都会创建一个路径: v2/keys///
-当 cluster.discovery_strategy 为 etcd 时,此配置项才有效。 - """ +当 cluster.discovery_strategy 为 etcd 时,此配置项才有效。""" } label { en: "Cluster Etcd Prefix" @@ -299,11 +288,10 @@ v2/keys///
cluster_etcd_node_ttl { desc { en: """Expiration time of the etcd key associated with the node. -It is refreshed automatically, as long as the node is alive. - """ +It is refreshed automatically, as long as the node is alive.""" + zh: """指定 etcd 中节点信息的过期时间。 -当 cluster.discovery_strategy 为 etcd 时,此配置项才有效。 - """ +当 cluster.discovery_strategy 为 etcd 时,此配置项才有效。""" } label { en: "Cluster Etcd Node TTL" @@ -315,8 +303,7 @@ It is refreshed automatically, as long as the node is alive. desc { en: """Options for the TLS connection to the etcd cluster.""" zh: """当使用 TLS 连接 etcd 时的配置选项。 -当 cluster.discovery_strategy 为 etcd 时,此配置项才有效。 - """ +当 cluster.discovery_strategy 为 etcd 时,此配置项才有效。""" } label { en: "Cluster Etcd SSL Option" @@ -328,8 +315,7 @@ It is refreshed automatically, as long as the node is alive. desc { en: """Kubernetes API endpoint URL.""" zh: """指定 Kubernetes API Server。如有多个 Server 使用逗号 , 分隔。 -当 cluster.discovery_strategy 为 k8s 时,此配置项才有效。 - """ +当 cluster.discovery_strategy 为 k8s 时,此配置项才有效。""" } label { en: "Cluster k8s ApiServer" @@ -341,8 +327,7 @@ It is refreshed automatically, as long as the node is alive. desc { en: """EMQX broker service name.""" zh: """指定 Kubernetes 中 EMQX 的服务名。 -当 cluster.discovery_strategy 为 k8s 时,此配置项才有效。 - """ +当 cluster.discovery_strategy 为 k8s 时,此配置项才有效。""" } label { en: "K8s Service Name" @@ -354,12 +339,11 @@ It is refreshed automatically, as long as the node is alive. desc { en: """Address type used for connecting to the discovered nodes. Setting cluster.k8s.address_type to ip will -make EMQX to discover IP addresses of peer nodes from Kubernetes API. -""" +make EMQX to discover IP addresses of peer nodes from Kubernetes API.""" + zh: """当使用 k8s 方式集群时,address_type 用来从 Kubernetes 接口的应答里获取什么形式的 Host 列表。 指定 cluster.k8s.address_typeip,则将从 Kubernetes 接口中获取集群中其他节点 -的IP地址。 -""" +的IP地址。""" } label { en: "K8s Address Type" @@ -371,8 +355,7 @@ make EMQX to discover IP addresses of peer nodes from Kubernetes API. desc { en: """Kubernetes namespace.""" zh: """当使用 k8s 方式并且 cluster.k8s.address_type 指定为 dns 类型时, -可设置 emqx 节点名的命名空间。与 cluster.k8s.suffix 一起使用用以拼接得到节点名列表。 - """ +可设置 emqx 节点名的命名空间。与 cluster.k8s.suffix 一起使用用以拼接得到节点名列表。""" } label { en: "K8s Namespace" @@ -386,8 +369,7 @@ make EMQX to discover IP addresses of peer nodes from Kubernetes API. Note: this parameter is only relevant when address_type is dns or hostname.""" zh: """当使用 k8s 方式并且 cluster.k8s.address_type 指定为 dns 类型时,可设置 emqx 节点名的后缀。 -与 cluster.k8s.namespace 一起使用用以拼接得到节点名列表。 - """ +与 cluster.k8s.namespace 一起使用用以拼接得到节点名列表。""" } label { en: "K8s Suffix" @@ -398,11 +380,9 @@ or hostname.""" node_name { desc { en: """Unique name of the EMQX node. It must follow %name%@FQDN or -%name%@IPv4 format. - """ +%name%@IPv4 format.""" zh: """节点名。格式为 \@\。其中 可以是 IP 地址,也可以是 FQDN。 -详见 http://erlang.org/doc/reference_manual/distributed.html。 - """ +详见 http://erlang.org/doc/reference_manual/distributed.html。""" } label { en: "Node Name" @@ -425,8 +405,7 @@ belong to different clusters from accidentally connecting to each other.""" node_data_dir { desc { - en: """ -Path to the persistent data directory.
+ en: """Path to the persistent data directory.
Possible auto-created subdirectories are:
- `mnesia/`: EMQX's built-in database directory.
For example, `mnesia/emqx@127.0.0.1`.
@@ -437,18 +416,17 @@ the old dir should be deleted first.
- `patches`: Hot-patch beam files are to be placed here.
- `trace`: Trace log files.
-**NOTE**: One data dir cannot be shared by two or more EMQX nodes. -""" - zh: """ -节点数据存放目录,可能会自动创建的子目录如下:
+**NOTE**: One data dir cannot be shared by two or more EMQX nodes.""" + + zh: """节点数据存放目录,可能会自动创建的子目录如下:
- `mnesia/`。EMQX的内置数据库目录。例如,`mnesia/emqx@127.0.0.1`。
如果节点要被重新命名(例如,`emqx@10.0.1.1`)。旧目录应该首先被删除。
- `configs`。在启动时生成的配置,以及集群/本地覆盖的配置。
- `patches`: 热补丁文件将被放在这里。
- `trace`: 日志跟踪文件。
-**注意**: 一个数据dir不能被两个或更多的EMQX节点同时使用。 - """ +**注意**: 一个数据dir不能被两个或更多的EMQX节点同时使用。""" + } label { en: "Node Data Dir" @@ -480,16 +458,15 @@ the old dir should be deleted first.
node_crash_dump_seconds { desc { - en: """This variable gives the number of seconds that the emulator is allowed to spend writing a crash dump. When the given number of seconds have elapsed, the emulator is terminated. -- If setting to 0 seconds, the runtime system does not even attempt to write the crash dump file. It only terminates. -- If setting to a positive value S, wait for S seconds to complete the crash dump file and then terminates the runtime system with a SIGALRM signal. -- A negative value causes the termination of the runtime system to wait indefinitely until the crash dump file has been completely written. - """ - zh: """该配置给出了运行时系统允许花费的写入崩溃转储的秒数。当给定的秒数已经过去,运行时系统将被终止。 -- 如果设置为0秒,运行时会立即终止,不会尝试写入崩溃转储文件。 -- 如果设置为一个正数 S,节点会等待 S 秒来完成崩溃转储文件,然后用SIGALRM信号终止运行时系统。 -- 如果设置为一个负值导致运行时系统的终止等待无限期地直到崩溃转储文件已经完全写入。 -""" + en: """This variable gives the number of seconds that the emulator is allowed to spend writing a crash dump. When the given number of seconds have elapsed, the emulator is terminated.
+- If setting to 0 seconds, the runtime system does not even attempt to write the crash dump file. It only terminates.
+- If setting to a positive value S, wait for S seconds to complete the crash dump file and then terminates the runtime system with a SIGALRM signal.
+- A negative value causes the termination of the runtime system to wait indefinitely until the crash dump file has been completely written.""" + + zh: """该配置给出了运行时系统允许花费的写入崩溃转储的秒数。当给定的秒数已经过去,运行时系统将被终止。
+- 如果设置为0秒,运行时会立即终止,不会尝试写入崩溃转储文件。
+- 如果设置为一个正数 S,节点会等待 S 秒来完成崩溃转储文件,然后用SIGALRM信号终止运行时系统。
+- 如果设置为一个负值导致运行时系统的终止等待无限期地直到崩溃转储文件已经完全写入。""" } label { en: "Crash Dump Seconds" @@ -501,12 +478,11 @@ the old dir should be deleted first.
desc { en: """This variable sets the maximum size of a crash dump file in bytes. The crash dump will be truncated if this limit is exceeded. -If setting it to 0, the runtime system does not even attempt to write a crash dump file. -""" +If setting it to 0, the runtime system does not even attempt to write a crash dump file.""" + zh: """限制崩溃文件的大小,当崩溃时节点内存太大, 如果为了保存现场,需要全部存到崩溃文件中,此处限制最多能保存多大的文件。 -如果超过此限制,崩溃转储将被截断。如果设置为0,系统不会尝试写入崩溃转储文件。 -""" +如果超过此限制,崩溃转储将被截断。如果设置为0,系统不会尝试写入崩溃转储文件。""" } label { en: "Crash Dump Bytes" @@ -517,8 +493,7 @@ If setting it to 0, the runtime system does not even attempt to write a crash du node_dist_net_ticktime { desc { en: """This is the approximate time an EMQX node may be unresponsive until it is considered down and thereby disconnected.""" - zh: """系统调优参数,此配置将覆盖 vm.args 文件里的 -kernel net_ticktime 参数。当一个节点持续无响应多久之后,认为其已经宕机并断开连接。 - """ + zh: """系统调优参数,此配置将覆盖 vm.args 文件里的 -kernel net_ticktime 参数。当一个节点持续无响应多久之后,认为其已经宕机并断开连接。""" } label { en: "Dist Net TickTime" @@ -529,8 +504,7 @@ If setting it to 0, the runtime system does not even attempt to write a crash du node_backtrace_depth { desc { en: """Maximum depth of the call stack printed in error messages and -process_info. - """ +process_info.""" zh: """错误信息中打印的最大堆栈层数""" } label { @@ -539,10 +513,10 @@ If setting it to 0, the runtime system does not even attempt to write a crash du } } + # TODO: check if this is still needed node_applications { desc { - en: """List of Erlang applications that shall be rebooted when the EMQX broker joins the cluster. - """ + en: """List of Erlang applications that shall be rebooted when the EMQX broker joins the cluster.""" zh: """当新EMQX 加入集群时,应重启的Erlang应用程序的列表。""" } label { @@ -551,6 +525,7 @@ If setting it to 0, the runtime system does not even attempt to write a crash du } } + # deprecated, TODO: remove node_etc_dir { desc { en: """etc dir for the node""" @@ -564,15 +539,12 @@ If setting it to 0, the runtime system does not even attempt to write a crash du db_backend { desc { - en: """ -Select the backend for the embedded database.
+ en: """Select the backend for the embedded database.
rlog is the default backend, that is suitable for very large clusters.
-mnesia is a backend that offers decent performance in small clusters. -""" - zh: """ rlog是默认的数据库,他适用于大规模的集群。 -mnesia是备选数据库,在小集群中提供了很好的性能。 - """ +mnesia is a backend that offers decent performance in small clusters.""" + zh: """配置后端数据库驱动,默认值为 rlog 它适用于大规模的集群。 +mnesia 是备选数据库,适合中小集群。""" } label { en: "DB Backend" @@ -582,23 +554,19 @@ mnesia是备选数据库,在小集群中提供了很好的性能。 db_role { desc { - en: """ -Select a node role.
+ en: """Select a node role.
core nodes provide durability of the data, and take care of writes. It is recommended to place core nodes in different racks or different availability zones.
replicant nodes are ephemeral worker nodes. Removing them from the cluster doesn't affect database redundancy
It is recommended to have more replicant nodes than core nodes.
Note: this parameter only takes effect when the backend is set -to rlog. -""" - zh: """ -选择节点的角色。
+to rlog.""" + zh: """选择节点的角色。
core 节点提供数据的持久性,并负责写入。建议将核心节点放置在不同的机架或不同的可用区。
repliant 节点是临时工作节点。 从集群中删除它们,不影响数据库冗余
建议复制节点多于核心节点。
-注意:该参数仅在设置backend时生效到 rlog。 - """ +注意:该参数仅在设置backend时生效到 rlog。""" } label { en: "DB Role" @@ -608,20 +576,17 @@ to rlog. db_core_nodes { desc { - en: """ -List of core nodes that the replicant will connect to.
+ en: """List of core nodes that the replicant will connect to.
Note: this parameter only takes effect when the backend is set to rlog and the role is set to replicant.
This value needs to be defined for manual or static cluster discovery mechanisms.
If an automatic cluster discovery mechanism is being used (such as etcd), -there is no need to set this value. -""" +there is no need to set this value.""" zh: """当前节点连接的核心节点列表。
注意:该参数仅在设置backend时生效到 rlog 并且设置rolereplicant时生效。
该值需要在手动或静态集群发现机制下设置。
-如果使用了自动集群发现机制(如etcd),则不需要设置该值。 - """ +如果使用了自动集群发现机制(如etcd),则不需要设置该值。""" } label { en: "Db Core Node" @@ -642,10 +607,8 @@ there is no need to set this value. db_tlog_push_mode { desc { - en: """ -In sync mode the core node waits for an ack from the replicant nodes before sending the next -transaction log entry. -""" + en: """In sync mode the core node waits for an ack from the replicant nodes before sending the next +transaction log entry.""" zh: """同步模式下,核心节点等待复制节点的确认信息,然后再发送下一条事务日志。""" } label { @@ -659,13 +622,11 @@ transaction log entry. en: """Defines the default transport for pushing transaction logs.
This may be overridden on a per-shard basis in db.shard_transports. gen_rpc uses the gen_rpc library, -distr uses the Erlang distribution.
""" - zh: """ -定义用于推送事务日志的默认传输。
+distr uses the Erlang distribution.""" + zh: """定义用于推送事务日志的默认传输。
这可以在 db.shard_transports 中基于每个分片被覆盖。 gen_rpc 使用 gen_rpc 库, -distr 使用 Erlang 发行版。
- """ +distr 使用 Erlang 发行版。""" } label { en: "Default Shard Transport" @@ -682,8 +643,7 @@ the default is to use the value set in db.default_shard_transport." zh: """允许为每个 shard 下的事务日志复制操作的传输方法进行调优。
gen_rpc 使用 gen_rpc 库, distr 使用 Erlang 自带的 rpc 库。
如果未指定, -默认是使用 db.default_shard_transport 中设置的值。 - """ +默认是使用 db.default_shard_transport 中设置的值。""" } label { en: "Shard Transports" @@ -750,8 +710,7 @@ Ensure that the number of completed transactions is less than the max_hist rpc_async_batch_size { desc { en: """The maximum number of batch messages sent in asynchronous mode. - Note that this configuration does not work in synchronous mode. - """ + Note that this configuration does not work in synchronous mode.""" zh: """异步模式下,发送的批量消息的最大数量。""" } label { @@ -768,8 +727,7 @@ If node name is emqxN@127.0.0.1, where the N is an integer, then the listening port will be 5370 + N.""" zh: """manual: 通过 tcp_server_port 来发现端口。
stateless: 使用无状态的方式来发现端口,使用如下算法。如果节点名称是 -emqxN@127.0.0.1, N 是一个数字,那么监听端口就是 5370 + N。 - """ +emqxN@127.0.0.1
, N 是一个数字,那么监听端口就是 5370 + N。""" } label { en: "RRC Port Discovery" @@ -782,8 +740,7 @@ emqxN@127.0.0.1, N 是一个数字,那么监听端口就是 5370 + N。 en: """Listening port used by RPC local service.
Note that this config only takes effect when rpc.port_discovery is set to manual.""" zh: """RPC 本地服务使用的 TCP 端口。
-只有当 rpc.port_discovery 设置为 manual 时,此配置才会生效。 - """ +只有当 rpc.port_discovery 设置为 manual 时,此配置才会生效。""" } label { en: "RPC TCP Server Port" @@ -798,8 +755,7 @@ Note that this config only takes effect when rpc.port_discovery is set to manual and driver is set to ssl.""" zh: """RPC 本地服务使用的监听SSL端口。
只有当 rpc.port_discovery 设置为 manual 且 dirver 设置为 ssl, -此配置才会生效。 - """ +此配置才会生效。""" } label { en: "RPC SSL Server Port" @@ -832,11 +788,10 @@ and driver is set to ssl.""" rpc_certfile { desc { en: """Path to TLS certificate file used to validate identity of the cluster nodes. -Note that this config only takes effect when rpc.driver is set to ssl. - """ +Note that this config only takes effect when rpc.driver is set to ssl.""" + zh: """TLS 证书文件的路径,用于验证集群节点的身份。 -只有当 rpc.driver 设置为 ssl 时,此配置才会生效。 - """ +只有当 rpc.driver 设置为 ssl 时,此配置才会生效。""" } label { en: "RPC Certfile" @@ -849,8 +804,7 @@ Note that this config only takes effect when rpc.driver is set to < en: """Path to the private key file for the rpc.certfile.
Note: contents of this file are secret, so it's necessary to set permissions to 600.""" zh: """rpc.certfile 的私钥文件的路径。
-注意:此文件内容是私钥,所以需要设置权限为 600。 - """ +注意:此文件内容是私钥,所以需要设置权限为 600。""" } label { en: "RPC Keyfile" @@ -863,8 +817,7 @@ Note: contents of this file are secret, so it's necessary to set permissions to en: """Path to certification authority TLS certificate file used to validate rpc.certfile.
Note: certificates of all nodes in the cluster must be signed by the same CA.""" zh: """验证 rpc.certfile 的 CA 证书文件的路径。
-注意:集群中所有节点的证书必须使用同一个 CA 签发。 - """ +注意:集群中所有节点的证书必须使用同一个 CA 签发。""" } label { en: "RPC Cacertfile" @@ -1007,14 +960,10 @@ until the RPC connection is considered lost.""" common_handler_level { desc { - en: """ -The log level for the current log handler. -Defaults to warning. -""" - zh: """ -当前日志处理进程的日志级别。 -默认为 warning 级别。 -""" + en: """The log level for the current log handler. +Defaults to warning.""" + zh: """当前日志处理进程的日志级别。 +默认为 warning 级别。""" } label { en: "Log Level" @@ -1024,22 +973,18 @@ Defaults to warning. common_handler_time_offset { desc { - en: """ -The time offset to be used when formatting the timestamp. + en: """The time offset to be used when formatting the timestamp. Can be one of: - system: the time offset used by the local system - utc: the UTC time offset - +-[hh]:[mm]: user specified time offset, such as "-02:00" or "+00:00" -Defaults to: system. -""" - zh: """ -日志中的时间戳使用的时间偏移量。 +Defaults to: system.""" + zh: """日志中的时间戳使用的时间偏移量。 可选值为: - system: 本地系统使用的时区偏移量 - utc: 0 时区的偏移量 - +-[hh]:[mm]: 自定义偏移量,比如 "-02:00" 或者 "+00:00" -默认值为本地系统的时区偏移量:system。 -""" +默认值为本地系统的时区偏移量:system。""" } label { en: "Time Offset" @@ -1049,14 +994,10 @@ Defaults to: system. common_handler_chars_limit { desc { - en: """ -Set the maximum length of a single log message. If this length is exceeded, the log message will be truncated. -NOTE: Restrict char limiter if formatter is JSON , it will get a truncated incomplete JSON data, which is not recommended. -""" - zh: """ -设置单个日志消息的最大长度。 如果超过此长度,则日志消息将被截断。最小可设置的长度为100。 -注意:如果日志格式为 JSON,限制字符长度可能会导致截断不完整的 JSON 数据。 -""" + en: """Set the maximum length of a single log message. If this length is exceeded, the log message will be truncated. +NOTE: Restrict char limiter if formatter is JSON , it will get a truncated incomplete JSON data, which is not recommended.""" + zh: """设置单个日志消息的最大长度。 如果超过此长度,则日志消息将被截断。最小可设置的长度为100。 +注意:如果日志格式为 JSON,限制字符长度可能会导致截断不完整的 JSON 数据。""" } label { en: "Single Log Max Length" @@ -1097,8 +1038,7 @@ If the message queue grows larger than this value, the handler starts handling log events synchronously instead, meaning that the client process sending the event must wait for a response. When the handler reduces the message queue to a level below the sync_mode_qlen threshold, -asynchronous operation is resumed. -""" +asynchronous operation is resumed.""" zh: """只要缓冲的日志事件的数量低于这个值,所有的日志事件都会被异步处理。 这意味着,日志落地速度不会影响正常的业务进程,因为它们不需要等待日志处理进程的响应。 如果消息队列的增长超过了这个值,处理程序开始同步处理日志事件。也就是说,发送事件的客户进程必须等待响应。 @@ -1120,8 +1060,7 @@ When drop mode is activated or deactivated, a message is printed in the logs.""" 要禁用某些模式,请执行以下操作。 - 如果sync_mode_qlen被设置为0,所有的日志事件都被同步处理。也就是说,异步日志被禁用。 - 如果sync_mode_qlen被设置为与drop_mode_qlen相同的值,同步模式被禁用。也就是说,处理程序总是以异步模式运行,除非调用drop或flushing。 -- 如果drop_mode_qlen被设置为与flush_qlen相同的值,则drop模式被禁用,永远不会发生。 -""" +- 如果drop_mode_qlen被设置为与flush_qlen相同的值,则drop模式被禁用,永远不会发生。""" } label { en: "Queue Length before Entering Drop Mode" @@ -1144,16 +1083,13 @@ To flush events, the handler discards the buffered log messages without logging. common_handler_supervisor_reports { desc { - en: """ -Type of supervisor reports that are logged. Defaults to error - - error: only log errors in the Erlang processes. - - progress: log process startup. -""" - zh: """ -Supervisor 报告的类型。默认为 error 类型。 + en: """Type of supervisor reports that are logged. Defaults to error
+ - error: only log errors in the Erlang processes
. + - progress: log process startup.""" + + zh: """Supervisor 报告的类型。默认为 error 类型。
- error:仅记录 Erlang 进程中的错误。 - - progress:除了 error 信息外,还需要记录进程启动的详细信息。 -""" + - progress:除了 error 信息外,还需要记录进程启动的详细信息。""" } label { en: "Report Type" @@ -1187,8 +1123,7 @@ Supervisor 报告的类型。默认为 error 类型。 desc { en: """This parameter controls log file rotation. The value `infinity` means the log file will grow indefinitely, otherwise the log file will be rotated once it reaches `max_size` in bytes.""" zh: """此参数控制日志文件轮换。 `infinity` 意味着日志文件将无限增长,否则日志文件将在达到 `max_size`(以字节为单位)时进行轮换。 -与 rotation count配合使用。如果 counter 为 10,则是10个文件轮换。 -""" +与 rotation count配合使用。如果 counter 为 10,则是10个文件轮换。""" } label { en: "Rotation Size" @@ -1200,8 +1135,7 @@ Supervisor 报告的类型。默认为 error 类型。 desc { en: """Enable log rotation feature.""" zh: """启用日志轮换功能。启动后生成日志文件后缀会加上对应的索引数字,比如:log/emqx.log.1。 -系统会默认生成*.siz/*.idx用于记录日志位置,请不要手动修改这两个文件。 -""" +系统会默认生成*.siz/*.idx用于记录日志位置,请不要手动修改这两个文件。""" } label { en: "Rotation Enable" @@ -1299,8 +1233,7 @@ Supervisor 报告的类型。默认为 error 类型。 authorization { desc { - en: """ -Authorization a.k.a. ACL.
+ en: """Authorization a.k.a. ACL.
In EMQX, MQTT client access control is extremely flexible.
An out-of-the-box set of authorization data sources are supported. For example,
@@ -1308,9 +1241,9 @@ For example,
'built_in_database' source can be used to store per-client customizable rule sets, natively in the EMQX node;
'http' source to make EMQX call an external HTTP API to make the decision;
-'PostgreSQL' etc. to look up clients or rules from external databases;
-""" - zh: """ 授权(ACL)。EMQX 支持完整的客户端访问控制(ACL)。
""" +'PostgreSQL' etc. to look up clients or rules from external databases""" + + zh: """授权(ACL)。EMQX 支持完整的客户端访问控制(ACL)。""" } label { en: "Authorization" @@ -1470,14 +1403,11 @@ Each sink is represented by a _log handler_, which can be configured independent desc_log_rotation { desc { - en: """ -By default, the logs are stored in `./log` directory (for installation from zip file) or in `/var/log/emqx` (for binary installation).
-This section of the configuration controls the number of files kept for each log handler. -""" - zh: """ -默认情况下,日志存储在 `./log` 目录(用于从 zip 文件安装)或 `/var/log/emqx`(用于二进制安装)。
-这部分配置,控制每个日志处理进程保留的文件数量。 -""" + en: """By default, the logs are stored in `./log` directory (for installation from zip file) or in `/var/log/emqx` (for binary installation).
+This section of the configuration controls the number of files kept for each log handler.""" + + zh: """默认情况下,日志存储在 `./log` 目录(用于从 zip 文件安装)或 `/var/log/emqx`(用于二进制安装)。
+这部分配置,控制每个日志处理进程保留的文件数量。""" } label { en: "Log Rotation" @@ -1487,14 +1417,11 @@ This section of the configuration controls the number of files kept for each log desc_log_overload_kill { desc { - en: """ -Log overload kill features an overload protection that activates when the log handlers use too much memory or have too many buffered log messages.
-When the overload is detected, the log handler is terminated and restarted after a cooldown period. -""" - zh: """ -日志过载终止,具有过载保护功能。当日志处理进程使用过多内存,或者缓存的日志消息过多时该功能被激活。
-检测到过载时,日志处理进程将终止,并在冷却期后重新启动。 -""" + en: """Log overload kill features an overload protection that activates when the log handlers use too much memory or have too many buffered log messages.
+When the overload is detected, the log handler is terminated and restarted after a cooldown period.""" + + zh: """日志过载终止,具有过载保护功能。当日志处理进程使用过多内存,或者缓存的日志消息过多时该功能被激活。
+检测到过载时,日志处理进程将终止,并在冷却期后重新启动。""" } label { en: "Log Overload Kill" diff --git a/apps/emqx_connector/i18n/emqx_connector_api.conf b/apps/emqx_connector/i18n/emqx_connector_api.conf index 2f468fff0..8575f01aa 100644 --- a/apps/emqx_connector/i18n/emqx_connector_api.conf +++ b/apps/emqx_connector/i18n/emqx_connector_api.conf @@ -13,14 +13,10 @@ emqx_connector_api { conn_test_post { desc { - en: """ -Test creating a new connector by given ID
-The ID must be of format '{type}:{name}' -""" - zh: """ -通过给定的 ID 测试创建一个新的连接器
-ID 的格式必须为“{type}:{name}” -""" + en: """Test creating a new connector by given ID
+The ID must be of format '{type}:{name}'""" + zh: """通过给定的 ID 测试创建一个新的连接器
+ID 的格式必须为“{type}:{name}”""" } label: { en: "Create Test Connector" diff --git a/apps/emqx_connector/i18n/emqx_connector_http.conf b/apps/emqx_connector/i18n/emqx_connector_http.conf index da886191b..c6efd03ca 100644 --- a/apps/emqx_connector/i18n/emqx_connector_http.conf +++ b/apps/emqx_connector/i18n/emqx_connector_http.conf @@ -1,17 +1,13 @@ emqx_connector_http { base_url { desc { - en: """ -The base URL is the URL includes only the scheme, host and port.
+ en: """The base URL is the URL includes only the scheme, host and port.
When send an HTTP request, the real URL to be used is the concatenation of the base URL and the path parameter
-For example: `http://localhost:9901/` -""" - zh: """ -base URL 只包含host和port。
+For example: `http://localhost:9901/`""" + zh: """base URL 只包含host和port。
发送HTTP请求时,真实的URL是由base URL 和 path parameter连接而成。
-示例:`http://localhost:9901/` -""" +示例:`http://localhost:9901/`""" } label: { en: "Base Url" diff --git a/apps/emqx_connector/i18n/emqx_connector_mongo.conf b/apps/emqx_connector/i18n/emqx_connector_mongo.conf index 619a8e3b4..1f00083a4 100644 --- a/apps/emqx_connector/i18n/emqx_connector_mongo.conf +++ b/apps/emqx_connector/i18n/emqx_connector_mongo.conf @@ -3,7 +3,7 @@ emqx_connector_mongo { single_mongo_type { desc { en: "Standalone instance. Must be set to 'single' when MongoDB server is running in standalone mode." - zh: "Standalone 模式。当 MongoDB 服务运行在 standalone 模式下,该配置必须设置为 'single'。 " + zh: "Standalone 模式。当 MongoDB 服务运行在 standalone 模式下,该配置必须设置为 'single'。" } label: { en: "Standalone instance" @@ -46,16 +46,12 @@ emqx_connector_mongo { server { desc { - en: """ -The IPv4 or IPv6 address or the hostname to connect to.
+ en: """The IPv4 or IPv6 address or the hostname to connect to.
A host entry has the following form: `Host[:Port]`.
-The MongoDB default port 27017 is used if `[:Port]` is not specified. -""" - zh: """ -将要连接的 IPv4 或 IPv6 地址,或者主机名。
+The MongoDB default port 27017 is used if `[:Port]` is not specified.""" + zh: """将要连接的 IPv4 或 IPv6 地址,或者主机名。
主机名具有以下形式:`Host[:Port]`。
-如果未指定 `[:Port]`,则使用 MongoDB 默认端口 27017。 -""" +如果未指定 `[:Port]`,则使用 MongoDB 默认端口 27017。""" } label: { en: "Server Host" @@ -65,18 +61,14 @@ The MongoDB default port 27017 is used if `[:Port]` is not specified. servers { desc { - en: """ -A Node list for Cluster to connect to. The nodes should be separated with commas, such as: `Node[,Node].` + en: """A Node list for Cluster to connect to. The nodes should be separated with commas, such as: `Node[,Node].` For each Node should be: The IPv4 or IPv6 address or the hostname to connect to. A host entry has the following form: `Host[:Port]`. -The MongoDB default port 27017 is used if `[:Port]` is not specified. -""" - zh: """ -集群将要连接的节点列表。 节点之间用逗号分隔,如:`Node[,Node].` +The MongoDB default port 27017 is used if `[:Port]` is not specified.""" + zh: """集群将要连接的节点列表。 节点之间用逗号分隔,如:`Node[,Node].` 每个节点的配置为:将要连接的 IPv4 或 IPv6 地址或主机名。 主机名具有以下形式:`Host[:Port]`。 -如果未指定 `[:Port]`,则使用 MongoDB 默认端口 27017。 -""" +如果未指定 `[:Port]`,则使用 MongoDB 默认端口 27017。""" } label: { en: "Servers" diff --git a/apps/emqx_connector/i18n/emqx_connector_mqtt_schema.conf b/apps/emqx_connector/i18n/emqx_connector_mqtt_schema.conf index 6f573bb73..b0d1e0821 100644 --- a/apps/emqx_connector/i18n/emqx_connector_mqtt_schema.conf +++ b/apps/emqx_connector/i18n/emqx_connector_mqtt_schema.conf @@ -9,8 +9,7 @@ emqx_connector_mqtt_schema { the rule.""" zh: """入口配置定义了该桥接如何从远程 MQTT Broker 接收消息,然后将消息发送到本地 Broker。
以下字段中允许使用带有变量的模板:'remote.qos', 'local.topic', 'local.qos', 'local.retain', 'local.payload'。
- 注意:如果此桥接被用作规则的输入,并且配置了 'local.topic',则从远程代理获取的消息将同时被发送到 'local.topic' 和规则。 - """ + 注意:如果此桥接被用作规则的输入,并且配置了 'local.topic',则从远程代理获取的消息将同时被发送到 'local.topic' 和规则。""" } label: { en: "Ingress Configs" @@ -27,8 +26,7 @@ is configured, then both the data got from the rule and the MQTT messages that m 'local.topic' will be forwarded.""" zh: """出口配置定义了该桥接如何将消息从本地 Broker 转发到远程 Broker。 以下字段中允许使用带有变量的模板:'remote.topic', 'local.qos', 'local.retain', 'local.payload'。
-注意:如果此桥接被用作规则的动作,并且配置了 'local.topic',则从规则输出的数据以及匹配到 'local.topic' 的 MQTT 消息都会被转发。 - """ +注意:如果此桥接被用作规则的动作,并且配置了 'local.topic',则从规则输出的数据以及匹配到 'local.topic' 的 MQTT 消息都会被转发。""" } label: { en: "Egress Configs" @@ -82,29 +80,23 @@ is configured, then both the data got from the rule and the MQTT messages that m mode { desc { - en: """ -The mode of the MQTT Bridge.
- + en: """The mode of the MQTT Bridge.
- cluster_shareload: create an MQTT connection on each node in the emqx cluster.
In 'cluster_shareload' mode, the incoming load from the remote broker is shared by using shared subscription.
Note that the 'clientid' is suffixed by the node name, this is to avoid clientid conflicts between different nodes. And we can only use shared subscription -topic filters for remote.topic of ingress connections. -""" - zh: """ -MQTT 桥的模式。
- +topic filters for remote.topic of ingress connections.""" + zh: """MQTT 桥的模式。
- cluster_shareload:在 emqx 集群的每个节点上创建一个 MQTT 连接。
在“cluster_shareload”模式下,来自远程代理的传入负载通过共享订阅的方式接收。
请注意,clientid 以节点名称为后缀,这是为了避免不同节点之间的 clientid 冲突。 -而且对于入口连接的 remote.topic,我们只能使用共享订阅主题过滤器。 -""" +而且对于入口连接的 remote.topic,我们只能使用共享订阅主题过滤器。""" } label: { - en: "MQTT Bridge Mode" - zh: "MQTT 桥接模式" - } + en: "MQTT Bridge Mode" + zh: "MQTT 桥接模式" + } } server { @@ -113,22 +105,18 @@ MQTT 桥的模式。
zh: "远程 MQTT Broker的主机和端口。" } label: { - en: "Broker IP And Port" - zh: "Broker主机和端口" + en: "Broker IP And Port" + zh: "Broker主机和端口" } } bridge_mode { desc { - en: """ -If enable bridge mode. + en: """If enable bridge mode. NOTE: This setting is only for MQTT protocol version older than 5.0, and the remote MQTT -broker MUST support this feature. - """ - zh: """ -是否启用 Bridge Mode。 -注意:此设置只针对 MQTT 协议版本 < 5.0 有效,并且需要远程 MQTT Broker 支持 Bridge Mode。 - """ +broker MUST support this feature.""" + zh: """是否启用 Bridge Mode。 +注意:此设置只针对 MQTT 协议版本 < 5.0 有效,并且需要远程 MQTT Broker 支持 Bridge Mode。""" } label { en: "Bridge Mode" @@ -215,14 +203,10 @@ broker MUST support this feature. ingress_local_topic { desc { - en: """ -Send messages to which topic of the local broker.
-Template with variables is allowed. -""" - zh: """ -向本地broker的哪个topic发送消息。
-允许使用带有变量的模板。 -""" + en: """Send messages to which topic of the local broker.
+Template with variables is allowed.""" + zh: """向本地broker的哪个topic发送消息。
+允许使用带有变量的模板。""" } label: { en: "Local Topic" @@ -232,14 +216,10 @@ Template with variables is allowed. ingress_local_qos { desc { - en: """ -The QoS of the MQTT message to be sent.
-Template with variables is allowed. -""" - zh: """ -待发送 MQTT 消息的 QoS。
-允许使用带有变量的模板。 -""" + en: """The QoS of the MQTT message to be sent.
+Template with variables is allowed.""" + zh: """待发送 MQTT 消息的 QoS。
+允许使用带有变量的模板。""" } label: { en: "Local QoS" @@ -260,14 +240,10 @@ Template with variables is allowed. egress_remote_topic { desc { - en: """ -Forward to which topic of the remote broker.
-Template with variables is allowed. -""" - zh: """ -转发到远程broker的哪个topic。
-允许使用带有变量的模板。 -""" + en: """Forward to which topic of the remote broker.
+Template with variables is allowed.""" + zh: """转发到远程broker的哪个topic。
+允许使用带有变量的模板。""" } label: { en: "Remote Topic" @@ -277,14 +253,10 @@ Template with variables is allowed. egress_remote_qos { desc { - en: """ -The QoS of the MQTT message to be sent.
-Template with variables is allowed. -""" - zh: """ -待发送 MQTT 消息的 QoS。
-允许使用带有变量的模板。 -""" + en: """The QoS of the MQTT message to be sent.
+Template with variables is allowed.""" + zh: """待发送 MQTT 消息的 QoS。
+允许使用带有变量的模板。""" } label: { en: "Remote QoS" @@ -294,14 +266,10 @@ Template with variables is allowed. retain { desc { - en: """ -The 'retain' flag of the MQTT message to be sent.
-Template with variables is allowed. -""" - zh: """ -要发送的 MQTT 消息的“保留”标志。
-允许使用带有变量的模板。 -""" + en: """The 'retain' flag of the MQTT message to be sent.
+Template with variables is allowed.""" + zh: """要发送的 MQTT 消息的“保留”标志。
+允许使用带有变量的模板。""" } label: { en: "Retain Flag" @@ -311,14 +279,10 @@ Template with variables is allowed. payload { desc { - en: """ -The payload of the MQTT message to be sent.
-Template with variables is allowed. -""" - zh: """ -要发送的 MQTT 消息的负载。
-允许使用带有变量的模板。 -""" + en: """The payload of the MQTT message to be sent.
+Template with variables is allowed.""" + zh: """要发送的 MQTT 消息的负载。
+允许使用带有变量的模板。""" } label: { en: "Payload" @@ -327,15 +291,15 @@ Template with variables is allowed. } server_configs { - desc { - en: """Configs related to the server.""" - zh: """服务器相关的配置。""" - } - label: { - en: "Server Configs" - zh: "服务配置。" - } - } + desc { + en: """Configs related to the server.""" + zh: """服务器相关的配置。""" + } + label: { + en: "Server Configs" + zh: "服务配置。" + } + } clientid_prefix { desc { diff --git a/apps/emqx_connector/i18n/emqx_connector_mysql.conf b/apps/emqx_connector/i18n/emqx_connector_mysql.conf index 499caae12..89e95534b 100644 --- a/apps/emqx_connector/i18n/emqx_connector_mysql.conf +++ b/apps/emqx_connector/i18n/emqx_connector_mysql.conf @@ -2,16 +2,12 @@ emqx_connector_mysql { server { desc { - en: """ -The IPv4 or IPv6 address or the hostname to connect to.
+ en: """The IPv4 or IPv6 address or the hostname to connect to.
A host entry has the following form: `Host[:Port]`.
-The MySQL default port 3306 is used if `[:Port]` is not specified. -""" - zh: """ -将要连接的 IPv4 或 IPv6 地址,或者主机名。
+The MySQL default port 3306 is used if `[:Port]` is not specified.""" + zh: """将要连接的 IPv4 或 IPv6 地址,或者主机名。
主机名具有以下形式:`Host[:Port]`。
-如果未指定 `[:Port]`,则使用 MySQL 默认端口 3306。 -""" +如果未指定 `[:Port]`,则使用 MySQL 默认端口 3306。""" } label: { en: "Server Host" diff --git a/apps/emqx_connector/i18n/emqx_connector_pgsql.conf b/apps/emqx_connector/i18n/emqx_connector_pgsql.conf index 6aa792070..33246c844 100644 --- a/apps/emqx_connector/i18n/emqx_connector_pgsql.conf +++ b/apps/emqx_connector/i18n/emqx_connector_pgsql.conf @@ -2,16 +2,12 @@ emqx_connector_pgsql { server { desc { - en: """ -The IPv4 or IPv6 address or the hostname to connect to.
+ en: """The IPv4 or IPv6 address or the hostname to connect to.
A host entry has the following form: `Host[:Port]`.
-The PostgreSQL default port 5432 is used if `[:Port]` is not specified. -""" - zh: """ -将要连接的 IPv4 或 IPv6 地址,或者主机名。
+The PostgreSQL default port 5432 is used if `[:Port]` is not specified.""" + zh: """将要连接的 IPv4 或 IPv6 地址,或者主机名。
主机名具有以下形式:`Host[:Port]`。
-如果未指定 `[:Port]`,则使用 PostgreSQL 默认端口 5432。 -""" +如果未指定 `[:Port]`,则使用 PostgreSQL 默认端口 5432。""" } label: { en: "Server Host" diff --git a/apps/emqx_connector/i18n/emqx_connector_redis.conf b/apps/emqx_connector/i18n/emqx_connector_redis.conf index e42e73498..5915725a2 100644 --- a/apps/emqx_connector/i18n/emqx_connector_redis.conf +++ b/apps/emqx_connector/i18n/emqx_connector_redis.conf @@ -46,16 +46,12 @@ emqx_connector_redis { server { desc { - en: """ -The IPv4 or IPv6 address or the hostname to connect to.
+ en: """The IPv4 or IPv6 address or the hostname to connect to.
A host entry has the following form: `Host[:Port]`.
-The Redis default port 6379 is used if `[:Port]` is not specified. -""" - zh: """ -将要连接的 IPv4 或 IPv6 地址,或者主机名。
+The Redis default port 6379 is used if `[:Port]` is not specified.""" + zh: """将要连接的 IPv4 或 IPv6 地址,或者主机名。
主机名具有以下形式:`Host[:Port]`。
-如果未指定 `[:Port]`,则使用 Redis 默认端口 6379。 -""" +如果未指定 `[:Port]`,则使用 Redis 默认端口 6379。""" } label: { en: "Server Host" @@ -65,19 +61,14 @@ The Redis default port 6379 is used if `[:Port]` is not specified. servers { desc { - en: """ -A Node list for Cluster to connect to. The nodes should be separated with commas, such as: `Node[,Node].` + en: """A Node list for Cluster to connect to. The nodes should be separated with commas, such as: `Node[,Node].` For each Node should be: The IPv4 or IPv6 address or the hostname to connect to. A host entry has the following form: `Host[:Port]`. -The Redis default port 6379 is used if `[:Port]` is not specified. -""" - zh: """ - -集群将要连接的节点列表。 节点之间用逗号分隔,如:`Node[,Node].` +The Redis default port 6379 is used if `[:Port]` is not specified.""" + zh: """集群将要连接的节点列表。 节点之间用逗号分隔,如:`Node[,Node].` 每个节点的配置为:将要连接的 IPv4 或 IPv6 地址或主机名。 主机名具有以下形式:`Host[:Port]`。 -如果未指定 `[:Port]`,则使用 Redis 默认端口 6379。 -""" +如果未指定 `[:Port]`,则使用 Redis 默认端口 6379。""" } label: { en: "Servers" diff --git a/apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf b/apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf index 0b2a3026f..f81816e63 100644 --- a/apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf +++ b/apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf @@ -10,8 +10,7 @@ Alternatively, the HTTP listener can specify a unique IP address for each listen but use the same port.""" zh: """Dashboard 监听器设置。监听器必须有唯一的端口号和IP地址的组合。 例如,可以通过指定IP地址 0.0.0.0 来监听机器上给定端口上的所有配置的IP地址。 -或者,可以为每个监听器指定唯一的IP地址,但使用相同的端口。 -""" +或者,可以为每个监听器指定唯一的IP地址,但使用相同的端口。""" } label { en: "Listeners" @@ -181,8 +180,7 @@ The configuration is only valid when the inet6 is true.""" en: """The initial default password for dashboard 'admin' user. For safety, it should be changed as soon as possible. This value is not valid when you log in to Dashboard for the first time via the web -and change to a complex password as prompted. -""" +and change to a complex password as prompted.""" zh: """Dashboard 的默认密码,为了安全,应该尽快修改密码。 当通过网页首次登录 Dashboard 并按提示修改成复杂密码后,此值就会失效。""" } diff --git a/apps/emqx_exhook/i18n/emqx_exhook_api_i18n.conf b/apps/emqx_exhook/i18n/emqx_exhook_api_i18n.conf index 492c2c4ec..46854a3db 100644 --- a/apps/emqx_exhook/i18n/emqx_exhook_api_i18n.conf +++ b/apps/emqx_exhook/i18n/emqx_exhook_api_i18n.conf @@ -130,18 +130,17 @@ NOTE: The position should be \"front | rear | before:{name} | after:{name}""" status { desc { - en: """The status of Exhook server. -connected: connection succeeded -connecting: connection failed, reconnecting -disconnected: failed to connect and didn't reconnect -disabled: this server is disabled -error: failed to view the status of this server -""" - zh: """Exhook 服务器的状态。 -connected: 连接成功 -connecting: 连接失败,重连中 -disconnected: 连接失败,且未设置自动重连 -disabled: 该服务器未开启 + en: """The status of Exhook server.
+connected: connection succeeded
+connecting: connection failed, reconnecting
+disconnected: failed to connect and didn't reconnect
+disabled: this server is disabled
+error: failed to view the status of this server""" + zh: """Exhook 服务器的状态。
+connected: 连接成功
+connecting: 连接失败,重连中
+disconnected: 连接失败,且未设置自动重连
+disabled: 该服务器未开启
error: 查看该服务器状态时发生错误""" } } diff --git a/apps/emqx_gateway/i18n/emqx_gateway_api_i18n.conf b/apps/emqx_gateway/i18n/emqx_gateway_api_i18n.conf index 197e6a5ed..ab30a9c22 100644 --- a/apps/emqx_gateway/i18n/emqx_gateway_api_i18n.conf +++ b/apps/emqx_gateway/i18n/emqx_gateway_api_i18n.conf @@ -33,7 +33,7 @@ including current running status, number of connections, listener status, etc."" update_gateway { desc { en: """Update the gateway basic configurations and running status.
-Note: The Authentication and Listener configurations should be updated by other special APIs. """ +Note: The Authentication and Listener configurations should be updated by other special APIs.""" zh: """更新指定网关的基础配置、和启用的状态。
注:认证、和监听器的配置更新需参考对应的 API 接口。""" } @@ -49,11 +49,9 @@ Note: The Authentication and Listener configurations should be updated by other gateway_name_in_qs { desc { en: """Gateway Name.
-It's enum with `stomp`, `mqttsn`, `coap`, `lwm2m`, `exproto` -""" +It's enum with `stomp`, `mqttsn`, `coap`, `lwm2m`, `exproto`""" zh: """网关名称.
-可取值为 `stomp`、`mqttsn`、`coap`、`lwm2m`、`exproto` -""" +可取值为 `stomp`、`mqttsn`、`coap`、`lwm2m`、`exproto`""" } } diff --git a/apps/emqx_gateway/i18n/emqx_gateway_api_listeners_i18n.conf b/apps/emqx_gateway/i18n/emqx_gateway_api_listeners_i18n.conf index dc14a7e01..9319bb3e5 100644 --- a/apps/emqx_gateway/i18n/emqx_gateway_api_listeners_i18n.conf +++ b/apps/emqx_gateway/i18n/emqx_gateway_api_listeners_i18n.conf @@ -118,7 +118,7 @@ When authenticator is enabled for a listener, all clients connecting to that lis listener_status { desc { - en: """listener status """ + en: """listener status""" zh: """监听器状态""" } } diff --git a/apps/emqx_gateway/i18n/emqx_gateway_schema_i18n.conf b/apps/emqx_gateway/i18n/emqx_gateway_schema_i18n.conf index a05fec5c4..74a70eb73 100644 --- a/apps/emqx_gateway/i18n/emqx_gateway_schema_i18n.conf +++ b/apps/emqx_gateway/i18n/emqx_gateway_schema_i18n.conf @@ -49,7 +49,7 @@ When the broadcast option is enabled, the gateway will broadcast AD mqttsn_broadcast { desc { en: """Whether to periodically broadcast ADVERTISE messages""" - zh: """是否周期性广播 ADVERTISE 消息 """ + zh: """是否周期性广播 ADVERTISE 消息""" } } @@ -81,7 +81,7 @@ A 'pre-defined' topic ID is a topic ID whose mapping to a topic name is known in mqttsn_predefined_id { desc { en: """Topic ID. Range: 1-65535""" - zh: """主题 ID。范围:1-65535 """ + zh: """主题 ID。范围:1-65535""" } } @@ -122,54 +122,50 @@ Connection mode is a feature of non-standard protocols. When connection mode is coap_notify_type { desc { en: """The Notification Message will be delivered to the CoAP client if a new message received on an observed topic. -The type of delivered coap message can be set to: - - non: Non-confirmable; - - con: Confirmable; - - qos: Mapping from QoS type of received message, QoS0 -> non, QoS1,2 -> con -""" - zh: """投递给 CoAP 客户端的通知消息类型。当客户端 Observe 一个资源(或订阅某个主题)时,网关会向客户端推送新产生的消息。其消息类型可设置为: - - non: 不需要客户端返回确认消息; - - con: 需要客户端返回一个确认消息; - - qos: 取决于消息的 QoS 等级; QoS 0 会以 `non` 类型下发,QoS 1/2 会以 `con` 类型下发 -""" +The type of delivered coap message can be set to:
+ - non: Non-confirmable;
+ - con: Confirmable;
+ - qos: Mapping from QoS type of received message, QoS0 -> non, QoS1,2 -> con""" + zh: """投递给 CoAP 客户端的通知消息类型。当客户端 Observe 一个资源(或订阅某个主题)时,网关会向客户端推送新产生的消息。其消息类型可设置为:
+ - non: 不需要客户端返回确认消息;
+ - con: 需要客户端返回一个确认消息;
+ - qos: 取决于消息的 QoS 等级; QoS 0 会以 `non` 类型下发,QoS 1/2 会以 `con` 类型下发""" } } coap_subscribe_qos { desc { en: """The Default QoS Level indicator for subscribe request. -This option specifies the QoS level for the CoAP Client when establishing a subscription membership, if the subscribe request is not carried `qos` option. The indicator can be set to: - - qos0, qos1, qos2: Fixed default QoS level - - coap: Dynamic QoS level by the message type of subscribe request - * qos0: If the subscribe request is non-confirmable - * qos1: If the subscribe request is confirmable -""" +This option specifies the QoS level for the CoAP Client when establishing a subscription membership, if the subscribe request is not carried `qos` option. The indicator can be set to:
+ - qos0, qos1, qos2: Fixed default QoS level
+ - coap: Dynamic QoS level by the message type of subscribe request
+ * qos0: If the subscribe request is non-confirmable
+ * qos1: If the subscribe request is confirmable""" + zh: """客户端订阅请求的默认 QoS 等级。 -当 CoAP 客户端发起订阅请求时,如果未携带 `qos` 参数则会使用该默认值。默认值可设置为: - - qos0、 qos1、qos2: 设置为固定的 QoS 等级 - - coap: 依据订阅操作的 CoAP 报文类型来动态决定 - * 当订阅请求为 `non-confirmable` 类型时,取值为 qos0 - * 当订阅请求为 `confirmable` 类型时,取值为 qos1 -""" +当 CoAP 客户端发起订阅请求时,如果未携带 `qos` 参数则会使用该默认值。默认值可设置为:
+ - qos0、 qos1、qos2: 设置为固定的 QoS 等级
+ - coap: 依据订阅操作的 CoAP 报文类型来动态决定
+ * 当订阅请求为 `non-confirmable` 类型时,取值为 qos0
+ * 当订阅请求为 `confirmable` 类型时,取值为 qos1""" } } coap_publish_qos { desc { en: """The Default QoS Level indicator for publish request. -This option specifies the QoS level for the CoAP Client when publishing a message to EMQX PUB/SUB system, if the publish request is not carried `qos` option. The indicator can be set to: - - qos0, qos1, qos2: Fixed default QoS level - - coap: Dynamic QoS level by the message type of publish request - * qos0: If the publish request is non-confirmable +This option specifies the QoS level for the CoAP Client when publishing a message to EMQX PUB/SUB system, if the publish request is not carried `qos` option. The indicator can be set to:
+ - qos0, qos1, qos2: Fixed default QoS level
+ - coap: Dynamic QoS level by the message type of publish request
+ * qos0: If the publish request is non-confirmable
* qos1: If the publish request is confirmable""" zh: """客户端发布请求的默认 QoS 等级。 -当 CoAP 客户端发起发布请求时,如果未携带 `qos` 参数则会使用该默认值。默认值可设置为: - - qos0、qos1、qos2: 设置为固定的 QoS 等级 - - coap: 依据发布操作的 CoAP 报文类型来动态决定 - * 当发布请求为 `non-confirmable` 类型时,取值为 qos0 - * 当发布请求为 `confirmable` 类型时,取值为 qos1 - """ +当 CoAP 客户端发起发布请求时,如果未携带 `qos` 参数则会使用该默认值。默认值可设置为:
+ - qos0、qos1、qos2: 设置为固定的 QoS 等级
+ - coap: 依据发布操作的 CoAP 报文类型来动态决定
+ * 当发布请求为 `non-confirmable` 类型时,取值为 qos0
+ * 当发布请求为 `confirmable` 类型时,取值为 qos1""" } } @@ -220,14 +216,12 @@ For example, after receiving an update message from a client, any messages withi lwm2m_update_msg_publish_condition { desc { - en: """Policy for publishing UPDATE event message. - - always: send update events as long as the UPDATE request is received. - - contains_object_list: send update events only if the UPDATE request carries any Object List -""" - zh: """发布UPDATE事件消息的策略。 - - always: 只要收到 UPDATE 请求,就发送更新事件。 - - contains_object_list: 仅当 UPDATE 请求携带 Object 列表时才发送更新事件。 -""" + en: """Policy for publishing UPDATE event message.
+ - always: send update events as long as the UPDATE request is received.
+ - contains_object_list: send update events only if the UPDATE request carries any Object List""" + zh: """发布UPDATE事件消息的策略。
+ - always: 只要收到 UPDATE 请求,就发送更新事件。
+ - contains_object_list: 仅当 UPDATE 请求携带 Object 列表时才发送更新事件。""" } } @@ -378,8 +372,8 @@ After succeed observe a resource of LwM2M client, Gateway will send the notify e gateway_common_mountpoint { desc { - en: """ """ - zh: """ """ + en: """""" + zh: """""" } } @@ -439,8 +433,8 @@ After succeed observe a resource of LwM2M client, Gateway will send the notify e tcp_listener { desc { - en: """ """ - zh: """ """ + en: """""" + zh: """""" } } @@ -476,8 +470,8 @@ EMQX will close the TCP connection if proxy protocol packet is not received with ssl_listener { desc { - en: """ """ - zh: """ """ + en: """""" + zh: """""" } } @@ -490,8 +484,8 @@ EMQX will close the TCP connection if proxy protocol packet is not received with udp_listener { desc { - en: """ """ - zh: """ """ + en: """""" + zh: """""" } } @@ -541,8 +535,8 @@ See: https://erlang.org/doc/man/inet.html#setopts-2""" dtls_listener { desc { - en: """ """ - zh: """ """ + en: """""" + zh: """""" } } @@ -601,19 +595,25 @@ When set to false clients will be allowed to connect without authen gateway_common_listener_mountpoint { desc { en: """When publishing or subscribing, prefix all topics with a mountpoint string. -The prefixed string will be removed from the topic name when the message is delivered to the subscriber. The mountpoint is a way that users can use to implement isolation of message routing between different listeners. -For example if a client A subscribes to `t` with `listeners.tcp.\.mountpoint` set to `some_tenant`, then the client actually subscribes to the topic `some_tenant/t`. Similarly, if another client B (connected to the same listener as the client A) sends a message to topic `t`, the message is routed to all the clients subscribed `some_tenant/t`, so client A will receive the message, with topic name `t`. Set to `\"\"` to disable the feature. -Variables in mountpoint string: - - ${clientid}: clientid - - ${username}: username -""" +The prefixed string will be removed from the topic name when the message is delivered to the subscriber. +The mountpoint is a way that users can use to implement isolation of message routing between different listeners. +For example if a client A subscribes to `t` with `listeners.tcp.\.mountpoint` set to `some_tenant`, +then the client actually subscribes to the topic `some_tenant/t`. +Similarly, if another client B (connected to the same listener as the client A) sends a message to topic `t`, +the message is routed to all the clients subscribed `some_tenant/t`, +so client A will receive the message, with topic name `t`. Set to `\"\"` to disable the feature. +Variables in mountpoint string:
+ - ${clientid}: clientid
+ - ${username}: username""" zh: """发布或订阅时,在所有主题前增加前缀字符串。 当消息投递给订阅者时,前缀字符串将从主题名称中删除。挂载点是用户可以用来实现不同监听器之间的消息路由隔离的一种方式。 -例如,如果客户端 A 在 `listeners.tcp.\.mountpoint` 设置为 `some_tenant` 的情况下订阅 `t`,则客户端实际上订阅了 `some_tenant/t` 主题。 类似地,如果另一个客户端 B(连接到与客户端 A 相同的侦听器)向主题 `t` 发送消息,则该消息被路由到所有订阅了 `some_tenant/t` 的客户端,因此客户端 A 将收到该消息,带有 主题名称`t`。 设置为 `\"\"` 以禁用该功能。 -挂载点字符串中可用的变量: - - ${clientid}:clientid - - ${username}:用户名 -""" +例如,如果客户端 A 在 `listeners.tcp.\.mountpoint` 设置为 `some_tenant` 的情况下订阅 `t`, +则客户端实际上订阅了 `some_tenant/t` 主题。 +类似地,如果另一个客户端 B(连接到与客户端 A 相同的侦听器)向主题 `t` 发送消息, +则该消息被路由到所有订阅了 `some_tenant/t` 的客户端,因此客户端 A 将收到该消息,带有 主题名称`t`。 设置为 `\"\"` 以禁用该功能。 +挂载点字符串中可用的变量:
+ - ${clientid}:clientid
+ - ${username}:用户名""" } } diff --git a/apps/emqx_management/i18n/emqx_mgmt_api_key_i18n.conf b/apps/emqx_management/i18n/emqx_mgmt_api_key_i18n.conf index eae559660..f96c5c11b 100644 --- a/apps/emqx_management/i18n/emqx_mgmt_api_key_i18n.conf +++ b/apps/emqx_management/i18n/emqx_mgmt_api_key_i18n.conf @@ -16,14 +16,13 @@ emqx_mgmt_api_key_schema { ``` 7e729ae70d23144b:2QILI9AcQ9BYlVqLDHQNWN2saIjBV4egr1CZneTNKr9CpK ec3907f865805db0:Ee3taYltUKtoBVD9C3XjQl9C6NXheip8Z9B69BpUv5JxVHL - ``` -""" + ```""" + zh: """用于在启动 emqx 时,添加 API 密钥,其格式为: ``` 7e729ae70d23144b:2QILI9AcQ9BYlVqLDHQNWN2saIjBV4egr1CZneTNKr9CpK ec3907f865805db0:Ee3taYltUKtoBVD9C3XjQl9C6NXheip8Z9B69BpUv5JxVHL - ``` -""" + ```""" } label { en: "Initialize api_key file." diff --git a/apps/emqx_management/i18n/emqx_mgmt_api_publish_i18n.conf b/apps/emqx_management/i18n/emqx_mgmt_api_publish_i18n.conf index 4ba7e8dba..4123ceefd 100644 --- a/apps/emqx_management/i18n/emqx_mgmt_api_publish_i18n.conf +++ b/apps/emqx_management/i18n/emqx_mgmt_api_publish_i18n.conf @@ -2,28 +2,24 @@ emqx_mgmt_api_publish { publish_api { desc { - en: """ -Publish one message.
+ en: """Publish one message.
Possible HTTP status response codes are:
200: The message is delivered to at least one subscriber;
202: No matched subscribers;
400: Message is invalid. for example bad topic name, or QoS is out of range;
-503: Failed to deliver the message to subscriber(s);
-""" - zh: """ -发布一个消息。
+503: Failed to deliver the message to subscriber(s)""" + + zh: """发布一个消息。
可能的 HTTP 状态码如下:
200: 消息被成功发送到至少一个订阅。
202: 没有匹配到任何订阅。
400: 消息编码错误,如非法主题,或 QoS 超出范围等。
-503: 服务重启等过程中导致转发失败。

-""" +503: 服务重启等过程中导致转发失败。""" } } publish_bulk_api { desc { - en: """ -Publish a batch of messages.
+ en: """Publish a batch of messages.
Possible HTTP response status code are:
200: All messages are delivered to at least one subscriber;
202: At least one message was not delivered to any subscriber;
@@ -33,10 +29,8 @@ Possible HTTP response status code are:
In case there is at lest one invalid message in the batch, the HTTP response body is the same as for /publish API.
Otherwise the HTTP response body is an array of JSON objects indicating the publish -result of each individual message in the batch. -""" - zh: """ -批量发布一组消息。
+result of each individual message in the batch.""" + zh: """批量发布一组消息。
可能的 HTTP 状态码如下:
200: 所有的消息都被成功发送到至少一个订阅。
202: 至少有一个消息没有匹配到任何订阅。
@@ -45,9 +39,7 @@ result of each individual message in the batch. 请求的 Body 或者 Body 中包含的某个消息无法通过 API 规范的类型检查时,HTTP 响应的消息与发布单个消息的 API /publish 是一样的。 -如果所有的消息都是合法的,那么 HTTP 返回的内容是一个 JSON 数组,每个元素代表了该消息转发的状态。 - -""" +如果所有的消息都是合法的,那么 HTTP 返回的内容是一个 JSON 数组,每个元素代表了该消息转发的状态。""" } } @@ -89,8 +81,7 @@ result of each individual message in the batch. } reason_code { desc { - en: """ -The MQTT reason code, as the same ones used in PUBACK packet.
+ en: """The MQTT reason code, as the same ones used in PUBACK packet.
Currently supported codes are:
16(0x10): No matching subscribers;
@@ -98,18 +89,15 @@ Currently supported codes are:
144(0x90): Topic name invalid;
151(0x97): Publish rate limited, or message size exceeded limit. The global size limit can be configured with mqtt.max_packet_size
NOTE: The message size is estimated with the received topic and payload size, meaning the actual size of serialized bytes (when sent to MQTT subscriber) -might be slightly over the limit. -""" - zh: """ -MQTT 消息发布的错误码,这些错误码也是 MQTT 规范中 PUBACK 消息可能携带的错误码。
+might be slightly over the limit.""" + zh: """MQTT 消息发布的错误码,这些错误码也是 MQTT 规范中 PUBACK 消息可能携带的错误码。
当前支持如下错误码:
16(0x10):没能匹配到任何订阅;
131(0x81):消息转发时发生错误,例如 EMQX 服务重启;
144(0x90):主题名称非法;
151(0x97):受到了速率限制,或者消息尺寸过大。全局消息大小限制可以通过配置项 mqtt.max_packet_size 来进行修改。
-注意:消息尺寸的是通过主题和消息体的字节数进行估算的。具体发布时所占用的字节数可能会稍大于这个估算的值。 -""" +注意:消息尺寸的是通过主题和消息体的字节数进行估算的。具体发布时所占用的字节数可能会稍大于这个估算的值。""" } } error_message { @@ -127,9 +115,7 @@ MQTT 消息发布的错误码,这些错误码也是 MQTT 规范中 PUBACK 消 msg_payload_format_indicator { desc { en: """0 (0x00) Byte Indicates that the Payload is unspecified bytes, which is equivalent to not sending a Payload Format Indicator. - -1 (0x01) Byte Indicates that the Payload is UTF-8 Encoded Character Data. The UTF-8 data in the Payload MUST be well-formed UTF-8 as defined by the Unicode specification and restated in RFC 3629. -""" +1 (0x01) Byte Indicates that the Payload is UTF-8 Encoded Character Data. The UTF-8 data in the Payload MUST be well-formed UTF-8 as defined by the Unicode specification and restated in RFC 3629.""" zh: "载荷格式指示标识符,0 表示载荷是未指定格式的数据,相当于没有发送载荷格式指示;1 表示载荷是 UTF-8 编码的字符数据,载荷中的 UTF-8 数据必须是按照 Unicode 的规范和 RFC 3629 的标准要求进行编码的。" } } diff --git a/apps/emqx_plugins/i18n/emqx_plugins_schema.conf b/apps/emqx_plugins/i18n/emqx_plugins_schema.conf index 454d36f6f..c3fed1884 100644 --- a/apps/emqx_plugins/i18n/emqx_plugins_schema.conf +++ b/apps/emqx_plugins/i18n/emqx_plugins_schema.conf @@ -1,17 +1,14 @@ emqx_plugins_schema { plugins { desc { - en: """ -Manage EMQX plugins.
+ en: """Manage EMQX plugins.
Plugins can be pre-built as a part of EMQX package, or installed as a standalone package in a location specified by install_dir config key
-The standalone-installed plugins are referred to as 'external' plugins. -""" +The standalone-installed plugins are referred to as 'external' plugins.""" zh: """管理EMQX插件。
插件可以是EMQX安装包中的一部分,也可以是一个独立的安装包。
-独立安装的插件称为“外部插件”。 - """ +独立安装的插件称为“外部插件”。""" } label { en: "Plugins" @@ -32,8 +29,7 @@ The standalone-installed plugins are referred to as 'external' plugins. desc { en: """The {name}-{version} of the plugin.
It should match the plugin application name-version as the for the plugin release package name
-For example: my_plugin-0.1.0. -""" +For example: my_plugin-0.1.0.""" zh: """插件的名称{name}-{version}。
它应该与插件的发布包名称一致,如my_plugin-0.1.0。""" } @@ -65,14 +61,12 @@ The plugins are started in the defined order""" } install_dir { desc { - en: """ -The installation directory for the external plugins. + en: """The installation directory for the external plugins. The plugin beam files and configuration files should reside in the subdirectory named as emqx_foo_bar-0.1.0.
NOTE: For security reasons, this directory should **NOT** be writable -by anyone except emqx (or any user which runs EMQX). -""" +by anyone except emqx (or any user which runs EMQX).""" zh: "插件安装包的目录,出于安全考虑,该目录应该值允许 emqx,或用于运行 EMQX 服务的用户拥有写入权限。" } label { @@ -83,11 +77,9 @@ by anyone except emqx (or any user which runs EMQX). check_interval { desc { en: """Check interval: check if the status of the plugins in the cluster is consistent,
-if the results of 3 consecutive checks are not consistent, then alarm. -""" +if the results of 3 consecutive checks are not consistent, then alarm.""" zh: """检查间隔:检查集群中插件的状态是否一致,
-如果连续3次检查结果不一致,则报警。 -""" +如果连续3次检查结果不一致,则报警。""" } } } diff --git a/apps/emqx_prometheus/i18n/emqx_prometheus_schema_i18n.conf b/apps/emqx_prometheus/i18n/emqx_prometheus_schema_i18n.conf index f25e35219..e9a0fc11e 100644 --- a/apps/emqx_prometheus/i18n/emqx_prometheus_schema_i18n.conf +++ b/apps/emqx_prometheus/i18n/emqx_prometheus_schema_i18n.conf @@ -40,15 +40,11 @@ For example, { Authorization = "some-authz-tokens"}""" - ${name}: Name of EMQX node.
- ${host}: Host name of EMQX node.
For example, when the EMQX node name is emqx@127.0.0.1 then the name variable takes value emqx and the host variable takes value 127.0.0.1.
- -Default value is: ${name}/instance/${name}~${host} -""" +Default value is: ${name}/instance/${name}~${host}""" zh: """推送到 Push Gateway 的 Job 名称。可用变量为:
- ${name}: EMQX 节点的名称。 - ${host}: EMQX 节点主机名。 - 例如,当 EMQX 节点名为 emqx@127.0.0.1 则 name 变量的值为 emqx,host 变量的值为 127.0.0.1
- 默认值为: ${name}/instance/${name}~${host}""" } } diff --git a/apps/emqx_psk/i18n/emqx_psk_i18n.conf b/apps/emqx_psk/i18n/emqx_psk_i18n.conf index 6bba9c6d5..60d45977a 100644 --- a/apps/emqx_psk/i18n/emqx_psk_i18n.conf +++ b/apps/emqx_psk/i18n/emqx_psk_i18n.conf @@ -10,8 +10,7 @@ configured, such as RSA-PSK-AES256-GCM-SHA384. See listener SSL options config for more details. -The IDs and secrets can be provided from a file which is configurable by the init_file field. -""" +The IDs and secrets can be provided from a file which is configurable by the init_file field.""" zh: """此配置用于启用 TLS-PSK 身份验证。 PSK 是 “Pre-Shared-Keys” 的缩写。 diff --git a/apps/emqx_retainer/i18n/emqx_retainer_i18n.conf b/apps/emqx_retainer/i18n/emqx_retainer_i18n.conf index 1808f3fe7..274c260d4 100644 --- a/apps/emqx_retainer/i18n/emqx_retainer_i18n.conf +++ b/apps/emqx_retainer/i18n/emqx_retainer_i18n.conf @@ -24,8 +24,7 @@ emqx_retainer_schema { msg_clear_interval { desc { en: """Periodic interval for cleaning up expired messages. -Never clear if the value is 0. - """ +Never clear if the value is 0.""" zh: "消息清理间隔。0 代表不进行清理" } } @@ -99,8 +98,7 @@ If this field is empty, limiter is not used.""" zh: """批量发送的限流器的名称。 限流器可以用来防止短时间内向客户端发送太多的消息,从而避免过多的消息导致客户端队列堵塞甚至崩溃。 这个名称需要是指向 `limiter.batch` 下的一个真实存在的限流器。 -如果这个字段为空,则不使用限流器。 - """ +如果这个字段为空,则不使用限流器。""" } } diff --git a/apps/emqx_rule_engine/i18n/emqx_rule_api_schema.conf b/apps/emqx_rule_engine/i18n/emqx_rule_api_schema.conf index 88a5313ad..e4c2314de 100644 --- a/apps/emqx_rule_engine/i18n/emqx_rule_api_schema.conf +++ b/apps/emqx_rule_engine/i18n/emqx_rule_api_schema.conf @@ -344,11 +344,11 @@ emqx_rule_api_schema { rule_id { desc { en: "The ID of the rule" - zh: "规则的 ID " + zh: "规则的 ID" } label: { en: "Rule ID" - zh: "规则 ID " + zh: "规则 ID" } } diff --git a/apps/emqx_rule_engine/i18n/emqx_rule_engine_schema.conf b/apps/emqx_rule_engine/i18n/emqx_rule_engine_schema.conf index bc5735c67..ca0a73f0f 100644 --- a/apps/emqx_rule_engine/i18n/emqx_rule_engine_schema.conf +++ b/apps/emqx_rule_engine/i18n/emqx_rule_engine_schema.conf @@ -13,14 +13,10 @@ emqx_rule_engine_schema { rules_sql { desc { - en: """ -SQL query to transform the messages. -Example: SELECT * FROM "test/topic" WHERE payload.x = 1 -""" - zh: """ -用于处理消息的 SQL 。 -示例:SELECT * FROM "test/topic" WHERE payload.x = 1 -""" + en: """SQL query to transform the messages. +Example: SELECT * FROM "test/topic" WHERE payload.x = 1""" + zh: """用于处理消息的 SQL 。 +示例:SELECT * FROM "test/topic" WHERE payload.x = 1""" } label: { en: "Rule SQL" @@ -30,8 +26,7 @@ Example: SELECT * FROM "test/topic" WHERE payload.x = 1 rules_actions { desc { - en: """ -A list of actions of the rule. + en: """A list of actions of the rule. An action can be a string that refers to the channel ID of an EMQX bridge, or an object that refers to a function. There a some built-in functions like "republish" and "console", and we also support user @@ -42,16 +37,14 @@ be executed until it returns. If one of the action crashed, all other actions come after it will still be executed, in the original order. If there's any error when running an action, there will be an error message, and the 'failure' -counter of the function action or the bridge channel will increase. -""" - zh: """ -规则的动作列表。 +counter of the function action or the bridge channel will increase.""" + + zh: """规则的动作列表。 动作可以是指向 EMQX bridge 的引用,也可以是一个指向函数的对象。 我们支持一些内置函数,如“republish”和“console”,我们还支持用户提供的函数,它的格式为:“{module}:{function}”。 列表中的动作按顺序执行。这意味着如果其中一个动作执行缓慢,则以下所有动作都不会被执行直到它返回。 如果其中一个动作崩溃,在它之后的所有动作仍然会被按照原始顺序执行。 -如果运行动作时出现任何错误,则会出现错误消息,并且相应的计数器会增加。 -""" +如果运行动作时出现任何错误,则会出现错误消息,并且相应的计数器会增加。""" } label: { en: "Rule Action List" @@ -116,19 +109,16 @@ counter of the function action or the bridge channel will increase. user_provided_function_function { desc { - en: """ -The user provided function. Should be in the format: '{module}:{function}'. + en: """The user provided function. Should be in the format: '{module}:{function}'. Where {module} is the Erlang callback module and {function} is the Erlang function. To write your own function, checkout the function console and republish in the source file: -apps/emqx_rule_engine/src/emqx_rule_actions.erl as an example. -""" - zh: """ -用户提供的函数。 格式应为:'{module}:{function}'。 +apps/emqx_rule_engine/src/emqx_rule_actions.erl as an example.""" + + zh: """用户提供的函数。 格式应为:'{module}:{function}'。 其中 {module} 是 Erlang 回调模块, {function} 是 Erlang 函数。 -要编写自己的函数,请检查源文件:apps/emqx_rule_engine/src/emqx_rule_actions.erl 中的示例函数 consolerepublish 。 -""" +要编写自己的函数,请检查源文件:apps/emqx_rule_engine/src/emqx_rule_actions.erl 中的示例函数 consolerepublish 。""" } label: { en: "User Provided Function" @@ -138,15 +128,11 @@ To write your own function, checkout the function console and user_provided_function_args { desc { - en: """ -The args will be passed as the 3rd argument to module:function/3, + en: """The args will be passed as the 3rd argument to module:function/3, checkout the function console and republish in the source file: -apps/emqx_rule_engine/src/emqx_rule_actions.erl as an example. -""" - zh: """ -用户提供的参数将作为函数 module:function/3 的第三个参数, -请检查源文件:apps/emqx_rule_engine/src/emqx_rule_actions.erl 中的示例函数 consolerepublish 。 -""" +apps/emqx_rule_engine/src/emqx_rule_actions.erl as an example.""" + zh: """用户提供的参数将作为函数 module:function/3 的第三个参数, +请检查源文件:apps/emqx_rule_engine/src/emqx_rule_actions.erl 中的示例函数 consolerepublish 。""" } label: { en: "User Provided Function Args" @@ -156,14 +142,10 @@ checkout the function console and republish in the sou republish_args_topic { desc { - en: """ -The target topic of message to be re-published. -Template with variables is allowed, see description of the 'republish_args'. -""" - zh: """ -重新发布消息的目标主题。 -允许使用带有变量的模板,请参阅“republish_args”的描述。 -""" + en: """The target topic of message to be re-published. +Template with variables is allowed, see description of the 'republish_args'.""" + zh: """重新发布消息的目标主题。 +允许使用带有变量的模板,请参阅“republish_args”的描述。""" } label: { en: "Target Topic" @@ -173,16 +155,12 @@ Template with variables is allowed, see description of the 'republish_args'. republish_args_qos { desc { - en: """ -The qos of the message to be re-published. + en: """The qos of the message to be re-published. Template with variables is allowed, see description of the 'republish_args'. Defaults to ${qos}. If variable ${qos} is not found from the selected result of the rule, -0 is used. -""" - zh: """ -要重新发布的消息的 qos。允许使用带有变量的模板,请参阅“republish_args”的描述。 -默认为 ${qos}。 如果从规则的选择结果中没有找到变量 ${qos},则使用 0。 -""" +0 is used.""" + zh: """要重新发布的消息的 qos。允许使用带有变量的模板,请参阅“republish_args”的描述。 +默认为 ${qos}。 如果从规则的选择结果中没有找到变量 ${qos},则使用 0。""" } label: { en: "Message QoS" @@ -192,16 +170,12 @@ Defaults to ${qos}. If variable ${qos} is not found from the selected result of republish_args_retain { desc { - en: """ -The 'retain' flag of the message to be re-published. + en: """The 'retain' flag of the message to be re-published. Template with variables is allowed, see description of the 'republish_args'. Defaults to ${retain}. If variable ${retain} is not found from the selected result -of the rule, false is used. -""" - zh: """ -要重新发布的消息的“保留”标志。允许使用带有变量的模板,请参阅“republish_args”的描述。 -默认为 ${retain}。 如果从所选结果中未找到变量 ${retain},则使用 false。 -""" +of the rule, false is used.""" + zh: """要重新发布的消息的“保留”标志。允许使用带有变量的模板,请参阅“republish_args”的描述。 +默认为 ${retain}。 如果从所选结果中未找到变量 ${retain},则使用 false。""" } label: { en: "Retain Flag" @@ -211,16 +185,12 @@ of the rule, false is used. republish_args_payload { desc { - en: """ -The payload of the message to be re-published. + en: """The payload of the message to be re-published. Template with variables is allowed, see description of the 'republish_args'. Defaults to ${payload}. If variable ${payload} is not found from the selected result -of the rule, then the string "undefined" is used. -""" - zh: """ -要重新发布的消息的有效负载。允许使用带有变量的模板,请参阅“republish_args”的描述。 -默认为 ${payload}。 如果从所选结果中未找到变量 ${payload},则使用字符串 "undefined"。 -""" +of the rule, then the string "undefined" is used.""" + zh: """要重新发布的消息的有效负载。允许使用带有变量的模板,请参阅“republish_args”的描述。 +默认为 ${payload}。 如果从所选结果中未找到变量 ${payload},则使用字符串 "undefined"。""" } label: { en: "Message Payload" @@ -229,8 +199,7 @@ of the rule, then the string "undefined" is used. } republish_args_user_properties { desc { - en: """ -From which variable should the MQTT message's User-Property pairs be taken from. + en: """From which variable should the MQTT message's User-Property pairs be taken from. The value must be a map. You may configure it to ${pub_props.'User-Property'} or use SELECT *,pub_props.'User-Property' as user_properties @@ -238,17 +207,15 @@ to forward the original user properties to the republished message. You may also call map_put function like map_put('my-prop-name', 'my-prop-value', user_properties) as user_properties to inject user properties. -NOTE: MQTT spec allows duplicated user property names, but EMQX Rule-Engine does not. -""" - zh: """ -指定使用哪个变量来填充 MQTT 消息的 User-Property 列表。这个变量的值必须是一个 map 类型。 +NOTE: MQTT spec allows duplicated user property names, but EMQX Rule-Engine does not.""" + + zh: """指定使用哪个变量来填充 MQTT 消息的 User-Property 列表。这个变量的值必须是一个 map 类型。 可以设置成 ${pub_props.'User-Property'} 或者 使用 SELECT *,pub_props.'User-Property' as user_properties 来把源 MQTT 消息 的 User-Property 列表用于填充。 也可以使用 map_put 函数来添加新的 User-Property, map_put('my-prop-name', 'my-prop-value', user_properties) as user_properties -注意:MQTT 协议允许一个消息中出现多次同一个 property 名,但是 EMQX 的规则引擎不允许。 -""" +注意:MQTT 协议允许一个消息中出现多次同一个 property 名,但是 EMQX 的规则引擎不允许。""" } } @@ -370,8 +337,7 @@ Then there are 3 variables available: clientid, qos an When the rule is triggered by an MQTT message with payload = `hello`, qos = 1, clientid = `Steve`, the rule will republish a new MQTT message to topic `t/Steve`, payload = `msg: hello`, and `qos = 1`.""" - zh: """ -内置 'republish' 动作的参数。 + zh: """内置 'republish' 动作的参数。 可以在参数中使用变量。 变量是规则中选择的字段。 例如规则 SQL 定义如下: diff --git a/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_gcp_pubsub.conf b/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_gcp_pubsub.conf index dd7ce49ec..af2a93f82 100644 --- a/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_gcp_pubsub.conf +++ b/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_gcp_pubsub.conf @@ -103,11 +103,9 @@ emqx_ee_bridge_gcp_pubsub { matching `local_topic` will be forwarded.
NOTE: if this bridge is used as the action of a rule (EMQX rule engine), and also local_topic is configured, then both the data got from the rule and the MQTT messages that match local_topic -will be forwarded. -""" +will be forwarded.""" zh: """发送到 'local_topic' 的消息都会转发到 GCP PubSub。
-注意:如果这个 Bridge 被用作规则(EMQX 规则引擎)的输出,同时也配置了 'local_topic' ,那么这两部分的消息都会被转发到 GCP PubSub。 -""" +注意:如果这个 Bridge 被用作规则(EMQX 规则引擎)的输出,同时也配置了 'local_topic' ,那么这两部分的消息都会被转发到 GCP PubSub。""" } label { en: "Local Topic" diff --git a/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_hstreamdb.conf b/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_hstreamdb.conf index dd3346579..dce40aa85 100644 --- a/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_hstreamdb.conf +++ b/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_hstreamdb.conf @@ -1,17 +1,13 @@ emqx_ee_bridge_hstreamdb { local_topic { desc { - en: """ -The MQTT topic filter to be forwarded to the HStreamDB. All MQTT 'PUBLISH' messages with the topic + en: """The MQTT topic filter to be forwarded to the HStreamDB. All MQTT 'PUBLISH' messages with the topic matching the local_topic will be forwarded.
NOTE: if this bridge is used as the action of a rule (EMQX rule engine), and also local_topic is configured, then both the data got from the rule and the MQTT messages that match local_topic -will be forwarded. -""" - zh: """ -发送到 'local_topic' 的消息都会转发到 HStreamDB。
-注意:如果这个 Bridge 被用作规则(EMQX 规则引擎)的输出,同时也配置了 'local_topic' ,那么这两部分的消息都会被转发到 HStreamDB。 -""" +will be forwarded.""" + zh: """发送到 'local_topic' 的消息都会转发到 HStreamDB。
+注意:如果这个 Bridge 被用作规则(EMQX 规则引擎)的输出,同时也配置了 'local_topic' ,那么这两部分的消息都会被转发到 HStreamDB。""" } label { en: "Local Topic" diff --git a/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_influxdb.conf b/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_influxdb.conf index b2c3c5a73..8b2eadcfa 100644 --- a/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_influxdb.conf +++ b/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_influxdb.conf @@ -5,11 +5,9 @@ emqx_ee_bridge_influxdb { matching the local_topic will be forwarded.
NOTE: if this bridge is used as the action of a rule (EMQX rule engine), and also local_topic is configured, then both the data got from the rule and the MQTT messages that match local_topic -will be forwarded. -""" +will be forwarded.""" zh: """发送到 'local_topic' 的消息都会转发到 InfluxDB。
-注意:如果这个 Bridge 被用作规则(EMQX 规则引擎)的输出,同时也配置了 'local_topic' ,那么这两部分的消息都会被转发到 InfluxDB。 -""" +注意:如果这个 Bridge 被用作规则(EMQX 规则引擎)的输出,同时也配置了 'local_topic' ,那么这两部分的消息都会被转发到 InfluxDB。""" } label { en: "Local Topic" @@ -24,16 +22,14 @@ See also [InfluxDB 2.3 Line Protocol](https://docs.influxdata.com/influxdb/v2.3/ TLDR:
``` [,=[,=]] =[,=] [] -``` -""" +```""" zh: """使用 InfluxDB API Line Protocol 写入 InfluxDB 的数据,支持占位符
参考 [InfluxDB 2.3 Line Protocol](https://docs.influxdata.com/influxdb/v2.3/reference/syntax/line-protocol/) 及 [InfluxDB 1.8 Line Protocol](https://docs.influxdata.com/influxdb/v1.8/write_protocols/line_protocol_tutorial/)
TLDR:
``` [,=[,=]] =[,=] [] -``` -""" +```""" } label { en: "Write Syntax" diff --git a/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_kafka.conf b/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_kafka.conf index fff798e19..e72da2323 100644 --- a/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_kafka.conf +++ b/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_kafka.conf @@ -325,10 +325,10 @@ emqx_ee_bridge_kafka { desc { en: "Partition strategy is to tell the producer how to dispatch messages to Kafka partitions.\n\n" "random: Randomly pick a partition for each message\n" - "key_dispatch: Hash Kafka message key to a partition number\n" + "key_dispatch: Hash Kafka message key to a partition number" zh: "设置消息发布时应该如何选择 Kafka 分区。\n\n" "random: 为每个消息随机选择一个分区。\n" - "key_dispatch: Hash Kafka message key to a partition number\n" + "key_dispatch: Hash Kafka message key to a partition number" } label { en: "Partition Strategy" @@ -341,11 +341,11 @@ emqx_ee_bridge_kafka { "before it sends back the acknowledgement to EMQX Kafka producer\n\n" "all_isr: Require all in-sync replicas to acknowledge.\n" "leader_only: Require only the partition-leader's acknowledgement.\n" - "none: No need for Kafka to acknowledge at all.\n" + "none: No need for Kafka to acknowledge at all." zh: "设置 Kafka leader 在返回给 EMQX 确认之前需要等待多少个 follower 的确认。\n\n" "all_isr: 需要所有的在线复制者都确认。\n" "leader_only: 仅需要分区 leader 确认。\n" - "none: 无需 Kafka 回复任何确认。\n" + "none: 无需 Kafka 回复任何确认。" } label { en: "Required Acks" @@ -382,7 +382,7 @@ emqx_ee_bridge_kafka { desc { en: "Configure producer message buffer.\n\n" "Tell Kafka producer how to buffer messages when EMQX has more messages to send than " - "Kafka can keep up, or when Kafka is down.\n\n" + "Kafka can keep up, or when Kafka is down." zh: "配置消息缓存的相关参数。\n\n" "当 EMQX 需要发送的消息超过 Kafka 处理能力,或者当 Kafka 临时下线时,EMQX 内部会将消息缓存起来。" } diff --git a/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_mysql.conf b/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_mysql.conf index 0c56b1976..345fd9cba 100644 --- a/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_mysql.conf +++ b/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_mysql.conf @@ -6,11 +6,9 @@ emqx_ee_bridge_mysql { matching the local_topic will be forwarded.
NOTE: if this bridge is used as the action of a rule (EMQX rule engine), and also local_topic is configured, then both the data got from the rule and the MQTT messages that match local_topic -will be forwarded. -""" +will be forwarded.""" zh: """发送到 'local_topic' 的消息都会转发到 MySQL。
-注意:如果这个 Bridge 被用作规则(EMQX 规则引擎)的输出,同时也配置了 'local_topic' ,那么这两部分的消息都会被转发。 -""" +注意:如果这个 Bridge 被用作规则(EMQX 规则引擎)的输出,同时也配置了 'local_topic' ,那么这两部分的消息都会被转发。""" } label { en: "Local Topic" diff --git a/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_pgsql.conf b/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_pgsql.conf index 0f80e1a1b..81d2330c5 100644 --- a/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_pgsql.conf +++ b/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_pgsql.conf @@ -6,11 +6,9 @@ emqx_ee_bridge_pgsql { matching the local_topic will be forwarded.
NOTE: if this bridge is used as the action of a rule (EMQX rule engine), and also local_topic is configured, then both the data got from the rule and the MQTT messages that match local_topic -will be forwarded. -""" +will be forwarded.""" zh: """发送到 'local_topic' 的消息都会转发到 PostgreSQL。
-注意:如果这个 Bridge 被用作规则(EMQX 规则引擎)的输出,同时也配置了 'local_topic' ,那么这两部分的消息都会被转发。 -""" +注意:如果这个 Bridge 被用作规则(EMQX 规则引擎)的输出,同时也配置了 'local_topic' ,那么这两部分的消息都会被转发。""" } label { en: "Local Topic" diff --git a/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_redis.conf b/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_redis.conf index 0589899b5..4de42b4e3 100644 --- a/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_redis.conf +++ b/lib-ee/emqx_ee_bridge/i18n/emqx_ee_bridge_redis.conf @@ -5,11 +5,9 @@ emqx_ee_bridge_redis { matching the local_topic will be forwarded.
NOTE: if this bridge is used as the action of a rule (EMQX rule engine), and also local_topic is configured, then both the data got from the rule and the MQTT messages that match local_topic -will be forwarded. -""" +will be forwarded.""" zh: """发送到 'local_topic' 的消息都会转发到 Redis。
-注意:如果这个 Bridge 被用作规则(EMQX 规则引擎)的输出,同时也配置了 'local_topic' ,那么这两部分的消息都会被转发到 Redis。 -""" +注意:如果这个 Bridge 被用作规则(EMQX 规则引擎)的输出,同时也配置了 'local_topic' ,那么这两部分的消息都会被转发到 Redis。""" } label { en: "Local Topic" @@ -21,11 +19,9 @@ will be forwarded. desc { en: """Redis command template used to export messages. Each list element stands for a command name or its argument. For example, to push payloads in a Redis list by key `msgs`, the elements should be the following: -`rpush`, `msgs`, `${payload}`. -""" +`rpush`, `msgs`, `${payload}`.""" zh: """用于推送数据的 Redis 命令模板。 每个列表元素代表一个命令名称或其参数。 -例如,要通过键值 `msgs` 将消息体推送到 Redis 列表中,数组元素应该是: `rpush`, `msgs`, `${payload}`。 -""" +例如,要通过键值 `msgs` 将消息体推送到 Redis 列表中,数组元素应该是: `rpush`, `msgs`, `${payload}`。""" } label { en: "Redis Command Template" diff --git a/lib-ee/emqx_ee_connector/i18n/emqx_ee_connector_influxdb.conf b/lib-ee/emqx_ee_connector/i18n/emqx_ee_connector_influxdb.conf index 81ea39d49..c00e88ef9 100644 --- a/lib-ee/emqx_ee_connector/i18n/emqx_ee_connector_influxdb.conf +++ b/lib-ee/emqx_ee_connector/i18n/emqx_ee_connector_influxdb.conf @@ -4,12 +4,10 @@ emqx_ee_connector_influxdb { desc { en: """The IPv4 or IPv6 address or the hostname to connect to.
A host entry has the following form: `Host[:Port]`.
-The InfluxDB default port 8086 is used if `[:Port]` is not specified. -""" +The InfluxDB default port 8086 is used if `[:Port]` is not specified.""" zh: """将要连接的 IPv4 或 IPv6 地址,或者主机名。
主机名具有以下形式:`Host[:Port]`。
-如果未指定 `[:Port]`,则使用 InfluxDB 默认端口 8086。 -""" +如果未指定 `[:Port]`,则使用 InfluxDB 默认端口 8086。""" } label { en: "Server Host" diff --git a/lib-ee/emqx_license/i18n/emqx_license_schema_i18n.conf b/lib-ee/emqx_license/i18n/emqx_license_schema_i18n.conf index 379cb3358..c330f1cb2 100644 --- a/lib-ee/emqx_license/i18n/emqx_license_schema_i18n.conf +++ b/lib-ee/emqx_license/i18n/emqx_license_schema_i18n.conf @@ -12,7 +12,7 @@ emqx_license_schema { "EMQX 自带一个默认的试用许可证," "默认试用许可允许最多接入 100 个连接,签发时间是 2023年1月9日,有效期是 5 年(1825 天)。" "若需要在生产环境部署,\n" - "请访问 https://www.emqx.com/apply-licenses/emqx 来申请。\n" + "请访问 https://www.emqx.com/apply-licenses/emqx 来申请。" } label { en: "License" diff --git a/scripts/check-i18n-style.escript b/scripts/check-i18n-style.escript new file mode 100755 index 000000000..6ad6c1770 --- /dev/null +++ b/scripts/check-i18n-style.escript @@ -0,0 +1,116 @@ +#!/usr/bin/env escript + +-mode(compile). + +-define(YELLOW, "\e[33m"). +-define(RED, "\e[31m"). +-define(RESET, "\e[39m"). + +main([Files0]) -> + _ = put(errors, 0), + Files = string:tokens(Files0, "\n"), + ok = load_hocon(), + ok = lists:foreach(fun check/1, Files), + case get(errors) of + 1 -> + logerr("1 error found~n", []); + N when is_integer(N) andalso N > 1 -> + logerr("~p errors found~n", [N]); + _ -> + io:format(user, "OK~n", []) + end. + +load_hocon() -> + Dir = "_build/default/lib/hocon/ebin", + File = filename:join([Dir, "hocon.beam"]), + case filelib:is_regular(File) of + true -> + code:add_path(Dir), + ok; + false -> + die("HOCON is not compiled in " ++ Dir ++ "~n") + end. + +die(Msg) -> + die(Msg, []). + +die(Msg, Args) -> + ok = logerr(Msg, Args), + halt(1). + +logerr(Fmt, Args) -> + io:format(standard_error, ?RED ++ "ERROR: " ++ Fmt ++ ?RESET, Args), + N = get(errors), + _ = put(errors, N + 1), + ok. + + +check(File) -> + io:format(user, "checking: ~s~n", [File]), + {ok, C} = hocon:load(File), + maps:foreach(fun check_one_field/2, C), + ok. + +check_one_field(Name, Field) -> + maps:foreach(fun(SubName, DescAndLabel) -> + check_desc_and_label([Name, ".", SubName], DescAndLabel) + end, Field). + +check_desc_and_label(Name, D) -> + case maps:keys(D) -- [<<"desc">>, <<"label">>] of + [] -> + ok; + Unknown -> + die("~s: unknown tags ~p~n", [Name, Unknown]) + end, + ok = check_desc(Name, D), + ok = check_label(Name, D). + +check_label(_Name, #{<<"label">> := _Label}) -> + ok; +check_label(_Name, _) -> + %% some may not have label + ok. + +check_desc(Name, #{<<"desc">> := Desc}) -> + do_check_desc(Name, Desc); +check_desc(Name, _) -> + die("~s: no 'desc'~n", [Name]). + +do_check_desc(Name, #{<<"zh">> := Zh, <<"en">> := En}) -> + ok = check_desc_string(Name, "zh", Zh), + ok = check_desc_string(Name, "en", En); +do_check_desc(Name, _) -> + die("~s: missing 'zh' or 'en'~n", [Name]). + +check_desc_string(Name, Tr, <<>>) -> + io:format(standard_error, ?YELLOW ++ "WARNING: ~s.~s: empty string~n" ++ ?RESET, [Name, Tr]); +check_desc_string(Name, Tr, BinStr) -> + Str = unicode:characters_to_list(BinStr, utf8), + Err = fun(Reason) -> + logerr("~s.~s: ~s~n", [Name, Tr, Reason]) + end, + case Str of + [$\s | _] -> + Err("remove leading whitespace"); + [$\n | _] -> + Err("remove leading line-break"); + "
" -> + Err("remove leading
"); + "
" -> + Err("remove leading
"); + _ -> + ok + end, + case lists:reverse(Str) of + [$\s | _] -> + Err("remove trailing whitespace"); + [$\n | _] -> + Err("remove trailing line-break"); + ">/rb<" ++ _ -> + Err("remove trailing
"); + ">/ rb<" ++ _ -> + Err("remove trailing
"); + _ -> + ok + end. diff --git a/scripts/check-i18n-style.sh b/scripts/check-i18n-style.sh new file mode 100755 index 000000000..0be565f30 --- /dev/null +++ b/scripts/check-i18n-style.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd -P -- "$(dirname -- "$0")/.." + +all_files="$(git ls-files '*i18n*.conf')" + +./scripts/check-i18n-style.escript "$all_files" From 48efe552d1bf74ca6134cfb168986afb7edc4b2b Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Thu, 9 Feb 2023 14:31:42 +0300 Subject: [PATCH 0491/1802] test(mqtt-bridge): manage test flaps by waiting for flush events --- .../test/emqx_bridge_mqtt_SUITE.erl | 44 ++++++++++++------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl b/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl index c4afa4db2..49b333216 100644 --- a/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl +++ b/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl @@ -317,10 +317,13 @@ t_mqtt_conn_bridge_egress(_) -> RemoteTopic = <>, Payload = <<"hello">>, emqx:subscribe(RemoteTopic), - timer:sleep(100), - %% PUBLISH a message to the 'local' broker, as we have only one broker, - %% the remote broker is also the local one. - emqx:publish(emqx_message:make(LocalTopic, Payload)), + + ?wait_async_action( + %% PUBLISH a message to the 'local' broker, as we have only one broker, + %% the remote broker is also the local one. + emqx:publish(emqx_message:make(LocalTopic, Payload)), + #{?snk_kind := buffer_worker_flush_ack} + ), %% we should receive a message on the "remote" broker, with specified topic Msg = assert_mqtt_msg_received(RemoteTopic, Payload), @@ -356,10 +359,13 @@ t_mqtt_conn_bridge_egress_no_payload_template(_) -> RemoteTopic = <>, Payload = <<"hello">>, emqx:subscribe(RemoteTopic), - timer:sleep(100), - %% PUBLISH a message to the 'local' broker, as we have only one broker, - %% the remote broker is also the local one. - emqx:publish(emqx_message:make(LocalTopic, Payload)), + + ?wait_async_action( + %% PUBLISH a message to the 'local' broker, as we have only one broker, + %% the remote broker is also the local one. + emqx:publish(emqx_message:make(LocalTopic, Payload)), + #{?snk_kind := buffer_worker_flush_ack} + ), %% we should receive a message on the "remote" broker, with specified topic Msg = assert_mqtt_msg_received(RemoteTopic), @@ -444,10 +450,13 @@ t_mqtt_conn_bridge_ingress_and_egress(_) -> } ] } = request_bridge_metrics(BridgeIDEgress), - timer:sleep(100), - %% PUBLISH a message to the 'local' broker, as we have only one broker, - %% the remote broker is also the local one. - emqx:publish(emqx_message:make(LocalTopic, Payload)), + + ?wait_async_action( + %% PUBLISH a message to the 'local' broker, as we have only one broker, + %% the remote broker is also the local one. + emqx:publish(emqx_message:make(LocalTopic, Payload)), + #{?snk_kind := buffer_worker_flush_ack} + ), %% we should receive a message on the "remote" broker, with specified topic assert_mqtt_msg_received(RemoteTopic, Payload), @@ -677,10 +686,13 @@ t_mqtt_conn_bridge_egress_reconnect(_) -> RemoteTopic = <>, Payload0 = <<"hello">>, emqx:subscribe(RemoteTopic), - timer:sleep(100), - %% PUBLISH a message to the 'local' broker, as we have only one broker, - %% the remote broker is also the local one. - emqx:publish(emqx_message:make(LocalTopic, Payload0)), + + ?wait_async_action( + %% PUBLISH a message to the 'local' broker, as we have only one broker, + %% the remote broker is also the local one. + emqx:publish(emqx_message:make(LocalTopic, Payload0)), + #{?snk_kind := buffer_worker_flush_ack} + ), %% we should receive a message on the "remote" broker, with specified topic assert_mqtt_msg_received(RemoteTopic, Payload0), From 202f6d0181af80840576d3c90b686a1a8b48fb2e Mon Sep 17 00:00:00 2001 From: Ivan Dyachkov Date: Mon, 6 Feb 2023 11:00:29 +0100 Subject: [PATCH 0492/1802] ci: stop building alpine docker image --- .ci/docker-compose-file/.env | 1 - .../build_and_push_docker_images.yaml | 17 +---- .github/workflows/run_fvt_tests.yaml | 4 +- build | 8 +- deploy/docker/Dockerfile.alpine | 76 ------------------- 5 files changed, 7 insertions(+), 99 deletions(-) delete mode 100644 deploy/docker/Dockerfile.alpine diff --git a/.ci/docker-compose-file/.env b/.ci/docker-compose-file/.env index e5564efb7..a71e174d9 100644 --- a/.ci/docker-compose-file/.env +++ b/.ci/docker-compose-file/.env @@ -6,4 +6,3 @@ LDAP_TAG=2.4.50 INFLUXDB_TAG=2.5.0 TARGET=emqx/emqx -EMQX_TAG=build-alpine-amd64 diff --git a/.github/workflows/build_and_push_docker_images.yaml b/.github/workflows/build_and_push_docker_images.yaml index 213ab27fd..b21683dc7 100644 --- a/.github/workflows/build_and_push_docker_images.yaml +++ b/.github/workflows/build_and_push_docker_images.yaml @@ -117,14 +117,10 @@ jobs: matrix: profile: - "${{ needs.prepare.outputs.PROFILE }}" - flavor: - - '' - - '-elixir' registry: - 'docker.io' - 'public.ecr.aws' os: - - [alpine3.15.1, "alpine:3.15.1", "deploy/docker/Dockerfile.alpine"] - [debian11, "debian:11-slim", "deploy/docker/Dockerfile"] # NOTE: 'otp' and 'elixir' are to configure emqx-builder image # only support latest otp and elixir, not a matrix @@ -133,12 +129,11 @@ jobs: otp: - 24.3.4.2-1 # switch to 25 once ready to release 5.1 elixir: - - 1.13.4 # update to latest + - 'no_elixir' + - '1.13.4' # update to latest exclude: # TODO: publish enterprise to ecr too? - registry: 'public.ecr.aws' profile: emqx-enterprise - - flavor: '-elixir' - os: [alpine3.15.1, "alpine:3.15.1", "deploy/docker/Dockerfile.alpine"] steps: - uses: actions/download-artifact@v3 @@ -173,14 +168,10 @@ jobs: run: | extra_labels= img_suffix= - flavor="${{ matrix.flavor }}" - if [ "${{ matrix.flavor }}" = '-elixir' ]; then + if [ "${{ matrix.elixir }}" != 'no_elixir' ]; then img_suffix="-elixir" extra_labels="org.opencontainers.image.elixir.version=${{ matrix.elixir }}" fi - if [[ "${{ matrix.os[0] }}" =~ "alpine" ]]; then - img_suffix="${img_suffix}-alpine" - fi echo "img_suffix=$img_suffix" >> $GITHUB_OUTPUT echo "extra_labels=$extra_labels" >> $GITHUB_OUTPUT @@ -209,6 +200,6 @@ jobs: tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} build-args: | - EMQX_NAME=${{ matrix.profile }}${{ matrix.flavor }} + EMQX_NAME=${{ matrix.profile }}${{ steps.pre-meta.outputs.img_suffix }} file: source/${{ matrix.os[2] }} context: source diff --git a/.github/workflows/run_fvt_tests.yaml b/.github/workflows/run_fvt_tests.yaml index f233562d1..994326d23 100644 --- a/.github/workflows/run_fvt_tests.yaml +++ b/.github/workflows/run_fvt_tests.yaml @@ -16,7 +16,7 @@ jobs: prepare: runs-on: ubuntu-20.04 # prepare source with any OTP version, no need for a matrix - container: ghcr.io/emqx/emqx-builder/5.0-27:1.13.4-24.3.4.2-1-alpine3.15.1 + container: ghcr.io/emqx/emqx-builder/5.0-27:1.13.4-24.3.4.2-1-debian11 steps: - uses: actions/checkout@v3 @@ -47,7 +47,7 @@ jobs: - mnesia - rlog os: - - ["alpine3.15.1", "alpine:3.15.1"] + - ["debian11", "debian:11-slim"] builder: - 5.0-27 otp: diff --git a/build b/build index 06b1d29e2..195464612 100755 --- a/build +++ b/build @@ -322,13 +322,7 @@ make_tgz() { make_docker() { EMQX_BUILDER="${EMQX_BUILDER:-${EMQX_DEFAULT_BUILDER}}" EMQX_RUNNER="${EMQX_RUNNER:-${EMQX_DEFAULT_RUNNER}}" - if [ -z "${EMQX_DOCKERFILE:-}" ]; then - if [[ "$EMQX_BUILDER" =~ "alpine" ]]; then - EMQX_DOCKERFILE='deploy/docker/Dockerfile.alpine' - else - EMQX_DOCKERFILE='deploy/docker/Dockerfile' - fi - fi + EMQX_DOCKERFILE="${EMQX_DOCKERFILE:-deploy/docker/Dockerfile}" if [[ "$PROFILE" = *-elixir ]]; then PKG_VSN="$PKG_VSN-elixir" fi diff --git a/deploy/docker/Dockerfile.alpine b/deploy/docker/Dockerfile.alpine deleted file mode 100644 index 0f72be9ab..000000000 --- a/deploy/docker/Dockerfile.alpine +++ /dev/null @@ -1,76 +0,0 @@ -ARG BUILD_FROM=ghcr.io/emqx/emqx-builder/5.0-26:1.13.4-24.3.4.2-1-alpine3.15.1 -ARG RUN_FROM=alpine:3.15.1 -FROM ${BUILD_FROM} AS builder - -RUN apk add --no-cache \ - autoconf \ - automake \ - bash \ - bison \ - bsd-compat-headers \ - coreutils \ - curl \ - flex \ - g++ \ - gcc \ - git \ - jq \ - libc-dev \ - libstdc++ \ - libtool \ - make \ - ncurses-dev \ - openssl-dev \ - perl - -COPY . /emqx - -ARG EMQX_NAME=emqx -ENV EMQX_RELUP=false - -RUN export PROFILE=${EMQX_NAME%%-elixir} \ - && export EMQX_NAME1=$EMQX_NAME \ - && export EMQX_NAME=$PROFILE \ - && export EMQX_LIB_PATH="_build/$EMQX_NAME/lib" \ - && export EMQX_REL_PATH="/emqx/_build/$EMQX_NAME/rel/emqx" \ - && export EMQX_REL_FORM='docker' \ - && cd /emqx \ - && rm -rf $EMQX_LIB_PATH \ - && make $EMQX_NAME1 \ - && mkdir -p /emqx-rel \ - && mv $EMQX_REL_PATH /emqx-rel - -FROM $RUN_FROM - -COPY deploy/docker/docker-entrypoint.sh /usr/bin/ -COPY --from=builder /emqx-rel/emqx /opt/emqx - -RUN ln -s /opt/emqx/bin/* /usr/local/bin/ -RUN apk add --no-cache curl ncurses-libs openssl sudo libstdc++ bash - -WORKDIR /opt/emqx - -RUN adduser -D -u 1000 emqx \ - && echo "emqx ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers - -RUN chgrp -Rf emqx /opt/emqx && chmod -Rf g+w /opt/emqx \ - && chown -Rf emqx /opt/emqx - -USER emqx - -VOLUME ["/opt/emqx/log", "/opt/emqx/data"] - -# emqx will occupy these port: -# - 1883 port for MQTT -# - 8083 for WebSocket/HTTP -# - 8084 for WSS/HTTPS -# - 8883 port for MQTT(SSL) -# - 11883 port for internal MQTT/TCP -# - 18083 for dashboard and API -# - 4370 default Erlang distrbution port -# - 5369 for backplain gen_rpc -EXPOSE 1883 8083 8084 8883 11883 18083 4370 5369 - -ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"] - -CMD ["/opt/emqx/bin/emqx", "foreground"] From ea534f93ad86f7c60054da2c2b49814e8c552663 Mon Sep 17 00:00:00 2001 From: Ivan Dyachkov Date: Thu, 9 Feb 2023 13:29:03 +0100 Subject: [PATCH 0493/1802] chore: add changelog --- changes/v5.0.17/feat-9917.en.md | 1 + changes/v5.0.17/feat-9917.zh.md | 1 + 2 files changed, 2 insertions(+) create mode 100644 changes/v5.0.17/feat-9917.en.md create mode 100644 changes/v5.0.17/feat-9917.zh.md diff --git a/changes/v5.0.17/feat-9917.en.md b/changes/v5.0.17/feat-9917.en.md new file mode 100644 index 000000000..dfc00649e --- /dev/null +++ b/changes/v5.0.17/feat-9917.en.md @@ -0,0 +1 @@ +Stop building -alpine docker image because it's size is larger than the regular one based on debian slim diff --git a/changes/v5.0.17/feat-9917.zh.md b/changes/v5.0.17/feat-9917.zh.md new file mode 100644 index 000000000..797062b60 --- /dev/null +++ b/changes/v5.0.17/feat-9917.zh.md @@ -0,0 +1 @@ +停止构建-alpine docker镜像,因为它的大小比基于debian slim的普通镜像要大。 From 42e8f3256c713c21e1ab2f28213b0637c3b4b09f Mon Sep 17 00:00:00 2001 From: Ivan Dyachkov Date: Thu, 9 Feb 2023 16:12:03 +0100 Subject: [PATCH 0494/1802] chore: add changelog --- changes/v5.0.17/feat-9936.en.md | 1 + changes/v5.0.17/feat-9936.zh.md | 1 + 2 files changed, 2 insertions(+) create mode 100644 changes/v5.0.17/feat-9936.en.md create mode 100644 changes/v5.0.17/feat-9936.zh.md diff --git a/changes/v5.0.17/feat-9936.en.md b/changes/v5.0.17/feat-9936.en.md new file mode 100644 index 000000000..3010e6fed --- /dev/null +++ b/changes/v5.0.17/feat-9936.en.md @@ -0,0 +1 @@ +Disable disksup (part of os_mon) in releases by default. diff --git a/changes/v5.0.17/feat-9936.zh.md b/changes/v5.0.17/feat-9936.zh.md new file mode 100644 index 000000000..f459f9e52 --- /dev/null +++ b/changes/v5.0.17/feat-9936.zh.md @@ -0,0 +1 @@ +在发行版中默认禁用diskup(os_mon的一部分)。 From cf77dcf25eded3b3058b8192ce470f28edd11a38 Mon Sep 17 00:00:00 2001 From: Erik Timan Date: Thu, 2 Feb 2023 14:53:17 +0100 Subject: [PATCH 0495/1802] test(emqx_ee_connector): add basic tests for influxdb incl. SSL opts This adds a test suite for the emqx_ee_connector_influxdb. We add it so that SSL transport options are properly tested. --- lib-ee/emqx_ee_connector/docker-ct | 2 + .../src/emqx_ee_connector_influxdb.erl | 14 ++ .../test/emqx_ee_connector_influxdb_SUITE.erl | 196 ++++++++++++++++++ 3 files changed, 212 insertions(+) create mode 100644 lib-ee/emqx_ee_connector/docker-ct create mode 100644 lib-ee/emqx_ee_connector/test/emqx_ee_connector_influxdb_SUITE.erl diff --git a/lib-ee/emqx_ee_connector/docker-ct b/lib-ee/emqx_ee_connector/docker-ct new file mode 100644 index 000000000..ef579c036 --- /dev/null +++ b/lib-ee/emqx_ee_connector/docker-ct @@ -0,0 +1,2 @@ +toxiproxy +influxdb diff --git a/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl b/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl index d689f4bf3..988c19156 100644 --- a/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl +++ b/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl @@ -29,6 +29,7 @@ -export([reply_callback/2]). -export([ + roots/0, namespace/0, fields/1, desc/1 @@ -139,6 +140,19 @@ on_get_status(_InstId, #{client := Client}) -> %% schema namespace() -> connector_influxdb. +roots() -> + [ + {config, #{ + type => hoconsc:union( + [ + hoconsc:ref(?MODULE, influxdb_udp), + hoconsc:ref(?MODULE, influxdb_api_v1), + hoconsc:ref(?MODULE, influxdb_api_v2) + ] + ) + }} + ]. + fields(common) -> [ {server, server()}, diff --git a/lib-ee/emqx_ee_connector/test/emqx_ee_connector_influxdb_SUITE.erl b/lib-ee/emqx_ee_connector/test/emqx_ee_connector_influxdb_SUITE.erl new file mode 100644 index 000000000..01bb8a08e --- /dev/null +++ b/lib-ee/emqx_ee_connector/test/emqx_ee_connector_influxdb_SUITE.erl @@ -0,0 +1,196 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- + +-module(emqx_ee_connector_influxdb_SUITE). + +-compile(nowarn_export_all). +-compile(export_all). + +-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). + +all() -> + emqx_common_test_helpers:all(?MODULE). + +groups() -> + []. + +init_per_suite(Config) -> + InfluxDBTCPHost = os:getenv("INFLUXDB_APIV2_TCP_HOST", "toxiproxy"), + InfluxDBTCPPort = list_to_integer(os:getenv("INFLUXDB_APIV2_TCP_PORT", "8086")), + InfluxDBTLSHost = os:getenv("INFLUXDB_APIV2_TLS_HOST", "toxiproxy"), + InfluxDBTLSPort = list_to_integer(os:getenv("INFLUXDB_APIV2_TLS_PORT", "8087")), + case emqx_common_test_helpers:is_tcp_server_available(InfluxDBTCPHost, InfluxDBTCPPort) 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), + [ + {influxdb_tcp_host, InfluxDBTCPHost}, + {influxdb_tcp_port, InfluxDBTCPPort}, + {influxdb_tls_host, InfluxDBTLSHost}, + {influxdb_tls_port, InfluxDBTLSPort} + | Config + ]; + false -> + {skip, no_influxdb} + end. + +end_per_suite(_Config) -> + ok = emqx_common_test_helpers:stop_apps([emqx_conf]), + ok = emqx_connector_test_helpers:stop_apps([emqx_resource]), + _ = application:stop(emqx_connector). + +init_per_testcase(_, Config) -> + Config. + +end_per_testcase(_, _Config) -> + ok. + +% %%------------------------------------------------------------------------------ +% %% Testcases +% %%------------------------------------------------------------------------------ + +t_lifecycle(Config) -> + Host = ?config(influxdb_tcp_host, Config), + Port = ?config(influxdb_tcp_port, Config), + perform_lifecycle_check( + <<"emqx_ee_connector_influxdb_SUITE">>, + influxdb_config(Host, Port, false, "verify_none") + ). + +perform_lifecycle_check(PoolName, InitialConfig) -> + {ok, #{config := CheckedConfig}} = + emqx_resource:check_config(?INFLUXDB_RESOURCE_MOD, InitialConfig), + % We need to add a write_syntax to the config since the connector + % expects this + FullConfig = CheckedConfig#{write_syntax => influxdb_write_syntax()}, + {ok, #{ + state := #{client := #{pool := ReturnedPoolName}} = State, + status := InitialStatus + }} = emqx_resource:create_local( + PoolName, + ?CONNECTOR_RESOURCE_GROUP, + ?INFLUXDB_RESOURCE_MOD, + FullConfig, + #{} + ), + ?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(PoolName), + ?assertEqual({ok, connected}, emqx_resource:health_check(PoolName)), + % % Perform query as further check that the resource is working as expected + ?assertMatch(ok, emqx_resource:query(PoolName, test_query())), + ?assertEqual(ok, emqx_resource:stop(PoolName)), + % 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(PoolName), + ?assertEqual(stopped, StoppedStatus), + ?assertEqual({error, resource_is_stopped}, emqx_resource:health_check(PoolName)), + % Resource healthcheck shortcuts things by checking ets. Go deeper by checking pool itself. + ?assertEqual({error, not_found}, ecpool:stop_sup_pool(ReturnedPoolName)), + % Can call stop/1 again on an already stopped instance + ?assertEqual(ok, emqx_resource:stop(PoolName)), + % Make sure it can be restarted and the healthchecks and queries work properly + ?assertEqual(ok, emqx_resource:restart(PoolName)), + % async restart, need to wait resource + timer:sleep(500), + {ok, ?CONNECTOR_RESOURCE_GROUP, #{status := InitialStatus}} = + emqx_resource:get_instance(PoolName), + ?assertEqual({ok, connected}, emqx_resource:health_check(PoolName)), + ?assertMatch(ok, emqx_resource:query(PoolName, test_query())), + % Stop and remove the resource in one go. + ?assertEqual(ok, emqx_resource:remove_local(PoolName)), + ?assertEqual({error, not_found}, ecpool:stop_sup_pool(ReturnedPoolName)), + % 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(PoolName)). + +t_tls_opts(Config) -> + PoolName = <<"emqx_ee_connector_influxdb_SUITE">>, + Host = ?config(influxdb_tls_host, Config), + Port = ?config(influxdb_tls_port, Config), + VerifyNoneStatus = perform_tls_opts_check( + PoolName, influxdb_config(Host, Port, true, "verify_none") + ), + ?assertEqual(connected, VerifyNoneStatus), + VerifyPeerStatus = perform_tls_opts_check( + PoolName, influxdb_config(Host, Port, true, "verify_peer") + ), + ?assertEqual(disconnected, VerifyPeerStatus), + ok. + +perform_tls_opts_check(PoolName, InitialConfig) -> + {ok, #{config := CheckedConfig}} = + emqx_resource:check_config(?INFLUXDB_RESOURCE_MOD, InitialConfig), + % We need to add a write_syntax to the config since the connector + % expects this + FullConfig = CheckedConfig#{write_syntax => influxdb_write_syntax()}, + {ok, #{ + config := #{ssl := #{enable := SslEnabled}}, + status := Status + }} = emqx_resource:create_local( + PoolName, + ?CONNECTOR_RESOURCE_GROUP, + ?INFLUXDB_RESOURCE_MOD, + FullConfig, + #{} + ), + ?assert(SslEnabled), + % Stop and remove the resource in one go. + ?assertEqual(ok, emqx_resource:remove_local(PoolName)), + Status. + +% %%------------------------------------------------------------------------------ +% %% Helpers +% %%------------------------------------------------------------------------------ + +influxdb_config(Host, Port, SslEnabled, Verify) -> + RawConfig = list_to_binary( + io_lib:format( + "" + "\n" + " bucket = mqtt\n" + " org = emqx\n" + " token = abcdefg\n" + " server = \"~s:~b\"\n" + " ssl {\n" + " enable = ~s\n" + " verify = ~s\n" + " }\n" + " " + "", + [Host, Port, SslEnabled, Verify] + ) + ), + + {ok, ResourceConfig} = hocon:binary(RawConfig), + #{<<"config">> => ResourceConfig}. + +influxdb_write_syntax() -> + [ + #{ + measurement => "${topic}", + tags => [{"clientid", "${clientid}"}], + fields => [{"payload", "${payload}"}], + timestamp => undefined + } + ]. + +test_query() -> + {send_message, #{ + <<"clientid">> => <<"something">>, + <<"payload">> => #{bool => true}, + <<"topic">> => <<"connector_test">> + }}. From cfd0e9ebdd9dbf3fef3d9b0c4d8ced08008ea7ba Mon Sep 17 00:00:00 2001 From: Erik Timan Date: Thu, 9 Feb 2023 17:08:36 +0100 Subject: [PATCH 0496/1802] refactor(emqx_ee_connector): use emqx_tls_lib for influx ssl opts We used to simply pass on SSL options given to the influxdb EE connector, but we now pass them to emqx_tls_lib instead. This ensures a proper handling of SSL options and also allow us to use meck to inject custom options in tests. --- lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl b/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl index 988c19156..f056f4af2 100644 --- a/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl +++ b/lib-ee/emqx_ee_connector/src/emqx_ee_connector_influxdb.erl @@ -341,7 +341,7 @@ ssl_config(SSL = #{enable := true}) -> [ {https_enabled, true}, {transport, ssl}, - {transport_opts, maps:to_list(maps:remove(enable, SSL))} + {transport_opts, emqx_tls_lib:to_client_opts(SSL)} ]. username(#{username := Username}) -> From bc6c653c9fd83dbd446de42b9465fc8289820cc0 Mon Sep 17 00:00:00 2001 From: Erik Timan Date: Thu, 9 Feb 2023 17:12:01 +0100 Subject: [PATCH 0497/1802] test(emqx_ee_connector): rework and improve influxdb tests --- .../test/emqx_ee_connector_influxdb_SUITE.erl | 88 +++++++++++++------ 1 file changed, 62 insertions(+), 26 deletions(-) diff --git a/lib-ee/emqx_ee_connector/test/emqx_ee_connector_influxdb_SUITE.erl b/lib-ee/emqx_ee_connector/test/emqx_ee_connector_influxdb_SUITE.erl index 01bb8a08e..a2e6c7c8f 100644 --- a/lib-ee/emqx_ee_connector/test/emqx_ee_connector_influxdb_SUITE.erl +++ b/lib-ee/emqx_ee_connector/test/emqx_ee_connector_influxdb_SUITE.erl @@ -117,40 +117,64 @@ perform_lifecycle_check(PoolName, InitialConfig) -> % 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(PoolName)). -t_tls_opts(Config) -> +t_tls_verify_none(Config) -> PoolName = <<"emqx_ee_connector_influxdb_SUITE">>, Host = ?config(influxdb_tls_host, Config), Port = ?config(influxdb_tls_port, Config), - VerifyNoneStatus = perform_tls_opts_check( - PoolName, influxdb_config(Host, Port, true, "verify_none") - ), - ?assertEqual(connected, VerifyNoneStatus), - VerifyPeerStatus = perform_tls_opts_check( - PoolName, influxdb_config(Host, Port, true, "verify_peer") - ), - ?assertEqual(disconnected, VerifyPeerStatus), + InitialConfig = influxdb_config(Host, Port, true, "verify_none"), + ValidStatus = perform_tls_opts_check(PoolName, InitialConfig, valid), + ?assertEqual(connected, ValidStatus), + InvalidStatus = perform_tls_opts_check(PoolName, InitialConfig, fail), + ?assertEqual(disconnected, InvalidStatus), ok. -perform_tls_opts_check(PoolName, InitialConfig) -> +t_tls_verify_peer(Config) -> + PoolName = <<"emqx_ee_connector_influxdb_SUITE">>, + Host = ?config(influxdb_tls_host, Config), + Port = ?config(influxdb_tls_port, Config), + InitialConfig = influxdb_config(Host, Port, true, "verify_peer"), + ValidStatus = perform_tls_opts_check(PoolName, InitialConfig, valid), + ?assertEqual(connected, ValidStatus), + InvalidStatus = perform_tls_opts_check(PoolName, InitialConfig, fail), + ?assertEqual(disconnected, InvalidStatus), + ok. + +perform_tls_opts_check(PoolName, InitialConfig, VerifyReturn) -> {ok, #{config := CheckedConfig}} = emqx_resource:check_config(?INFLUXDB_RESOURCE_MOD, InitialConfig), - % We need to add a write_syntax to the config since the connector - % expects this - FullConfig = CheckedConfig#{write_syntax => influxdb_write_syntax()}, - {ok, #{ - config := #{ssl := #{enable := SslEnabled}}, - status := Status - }} = emqx_resource:create_local( - PoolName, - ?CONNECTOR_RESOURCE_GROUP, - ?INFLUXDB_RESOURCE_MOD, - FullConfig, - #{} + % Meck handling of TLS opt handling so that we can inject custom + % verification returns + meck:new(emqx_tls_lib, [passthrough, no_link]), + meck:expect( + emqx_tls_lib, + to_client_opts, + fun(Opts) -> + Verify = {verify_fun, {custom_verify(), {return, VerifyReturn}}}, + [Verify | meck:passthrough([Opts])] + end ), - ?assert(SslEnabled), - % Stop and remove the resource in one go. - ?assertEqual(ok, emqx_resource:remove_local(PoolName)), - Status. + try + % We need to add a write_syntax to the config since the connector + % expects this + FullConfig = CheckedConfig#{write_syntax => influxdb_write_syntax()}, + {ok, #{ + config := #{ssl := #{enable := SslEnabled}}, + status := Status + }} = emqx_resource:create_local( + PoolName, + ?CONNECTOR_RESOURCE_GROUP, + ?INFLUXDB_RESOURCE_MOD, + FullConfig, + #{} + ), + ?assert(SslEnabled), + ?assert(meck:validate(emqx_tls_lib)), + % Stop and remove the resource in one go. + ?assertEqual(ok, emqx_resource:remove_local(PoolName)), + Status + after + meck:unload(emqx_tls_lib) + end. % %%------------------------------------------------------------------------------ % %% Helpers @@ -178,6 +202,18 @@ influxdb_config(Host, Port, SslEnabled, Verify) -> {ok, ResourceConfig} = hocon:binary(RawConfig), #{<<"config">> => ResourceConfig}. +custom_verify() -> + fun + (_, {bad_cert, unknown_ca} = Event, {return, Return} = UserState) -> + ct:pal("Call to custom verify fun. Event: ~p UserState: ~p", [Event, UserState]), + {Return, UserState}; + (_, Event, UserState) -> + ct:pal("Unexpected call to custom verify fun. Event: ~p UserState: ~p", [ + Event, UserState + ]), + {fail, unexpected_call_to_verify_fun} + end. + influxdb_write_syntax() -> [ #{ From 4eb4430fe22e7f024ae8fadc5047993dab856271 Mon Sep 17 00:00:00 2001 From: Erik Timan Date: Thu, 9 Feb 2023 17:22:19 +0100 Subject: [PATCH 0498/1802] chore: bump VSN --- lib-ee/emqx_ee_connector/src/emqx_ee_connector.app.src | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib-ee/emqx_ee_connector/src/emqx_ee_connector.app.src b/lib-ee/emqx_ee_connector/src/emqx_ee_connector.app.src index 56d128601..1d9927d3f 100644 --- a/lib-ee/emqx_ee_connector/src/emqx_ee_connector.app.src +++ b/lib-ee/emqx_ee_connector/src/emqx_ee_connector.app.src @@ -1,6 +1,6 @@ {application, emqx_ee_connector, [ {description, "EMQX Enterprise connectors"}, - {vsn, "0.1.4"}, + {vsn, "0.1.5"}, {registered, []}, {applications, [ kernel, From 5328955ff442cd8fa74271b6d4b0f2dfcf4961c2 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Thu, 9 Feb 2023 17:20:51 +0100 Subject: [PATCH 0499/1802] fix(config): add back reconnect_interval field as deprecated --- apps/emqx_bridge/test/emqx_bridge_compatible_config_tests.erl | 2 ++ apps/emqx_connector/src/emqx_connector.app.src | 2 +- apps/emqx_connector/src/mqtt/emqx_connector_mqtt_schema.erl | 1 + changes/v5.0.17/fix-9946.en.md | 3 +++ changes/v5.0.17/fix-9946.zh.md | 2 ++ 5 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 changes/v5.0.17/fix-9946.en.md create mode 100644 changes/v5.0.17/fix-9946.zh.md diff --git a/apps/emqx_bridge/test/emqx_bridge_compatible_config_tests.erl b/apps/emqx_bridge/test/emqx_bridge_compatible_config_tests.erl index 36dd6324a..5e0b4912f 100644 --- a/apps/emqx_bridge/test/emqx_bridge_compatible_config_tests.erl +++ b/apps/emqx_bridge/test/emqx_bridge_compatible_config_tests.erl @@ -177,6 +177,7 @@ bridges { keepalive = \"60s\" mode = cluster_shareload proto_ver = \"v4\" + reconnect_interval = \"15s\" server = \"localhost:1883\" ssl {enable = false, verify = \"verify_peer\"} } @@ -194,6 +195,7 @@ bridges { keepalive = \"60s\" mode = \"cluster_shareload\" proto_ver = \"v4\" + reconnect_interval = \"15s\" server = \"localhost:1883\" ssl {enable = false, verify = \"verify_peer\"} } diff --git a/apps/emqx_connector/src/emqx_connector.app.src b/apps/emqx_connector/src/emqx_connector.app.src index 2e955a5e9..aedc17c33 100644 --- a/apps/emqx_connector/src/emqx_connector.app.src +++ b/apps/emqx_connector/src/emqx_connector.app.src @@ -1,7 +1,7 @@ %% -*- mode: erlang -*- {application, emqx_connector, [ {description, "EMQX Data Integration Connectors"}, - {vsn, "0.1.13"}, + {vsn, "0.1.14"}, {registered, []}, {mod, {emqx_connector_app, []}}, {applications, [ diff --git a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_schema.erl b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_schema.erl index 6fabc95e8..f2163d952 100644 --- a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_schema.erl +++ b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_schema.erl @@ -72,6 +72,7 @@ fields("server_configs") -> )}, {server, emqx_schema:servers_sc(#{desc => ?DESC("server")}, ?MQTT_HOST_OPTS)}, {clientid_prefix, mk(binary(), #{required => false, desc => ?DESC("clientid_prefix")})}, + {reconnect_interval, mk(string(), #{deprecated => {since, "v5.0.16"}})}, {proto_ver, mk( hoconsc:enum([v3, v4, v5]), diff --git a/changes/v5.0.17/fix-9946.en.md b/changes/v5.0.17/fix-9946.en.md new file mode 100644 index 000000000..ba6f768fa --- /dev/null +++ b/changes/v5.0.17/fix-9946.en.md @@ -0,0 +1,3 @@ +Add back `reconnect_interval` as deprecated field for MQTT bridge. +The field was removed from v5.0.16/e5.0.0 by mistake, caused new version unable to start on old config. +Now it's added back as deprecated (config value is ignored if provided). diff --git a/changes/v5.0.17/fix-9946.zh.md b/changes/v5.0.17/fix-9946.zh.md new file mode 100644 index 000000000..be071e470 --- /dev/null +++ b/changes/v5.0.17/fix-9946.zh.md @@ -0,0 +1,2 @@ +MQTT 桥接配置参数 `reconnect_interval` 在 v5.0.16/e5.0.0 中被错误删除, +导致旧版本配置在升级后无法使用。此修复把配置加回,但是值会忽略。 From 8bd7c0eb3f64ad7bf5f89613c17f6b61aebbeb96 Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Wed, 8 Feb 2023 21:02:52 +0300 Subject: [PATCH 0500/1802] feat(mqtt-bridge): report recoverable errors of async queries This should help to avoid delivery failures of messages which could be safely retried, in the event of intermittent connectivity loss for example. It should now be safe since 73d5592b. --- .../src/emqx_connector_mqtt.erl | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/apps/emqx_connector/src/emqx_connector_mqtt.erl b/apps/emqx_connector/src/emqx_connector_mqtt.erl index cffd138b5..3f067dc06 100644 --- a/apps/emqx_connector/src/emqx_connector_mqtt.erl +++ b/apps/emqx_connector/src/emqx_connector_mqtt.erl @@ -45,6 +45,8 @@ on_get_status/2 ]). +-export([on_async_result/2]). + -behaviour(hocon_schema). -import(hoconsc, [mk/2]). @@ -194,8 +196,9 @@ on_query(_InstId, {send_message, Msg}, #{name := InstanceId}) -> classify_error(Reason) end. -on_query_async(_InstId, {send_message, Msg}, Callback, #{name := InstanceId}) -> +on_query_async(_InstId, {send_message, Msg}, CallbackIn, #{name := InstanceId}) -> ?TRACE("QUERY", "async_send_msg_to_remote_node", #{message => Msg, connector => InstanceId}), + Callback = {fun on_async_result/2, [CallbackIn]}, case emqx_connector_mqtt_worker:send_to_remote_async(InstanceId, Msg, Callback) of ok -> ok; @@ -205,6 +208,20 @@ on_query_async(_InstId, {send_message, Msg}, Callback, #{name := InstanceId}) -> classify_error(Reason) end. +on_async_result(Callback, ok) -> + apply_callback_function(Callback, ok); +on_async_result(Callback, {ok, _} = Ok) -> + apply_callback_function(Callback, Ok); +on_async_result(Callback, {error, Reason}) -> + apply_callback_function(Callback, classify_error(Reason)). + +apply_callback_function(F, Result) when is_function(F) -> + erlang:apply(F, [Result]); +apply_callback_function({F, A}, Result) when is_function(F), is_list(A) -> + erlang:apply(F, A ++ [Result]); +apply_callback_function({M, F, A}, Result) when is_atom(M), is_atom(F), is_list(A) -> + erlang:apply(M, F, A ++ [Result]). + on_get_status(_InstId, #{name := InstanceId}) -> emqx_connector_mqtt_worker:status(InstanceId). @@ -214,6 +231,8 @@ classify_error({disconnected, _RC, _} = Reason) -> {error, {recoverable_error, Reason}}; classify_error({shutdown, _} = Reason) -> {error, {recoverable_error, Reason}}; +classify_error(shutdown = Reason) -> + {error, {recoverable_error, Reason}}; classify_error(Reason) -> {error, {unrecoverable_error, Reason}}. From 53488b4377a99469e5e561ca9687faa9df6be91b Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Fri, 10 Feb 2023 17:51:36 +0800 Subject: [PATCH 0501/1802] fix: add push_gateway_server's validator --- apps/emqx_prometheus/src/emqx_prometheus_schema.erl | 10 +++++++++- .../emqx_prometheus/test/emqx_prometheus_api_SUITE.erl | 6 ++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/apps/emqx_prometheus/src/emqx_prometheus_schema.erl b/apps/emqx_prometheus/src/emqx_prometheus_schema.erl index c13d198a2..fcda5dea0 100644 --- a/apps/emqx_prometheus/src/emqx_prometheus_schema.erl +++ b/apps/emqx_prometheus/src/emqx_prometheus_schema.erl @@ -26,7 +26,8 @@ fields/1, desc/1, translation/1, - convert_headers/1 + convert_headers/1, + validate_push_gateway_server/1 ]). namespace() -> "prometheus". @@ -41,6 +42,7 @@ fields("prometheus") -> #{ default => "http://127.0.0.1:9091", required => true, + validator => fun ?MODULE:validate_push_gateway_server/1, desc => ?DESC(push_gateway_server) } )}, @@ -158,6 +160,12 @@ convert_headers(Headers) when is_map(Headers) -> convert_headers(Headers) when is_list(Headers) -> Headers. +validate_push_gateway_server(Url) -> + case uri_string:parse(Url) of + #{scheme := S} when S =:= "https" orelse S =:= "http" -> ok; + _ -> {error, "Invalid url"} + end. + %% for CI test, CI don't load the whole emqx_conf_schema. translation(Name) -> emqx_conf_schema:translation(Name). diff --git a/apps/emqx_prometheus/test/emqx_prometheus_api_SUITE.erl b/apps/emqx_prometheus/test/emqx_prometheus_api_SUITE.erl index 0968100b8..c4867d9fd 100644 --- a/apps/emqx_prometheus/test/emqx_prometheus_api_SUITE.erl +++ b/apps/emqx_prometheus/test/emqx_prometheus_api_SUITE.erl @@ -92,6 +92,12 @@ t_prometheus_api(_) -> NewConf1 = Conf#{<<"enable">> => (not Enable)}, {ok, _Response3} = emqx_mgmt_api_test_util:request_api(put, Path, "", Auth, NewConf1), ?assertEqual((not Enable), undefined =/= erlang:whereis(emqx_prometheus)), + + ConfWithoutScheme = Conf#{<<"push_gateway_server">> => "127.0.0.1:8081"}, + ?assertMatch( + {error, {"HTTP/1.1", 400, _}}, + emqx_mgmt_api_test_util:request_api(put, Path, "", Auth, ConfWithoutScheme) + ), ok. t_stats_api(_) -> From e196d6f42aec01564dc4db2fe6d5d44a672ac74f Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Fri, 10 Feb 2023 12:00:20 +0100 Subject: [PATCH 0502/1802] fix(emqx_bridge): unwrap rpc multicall result --- apps/emqx_bridge/src/emqx_bridge_api.erl | 11 ++++++-- .../test/emqx_bridge_api_SUITE.erl | 28 +++++++++++++++++-- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/apps/emqx_bridge/src/emqx_bridge_api.erl b/apps/emqx_bridge/src/emqx_bridge_api.erl index 04760f021..a6c2cee27 100644 --- a/apps/emqx_bridge/src/emqx_bridge_api.erl +++ b/apps/emqx_bridge/src/emqx_bridge_api.erl @@ -856,7 +856,7 @@ unpack_bridge_conf(Type, PackedConf) -> is_ok(ok) -> ok; -is_ok({ok, _} = OkResult) -> +is_ok(OkResult = {ok, _}) -> OkResult; is_ok(Error = {error, _}) -> Error; @@ -933,7 +933,9 @@ maybe_try_restart(_, _, _) -> {501}. do_bpapi_call(all, Call, Args) -> - do_bpapi_call_vsn(emqx_bpapi:supported_version(emqx_bridge), Call, Args); + maybe_unwrap( + do_bpapi_call_vsn(emqx_bpapi:supported_version(emqx_bridge), Call, Args) + ); do_bpapi_call(Node, Call, Args) -> do_bpapi_call_vsn(emqx_bpapi:supported_version(Node, emqx_bridge), Call, Args). @@ -945,6 +947,11 @@ do_bpapi_call_vsn(SupportedVersion, Call, Args) -> {error, not_implemented} end. +maybe_unwrap({error, not_implemented}) -> + {error, not_implemented}; +maybe_unwrap(RpcMulticallResult) -> + emqx_rpc:unwrap_erpc(RpcMulticallResult). + supported_versions(start_bridge_to_node) -> [2]; supported_versions(start_bridges_to_all_nodes) -> [2]; supported_versions(_Call) -> [1, 2]. diff --git a/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl b/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl index d6e8708ff..f81ecb76f 100644 --- a/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl +++ b/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl @@ -38,12 +38,16 @@ <<"type">> => TYPE, <<"name">> => NAME }). --define(MQTT_BRIDGE(SERVER), ?BRIDGE(<<"mqtt_egress_test_bridge">>, <<"mqtt">>)#{ + +-define(BRIDGE_TYPE_MQTT, <<"mqtt">>). +-define(MQTT_BRIDGE(SERVER, NAME), ?BRIDGE(NAME, ?BRIDGE_TYPE_MQTT)#{ <<"server">> => SERVER, <<"username">> => <<"user1">>, <<"password">> => <<"">>, <<"proto_ver">> => <<"v5">> }). +-define(MQTT_BRIDGE(SERVER), ?MQTT_BRIDGE(SERVER, <<"mqtt_egress_test_bridge">>)). + -define(HTTP_BRIDGE(URL, TYPE, NAME), ?BRIDGE(NAME, TYPE)#{ <<"url">> => URL, <<"local_topic">> => <<"emqx_webhook/#">>, @@ -536,7 +540,27 @@ do_start_stop_bridges(Type, Config) -> ?assertMatch(#{<<"status">> := <<"connected">>}, jsx:decode(Bridge4)), %% delete the bridge {ok, 204, <<>>} = request(delete, uri(["bridges", BridgeID]), []), - {ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []). + {ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []), + + %% Create broken bridge + BadServer = <<"nohost">>, + BadName = <<"bad_", (atom_to_binary(Type))/binary>>, + {ok, 201, BadBridge1} = request( + post, + uri(["bridges"]), + ?MQTT_BRIDGE(BadServer, BadName) + ), + #{ + <<"type">> := ?BRIDGE_TYPE_MQTT, + <<"name">> := BadName, + <<"enable">> := true, + <<"server">> := BadServer, + <<"status">> := <<"disconnected">>, + <<"node_status">> := [_ | _] + } = jsx:decode(BadBridge1), + BadBridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE_MQTT, BadName), + {ok, 500, _} = request(post, operation_path(Type, start, BadBridgeID), <<"">>), + ok. t_enable_disable_bridges(Config) -> %% assert we there's no bridges at first From 97f5f318a713f09f3d55f532f4642456e0065b3c Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Fri, 10 Feb 2023 12:08:07 +0100 Subject: [PATCH 0503/1802] chore: add changelog --- changes/v5.0.17/fix-9951.en.md | 1 + changes/v5.0.17/fix-9951.zh.md | 1 + 2 files changed, 2 insertions(+) create mode 100644 changes/v5.0.17/fix-9951.en.md create mode 100644 changes/v5.0.17/fix-9951.zh.md diff --git a/changes/v5.0.17/fix-9951.en.md b/changes/v5.0.17/fix-9951.en.md new file mode 100644 index 000000000..e2439cf01 --- /dev/null +++ b/changes/v5.0.17/fix-9951.en.md @@ -0,0 +1 @@ +Propagate errors from operations (`start|stop|restart`) on bridges API if called for all nodes. diff --git a/changes/v5.0.17/fix-9951.zh.md b/changes/v5.0.17/fix-9951.zh.md new file mode 100644 index 000000000..aba8fd078 --- /dev/null +++ b/changes/v5.0.17/fix-9951.zh.md @@ -0,0 +1 @@ +如果对所有节点调用,则传播桥梁 API 上的操作 (`start|stop|restart`) 的错误。 From 1b195413c32f7769be9919802f0135c062072e1a Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Fri, 10 Feb 2023 11:47:49 +0300 Subject: [PATCH 0504/1802] chore: add changelog entry --- changes/v5.0.17/fix-9938.en.md | 1 + changes/v5.0.17/fix-9938.zh.md | 1 + 2 files changed, 2 insertions(+) create mode 100644 changes/v5.0.17/fix-9938.en.md create mode 100644 changes/v5.0.17/fix-9938.zh.md diff --git a/changes/v5.0.17/fix-9938.en.md b/changes/v5.0.17/fix-9938.en.md new file mode 100644 index 000000000..54de76fd7 --- /dev/null +++ b/changes/v5.0.17/fix-9938.en.md @@ -0,0 +1 @@ +Report some egress MQTT bridge errors as recoverable, and thus retryable. diff --git a/changes/v5.0.17/fix-9938.zh.md b/changes/v5.0.17/fix-9938.zh.md new file mode 100644 index 000000000..f8bd41cf0 --- /dev/null +++ b/changes/v5.0.17/fix-9938.zh.md @@ -0,0 +1 @@ +将一些出口 MQTT 网桥错误报告为可恢复,因此可重试。 From 150f9dab1d909d6981bd77cd400fee1db7127d47 Mon Sep 17 00:00:00 2001 From: Stefan Strigler Date: Fri, 10 Feb 2023 13:42:14 +0100 Subject: [PATCH 0505/1802] chore: fix zn translation Co-authored-by: William Yang --- changes/v5.0.17/fix-9951.zh.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changes/v5.0.17/fix-9951.zh.md b/changes/v5.0.17/fix-9951.zh.md index aba8fd078..aa540740c 100644 --- a/changes/v5.0.17/fix-9951.zh.md +++ b/changes/v5.0.17/fix-9951.zh.md @@ -1 +1 @@ -如果对所有节点调用,则传播桥梁 API 上的操作 (`start|stop|restart`) 的错误。 +返回桥接 API 操作(`start|stop|restart`) 的多节点远程调用的错误信息。 From 7002fe2ef415975fb75adf7ceda8fc0e396ea6c9 Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Fri, 10 Feb 2023 16:15:46 +0300 Subject: [PATCH 0506/1802] fix(mqtt-bridge): disallow QoS 2 on ingress bridges --- .../test/emqx_bridge_mqtt_SUITE.erl | 32 +++++++++++++- .../src/mqtt/emqx_connector_mqtt_schema.erl | 6 +-- .../src/mqtt/emqx_connector_mqtt_worker.erl | 42 +++++++++++++++---- 3 files changed, 68 insertions(+), 12 deletions(-) diff --git a/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl b/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl index 49b333216..40a8c0bf2 100644 --- a/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl +++ b/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl @@ -52,7 +52,7 @@ -define(INGRESS_CONF, #{ <<"remote">> => #{ <<"topic">> => <>, - <<"qos">> => 2 + <<"qos">> => 1 }, <<"local">> => #{ <<"topic">> => <>, @@ -77,7 +77,7 @@ -define(INGRESS_CONF_NO_PAYLOAD_TEMPLATE, #{ <<"remote">> => #{ <<"topic">> => <>, - <<"qos">> => 2 + <<"qos">> => 1 }, <<"local">> => #{ <<"topic">> => <>, @@ -265,6 +265,34 @@ t_mqtt_conn_bridge_ignores_clean_start(_) -> ok. +t_mqtt_conn_bridge_ingress_downgrades_qos_2(_) -> + BridgeName = atom_to_binary(?FUNCTION_NAME), + BridgeID = create_bridge( + ?SERVER_CONF(<<"user1">>)#{ + <<"type">> => ?TYPE_MQTT, + <<"name">> => BridgeName, + <<"ingress">> => emqx_map_lib:deep_merge( + ?INGRESS_CONF, + #{<<"remote">> => #{<<"qos">> => 2}} + ) + } + ), + + RemoteTopic = <>, + LocalTopic = <>, + Payload = <<"whatqos">>, + emqx:subscribe(LocalTopic), + emqx:publish(emqx_message:make(undefined, _QoS = 2, RemoteTopic, Payload)), + + %% we should receive a message on the local broker, with specified topic + Msg = assert_mqtt_msg_received(LocalTopic, Payload), + ?assertMatch(#message{qos = 1}, Msg), + + %% delete the bridge + {ok, 204, <<>>} = request(delete, uri(["bridges", BridgeID]), []), + + ok. + t_mqtt_conn_bridge_ingress_no_payload_template(_) -> User1 = <<"user1">>, BridgeIDIngress = create_bridge( diff --git a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_schema.erl b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_schema.erl index f2163d952..365b082f2 100644 --- a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_schema.erl +++ b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_schema.erl @@ -18,6 +18,7 @@ -include_lib("typerefl/include/types.hrl"). -include_lib("hocon/include/hoconsc.hrl"). +-include_lib("emqx/include/logger.hrl"). -behaviour(hocon_schema). @@ -143,8 +144,7 @@ fields("ingress") -> mk( ref(?MODULE, "ingress_local"), #{ - desc => ?DESC(emqx_connector_mqtt_schema, "ingress_local"), - is_required => false + desc => ?DESC(emqx_connector_mqtt_schema, "ingress_local") } )} ]; @@ -161,7 +161,7 @@ fields("ingress_remote") -> )}, {qos, mk( - qos(), + emqx_schema:qos(), #{ default => 1, desc => ?DESC("ingress_remote_qos") diff --git a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl index 6da63f99a..7240151a5 100644 --- a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl +++ b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl @@ -114,7 +114,7 @@ start_link(Name, BridgeOpts) -> name => Name, options => BridgeOpts }), - Conf = init_config(BridgeOpts), + Conf = init_config(Name, BridgeOpts), Options = mk_client_options(Conf, BridgeOpts), case emqtt:start_link(Options) of {ok, Pid} -> @@ -129,13 +129,13 @@ start_link(Name, BridgeOpts) -> Error end. -init_config(Opts) -> +init_config(Name, Opts) -> Mountpoint = maps:get(forward_mountpoint, Opts, undefined), Subscriptions = maps:get(subscriptions, Opts, undefined), Forwards = maps:get(forwards, Opts, undefined), #{ mountpoint => format_mountpoint(Mountpoint), - subscriptions => pre_process_subscriptions(Subscriptions), + subscriptions => pre_process_subscriptions(Subscriptions, Name, Opts), forwards => pre_process_forwards(Forwards) }. @@ -282,11 +282,18 @@ format_mountpoint(undefined) -> format_mountpoint(Prefix) -> binary:replace(iolist_to_binary(Prefix), <<"${node}">>, atom_to_binary(node(), utf8)). -pre_process_subscriptions(undefined) -> +pre_process_subscriptions(undefined, _, _) -> undefined; -pre_process_subscriptions(#{local := LC} = Conf) when is_map(Conf) -> - Conf#{local => pre_process_in_out_common(LC)}; -pre_process_subscriptions(Conf) when is_map(Conf) -> +pre_process_subscriptions( + #{remote := RC, local := LC} = Conf, + BridgeName, + BridgeOpts +) when is_map(Conf) -> + Conf#{ + remote => pre_process_in_remote(RC, BridgeName, BridgeOpts), + local => pre_process_in_out_common(LC) + }; +pre_process_subscriptions(Conf, _, _) when is_map(Conf) -> %% have no 'local' field in the config undefined. @@ -314,6 +321,27 @@ pre_process_conf(Key, Conf) -> Conf#{Key => Val} end. +pre_process_in_remote(#{qos := QoSIn} = Conf, BridgeName, BridgeOpts) -> + QoS = downgrade_ingress_qos(QoSIn), + case QoS of + QoSIn -> + ok; + _ -> + ?SLOG(warning, #{ + msg => "downgraded_unsupported_ingress_qos", + qos_configured => QoSIn, + qos_used => QoS, + name => BridgeName, + options => BridgeOpts + }) + end, + Conf#{qos => QoS}. + +downgrade_ingress_qos(2) -> + 1; +downgrade_ingress_qos(QoS) -> + QoS. + get_pid(Name) -> case gproc:where(?NAME(Name)) of Pid when is_pid(Pid) -> From cbb2885499d40903438b67154c83aa0c112fc5db Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Fri, 10 Feb 2023 16:17:55 +0300 Subject: [PATCH 0507/1802] fix(mqtt-bridge): allow to configure `clean_start` for ingresses --- .../test/emqx_bridge_mqtt_SUITE.erl | 16 ++++++++-------- .../i18n/emqx_connector_mqtt_schema.conf | 4 ++-- .../emqx_connector/src/emqx_connector_mqtt.erl | 6 ++---- .../src/mqtt/emqx_connector_mqtt_schema.erl | 4 +--- .../src/mqtt/emqx_connector_mqtt_worker.erl | 18 ++++++++++++++++-- 5 files changed, 29 insertions(+), 19 deletions(-) diff --git a/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl b/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl index 40a8c0bf2..1bb477dad 100644 --- a/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl +++ b/apps/emqx_bridge/test/emqx_bridge_mqtt_SUITE.erl @@ -242,26 +242,26 @@ t_mqtt_conn_bridge_ingress(_) -> ok. -t_mqtt_conn_bridge_ignores_clean_start(_) -> +t_mqtt_egress_bridge_ignores_clean_start(_) -> BridgeName = atom_to_binary(?FUNCTION_NAME), BridgeID = create_bridge( ?SERVER_CONF(<<"user1">>)#{ <<"type">> => ?TYPE_MQTT, <<"name">> => BridgeName, - <<"ingress">> => ?INGRESS_CONF, + <<"egress">> => ?EGRESS_CONF, <<"clean_start">> => false } ), - {ok, 200, BridgeJSON} = request(get, uri(["bridges", BridgeID]), []), - Bridge = jsx:decode(BridgeJSON), - - %% verify that there's no `clean_start` in response - ?assertEqual(#{}, maps:with([<<"clean_start">>], Bridge)), + {ok, _, #{state := #{name := WorkerName}}} = + emqx_resource:get_instance(emqx_bridge_resource:resource_id(BridgeID)), + ?assertMatch( + #{clean_start := true}, + maps:from_list(emqx_connector_mqtt_worker:info(WorkerName)) + ), %% delete the bridge {ok, 204, <<>>} = request(delete, uri(["bridges", BridgeID]), []), - {ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []), ok. diff --git a/apps/emqx_connector/i18n/emqx_connector_mqtt_schema.conf b/apps/emqx_connector/i18n/emqx_connector_mqtt_schema.conf index b0d1e0821..b075681f3 100644 --- a/apps/emqx_connector/i18n/emqx_connector_mqtt_schema.conf +++ b/apps/emqx_connector/i18n/emqx_connector_mqtt_schema.conf @@ -159,8 +159,8 @@ broker MUST support this feature.""" clean_start { desc { - en: "The clean-start or the clean-session of the MQTT protocol" - zh: "MQTT 清除会话" + en: "Whether or not to start a clean session when reconnecting a remote broker for ingress bridge" + zh: "与 ingress MQTT 桥的远程服务器重连时是否清除老的 MQTT 会话。" } label: { en: "Clean Session" diff --git a/apps/emqx_connector/src/emqx_connector_mqtt.erl b/apps/emqx_connector/src/emqx_connector_mqtt.erl index cffd138b5..5c62e7086 100644 --- a/apps/emqx_connector/src/emqx_connector_mqtt.erl +++ b/apps/emqx_connector/src/emqx_connector_mqtt.erl @@ -251,6 +251,7 @@ basic_config( server := Server, proto_ver := ProtoVer, bridge_mode := BridgeMode, + clean_start := CleanStart, keepalive := KeepAlive, retry_interval := RetryIntv, max_inflight := MaxInflight, @@ -270,11 +271,8 @@ basic_config( %% non-standard mqtt connection packets will be filtered out by LB. %% So let's disable bridge_mode. bridge_mode => BridgeMode, - %% NOTE - %% We are ignoring the user configuration here because there's currently no reliable way - %% to ensure proper session recovery according to the MQTT spec. - clean_start => true, keepalive => ms_to_s(KeepAlive), + clean_start => CleanStart, retry_interval => RetryIntv, max_inflight => MaxInflight, ssl => EnableSsl, diff --git a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_schema.erl b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_schema.erl index 365b082f2..073b75ae8 100644 --- a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_schema.erl +++ b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_schema.erl @@ -112,9 +112,7 @@ fields("server_configs") -> boolean(), #{ default => true, - desc => ?DESC("clean_start"), - hidden => true, - deprecated => {since, "v5.0.16"} + desc => ?DESC("clean_start") } )}, {keepalive, mk_duration("MQTT Keepalive.", #{default => "300s"})}, diff --git a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl index 7240151a5..8e41a9f0f 100644 --- a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl +++ b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl @@ -75,6 +75,7 @@ connect/1, status/1, ping/1, + info/1, send_to_remote/2, send_to_remote_async/3 ]). @@ -145,6 +146,16 @@ mk_client_options(Conf, BridgeOpts) -> Mountpoint = maps:get(receive_mountpoint, BridgeOpts, undefined), Subscriptions = maps:get(subscriptions, Conf), Vars = emqx_connector_mqtt_msg:make_pub_vars(Mountpoint, Subscriptions), + CleanStart = + case Subscriptions of + #{remote := _} -> + maps:get(clean_start, BridgeOpts); + undefined -> + %% NOTE + %% We are ignoring the user configuration here because there's currently no reliable way + %% to ensure proper session recovery according to the MQTT spec. + true + end, Opts = maps:without( [ address, @@ -160,6 +171,7 @@ mk_client_options(Conf, BridgeOpts) -> Opts#{ msg_handler => mk_client_event_handler(Vars, #{server => Server}), hosts => [HostPort], + clean_start => CleanStart, force_ping => true, proto_ver => maps:get(proto_ver, BridgeOpts, v4) }. @@ -205,10 +217,12 @@ subscribe_remote_topics(_Ref, undefined) -> stop(Ref) -> emqtt:stop(ref(Ref)). +info(Ref) -> + emqtt:info(ref(Ref)). + status(Ref) -> try - Info = emqtt:info(ref(Ref)), - case proplists:get_value(socket, Info) of + case proplists:get_value(socket, info(Ref)) of Socket when Socket /= undefined -> connected; undefined -> From dcee9e1626202b0d3a200dde385e30eeb876c0cd Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Fri, 10 Feb 2023 17:06:56 +0300 Subject: [PATCH 0508/1802] chore: add changelog entry --- changes/v5.0.17/fix-9952.en.md | 2 ++ changes/v5.0.17/fix-9952.zh.md | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 changes/v5.0.17/fix-9952.en.md create mode 100644 changes/v5.0.17/fix-9952.zh.md diff --git a/changes/v5.0.17/fix-9952.en.md b/changes/v5.0.17/fix-9952.en.md new file mode 100644 index 000000000..3a951b423 --- /dev/null +++ b/changes/v5.0.17/fix-9952.en.md @@ -0,0 +1,2 @@ +Disallow subscribing with QoS 2 for ingress MQTT bridges. +Allow user to configure `clean_start` option for ingress MQTT bridges however. diff --git a/changes/v5.0.17/fix-9952.zh.md b/changes/v5.0.17/fix-9952.zh.md new file mode 100644 index 000000000..1b9064c7d --- /dev/null +++ b/changes/v5.0.17/fix-9952.zh.md @@ -0,0 +1,2 @@ +不允许对 ingress MQTT 网桥的 QoS 2 进行订阅。 +但允许用户为 ingress MQTT 桥配置 "clean_start" 选项。 From fb61c2b2661f1035507b4e7c78bc37ec9b50214c Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Fri, 10 Feb 2023 17:05:09 +0100 Subject: [PATCH 0509/1802] perf: avoid getting metrics (gen_server:call) for each resource lookup --- apps/emqx_resource/include/emqx_resource.hrl | 2 +- apps/emqx_resource/src/emqx_resource_manager.erl | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/apps/emqx_resource/include/emqx_resource.hrl b/apps/emqx_resource/include/emqx_resource.hrl index ce4e02c2a..fa7f2eb38 100644 --- a/apps/emqx_resource/include/emqx_resource.hrl +++ b/apps/emqx_resource/include/emqx_resource.hrl @@ -43,7 +43,7 @@ config := resource_config(), state := resource_state(), status := resource_status(), - metrics := emqx_metrics_worker:metrics() + metrics => emqx_metrics_worker:metrics() }. -type resource_group() :: binary(). -type creation_opts() :: #{ diff --git a/apps/emqx_resource/src/emqx_resource_manager.erl b/apps/emqx_resource/src/emqx_resource_manager.erl index 4846c8ae6..ee9e218b2 100644 --- a/apps/emqx_resource/src/emqx_resource_manager.erl +++ b/apps/emqx_resource/src/emqx_resource_manager.erl @@ -238,7 +238,7 @@ lookup(ResId) -> ets_lookup(ResId) -> case read_cache(ResId) of {Group, Data} -> - {ok, Group, data_record_to_external_map_with_metrics(Data)}; + {ok, Group, data_record_to_external_map(Data)}; not_found -> {error, not_found} end. @@ -620,8 +620,8 @@ maybe_reply(Actions, undefined, _Reply) -> maybe_reply(Actions, From, Reply) -> [{reply, From, Reply} | Actions]. --spec data_record_to_external_map_with_metrics(data()) -> resource_data(). -data_record_to_external_map_with_metrics(Data) -> +-spec data_record_to_external_map(data()) -> resource_data(). +data_record_to_external_map(Data) -> #{ id => Data#data.id, mod => Data#data.mod, @@ -629,10 +629,14 @@ data_record_to_external_map_with_metrics(Data) -> query_mode => Data#data.query_mode, config => Data#data.config, status => Data#data.status, - state => Data#data.state, - metrics => get_metrics(Data#data.id) + state => Data#data.state }. +-spec data_record_to_external_map_with_metrics(data()) -> resource_data(). +data_record_to_external_map_with_metrics(Data) -> + DataMap = data_record_to_external_map(Data), + DataMap#{metrics => get_metrics(Data#data.id)}. + -spec wait_for_ready(resource_id(), integer()) -> ok | timeout | {error, term()}. wait_for_ready(ResId, WaitTime) -> do_wait_for_ready(ResId, WaitTime div ?WAIT_FOR_RESOURCE_DELAY). From dfcc59afcf3a4276b7280bbabdfdce0e6548a85c Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Fri, 10 Feb 2023 17:41:45 +0100 Subject: [PATCH 0510/1802] chore: add changelog --- changes/v5.0.17/perf-9954.en.md | 1 + changes/v5.0.17/perf-9954.zh.md | 1 + scripts/format-changelog.sh | 5 +++-- 3 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 changes/v5.0.17/perf-9954.en.md create mode 100644 changes/v5.0.17/perf-9954.zh.md diff --git a/changes/v5.0.17/perf-9954.en.md b/changes/v5.0.17/perf-9954.en.md new file mode 100644 index 000000000..d816c2096 --- /dev/null +++ b/changes/v5.0.17/perf-9954.en.md @@ -0,0 +1 @@ +Improve bridge performance diff --git a/changes/v5.0.17/perf-9954.zh.md b/changes/v5.0.17/perf-9954.zh.md new file mode 100644 index 000000000..df1dabedd --- /dev/null +++ b/changes/v5.0.17/perf-9954.zh.md @@ -0,0 +1 @@ +优化桥接性能 diff --git a/scripts/format-changelog.sh b/scripts/format-changelog.sh index 862b42c17..23f801f2b 100755 --- a/scripts/format-changelog.sh +++ b/scripts/format-changelog.sh @@ -26,8 +26,8 @@ item() { } section() { - local section_name=$1 - for i in "${changes_dir}"/"${section_name}"-*."${language}".md; do + local prefix=$1 + for i in "${changes_dir}"/"${prefix}"-*."${language}".md; do item "${i}" done } @@ -39,6 +39,7 @@ if [ "${language}" = "en" ]; then ## Enhancements $(section feat) +$(section perf) ## Bug fixes From 72fe10f18b952fe9648b7c8bebd1dc545a0f3632 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Fri, 10 Feb 2023 19:41:14 +0100 Subject: [PATCH 0511/1802] chore: fix spell check errors --- apps/emqx_bridge/i18n/emqx_bridge_api.conf | 2 +- apps/emqx_connector/i18n/emqx_connector_mqtt_schema.conf | 2 +- apps/emqx_gateway/i18n/emqx_gateway_api_i18n.conf | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/emqx_bridge/i18n/emqx_bridge_api.conf b/apps/emqx_bridge/i18n/emqx_bridge_api.conf index 73f78609d..f5d372128 100644 --- a/apps/emqx_bridge/i18n/emqx_bridge_api.conf +++ b/apps/emqx_bridge/i18n/emqx_bridge_api.conf @@ -46,7 +46,7 @@ emqx_bridge_api { desc_param_path_enable { desc { - en: """Whether or not the bridge is enabled""" + en: """Whether to enable this bridge""" zh: """是否启用桥接""" } label: { diff --git a/apps/emqx_connector/i18n/emqx_connector_mqtt_schema.conf b/apps/emqx_connector/i18n/emqx_connector_mqtt_schema.conf index b075681f3..f9f79beb8 100644 --- a/apps/emqx_connector/i18n/emqx_connector_mqtt_schema.conf +++ b/apps/emqx_connector/i18n/emqx_connector_mqtt_schema.conf @@ -159,7 +159,7 @@ broker MUST support this feature.""" clean_start { desc { - en: "Whether or not to start a clean session when reconnecting a remote broker for ingress bridge" + en: "Whether to start a clean session when reconnecting a remote broker for ingress bridge" zh: "与 ingress MQTT 桥的远程服务器重连时是否清除老的 MQTT 会话。" } label: { diff --git a/apps/emqx_gateway/i18n/emqx_gateway_api_i18n.conf b/apps/emqx_gateway/i18n/emqx_gateway_api_i18n.conf index ab30a9c22..18ab1f242 100644 --- a/apps/emqx_gateway/i18n/emqx_gateway_api_i18n.conf +++ b/apps/emqx_gateway/i18n/emqx_gateway_api_i18n.conf @@ -57,7 +57,7 @@ It's enum with `stomp`, `mqttsn`, `coap`, `lwm2m`, `exproto`""" gateway_enable_in_path { desc { - en: """Whether or not gateway is enabled""" + en: """Whether to enable this gateway""" zh: """是否开启此网关""" } From e487247731f8f5eef03e19bfdee8c44ade52af59 Mon Sep 17 00:00:00 2001 From: Kjell Winblad Date: Wed, 8 Feb 2023 10:05:58 +0100 Subject: [PATCH 0512/1802] build: make sure run.sh with EE app is used with EE profile If one runs a test suite for an EE app using `scripts/ct/run.sh` without setting the EE profile, the test suite seems to just get stuck. This commit ensures that this will not happen and can hopefully save someone some time. Co-authored-by: Ivan Dyachkov --- scripts/ct/run.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scripts/ct/run.sh b/scripts/ct/run.sh index 69427c7c3..0641707e8 100755 --- a/scripts/ct/run.sh +++ b/scripts/ct/run.sh @@ -80,6 +80,14 @@ if [ "${WHICH_APP}" = 'novalue' ]; then exit 1 fi +if [[ "${WHICH_APP}" == lib-ee* && (-z "${PROFILE+x}" || "${PROFILE}" != emqx-enterprise) ]]; then + echo 'You are trying to run an enterprise test case without the emqx-enterprise profile.' + echo 'This will most likely not work.' + echo '' + echo 'Run "export PROFILE=emqx-enterprise" and "make" to fix this' + exit 1 +fi + ERLANG_CONTAINER='erlang' DOCKER_CT_ENVS_FILE="${WHICH_APP}/docker-ct" From a11217aa0f0412bd02ed8a3ad9a97bc2630e3f23 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Sun, 12 Feb 2023 11:36:53 +0100 Subject: [PATCH 0513/1802] chore: bump version to v5.0.17 --- apps/emqx/include/emqx_release.hrl | 2 +- deploy/charts/emqx/Chart.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/emqx/include/emqx_release.hrl b/apps/emqx/include/emqx_release.hrl index 4e4066eef..afd169598 100644 --- a/apps/emqx/include/emqx_release.hrl +++ b/apps/emqx/include/emqx_release.hrl @@ -32,7 +32,7 @@ %% `apps/emqx/src/bpapi/README.md' %% Community edition --define(EMQX_RELEASE_CE, "5.0.16"). +-define(EMQX_RELEASE_CE, "5.0.17-rc.1"). %% Enterprise edition -define(EMQX_RELEASE_EE, "5.0.0"). diff --git a/deploy/charts/emqx/Chart.yaml b/deploy/charts/emqx/Chart.yaml index 2208f3019..377d61985 100644 --- a/deploy/charts/emqx/Chart.yaml +++ b/deploy/charts/emqx/Chart.yaml @@ -14,8 +14,8 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. -version: 5.0.16 +version: 5.0.17 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. -appVersion: 5.0.16 +appVersion: 5.0.17 From 7d9b0f48263d92e3d355f0effc16906033ea43ed Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Mon, 13 Feb 2023 08:53:09 +0100 Subject: [PATCH 0514/1802] chore: bump version to v5.0.17 --- apps/emqx/include/emqx_release.hrl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx/include/emqx_release.hrl b/apps/emqx/include/emqx_release.hrl index afd169598..80dafdb54 100644 --- a/apps/emqx/include/emqx_release.hrl +++ b/apps/emqx/include/emqx_release.hrl @@ -32,7 +32,7 @@ %% `apps/emqx/src/bpapi/README.md' %% Community edition --define(EMQX_RELEASE_CE, "5.0.17-rc.1"). +-define(EMQX_RELEASE_CE, "5.0.17"). %% Enterprise edition -define(EMQX_RELEASE_EE, "5.0.0"). From 122ff97ffbb61fb4a32c35177c8f9272f4cc72f5 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Mon, 13 Feb 2023 08:53:39 +0100 Subject: [PATCH 0515/1802] chore: Generate changelog for v5.0.17 --- changes/v5.0.17-en.md | 42 ++++++++++++++++++++++++++++++++++++++++++ changes/v5.0.17-zh.md | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 changes/v5.0.17-en.md create mode 100644 changes/v5.0.17-zh.md diff --git a/changes/v5.0.17-en.md b/changes/v5.0.17-en.md new file mode 100644 index 000000000..0f71057dd --- /dev/null +++ b/changes/v5.0.17-en.md @@ -0,0 +1,42 @@ +# v5.0.17 + +## Enhancements + +- [#9802](https://github.com/emqx/emqx/pull/9802) Support HAProxy protocol for dashboard API. + +- [#9871](https://github.com/emqx/emqx/pull/9871) Allow the placeholder to be anywhere in the topic for `authz` rules. + e.g: + `{allow, {username, "who"}, publish, ["t/foo${username}boo/${clientid}xxx"]}.` + +- [#9910](https://github.com/emqx/emqx/pull/9910) Add `start` operation to bridges API to allow manual reconnect after failure. + +- [#9917](https://github.com/emqx/emqx/pull/9917) Stop building -alpine docker image because it's size is larger than the regular one based on debian slim + +- [#9930](https://github.com/emqx/emqx/pull/9930) Expose the stats `live_connections.count` and `live_connections.max` to Prometheus. + +- [#9936](https://github.com/emqx/emqx/pull/9936) Disable disksup (part of os_mon) in releases by default. +- [#9954](https://github.com/emqx/emqx/pull/9954) Improve bridge performance + +## Bug fixes + +- [#9864](https://github.com/emqx/emqx/pull/9864) Fix the exclusive topics aren't removed when the session has already been cleaned. + +- [#9875](https://github.com/emqx/emqx/pull/9875) Return `400` if a broken plugin package is uploaded from HTTP API. Also cleanup if plugin is not accepted. + +- [#9916](https://github.com/emqx/emqx/pull/9916) Fix MQTT bridge fails to verify TLS wildcard server certificate. + + +- [#9922](https://github.com/emqx/emqx/pull/9922) Fix the issue with the bridge resource buffer where it might become stuck if enough async queries fill the inflight window full before failing with retryable errors. + +- [#9923](https://github.com/emqx/emqx/pull/9923) Fix REPORT_CB/2 CRASH error logs when erros happen during boot-up or shutdown. + +- [#9938](https://github.com/emqx/emqx/pull/9938) Report some egress MQTT bridge errors as recoverable, and thus retryable. + +- [#9946](https://github.com/emqx/emqx/pull/9946) Add back `reconnect_interval` as deprecated field for MQTT bridge. + The field was removed from v5.0.16/e5.0.0 by mistake, caused new version unable to start on old config. + Now it's added back as deprecated (config value is ignored if provided). + +- [#9951](https://github.com/emqx/emqx/pull/9951) Propagate errors from operations (`start|stop|restart`) on bridges API if called for all nodes. + +- [#9952](https://github.com/emqx/emqx/pull/9952) Disallow subscribing with QoS 2 for ingress MQTT bridges. + Allow user to configure `clean_start` option for ingress MQTT bridges however. diff --git a/changes/v5.0.17-zh.md b/changes/v5.0.17-zh.md new file mode 100644 index 000000000..b7510deea --- /dev/null +++ b/changes/v5.0.17-zh.md @@ -0,0 +1,39 @@ +# v5.0.17 + +## 增强 + +- [#9802](https://github.com/emqx/emqx/pull/9802) 现在 dashboard 增加了对 `HAProxy` 协议的支持。 + +- [#9871](https://github.com/emqx/emqx/pull/9871) 允许占位符出现在 `authz` 规则中的主题里的任意位置。 + 例如: + `{allow, {username, "who"}, publish, ["t/foo${username}boo/${clientid}xxx"]}.` + +- [#9910](https://github.com/emqx/emqx/pull/9910) 在桥接 API 中增加 `start` 操作,允许失败后手动重新连接。 + +- [#9917](https://github.com/emqx/emqx/pull/9917) 停止构建-alpine docker镜像,因为它的大小比基于debian slim的普通镜像要大。 + +- [#9930](https://github.com/emqx/emqx/pull/9930) 将统计数据 `live_connections.count` 和 `live_connections.max` 公开给 Prometheus. + +- [#9936](https://github.com/emqx/emqx/pull/9936) 在发行版中默认禁用diskup(os_mon的一部分)。 + +## 修复 + +- [#9864](https://github.com/emqx/emqx/pull/9864) 修复会话清除后相关的排他订阅主题没有被清理的问题。 + +- [#9875](https://github.com/emqx/emqx/pull/9875) 当通过 HTTP API 上传一个损坏的插件包时,返回 `400`,且删除该插件包。 + +- [#9916](https://github.com/emqx/emqx/pull/9916) 修复MQTT桥接器无法验证TLS通配符服务器证书的问题。 + +- [#9922](https://github.com/emqx/emqx/pull/9922) 修复桥接资源缓冲区的问题,如果足够多的异步查询在失败并出现可重试错误之前将机上窗口填满,则可能会卡住。 + +- [#9923](https://github.com/emqx/emqx/pull/9923) 修复在启动和关闭过程中发生错误时,日志中的 REPORT_CB/2 CRASH 错误。 + +- [#9938](https://github.com/emqx/emqx/pull/9938) 将一些出口 MQTT 网桥错误报告为可恢复,因此可重试。 + +- [#9946](https://github.com/emqx/emqx/pull/9946) MQTT 桥接配置参数 `reconnect_interval` 在 v5.0.16/e5.0.0 中被错误删除, + 导致旧版本配置在升级后无法使用。此修复把配置加回,但是值会忽略。 + +- [#9951](https://github.com/emqx/emqx/pull/9951) 返回桥接 API 操作(`start|stop|restart`) 的多节点远程调用的错误信息。 + +- [#9952](https://github.com/emqx/emqx/pull/9952) 不允许对 ingress MQTT 网桥的 QoS 2 进行订阅。 + 但允许用户为 ingress MQTT 桥配置 "clean_start" 选项。 From 7947d83cb94f182148c3db58f413425938655a3f Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Mon, 13 Feb 2023 08:57:54 +0100 Subject: [PATCH 0516/1802] ci: fix change log format script --- scripts/format-changelog.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/format-changelog.sh b/scripts/format-changelog.sh index 23f801f2b..c7b413cc8 100755 --- a/scripts/format-changelog.sh +++ b/scripts/format-changelog.sh @@ -39,6 +39,7 @@ if [ "${language}" = "en" ]; then ## Enhancements $(section feat) + $(section perf) ## Bug fixes @@ -53,6 +54,8 @@ elif [ "${language}" = "zh" ]; then $(section feat) +$(section perf) + ## 修复 $(section fix) From 3175a80ba3772bf30d7d2d3adc5b5fa80ad18fa9 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Mon, 13 Feb 2023 08:58:31 +0100 Subject: [PATCH 0517/1802] chore: Generate changelog for v5.0.17 --- changes/v5.0.17-en.md | 1 + changes/v5.0.17-zh.md | 2 ++ 2 files changed, 3 insertions(+) diff --git a/changes/v5.0.17-en.md b/changes/v5.0.17-en.md index 0f71057dd..17a714cce 100644 --- a/changes/v5.0.17-en.md +++ b/changes/v5.0.17-en.md @@ -15,6 +15,7 @@ - [#9930](https://github.com/emqx/emqx/pull/9930) Expose the stats `live_connections.count` and `live_connections.max` to Prometheus. - [#9936](https://github.com/emqx/emqx/pull/9936) Disable disksup (part of os_mon) in releases by default. + - [#9954](https://github.com/emqx/emqx/pull/9954) Improve bridge performance ## Bug fixes diff --git a/changes/v5.0.17-zh.md b/changes/v5.0.17-zh.md index b7510deea..0692d29ac 100644 --- a/changes/v5.0.17-zh.md +++ b/changes/v5.0.17-zh.md @@ -16,6 +16,8 @@ - [#9936](https://github.com/emqx/emqx/pull/9936) 在发行版中默认禁用diskup(os_mon的一部分)。 +- [#9954](https://github.com/emqx/emqx/pull/9954) 优化桥接性能 + ## 修复 - [#9864](https://github.com/emqx/emqx/pull/9864) 修复会话清除后相关的排他订阅主题没有被清理的问题。 From 4ce4cdce65c87f342db50e68438c60ef5d66988f Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Mon, 13 Feb 2023 09:00:01 +0100 Subject: [PATCH 0518/1802] chore: remove old changelog files --- changes/v5.0.17/feat-9802.en.md | 1 - changes/v5.0.17/feat-9802.zh.md | 1 - changes/v5.0.17/feat-9871.en.md | 3 --- changes/v5.0.17/feat-9871.zh.md | 3 --- changes/v5.0.17/feat-9910.en.md | 1 - changes/v5.0.17/feat-9910.zh.md | 1 - changes/v5.0.17/feat-9917.en.md | 1 - changes/v5.0.17/feat-9917.zh.md | 1 - changes/v5.0.17/feat-9930.en.md | 1 - changes/v5.0.17/feat-9930.zh.md | 1 - changes/v5.0.17/feat-9936.en.md | 1 - changes/v5.0.17/feat-9936.zh.md | 1 - changes/v5.0.17/fix-9864.en.md | 1 - changes/v5.0.17/fix-9864.zh.md | 1 - changes/v5.0.17/fix-9875.en.md | 1 - changes/v5.0.17/fix-9875.zh.md | 1 - changes/v5.0.17/fix-9916.en.md | 2 -- changes/v5.0.17/fix-9916.zh.md | 1 - changes/v5.0.17/fix-9922.en.md | 1 - changes/v5.0.17/fix-9922.zh.md | 1 - changes/v5.0.17/fix-9923.en.md | 1 - changes/v5.0.17/fix-9923.zh.md | 1 - changes/v5.0.17/fix-9938.en.md | 1 - changes/v5.0.17/fix-9938.zh.md | 1 - changes/v5.0.17/fix-9946.en.md | 3 --- changes/v5.0.17/fix-9946.zh.md | 2 -- changes/v5.0.17/fix-9951.en.md | 1 - changes/v5.0.17/fix-9951.zh.md | 1 - changes/v5.0.17/fix-9952.en.md | 2 -- changes/v5.0.17/fix-9952.zh.md | 2 -- changes/v5.0.17/perf-9954.en.md | 1 - changes/v5.0.17/perf-9954.zh.md | 1 - 32 files changed, 42 deletions(-) delete mode 100644 changes/v5.0.17/feat-9802.en.md delete mode 100644 changes/v5.0.17/feat-9802.zh.md delete mode 100644 changes/v5.0.17/feat-9871.en.md delete mode 100644 changes/v5.0.17/feat-9871.zh.md delete mode 100644 changes/v5.0.17/feat-9910.en.md delete mode 100644 changes/v5.0.17/feat-9910.zh.md delete mode 100644 changes/v5.0.17/feat-9917.en.md delete mode 100644 changes/v5.0.17/feat-9917.zh.md delete mode 100644 changes/v5.0.17/feat-9930.en.md delete mode 100644 changes/v5.0.17/feat-9930.zh.md delete mode 100644 changes/v5.0.17/feat-9936.en.md delete mode 100644 changes/v5.0.17/feat-9936.zh.md delete mode 100644 changes/v5.0.17/fix-9864.en.md delete mode 100644 changes/v5.0.17/fix-9864.zh.md delete mode 100644 changes/v5.0.17/fix-9875.en.md delete mode 100644 changes/v5.0.17/fix-9875.zh.md delete mode 100644 changes/v5.0.17/fix-9916.en.md delete mode 100644 changes/v5.0.17/fix-9916.zh.md delete mode 100644 changes/v5.0.17/fix-9922.en.md delete mode 100644 changes/v5.0.17/fix-9922.zh.md delete mode 100644 changes/v5.0.17/fix-9923.en.md delete mode 100644 changes/v5.0.17/fix-9923.zh.md delete mode 100644 changes/v5.0.17/fix-9938.en.md delete mode 100644 changes/v5.0.17/fix-9938.zh.md delete mode 100644 changes/v5.0.17/fix-9946.en.md delete mode 100644 changes/v5.0.17/fix-9946.zh.md delete mode 100644 changes/v5.0.17/fix-9951.en.md delete mode 100644 changes/v5.0.17/fix-9951.zh.md delete mode 100644 changes/v5.0.17/fix-9952.en.md delete mode 100644 changes/v5.0.17/fix-9952.zh.md delete mode 100644 changes/v5.0.17/perf-9954.en.md delete mode 100644 changes/v5.0.17/perf-9954.zh.md diff --git a/changes/v5.0.17/feat-9802.en.md b/changes/v5.0.17/feat-9802.en.md deleted file mode 100644 index ac314879a..000000000 --- a/changes/v5.0.17/feat-9802.en.md +++ /dev/null @@ -1 +0,0 @@ -Support HAProxy protocol for dashboard API. diff --git a/changes/v5.0.17/feat-9802.zh.md b/changes/v5.0.17/feat-9802.zh.md deleted file mode 100644 index 9afcd8f17..000000000 --- a/changes/v5.0.17/feat-9802.zh.md +++ /dev/null @@ -1 +0,0 @@ -现在 dashboard 增加了对 `HAProxy` 协议的支持。 diff --git a/changes/v5.0.17/feat-9871.en.md b/changes/v5.0.17/feat-9871.en.md deleted file mode 100644 index b907aa3f1..000000000 --- a/changes/v5.0.17/feat-9871.en.md +++ /dev/null @@ -1,3 +0,0 @@ -Allow the placeholder to be anywhere in the topic for `authz` rules. -e.g: -`{allow, {username, "who"}, publish, ["t/foo${username}boo/${clientid}xxx"]}.` diff --git a/changes/v5.0.17/feat-9871.zh.md b/changes/v5.0.17/feat-9871.zh.md deleted file mode 100644 index ecd526a93..000000000 --- a/changes/v5.0.17/feat-9871.zh.md +++ /dev/null @@ -1,3 +0,0 @@ -允许占位符出现在 `authz` 规则中的主题里的任意位置。 -例如: -`{allow, {username, "who"}, publish, ["t/foo${username}boo/${clientid}xxx"]}.` diff --git a/changes/v5.0.17/feat-9910.en.md b/changes/v5.0.17/feat-9910.en.md deleted file mode 100644 index bf8e0e1b4..000000000 --- a/changes/v5.0.17/feat-9910.en.md +++ /dev/null @@ -1 +0,0 @@ -Add `start` operation to bridges API to allow manual reconnect after failure. diff --git a/changes/v5.0.17/feat-9910.zh.md b/changes/v5.0.17/feat-9910.zh.md deleted file mode 100644 index 3e35fff19..000000000 --- a/changes/v5.0.17/feat-9910.zh.md +++ /dev/null @@ -1 +0,0 @@ -在桥接 API 中增加 `start` 操作,允许失败后手动重新连接。 diff --git a/changes/v5.0.17/feat-9917.en.md b/changes/v5.0.17/feat-9917.en.md deleted file mode 100644 index dfc00649e..000000000 --- a/changes/v5.0.17/feat-9917.en.md +++ /dev/null @@ -1 +0,0 @@ -Stop building -alpine docker image because it's size is larger than the regular one based on debian slim diff --git a/changes/v5.0.17/feat-9917.zh.md b/changes/v5.0.17/feat-9917.zh.md deleted file mode 100644 index 797062b60..000000000 --- a/changes/v5.0.17/feat-9917.zh.md +++ /dev/null @@ -1 +0,0 @@ -停止构建-alpine docker镜像,因为它的大小比基于debian slim的普通镜像要大。 diff --git a/changes/v5.0.17/feat-9930.en.md b/changes/v5.0.17/feat-9930.en.md deleted file mode 100644 index 1747bb23e..000000000 --- a/changes/v5.0.17/feat-9930.en.md +++ /dev/null @@ -1 +0,0 @@ -Expose the stats `live_connections.count` and `live_connections.max` to Prometheus. diff --git a/changes/v5.0.17/feat-9930.zh.md b/changes/v5.0.17/feat-9930.zh.md deleted file mode 100644 index d17c1cf68..000000000 --- a/changes/v5.0.17/feat-9930.zh.md +++ /dev/null @@ -1 +0,0 @@ -将统计数据 `live_connections.count` 和 `live_connections.max` 公开给 Prometheus. diff --git a/changes/v5.0.17/feat-9936.en.md b/changes/v5.0.17/feat-9936.en.md deleted file mode 100644 index 3010e6fed..000000000 --- a/changes/v5.0.17/feat-9936.en.md +++ /dev/null @@ -1 +0,0 @@ -Disable disksup (part of os_mon) in releases by default. diff --git a/changes/v5.0.17/feat-9936.zh.md b/changes/v5.0.17/feat-9936.zh.md deleted file mode 100644 index f459f9e52..000000000 --- a/changes/v5.0.17/feat-9936.zh.md +++ /dev/null @@ -1 +0,0 @@ -在发行版中默认禁用diskup(os_mon的一部分)。 diff --git a/changes/v5.0.17/fix-9864.en.md b/changes/v5.0.17/fix-9864.en.md deleted file mode 100644 index ad5cd53c9..000000000 --- a/changes/v5.0.17/fix-9864.en.md +++ /dev/null @@ -1 +0,0 @@ -Fix the exclusive topics aren't removed when the session has already been cleaned. diff --git a/changes/v5.0.17/fix-9864.zh.md b/changes/v5.0.17/fix-9864.zh.md deleted file mode 100644 index b79d81988..000000000 --- a/changes/v5.0.17/fix-9864.zh.md +++ /dev/null @@ -1 +0,0 @@ -修复会话清除后相关的排他订阅主题没有被清理的问题。 diff --git a/changes/v5.0.17/fix-9875.en.md b/changes/v5.0.17/fix-9875.en.md deleted file mode 100644 index ca2d4094a..000000000 --- a/changes/v5.0.17/fix-9875.en.md +++ /dev/null @@ -1 +0,0 @@ -Return `400` if a broken plugin package is uploaded from HTTP API. Also cleanup if plugin is not accepted. diff --git a/changes/v5.0.17/fix-9875.zh.md b/changes/v5.0.17/fix-9875.zh.md deleted file mode 100644 index 6e1230f3a..000000000 --- a/changes/v5.0.17/fix-9875.zh.md +++ /dev/null @@ -1 +0,0 @@ -当通过 HTTP API 上传一个损坏的插件包时,返回 `400`,且删除该插件包。 diff --git a/changes/v5.0.17/fix-9916.en.md b/changes/v5.0.17/fix-9916.en.md deleted file mode 100644 index dd6de13e5..000000000 --- a/changes/v5.0.17/fix-9916.en.md +++ /dev/null @@ -1,2 +0,0 @@ -Fix MQTT bridge fails to verify TLS wildcard server certificate. - diff --git a/changes/v5.0.17/fix-9916.zh.md b/changes/v5.0.17/fix-9916.zh.md deleted file mode 100644 index 3bed13e4c..000000000 --- a/changes/v5.0.17/fix-9916.zh.md +++ /dev/null @@ -1 +0,0 @@ -修复MQTT桥接器无法验证TLS通配符服务器证书的问题。 diff --git a/changes/v5.0.17/fix-9922.en.md b/changes/v5.0.17/fix-9922.en.md deleted file mode 100644 index b58a8a766..000000000 --- a/changes/v5.0.17/fix-9922.en.md +++ /dev/null @@ -1 +0,0 @@ -Fix the issue with the bridge resource buffer where it might become stuck if enough async queries fill the inflight window full before failing with retryable errors. diff --git a/changes/v5.0.17/fix-9922.zh.md b/changes/v5.0.17/fix-9922.zh.md deleted file mode 100644 index cf50b55e4..000000000 --- a/changes/v5.0.17/fix-9922.zh.md +++ /dev/null @@ -1 +0,0 @@ -修复桥接资源缓冲区的问题,如果足够多的异步查询在失败并出现可重试错误之前将机上窗口填满,则可能会卡住。 diff --git a/changes/v5.0.17/fix-9923.en.md b/changes/v5.0.17/fix-9923.en.md deleted file mode 100644 index fa6926893..000000000 --- a/changes/v5.0.17/fix-9923.en.md +++ /dev/null @@ -1 +0,0 @@ -Fix REPORT_CB/2 CRASH error logs when erros happen during boot-up or shutdown. diff --git a/changes/v5.0.17/fix-9923.zh.md b/changes/v5.0.17/fix-9923.zh.md deleted file mode 100644 index 610b14b96..000000000 --- a/changes/v5.0.17/fix-9923.zh.md +++ /dev/null @@ -1 +0,0 @@ -修复在启动和关闭过程中发生错误时,日志中的 REPORT_CB/2 CRASH 错误。 diff --git a/changes/v5.0.17/fix-9938.en.md b/changes/v5.0.17/fix-9938.en.md deleted file mode 100644 index 54de76fd7..000000000 --- a/changes/v5.0.17/fix-9938.en.md +++ /dev/null @@ -1 +0,0 @@ -Report some egress MQTT bridge errors as recoverable, and thus retryable. diff --git a/changes/v5.0.17/fix-9938.zh.md b/changes/v5.0.17/fix-9938.zh.md deleted file mode 100644 index f8bd41cf0..000000000 --- a/changes/v5.0.17/fix-9938.zh.md +++ /dev/null @@ -1 +0,0 @@ -将一些出口 MQTT 网桥错误报告为可恢复,因此可重试。 diff --git a/changes/v5.0.17/fix-9946.en.md b/changes/v5.0.17/fix-9946.en.md deleted file mode 100644 index ba6f768fa..000000000 --- a/changes/v5.0.17/fix-9946.en.md +++ /dev/null @@ -1,3 +0,0 @@ -Add back `reconnect_interval` as deprecated field for MQTT bridge. -The field was removed from v5.0.16/e5.0.0 by mistake, caused new version unable to start on old config. -Now it's added back as deprecated (config value is ignored if provided). diff --git a/changes/v5.0.17/fix-9946.zh.md b/changes/v5.0.17/fix-9946.zh.md deleted file mode 100644 index be071e470..000000000 --- a/changes/v5.0.17/fix-9946.zh.md +++ /dev/null @@ -1,2 +0,0 @@ -MQTT 桥接配置参数 `reconnect_interval` 在 v5.0.16/e5.0.0 中被错误删除, -导致旧版本配置在升级后无法使用。此修复把配置加回,但是值会忽略。 diff --git a/changes/v5.0.17/fix-9951.en.md b/changes/v5.0.17/fix-9951.en.md deleted file mode 100644 index e2439cf01..000000000 --- a/changes/v5.0.17/fix-9951.en.md +++ /dev/null @@ -1 +0,0 @@ -Propagate errors from operations (`start|stop|restart`) on bridges API if called for all nodes. diff --git a/changes/v5.0.17/fix-9951.zh.md b/changes/v5.0.17/fix-9951.zh.md deleted file mode 100644 index aa540740c..000000000 --- a/changes/v5.0.17/fix-9951.zh.md +++ /dev/null @@ -1 +0,0 @@ -返回桥接 API 操作(`start|stop|restart`) 的多节点远程调用的错误信息。 diff --git a/changes/v5.0.17/fix-9952.en.md b/changes/v5.0.17/fix-9952.en.md deleted file mode 100644 index 3a951b423..000000000 --- a/changes/v5.0.17/fix-9952.en.md +++ /dev/null @@ -1,2 +0,0 @@ -Disallow subscribing with QoS 2 for ingress MQTT bridges. -Allow user to configure `clean_start` option for ingress MQTT bridges however. diff --git a/changes/v5.0.17/fix-9952.zh.md b/changes/v5.0.17/fix-9952.zh.md deleted file mode 100644 index 1b9064c7d..000000000 --- a/changes/v5.0.17/fix-9952.zh.md +++ /dev/null @@ -1,2 +0,0 @@ -不允许对 ingress MQTT 网桥的 QoS 2 进行订阅。 -但允许用户为 ingress MQTT 桥配置 "clean_start" 选项。 diff --git a/changes/v5.0.17/perf-9954.en.md b/changes/v5.0.17/perf-9954.en.md deleted file mode 100644 index d816c2096..000000000 --- a/changes/v5.0.17/perf-9954.en.md +++ /dev/null @@ -1 +0,0 @@ -Improve bridge performance diff --git a/changes/v5.0.17/perf-9954.zh.md b/changes/v5.0.17/perf-9954.zh.md deleted file mode 100644 index df1dabedd..000000000 --- a/changes/v5.0.17/perf-9954.zh.md +++ /dev/null @@ -1 +0,0 @@ -优化桥接性能 From b6b9df06dbe8c17ad319242adaf51bfa5aa73c60 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Sun, 12 Feb 2023 10:00:18 +0100 Subject: [PATCH 0519/1802] fix(bin/emqx): drop remsh and escript from ps -ef outputs --- bin/emqx | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/bin/emqx b/bin/emqx index 0a967a73c..911087416 100755 --- a/bin/emqx +++ b/bin/emqx @@ -464,9 +464,29 @@ if [ "$IS_BOOT_COMMAND" = 'yes' ]; then export EMQX_BOOT_CONFIGS fi else - # For non-boot commands, we try to get data_dir and ssl_dist_optfile from 'ps -ef' output + # For non-boot commands, we need below runtime facts to connect to the running node: + # 1. The running node name. + # 2. The Erlang cookie in use by the running node name. + # 3. SSL options if the node is using TLS for Erlang distribution + # + # There are 3 sources of truth to get those runtime information. + # Listed in the order of preference: + # 1. The boot command (which can be inspected from 'ps -ef' command output) + # 2. The generated app.