From 3d07ddd4f0b2e9dd237706d8e4d04e5d0c31e726 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Tue, 18 Jul 2023 15:23:37 +0800 Subject: [PATCH 01/31] chore: improve the `sys_msg_interval` config docs --- rel/i18n/emqx_schema.hocon | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/rel/i18n/emqx_schema.hocon b/rel/i18n/emqx_schema.hocon index f871cfc49..3ecb930cd 100644 --- a/rel/i18n/emqx_schema.hocon +++ b/rel/i18n/emqx_schema.hocon @@ -697,7 +697,12 @@ fields_mqtt_quic_listener_minimum_mtu.label: """Minimum MTU""" sys_msg_interval.desc: -"""Time interval of publishing `$SYS` messages.""" +"""Time interval for publishing following system messages: + - `$SYS/brokers` + - `$SYS/brokers//version` + - `$SYS/brokers//sysdescr` + - `$SYS/brokers//stats/` + - `$SYS/brokers//metrics/`""" mqtt_await_rel_timeout.desc: """For client to broker QoS 2 message, the time limit for the broker to wait before the `PUBREL` message is received. The wait is aborted after timed out, meaning the packet ID is freed for new `PUBLISH` requests. Receiving a stale `PUBREL` causes a warning level log. Note, the message is delivered to subscribers before entering the wait for PUBREL.""" From eb41b77de4bf54c12dadb307b38fe4fd2eb5cc62 Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Wed, 19 Jul 2023 11:37:06 -0300 Subject: [PATCH 02/31] fix(rule_metrics): notify rule metrics of late replies and expired requests Fixes https://emqx.atlassian.net/browse/EMQX-10600 --- .../test/emqx_bridge_http_SUITE.erl | 71 +++++++++++++++++-- apps/emqx_resource/include/emqx_resource.hrl | 6 +- .../src/emqx_resource_buffer_worker.erl | 38 +++++++++- .../src/emqx_rule_runtime.erl | 2 +- changes/ce/fix-11306.en.md | 1 + 5 files changed, 110 insertions(+), 8 deletions(-) create mode 100644 changes/ce/fix-11306.en.md diff --git a/apps/emqx_bridge_http/test/emqx_bridge_http_SUITE.erl b/apps/emqx_bridge_http/test/emqx_bridge_http_SUITE.erl index 30a01cf6a..7b1c32bda 100644 --- a/apps/emqx_bridge_http/test/emqx_bridge_http_SUITE.erl +++ b/apps/emqx_bridge_http/test/emqx_bridge_http_SUITE.erl @@ -40,13 +40,13 @@ groups() -> init_per_suite(_Config) -> emqx_common_test_helpers:render_and_load_app_config(emqx_conf), - ok = emqx_mgmt_api_test_util:init_suite([emqx_conf, emqx_bridge]), + ok = emqx_mgmt_api_test_util:init_suite([emqx_conf, emqx_bridge, emqx_rule_engine]), ok = emqx_connector_test_helpers:start_apps([emqx_resource]), {ok, _} = application:ensure_all_started(emqx_connector), []. end_per_suite(_Config) -> - ok = emqx_mgmt_api_test_util:end_suite([emqx_conf, emqx_bridge]), + ok = emqx_mgmt_api_test_util:end_suite([emqx_rule_engine, emqx_bridge, emqx_conf]), ok = emqx_connector_test_helpers:stop_apps([emqx_resource]), _ = application:stop(emqx_connector), _ = application:stop(emqx_bridge), @@ -77,13 +77,19 @@ init_per_testcase(t_too_many_requests, Config) -> ), ok = emqx_bridge_http_connector_test_server:set_handler(too_many_requests_http_handler()), [{http_server, #{port => HTTPPort, path => HTTPPath}} | Config]; +init_per_testcase(t_rule_action_expired, Config) -> + [ + {bridge_name, ?BRIDGE_NAME} + | Config + ]; init_per_testcase(_TestCase, Config) -> Server = start_http_server(#{response_delay_ms => 0}), [{http_server, Server} | Config]. end_per_testcase(TestCase, _Config) when TestCase =:= t_path_not_found; - TestCase =:= t_too_many_requests + TestCase =:= t_too_many_requests; + TestCase =:= t_rule_action_expired -> ok = emqx_bridge_http_connector_test_server:stop(), persistent_term:erase({?MODULE, times_called}), @@ -202,6 +208,7 @@ parse_http_request_assertive(ReqStr0) -> bridge_async_config(#{port := Port} = Config) -> Type = maps:get(type, Config, ?BRIDGE_TYPE), Name = maps:get(name, Config, ?BRIDGE_NAME), + Host = maps:get(host, Config, "localhost"), Path = maps:get(path, Config, ""), PoolSize = maps:get(pool_size, Config, 1), QueryMode = maps:get(query_mode, Config, "async"), @@ -218,7 +225,7 @@ bridge_async_config(#{port := Port} = Config) -> end, ConfigString = io_lib:format( "bridges.~s.~s {\n" - " url = \"http://localhost:~p~s\"\n" + " url = \"http://~s:~p~s\"\n" " connect_timeout = \"~p\"\n" " enable = true\n" %% local_topic @@ -248,6 +255,7 @@ bridge_async_config(#{port := Port} = Config) -> [ Type, Name, + Host, Port, Path, ConnectTimeout, @@ -540,6 +548,61 @@ t_too_many_requests(Config) -> ), ok. +t_rule_action_expired(Config) -> + ?check_trace( + begin + RuleTopic = <<"t/webhook/rule">>, + BridgeConfig = bridge_async_config(#{ + type => ?BRIDGE_TYPE, + name => ?BRIDGE_NAME, + host => "non.existent.host", + port => 9999, + path => <<"/some/path">>, + resume_interval => "100ms", + connect_timeout => "1s", + request_timeout => "100ms", + resource_request_ttl => "100ms" + }), + {ok, _} = emqx_bridge:create(?BRIDGE_TYPE, ?BRIDGE_NAME, BridgeConfig), + {ok, #{<<"id">> := RuleId}} = + emqx_bridge_testlib:create_rule_and_action_http(?BRIDGE_TYPE, RuleTopic, Config), + Msg = emqx_message:make(RuleTopic, <<"timeout">>), + emqx:publish(Msg), + ?retry( + _Interval = 500, + _NAttempts = 20, + ?assertMatch( + #{ + counters := #{ + matched := 1, + failed := 0, + dropped := 1 + } + }, + emqx_bridge:get_metrics(?BRIDGE_TYPE, ?BRIDGE_NAME) + ) + ), + ?retry( + _Interval = 500, + _NAttempts = 20, + ?assertMatch( + #{ + counters := #{ + matched := 1, + 'actions.failed' := 1, + 'actions.failed.unknown' := 1, + 'actions.total' := 1 + } + }, + emqx_metrics_worker:get_metrics(rule_metrics, RuleId) + ) + ), + ok + end, + [] + ), + ok. + %% helpers do_t_async_retries(TestContext, Error, Fn) -> #{error_attempts := ErrorAttempts} = TestContext, diff --git a/apps/emqx_resource/include/emqx_resource.hrl b/apps/emqx_resource/include/emqx_resource.hrl index 10dc001c2..6a90a1e0a 100644 --- a/apps/emqx_resource/include/emqx_resource.hrl +++ b/apps/emqx_resource/include/emqx_resource.hrl @@ -24,7 +24,11 @@ -type callback_mode() :: always_sync | async_if_possible. -type query_mode() :: simple_sync | simple_async | sync | async | no_queries. -type result() :: term(). --type reply_fun() :: {fun((result(), Args :: term()) -> any()), Args :: term()} | undefined. +-type reply_fun() :: + {fun((result(), Args :: term()) -> any()), Args :: term()} + | {fun((result(), Args :: term()) -> any()), Args :: term(), reply_context()} + | undefined. +-type reply_context() :: #{reply_dropped => boolean()}. -type query_opts() :: #{ %% The key used for picking a resource worker pick_key => term(), diff --git a/apps/emqx_resource/src/emqx_resource_buffer_worker.erl b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl index 5f352e181..279a141f5 100644 --- a/apps/emqx_resource/src/emqx_resource_buffer_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl @@ -366,6 +366,7 @@ resume_from_blocked(Data) -> true -> #{dropped_expired => length(Batch)}; false -> #{} end, + batch_reply_dropped(Batch, {error, request_expired}), NData = aggregate_counters(Data, Counters), ?tp(buffer_worker_retry_expired, #{expired => Batch}), resume_from_blocked(NData); @@ -378,6 +379,7 @@ resume_from_blocked(Data) -> {batch, Ref, NotExpired, Expired} -> NumExpired = length(Expired), ok = update_inflight_item(InflightTID, Ref, NotExpired, NumExpired), + batch_reply_dropped(Expired, {error, request_expired}), NData = aggregate_counters(Data, #{dropped_expired => NumExpired}), ?tp(buffer_worker_retry_expired, #{expired => Expired}), %% We retry msgs in inflight window sync, as if we send them @@ -484,6 +486,9 @@ do_reply_caller({F, Args}, {async_return, Result}) -> %% 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({F, Args, _Context}, Result) when is_function(F) -> _ = erlang:apply(F, Args ++ [Result]), ok. @@ -537,11 +542,13 @@ flush(Data0) -> {[], _AllExpired} -> ok = replayq:ack(Q1, QAckRef), NumExpired = length(Batch), + batch_reply_dropped(Batch, {error, request_expired}), Data3 = aggregate_counters(Data2, #{dropped_expired => NumExpired}), ?tp(buffer_worker_flush_all_expired, #{batch => Batch}), flush(Data3); {NotExpired, Expired} -> NumExpired = length(Expired), + batch_reply_dropped(Expired, {error, request_expired}), Data3 = aggregate_counters(Data2, #{dropped_expired => NumExpired}), IsBatch = (BatchSize > 1), %% We *must* use the new queue, because we currently can't @@ -809,6 +816,28 @@ reply_caller_defer_metrics(Id, ?REPLY(ReplyTo, HasBeenSent, Result), QueryOpts) end, {ShouldAck, PostFn, DeltaCounters}. +%% This is basically used only by rule actions. To avoid rule action metrics from +%% becoming inconsistent when we drop messages, we need a way to signal rule engine that +%% this action has reached a conclusion. +-spec reply_dropped(reply_fun(), {error, late_reply | request_expired}) -> ok. +reply_dropped(_ReplyTo = {Fn, Args, #{reply_dropped := true}}, Result) when + is_function(Fn), is_list(Args) +-> + %% We want to avoid bumping metrics inside the buffer worker, since it's costly. + spawn(fun() -> erlang:apply(Fn, Args ++ [Result]) end), + ok; +reply_dropped(_ReplyTo, _Result) -> + ok. + +-spec batch_reply_dropped([queue_query()], {error, late_reply | request_expired}) -> ok. +batch_reply_dropped(Batch, Result) -> + lists:foreach( + fun(?QUERY(ReplyTo, _CoreReq, _HasBeenSent, _ExpireAt)) -> + reply_dropped(ReplyTo, Result) + end, + Batch + ). + %% This is only called by `simple_{,a}sync_query', so we can bump the %% counters here. handle_query_result(Id, Result, HasBeenSent) -> @@ -1164,7 +1193,7 @@ handle_async_reply1( inflight_tid := InflightTID, resource_id := Id, buffer_worker := BufferWorkerPid, - min_query := ?QUERY(_, _, _, ExpireAt) = _Query + min_query := ?QUERY(ReplyTo, _, _, ExpireAt) = _Query } = ReplyContext, Result ) -> @@ -1178,7 +1207,11 @@ handle_async_reply1( IsAcked = ack_inflight(InflightTID, Ref, BufferWorkerPid), %% evalutate metrics call here since we're not inside %% buffer worker - IsAcked andalso emqx_resource_metrics:late_reply_inc(Id), + IsAcked andalso + begin + emqx_resource_metrics:late_reply_inc(Id), + reply_dropped(ReplyTo, {error, late_reply}) + end, ?tp(handle_async_reply_expired, #{expired => [_Query]}), ok; false -> @@ -1292,6 +1325,7 @@ handle_async_batch_reply2([Inflight], ReplyContext, Result, Now) -> %% evalutate metrics call here since we're not inside buffer %% worker emqx_resource_metrics:late_reply_inc(Id, NumExpired), + batch_reply_dropped(RealExpired, {error, late_reply}), case RealNotExpired of [] -> %% all expired, no need to update back the inflight batch diff --git a/apps/emqx_rule_engine/src/emqx_rule_runtime.erl b/apps/emqx_rule_engine/src/emqx_rule_runtime.erl index de1e92a3f..d62803d7e 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_runtime.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_runtime.erl @@ -350,7 +350,7 @@ do_handle_action(RuleId, {bridge, BridgeType, BridgeName, ResId}, Selected, _Env "bridge_action", #{bridge_id => emqx_bridge_resource:bridge_id(BridgeType, BridgeName)} ), - ReplyTo = {fun ?MODULE:inc_action_metrics/2, [RuleId]}, + ReplyTo = {fun ?MODULE:inc_action_metrics/2, [RuleId], #{reply_dropped => true}}, case emqx_bridge:send_message(BridgeType, BridgeName, ResId, Selected, #{reply_to => ReplyTo}) of diff --git a/changes/ce/fix-11306.en.md b/changes/ce/fix-11306.en.md new file mode 100644 index 000000000..519124c3d --- /dev/null +++ b/changes/ce/fix-11306.en.md @@ -0,0 +1 @@ +Fixed rule action metrics inconsistency where dropped requests were not accounted for. From 3b1e436d3f3d72b3d67207d80310e2434f99c5aa Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Thu, 20 Jul 2023 09:40:30 -0300 Subject: [PATCH 03/31] refactor: use `emqx_pool:async_submit` to avoid excessive spawning --- 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 279a141f5..5a08fff06 100644 --- a/apps/emqx_resource/src/emqx_resource_buffer_worker.erl +++ b/apps/emqx_resource/src/emqx_resource_buffer_worker.erl @@ -824,7 +824,7 @@ reply_dropped(_ReplyTo = {Fn, Args, #{reply_dropped := true}}, Result) when is_function(Fn), is_list(Args) -> %% We want to avoid bumping metrics inside the buffer worker, since it's costly. - spawn(fun() -> erlang:apply(Fn, Args ++ [Result]) end), + emqx_pool:async_submit(Fn, Args ++ [Result]), ok; reply_dropped(_ReplyTo, _Result) -> ok. From 85ab97970fb264ca42a38b5c142ac5c1a6a5be8b Mon Sep 17 00:00:00 2001 From: Paulo Zulato Date: Wed, 19 Jul 2023 14:43:49 -0300 Subject: [PATCH 04/31] fix(oracle): fix Pool Size parameter retrieval Fixes https://emqx.atlassian.net/browse/EMQX-10599 --- apps/emqx_bridge_oracle/test/emqx_bridge_oracle_SUITE.erl | 2 ++ apps/emqx_oracle/src/emqx_oracle.app.src | 2 +- apps/emqx_oracle/src/emqx_oracle.erl | 2 +- changes/ee/fix-11316.en.md | 1 + 4 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 changes/ee/fix-11316.en.md diff --git a/apps/emqx_bridge_oracle/test/emqx_bridge_oracle_SUITE.erl b/apps/emqx_bridge_oracle/test/emqx_bridge_oracle_SUITE.erl index 5c6eddb39..6dc3f4711 100644 --- a/apps/emqx_bridge_oracle/test/emqx_bridge_oracle_SUITE.erl +++ b/apps/emqx_bridge_oracle/test/emqx_bridge_oracle_SUITE.erl @@ -479,6 +479,8 @@ t_create_via_http(Config) -> end, [] ), + ResourceId = resource_id(Config), + ?assertMatch(1, length(ecpool:workers(ResourceId))), ok. t_start_stop(Config) -> diff --git a/apps/emqx_oracle/src/emqx_oracle.app.src b/apps/emqx_oracle/src/emqx_oracle.app.src index a5ca822e8..be3ed3276 100644 --- a/apps/emqx_oracle/src/emqx_oracle.app.src +++ b/apps/emqx_oracle/src/emqx_oracle.app.src @@ -1,6 +1,6 @@ {application, emqx_oracle, [ {description, "EMQX Enterprise Oracle Database Connector"}, - {vsn, "0.1.3"}, + {vsn, "0.1.4"}, {registered, []}, {applications, [ kernel, diff --git a/apps/emqx_oracle/src/emqx_oracle.erl b/apps/emqx_oracle/src/emqx_oracle.erl index 5a7f8d752..d379b7cf7 100644 --- a/apps/emqx_oracle/src/emqx_oracle.erl +++ b/apps/emqx_oracle/src/emqx_oracle.erl @@ -94,7 +94,7 @@ on_start( {password, jamdb_secret:wrap(maps:get(password, Config, ""))}, {sid, emqx_utils_conv:str(Sid)}, {service_name, ServiceName}, - {pool_size, maps:get(<<"pool_size">>, Config, ?DEFAULT_POOL_SIZE)}, + {pool_size, maps:get(pool_size, Config, ?DEFAULT_POOL_SIZE)}, {timeout, ?OPT_TIMEOUT}, {app_name, "EMQX Data To Oracle Database Action"} ], diff --git a/changes/ee/fix-11316.en.md b/changes/ee/fix-11316.en.md new file mode 100644 index 000000000..671e61048 --- /dev/null +++ b/changes/ee/fix-11316.en.md @@ -0,0 +1 @@ +Fixed Pool Size value not being considered in Oracle Bridge. From 29432009db0c3b813d97d0fc21c73a0e22c80d7d Mon Sep 17 00:00:00 2001 From: Ivan Dyachkov Date: Fri, 21 Jul 2023 18:42:14 +0200 Subject: [PATCH 05/31] chore: 5.1.1-alpha.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 2412de99e..991368d84 100644 --- a/apps/emqx/include/emqx_release.hrl +++ b/apps/emqx/include/emqx_release.hrl @@ -35,7 +35,7 @@ -define(EMQX_RELEASE_CE, "5.1.1"). %% Enterprise edition --define(EMQX_RELEASE_EE, "5.1.1-alpha.2"). +-define(EMQX_RELEASE_EE, "5.1.1-alpha.4"). %% The HTTP API version -define(EMQX_API_VERSION, "5.0"). From daa364955fa3a44c15b3013699e87a82988d5c65 Mon Sep 17 00:00:00 2001 From: firest Date: Sat, 22 Jul 2023 17:47:15 +0800 Subject: [PATCH 06/31] chore(dynamo): fix default template example --- rel/i18n/emqx_bridge_dynamo.hocon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rel/i18n/emqx_bridge_dynamo.hocon b/rel/i18n/emqx_bridge_dynamo.hocon index 899a47c75..4a07ebb7f 100644 --- a/rel/i18n/emqx_bridge_dynamo.hocon +++ b/rel/i18n/emqx_bridge_dynamo.hocon @@ -37,7 +37,7 @@ local_topic.label: template.desc: """Template, the default value is empty. When this value is empty the whole message will be stored in the database.
The template can be any valid json with placeholders and make sure all keys for table are here, example:
- {"id" : ${id}, "clientid" : ${clientid}, "data" : ${payload}}""" + {"id" : "${id}", "clientid" : "${clientid}", "data" : "${payload.data}"}""" template.label: """Template""" From 28dad5d7a91a57d7ac06d8337b6e0d116531d468 Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Tue, 18 Jul 2023 23:11:04 +0200 Subject: [PATCH 07/31] feat(index): add topic index facility Somewhat similar to `emqx_trie` in design and logic, yet built on top of a single, potentially pre-existing table. --- apps/emqx/src/emqx_topic_index.erl | 156 ++++++++++++++++++++++ apps/emqx/test/emqx_topic_index_SUITE.erl | 143 ++++++++++++++++++++ 2 files changed, 299 insertions(+) create mode 100644 apps/emqx/src/emqx_topic_index.erl create mode 100644 apps/emqx/test/emqx_topic_index_SUITE.erl diff --git a/apps/emqx/src/emqx_topic_index.erl b/apps/emqx/src/emqx_topic_index.erl new file mode 100644 index 000000000..9f0b5fba1 --- /dev/null +++ b/apps/emqx/src/emqx_topic_index.erl @@ -0,0 +1,156 @@ +%%-------------------------------------------------------------------- +%% 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. +%%-------------------------------------------------------------------- + +%% @doc Topic index for matching topics to topic filters. +%% +%% Works on top of ETS ordered_set table. Keys are parsed topic filters +%% with record ID appended to the end, wrapped in a tuple to disambiguate from +%% topic filter words. Existing table may be used if existing keys will not +%% collide with index keys. +%% +%% Designed to effectively answer questions like: +%% 1. Does any topic filter match given topic? +%% 2. Which records are associated with topic filters matching given topic? +%% +%% Questions like these are _only slightly_ less effective: +%% 1. Which topic filters match given topic? +%% 2. Which record IDs are associated with topic filters matching given topic? + +-module(emqx_topic_index). + +-export([new/0]). +-export([insert/4]). +-export([delete/3]). +-export([match/2]). +-export([matches/2]). + +-export([get_id/1]). +-export([get_topic/1]). +-export([get_record/2]). + +-type key(ID) :: [binary() | '+' | '#' | {ID}]. +-type match(ID) :: key(ID). + +new() -> + ets:new(?MODULE, [public, ordered_set, {write_concurrency, true}]). + +insert(Filter, ID, Record, Tab) -> + ets:insert(Tab, {emqx_topic:words(Filter) ++ [{ID}], Record}). + +delete(Filter, ID, Tab) -> + ets:delete(Tab, emqx_topic:words(Filter) ++ [{ID}]). + +-spec match(emqx_types:topic(), ets:table()) -> match(_ID) | false. +match(Topic, Tab) -> + {Words, RPrefix} = match_init(Topic), + match(Words, RPrefix, Tab). + +match(Words, RPrefix, Tab) -> + Prefix = lists:reverse(RPrefix), + K = ets:next(Tab, Prefix), + case match_filter(Prefix, K, Words =/= []) of + true -> + K; + stop -> + false; + Matched -> + match_rest(Matched, Words, RPrefix, Tab) + end. + +match_rest(false, [W | Rest], RPrefix, Tab) -> + match(Rest, [W | RPrefix], Tab); +match_rest(plus, [W | Rest], RPrefix, Tab) -> + case match(Rest, ['+' | RPrefix], Tab) of + Match when is_list(Match) -> + Match; + false -> + match(Rest, [W | RPrefix], Tab) + end; +match_rest(_, [], _RPrefix, _Tab) -> + false. + +-spec matches(emqx_types:topic(), ets:table()) -> [match(_ID)]. +matches(Topic, Tab) -> + {Words, RPrefix} = match_init(Topic), + matches(Words, RPrefix, Tab). + +matches(Words, RPrefix, Tab) -> + Prefix = lists:reverse(RPrefix), + matches(ets:next(Tab, Prefix), Prefix, Words, RPrefix, Tab). + +matches(K, Prefix, Words, RPrefix, Tab) -> + case match_filter(Prefix, K, Words =/= []) of + true -> + [K | matches(ets:next(Tab, K), Prefix, Words, RPrefix, Tab)]; + stop -> + []; + Matched -> + matches_rest(Matched, Words, RPrefix, Tab) + end. + +matches_rest(false, [W | Rest], RPrefix, Tab) -> + matches(Rest, [W | RPrefix], Tab); +matches_rest(plus, [W | Rest], RPrefix, Tab) -> + matches(Rest, ['+' | RPrefix], Tab) ++ matches(Rest, [W | RPrefix], Tab); +matches_rest(_, [], _RPrefix, _Tab) -> + []. + +match_filter([], [{_ID}], _IsPrefix = false) -> + % NOTE: exact match is `true` only if we match whole topic, not prefix + true; +match_filter([], ['#', {_ID}], _IsPrefix) -> + % NOTE: naturally, '#' < '+', so this is already optimal for `match/2` + true; +match_filter([], ['+' | _], _) -> + plus; +match_filter([], [_H | _], _) -> + false; +match_filter([H | T1], [H | T2], IsPrefix) -> + match_filter(T1, T2, IsPrefix); +match_filter([H1 | _], [H2 | _], _) when H2 > H1 -> + % NOTE: we're strictly past the prefix, no need to continue + stop; +match_filter(_, '$end_of_table', _) -> + stop. + +match_init(Topic) -> + case emqx_topic:words(Topic) of + [W = <<"$", _/bytes>> | Rest] -> + % NOTE + % This will effectively skip attempts to match special topics to `#` or `+/...`. + {Rest, [W]}; + Words -> + {Words, []} + end. + +-spec get_id(match(ID)) -> ID. +get_id([{ID}]) -> + ID; +get_id([_ | Rest]) -> + get_id(Rest). + +-spec get_topic(match(_ID)) -> emqx_types:topic(). +get_topic(K) -> + emqx_topic:join(cut_topic(K)). + +cut_topic([{_ID}]) -> + []; +cut_topic([W | Rest]) -> + [W | cut_topic(Rest)]. + +-spec get_record(match(_ID), ets:table()) -> _Record. +get_record(K, Tab) -> + ets:lookup_element(Tab, K, 2). diff --git a/apps/emqx/test/emqx_topic_index_SUITE.erl b/apps/emqx/test/emqx_topic_index_SUITE.erl new file mode 100644 index 000000000..98bfe48a1 --- /dev/null +++ b/apps/emqx/test/emqx_topic_index_SUITE.erl @@ -0,0 +1,143 @@ +%%-------------------------------------------------------------------- +%% 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_topic_index_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("eunit/include/eunit.hrl"). + +all() -> + emqx_common_test_helpers:all(?MODULE). + +t_insert(_) -> + Tab = emqx_topic_index:new(), + true = emqx_topic_index:insert(<<"sensor/1/metric/2">>, t_insert_1, <<>>, Tab), + true = emqx_topic_index:insert(<<"sensor/+/#">>, t_insert_2, <<>>, Tab), + true = emqx_topic_index:insert(<<"sensor/#">>, t_insert_3, <<>>, Tab), + ?assertEqual(<<"sensor/#">>, topic(match(<<"sensor">>, Tab))), + ?assertEqual(t_insert_3, id(match(<<"sensor">>, Tab))). + +t_match(_) -> + Tab = emqx_topic_index:new(), + true = emqx_topic_index:insert(<<"sensor/1/metric/2">>, t_match_1, <<>>, Tab), + true = emqx_topic_index:insert(<<"sensor/+/#">>, t_match_2, <<>>, Tab), + true = emqx_topic_index:insert(<<"sensor/#">>, t_match_3, <<>>, Tab), + ?assertMatch( + [<<"sensor/#">>, <<"sensor/+/#">>], + [topic(M) || M <- matches(<<"sensor/1">>, Tab)] + ). + +t_match2(_) -> + Tab = emqx_topic_index:new(), + true = emqx_topic_index:insert(<<"#">>, t_match2_1, <<>>, Tab), + true = emqx_topic_index:insert(<<"+/#">>, t_match2_2, <<>>, Tab), + true = emqx_topic_index:insert(<<"+/+/#">>, t_match2_3, <<>>, Tab), + ?assertEqual( + [<<"#">>, <<"+/#">>, <<"+/+/#">>], + [topic(M) || M <- matches(<<"a/b/c">>, Tab)] + ), + ?assertEqual( + false, + emqx_topic_index:match(<<"$SYS/broker/zenmq">>, Tab) + ). + +t_match3(_) -> + Tab = emqx_topic_index:new(), + Records = [ + {<<"d/#">>, t_match3_1}, + {<<"a/b/+">>, t_match3_2}, + {<<"a/#">>, t_match3_3}, + {<<"#">>, t_match3_4}, + {<<"$SYS/#">>, t_match3_sys} + ], + lists:foreach( + fun({Topic, ID}) -> emqx_topic_index:insert(Topic, ID, <<>>, Tab) end, + Records + ), + Matched = matches(<<"a/b/c">>, Tab), + case length(Matched) of + 3 -> ok; + _ -> error({unexpected, Matched}) + end, + ?assertEqual( + t_match3_sys, + id(match(<<"$SYS/a/b/c">>, Tab)) + ). + +t_match4(_) -> + Tab = emqx_topic_index:new(), + Records = [{<<"/#">>, t_match4_1}, {<<"/+">>, t_match4_2}, {<<"/+/a/b/c">>, t_match4_3}], + lists:foreach( + fun({Topic, ID}) -> emqx_topic_index:insert(Topic, ID, <<>>, Tab) end, + Records + ), + ?assertEqual( + [<<"/#">>, <<"/+">>], + [topic(M) || M <- matches(<<"/">>, Tab)] + ), + ?assertEqual( + [<<"/#">>, <<"/+/a/b/c">>], + [topic(M) || M <- matches(<<"/0/a/b/c">>, Tab)] + ). + +t_match5(_) -> + Tab = emqx_topic_index:new(), + T = <<"a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z">>, + Records = [ + {<<"#">>, t_match5_1}, + {<>, t_match5_2}, + {<>, t_match5_3} + ], + lists:foreach( + fun({Topic, ID}) -> emqx_topic_index:insert(Topic, ID, <<>>, Tab) end, + Records + ), + ?assertEqual( + [<<"#">>, <>], + [topic(M) || M <- matches(T, Tab)] + ), + ?assertEqual( + [<<"#">>, <>, <>], + [topic(M) || M <- matches(<>, Tab)] + ). + +t_match6(_) -> + Tab = emqx_topic_index:new(), + T = <<"a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z">>, + W = <<"+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/#">>, + emqx_topic_index:insert(W, ID = t_match6, <<>>, Tab), + ?assertEqual(ID, id(match(T, Tab))). + +t_match7(_) -> + Tab = emqx_topic_index:new(), + T = <<"a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z">>, + W = <<"a/+/c/+/e/+/g/+/i/+/k/+/m/+/o/+/q/+/s/+/u/+/w/+/y/+/#">>, + emqx_topic_index:insert(W, t_match7, <<>>, Tab), + ?assertEqual(W, topic(match(T, Tab))). + +match(T, Tab) -> + emqx_topic_index:match(T, Tab). + +matches(T, Tab) -> + lists:sort(emqx_topic_index:matches(T, Tab)). + +id(Match) -> + emqx_topic_index:get_id(Match). + +topic(Match) -> + emqx_topic_index:get_topic(Match). From b821bdee00aea7cb1014f57388a643c365b8ad25 Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Thu, 20 Jul 2023 14:49:10 +0200 Subject: [PATCH 08/31] perf(ruleeng): employ `emqx_topic_index` to speed up topic matching --- apps/emqx_rule_engine/include/rule_engine.hrl | 1 + .../emqx_rule_engine/src/emqx_rule_engine.erl | 68 +++++++++++++------ .../src/emqx_rule_engine_app.erl | 1 + 3 files changed, 49 insertions(+), 21 deletions(-) diff --git a/apps/emqx_rule_engine/include/rule_engine.hrl b/apps/emqx_rule_engine/include/rule_engine.hrl index b2a6a549e..7df5d9941 100644 --- a/apps/emqx_rule_engine/include/rule_engine.hrl +++ b/apps/emqx_rule_engine/include/rule_engine.hrl @@ -109,6 +109,7 @@ %% Tables -define(RULE_TAB, emqx_rule_engine). +-define(RULE_TOPIC_INDEX, emqx_rule_engine_topic_index). %% Allowed sql function provider modules -define(DEFAULT_SQL_FUNC_PROVIDER, emqx_rule_funcs). diff --git a/apps/emqx_rule_engine/src/emqx_rule_engine.erl b/apps/emqx_rule_engine/src/emqx_rule_engine.erl index 66c82d3a1..9d2d918ae 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_engine.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_engine.erl @@ -176,7 +176,7 @@ create_rule(Params) -> create_rule(Params = #{id := RuleId}, CreatedAt) when is_binary(RuleId) -> case get_rule(RuleId) of - not_found -> parse_and_insert(Params, CreatedAt); + not_found -> with_parsed_rule(Params, CreatedAt, fun insert_rule/1); {ok, _} -> {error, already_exists} end. @@ -185,18 +185,27 @@ update_rule(Params = #{id := RuleId}) when is_binary(RuleId) -> case get_rule(RuleId) of not_found -> {error, not_found}; - {ok, #{created_at := CreatedAt}} -> - parse_and_insert(Params, CreatedAt) + {ok, RulePrev = #{created_at := CreatedAt}} -> + with_parsed_rule(Params, CreatedAt, fun(Rule) -> update_rule(Rule, RulePrev) end) end. -spec delete_rule(RuleId :: rule_id()) -> ok. delete_rule(RuleId) when is_binary(RuleId) -> - gen_server:call(?RULE_ENGINE, {delete_rule, RuleId}, ?T_CALL). + case get_rule(RuleId) of + not_found -> + ok; + {ok, Rule} -> + gen_server:call(?RULE_ENGINE, {delete_rule, Rule}, ?T_CALL) + end. -spec insert_rule(Rule :: rule()) -> ok. insert_rule(Rule) -> gen_server:call(?RULE_ENGINE, {insert_rule, Rule}, ?T_CALL). +-spec update_rule(Rule :: rule(), RulePrev :: rule()) -> ok. +update_rule(Rule, RulePrev) -> + gen_server:call(?RULE_ENGINE, {update_rule, Rule, RulePrev}, ?T_CALL). + %%---------------------------------------------------------------------------------------- %% Rule Management %%---------------------------------------------------------------------------------------- @@ -216,9 +225,8 @@ get_rules_ordered_by_ts() -> -spec get_rules_for_topic(Topic :: binary()) -> [rule()]. get_rules_for_topic(Topic) -> [ - Rule - || Rule = #{from := From} <- get_rules(), - emqx_topic:match_any(Topic, From) + emqx_topic_index:get_record(M, ?RULE_TOPIC_INDEX) + || M <- emqx_topic_index:matches(Topic, ?RULE_TOPIC_INDEX) ]. -spec get_rules_with_same_event(Topic :: binary()) -> [rule()]. @@ -411,10 +419,17 @@ init([]) -> {ok, #{}}. handle_call({insert_rule, Rule}, _From, State) -> - do_insert_rule(Rule), + ok = do_insert_rule(Rule), + ok = do_update_rule_index(Rule), + {reply, ok, State}; +handle_call({update_rule, Rule, RulePrev}, _From, State) -> + ok = do_delete_rule_index(RulePrev), + ok = do_insert_rule(Rule), + ok = do_update_rule_index(Rule), {reply, ok, State}; handle_call({delete_rule, Rule}, _From, State) -> - do_delete_rule(Rule), + ok = do_delete_rule_index(Rule), + ok = do_delete_rule(Rule), {reply, ok, State}; handle_call(Req, _From, State) -> ?SLOG(error, #{msg => "unexpected_call", request => Req}), @@ -438,7 +453,7 @@ code_change(_OldVsn, State, _Extra) -> %% Internal Functions %%---------------------------------------------------------------------------------------- -parse_and_insert(Params = #{id := RuleId, sql := Sql, actions := Actions}, CreatedAt) -> +with_parsed_rule(Params = #{id := RuleId, sql := Sql, actions := Actions}, CreatedAt, Fun) -> case emqx_rule_sqlparser:parse(Sql) of {ok, Select} -> Rule = #{ @@ -459,7 +474,7 @@ parse_and_insert(Params = #{id := RuleId, sql := Sql, actions := Actions}, Creat conditions => emqx_rule_sqlparser:select_where(Select) %% -- calculated fields end }, - ok = insert_rule(Rule), + ok = Fun(Rule), {ok, Rule}; {error, Reason} -> {error, Reason} @@ -471,16 +486,27 @@ do_insert_rule(#{id := Id} = Rule) -> true = ets:insert(?RULE_TAB, {Id, maps:remove(id, Rule)}), ok. -do_delete_rule(RuleId) -> - case get_rule(RuleId) of - {ok, Rule} -> - ok = unload_hooks_for_rule(Rule), - ok = clear_metrics_for_rule(RuleId), - true = ets:delete(?RULE_TAB, RuleId), - ok; - not_found -> - ok - end. +do_delete_rule(#{id := Id} = Rule) -> + ok = unload_hooks_for_rule(Rule), + ok = clear_metrics_for_rule(Id), + true = ets:delete(?RULE_TAB, Id), + ok. + +do_update_rule_index(#{id := Id, from := From} = Rule) -> + ok = lists:foreach( + fun(Topic) -> + true = emqx_topic_index:insert(Topic, Id, Rule, ?RULE_TOPIC_INDEX) + end, + From + ). + +do_delete_rule_index(#{id := Id, from := From}) -> + ok = lists:foreach( + fun(Topic) -> + true = emqx_topic_index:delete(Topic, Id, ?RULE_TOPIC_INDEX) + end, + From + ). parse_actions(Actions) -> [do_parse_action(Act) || Act <- Actions]. 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 d8b031bdd..28515cb1a 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_engine_app.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_engine_app.erl @@ -26,6 +26,7 @@ start(_Type, _Args) -> _ = ets:new(?RULE_TAB, [named_table, public, ordered_set, {read_concurrency, true}]), + _ = ets:new(?RULE_TOPIC_INDEX, [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 4e4b1ac11570b862835203b722c988f1dde6238f Mon Sep 17 00:00:00 2001 From: JimMoen Date: Fri, 21 Jul 2023 17:58:54 +0800 Subject: [PATCH 09/31] refactor: module move to app emqx_rule_engine - Rename to emqx_rule_index.erl - Remove test funcs from src -> test dir --- .../emqx_rule_engine/src/emqx_rule_engine.erl | 11 ++- .../src/emqx_rule_index.erl} | 26 +---- .../test/emqx_rule_index_SUITE.erl} | 97 +++++++++++-------- changes/ce/perf-11282.en.md | 1 + 4 files changed, 69 insertions(+), 66 deletions(-) rename apps/{emqx/src/emqx_topic_index.erl => emqx_rule_engine/src/emqx_rule_index.erl} (90%) rename apps/{emqx/test/emqx_topic_index_SUITE.erl => emqx_rule_engine/test/emqx_rule_index_SUITE.erl} (52%) create mode 100644 changes/ce/perf-11282.en.md diff --git a/apps/emqx_rule_engine/src/emqx_rule_engine.erl b/apps/emqx_rule_engine/src/emqx_rule_engine.erl index 9d2d918ae..d92931d77 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_engine.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_engine.erl @@ -225,12 +225,13 @@ get_rules_ordered_by_ts() -> -spec get_rules_for_topic(Topic :: binary()) -> [rule()]. get_rules_for_topic(Topic) -> [ - emqx_topic_index:get_record(M, ?RULE_TOPIC_INDEX) - || M <- emqx_topic_index:matches(Topic, ?RULE_TOPIC_INDEX) + emqx_rule_index:get_record(M, ?RULE_TOPIC_INDEX) + || M <- emqx_rule_index:matches(Topic, ?RULE_TOPIC_INDEX) ]. -spec get_rules_with_same_event(Topic :: binary()) -> [rule()]. get_rules_with_same_event(Topic) -> + %% TODO: event matching index not implemented yet EventName = emqx_rule_events:event_name(Topic), [ Rule @@ -240,6 +241,7 @@ get_rules_with_same_event(Topic) -> -spec get_rule_ids_by_action(action_name()) -> [rule_id()]. get_rule_ids_by_action(BridgeId) when is_binary(BridgeId) -> + %% TODO: bridge matching index not implemented yet [ Id || #{actions := Acts, id := Id, from := Froms} <- get_rules(), @@ -247,6 +249,7 @@ get_rule_ids_by_action(BridgeId) when is_binary(BridgeId) -> references_ingress_bridge(Froms, BridgeId) ]; get_rule_ids_by_action(#{function := FuncName}) when is_binary(FuncName) -> + %% TODO: action id matching index not implemented yet {Mod, Fun} = case string:split(FuncName, ":", leading) of [M, F] -> {binary_to_module(M), F}; @@ -495,7 +498,7 @@ do_delete_rule(#{id := Id} = Rule) -> do_update_rule_index(#{id := Id, from := From} = Rule) -> ok = lists:foreach( fun(Topic) -> - true = emqx_topic_index:insert(Topic, Id, Rule, ?RULE_TOPIC_INDEX) + true = emqx_rule_index:insert(Topic, Id, Rule, ?RULE_TOPIC_INDEX) end, From ). @@ -503,7 +506,7 @@ do_update_rule_index(#{id := Id, from := From} = Rule) -> do_delete_rule_index(#{id := Id, from := From}) -> ok = lists:foreach( fun(Topic) -> - true = emqx_topic_index:delete(Topic, Id, ?RULE_TOPIC_INDEX) + true = emqx_rule_index:delete(Topic, Id, ?RULE_TOPIC_INDEX) end, From ). diff --git a/apps/emqx/src/emqx_topic_index.erl b/apps/emqx_rule_engine/src/emqx_rule_index.erl similarity index 90% rename from apps/emqx/src/emqx_topic_index.erl rename to apps/emqx_rule_engine/src/emqx_rule_index.erl index 9f0b5fba1..9c16bf3ad 100644 --- a/apps/emqx/src/emqx_topic_index.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_index.erl @@ -29,25 +29,24 @@ %% 1. Which topic filters match given topic? %% 2. Which record IDs are associated with topic filters matching given topic? --module(emqx_topic_index). +-module(emqx_rule_index). --export([new/0]). -export([insert/4]). -export([delete/3]). -export([match/2]). -export([matches/2]). --export([get_id/1]). --export([get_topic/1]). -export([get_record/2]). -type key(ID) :: [binary() | '+' | '#' | {ID}]. -type match(ID) :: key(ID). -new() -> - ets:new(?MODULE, [public, ordered_set, {write_concurrency, true}]). +-ifdef(TEST). +-export_type([match/1]). +-endif. insert(Filter, ID, Record, Tab) -> + %% TODO: topic compact. see also in emqx_trie.erl ets:insert(Tab, {emqx_topic:words(Filter) ++ [{ID}], Record}). delete(Filter, ID, Tab) -> @@ -136,21 +135,6 @@ match_init(Topic) -> {Words, []} end. --spec get_id(match(ID)) -> ID. -get_id([{ID}]) -> - ID; -get_id([_ | Rest]) -> - get_id(Rest). - --spec get_topic(match(_ID)) -> emqx_types:topic(). -get_topic(K) -> - emqx_topic:join(cut_topic(K)). - -cut_topic([{_ID}]) -> - []; -cut_topic([W | Rest]) -> - [W | cut_topic(Rest)]. - -spec get_record(match(_ID), ets:table()) -> _Record. get_record(K, Tab) -> ets:lookup_element(Tab, K, 2). diff --git a/apps/emqx/test/emqx_topic_index_SUITE.erl b/apps/emqx_rule_engine/test/emqx_rule_index_SUITE.erl similarity index 52% rename from apps/emqx/test/emqx_topic_index_SUITE.erl rename to apps/emqx_rule_engine/test/emqx_rule_index_SUITE.erl index 98bfe48a1..c4b1b4848 100644 --- a/apps/emqx/test/emqx_topic_index_SUITE.erl +++ b/apps/emqx_rule_engine/test/emqx_rule_index_SUITE.erl @@ -14,7 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_topic_index_SUITE). +-module(emqx_rule_index_SUITE). -compile(export_all). -compile(nowarn_export_all). @@ -25,39 +25,39 @@ all() -> emqx_common_test_helpers:all(?MODULE). t_insert(_) -> - Tab = emqx_topic_index:new(), - true = emqx_topic_index:insert(<<"sensor/1/metric/2">>, t_insert_1, <<>>, Tab), - true = emqx_topic_index:insert(<<"sensor/+/#">>, t_insert_2, <<>>, Tab), - true = emqx_topic_index:insert(<<"sensor/#">>, t_insert_3, <<>>, Tab), - ?assertEqual(<<"sensor/#">>, topic(match(<<"sensor">>, Tab))), - ?assertEqual(t_insert_3, id(match(<<"sensor">>, Tab))). + Tab = new(), + true = emqx_rule_index:insert(<<"sensor/1/metric/2">>, t_insert_1, <<>>, Tab), + true = emqx_rule_index:insert(<<"sensor/+/#">>, t_insert_2, <<>>, Tab), + true = emqx_rule_index:insert(<<"sensor/#">>, t_insert_3, <<>>, Tab), + ?assertEqual(<<"sensor/#">>, get_topic(match(<<"sensor">>, Tab))), + ?assertEqual(t_insert_3, get_id(match(<<"sensor">>, Tab))). t_match(_) -> - Tab = emqx_topic_index:new(), - true = emqx_topic_index:insert(<<"sensor/1/metric/2">>, t_match_1, <<>>, Tab), - true = emqx_topic_index:insert(<<"sensor/+/#">>, t_match_2, <<>>, Tab), - true = emqx_topic_index:insert(<<"sensor/#">>, t_match_3, <<>>, Tab), + Tab = new(), + true = emqx_rule_index:insert(<<"sensor/1/metric/2">>, t_match_1, <<>>, Tab), + true = emqx_rule_index:insert(<<"sensor/+/#">>, t_match_2, <<>>, Tab), + true = emqx_rule_index:insert(<<"sensor/#">>, t_match_3, <<>>, Tab), ?assertMatch( [<<"sensor/#">>, <<"sensor/+/#">>], - [topic(M) || M <- matches(<<"sensor/1">>, Tab)] + [get_topic(M) || M <- matches(<<"sensor/1">>, Tab)] ). t_match2(_) -> - Tab = emqx_topic_index:new(), - true = emqx_topic_index:insert(<<"#">>, t_match2_1, <<>>, Tab), - true = emqx_topic_index:insert(<<"+/#">>, t_match2_2, <<>>, Tab), - true = emqx_topic_index:insert(<<"+/+/#">>, t_match2_3, <<>>, Tab), + Tab = new(), + true = emqx_rule_index:insert(<<"#">>, t_match2_1, <<>>, Tab), + true = emqx_rule_index:insert(<<"+/#">>, t_match2_2, <<>>, Tab), + true = emqx_rule_index:insert(<<"+/+/#">>, t_match2_3, <<>>, Tab), ?assertEqual( [<<"#">>, <<"+/#">>, <<"+/+/#">>], - [topic(M) || M <- matches(<<"a/b/c">>, Tab)] + [get_topic(M) || M <- matches(<<"a/b/c">>, Tab)] ), ?assertEqual( false, - emqx_topic_index:match(<<"$SYS/broker/zenmq">>, Tab) + emqx_rule_index:match(<<"$SYS/broker/zenmq">>, Tab) ). t_match3(_) -> - Tab = emqx_topic_index:new(), + Tab = new(), Records = [ {<<"d/#">>, t_match3_1}, {<<"a/b/+">>, t_match3_2}, @@ -66,7 +66,7 @@ t_match3(_) -> {<<"$SYS/#">>, t_match3_sys} ], lists:foreach( - fun({Topic, ID}) -> emqx_topic_index:insert(Topic, ID, <<>>, Tab) end, + fun({Topic, ID}) -> emqx_rule_index:insert(Topic, ID, <<>>, Tab) end, Records ), Matched = matches(<<"a/b/c">>, Tab), @@ -76,27 +76,27 @@ t_match3(_) -> end, ?assertEqual( t_match3_sys, - id(match(<<"$SYS/a/b/c">>, Tab)) + get_id(match(<<"$SYS/a/b/c">>, Tab)) ). t_match4(_) -> - Tab = emqx_topic_index:new(), + Tab = new(), Records = [{<<"/#">>, t_match4_1}, {<<"/+">>, t_match4_2}, {<<"/+/a/b/c">>, t_match4_3}], lists:foreach( - fun({Topic, ID}) -> emqx_topic_index:insert(Topic, ID, <<>>, Tab) end, + fun({Topic, ID}) -> emqx_rule_index:insert(Topic, ID, <<>>, Tab) end, Records ), ?assertEqual( [<<"/#">>, <<"/+">>], - [topic(M) || M <- matches(<<"/">>, Tab)] + [get_topic(M) || M <- matches(<<"/">>, Tab)] ), ?assertEqual( [<<"/#">>, <<"/+/a/b/c">>], - [topic(M) || M <- matches(<<"/0/a/b/c">>, Tab)] + [get_topic(M) || M <- matches(<<"/0/a/b/c">>, Tab)] ). t_match5(_) -> - Tab = emqx_topic_index:new(), + Tab = new(), T = <<"a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z">>, Records = [ {<<"#">>, t_match5_1}, @@ -104,40 +104,55 @@ t_match5(_) -> {<>, t_match5_3} ], lists:foreach( - fun({Topic, ID}) -> emqx_topic_index:insert(Topic, ID, <<>>, Tab) end, + fun({Topic, ID}) -> emqx_rule_index:insert(Topic, ID, <<>>, Tab) end, Records ), ?assertEqual( [<<"#">>, <>], - [topic(M) || M <- matches(T, Tab)] + [get_topic(M) || M <- matches(T, Tab)] ), ?assertEqual( [<<"#">>, <>, <>], - [topic(M) || M <- matches(<>, Tab)] + [get_topic(M) || M <- matches(<>, Tab)] ). t_match6(_) -> - Tab = emqx_topic_index:new(), + Tab = new(), T = <<"a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z">>, W = <<"+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/#">>, - emqx_topic_index:insert(W, ID = t_match6, <<>>, Tab), - ?assertEqual(ID, id(match(T, Tab))). + emqx_rule_index:insert(W, ID = t_match6, <<>>, Tab), + ?assertEqual(ID, get_id(match(T, Tab))). t_match7(_) -> - Tab = emqx_topic_index:new(), + Tab = new(), T = <<"a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z">>, W = <<"a/+/c/+/e/+/g/+/i/+/k/+/m/+/o/+/q/+/s/+/u/+/w/+/y/+/#">>, - emqx_topic_index:insert(W, t_match7, <<>>, Tab), - ?assertEqual(W, topic(match(T, Tab))). + emqx_rule_index:insert(W, t_match7, <<>>, Tab), + ?assertEqual(W, get_topic(match(T, Tab))). + +new() -> + ets:new(?MODULE, [public, ordered_set, {write_concurrency, true}]). match(T, Tab) -> - emqx_topic_index:match(T, Tab). + emqx_rule_index:match(T, Tab). matches(T, Tab) -> - lists:sort(emqx_topic_index:matches(T, Tab)). + lists:sort(emqx_rule_index:matches(T, Tab)). -id(Match) -> - emqx_topic_index:get_id(Match). +-spec get_id(emqx_rule_index:match(ID)) -> ID. +get_id([{ID}]) -> + ID; +get_id([_ | Rest]) -> + get_id(Rest). -topic(Match) -> - emqx_topic_index:get_topic(Match). +-spec get_topic(emqx_rule_index:match(_ID)) -> emqx_types:topic(). +get_topic(K) -> + emqx_topic:join(cut_topic(K)). + +cut_topic(K) -> + cut_topic(K, []). + +cut_topic([{_ID}], Acc) -> + lists:reverse(Acc); +cut_topic([W | Rest], Acc) -> + cut_topic(Rest, [W | Acc]). diff --git a/changes/ce/perf-11282.en.md b/changes/ce/perf-11282.en.md new file mode 100644 index 000000000..107889957 --- /dev/null +++ b/changes/ce/perf-11282.en.md @@ -0,0 +1 @@ +Added indexing to the rule engine's topic matching to improve rule search performance. From c393c2e091aca847a9473ff848cf8f8d15f6c96f Mon Sep 17 00:00:00 2001 From: JimMoen Date: Fri, 21 Jul 2023 19:42:02 +0800 Subject: [PATCH 10/31] test: ets table cleanup after cases --- .../test/emqx_rule_index_SUITE.erl | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/apps/emqx_rule_engine/test/emqx_rule_index_SUITE.erl b/apps/emqx_rule_engine/test/emqx_rule_index_SUITE.erl index c4b1b4848..cf4b67cd4 100644 --- a/apps/emqx_rule_engine/test/emqx_rule_index_SUITE.erl +++ b/apps/emqx_rule_engine/test/emqx_rule_index_SUITE.erl @@ -30,7 +30,8 @@ t_insert(_) -> true = emqx_rule_index:insert(<<"sensor/+/#">>, t_insert_2, <<>>, Tab), true = emqx_rule_index:insert(<<"sensor/#">>, t_insert_3, <<>>, Tab), ?assertEqual(<<"sensor/#">>, get_topic(match(<<"sensor">>, Tab))), - ?assertEqual(t_insert_3, get_id(match(<<"sensor">>, Tab))). + ?assertEqual(t_insert_3, get_id(match(<<"sensor">>, Tab))), + true = ets:delete(Tab). t_match(_) -> Tab = new(), @@ -40,7 +41,8 @@ t_match(_) -> ?assertMatch( [<<"sensor/#">>, <<"sensor/+/#">>], [get_topic(M) || M <- matches(<<"sensor/1">>, Tab)] - ). + ), + true = ets:delete(Tab). t_match2(_) -> Tab = new(), @@ -54,7 +56,8 @@ t_match2(_) -> ?assertEqual( false, emqx_rule_index:match(<<"$SYS/broker/zenmq">>, Tab) - ). + ), + true = ets:delete(Tab). t_match3(_) -> Tab = new(), @@ -77,7 +80,8 @@ t_match3(_) -> ?assertEqual( t_match3_sys, get_id(match(<<"$SYS/a/b/c">>, Tab)) - ). + ), + true = ets:delete(Tab). t_match4(_) -> Tab = new(), @@ -93,7 +97,8 @@ t_match4(_) -> ?assertEqual( [<<"/#">>, <<"/+/a/b/c">>], [get_topic(M) || M <- matches(<<"/0/a/b/c">>, Tab)] - ). + ), + true = ets:delete(Tab). t_match5(_) -> Tab = new(), @@ -114,21 +119,24 @@ t_match5(_) -> ?assertEqual( [<<"#">>, <>, <>], [get_topic(M) || M <- matches(<>, Tab)] - ). + ), + true = ets:delete(Tab). t_match6(_) -> Tab = new(), T = <<"a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z">>, W = <<"+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/#">>, emqx_rule_index:insert(W, ID = t_match6, <<>>, Tab), - ?assertEqual(ID, get_id(match(T, Tab))). + ?assertEqual(ID, get_id(match(T, Tab))), + true = ets:delete(Tab). t_match7(_) -> Tab = new(), T = <<"a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z">>, W = <<"a/+/c/+/e/+/g/+/i/+/k/+/m/+/o/+/q/+/s/+/u/+/w/+/y/+/#">>, emqx_rule_index:insert(W, t_match7, <<>>, Tab), - ?assertEqual(W, get_topic(match(T, Tab))). + ?assertEqual(W, get_topic(match(T, Tab))), + true = ets:delete(Tab). new() -> ets:new(?MODULE, [public, ordered_set, {write_concurrency, true}]). From af6405fa25b2690fe42984e01e46f96d880fd26c Mon Sep 17 00:00:00 2001 From: firest Date: Mon, 24 Jul 2023 15:18:17 +0800 Subject: [PATCH 11/31] fix(nested_put): fix a data loss bug introduced by #11172 --- .../src/emqx_rule_runtime.erl | 21 +++++++--- .../test/emqx_rule_engine_SUITE.erl | 42 ++++++++++++++++++- 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/apps/emqx_rule_engine/src/emqx_rule_runtime.erl b/apps/emqx_rule_engine/src/emqx_rule_runtime.erl index d62803d7e..f047e2047 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_runtime.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_runtime.erl @@ -378,9 +378,9 @@ eval({Op, _} = Exp, Context) when is_list(Context) andalso (Op == path orelse Op end end; eval({path, [{key, <<"payload">>} | Path]}, #{payload := Payload}) -> - nested_get({path, Path}, may_decode_payload(Payload)); + nested_get({path, Path}, maybe_decode_payload(Payload)); eval({path, [{key, <<"payload">>} | Path]}, #{<<"payload">> := Payload}) -> - nested_get({path, Path}, may_decode_payload(Payload)); + nested_get({path, Path}, maybe_decode_payload(Payload)); eval({path, _} = Path, Columns) -> nested_get(Path, Columns); eval({range, {Begin, End}}, _Columns) -> @@ -410,6 +410,16 @@ eval({'case', CaseOn, CaseClauses, ElseClauses}, Columns) -> eval({'fun', {_, Name}, Args}, Columns) -> apply_func(Name, [eval(Arg, Columns) || Arg <- Args], Columns). +%% the payload maybe is JSON data, decode it to a `map` first for nested put +ensure_decoded_payload({path, [{key, payload} | _]}, #{payload := Payload} = Columns) -> + Columns#{payload => maybe_decode_payload(Payload)}; +ensure_decoded_payload( + {path, [{key, <<"payload">>} | _]}, #{<<"payload">> := Payload} = Columns +) -> + Columns#{<<"payload">> => maybe_decode_payload(Payload)}; +ensure_decoded_payload(_, Columns) -> + Columns. + alias({var, Var}, _Columns) -> {var, Var}; alias({const, Val}, _Columns) when is_binary(Val) -> @@ -497,12 +507,12 @@ add_metadata(Columns, Metadata) when is_map(Columns), is_map(Metadata) -> %%------------------------------------------------------------------------------ %% Internal Functions %%------------------------------------------------------------------------------ -may_decode_payload(Payload) when is_binary(Payload) -> +maybe_decode_payload(Payload) when is_binary(Payload) -> case get_cached_payload() of undefined -> safe_decode_and_cache(Payload); DecodedP -> DecodedP end; -may_decode_payload(Payload) -> +maybe_decode_payload(Payload) -> Payload. get_cached_payload() -> @@ -522,7 +532,8 @@ safe_decode_and_cache(MaybeJson) -> ensure_list(List) when is_list(List) -> List; ensure_list(_NotList) -> []. -nested_put(Alias, Val, Columns) -> +nested_put(Alias, Val, Columns0) -> + Columns = ensure_decoded_payload(Alias, Columns0), emqx_rule_maps:nested_put(Alias, Val, Columns). inc_action_metrics(RuleId, Result) -> diff --git a/apps/emqx_rule_engine/test/emqx_rule_engine_SUITE.erl b/apps/emqx_rule_engine/test/emqx_rule_engine_SUITE.erl index c8bebab99..8c3bd0ebb 100644 --- a/apps/emqx_rule_engine/test/emqx_rule_engine_SUITE.erl +++ b/apps/emqx_rule_engine/test/emqx_rule_engine_SUITE.erl @@ -104,7 +104,8 @@ groups() -> t_sqlparse_true_false, t_sqlparse_undefined_variable, t_sqlparse_new_map, - t_sqlparse_invalid_json + t_sqlparse_invalid_json, + t_sqlselect_as_put ]}, {events, [], [ t_events, @@ -1587,6 +1588,45 @@ t_sqlselect_message_publish_event_keep_original_props_2(_Config) -> emqtt:stop(Client1), delete_rule(TopicRule). +t_sqlselect_as_put(_Config) -> + %% Verify SELECT with 'AS' to update the payload + Sql = + "select payload, " + "'STEVE' as payload.data[1].name " + "from \"t/#\" ", + PayloadMap = #{ + <<"f1">> => <<"f1">>, + <<"f2">> => <<"f2">>, + <<"data">> => [ + #{<<"name">> => <<"n1">>, <<"idx">> => 1}, + #{<<"name">> => <<"n2">>, <<"idx">> => 2} + ] + }, + PayloadBin = emqx_utils_json:encode(PayloadMap), + SqlResult = emqx_rule_sqltester:test( + #{ + sql => Sql, + context => + #{ + payload => PayloadBin, + topic => <<"t/a">> + } + } + ), + ?assertMatch({ok, #{<<"payload">> := _}}, SqlResult), + {ok, #{<<"payload">> := PayloadMap2}} = SqlResult, + ?assertMatch( + #{ + <<"f1">> := <<"f1">>, + <<"f2">> := <<"f2">>, + <<"data">> := [ + #{<<"name">> := <<"STEVE">>, <<"idx">> := 1}, + #{<<"name">> := <<"n2">>, <<"idx">> := 2} + ] + }, + PayloadMap2 + ). + t_sqlparse_event_1(_Config) -> Sql = "select topic as tp " From 613dc1646c82951c9e486840cbe17aa9a3f506e0 Mon Sep 17 00:00:00 2001 From: JimMoen Date: Mon, 24 Jul 2023 17:18:29 +0800 Subject: [PATCH 12/31] chore: hidden hstreamdb bridge schema --- apps/emqx_bridge/src/schema/emqx_bridge_enterprise.erl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/emqx_bridge/src/schema/emqx_bridge_enterprise.erl b/apps/emqx_bridge/src/schema/emqx_bridge_enterprise.erl index e76d1af37..9e3161212 100644 --- a/apps/emqx_bridge/src/schema/emqx_bridge_enterprise.erl +++ b/apps/emqx_bridge/src/schema/emqx_bridge_enterprise.erl @@ -128,7 +128,8 @@ fields(bridges) -> hoconsc:map(name, ref(emqx_bridge_hstreamdb, "config")), #{ desc => <<"HStreamDB Bridge Config">>, - required => false + required => false, + importance => ?IMPORTANCE_HIDDEN } )}, {mysql, From 6432c9c8fc99f3fc0388a7e8ef43bc3384f7215e Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Fri, 21 Jul 2023 20:06:46 +0200 Subject: [PATCH 13/31] fix(topicidx): allow to return matches unique by record id --- apps/emqx_rule_engine/src/emqx_rule_index.erl | 128 +++++++++++------- .../test/emqx_rule_index_SUITE.erl | 58 ++++---- 2 files changed, 109 insertions(+), 77 deletions(-) diff --git a/apps/emqx_rule_engine/src/emqx_rule_index.erl b/apps/emqx_rule_engine/src/emqx_rule_index.erl index 9c16bf3ad..7a3159f9a 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_index.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_index.erl @@ -16,29 +16,29 @@ %% @doc Topic index for matching topics to topic filters. %% -%% Works on top of ETS ordered_set table. Keys are parsed topic filters -%% with record ID appended to the end, wrapped in a tuple to disambiguate from -%% topic filter words. Existing table may be used if existing keys will not -%% collide with index keys. +%% Works on top of ETS ordered_set table. Keys are tuples constructed from +%% parsed topic filters and record IDs, wrapped in a tuple to order them +%% strictly greater than unit tuple (`{}`). Existing table may be used if +%% existing keys will not collide with index keys. %% %% Designed to effectively answer questions like: %% 1. Does any topic filter match given topic? %% 2. Which records are associated with topic filters matching given topic? -%% -%% Questions like these are _only slightly_ less effective: -%% 1. Which topic filters match given topic? -%% 2. Which record IDs are associated with topic filters matching given topic? +%% 3. Which topic filters match given topic? +%% 4. Which record IDs are associated with topic filters matching given topic? -module(emqx_rule_index). -export([insert/4]). -export([delete/3]). -export([match/2]). --export([matches/2]). +-export([matches/3]). +-export([get_id/1]). +-export([get_topic/1]). -export([get_record/2]). --type key(ID) :: [binary() | '+' | '#' | {ID}]. +-type key(ID) :: {[binary() | '+' | '#'], {ID}}. -type match(ID) :: key(ID). -ifdef(TEST). @@ -46,11 +46,10 @@ -endif. insert(Filter, ID, Record, Tab) -> - %% TODO: topic compact. see also in emqx_trie.erl - ets:insert(Tab, {emqx_topic:words(Filter) ++ [{ID}], Record}). + ets:insert(Tab, {{emqx_topic:words(Filter), {ID}}, Record}). delete(Filter, ID, Tab) -> - ets:delete(Tab, emqx_topic:words(Filter) ++ [{ID}]). + ets:delete(Tab, {emqx_topic:words(Filter), {ID}}). -spec match(emqx_types:topic(), ets:table()) -> match(_ID) | false. match(Topic, Tab) -> @@ -59,8 +58,8 @@ match(Topic, Tab) -> match(Words, RPrefix, Tab) -> Prefix = lists:reverse(RPrefix), - K = ets:next(Tab, Prefix), - case match_filter(Prefix, K, Words =/= []) of + K = ets:next(Tab, {Prefix, {}}), + case match_filter(Prefix, K, Words == []) of true -> K; stop -> @@ -73,7 +72,7 @@ match_rest(false, [W | Rest], RPrefix, Tab) -> match(Rest, [W | RPrefix], Tab); match_rest(plus, [W | Rest], RPrefix, Tab) -> case match(Rest, ['+' | RPrefix], Tab) of - Match when is_list(Match) -> + Match = {_, _} -> Match; false -> match(Rest, [W | RPrefix], Tab) @@ -81,48 +80,71 @@ match_rest(plus, [W | Rest], RPrefix, Tab) -> match_rest(_, [], _RPrefix, _Tab) -> false. --spec matches(emqx_types:topic(), ets:table()) -> [match(_ID)]. -matches(Topic, Tab) -> +-spec matches(emqx_types:topic(), ets:table(), _Opts :: [unique]) -> [match(_ID)]. +matches(Topic, Tab, Opts) -> {Words, RPrefix} = match_init(Topic), - matches(Words, RPrefix, Tab). - -matches(Words, RPrefix, Tab) -> - Prefix = lists:reverse(RPrefix), - matches(ets:next(Tab, Prefix), Prefix, Words, RPrefix, Tab). - -matches(K, Prefix, Words, RPrefix, Tab) -> - case match_filter(Prefix, K, Words =/= []) of - true -> - [K | matches(ets:next(Tab, K), Prefix, Words, RPrefix, Tab)]; - stop -> - []; - Matched -> - matches_rest(Matched, Words, RPrefix, Tab) + AccIn = + case Opts of + [unique | _] -> #{}; + [] -> [] + end, + Matches = matches(Words, RPrefix, AccIn, Tab), + case Matches of + #{} -> maps:values(Matches); + _ -> Matches end. -matches_rest(false, [W | Rest], RPrefix, Tab) -> - matches(Rest, [W | RPrefix], Tab); -matches_rest(plus, [W | Rest], RPrefix, Tab) -> - matches(Rest, ['+' | RPrefix], Tab) ++ matches(Rest, [W | RPrefix], Tab); -matches_rest(_, [], _RPrefix, _Tab) -> - []. +matches(Words, RPrefix, Acc, Tab) -> + Prefix = lists:reverse(RPrefix), + matches(ets:next(Tab, {Prefix, {}}), Prefix, Words, RPrefix, Acc, Tab). -match_filter([], [{_ID}], _IsPrefix = false) -> - % NOTE: exact match is `true` only if we match whole topic, not prefix - true; -match_filter([], ['#', {_ID}], _IsPrefix) -> +matches(K, Prefix, Words, RPrefix, Acc, Tab) -> + case match_filter(Prefix, K, Words == []) of + true -> + matches(ets:next(Tab, K), Prefix, Words, RPrefix, match_add(K, Acc), Tab); + stop -> + Acc; + Matched -> + matches_rest(Matched, Words, RPrefix, Acc, Tab) + end. + +matches_rest(false, [W | Rest], RPrefix, Acc, Tab) -> + matches(Rest, [W | RPrefix], Acc, Tab); +matches_rest(plus, [W | Rest], RPrefix, Acc, Tab) -> + NAcc = matches(Rest, ['+' | RPrefix], Acc, Tab), + matches(Rest, [W | RPrefix], NAcc, Tab); +matches_rest(_, [], _RPrefix, Acc, _Tab) -> + Acc. + +match_add(K = {_Filter, ID}, Acc = #{}) -> + Acc#{ID => K}; +match_add(K, Acc) -> + [K | Acc]. + +match_filter(Prefix, {Filter, _ID}, NotPrefix) -> + case match_filter(Prefix, Filter) of + exact -> + % NOTE: exact match is `true` only if we match whole topic, not prefix + NotPrefix; + Match -> + Match + end; +match_filter(_, '$end_of_table', _) -> + stop. + +match_filter([], []) -> + exact; +match_filter([], ['#']) -> % NOTE: naturally, '#' < '+', so this is already optimal for `match/2` true; -match_filter([], ['+' | _], _) -> +match_filter([], ['+' | _]) -> plus; -match_filter([], [_H | _], _) -> +match_filter([], [_H | _]) -> false; -match_filter([H | T1], [H | T2], IsPrefix) -> - match_filter(T1, T2, IsPrefix); -match_filter([H1 | _], [H2 | _], _) when H2 > H1 -> +match_filter([H | T1], [H | T2]) -> + match_filter(T1, T2); +match_filter([H1 | _], [H2 | _]) when H2 > H1 -> % NOTE: we're strictly past the prefix, no need to continue - stop; -match_filter(_, '$end_of_table', _) -> stop. match_init(Topic) -> @@ -135,6 +157,14 @@ match_init(Topic) -> {Words, []} end. +-spec get_id(match(ID)) -> ID. +get_id({_Filter, {ID}}) -> + ID. + +-spec get_topic(match(_ID)) -> emqx_types:topic(). +get_topic({Filter, _ID}) -> + emqx_topic:join(Filter). + -spec get_record(match(_ID), ets:table()) -> _Record. get_record(K, Tab) -> ets:lookup_element(Tab, K, 2). diff --git a/apps/emqx_rule_engine/test/emqx_rule_index_SUITE.erl b/apps/emqx_rule_engine/test/emqx_rule_index_SUITE.erl index cf4b67cd4..42f3f1da0 100644 --- a/apps/emqx_rule_engine/test/emqx_rule_index_SUITE.erl +++ b/apps/emqx_rule_engine/test/emqx_rule_index_SUITE.erl @@ -29,8 +29,8 @@ t_insert(_) -> true = emqx_rule_index:insert(<<"sensor/1/metric/2">>, t_insert_1, <<>>, Tab), true = emqx_rule_index:insert(<<"sensor/+/#">>, t_insert_2, <<>>, Tab), true = emqx_rule_index:insert(<<"sensor/#">>, t_insert_3, <<>>, Tab), - ?assertEqual(<<"sensor/#">>, get_topic(match(<<"sensor">>, Tab))), - ?assertEqual(t_insert_3, get_id(match(<<"sensor">>, Tab))), + ?assertEqual(<<"sensor/#">>, topic(match(<<"sensor">>, Tab))), + ?assertEqual(t_insert_3, id(match(<<"sensor">>, Tab))), true = ets:delete(Tab). t_match(_) -> @@ -40,7 +40,7 @@ t_match(_) -> true = emqx_rule_index:insert(<<"sensor/#">>, t_match_3, <<>>, Tab), ?assertMatch( [<<"sensor/#">>, <<"sensor/+/#">>], - [get_topic(M) || M <- matches(<<"sensor/1">>, Tab)] + [topic(M) || M <- matches(<<"sensor/1">>, Tab)] ), true = ets:delete(Tab). @@ -51,7 +51,7 @@ t_match2(_) -> true = emqx_rule_index:insert(<<"+/+/#">>, t_match2_3, <<>>, Tab), ?assertEqual( [<<"#">>, <<"+/#">>, <<"+/+/#">>], - [get_topic(M) || M <- matches(<<"a/b/c">>, Tab)] + [topic(M) || M <- matches(<<"a/b/c">>, Tab)] ), ?assertEqual( false, @@ -79,7 +79,7 @@ t_match3(_) -> end, ?assertEqual( t_match3_sys, - get_id(match(<<"$SYS/a/b/c">>, Tab)) + id(match(<<"$SYS/a/b/c">>, Tab)) ), true = ets:delete(Tab). @@ -92,11 +92,11 @@ t_match4(_) -> ), ?assertEqual( [<<"/#">>, <<"/+">>], - [get_topic(M) || M <- matches(<<"/">>, Tab)] + [topic(M) || M <- matches(<<"/">>, Tab)] ), ?assertEqual( [<<"/#">>, <<"/+/a/b/c">>], - [get_topic(M) || M <- matches(<<"/0/a/b/c">>, Tab)] + [topic(M) || M <- matches(<<"/0/a/b/c">>, Tab)] ), true = ets:delete(Tab). @@ -114,11 +114,11 @@ t_match5(_) -> ), ?assertEqual( [<<"#">>, <>], - [get_topic(M) || M <- matches(T, Tab)] + [topic(M) || M <- matches(T, Tab)] ), ?assertEqual( [<<"#">>, <>, <>], - [get_topic(M) || M <- matches(<>, Tab)] + [topic(M) || M <- matches(<>, Tab)] ), true = ets:delete(Tab). @@ -127,7 +127,7 @@ t_match6(_) -> T = <<"a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z">>, W = <<"+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/#">>, emqx_rule_index:insert(W, ID = t_match6, <<>>, Tab), - ?assertEqual(ID, get_id(match(T, Tab))), + ?assertEqual(ID, id(match(T, Tab))), true = ets:delete(Tab). t_match7(_) -> @@ -135,9 +135,23 @@ t_match7(_) -> T = <<"a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z">>, W = <<"a/+/c/+/e/+/g/+/i/+/k/+/m/+/o/+/q/+/s/+/u/+/w/+/y/+/#">>, emqx_rule_index:insert(W, t_match7, <<>>, Tab), - ?assertEqual(W, get_topic(match(T, Tab))), + ?assertEqual(W, topic(match(T, Tab))), true = ets:delete(Tab). +t_match_unique(_) -> + Tab = new(), + emqx_rule_index:insert(<<"a/b/c">>, t_match_id1, <<>>, Tab), + emqx_rule_index:insert(<<"a/b/+">>, t_match_id1, <<>>, Tab), + emqx_rule_index:insert(<<"a/b/c/+">>, t_match_id2, <<>>, Tab), + ?assertEqual( + [t_match_id1, t_match_id1], + [id(M) || M <- emqx_rule_index:matches(<<"a/b/c">>, Tab, [])] + ), + ?assertEqual( + [t_match_id1], + [id(M) || M <- emqx_rule_index:matches(<<"a/b/c">>, Tab, [unique])] + ). + new() -> ets:new(?MODULE, [public, ordered_set, {write_concurrency, true}]). @@ -145,22 +159,10 @@ match(T, Tab) -> emqx_rule_index:match(T, Tab). matches(T, Tab) -> - lists:sort(emqx_rule_index:matches(T, Tab)). + lists:sort(emqx_rule_index:matches(T, Tab, [])). --spec get_id(emqx_rule_index:match(ID)) -> ID. -get_id([{ID}]) -> - ID; -get_id([_ | Rest]) -> - get_id(Rest). +id(Match) -> + emqx_rule_index:get_id(Match). --spec get_topic(emqx_rule_index:match(_ID)) -> emqx_types:topic(). -get_topic(K) -> - emqx_topic:join(cut_topic(K)). - -cut_topic(K) -> - cut_topic(K, []). - -cut_topic([{_ID}], Acc) -> - lists:reverse(Acc); -cut_topic([W | Rest], Acc) -> - cut_topic(Rest, [W | Acc]). +topic(Match) -> + emqx_rule_index:get_topic(Match). From 04960383616ceb7eecd4bc04e70078412e19fbf4 Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Fri, 21 Jul 2023 20:09:17 +0200 Subject: [PATCH 14/31] fix(ruleeng): ensure topic index matched rules evalauted once --- apps/emqx_rule_engine/src/emqx_rule_engine.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx_rule_engine/src/emqx_rule_engine.erl b/apps/emqx_rule_engine/src/emqx_rule_engine.erl index d92931d77..dd4b52d44 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_engine.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_engine.erl @@ -226,7 +226,7 @@ get_rules_ordered_by_ts() -> get_rules_for_topic(Topic) -> [ emqx_rule_index:get_record(M, ?RULE_TOPIC_INDEX) - || M <- emqx_rule_index:matches(Topic, ?RULE_TOPIC_INDEX) + || M <- emqx_rule_index:matches(Topic, ?RULE_TOPIC_INDEX, [unique]) ]. -spec get_rules_with_same_event(Topic :: binary()) -> [rule()]. From dcf4819c044221147e835da873e8e0997ffe6a3d Mon Sep 17 00:00:00 2001 From: JianBo He Date: Mon, 24 Jul 2023 19:30:34 +0800 Subject: [PATCH 15/31] test(rule): add tests to ensure the rules ordering --- apps/emqx_rule_engine/src/emqx_rule_index.erl | 8 +++- .../test/emqx_rule_engine_SUITE.erl | 40 +++++++++++++++++++ .../test/emqx_rule_index_SUITE.erl | 10 +++++ 3 files changed, 56 insertions(+), 2 deletions(-) diff --git a/apps/emqx_rule_engine/src/emqx_rule_index.erl b/apps/emqx_rule_engine/src/emqx_rule_index.erl index 7a3159f9a..70564f62c 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_index.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_index.erl @@ -89,9 +89,13 @@ matches(Topic, Tab, Opts) -> [] -> [] end, Matches = matches(Words, RPrefix, AccIn, Tab), + %% return rules ordered by Rule ID case Matches of - #{} -> maps:values(Matches); - _ -> Matches + #{} -> + maps:values(Matches); + _ -> + F = fun({_, {ID1}}, {_, {ID2}}) -> ID1 < ID2 end, + lists:sort(F, Matches) end. matches(Words, RPrefix, Acc, Tab) -> diff --git a/apps/emqx_rule_engine/test/emqx_rule_engine_SUITE.erl b/apps/emqx_rule_engine/test/emqx_rule_engine_SUITE.erl index c8bebab99..87563f660 100644 --- a/apps/emqx_rule_engine/test/emqx_rule_engine_SUITE.erl +++ b/apps/emqx_rule_engine/test/emqx_rule_engine_SUITE.erl @@ -58,6 +58,7 @@ groups() -> t_create_existing_rule, t_get_rules_for_topic, t_get_rules_for_topic_2, + t_get_rules_for_topic_3, t_get_rules_with_same_event, t_get_rule_ids_by_action, t_ensure_action_removed @@ -399,6 +400,45 @@ t_get_rules_for_topic_2(_Config) -> ]), ok. +t_get_rules_for_topic_3(_Config) -> + ok = create_rules( + [ + make_simple_rule(<<"rule-debug-5">>, <<"select * from \"simple/#\"">>), + make_simple_rule(<<"rule-debug-4">>, <<"select * from \"simple/+\"">>), + make_simple_rule(<<"rule-debug-3">>, <<"select * from \"simple/+/1\"">>), + make_simple_rule(<<"rule-debug-2">>, <<"select * from \"simple/1\"">>), + make_simple_rule( + <<"rule-debug-1">>, + <<"select * from \"simple/2\", \"simple/+\", \"simple/3\"">> + ) + ] + ), + Rules1 = get_rules_for_topic_in_e510_impl(<<"simple/1">>), + Rules2 = emqx_rule_engine:get_rules_for_topic(<<"simple/1">>), + %% assert, ensure the order of rules is the same as e5.1.0 + ?assertEqual(Rules1, Rules2), + ?assertEqual( + [<<"rule-debug-1">>, <<"rule-debug-2">>, <<"rule-debug-4">>, <<"rule-debug-5">>], + [Id || #{id := Id} <- Rules1] + ), + + ok = delete_rules_by_ids([ + <<"rule-debug-1">>, + <<"rule-debug-2">>, + <<"rule-debug-3">>, + <<"rule-debug-4">>, + <<"rule-debug-5">>, + <<"rule-debug-6">> + ]), + ok. + +get_rules_for_topic_in_e510_impl(Topic) -> + [ + Rule + || Rule = #{from := From} <- emqx_rule_engine:get_rules(), + emqx_topic:match_any(Topic, From) + ]. + t_get_rules_with_same_event(_Config) -> PubT = <<"simple/1">>, PubN = length(emqx_rule_engine:get_rules_with_same_event(PubT)), diff --git a/apps/emqx_rule_engine/test/emqx_rule_index_SUITE.erl b/apps/emqx_rule_engine/test/emqx_rule_index_SUITE.erl index 42f3f1da0..76ad1cda6 100644 --- a/apps/emqx_rule_engine/test/emqx_rule_index_SUITE.erl +++ b/apps/emqx_rule_engine/test/emqx_rule_index_SUITE.erl @@ -152,6 +152,16 @@ t_match_unique(_) -> [id(M) || M <- emqx_rule_index:matches(<<"a/b/c">>, Tab, [unique])] ). +t_match_ordering(_) -> + Tab = new(), + emqx_rule_index:insert(<<"a/b/+">>, t_match_id2, <<>>, Tab), + emqx_rule_index:insert(<<"a/b/c">>, t_match_id1, <<>>, Tab), + emqx_rule_index:insert(<<"a/b/#">>, t_match_id3, <<>>, Tab), + Ids1 = [id(M) || M <- emqx_rule_index:matches(<<"a/b/c">>, Tab, [])], + Ids2 = [id(M) || M <- emqx_rule_index:matches(<<"a/b/c">>, Tab, [unique])], + ?assertEqual(Ids1, Ids2), + ?assertEqual([t_match_id1, t_match_id2, t_match_id3], Ids1). + new() -> ets:new(?MODULE, [public, ordered_set, {write_concurrency, true}]). From 511d1b6ca10f506a0ca7837ef0bfa636775063f0 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Mon, 24 Jul 2023 20:11:44 +0800 Subject: [PATCH 16/31] chore: hide the hstreamdb http api --- apps/emqx_bridge/src/schema/emqx_bridge_enterprise.erl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/emqx_bridge/src/schema/emqx_bridge_enterprise.erl b/apps/emqx_bridge/src/schema/emqx_bridge_enterprise.erl index 9e3161212..9ed87125b 100644 --- a/apps/emqx_bridge/src/schema/emqx_bridge_enterprise.erl +++ b/apps/emqx_bridge/src/schema/emqx_bridge_enterprise.erl @@ -32,7 +32,8 @@ api_schemas(Method) -> api_ref(emqx_bridge_mongodb, <<"mongodb_rs">>, Method ++ "_rs"), api_ref(emqx_bridge_mongodb, <<"mongodb_sharded">>, Method ++ "_sharded"), api_ref(emqx_bridge_mongodb, <<"mongodb_single">>, Method ++ "_single"), - api_ref(emqx_bridge_hstreamdb, <<"hstreamdb">>, Method), + %% TODO: un-hide for e5.2.0... + %%api_ref(emqx_bridge_hstreamdb, <<"hstreamdb">>, Method), api_ref(emqx_bridge_influxdb, <<"influxdb_api_v1">>, Method ++ "_api_v1"), api_ref(emqx_bridge_influxdb, <<"influxdb_api_v2">>, Method ++ "_api_v2"), api_ref(emqx_bridge_redis, <<"redis_single">>, Method ++ "_single"), From 5ffd7f2a7319dd4dabb431d2641ae02c7eca737b Mon Sep 17 00:00:00 2001 From: JianBo He Date: Mon, 24 Jul 2023 20:14:47 +0800 Subject: [PATCH 17/31] chore: remove the hstreamdb changes due to we hide it in e5.1.1 --- changes/ee/feat-10203.en.md | 1 - 1 file changed, 1 deletion(-) delete mode 100644 changes/ee/feat-10203.en.md diff --git a/changes/ee/feat-10203.en.md b/changes/ee/feat-10203.en.md deleted file mode 100644 index a2ff3b3bb..000000000 --- a/changes/ee/feat-10203.en.md +++ /dev/null @@ -1 +0,0 @@ -Add HStreamDB bridge support, adapted to the HStreamDB `v0.15.0`. From e630331de14ca266d5bb9d7bb1c187acc1e3cf50 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Mon, 24 Jul 2023 23:04:53 +0800 Subject: [PATCH 18/31] fix(rule): fix a quering problem when 'a/b' and 'a/b/#' exist at the same time. When using `ets:next` to query the next level of topic words, we should prioritize the next level of '#', '+'. --- apps/emqx_rule_engine/src/emqx_rule_index.erl | 11 +++++++++- .../test/emqx_rule_engine_SUITE.erl | 3 +-- .../test/emqx_rule_index_SUITE.erl | 20 ++++++++++++++++++- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/apps/emqx_rule_engine/src/emqx_rule_index.erl b/apps/emqx_rule_engine/src/emqx_rule_index.erl index 70564f62c..16f23896a 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_index.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_index.erl @@ -114,6 +114,10 @@ matches(K, Prefix, Words, RPrefix, Acc, Tab) -> matches_rest(false, [W | Rest], RPrefix, Acc, Tab) -> matches(Rest, [W | RPrefix], Acc, Tab); +matches_rest({false, exact}, [W | Rest], RPrefix, Acc, Tab) -> + NAcc1 = matches(Rest, ['#' | RPrefix], Acc, Tab), + NAcc2 = matches(Rest, ['+' | RPrefix], NAcc1, Tab), + matches(Rest, [W | RPrefix], NAcc2, Tab); matches_rest(plus, [W | Rest], RPrefix, Acc, Tab) -> NAcc = matches(Rest, ['+' | RPrefix], Acc, Tab), matches(Rest, [W | RPrefix], NAcc, Tab); @@ -129,7 +133,12 @@ match_filter(Prefix, {Filter, _ID}, NotPrefix) -> case match_filter(Prefix, Filter) of exact -> % NOTE: exact match is `true` only if we match whole topic, not prefix - NotPrefix; + case NotPrefix of + true -> + true; + false -> + {false, exact} + end; Match -> Match end; diff --git a/apps/emqx_rule_engine/test/emqx_rule_engine_SUITE.erl b/apps/emqx_rule_engine/test/emqx_rule_engine_SUITE.erl index 87563f660..2b70ea8ae 100644 --- a/apps/emqx_rule_engine/test/emqx_rule_engine_SUITE.erl +++ b/apps/emqx_rule_engine/test/emqx_rule_engine_SUITE.erl @@ -427,8 +427,7 @@ t_get_rules_for_topic_3(_Config) -> <<"rule-debug-2">>, <<"rule-debug-3">>, <<"rule-debug-4">>, - <<"rule-debug-5">>, - <<"rule-debug-6">> + <<"rule-debug-5">> ]), ok. diff --git a/apps/emqx_rule_engine/test/emqx_rule_index_SUITE.erl b/apps/emqx_rule_engine/test/emqx_rule_index_SUITE.erl index 76ad1cda6..027d85b7e 100644 --- a/apps/emqx_rule_engine/test/emqx_rule_index_SUITE.erl +++ b/apps/emqx_rule_engine/test/emqx_rule_index_SUITE.erl @@ -162,8 +162,26 @@ t_match_ordering(_) -> ?assertEqual(Ids1, Ids2), ?assertEqual([t_match_id1, t_match_id2, t_match_id3], Ids1). +t_match_wildcards(_) -> + Tab = new(), + emqx_rule_index:insert(<<"a/b">>, id1, <<>>, Tab), + emqx_rule_index:insert(<<"a/b/#">>, id2, <<>>, Tab), + emqx_rule_index:insert(<<"a/b/#">>, id3, <<>>, Tab), + emqx_rule_index:insert(<<"a/b/c">>, id4, <<>>, Tab), + emqx_rule_index:insert(<<"a/b/+">>, id5, <<>>, Tab), + emqx_rule_index:insert(<<"a/b/d">>, id6, <<>>, Tab), + emqx_rule_index:insert(<<"a/+/+">>, id7, <<>>, Tab), + emqx_rule_index:insert(<<"a/+/#">>, id8, <<>>, Tab), + + Rules = [id(M) || M <- emqx_rule_index:matches(<<"a/b/c">>, Tab, [])], + ?assertEqual([id2, id3, id4, id5, id7, id8], Rules), + + Rules1 = [id(M) || M <- emqx_rule_index:matches(<<"a/b">>, Tab, [])], + ?assertEqual([id1, id2, id3, id8], Rules1), + ok. + new() -> - ets:new(?MODULE, [public, ordered_set, {write_concurrency, true}]). + ets:new(?MODULE, [public, ordered_set, {read_concurrency, true}]). match(T, Tab) -> emqx_rule_index:match(T, Tab). From 69f4275871d1b41d5a119ef45ec50a53aa63dd86 Mon Sep 17 00:00:00 2001 From: Paulo Zulato Date: Fri, 21 Jul 2023 10:54:53 -0300 Subject: [PATCH 19/31] fix(oracle): fix return error checking on table validation Fixes https://emqx.atlassian.net/browse/EMQX-10622 --- .../test/emqx_bridge_oracle_SUITE.erl | 45 +++++++++++++++++-- apps/emqx_oracle/src/emqx_oracle.erl | 25 ++++++++++- changes/ee/fix-11326.en.md | 1 + 3 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 changes/ee/fix-11326.en.md diff --git a/apps/emqx_bridge_oracle/test/emqx_bridge_oracle_SUITE.erl b/apps/emqx_bridge_oracle/test/emqx_bridge_oracle_SUITE.erl index 887c33692..bd3ac289c 100644 --- a/apps/emqx_bridge_oracle/test/emqx_bridge_oracle_SUITE.erl +++ b/apps/emqx_bridge_oracle/test/emqx_bridge_oracle_SUITE.erl @@ -165,6 +165,9 @@ sql_insert_template_for_bridge() -> sql_insert_template_with_nested_token_for_bridge() -> "INSERT INTO mqtt_test(topic, msgid, payload, retain) VALUES (${topic}, ${id}, ${payload.msg}, ${retain})". +sql_insert_template_with_inconsistent_datatype() -> + "INSERT INTO mqtt_test(topic, msgid, payload, retain) VALUES (${topic}, ${id}, ${payload}, ${flags})". + sql_create_table() -> "CREATE TABLE mqtt_test (topic VARCHAR2(255), msgid VARCHAR2(64), payload NCLOB, retain NUMBER(1))". @@ -333,10 +336,11 @@ update_bridge_api(Config, Overrides) -> probe_bridge_api(Config) -> probe_bridge_api(Config, _Overrides = #{}). -probe_bridge_api(Config, _Overrides) -> +probe_bridge_api(Config, Overrides) -> TypeBin = ?BRIDGE_TYPE_BIN, Name = ?config(oracle_name, Config), - OracleConfig = ?config(oracle_config, Config), + OracleConfig0 = ?config(oracle_config, Config), + OracleConfig = emqx_utils_maps:deep_merge(OracleConfig0, Overrides), Params = OracleConfig#{<<"type">> => TypeBin, <<"name">> => Name}, Path = emqx_mgmt_api_test_util:api_path(["bridges_probe"]), AuthHeader = emqx_mgmt_api_test_util:auth_header_(), @@ -539,6 +543,14 @@ t_start_stop(Config) -> ok. t_probe_with_nested_tokens(Config) -> + ProbeRes0 = probe_bridge_api( + Config, + #{<<"sql">> => sql_insert_template_with_nested_token_for_bridge()} + ), + ?assertMatch({ok, {{_, 204, _}, _Headers, _Body}}, ProbeRes0). + +t_message_with_nested_tokens(Config) -> + BridgeId = bridge_id(Config), ResourceId = resource_id(Config), reset_table(Config), ?assertMatch( @@ -553,7 +565,34 @@ t_probe_with_nested_tokens(Config) -> _Sleep = 1_000, _Attempts = 20, ?assertEqual({ok, connected}, emqx_resource_manager:health_check(ResourceId)) - ). + ), + MsgId = erlang:unique_integer(), + Data = binary_to_list(?config(oracle_name, Config)), + Params = #{ + topic => ?config(mqtt_topic, Config), + id => MsgId, + payload => emqx_utils_json:encode(#{<<"msg">> => Data}), + retain => false + }, + emqx_bridge:send_message(BridgeId, Params), + ?retry( + _Sleep = 1_000, + _Attempts = 20, + ?assertMatch( + {ok, [{result_set, [<<"PAYLOAD">>], _, [[Data]]}]}, + emqx_resource:simple_sync_query( + ResourceId, {query, "SELECT payload FROM mqtt_test"} + ) + ) + ), + ok. + +t_probe_with_inconsistent_datatype(Config) -> + ProbeRes0 = probe_bridge_api( + Config, + #{<<"sql">> => sql_insert_template_with_inconsistent_datatype()} + ), + ?assertMatch({ok, {{_, 204, _}, _Headers, _Body}}, ProbeRes0). t_on_get_status(Config) -> ProxyPort = ?config(proxy_port, Config), diff --git a/apps/emqx_oracle/src/emqx_oracle.erl b/apps/emqx_oracle/src/emqx_oracle.erl index d0e115660..c7ed80257 100644 --- a/apps/emqx_oracle/src/emqx_oracle.erl +++ b/apps/emqx_oracle/src/emqx_oracle.erl @@ -433,9 +433,32 @@ check_if_table_exists(Conn, SQL, Tokens0) -> case jamdb_oracle:sql_query(Conn, {SqlQuery, Params}) of {ok, [{proc_result, 0, _Description}]} -> ok; - {ok, [{proc_result, 6550, _Description}]} -> + {ok, [{proc_result, 942, _Description}]} -> %% Target table is not created {error, undefined_table}; + {ok, [{proc_result, _, Description}]} -> + % only the last result is returned, so we need to check on description if it + % contains the "Table doesn't exist" error as it can not be the last one. + % (for instance, the ORA-06550 can be the result value when table does not exist) + ErrorCodes = + case re:run(Description, <<"(ORA-[0-9]+)">>, [global, {capture, first, binary}]) of + {match, OraCodes} -> OraCodes; + _ -> [] + end, + OraMap = maps:from_keys([ErrorCode || [ErrorCode] <- ErrorCodes], true), + case OraMap of + _ when is_map_key(<<"ORA-00942">>, OraMap) -> + % ORA-00942: table or view does not exist + {error, undefined_table}; + _ when is_map_key(<<"ORA-00932">>, OraMap) -> + % ORA-00932: inconsistent datatypes + % There is a some type inconsistency with table definition but + % table does exist. Probably this inconsistency was caused by + % token discarding in this test query. + ok; + _ -> + {error, Description} + end; Reason -> {error, Reason} end. diff --git a/changes/ee/fix-11326.en.md b/changes/ee/fix-11326.en.md new file mode 100644 index 000000000..5bbba060c --- /dev/null +++ b/changes/ee/fix-11326.en.md @@ -0,0 +1 @@ +Fixed return error checking on table validation in the Oracle bridge. From d05a5cfe0fc4ebd39ef46653b115fc7f28545e87 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Tue, 25 Jul 2023 14:28:50 +0800 Subject: [PATCH 20/31] fix(rule): fix the `matches/2` for some edge cases --- apps/emqx_rule_engine/src/emqx_rule_index.erl | 8 +- .../test/emqx_rule_index_SUITE.erl | 142 ++++++++++++++++-- 2 files changed, 132 insertions(+), 18 deletions(-) diff --git a/apps/emqx_rule_engine/src/emqx_rule_index.erl b/apps/emqx_rule_engine/src/emqx_rule_index.erl index 16f23896a..4dd395b94 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_index.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_index.erl @@ -114,8 +114,8 @@ matches(K, Prefix, Words, RPrefix, Acc, Tab) -> matches_rest(false, [W | Rest], RPrefix, Acc, Tab) -> matches(Rest, [W | RPrefix], Acc, Tab); -matches_rest({false, exact}, [W | Rest], RPrefix, Acc, Tab) -> - NAcc1 = matches(Rest, ['#' | RPrefix], Acc, Tab), +matches_rest(sharp, [W | Rest], RPrefix, Acc, Tab) -> + NAcc1 = matches([], ['#' | RPrefix], Acc, Tab), NAcc2 = matches(Rest, ['+' | RPrefix], NAcc1, Tab), matches(Rest, [W | RPrefix], NAcc2, Tab); matches_rest(plus, [W | Rest], RPrefix, Acc, Tab) -> @@ -137,7 +137,7 @@ match_filter(Prefix, {Filter, _ID}, NotPrefix) -> true -> true; false -> - {false, exact} + sharp end; Match -> Match @@ -147,6 +147,8 @@ match_filter(_, '$end_of_table', _) -> match_filter([], []) -> exact; +match_filter([], ['' | _]) -> + sharp; match_filter([], ['#']) -> % NOTE: naturally, '#' < '+', so this is already optimal for `match/2` true; diff --git a/apps/emqx_rule_engine/test/emqx_rule_index_SUITE.erl b/apps/emqx_rule_engine/test/emqx_rule_index_SUITE.erl index 027d85b7e..8a65ee8e8 100644 --- a/apps/emqx_rule_engine/test/emqx_rule_index_SUITE.erl +++ b/apps/emqx_rule_engine/test/emqx_rule_index_SUITE.erl @@ -19,6 +19,7 @@ -compile(export_all). -compile(nowarn_export_all). +-include_lib("proper/include/proper.hrl"). -include_lib("eunit/include/eunit.hrl"). all() -> @@ -162,23 +163,106 @@ t_match_ordering(_) -> ?assertEqual(Ids1, Ids2), ?assertEqual([t_match_id1, t_match_id2, t_match_id3], Ids1). -t_match_wildcards(_) -> - Tab = new(), - emqx_rule_index:insert(<<"a/b">>, id1, <<>>, Tab), - emqx_rule_index:insert(<<"a/b/#">>, id2, <<>>, Tab), - emqx_rule_index:insert(<<"a/b/#">>, id3, <<>>, Tab), - emqx_rule_index:insert(<<"a/b/c">>, id4, <<>>, Tab), - emqx_rule_index:insert(<<"a/b/+">>, id5, <<>>, Tab), - emqx_rule_index:insert(<<"a/b/d">>, id6, <<>>, Tab), - emqx_rule_index:insert(<<"a/+/+">>, id7, <<>>, Tab), - emqx_rule_index:insert(<<"a/+/#">>, id8, <<>>, Tab), +t_match_wildcard_edge_cases(_) -> + CommonTopics = [ + <<"a/b">>, + <<"a/b/#">>, + <<"a/b/#">>, + <<"a/b/c">>, + <<"a/b/+">>, + <<"a/b/d">>, + <<"a/+/+">>, + <<"a/+/#">> + ], + Datasets = + [ + %% Topics, TopicName, Results + {CommonTopics, <<"a/b/c">>, [2, 3, 4, 5, 7, 8]}, + {CommonTopics, <<"a/b">>, [1, 2, 3, 8]}, + {[<<"+/b/c">>, <<"/">>], <<"a/b/c">>, [1]}, + {[<<"#">>, <<"/">>], <<"a">>, [1]}, + {[<<"/">>, <<"+">>], <<"a">>, [2]} + ], + F = fun({Topics, TopicName, Expected}) -> + Tab = new(), + _ = lists:foldl( + fun(T, N) -> + emqx_rule_index:insert(T, N, <<>>, Tab), + N + 1 + end, + 1, + Topics + ), + Results = [id(M) || M <- emqx_rule_index:matches(TopicName, Tab, [unique])], + case Results == Expected of + true -> + ets:delete(Tab); + false -> + ct:pal( + "Base topics: ~p~n" + "Topic name: ~p~n" + "Index results: ~p~n" + "Expected results:: ~p~n", + [Topics, TopicName, Results, Expected] + ), + error(bad_matches) + end + end, + lists:foreach(F, Datasets). - Rules = [id(M) || M <- emqx_rule_index:matches(<<"a/b/c">>, Tab, [])], - ?assertEqual([id2, id3, id4, id5, id7, id8], Rules), +t_prop_matches(_) -> + ?assert( + proper:quickcheck( + topic_matches_prop(), + [{max_size, 100}, {numtests, 100}] + ) + ). - Rules1 = [id(M) || M <- emqx_rule_index:matches(<<"a/b">>, Tab, [])], - ?assertEqual([id1, id2, id3, id8], Rules1), - ok. +topic_matches_prop() -> + ?FORALL( + Topics0, + list(emqx_proper_types:topic()), + begin + Tab = new(), + Topics = lists:filter(fun(Topic) -> Topic =/= <<>> end, Topics0), + lists:foldl( + fun(Topic, N) -> + true = emqx_rule_index:insert(Topic, N, <<>>, Tab), + N + 1 + end, + 1, + Topics + ), + lists:foreach( + fun(Topic0) -> + Topic = topic_filter_to_topic_name(Topic0), + Ids1 = [ + emqx_rule_index:get_id(R) + || R <- emqx_rule_index:matches(Topic, Tab, [unique]) + ], + Ids2 = topic_matches(Topic, Topics), + case Ids2 == Ids1 of + true -> + ok; + false -> + ct:pal( + "Base topics: ~p~n" + "Topic name: ~p~n" + "Index results: ~p~n" + "Topic match results:: ~p~n", + [Topics, Topic, Ids1, Ids2] + ), + error(bad_matches) + end + end, + Topics + ), + true + end + ). + +%%-------------------------------------------------------------------- +%% helpers new() -> ets:new(?MODULE, [public, ordered_set, {read_concurrency, true}]). @@ -194,3 +278,31 @@ id(Match) -> topic(Match) -> emqx_rule_index:get_topic(Match). + +topic_filter_to_topic_name(Topic) when is_binary(Topic) -> + topic_filter_to_topic_name(emqx_topic:words(Topic)); +topic_filter_to_topic_name(Words) when is_list(Words) -> + topic_filter_to_topic_name(Words, []). + +topic_filter_to_topic_name([], Acc) -> + emqx_topic:join(lists:reverse(Acc)); +topic_filter_to_topic_name(['#' | _Rest], Acc) -> + case rand:uniform(2) of + 1 -> emqx_topic:join(lists:reverse(Acc)); + _ -> emqx_topic:join(lists:reverse([<<"_sharp">> | Acc])) + end; +topic_filter_to_topic_name(['+' | Rest], Acc) -> + topic_filter_to_topic_name(Rest, [<<"_plus">> | Acc]); +topic_filter_to_topic_name([H | Rest], Acc) -> + topic_filter_to_topic_name(Rest, [H | Acc]). + +topic_matches(Topic, Topics0) -> + Topics = lists:zip(lists:seq(1, length(Topics0)), Topics0), + lists:sort( + lists:filtermap( + fun({Id, Topic0}) -> + emqx_topic:match(Topic, Topic0) andalso {true, Id} + end, + Topics + ) + ). From 7a16ff4f04e15112bb40ef8c7c560243a73e4d72 Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Mon, 24 Jul 2023 17:47:17 -0300 Subject: [PATCH 21/31] fix(postgres_bridge): fix table existence check and handle sync_required Fixes https://emqx.atlassian.net/browse/EMQX-10629 During health checking, we check whether tables in the SQL statement exist. Such check was done by asking the backend to parse the statement using a named prepared statements. Concurrent health checks could then result in the error: ```erlang {error,{error,error,<<"42P05">>,duplicate_prepared_statement,<<"prepared statement \"get_status\" already exists">>,[{file,<<"prepare.c">>},{line,<<"451">>},{routine,<<"StorePreparedStatement">>},{severity,<<"ERROR">>}]}} ``` This could lead to an inconsistent state in the driver process, which would crash later when a message from the backend (`READY_FOR_QUERY`, "idle"): ``` 2023-07-24T13:05:58.892043+00:00 [error] Generic server <0.2134.0> terminating. Reason: {'module could not be loaded',[{undefined,handle_message,[90,<<"I">>,... ``` Added calls to `epgsql:sync/1` for functions that could return `{error, sync_required}`. Also, redundant calls to `parse2` were removed to reduce the number of requests. --- .../test/emqx_bridge_pgsql_SUITE.erl | 28 +++++++ .../src/emqx_connector_pgsql.erl | 84 +++++++++++++------ changes/ee/fix-11338.en.md | 1 + 3 files changed, 88 insertions(+), 25 deletions(-) create mode 100644 changes/ee/fix-11338.en.md diff --git a/apps/emqx_bridge_pgsql/test/emqx_bridge_pgsql_SUITE.erl b/apps/emqx_bridge_pgsql/test/emqx_bridge_pgsql_SUITE.erl index d16488bc6..262774a24 100644 --- a/apps/emqx_bridge_pgsql/test/emqx_bridge_pgsql_SUITE.erl +++ b/apps/emqx_bridge_pgsql/test/emqx_bridge_pgsql_SUITE.erl @@ -700,3 +700,31 @@ t_table_removed(Config) -> ), connect_and_create_table(Config), ok. + +t_concurrent_health_checks(Config) -> + Name = ?config(pgsql_name, Config), + BridgeType = ?config(pgsql_bridge_type, Config), + ResourceID = emqx_bridge_resource:resource_id(BridgeType, Name), + ?check_trace( + begin + connect_and_create_table(Config), + ?assertMatch({ok, _}, create_bridge(Config)), + ?retry( + _Sleep = 1_000, + _Attempts = 20, + ?assertEqual({ok, connected}, emqx_resource_manager:health_check(ResourceID)) + ), + emqx_utils:pmap( + fun(_) -> + ?assertEqual({ok, connected}, emqx_resource_manager:health_check(ResourceID)) + end, + lists:seq(1, 20) + ), + ok + end, + fun(Trace) -> + ?assertEqual([], ?of_kind(postgres_connector_bad_parse2, Trace)), + ok + end + ), + ok. diff --git a/apps/emqx_connector/src/emqx_connector_pgsql.erl b/apps/emqx_connector/src/emqx_connector_pgsql.erl index c468aa8bd..04ba4fd51 100644 --- a/apps/emqx_connector/src/emqx_connector_pgsql.erl +++ b/apps/emqx_connector/src/emqx_connector_pgsql.erl @@ -62,6 +62,11 @@ prepare_statement := epgsql:statement() }. +%% FIXME: add `{error, sync_required}' to `epgsql:execute_batch' +%% We want to be able to call sync if any message from the backend leaves the driver in an +%% inconsistent state needing sync. +-dialyzer({nowarn_function, [execute_batch/3]}). + %%===================================================================== roots() -> @@ -252,6 +257,8 @@ on_sql_query(InstId, PoolName, Type, NameOrSQL, Data) -> reason => Reason }), case Reason of + sync_required -> + {error, {recoverable_error, Reason}}; ecpool_empty -> {error, {recoverable_error, Reason}}; {error, error, _, undefined_table, _, _} -> @@ -307,28 +314,13 @@ do_check_prepares( prepare_sql := #{<<"send_message">> := SQL} } = State ) -> - % it's already connected. Verify if target table still exists - Workers = [Worker || {_WorkerName, Worker} <- ecpool:workers(PoolName)], - lists:foldl( - fun - (WorkerPid, ok) -> - case ecpool_worker:client(WorkerPid) of - {ok, Conn} -> - case epgsql:parse2(Conn, "get_status", SQL, []) of - {error, {_, _, _, undefined_table, _, _}} -> - {error, {undefined_table, State}}; - _ -> - ok - end; - _ -> - ok - end; - (_, Acc) -> - Acc - end, - ok, - Workers - ); + WorkerPids = [Worker || {_WorkerName, Worker} <- ecpool:workers(PoolName)], + case validate_table_existence(WorkerPids, SQL) of + ok -> + ok; + {error, undefined_table} -> + {error, {undefined_table, State}} + end; do_check_prepares(#{prepare_sql := Prepares}) when is_map(Prepares) -> ok; do_check_prepares(State = #{pool_name := PoolName, prepare_sql := {error, Prepares}}) -> @@ -344,6 +336,30 @@ do_check_prepares(State = #{pool_name := PoolName, prepare_sql := {error, Prepar {error, Error} end. +-spec validate_table_existence([pid()], binary()) -> ok | {error, undefined_table}. +validate_table_existence([WorkerPid | Rest], SQL) -> + try ecpool_worker:client(WorkerPid) of + {ok, Conn} -> + case epgsql:parse2(Conn, "", SQL, []) of + {error, {_, _, _, undefined_table, _, _}} -> + {error, undefined_table}; + Res when is_tuple(Res) andalso ok == element(1, Res) -> + ok; + Res -> + ?tp(postgres_connector_bad_parse2, #{result => Res}), + validate_table_existence(Rest, SQL) + end; + _ -> + validate_table_existence(Rest, SQL) + catch + exit:{noproc, _} -> + validate_table_existence(Rest, SQL) + end; +validate_table_existence([], _SQL) -> + %% All workers either replied an unexpected error; we will retry + %% on the next health check. + ok. + %% =================================================================== connect(Opts) -> @@ -358,13 +374,31 @@ connect(Opts) -> end. query(Conn, SQL, Params) -> - epgsql:equery(Conn, SQL, Params). + case epgsql:equery(Conn, SQL, Params) of + {error, sync_required} = Res -> + ok = epgsql:sync(Conn), + Res; + Res -> + Res + end. prepared_query(Conn, Name, Params) -> - epgsql:prepared_query2(Conn, Name, Params). + case epgsql:prepared_query2(Conn, Name, Params) of + {error, sync_required} = Res -> + ok = epgsql:sync(Conn), + Res; + Res -> + Res + end. execute_batch(Conn, Statement, Params) -> - epgsql:execute_batch(Conn, Statement, Params). + case epgsql:execute_batch(Conn, Statement, Params) of + {error, sync_required} = Res -> + ok = epgsql:sync(Conn), + Res; + Res -> + Res + end. conn_opts(Opts) -> conn_opts(Opts, []). diff --git a/changes/ee/fix-11338.en.md b/changes/ee/fix-11338.en.md new file mode 100644 index 000000000..ed1924c13 --- /dev/null +++ b/changes/ee/fix-11338.en.md @@ -0,0 +1 @@ +Fixed an issue where the PostgreSQL bridge connection could crash under high message rates. From deaac9bd73c9230d291afeecd138bafcc90b9e6a Mon Sep 17 00:00:00 2001 From: Ilya Averyanov Date: Mon, 24 Jul 2023 23:57:09 +0300 Subject: [PATCH 22/31] fix(authz): correctly identify qos of subscribe actions --- apps/emqx/include/asserts.hrl | 22 +++ apps/emqx/src/emqx_channel.erl | 19 +-- apps/emqx/test/emqx_channel_SUITE.erl | 3 +- .../test/emqx_authz_rich_actions_SUITE.erl | 138 ++++++++++++++++++ .../test/emqx_mgmt_api_clients_SUITE.erl | 2 +- 5 files changed, 170 insertions(+), 14 deletions(-) create mode 100644 apps/emqx_authz/test/emqx_authz_rich_actions_SUITE.erl diff --git a/apps/emqx/include/asserts.hrl b/apps/emqx/include/asserts.hrl index 8baa8fee8..5f27b0332 100644 --- a/apps/emqx/include/asserts.hrl +++ b/apps/emqx/include/asserts.hrl @@ -60,6 +60,28 @@ end)() ). +-define(assertNotReceive(PATTERN), + ?assertNotReceive(PATTERN, 300) +). + +-define(assertNotReceive(PATTERN, TIMEOUT), + (fun() -> + receive + X__V = PATTERN -> + erlang:error( + {assertNotReceive, [ + {module, ?MODULE}, + {line, ?LINE}, + {expression, (??PATTERN)}, + {message, X__V} + ]} + ) + after TIMEOUT -> + ok + end + end)() +). + -define(retrying(CONFIG, NUM_RETRIES, TEST_BODY_FN), begin __TEST_CASE = ?FUNCTION_NAME, (fun diff --git a/apps/emqx/src/emqx_channel.erl b/apps/emqx/src/emqx_channel.erl index 1ed6a238b..23fc8482e 100644 --- a/apps/emqx/src/emqx_channel.erl +++ b/apps/emqx/src/emqx_channel.erl @@ -492,7 +492,7 @@ handle_in( ok -> TopicFilters0 = parse_topic_filters(TopicFilters), TopicFilters1 = enrich_subopts_subid(Properties, TopicFilters0), - TupleTopicFilters0 = check_sub_authzs(SubPkt, TopicFilters1, Channel), + TupleTopicFilters0 = check_sub_authzs(TopicFilters1, Channel), HasAuthzDeny = lists:any( fun({_TopicFilter, ReasonCode}) -> ReasonCode =:= ?RC_NOT_AUTHORIZED @@ -1846,9 +1846,7 @@ authz_action(#mqtt_packet{ header = #mqtt_packet_header{qos = QoS, retain = Retain}, variable = #mqtt_packet_publish{} }) -> ?AUTHZ_PUBLISH(QoS, Retain); -authz_action(#mqtt_packet{ - header = #mqtt_packet_header{qos = QoS}, variable = #mqtt_packet_subscribe{} -}) -> +authz_action({_Topic, #{qos := QoS} = _SubOpts} = _TopicFilter) -> ?AUTHZ_SUBSCRIBE(QoS); %% Will message authz_action(#message{qos = QoS, flags = #{retain := Retain}}) -> @@ -1889,23 +1887,22 @@ check_pub_caps( %%-------------------------------------------------------------------- %% Check Sub Authorization -check_sub_authzs(Packet, TopicFilters, Channel) -> - Action = authz_action(Packet), - check_sub_authzs(Action, TopicFilters, Channel, []). +check_sub_authzs(TopicFilters, Channel) -> + check_sub_authzs(TopicFilters, Channel, []). check_sub_authzs( - Action, [TopicFilter = {Topic, _} | More], Channel = #channel{clientinfo = ClientInfo}, Acc ) -> + Action = authz_action(TopicFilter), case emqx_access_control:authorize(ClientInfo, Action, Topic) of allow -> - check_sub_authzs(Action, More, Channel, [{TopicFilter, ?RC_SUCCESS} | Acc]); + check_sub_authzs(More, Channel, [{TopicFilter, ?RC_SUCCESS} | Acc]); deny -> - check_sub_authzs(Action, More, Channel, [{TopicFilter, ?RC_NOT_AUTHORIZED} | Acc]) + check_sub_authzs(More, Channel, [{TopicFilter, ?RC_NOT_AUTHORIZED} | Acc]) end; -check_sub_authzs(_Action, [], _Channel, Acc) -> +check_sub_authzs([], _Channel, Acc) -> lists:reverse(Acc). %%-------------------------------------------------------------------- diff --git a/apps/emqx/test/emqx_channel_SUITE.erl b/apps/emqx/test/emqx_channel_SUITE.erl index f266dbcfa..5653cd2d2 100644 --- a/apps/emqx/test/emqx_channel_SUITE.erl +++ b/apps/emqx/test/emqx_channel_SUITE.erl @@ -908,8 +908,7 @@ t_check_pub_alias(_) -> t_check_sub_authzs(_) -> emqx_config:put_zone_conf(default, [authorization, enable], true), TopicFilter = {<<"t">>, ?DEFAULT_SUBOPTS}, - Subscribe = ?SUBSCRIBE_PACKET(1, [TopicFilter]), - [{TopicFilter, 0}] = emqx_channel:check_sub_authzs(Subscribe, [TopicFilter], channel()). + [{TopicFilter, 0}] = emqx_channel:check_sub_authzs([TopicFilter], channel()). t_enrich_connack_caps(_) -> ok = meck:new(emqx_mqtt_caps, [passthrough, no_history]), diff --git a/apps/emqx_authz/test/emqx_authz_rich_actions_SUITE.erl b/apps/emqx_authz/test/emqx_authz_rich_actions_SUITE.erl new file mode 100644 index 000000000..8d24b5472 --- /dev/null +++ b/apps/emqx_authz/test/emqx_authz_rich_actions_SUITE.erl @@ -0,0 +1,138 @@ +%%-------------------------------------------------------------------- +%% 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 Test suite verifies that MQTT retain and qos parameters +%% correctly reach the authorization. +%%-------------------------------------------------------------------- + +-module(emqx_authz_rich_actions_SUITE). + +-compile(nowarn_export_all). +-compile(export_all). + +-include_lib("eunit/include/eunit.hrl"). +-include_lib("common_test/include/ct.hrl"). +-include_lib("emqx/include/asserts.hrl"). + +all() -> + emqx_common_test_helpers:all(?MODULE). + +groups() -> + []. + +init_per_testcase(TestCase, Config) -> + Apps = emqx_cth_suite:start( + [ + {emqx_conf, "authorization.no_match = deny, authorization.cache.enable = false"}, + emqx_authz + ], + #{work_dir => filename:join(?config(priv_dir, Config), TestCase)} + ), + [{tc_apps, Apps} | Config]. + +end_per_testcase(_TestCase, Config) -> + emqx_cth_suite:stop(?config(tc_apps, Config)), + _ = emqx_authz:set_feature_available(rich_actions, true). + +%%------------------------------------------------------------------------------ +%% Testcases +%%------------------------------------------------------------------------------ + +t_rich_actions_subscribe(_Config) -> + ok = setup_config(#{ + <<"type">> => <<"file">>, + <<"enable">> => true, + <<"rules">> => + << + "{allow, {user, \"username\"}, {subscribe, [{qos, 1}]}, [\"t1\"]}." + "\n{allow, {user, \"username\"}, {subscribe, [{qos, 2}]}, [\"t2\"]}." + >> + }), + + {ok, C} = emqtt:start_link([{username, <<"username">>}]), + {ok, _} = emqtt:connect(C), + + ?assertMatch( + {ok, _, [1]}, + emqtt:subscribe(C, <<"t1">>, 1) + ), + + ?assertMatch( + {ok, _, [1, 2]}, + emqtt:subscribe(C, #{}, [{<<"t1">>, [{qos, 1}]}, {<<"t2">>, [{qos, 2}]}]) + ), + + ?assertMatch( + {ok, _, [128, 128]}, + emqtt:subscribe(C, #{}, [{<<"t1">>, [{qos, 2}]}, {<<"t2">>, [{qos, 1}]}]) + ), + + ok = emqtt:stop(C). + +t_rich_actions_publish(_Config) -> + ok = setup_config(#{ + <<"type">> => <<"file">>, + <<"enable">> => true, + <<"rules">> => + << + "{allow, {user, \"publisher\"}, {publish, [{qos, 0}]}, [\"t0\"]}." + "\n{allow, {user, \"publisher\"}, {publish, [{qos, 1}, {retain, true}]}, [\"t1\"]}." + "\n{allow, {user, \"subscriber\"}, subscribe, [\"#\"]}." + >> + }), + + {ok, PC} = emqtt:start_link([{username, <<"publisher">>}]), + {ok, _} = emqtt:connect(PC), + + {ok, SC} = emqtt:start_link([{username, <<"subscriber">>}]), + {ok, _} = emqtt:connect(SC), + {ok, _, _} = emqtt:subscribe(SC, <<"#">>, 1), + + _ = emqtt:publish(PC, <<"t0">>, <<"qos0">>, [{qos, 0}]), + _ = emqtt:publish(PC, <<"t1">>, <<"qos1-retain">>, [{qos, 1}, {retain, true}]), + + _ = emqtt:publish(PC, <<"t0">>, <<"qos1">>, [{qos, 1}]), + _ = emqtt:publish(PC, <<"t1">>, <<"qos1-noretain">>, [{qos, 1}, {retain, false}]), + + ?assertReceive( + {publish, #{topic := <<"t0">>, payload := <<"qos0">>}} + ), + + ?assertReceive( + {publish, #{topic := <<"t1">>, payload := <<"qos1-retain">>}} + ), + + ?assertNotReceive( + {publish, #{topic := <<"t0">>, payload := <<"qos1">>}} + ), + + ?assertNotReceive( + {publish, #{topic := <<"t1">>, payload := <<"qos1-noretain">>}} + ), + + ok = emqtt:stop(PC), + ok = emqtt:stop(SC). + +%%------------------------------------------------------------------------------ +%% Helpers +%%------------------------------------------------------------------------------ + +setup_config(Params) -> + emqx_authz_test_lib:setup_config( + Params, + #{} + ). + +stop_apps(Apps) -> + lists:foreach(fun application:stop/1, Apps). 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 47756cc4c..efdaa9c96 100644 --- a/apps/emqx_management/test/emqx_mgmt_api_clients_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_api_clients_SUITE.erl @@ -172,7 +172,7 @@ t_authz_cache(_) -> {ok, C} = emqtt:start_link(#{clientid => ClientId}), {ok, _} = emqtt:connect(C), - {ok, _, _} = emqtt:subscribe(C, <<"topic/1">>, 0), + {ok, _, _} = emqtt:subscribe(C, <<"topic/1">>, 1), ClientAuthzCachePath = emqx_mgmt_api_test_util:api_path([ "clients", From 8ec14bb07eb4481225096d4cb14b183d0f41673c Mon Sep 17 00:00:00 2001 From: Paulo Zulato Date: Mon, 24 Jul 2023 16:00:34 -0300 Subject: [PATCH 23/31] fix(topic_rewrite): handle error when target contains wildcards Fixes https://emqx.atlassian.net/browse/EMQX-10565 --- apps/emqx_modules/src/emqx_rewrite_api.erl | 28 +++++++++++++++++-- .../test/emqx_rewrite_api_SUITE.erl | 2 +- changes/ce/fix-11337.en.md | 1 + rel/i18n/emqx_rewrite_api.hocon | 5 ++++ 4 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 changes/ce/fix-11337.en.md diff --git a/apps/emqx_modules/src/emqx_rewrite_api.erl b/apps/emqx_modules/src/emqx_rewrite_api.erl index cf22daec6..4cf65da8e 100644 --- a/apps/emqx_modules/src/emqx_rewrite_api.erl +++ b/apps/emqx_modules/src/emqx_rewrite_api.erl @@ -28,6 +28,7 @@ -define(MAX_RULES_LIMIT, 20). -define(EXCEED_LIMIT, 'EXCEED_LIMIT'). +-define(BAD_REQUEST, 'BAD_REQUEST'). api_spec() -> emqx_dashboard_swagger:spec(?MODULE). @@ -62,6 +63,10 @@ schema("/mqtt/topic_rewrite") -> hoconsc:array(hoconsc:ref(emqx_modules_schema, "rewrite")), #{desc => ?DESC(update_topic_rewrite_api)} ), + 400 => emqx_dashboard_swagger:error_codes( + [?BAD_REQUEST], + ?DESC(update_topic_rewrite_api_response400) + ), 413 => emqx_dashboard_swagger:error_codes( [?EXCEED_LIMIT], ?DESC(update_topic_rewrite_api_response413) @@ -75,11 +80,30 @@ topic_rewrite(get, _Params) -> topic_rewrite(put, #{body := Body}) -> case length(Body) < ?MAX_RULES_LIMIT of true -> - ok = emqx_rewrite:update(Body), - {200, emqx_rewrite:list()}; + try + ok = emqx_rewrite:update(Body), + {200, emqx_rewrite:list()} + catch + throw:#{ + kind := validation_error, + reason := #{ + msg := "cannot_use_wildcard_for_destination_topic", + invalid_topics := InvalidTopics + } + } -> + Message = get_invalid_wildcard_topic_msg(InvalidTopics), + {400, #{code => ?BAD_REQUEST, message => Message}} + end; _ -> Message = iolist_to_binary( io_lib:format("Max rewrite rules count is ~p", [?MAX_RULES_LIMIT]) ), {413, #{code => ?EXCEED_LIMIT, message => Message}} end. + +get_invalid_wildcard_topic_msg(InvalidTopics) -> + iolist_to_binary( + io_lib:format("Cannot use wildcard for destination topic. Invalid topics: ~p", [ + InvalidTopics + ]) + ). diff --git a/apps/emqx_modules/test/emqx_rewrite_api_SUITE.erl b/apps/emqx_modules/test/emqx_rewrite_api_SUITE.erl index 6c65d351b..96d590b5c 100644 --- a/apps/emqx_modules/test/emqx_rewrite_api_SUITE.erl +++ b/apps/emqx_modules/test/emqx_rewrite_api_SUITE.erl @@ -130,7 +130,7 @@ t_mqtt_topic_rewrite_wildcard(_) -> lists:foreach( fun(Rule) -> ?assertMatch( - {ok, 500, _}, + {ok, 400, _}, request( put, uri(["mqtt", "topic_rewrite"]), diff --git a/changes/ce/fix-11337.en.md b/changes/ce/fix-11337.en.md new file mode 100644 index 000000000..c695cb87f --- /dev/null +++ b/changes/ce/fix-11337.en.md @@ -0,0 +1 @@ +Fix HTTP API error when a publish topic rewrite rule targets a topic with wildcards. Now it returns error 400 (Bad Match) instead of error 500 (Internal Error). diff --git a/rel/i18n/emqx_rewrite_api.hocon b/rel/i18n/emqx_rewrite_api.hocon index 1b56cf24d..58b7af0cd 100644 --- a/rel/i18n/emqx_rewrite_api.hocon +++ b/rel/i18n/emqx_rewrite_api.hocon @@ -15,4 +15,9 @@ update_topic_rewrite_api_response413.desc: update_topic_rewrite_api_response413.label: """Rules count exceed limit""" +update_topic_rewrite_api_response400.desc: +"""Bad request""" +update_topic_rewrite_api_response400.label: +"""Bad request""" + } From f9d3d3325bb8a3e45825cc7e257b6293083985b1 Mon Sep 17 00:00:00 2001 From: Ivan Dyachkov Date: Tue, 25 Jul 2023 13:22:40 +0200 Subject: [PATCH 24/31] chore: e5.1.1-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 991368d84..2acb56695 100644 --- a/apps/emqx/include/emqx_release.hrl +++ b/apps/emqx/include/emqx_release.hrl @@ -35,7 +35,7 @@ -define(EMQX_RELEASE_CE, "5.1.1"). %% Enterprise edition --define(EMQX_RELEASE_EE, "5.1.1-alpha.4"). +-define(EMQX_RELEASE_EE, "5.1.1-rc.1"). %% The HTTP API version -define(EMQX_API_VERSION, "5.0"). From 5178d56a38fd72455259e754897de950213ac8e9 Mon Sep 17 00:00:00 2001 From: Kinplemelon Date: Wed, 26 Jul 2023 10:20:47 +0800 Subject: [PATCH 25/31] chore: upgrade dashboard to e1.1.1-beta.9 for ee --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 948d1d20b..78a385470 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ endif # Dashboard version # from https://github.com/emqx/emqx-dashboard5 export EMQX_DASHBOARD_VERSION ?= v1.3.1 -export EMQX_EE_DASHBOARD_VERSION ?= e1.1.1-beta.4 +export EMQX_EE_DASHBOARD_VERSION ?= e1.1.1-beta.9 # `:=` should be used here, otherwise the `$(shell ...)` will be executed every time when the variable is used # In make 4.4+, for backward-compatibility the value from the original environment is used. From 718f7ce4fc164bd026ef125fdadf167c104c3313 Mon Sep 17 00:00:00 2001 From: Kinplemelon Date: Thu, 27 Jul 2023 10:37:17 +0800 Subject: [PATCH 26/31] chore: upgrade dashboard to e1.1.1 for ee --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 78a385470..280a69082 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ endif # Dashboard version # from https://github.com/emqx/emqx-dashboard5 export EMQX_DASHBOARD_VERSION ?= v1.3.1 -export EMQX_EE_DASHBOARD_VERSION ?= e1.1.1-beta.9 +export EMQX_EE_DASHBOARD_VERSION ?= e1.1.1 # `:=` should be used here, otherwise the `$(shell ...)` will be executed every time when the variable is used # In make 4.4+, for backward-compatibility the value from the original environment is used. From 5e4855334ef1346b7b166a72fab352c8b85af9c0 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Thu, 27 Jul 2023 13:39:37 +0800 Subject: [PATCH 27/31] Revert "Fix(topicidx): allow to return matches unique by record id" --- .../emqx_rule_engine/src/emqx_rule_engine.erl | 2 +- apps/emqx_rule_engine/src/emqx_rule_index.erl | 131 ++++-------- .../test/emqx_rule_engine_SUITE.erl | 39 ---- .../test/emqx_rule_index_SUITE.erl | 196 +++--------------- 4 files changed, 71 insertions(+), 297 deletions(-) diff --git a/apps/emqx_rule_engine/src/emqx_rule_engine.erl b/apps/emqx_rule_engine/src/emqx_rule_engine.erl index dd4b52d44..d92931d77 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_engine.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_engine.erl @@ -226,7 +226,7 @@ get_rules_ordered_by_ts() -> get_rules_for_topic(Topic) -> [ emqx_rule_index:get_record(M, ?RULE_TOPIC_INDEX) - || M <- emqx_rule_index:matches(Topic, ?RULE_TOPIC_INDEX, [unique]) + || M <- emqx_rule_index:matches(Topic, ?RULE_TOPIC_INDEX) ]. -spec get_rules_with_same_event(Topic :: binary()) -> [rule()]. diff --git a/apps/emqx_rule_engine/src/emqx_rule_index.erl b/apps/emqx_rule_engine/src/emqx_rule_index.erl index 4dd395b94..9c16bf3ad 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_index.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_index.erl @@ -16,29 +16,29 @@ %% @doc Topic index for matching topics to topic filters. %% -%% Works on top of ETS ordered_set table. Keys are tuples constructed from -%% parsed topic filters and record IDs, wrapped in a tuple to order them -%% strictly greater than unit tuple (`{}`). Existing table may be used if -%% existing keys will not collide with index keys. +%% Works on top of ETS ordered_set table. Keys are parsed topic filters +%% with record ID appended to the end, wrapped in a tuple to disambiguate from +%% topic filter words. Existing table may be used if existing keys will not +%% collide with index keys. %% %% Designed to effectively answer questions like: %% 1. Does any topic filter match given topic? %% 2. Which records are associated with topic filters matching given topic? -%% 3. Which topic filters match given topic? -%% 4. Which record IDs are associated with topic filters matching given topic? +%% +%% Questions like these are _only slightly_ less effective: +%% 1. Which topic filters match given topic? +%% 2. Which record IDs are associated with topic filters matching given topic? -module(emqx_rule_index). -export([insert/4]). -export([delete/3]). -export([match/2]). --export([matches/3]). +-export([matches/2]). --export([get_id/1]). --export([get_topic/1]). -export([get_record/2]). --type key(ID) :: {[binary() | '+' | '#'], {ID}}. +-type key(ID) :: [binary() | '+' | '#' | {ID}]. -type match(ID) :: key(ID). -ifdef(TEST). @@ -46,10 +46,11 @@ -endif. insert(Filter, ID, Record, Tab) -> - ets:insert(Tab, {{emqx_topic:words(Filter), {ID}}, Record}). + %% TODO: topic compact. see also in emqx_trie.erl + ets:insert(Tab, {emqx_topic:words(Filter) ++ [{ID}], Record}). delete(Filter, ID, Tab) -> - ets:delete(Tab, {emqx_topic:words(Filter), {ID}}). + ets:delete(Tab, emqx_topic:words(Filter) ++ [{ID}]). -spec match(emqx_types:topic(), ets:table()) -> match(_ID) | false. match(Topic, Tab) -> @@ -58,8 +59,8 @@ match(Topic, Tab) -> match(Words, RPrefix, Tab) -> Prefix = lists:reverse(RPrefix), - K = ets:next(Tab, {Prefix, {}}), - case match_filter(Prefix, K, Words == []) of + K = ets:next(Tab, Prefix), + case match_filter(Prefix, K, Words =/= []) of true -> K; stop -> @@ -72,7 +73,7 @@ match_rest(false, [W | Rest], RPrefix, Tab) -> match(Rest, [W | RPrefix], Tab); match_rest(plus, [W | Rest], RPrefix, Tab) -> case match(Rest, ['+' | RPrefix], Tab) of - Match = {_, _} -> + Match when is_list(Match) -> Match; false -> match(Rest, [W | RPrefix], Tab) @@ -80,86 +81,48 @@ match_rest(plus, [W | Rest], RPrefix, Tab) -> match_rest(_, [], _RPrefix, _Tab) -> false. --spec matches(emqx_types:topic(), ets:table(), _Opts :: [unique]) -> [match(_ID)]. -matches(Topic, Tab, Opts) -> +-spec matches(emqx_types:topic(), ets:table()) -> [match(_ID)]. +matches(Topic, Tab) -> {Words, RPrefix} = match_init(Topic), - AccIn = - case Opts of - [unique | _] -> #{}; - [] -> [] - end, - Matches = matches(Words, RPrefix, AccIn, Tab), - %% return rules ordered by Rule ID - case Matches of - #{} -> - maps:values(Matches); - _ -> - F = fun({_, {ID1}}, {_, {ID2}}) -> ID1 < ID2 end, - lists:sort(F, Matches) - end. + matches(Words, RPrefix, Tab). -matches(Words, RPrefix, Acc, Tab) -> +matches(Words, RPrefix, Tab) -> Prefix = lists:reverse(RPrefix), - matches(ets:next(Tab, {Prefix, {}}), Prefix, Words, RPrefix, Acc, Tab). + matches(ets:next(Tab, Prefix), Prefix, Words, RPrefix, Tab). -matches(K, Prefix, Words, RPrefix, Acc, Tab) -> - case match_filter(Prefix, K, Words == []) of +matches(K, Prefix, Words, RPrefix, Tab) -> + case match_filter(Prefix, K, Words =/= []) of true -> - matches(ets:next(Tab, K), Prefix, Words, RPrefix, match_add(K, Acc), Tab); + [K | matches(ets:next(Tab, K), Prefix, Words, RPrefix, Tab)]; stop -> - Acc; + []; Matched -> - matches_rest(Matched, Words, RPrefix, Acc, Tab) + matches_rest(Matched, Words, RPrefix, Tab) end. -matches_rest(false, [W | Rest], RPrefix, Acc, Tab) -> - matches(Rest, [W | RPrefix], Acc, Tab); -matches_rest(sharp, [W | Rest], RPrefix, Acc, Tab) -> - NAcc1 = matches([], ['#' | RPrefix], Acc, Tab), - NAcc2 = matches(Rest, ['+' | RPrefix], NAcc1, Tab), - matches(Rest, [W | RPrefix], NAcc2, Tab); -matches_rest(plus, [W | Rest], RPrefix, Acc, Tab) -> - NAcc = matches(Rest, ['+' | RPrefix], Acc, Tab), - matches(Rest, [W | RPrefix], NAcc, Tab); -matches_rest(_, [], _RPrefix, Acc, _Tab) -> - Acc. +matches_rest(false, [W | Rest], RPrefix, Tab) -> + matches(Rest, [W | RPrefix], Tab); +matches_rest(plus, [W | Rest], RPrefix, Tab) -> + matches(Rest, ['+' | RPrefix], Tab) ++ matches(Rest, [W | RPrefix], Tab); +matches_rest(_, [], _RPrefix, _Tab) -> + []. -match_add(K = {_Filter, ID}, Acc = #{}) -> - Acc#{ID => K}; -match_add(K, Acc) -> - [K | Acc]. - -match_filter(Prefix, {Filter, _ID}, NotPrefix) -> - case match_filter(Prefix, Filter) of - exact -> - % NOTE: exact match is `true` only if we match whole topic, not prefix - case NotPrefix of - true -> - true; - false -> - sharp - end; - Match -> - Match - end; -match_filter(_, '$end_of_table', _) -> - stop. - -match_filter([], []) -> - exact; -match_filter([], ['' | _]) -> - sharp; -match_filter([], ['#']) -> +match_filter([], [{_ID}], _IsPrefix = false) -> + % NOTE: exact match is `true` only if we match whole topic, not prefix + true; +match_filter([], ['#', {_ID}], _IsPrefix) -> % NOTE: naturally, '#' < '+', so this is already optimal for `match/2` true; -match_filter([], ['+' | _]) -> +match_filter([], ['+' | _], _) -> plus; -match_filter([], [_H | _]) -> +match_filter([], [_H | _], _) -> false; -match_filter([H | T1], [H | T2]) -> - match_filter(T1, T2); -match_filter([H1 | _], [H2 | _]) when H2 > H1 -> +match_filter([H | T1], [H | T2], IsPrefix) -> + match_filter(T1, T2, IsPrefix); +match_filter([H1 | _], [H2 | _], _) when H2 > H1 -> % NOTE: we're strictly past the prefix, no need to continue + stop; +match_filter(_, '$end_of_table', _) -> stop. match_init(Topic) -> @@ -172,14 +135,6 @@ match_init(Topic) -> {Words, []} end. --spec get_id(match(ID)) -> ID. -get_id({_Filter, {ID}}) -> - ID. - --spec get_topic(match(_ID)) -> emqx_types:topic(). -get_topic({Filter, _ID}) -> - emqx_topic:join(Filter). - -spec get_record(match(_ID), ets:table()) -> _Record. get_record(K, Tab) -> ets:lookup_element(Tab, K, 2). diff --git a/apps/emqx_rule_engine/test/emqx_rule_engine_SUITE.erl b/apps/emqx_rule_engine/test/emqx_rule_engine_SUITE.erl index a069a132c..8c3bd0ebb 100644 --- a/apps/emqx_rule_engine/test/emqx_rule_engine_SUITE.erl +++ b/apps/emqx_rule_engine/test/emqx_rule_engine_SUITE.erl @@ -58,7 +58,6 @@ groups() -> t_create_existing_rule, t_get_rules_for_topic, t_get_rules_for_topic_2, - t_get_rules_for_topic_3, t_get_rules_with_same_event, t_get_rule_ids_by_action, t_ensure_action_removed @@ -401,44 +400,6 @@ t_get_rules_for_topic_2(_Config) -> ]), ok. -t_get_rules_for_topic_3(_Config) -> - ok = create_rules( - [ - make_simple_rule(<<"rule-debug-5">>, <<"select * from \"simple/#\"">>), - make_simple_rule(<<"rule-debug-4">>, <<"select * from \"simple/+\"">>), - make_simple_rule(<<"rule-debug-3">>, <<"select * from \"simple/+/1\"">>), - make_simple_rule(<<"rule-debug-2">>, <<"select * from \"simple/1\"">>), - make_simple_rule( - <<"rule-debug-1">>, - <<"select * from \"simple/2\", \"simple/+\", \"simple/3\"">> - ) - ] - ), - Rules1 = get_rules_for_topic_in_e510_impl(<<"simple/1">>), - Rules2 = emqx_rule_engine:get_rules_for_topic(<<"simple/1">>), - %% assert, ensure the order of rules is the same as e5.1.0 - ?assertEqual(Rules1, Rules2), - ?assertEqual( - [<<"rule-debug-1">>, <<"rule-debug-2">>, <<"rule-debug-4">>, <<"rule-debug-5">>], - [Id || #{id := Id} <- Rules1] - ), - - ok = delete_rules_by_ids([ - <<"rule-debug-1">>, - <<"rule-debug-2">>, - <<"rule-debug-3">>, - <<"rule-debug-4">>, - <<"rule-debug-5">> - ]), - ok. - -get_rules_for_topic_in_e510_impl(Topic) -> - [ - Rule - || Rule = #{from := From} <- emqx_rule_engine:get_rules(), - emqx_topic:match_any(Topic, From) - ]. - t_get_rules_with_same_event(_Config) -> PubT = <<"simple/1">>, PubN = length(emqx_rule_engine:get_rules_with_same_event(PubT)), diff --git a/apps/emqx_rule_engine/test/emqx_rule_index_SUITE.erl b/apps/emqx_rule_engine/test/emqx_rule_index_SUITE.erl index 8a65ee8e8..cf4b67cd4 100644 --- a/apps/emqx_rule_engine/test/emqx_rule_index_SUITE.erl +++ b/apps/emqx_rule_engine/test/emqx_rule_index_SUITE.erl @@ -19,7 +19,6 @@ -compile(export_all). -compile(nowarn_export_all). --include_lib("proper/include/proper.hrl"). -include_lib("eunit/include/eunit.hrl"). all() -> @@ -30,8 +29,8 @@ t_insert(_) -> true = emqx_rule_index:insert(<<"sensor/1/metric/2">>, t_insert_1, <<>>, Tab), true = emqx_rule_index:insert(<<"sensor/+/#">>, t_insert_2, <<>>, Tab), true = emqx_rule_index:insert(<<"sensor/#">>, t_insert_3, <<>>, Tab), - ?assertEqual(<<"sensor/#">>, topic(match(<<"sensor">>, Tab))), - ?assertEqual(t_insert_3, id(match(<<"sensor">>, Tab))), + ?assertEqual(<<"sensor/#">>, get_topic(match(<<"sensor">>, Tab))), + ?assertEqual(t_insert_3, get_id(match(<<"sensor">>, Tab))), true = ets:delete(Tab). t_match(_) -> @@ -41,7 +40,7 @@ t_match(_) -> true = emqx_rule_index:insert(<<"sensor/#">>, t_match_3, <<>>, Tab), ?assertMatch( [<<"sensor/#">>, <<"sensor/+/#">>], - [topic(M) || M <- matches(<<"sensor/1">>, Tab)] + [get_topic(M) || M <- matches(<<"sensor/1">>, Tab)] ), true = ets:delete(Tab). @@ -52,7 +51,7 @@ t_match2(_) -> true = emqx_rule_index:insert(<<"+/+/#">>, t_match2_3, <<>>, Tab), ?assertEqual( [<<"#">>, <<"+/#">>, <<"+/+/#">>], - [topic(M) || M <- matches(<<"a/b/c">>, Tab)] + [get_topic(M) || M <- matches(<<"a/b/c">>, Tab)] ), ?assertEqual( false, @@ -80,7 +79,7 @@ t_match3(_) -> end, ?assertEqual( t_match3_sys, - id(match(<<"$SYS/a/b/c">>, Tab)) + get_id(match(<<"$SYS/a/b/c">>, Tab)) ), true = ets:delete(Tab). @@ -93,11 +92,11 @@ t_match4(_) -> ), ?assertEqual( [<<"/#">>, <<"/+">>], - [topic(M) || M <- matches(<<"/">>, Tab)] + [get_topic(M) || M <- matches(<<"/">>, Tab)] ), ?assertEqual( [<<"/#">>, <<"/+/a/b/c">>], - [topic(M) || M <- matches(<<"/0/a/b/c">>, Tab)] + [get_topic(M) || M <- matches(<<"/0/a/b/c">>, Tab)] ), true = ets:delete(Tab). @@ -115,11 +114,11 @@ t_match5(_) -> ), ?assertEqual( [<<"#">>, <>], - [topic(M) || M <- matches(T, Tab)] + [get_topic(M) || M <- matches(T, Tab)] ), ?assertEqual( [<<"#">>, <>, <>], - [topic(M) || M <- matches(<>, Tab)] + [get_topic(M) || M <- matches(<>, Tab)] ), true = ets:delete(Tab). @@ -128,7 +127,7 @@ t_match6(_) -> T = <<"a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z">>, W = <<"+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/#">>, emqx_rule_index:insert(W, ID = t_match6, <<>>, Tab), - ?assertEqual(ID, id(match(T, Tab))), + ?assertEqual(ID, get_id(match(T, Tab))), true = ets:delete(Tab). t_match7(_) -> @@ -136,173 +135,32 @@ t_match7(_) -> T = <<"a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z">>, W = <<"a/+/c/+/e/+/g/+/i/+/k/+/m/+/o/+/q/+/s/+/u/+/w/+/y/+/#">>, emqx_rule_index:insert(W, t_match7, <<>>, Tab), - ?assertEqual(W, topic(match(T, Tab))), + ?assertEqual(W, get_topic(match(T, Tab))), true = ets:delete(Tab). -t_match_unique(_) -> - Tab = new(), - emqx_rule_index:insert(<<"a/b/c">>, t_match_id1, <<>>, Tab), - emqx_rule_index:insert(<<"a/b/+">>, t_match_id1, <<>>, Tab), - emqx_rule_index:insert(<<"a/b/c/+">>, t_match_id2, <<>>, Tab), - ?assertEqual( - [t_match_id1, t_match_id1], - [id(M) || M <- emqx_rule_index:matches(<<"a/b/c">>, Tab, [])] - ), - ?assertEqual( - [t_match_id1], - [id(M) || M <- emqx_rule_index:matches(<<"a/b/c">>, Tab, [unique])] - ). - -t_match_ordering(_) -> - Tab = new(), - emqx_rule_index:insert(<<"a/b/+">>, t_match_id2, <<>>, Tab), - emqx_rule_index:insert(<<"a/b/c">>, t_match_id1, <<>>, Tab), - emqx_rule_index:insert(<<"a/b/#">>, t_match_id3, <<>>, Tab), - Ids1 = [id(M) || M <- emqx_rule_index:matches(<<"a/b/c">>, Tab, [])], - Ids2 = [id(M) || M <- emqx_rule_index:matches(<<"a/b/c">>, Tab, [unique])], - ?assertEqual(Ids1, Ids2), - ?assertEqual([t_match_id1, t_match_id2, t_match_id3], Ids1). - -t_match_wildcard_edge_cases(_) -> - CommonTopics = [ - <<"a/b">>, - <<"a/b/#">>, - <<"a/b/#">>, - <<"a/b/c">>, - <<"a/b/+">>, - <<"a/b/d">>, - <<"a/+/+">>, - <<"a/+/#">> - ], - Datasets = - [ - %% Topics, TopicName, Results - {CommonTopics, <<"a/b/c">>, [2, 3, 4, 5, 7, 8]}, - {CommonTopics, <<"a/b">>, [1, 2, 3, 8]}, - {[<<"+/b/c">>, <<"/">>], <<"a/b/c">>, [1]}, - {[<<"#">>, <<"/">>], <<"a">>, [1]}, - {[<<"/">>, <<"+">>], <<"a">>, [2]} - ], - F = fun({Topics, TopicName, Expected}) -> - Tab = new(), - _ = lists:foldl( - fun(T, N) -> - emqx_rule_index:insert(T, N, <<>>, Tab), - N + 1 - end, - 1, - Topics - ), - Results = [id(M) || M <- emqx_rule_index:matches(TopicName, Tab, [unique])], - case Results == Expected of - true -> - ets:delete(Tab); - false -> - ct:pal( - "Base topics: ~p~n" - "Topic name: ~p~n" - "Index results: ~p~n" - "Expected results:: ~p~n", - [Topics, TopicName, Results, Expected] - ), - error(bad_matches) - end - end, - lists:foreach(F, Datasets). - -t_prop_matches(_) -> - ?assert( - proper:quickcheck( - topic_matches_prop(), - [{max_size, 100}, {numtests, 100}] - ) - ). - -topic_matches_prop() -> - ?FORALL( - Topics0, - list(emqx_proper_types:topic()), - begin - Tab = new(), - Topics = lists:filter(fun(Topic) -> Topic =/= <<>> end, Topics0), - lists:foldl( - fun(Topic, N) -> - true = emqx_rule_index:insert(Topic, N, <<>>, Tab), - N + 1 - end, - 1, - Topics - ), - lists:foreach( - fun(Topic0) -> - Topic = topic_filter_to_topic_name(Topic0), - Ids1 = [ - emqx_rule_index:get_id(R) - || R <- emqx_rule_index:matches(Topic, Tab, [unique]) - ], - Ids2 = topic_matches(Topic, Topics), - case Ids2 == Ids1 of - true -> - ok; - false -> - ct:pal( - "Base topics: ~p~n" - "Topic name: ~p~n" - "Index results: ~p~n" - "Topic match results:: ~p~n", - [Topics, Topic, Ids1, Ids2] - ), - error(bad_matches) - end - end, - Topics - ), - true - end - ). - -%%-------------------------------------------------------------------- -%% helpers - new() -> - ets:new(?MODULE, [public, ordered_set, {read_concurrency, true}]). + ets:new(?MODULE, [public, ordered_set, {write_concurrency, true}]). match(T, Tab) -> emqx_rule_index:match(T, Tab). matches(T, Tab) -> - lists:sort(emqx_rule_index:matches(T, Tab, [])). + lists:sort(emqx_rule_index:matches(T, Tab)). -id(Match) -> - emqx_rule_index:get_id(Match). +-spec get_id(emqx_rule_index:match(ID)) -> ID. +get_id([{ID}]) -> + ID; +get_id([_ | Rest]) -> + get_id(Rest). -topic(Match) -> - emqx_rule_index:get_topic(Match). +-spec get_topic(emqx_rule_index:match(_ID)) -> emqx_types:topic(). +get_topic(K) -> + emqx_topic:join(cut_topic(K)). -topic_filter_to_topic_name(Topic) when is_binary(Topic) -> - topic_filter_to_topic_name(emqx_topic:words(Topic)); -topic_filter_to_topic_name(Words) when is_list(Words) -> - topic_filter_to_topic_name(Words, []). +cut_topic(K) -> + cut_topic(K, []). -topic_filter_to_topic_name([], Acc) -> - emqx_topic:join(lists:reverse(Acc)); -topic_filter_to_topic_name(['#' | _Rest], Acc) -> - case rand:uniform(2) of - 1 -> emqx_topic:join(lists:reverse(Acc)); - _ -> emqx_topic:join(lists:reverse([<<"_sharp">> | Acc])) - end; -topic_filter_to_topic_name(['+' | Rest], Acc) -> - topic_filter_to_topic_name(Rest, [<<"_plus">> | Acc]); -topic_filter_to_topic_name([H | Rest], Acc) -> - topic_filter_to_topic_name(Rest, [H | Acc]). - -topic_matches(Topic, Topics0) -> - Topics = lists:zip(lists:seq(1, length(Topics0)), Topics0), - lists:sort( - lists:filtermap( - fun({Id, Topic0}) -> - emqx_topic:match(Topic, Topic0) andalso {true, Id} - end, - Topics - ) - ). +cut_topic([{_ID}], Acc) -> + lists:reverse(Acc); +cut_topic([W | Rest], Acc) -> + cut_topic(Rest, [W | Acc]). From 951a96457b179d2c90e8423b53676db440cd3f65 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Thu, 27 Jul 2023 13:42:43 +0800 Subject: [PATCH 28/31] Revert "feat(index): add topic index facility " --- apps/emqx_rule_engine/include/rule_engine.hrl | 1 - .../emqx_rule_engine/src/emqx_rule_engine.erl | 71 +++----- .../src/emqx_rule_engine_app.erl | 1 - apps/emqx_rule_engine/src/emqx_rule_index.erl | 140 --------------- .../test/emqx_rule_index_SUITE.erl | 166 ------------------ changes/ce/perf-11282.en.md | 1 - 6 files changed, 21 insertions(+), 359 deletions(-) delete mode 100644 apps/emqx_rule_engine/src/emqx_rule_index.erl delete mode 100644 apps/emqx_rule_engine/test/emqx_rule_index_SUITE.erl delete mode 100644 changes/ce/perf-11282.en.md diff --git a/apps/emqx_rule_engine/include/rule_engine.hrl b/apps/emqx_rule_engine/include/rule_engine.hrl index 7df5d9941..b2a6a549e 100644 --- a/apps/emqx_rule_engine/include/rule_engine.hrl +++ b/apps/emqx_rule_engine/include/rule_engine.hrl @@ -109,7 +109,6 @@ %% Tables -define(RULE_TAB, emqx_rule_engine). --define(RULE_TOPIC_INDEX, emqx_rule_engine_topic_index). %% Allowed sql function provider modules -define(DEFAULT_SQL_FUNC_PROVIDER, emqx_rule_funcs). diff --git a/apps/emqx_rule_engine/src/emqx_rule_engine.erl b/apps/emqx_rule_engine/src/emqx_rule_engine.erl index d92931d77..66c82d3a1 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_engine.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_engine.erl @@ -176,7 +176,7 @@ create_rule(Params) -> create_rule(Params = #{id := RuleId}, CreatedAt) when is_binary(RuleId) -> case get_rule(RuleId) of - not_found -> with_parsed_rule(Params, CreatedAt, fun insert_rule/1); + not_found -> parse_and_insert(Params, CreatedAt); {ok, _} -> {error, already_exists} end. @@ -185,27 +185,18 @@ update_rule(Params = #{id := RuleId}) when is_binary(RuleId) -> case get_rule(RuleId) of not_found -> {error, not_found}; - {ok, RulePrev = #{created_at := CreatedAt}} -> - with_parsed_rule(Params, CreatedAt, fun(Rule) -> update_rule(Rule, RulePrev) end) + {ok, #{created_at := CreatedAt}} -> + parse_and_insert(Params, CreatedAt) end. -spec delete_rule(RuleId :: rule_id()) -> ok. delete_rule(RuleId) when is_binary(RuleId) -> - case get_rule(RuleId) of - not_found -> - ok; - {ok, Rule} -> - gen_server:call(?RULE_ENGINE, {delete_rule, Rule}, ?T_CALL) - end. + gen_server:call(?RULE_ENGINE, {delete_rule, RuleId}, ?T_CALL). -spec insert_rule(Rule :: rule()) -> ok. insert_rule(Rule) -> gen_server:call(?RULE_ENGINE, {insert_rule, Rule}, ?T_CALL). --spec update_rule(Rule :: rule(), RulePrev :: rule()) -> ok. -update_rule(Rule, RulePrev) -> - gen_server:call(?RULE_ENGINE, {update_rule, Rule, RulePrev}, ?T_CALL). - %%---------------------------------------------------------------------------------------- %% Rule Management %%---------------------------------------------------------------------------------------- @@ -225,13 +216,13 @@ get_rules_ordered_by_ts() -> -spec get_rules_for_topic(Topic :: binary()) -> [rule()]. get_rules_for_topic(Topic) -> [ - emqx_rule_index:get_record(M, ?RULE_TOPIC_INDEX) - || M <- emqx_rule_index:matches(Topic, ?RULE_TOPIC_INDEX) + Rule + || Rule = #{from := From} <- get_rules(), + emqx_topic:match_any(Topic, From) ]. -spec get_rules_with_same_event(Topic :: binary()) -> [rule()]. get_rules_with_same_event(Topic) -> - %% TODO: event matching index not implemented yet EventName = emqx_rule_events:event_name(Topic), [ Rule @@ -241,7 +232,6 @@ get_rules_with_same_event(Topic) -> -spec get_rule_ids_by_action(action_name()) -> [rule_id()]. get_rule_ids_by_action(BridgeId) when is_binary(BridgeId) -> - %% TODO: bridge matching index not implemented yet [ Id || #{actions := Acts, id := Id, from := Froms} <- get_rules(), @@ -249,7 +239,6 @@ get_rule_ids_by_action(BridgeId) when is_binary(BridgeId) -> references_ingress_bridge(Froms, BridgeId) ]; get_rule_ids_by_action(#{function := FuncName}) when is_binary(FuncName) -> - %% TODO: action id matching index not implemented yet {Mod, Fun} = case string:split(FuncName, ":", leading) of [M, F] -> {binary_to_module(M), F}; @@ -422,17 +411,10 @@ init([]) -> {ok, #{}}. handle_call({insert_rule, Rule}, _From, State) -> - ok = do_insert_rule(Rule), - ok = do_update_rule_index(Rule), - {reply, ok, State}; -handle_call({update_rule, Rule, RulePrev}, _From, State) -> - ok = do_delete_rule_index(RulePrev), - ok = do_insert_rule(Rule), - ok = do_update_rule_index(Rule), + do_insert_rule(Rule), {reply, ok, State}; handle_call({delete_rule, Rule}, _From, State) -> - ok = do_delete_rule_index(Rule), - ok = do_delete_rule(Rule), + do_delete_rule(Rule), {reply, ok, State}; handle_call(Req, _From, State) -> ?SLOG(error, #{msg => "unexpected_call", request => Req}), @@ -456,7 +438,7 @@ code_change(_OldVsn, State, _Extra) -> %% Internal Functions %%---------------------------------------------------------------------------------------- -with_parsed_rule(Params = #{id := RuleId, sql := Sql, actions := Actions}, CreatedAt, Fun) -> +parse_and_insert(Params = #{id := RuleId, sql := Sql, actions := Actions}, CreatedAt) -> case emqx_rule_sqlparser:parse(Sql) of {ok, Select} -> Rule = #{ @@ -477,7 +459,7 @@ with_parsed_rule(Params = #{id := RuleId, sql := Sql, actions := Actions}, Creat conditions => emqx_rule_sqlparser:select_where(Select) %% -- calculated fields end }, - ok = Fun(Rule), + ok = insert_rule(Rule), {ok, Rule}; {error, Reason} -> {error, Reason} @@ -489,27 +471,16 @@ do_insert_rule(#{id := Id} = Rule) -> true = ets:insert(?RULE_TAB, {Id, maps:remove(id, Rule)}), ok. -do_delete_rule(#{id := Id} = Rule) -> - ok = unload_hooks_for_rule(Rule), - ok = clear_metrics_for_rule(Id), - true = ets:delete(?RULE_TAB, Id), - ok. - -do_update_rule_index(#{id := Id, from := From} = Rule) -> - ok = lists:foreach( - fun(Topic) -> - true = emqx_rule_index:insert(Topic, Id, Rule, ?RULE_TOPIC_INDEX) - end, - From - ). - -do_delete_rule_index(#{id := Id, from := From}) -> - ok = lists:foreach( - fun(Topic) -> - true = emqx_rule_index:delete(Topic, Id, ?RULE_TOPIC_INDEX) - end, - From - ). +do_delete_rule(RuleId) -> + case get_rule(RuleId) of + {ok, Rule} -> + ok = unload_hooks_for_rule(Rule), + ok = clear_metrics_for_rule(RuleId), + true = ets:delete(?RULE_TAB, RuleId), + ok; + not_found -> + ok + end. parse_actions(Actions) -> [do_parse_action(Act) || Act <- Actions]. 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 28515cb1a..d8b031bdd 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_engine_app.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_engine_app.erl @@ -26,7 +26,6 @@ start(_Type, _Args) -> _ = ets:new(?RULE_TAB, [named_table, public, ordered_set, {read_concurrency, true}]), - _ = ets:new(?RULE_TOPIC_INDEX, [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(), diff --git a/apps/emqx_rule_engine/src/emqx_rule_index.erl b/apps/emqx_rule_engine/src/emqx_rule_index.erl deleted file mode 100644 index 9c16bf3ad..000000000 --- a/apps/emqx_rule_engine/src/emqx_rule_index.erl +++ /dev/null @@ -1,140 +0,0 @@ -%%-------------------------------------------------------------------- -%% 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. -%%-------------------------------------------------------------------- - -%% @doc Topic index for matching topics to topic filters. -%% -%% Works on top of ETS ordered_set table. Keys are parsed topic filters -%% with record ID appended to the end, wrapped in a tuple to disambiguate from -%% topic filter words. Existing table may be used if existing keys will not -%% collide with index keys. -%% -%% Designed to effectively answer questions like: -%% 1. Does any topic filter match given topic? -%% 2. Which records are associated with topic filters matching given topic? -%% -%% Questions like these are _only slightly_ less effective: -%% 1. Which topic filters match given topic? -%% 2. Which record IDs are associated with topic filters matching given topic? - --module(emqx_rule_index). - --export([insert/4]). --export([delete/3]). --export([match/2]). --export([matches/2]). - --export([get_record/2]). - --type key(ID) :: [binary() | '+' | '#' | {ID}]. --type match(ID) :: key(ID). - --ifdef(TEST). --export_type([match/1]). --endif. - -insert(Filter, ID, Record, Tab) -> - %% TODO: topic compact. see also in emqx_trie.erl - ets:insert(Tab, {emqx_topic:words(Filter) ++ [{ID}], Record}). - -delete(Filter, ID, Tab) -> - ets:delete(Tab, emqx_topic:words(Filter) ++ [{ID}]). - --spec match(emqx_types:topic(), ets:table()) -> match(_ID) | false. -match(Topic, Tab) -> - {Words, RPrefix} = match_init(Topic), - match(Words, RPrefix, Tab). - -match(Words, RPrefix, Tab) -> - Prefix = lists:reverse(RPrefix), - K = ets:next(Tab, Prefix), - case match_filter(Prefix, K, Words =/= []) of - true -> - K; - stop -> - false; - Matched -> - match_rest(Matched, Words, RPrefix, Tab) - end. - -match_rest(false, [W | Rest], RPrefix, Tab) -> - match(Rest, [W | RPrefix], Tab); -match_rest(plus, [W | Rest], RPrefix, Tab) -> - case match(Rest, ['+' | RPrefix], Tab) of - Match when is_list(Match) -> - Match; - false -> - match(Rest, [W | RPrefix], Tab) - end; -match_rest(_, [], _RPrefix, _Tab) -> - false. - --spec matches(emqx_types:topic(), ets:table()) -> [match(_ID)]. -matches(Topic, Tab) -> - {Words, RPrefix} = match_init(Topic), - matches(Words, RPrefix, Tab). - -matches(Words, RPrefix, Tab) -> - Prefix = lists:reverse(RPrefix), - matches(ets:next(Tab, Prefix), Prefix, Words, RPrefix, Tab). - -matches(K, Prefix, Words, RPrefix, Tab) -> - case match_filter(Prefix, K, Words =/= []) of - true -> - [K | matches(ets:next(Tab, K), Prefix, Words, RPrefix, Tab)]; - stop -> - []; - Matched -> - matches_rest(Matched, Words, RPrefix, Tab) - end. - -matches_rest(false, [W | Rest], RPrefix, Tab) -> - matches(Rest, [W | RPrefix], Tab); -matches_rest(plus, [W | Rest], RPrefix, Tab) -> - matches(Rest, ['+' | RPrefix], Tab) ++ matches(Rest, [W | RPrefix], Tab); -matches_rest(_, [], _RPrefix, _Tab) -> - []. - -match_filter([], [{_ID}], _IsPrefix = false) -> - % NOTE: exact match is `true` only if we match whole topic, not prefix - true; -match_filter([], ['#', {_ID}], _IsPrefix) -> - % NOTE: naturally, '#' < '+', so this is already optimal for `match/2` - true; -match_filter([], ['+' | _], _) -> - plus; -match_filter([], [_H | _], _) -> - false; -match_filter([H | T1], [H | T2], IsPrefix) -> - match_filter(T1, T2, IsPrefix); -match_filter([H1 | _], [H2 | _], _) when H2 > H1 -> - % NOTE: we're strictly past the prefix, no need to continue - stop; -match_filter(_, '$end_of_table', _) -> - stop. - -match_init(Topic) -> - case emqx_topic:words(Topic) of - [W = <<"$", _/bytes>> | Rest] -> - % NOTE - % This will effectively skip attempts to match special topics to `#` or `+/...`. - {Rest, [W]}; - Words -> - {Words, []} - end. - --spec get_record(match(_ID), ets:table()) -> _Record. -get_record(K, Tab) -> - ets:lookup_element(Tab, K, 2). diff --git a/apps/emqx_rule_engine/test/emqx_rule_index_SUITE.erl b/apps/emqx_rule_engine/test/emqx_rule_index_SUITE.erl deleted file mode 100644 index cf4b67cd4..000000000 --- a/apps/emqx_rule_engine/test/emqx_rule_index_SUITE.erl +++ /dev/null @@ -1,166 +0,0 @@ -%%-------------------------------------------------------------------- -%% 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_rule_index_SUITE). - --compile(export_all). --compile(nowarn_export_all). - --include_lib("eunit/include/eunit.hrl"). - -all() -> - emqx_common_test_helpers:all(?MODULE). - -t_insert(_) -> - Tab = new(), - true = emqx_rule_index:insert(<<"sensor/1/metric/2">>, t_insert_1, <<>>, Tab), - true = emqx_rule_index:insert(<<"sensor/+/#">>, t_insert_2, <<>>, Tab), - true = emqx_rule_index:insert(<<"sensor/#">>, t_insert_3, <<>>, Tab), - ?assertEqual(<<"sensor/#">>, get_topic(match(<<"sensor">>, Tab))), - ?assertEqual(t_insert_3, get_id(match(<<"sensor">>, Tab))), - true = ets:delete(Tab). - -t_match(_) -> - Tab = new(), - true = emqx_rule_index:insert(<<"sensor/1/metric/2">>, t_match_1, <<>>, Tab), - true = emqx_rule_index:insert(<<"sensor/+/#">>, t_match_2, <<>>, Tab), - true = emqx_rule_index:insert(<<"sensor/#">>, t_match_3, <<>>, Tab), - ?assertMatch( - [<<"sensor/#">>, <<"sensor/+/#">>], - [get_topic(M) || M <- matches(<<"sensor/1">>, Tab)] - ), - true = ets:delete(Tab). - -t_match2(_) -> - Tab = new(), - true = emqx_rule_index:insert(<<"#">>, t_match2_1, <<>>, Tab), - true = emqx_rule_index:insert(<<"+/#">>, t_match2_2, <<>>, Tab), - true = emqx_rule_index:insert(<<"+/+/#">>, t_match2_3, <<>>, Tab), - ?assertEqual( - [<<"#">>, <<"+/#">>, <<"+/+/#">>], - [get_topic(M) || M <- matches(<<"a/b/c">>, Tab)] - ), - ?assertEqual( - false, - emqx_rule_index:match(<<"$SYS/broker/zenmq">>, Tab) - ), - true = ets:delete(Tab). - -t_match3(_) -> - Tab = new(), - Records = [ - {<<"d/#">>, t_match3_1}, - {<<"a/b/+">>, t_match3_2}, - {<<"a/#">>, t_match3_3}, - {<<"#">>, t_match3_4}, - {<<"$SYS/#">>, t_match3_sys} - ], - lists:foreach( - fun({Topic, ID}) -> emqx_rule_index:insert(Topic, ID, <<>>, Tab) end, - Records - ), - Matched = matches(<<"a/b/c">>, Tab), - case length(Matched) of - 3 -> ok; - _ -> error({unexpected, Matched}) - end, - ?assertEqual( - t_match3_sys, - get_id(match(<<"$SYS/a/b/c">>, Tab)) - ), - true = ets:delete(Tab). - -t_match4(_) -> - Tab = new(), - Records = [{<<"/#">>, t_match4_1}, {<<"/+">>, t_match4_2}, {<<"/+/a/b/c">>, t_match4_3}], - lists:foreach( - fun({Topic, ID}) -> emqx_rule_index:insert(Topic, ID, <<>>, Tab) end, - Records - ), - ?assertEqual( - [<<"/#">>, <<"/+">>], - [get_topic(M) || M <- matches(<<"/">>, Tab)] - ), - ?assertEqual( - [<<"/#">>, <<"/+/a/b/c">>], - [get_topic(M) || M <- matches(<<"/0/a/b/c">>, Tab)] - ), - true = ets:delete(Tab). - -t_match5(_) -> - Tab = new(), - T = <<"a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z">>, - Records = [ - {<<"#">>, t_match5_1}, - {<>, t_match5_2}, - {<>, t_match5_3} - ], - lists:foreach( - fun({Topic, ID}) -> emqx_rule_index:insert(Topic, ID, <<>>, Tab) end, - Records - ), - ?assertEqual( - [<<"#">>, <>], - [get_topic(M) || M <- matches(T, Tab)] - ), - ?assertEqual( - [<<"#">>, <>, <>], - [get_topic(M) || M <- matches(<>, Tab)] - ), - true = ets:delete(Tab). - -t_match6(_) -> - Tab = new(), - T = <<"a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z">>, - W = <<"+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/+/#">>, - emqx_rule_index:insert(W, ID = t_match6, <<>>, Tab), - ?assertEqual(ID, get_id(match(T, Tab))), - true = ets:delete(Tab). - -t_match7(_) -> - Tab = new(), - T = <<"a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z">>, - W = <<"a/+/c/+/e/+/g/+/i/+/k/+/m/+/o/+/q/+/s/+/u/+/w/+/y/+/#">>, - emqx_rule_index:insert(W, t_match7, <<>>, Tab), - ?assertEqual(W, get_topic(match(T, Tab))), - true = ets:delete(Tab). - -new() -> - ets:new(?MODULE, [public, ordered_set, {write_concurrency, true}]). - -match(T, Tab) -> - emqx_rule_index:match(T, Tab). - -matches(T, Tab) -> - lists:sort(emqx_rule_index:matches(T, Tab)). - --spec get_id(emqx_rule_index:match(ID)) -> ID. -get_id([{ID}]) -> - ID; -get_id([_ | Rest]) -> - get_id(Rest). - --spec get_topic(emqx_rule_index:match(_ID)) -> emqx_types:topic(). -get_topic(K) -> - emqx_topic:join(cut_topic(K)). - -cut_topic(K) -> - cut_topic(K, []). - -cut_topic([{_ID}], Acc) -> - lists:reverse(Acc); -cut_topic([W | Rest], Acc) -> - cut_topic(Rest, [W | Acc]). diff --git a/changes/ce/perf-11282.en.md b/changes/ce/perf-11282.en.md deleted file mode 100644 index 107889957..000000000 --- a/changes/ce/perf-11282.en.md +++ /dev/null @@ -1 +0,0 @@ -Added indexing to the rule engine's topic matching to improve rule search performance. From 50a0900d92024120e7b5056e5bdf085fb48b7b97 Mon Sep 17 00:00:00 2001 From: Ivan Dyachkov Date: Thu, 27 Jul 2023 12:18:03 +0200 Subject: [PATCH 29/31] chore: e5.1.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 2acb56695..102a28f4f 100644 --- a/apps/emqx/include/emqx_release.hrl +++ b/apps/emqx/include/emqx_release.hrl @@ -35,7 +35,7 @@ -define(EMQX_RELEASE_CE, "5.1.1"). %% Enterprise edition --define(EMQX_RELEASE_EE, "5.1.1-rc.1"). +-define(EMQX_RELEASE_EE, "5.1.1"). %% The HTTP API version -define(EMQX_API_VERSION, "5.0"). From 889bd9cd61759ad5b4a0a265baffd8a59cf3a8f4 Mon Sep 17 00:00:00 2001 From: Ivan Dyachkov Date: Thu, 27 Jul 2023 12:31:43 +0200 Subject: [PATCH 30/31] docs: add changelog for e5.1.1 --- changes/e5.1.1.en.md | 127 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 changes/e5.1.1.en.md diff --git a/changes/e5.1.1.en.md b/changes/e5.1.1.en.md new file mode 100644 index 000000000..9f547e5f1 --- /dev/null +++ b/changes/e5.1.1.en.md @@ -0,0 +1,127 @@ +## e5.1.1 + +## Enhancements + +- [#10667](https://github.com/emqx/emqx/pull/10667) The MongoDB connector and bridge have been refactored into a separate app to improve the code structure. +- [#11115](https://github.com/emqx/emqx/pull/11115) Added info logs to indicate when buffered messages are dropped due to time-to-live (TTL) expiration. +- [#11133](https://github.com/emqx/emqx/pull/11133) Renamed `deliver_rate` to `delivery_rate` in the configuration of `retainer`, while being compatible with the previous `deliver_rate`. +- [#11137](https://github.com/emqx/emqx/pull/11137) Refactored the Dashboard listener configuration to use a nested `ssl_options` field for SSL settings. +- [#11138](https://github.com/emqx/emqx/pull/11138) Changed the default value of k8s `api_server` from `http://127.0.0.1:9091` to `https://kubernetes.default.svc:443`. + - `emqx_ctl conf show cluster` no longer displays irrelevant configuration items when `discovery_strategy=static`. + Configuration information related to `etcd/k8s/dns` will not be shown. + - Removed `zones `(deprecated config key) from `emqx_ctl conf show_keys`. +- [#11165](https://github.com/emqx/emqx/pull/11165) Removed the `/configs/limiter` API from `swagger.json`. Only the API documentation was removed, + and the `/configs/limiter` API functionalities remain unchanged. +- [#11166](https://github.com/emqx/emqx/pull/11166) Added 3 random SQL functions to the rule engine: + - `random()`: Generates a random number between 0 and 1 (0.0 =< X < 1.0). + - `uuid_v4()`: Generates a random UUID (version 4) string. + - `uuid_v4_no_hyphen()`: Generates a random UUID (version 4) string without hyphens. +- [#11180](https://github.com/emqx/emqx/pull/11180) Added a new configuration API `/configs` (GET/PUT) that supports reloading the HOCON format configuration file. +- [#11226](https://github.com/emqx/emqx/pull/11226) Unified the listener switch to `enable`, while being compatible with the previous `enabled`. +- [#11249](https://github.com/emqx/emqx/pull/11249) Added `/license/setting` REST API endpoint to read and update licensed connections usage alarm watermark. +- [#11251](https://github.com/emqx/emqx/pull/11251) Added the `/cluster/topology` REST API endpoint: + A `GET` request to this endpoint returns the cluster topology, showing connections between RLOG core and replicant nodes. +- [#11253](https://github.com/emqx/emqx/pull/11253) The Webhook/HTTP bridge has been refactored into its own Erlang application. This allows for more flexibility in the future and allows the bridge to be run as a standalone application. +- [#11079](https://github.com/emqx/emqx/pull/11079) Added support for custom headers in messages for Kafka bridge producer mode. +- [#11132](https://github.com/emqx/emqx/pull/11132) Added support for MQTT action authorization based on QoS level and Retain flag values. + Now, EMQX can verify whether clients have the permission to publish/subscribe using specific QoS levels, and whether they have the permission to publish retained messages. +- [#11207](https://github.com/emqx/emqx/pull/11207) Updated the driver versions of multiple data bridges to enhance security and ensure that sensitive data will not be leaked. This includes: + - TDengine + - MongoDB + - MySQL + - Clickhouse +- [#11241](https://github.com/emqx/emqx/pull/11241) Schema Registry has been refactored into its own Erlang application. This allows for more flexibility in the future. +- [#11020](https://github.com/emqx/emqx/pull/11020) Upgraded emqtt dependency to prevent sensitive data leakage in the debug log. +- [#11135](https://github.com/emqx/emqx/pull/11135) Improved time offset parser in rule engine and return uniform error codes. +- [#11236](https://github.com/emqx/emqx/pull/11236) Improved the speed of clients querying in REST API `/clients` endpoint with default parameters. + +## Bug Fixes + +- [#11004](https://github.com/emqx/emqx/pull/11004) Wildcards are no longer allowed for the destination topic in topic rewrite. +- [#11026](https://github.com/emqx/emqx/pull/11026) Addressed an inconsistency in the usage of `div` and `mod` operations within the rule engine. Previously, the `div'` operation could only be used as an infix operation, and `mod` could only be applied through a function call. Now, both `div` and `mod` can be used via function call syntax and infix syntax. +- [#11037](https://github.com/emqx/emqx/pull/11037) When starting an HTTP connector, EMQX now returns a descriptive error in case the system is unable to connect to the remote target system. +- [#11039](https://github.com/emqx/emqx/pull/11039) Fixed database number validation for Redis connector. Previously, negative numbers were accepted as valid database numbers. +- [#11074](https://github.com/emqx/emqx/pull/11074) Fixed a bug to adhere to Protocol spec MQTT-5.0 [MQTT-3.8.3-4]. +- [#11077](https://github.com/emqx/emqx/pull/11077) Fixed a crash when updating listener binding with a non-integer port. +- [#11094](https://github.com/emqx/emqx/pull/11094) Fixed an issue where connection errors in Kafka Producer would not be reported when reconnecting the bridge. +- [#11103](https://github.com/emqx/emqx/pull/11103) Updated `erlcloud` dependency. +- [#11106](https://github.com/emqx/emqx/pull/11106) Added validation for the maximum number of `worker_pool_size` of a bridge resource. + Now the maximum amount is 1024 to avoid large memory consumption from an unreasonable number of workers. +- [#11118](https://github.com/emqx/emqx/pull/11118) Ensured that validation errors in REST API responses are slightly less confusing. Now, if there are out-of-range errors, they will be presented as `{"value": 42, "reason": {"expected": "1..10"}, ...}`, replacing the previous usage of `expected_type` with `expected`. +- [#11126](https://github.com/emqx/emqx/pull/11126) Rule metrics for async mode bridges will set failure counters correctly now. +- [#11134](https://github.com/emqx/emqx/pull/11134) Fixed the value of the uppercase `authorization` header not being obfuscated in the log. +- [#11139](https://github.com/emqx/emqx/pull/11139) The Redis connector has been refactored into its own Erlang application to improve the code structure. +- [#11145](https://github.com/emqx/emqx/pull/11145) Added several fixes and improvements in Ekka and Mria. + Ekka: + - Improved cluster discovery log messages to consistently describe actual events + [Ekka PR](https://github.com/emqx/ekka/pull/204). + - Removed deprecated cluster auto-clean configuration parameter (it has been moved to Mria) + [Ekka PR](https://github.com/emqx/ekka/pull/203). + Mria: + - Ping now only runs on replicant nodes. Previously, `mria_lb` was trying to ping both stopped and running + replicant nodes, which could result in timeout errors. + [Mria PR](https://github.com/emqx/mria/pull/146) + - Used `null_copies` storage when copying `$mria_rlog_sync` table. + This fix has no effect on EMQX for now, as `$mria_rlog_sync` is only used in `mria:sync_transaction/2,3,4`, + which is not utilized by EMQX. + [Mria PR](https://github.com/emqx/mria/pull/144) +- [#11148](https://github.com/emqx/emqx/pull/11148) Fixed an issue when nodes tried to synchronize configuration update operations to a node which has already left the cluster. +- [#11150](https://github.com/emqx/emqx/pull/11150) Wait for Mria table when emqx_psk app is being started to ensure that PSK data is synced to replicant nodes even if they don't have init PSK file. +- [#11151](https://github.com/emqx/emqx/pull/11151) The MySQL connector has been refactored into its own Erlang application to improve the code structure. +- [#11158](https://github.com/emqx/emqx/pull/11158) Wait for Mria table when the mnesia backend of retainer starts to avoid a possible error of the retainer when joining a cluster. +- [#11162](https://github.com/emqx/emqx/pull/11162) Fixed an issue in webhook bridge where, in async query mode, HTTP status codes like 4XX and 5XX would be treated as successes in the bridge metrics. +- [#11164](https://github.com/emqx/emqx/pull/11164) Reintroduced support for nested (i.e.: `${payload.a.b.c}`) placeholders for extracting data from rule action messages without the need for calling `json_decode(payload)` first. +- [#11172](https://github.com/emqx/emqx/pull/11172) Fixed the `payload` field in rule engine SQL being duplicated in the below situations: + - When using a `foreach` sentence without the `as` sub-expression and selecting all fields (using the `*` or omitting the `do` sub-expression). + For example: + `FOREACH payload.sensors FROM "t/#"` + - When selecting the `payload` field and all fields. + For example: + `SELECT payload.sensors, * FROM "t/#"` +- [#11174](https://github.com/emqx/emqx/pull/11174) Fixed the encoding of the `server` key coming from an ingress MQTT bridge. + Before the fix, it was encoded as a list of integers corresponding to the ASCII characters of the server string. +- [#11184](https://github.com/emqx/emqx/pull/11184) Config value for `mqtt.max_packet_size` now has a max value of 256MB as defined by the protocol. +- [#11192](https://github.com/emqx/emqx/pull/11192) Fixed an issue with producing invalid HOCON file when an atom type was used. Also removed unnecessary `"` around keys and latin1 strings from HOCON file. +- [#11195](https://github.com/emqx/emqx/pull/11195) Fixed an issue where the REST API could create duplicate subscriptions for specified clients of the Stomp gateway. +- [#11206](https://github.com/emqx/emqx/pull/11206) Made the `username` and `password` params of CoAP client optional in connection mode. +- [#11208](https://github.com/emqx/emqx/pull/11208) Fixed the issue of abnormal data statistics for LwM2M clients. +- [#11211](https://github.com/emqx/emqx/pull/11211) HTTP API `DELETE` operations on non-existent resources now consistently returns `404`. +- [#11214](https://github.com/emqx/emqx/pull/11214) Fixed a bug where node configuration may fail to synchronize correctly when the node joins the cluster. +- [#11229](https://github.com/emqx/emqx/pull/11229) Fixed an issue that prevented plugins from starting/stopping after changing configuration via `emqx ctl conf load`. +- [#11237](https://github.com/emqx/emqx/pull/11237) The `headers` default value in /prometheus API should be a map instead of a list. +- [#11250](https://github.com/emqx/emqx/pull/11250) Fixed a bug when the order of MQTT packets withing a WebSocket packet will be reversed. +- [#11271](https://github.com/emqx/emqx/pull/11271) Ensured that the range of all percentage type configurations is from 0% to 100% in the REST API and configuration. For example, `sysom.os.sysmem_high_watermark=101%` is invalid now. +- [#11272](https://github.com/emqx/emqx/pull/11272) Fixed a typo in the log, where an abnormal `PUBREL` packet was mistakenly referred to as `pubrec`. +- [#11281](https://github.com/emqx/emqx/pull/11281) Restored support for the special `$queue/` shared subscription topic prefix. +- [#11294](https://github.com/emqx/emqx/pull/11294) Fixed `emqx ctl cluster join`, `leave`, and `status` commands. +- [#11306](https://github.com/emqx/emqx/pull/11306) Fixed rule action metrics inconsistency where dropped requests were not accounted for. +- [#11309](https://github.com/emqx/emqx/pull/11309) Improved startup order of EMQX applications. Simplified build scripts and improved code reuse. +- [#11322](https://github.com/emqx/emqx/pull/11322) Added support for importing additional configurations from EMQX backup file (`emqx ctl import` command): + - rule_engine (previously not imported due to the bug) + - topic_metrics (previously not implemented) + - slow_subs (previously not implemented). +- [#10645](https://github.com/emqx/emqx/pull/10645) Changed health check for Oracle Database, PostgreSQL, MySQL and Kafka Producer data bridges to ensure target table/topic exists. +- [#11107](https://github.com/emqx/emqx/pull/11107) MongoDB bridge health check now returns the failure reason. +- [#11139](https://github.com/emqx/emqx/pull/11139) The Redis bridge has been refactored into its own Erlang application to improve the code structure and to make it easier to maintain. +- [#11151](https://github.com/emqx/emqx/pull/11151) The MySQL bridge has been refactored into its own Erlang application to improve the code structure and to make it easier to maintain. +- [#11163](https://github.com/emqx/emqx/pull/11163) Hid `topology.pool_size` in MondoDB bridges and fixed it to 1 to avoid confusion. +- [#11175](https://github.com/emqx/emqx/pull/11175) Now when using a nonexistent hostname for connecting to MySQL, a 400 error is returned rather than 503 in the REST API. +- [#11198](https://github.com/emqx/emqx/pull/11198) Fixed global rebalance status evaluation on replicant nodes. Previously, `/api/v5/load_rebalance/global_status` API method could return incomplete results if handled by a replicant node. +- [#11223](https://github.com/emqx/emqx/pull/11223) In InfluxDB bridging, mixing decimals and integers in a field may lead to serialization failure in the Influx Line Protocol, resulting in the inability to write to the InfluxDB bridge (when the decimal point is 0, InfluxDB mistakenly interprets it as an integer). + See also: [InfluxDB v2.7 Line-Protocol](https://docs.influxdata.com/influxdb/v2.7/reference/syntax/line-protocol/#float). +- [#11225](https://github.com/emqx/emqx/pull/11225) The `username` field in PostgreSQL/Timescale/MatrixDB bridges configuration is now a required one. +- [#11242](https://github.com/emqx/emqx/pull/11242) Restarted emqx_ee_schema_registry when a node joins a cluster. As emqx_ee_schema_registry uses Mria tables, a node joining a cluster needs to restart this application in order to start relevant Mria shard processes, ensuring a correct behaviour in Core/Replicant mode. +- [#11266](https://github.com/emqx/emqx/pull/11266) Fixed and improved support for TDengine `insert` syntax: + 1. Added support for inserting into multi-table in the template. + For example: + `insert into table_1 values (${ts}, '${id}', '${topic}') + table_2 values (${ts}, '${id}', '${topic}')` + 2. Added support for mixing prefixes/suffixes and placeholders in the template. + For example: + `insert into table_${topic} values (${ts}, '${id}', '${topic}')` + Note: This is a breaking change. Previously, the values of string type were quoted automatically, but now they must be quoted explicitly. + For example: + `insert into table values (${ts}, '${a_string}')` +- [#11307](https://github.com/emqx/emqx/pull/11307) Fixed check for table existence to return a more friendly message in the Oracle bridge. +- [#11316](https://github.com/emqx/emqx/pull/11316) Fixed Pool Size value not being considered in Oracle Bridge. +- [#11326](https://github.com/emqx/emqx/pull/11326) Fixed return error checking on table validation in the Oracle bridge. From 63adeabf72c5a68c949af1dba88fb08ff72aaaf1 Mon Sep 17 00:00:00 2001 From: Ivan Dyachkov Date: Thu, 27 Jul 2023 15:29:03 +0200 Subject: [PATCH 31/31] chore: bump app versions --- apps/emqx_connector/src/emqx_connector.app.src | 2 +- apps/emqx_modules/src/emqx_modules.app.src | 2 +- apps/emqx_oracle/src/emqx_oracle.app.src | 2 +- apps/emqx_rule_engine/src/emqx_rule_engine.app.src | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/emqx_connector/src/emqx_connector.app.src b/apps/emqx_connector/src/emqx_connector.app.src index 9dcec9187..7614ddac3 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.27"}, + {vsn, "0.1.28"}, {registered, []}, {mod, {emqx_connector_app, []}}, {applications, [ diff --git a/apps/emqx_modules/src/emqx_modules.app.src b/apps/emqx_modules/src/emqx_modules.app.src index 4de1c2e9b..b7a9d7f4d 100644 --- a/apps/emqx_modules/src/emqx_modules.app.src +++ b/apps/emqx_modules/src/emqx_modules.app.src @@ -1,7 +1,7 @@ %% -*- mode: erlang -*- {application, emqx_modules, [ {description, "EMQX Modules"}, - {vsn, "5.0.18"}, + {vsn, "5.0.19"}, {modules, []}, {applications, [kernel, stdlib, emqx, emqx_ctl]}, {mod, {emqx_modules_app, []}}, diff --git a/apps/emqx_oracle/src/emqx_oracle.app.src b/apps/emqx_oracle/src/emqx_oracle.app.src index be3ed3276..e2d6d856f 100644 --- a/apps/emqx_oracle/src/emqx_oracle.app.src +++ b/apps/emqx_oracle/src/emqx_oracle.app.src @@ -1,6 +1,6 @@ {application, emqx_oracle, [ {description, "EMQX Enterprise Oracle Database Connector"}, - {vsn, "0.1.4"}, + {vsn, "0.1.5"}, {registered, []}, {applications, [ kernel, 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 f0388631f..09d57a4f9 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.21"}, + {vsn, "5.0.22"}, {modules, []}, {registered, [emqx_rule_engine_sup, emqx_rule_engine]}, {applications, [kernel, stdlib, rulesql, getopt, emqx_ctl, uuid]},