emqx/apps/emqx_rule_engine/src/emqx_rule_engine.erl

275 lines
8.9 KiB
Erlang

%%--------------------------------------------------------------------
%% Copyright (c) 2020-2021 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_engine).
-behaviour(gen_server).
-behaviour(emqx_config_handler).
-include("rule_engine.hrl").
-include_lib("emqx/include/logger.hrl").
-include_lib("stdlib/include/qlc.hrl").
-export([start_link/0]).
-export([ post_config_update/5
, config_key_path/0
]).
%% Rule Management
-export([ load_rules/0
]).
-export([ create_rule/1
, insert_rule/1
, update_rule/1
, delete_rule/1
, get_rule/1
]).
-export([ get_rules/0
, get_rules_for_topic/1
, get_rules_with_same_event/1
, get_rules_ordered_by_ts/0
]).
%% exported for cluster_call
-export([ do_delete_rule/1
, do_insert_rule/1
]).
-export([ load_hooks_for_rule/1
, unload_hooks_for_rule/1
, add_metrics_for_rule/1
, clear_metrics_for_rule/1
]).
%% gen_server Callbacks
-export([ init/1
, handle_call/3
, handle_cast/2
, handle_info/2
, terminate/2
, code_change/3
]).
-define(RULE_ENGINE, ?MODULE).
-define(T_CALL, infinity).
config_key_path() ->
[rule_engine, rules].
-spec(start_link() -> {ok, pid()} | ignore | {error, Reason :: term()}).
start_link() ->
gen_server:start_link({local, ?RULE_ENGINE}, ?MODULE, [], []).
%%------------------------------------------------------------------------------
%% The config handler for emqx_rule_engine
%%------------------------------------------------------------------------------
post_config_update(_, _Req, NewRules, OldRules, _AppEnvs) ->
#{added := Added, removed := Removed, changed := Updated}
= emqx_map_lib:diff_maps(NewRules, OldRules),
maps_foreach(fun({Id, {_Old, New}}) ->
{ok, _} = update_rule(New#{id => bin(Id)})
end, Updated),
maps_foreach(fun({Id, _Rule}) ->
ok = delete_rule(bin(Id))
end, Removed),
maps_foreach(fun({Id, Rule}) ->
{ok, _} = create_rule(Rule#{id => bin(Id)})
end, Added),
{ok, get_rules()}.
%%------------------------------------------------------------------------------
%% APIs for rules
%%------------------------------------------------------------------------------
-spec load_rules() -> ok.
load_rules() ->
maps_foreach(fun({Id, Rule}) ->
{ok, _} = create_rule(Rule#{id => bin(Id)})
end, emqx_conf:get([rule_engine, rules], #{})).
-spec create_rule(map()) -> {ok, rule()} | {error, term()}.
create_rule(Params = #{id := RuleId}) when is_binary(RuleId) ->
case get_rule(RuleId) of
not_found -> do_create_rule(Params);
{ok, _} -> {error, {already_exists, RuleId}}
end.
-spec update_rule(map()) -> {ok, rule()} | {error, term()}.
update_rule(Params = #{id := RuleId}) when is_binary(RuleId) ->
ok = delete_rule(RuleId),
do_create_rule(Params).
-spec(delete_rule(RuleId :: rule_id()) -> ok).
delete_rule(RuleId) when is_binary(RuleId) ->
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).
%%------------------------------------------------------------------------------
%% Rule Management
%%------------------------------------------------------------------------------
-spec(get_rules() -> [rule()]).
get_rules() ->
get_all_records(?RULE_TAB).
get_rules_ordered_by_ts() ->
lists:sort(fun(#{created_at := CreatedA}, #{created_at := CreatedB}) ->
CreatedA =< CreatedB
end, get_rules()).
-spec(get_rules_for_topic(Topic :: binary()) -> [rule()]).
get_rules_for_topic(Topic) ->
[Rule || Rule = #{from := From} <- get_rules(),
emqx_plugin_libs_rule:can_topic_match_oneof(Topic, From)].
-spec(get_rules_with_same_event(Topic :: binary()) -> [rule()]).
get_rules_with_same_event(Topic) ->
EventName = emqx_rule_events:event_name(Topic),
[Rule || Rule = #{from := From} <- get_rules(),
lists:any(fun(T) -> is_of_event_name(EventName, T) end, From)].
is_of_event_name(EventName, Topic) ->
EventName =:= emqx_rule_events:event_name(Topic).
-spec(get_rule(Id :: rule_id()) -> {ok, rule()} | not_found).
get_rule(Id) ->
case ets:lookup(?RULE_TAB, Id) of
[{Id, Rule}] -> {ok, Rule#{id => Id}};
[] -> not_found
end.
load_hooks_for_rule(#{from := Topics}) ->
lists:foreach(fun emqx_rule_events:load/1, Topics).
add_metrics_for_rule(#{id := Id}) ->
ok = emqx_plugin_libs_metrics:create_metrics(rule_metrics, Id).
clear_metrics_for_rule(#{id := Id}) ->
ok = emqx_plugin_libs_metrics:clear_metrics(rule_metrics, Id).
unload_hooks_for_rule(#{id := Id, from := Topics}) ->
lists:foreach(fun(Topic) ->
case get_rules_with_same_event(Topic) of
[#{id := Id0}] when Id0 == Id -> %% we are now deleting the last rule
emqx_rule_events:unload(Topic);
_ -> ok
end
end, Topics).
%%------------------------------------------------------------------------------
%% gen_server callbacks
%%------------------------------------------------------------------------------
init([]) ->
_TableId = ets:new(?KV_TAB, [named_table, set, public, {write_concurrency, true},
{read_concurrency, true}]),
{ok, #{}}.
handle_call({insert_rule, Rule}, _From, State) ->
_ = emqx_plugin_libs_rule:cluster_call(?MODULE, do_insert_rule, [Rule]),
{reply, ok, State};
handle_call({delete_rule, Rule}, _From, State) ->
_ = emqx_plugin_libs_rule:cluster_call(?MODULE, do_delete_rule, [Rule]),
{reply, ok, State};
handle_call(Req, _From, State) ->
?SLOG(error, #{msg => "unexpected_call", request => Req}),
{reply, ignored, State}.
handle_cast(Msg, State) ->
?SLOG(error, #{msg => "unexpected_cast", request => Msg}),
{noreply, State}.
handle_info(Info, State) ->
?SLOG(error, #{msg => "unexpected_info", request => Info}),
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%------------------------------------------------------------------------------
%% Internal Functions
%%------------------------------------------------------------------------------
do_create_rule(Params = #{id := RuleId, sql := Sql, outputs := Outputs}) ->
case emqx_rule_sqlparser:parse(Sql) of
{ok, Select} ->
Rule = #{
id => RuleId,
created_at => erlang:system_time(millisecond),
enabled => maps:get(enabled, Params, true),
sql => Sql,
outputs => parse_outputs(Outputs),
description => maps:get(description, Params, ""),
%% -- calculated fields:
from => emqx_rule_sqlparser:select_from(Select),
is_foreach => emqx_rule_sqlparser:select_is_foreach(Select),
fields => emqx_rule_sqlparser:select_fields(Select),
doeach => emqx_rule_sqlparser:select_doeach(Select),
incase => emqx_rule_sqlparser:select_incase(Select),
conditions => emqx_rule_sqlparser:select_where(Select)
%% -- calculated fields end
},
ok = insert_rule(Rule),
{ok, Rule};
{error, Reason} -> {error, Reason}
end.
do_insert_rule(#{id := Id} = Rule) ->
ok = load_hooks_for_rule(Rule),
ok = add_metrics_for_rule(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(Rule),
true = ets:delete(?RULE_TAB, RuleId),
ok;
not_found -> ok
end.
parse_outputs(Outputs) ->
[do_parse_output(Out) || Out <- Outputs].
do_parse_output(Output) when is_map(Output) ->
emqx_rule_outputs:parse_output(Output);
do_parse_output(BridgeChannelId) when is_binary(BridgeChannelId) ->
BridgeChannelId.
get_all_records(Tab) ->
[Rule#{id => Id} || {Id, Rule} <- ets:tab2list(Tab)].
maps_foreach(Fun, Map) ->
lists:foreach(Fun, maps:to_list(Map)).
bin(A) when is_atom(A) -> atom_to_binary(A, utf8);
bin(B) when is_binary(B) -> B.