fix(bridge): cannot start mqtt bridge from config

This commit is contained in:
Shawn 2021-10-26 10:43:05 +08:00
parent 63f942a1b8
commit f01f9632c1
6 changed files with 73 additions and 171 deletions

View File

@ -6,7 +6,6 @@
bridges.mqtt.my_mqtt_bridge_to_aws { bridges.mqtt.my_mqtt_bridge_to_aws {
server = "127.0.0.1:1883" server = "127.0.0.1:1883"
proto_ver = "v4" proto_ver = "v4"
clientid = "my_mqtt_bridge_to_aws"
username = "username1" username = "username1"
password = "" password = ""
clean_start = true clean_start = true
@ -19,7 +18,6 @@ bridges.mqtt.my_mqtt_bridge_to_aws {
dir = "{{ platform_data_dir }}/replayq/bridge_mqtt/" dir = "{{ platform_data_dir }}/replayq/bridge_mqtt/"
seg_bytes = "100MB" seg_bytes = "100MB"
offload = false offload = false
max_total_bytes = "1GB"
} }
ssl { ssl {
enable = false enable = false

View File

@ -221,15 +221,19 @@ get_matched_bridges(Topic) ->
Bridges = emqx:get_config([bridges], #{}), Bridges = emqx:get_config([bridges], #{}),
maps:fold(fun (BType, Conf, Acc0) -> maps:fold(fun (BType, Conf, Acc0) ->
maps:fold(fun maps:fold(fun
(BName, #{from_local_topic := Filter}, Acc1) -> (BName, #{egress := Egress}, Acc1) ->
case emqx_topic:match(Topic, Filter) of get_matched_bridge_id(Egress, Topic, BType, BName, Acc1);
true -> [bridge_id(BType, BName) | Acc1]; (BName, BridgeConf, Acc1) ->
false -> Acc1 get_matched_bridge_id(BridgeConf, Topic, BType, BName, Acc1)
end;
(_Name, _BridgeConf, Acc1) -> Acc1
end, Acc0, Conf) end, Acc0, Conf)
end, [], Bridges). end, [], Bridges).
get_matched_bridge_id(#{from_local_topic := Filter}, Topic, BType, BName, Acc) ->
case emqx_topic:match(Topic, Filter) of
true -> [bridge_id(BType, BName) | Acc];
false -> Acc
end.
bin(Bin) when is_binary(Bin) -> Bin; bin(Bin) when is_binary(Bin) -> Bin;
bin(Str) when is_list(Str) -> list_to_binary(Str); bin(Str) when is_list(Str) -> list_to_binary(Str);
bin(Atom) when is_atom(Atom) -> atom_to_binary(Atom, utf8). bin(Atom) when is_atom(Atom) -> atom_to_binary(Atom, utf8).

View File

@ -74,8 +74,9 @@ load_bridges(Configs) ->
%% TODO: move this monitor into emqx_resource %% TODO: move this monitor into emqx_resource
%% emqx_resource:check_and_create_local(ResourceId, ResourceType, Config, #{keep_retry => true}). %% emqx_resource:check_and_create_local(ResourceId, ResourceType, Config, #{keep_retry => true}).
load_bridge(<<"http">>, Name, Config) -> load_bridge(<<"http">>, Name, Config) ->
Config1 = parse_http_confs(Config), do_load_bridge(<<"http">>, Name, parse_http_confs(Config));
do_load_bridge(<<"http">>, Name, Config1). load_bridge(Type, Name, Config) ->
do_load_bridge(Type, Name, Config).
do_load_bridge(Type, Name, Config) -> do_load_bridge(Type, Name, Config) ->
case emqx_resource:check_and_create_local( case emqx_resource:check_and_create_local(

View File

@ -46,7 +46,7 @@
%%===================================================================== %%=====================================================================
%% Hocon schema %% Hocon schema
roots() -> roots() ->
[{config, #{type => hoconsc:ref(?MODULE, "config")}}]. fields("config").
fields("config") -> fields("config") ->
emqx_connector_mqtt_schema:fields("config"). emqx_connector_mqtt_schema:fields("config").
@ -89,109 +89,62 @@ drop_bridge(Name) ->
%% =================================================================== %% ===================================================================
%% When use this bridge as a data source, ?MODULE:on_message_received/2 will be called %% When use this bridge as a data source, ?MODULE:on_message_received/2 will be called
%% if the bridge received msgs from the remote broker. %% if the bridge received msgs from the remote broker.
on_message_received(Msg, ChannId) -> on_message_received(Msg, BridgeId) ->
Name = atom_to_binary(ChannId, utf8), Name = atom_to_binary(BridgeId, utf8),
emqx:run_hook(<<"$bridges/", Name/binary>>, [Msg]). emqx:run_hook(<<"$bridges/", Name/binary>>, [Msg]).
%% =================================================================== %% ===================================================================
on_start(InstId, Conf) -> on_start(InstId, Conf) ->
?SLOG(info, #{msg => "starting mqtt connector",
connector => InstId, config => Conf}),
"bridge:" ++ NamePrefix = binary_to_list(InstId), "bridge:" ++ NamePrefix = binary_to_list(InstId),
BridgeId = list_to_atom(NamePrefix),
?SLOG(info, #{msg => "starting mqtt connector",
connector => BridgeId, config => Conf}),
BasicConf = basic_config(Conf), BasicConf = basic_config(Conf),
InitRes = {ok, #{name_prefix => NamePrefix, baisc_conf => BasicConf, channels => []}}, SubRemoteConf = maps:get(ingress, Conf, #{}),
InOutConfigs = taged_map_list(ingress, maps:get(ingress, Conf, #{})) FrowardConf = maps:get(egress, Conf, #{}),
++ taged_map_list(egress, maps:get(egress, Conf, #{})), BridgeConf = BasicConf#{
lists:foldl(fun name => BridgeId,
(_InOutConf, {error, Reason}) -> clientid => clientid(BridgeId),
{error, Reason}; subscriptions => SubRemoteConf#{
(InOutConf, {ok, #{channels := SubBridges} = Res}) -> to_local_topic => maps:get(to_local_topic, SubRemoteConf, undefined),
case create_channel(InOutConf, NamePrefix, BasicConf) of on_message_received => {fun ?MODULE:on_message_received/2, [BridgeId]}
{error, Reason} -> {error, Reason};
{ok, Name} -> {ok, Res#{channels => [Name | SubBridges]}}
end
end, InitRes, InOutConfigs).
on_stop(InstId, #{channels := NameList}) ->
?SLOG(info, #{msg => "stopping mqtt connector",
connector => InstId}),
lists:foreach(fun(Name) ->
remove_channel(Name)
end, NameList).
%% TODO: let the emqx_resource trigger on_query/4 automatically according to the
%% `ingress` and `egress` config
on_query(_InstId, {create_channel, Conf}, _AfterQuery, #{name_prefix := Prefix,
baisc_conf := BasicConf}) ->
create_channel(Conf, Prefix, BasicConf);
on_query(_InstId, {send_message, ChannelId, Msg}, _AfterQuery, _State) ->
?SLOG(debug, #{msg => "send msg to remote node", message => Msg,
channel_id => ChannelId}),
emqx_connector_mqtt_worker:send_to_remote(ChannelId, Msg).
on_health_check(_InstId, #{channels := NameList} = State) ->
Results = [{Name, emqx_connector_mqtt_worker:ping(Name)} || Name <- NameList],
case lists:all(fun({_, pong}) -> true; ({_, _}) -> false end, Results) of
true -> {ok, State};
false -> {error, {some_channel_down, Results}, State}
end.
create_channel({{ingress, Id}, #{from_remote_topic := RemoteT} = Conf},
NamePrefix, BasicConf) ->
LocalT = maps:get(to_local_topic, Conf, undefined),
ChannId = ingress_channel_id(NamePrefix, Id),
?SLOG(info, #{msg => "creating ingress channel",
to_remote_topic => RemoteT,
to_local_topic => LocalT,
channel_id => ChannId}),
do_create_channel(BasicConf#{
name => ChannId,
clientid => clientid(ChannId),
subscriptions => Conf#{
to_local_topic => LocalT,
on_message_received => {fun ?MODULE:on_message_received/2, [ChannId]}
}, },
forwards => undefined}); forwards => FrowardConf#{
from_local_topic => maps:get(from_local_topic, FrowardConf, undefined)
create_channel({{egress, Id}, #{to_remote_topic := RemoteT} = Conf}, }
NamePrefix, BasicConf) -> },
LocalT = maps:get(from_local_topic, Conf, undefined), case ?MODULE:create_bridge(BridgeConf) of
ChannId = egress_channel_id(NamePrefix, Id),
?SLOG(info, #{msg => "creating egress channel",
to_remote_topic => RemoteT,
to_local_topic => LocalT,
channel_id => ChannId}),
do_create_channel(BasicConf#{
name => ChannId,
clientid => clientid(ChannId),
subscriptions => undefined,
forwards => Conf#{from_local_topic => LocalT}}).
remove_channel(ChannId) ->
?SLOG(info, #{msg => "removing channel",
channel_id => ChannId}),
case ?MODULE:drop_bridge(ChannId) of
ok -> ok;
{error, not_found} -> ok;
{error, Reason} ->
?SLOG(error, #{msg => "stop channel failed",
channel_id => ChannId, reason => Reason})
end.
do_create_channel(#{name := Name} = Conf) ->
case ?MODULE:create_bridge(Conf) of
{ok, _Pid} -> {ok, _Pid} ->
start_channel(Name); case emqx_connector_mqtt_worker:ensure_started(BridgeId) of
ok -> {ok, #{name => BridgeId}};
{error, Reason} -> {error, Reason}
end;
{error, {already_started, _Pid}} -> {error, {already_started, _Pid}} ->
{ok, Name}; {ok, #{name => BridgeId}};
{error, Reason} -> {error, Reason} ->
{error, Reason} {error, Reason}
end. end.
start_channel(Name) -> on_stop(_InstId, #{name := BridgeId}) ->
case emqx_connector_mqtt_worker:ensure_started(Name) of ?SLOG(info, #{msg => "stopping mqtt connector",
ok -> {ok, Name}; connector => BridgeId}),
{error, Reason} -> {error, Reason} case ?MODULE:drop_bridge(BridgeId) of
ok -> ok;
{error, not_found} -> ok;
{error, Reason} ->
?SLOG(error, #{msg => "stop mqtt connector",
connector => BridgeId, reason => Reason})
end.
on_query(_InstId, {send_message, BridgeId, Msg}, _AfterQuery, _State) ->
?SLOG(debug, #{msg => "send msg to remote node", message => Msg,
connector => BridgeId}),
emqx_connector_mqtt_worker:send_to_remote(BridgeId, Msg).
on_health_check(_InstId, #{name := BridgeId} = State) ->
case emqx_connector_mqtt_worker:ping(BridgeId) of
pong -> {ok, State};
_ -> {error, {connector_down, BridgeId}, State}
end. end.
basic_config(#{ basic_config(#{
@ -225,19 +178,8 @@ basic_config(#{
if_record_metrics => true if_record_metrics => true
}. }.
taged_map_list(Tag, Map) ->
[{{Tag, K}, V} || {K, V} <- maps:to_list(Map)].
ingress_channel_id(Prefix, Id) ->
channel_name("ingress", Prefix, Id).
egress_channel_id(Prefix, Id) ->
channel_name("egress", Prefix, Id).
channel_name(Type, Prefix, Id) ->
list_to_atom(str(Prefix) ++ ":" ++ Type ++ ":" ++ str(Id)).
clientid(Id) -> clientid(Id) ->
list_to_binary(str(Id) ++ ":" ++ emqx_misc:gen_id(8)). list_to_binary(lists:concat([str(Id), ":", node()])).
str(A) when is_atom(A) -> str(A) when is_atom(A) ->
atom_to_list(A); atom_to_list(A);

View File

@ -92,7 +92,7 @@ the rule.
""" """
})} })}
, {egress, , {egress,
sc(hoconsc:map(id, ref("egress")), sc(ref("egress"),
#{ default => #{} #{ default => #{}
, desc => """ , desc => """
The egress config defines how this bridge forwards messages from the local broker to the remote The egress config defines how this bridge forwards messages from the local broker to the remote

View File

@ -101,14 +101,12 @@
-export([msg_marshaller/1]). -export([msg_marshaller/1]).
-export_type([ config/0 -export_type([ config/0
, batch/0
, ack_ref/0 , ack_ref/0
]). ]).
-type id() :: atom() | string() | pid(). -type id() :: atom() | string() | pid().
-type qos() :: emqx_types:qos(). -type qos() :: emqx_types:qos().
-type config() :: map(). -type config() :: map().
-type batch() :: [emqx_connector_mqtt_msg:exp_msg()].
-type ack_ref() :: term(). -type ack_ref() :: term().
-type topic() :: emqx_types:topic(). -type topic() :: emqx_types:topic().
@ -117,7 +115,7 @@
%% same as default in-flight limit for emqtt %% same as default in-flight limit for emqtt
-define(DEFAULT_BATCH_SIZE, 32). -define(DEFAULT_INFLIGHT_SIZE, 32).
-define(DEFAULT_RECONNECT_DELAY_MS, timer:seconds(5)). -define(DEFAULT_RECONNECT_DELAY_MS, timer:seconds(5)).
-define(DEFAULT_SEG_BYTES, (1 bsl 20)). -define(DEFAULT_SEG_BYTES, (1 bsl 20)).
-define(DEFAULT_MAX_TOTAL_SIZE, (1 bsl 31)). -define(DEFAULT_MAX_TOTAL_SIZE, (1 bsl 31)).
@ -205,12 +203,10 @@ init_state(Opts) ->
ReconnDelayMs = maps:get(reconnect_interval, Opts, ?DEFAULT_RECONNECT_DELAY_MS), ReconnDelayMs = maps:get(reconnect_interval, Opts, ?DEFAULT_RECONNECT_DELAY_MS),
StartType = maps:get(start_type, Opts, manual), StartType = maps:get(start_type, Opts, manual),
Mountpoint = maps:get(forward_mountpoint, Opts, undefined), Mountpoint = maps:get(forward_mountpoint, Opts, undefined),
MaxInflightSize = maps:get(max_inflight, Opts, ?DEFAULT_BATCH_SIZE), MaxInflightSize = maps:get(max_inflight, Opts, ?DEFAULT_INFLIGHT_SIZE),
BatchSize = maps:get(batch_size, Opts, ?DEFAULT_BATCH_SIZE),
Name = maps:get(name, Opts, undefined), Name = maps:get(name, Opts, undefined),
#{start_type => StartType, #{start_type => StartType,
reconnect_interval => ReconnDelayMs, reconnect_interval => ReconnDelayMs,
batch_size => BatchSize,
mountpoint => format_mountpoint(Mountpoint), mountpoint => format_mountpoint(Mountpoint),
inflight => [], inflight => [],
max_inflight => MaxInflightSize, max_inflight => MaxInflightSize,
@ -327,10 +323,6 @@ common(_StateName, {call, From}, get_forwards, #{connect_opts := #{forwards := F
{keep_state_and_data, [{reply, From, Forwards}]}; {keep_state_and_data, [{reply, From, Forwards}]};
common(_StateName, {call, From}, get_subscriptions, #{connection := Connection}) -> common(_StateName, {call, From}, get_subscriptions, #{connection := Connection}) ->
{keep_state_and_data, [{reply, From, maps:get(subscriptions, Connection, #{})}]}; {keep_state_and_data, [{reply, From, maps:get(subscriptions, Connection, #{})}]};
common(_StateName, info, {deliver, _, Msg}, State = #{replayq := Q}) ->
Msgs = collect([Msg]),
NewQ = replayq:append(Q, Msgs),
{keep_state, State#{replayq => NewQ}, {next_event, internal, maybe_send}};
common(_StateName, info, {'EXIT', _, _}, State) -> common(_StateName, info, {'EXIT', _, _}, State) ->
{keep_state, State}; {keep_state, State};
common(_StateName, cast, {send_to_remote, Msg}, #{replayq := Q} = State) -> common(_StateName, cast, {send_to_remote, Msg}, #{replayq := Q} = State) ->
@ -342,13 +334,9 @@ common(StateName, Type, Content, #{name := Name} = State) ->
content => Content}), content => Content}),
{keep_state, State}. {keep_state, State}.
do_connect(#{connect_opts := ConnectOpts = #{forwards := Forwards}, do_connect(#{connect_opts := ConnectOpts,
inflight := Inflight, inflight := Inflight,
name := Name} = State) -> name := Name} = State) ->
case Forwards of
undefined -> ok;
#{from_local_topic := Topic} -> from_local_topic(Topic, Name)
end,
case emqx_connector_mqtt_mod:start(ConnectOpts) of case emqx_connector_mqtt_mod:start(ConnectOpts) of
{ok, Conn} -> {ok, Conn} ->
?tp(info, connected, #{name => Name, inflight => length(Inflight)}), ?tp(info, connected, #{name => Name, inflight => length(Inflight)}),
@ -360,19 +348,10 @@ do_connect(#{connect_opts := ConnectOpts = #{forwards := Forwards},
{error, Reason, State} {error, Reason, State}
end. end.
collect(Acc) ->
receive
{deliver, _, Msg} ->
collect([Msg | Acc])
after
0 ->
lists:reverse(Acc)
end.
%% Retry all inflight (previously sent but not acked) batches. %% Retry all inflight (previously sent but not acked) batches.
retry_inflight(State, []) -> {ok, State}; retry_inflight(State, []) -> {ok, State};
retry_inflight(State, [#{q_ack_ref := QAckRef, batch := Batch} | Rest] = OldInf) -> retry_inflight(State, [#{q_ack_ref := QAckRef, msg := Msg} | Rest] = OldInf) ->
case do_send(State, QAckRef, Batch) of case do_send(State, QAckRef, Msg) of
{ok, State1} -> {ok, State1} ->
retry_inflight(State1, Rest); retry_inflight(State1, Rest);
{error, #{inflight := NewInf} = State1} -> {error, #{inflight := NewInf} = State1} ->
@ -393,34 +372,33 @@ pop_and_send_loop(#{replayq := Q} = State, N) ->
false -> false ->
BatchSize = 1, BatchSize = 1,
Opts = #{count_limit => BatchSize, bytes_limit => 999999999}, Opts = #{count_limit => BatchSize, bytes_limit => 999999999},
{Q1, QAckRef, Batch} = replayq:pop(Q, Opts), {Q1, QAckRef, [Msg]} = replayq:pop(Q, Opts),
case do_send(State#{replayq := Q1}, QAckRef, Batch) of case do_send(State#{replayq := Q1}, QAckRef, Msg) of
{ok, NewState} -> pop_and_send_loop(NewState, N - 1); {ok, NewState} -> pop_and_send_loop(NewState, N - 1);
{error, NewState} -> {error, NewState} {error, NewState} -> {error, NewState}
end end
end. end.
%% Assert non-empty batch because we have a is_empty check earlier. do_send(#{connect_opts := #{forwards := undefined}}, _QAckRef, Msg) ->
do_send(#{connect_opts := #{forwards := undefined}}, _QAckRef, Batch) ->
?SLOG(error, #{msg => "cannot forward messages to remote broker" ?SLOG(error, #{msg => "cannot forward messages to remote broker"
" as egress_channel is not configured", " as forwards is not configured",
messages => Batch}); messages => Msg});
do_send(#{inflight := Inflight, do_send(#{inflight := Inflight,
connection := Connection, connection := Connection,
mountpoint := Mountpoint, mountpoint := Mountpoint,
connect_opts := #{forwards := Forwards}} = State, QAckRef, [_ | _] = Batch) -> connect_opts := #{forwards := Forwards}} = State, QAckRef, Msg) ->
Vars = emqx_connector_mqtt_msg:make_pub_vars(Mountpoint, Forwards), Vars = emqx_connector_mqtt_msg:make_pub_vars(Mountpoint, Forwards),
ExportMsg = fun(Message) -> ExportMsg = fun(Message) ->
emqx_metrics:inc('bridge.mqtt.message_sent_to_remote'), emqx_metrics:inc('bridge.mqtt.message_sent_to_remote'),
emqx_connector_mqtt_msg:to_remote_msg(Message, Vars) emqx_connector_mqtt_msg:to_remote_msg(Message, Vars)
end, end,
?SLOG(debug, #{msg => "publish to remote broker", ?SLOG(debug, #{msg => "publish to remote broker",
message => Batch, vars => Vars}), message => Msg, vars => Vars}),
case emqx_connector_mqtt_mod:send(Connection, [ExportMsg(M) || M <- Batch]) of case emqx_connector_mqtt_mod:send(Connection, [ExportMsg(Msg)]) of
{ok, Refs} -> {ok, Refs} ->
{ok, State#{inflight := Inflight ++ [#{q_ack_ref => QAckRef, {ok, State#{inflight := Inflight ++ [#{q_ack_ref => QAckRef,
send_ack_ref => map_set(Refs), send_ack_ref => map_set(Refs),
batch => Batch}]}}; msg => Msg}]}};
{error, Reason} -> {error, Reason} ->
?SLOG(info, #{msg => "mqtt_bridge_produce_failed", ?SLOG(info, #{msg => "mqtt_bridge_produce_failed",
reason => Reason}), reason => Reason}),
@ -473,27 +451,6 @@ drop_acked_batches(Q, [#{send_ack_ref := Refs,
All All
end. end.
from_local_topic(undefined, _Name) ->
ok;
from_local_topic(Topic, Name) ->
do_subscribe(Topic, Name).
topic(T) -> iolist_to_binary(T).
validate(RawTopic) ->
Topic = topic(RawTopic),
try emqx_topic:validate(Topic) of
_Success -> Topic
catch
error:Reason ->
error({bad_topic, Topic, Reason})
end.
do_subscribe(RawTopic, Name) ->
TopicFilter = validate(RawTopic),
{Topic, SubOpts} = emqx_topic:parse(TopicFilter, #{qos => ?QOS_2}),
emqx_broker:subscribe(Topic, Name, SubOpts).
disconnect(#{connection := Conn} = State) when Conn =/= undefined -> disconnect(#{connection := Conn} = State) when Conn =/= undefined ->
emqx_connector_mqtt_mod:stop(Conn), emqx_connector_mqtt_mod:stop(Conn),
State#{connection => undefined}; State#{connection => undefined};