feat(clusterlink): integrate node local routes replication and message forwarding

This commit is contained in:
Serge Tupchii 2024-05-16 20:19:55 +03:00
parent 7df91d852c
commit f036b641eb
10 changed files with 93 additions and 45 deletions

View File

@ -67,7 +67,7 @@
-record(route, { -record(route, {
topic :: binary(), topic :: binary(),
dest :: node() | {binary(), node()} | emqx_session:session_id() dest :: node() | {binary(), node()} | emqx_session:session_id() | emqx_external_broker:dest()
}). }).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------

View File

@ -256,9 +256,10 @@ do_publish_many([Msg | T]) ->
do_publish(#message{topic = Topic} = Msg) -> do_publish(#message{topic = Topic} = Msg) ->
PersistRes = persist_publish(Msg), PersistRes = persist_publish(Msg),
{Routes, ExtRoutes} = aggre(emqx_router:match_routes(Topic)), Routes = aggre(emqx_router:match_routes(Topic)),
Routes1 = maybe_add_ext_routes(ExtRoutes, Routes, Msg), Delivery = delivery(Msg),
route(Routes1, delivery(Msg), PersistRes). RouteRes = route(Routes, Delivery, PersistRes),
ext_route(ext_routes(Topic, Msg), Delivery, RouteRes).
persist_publish(Msg) -> persist_publish(Msg) ->
case emqx_persistent_message:persist(Msg) of case emqx_persistent_message:persist(Msg) of
@ -322,41 +323,44 @@ do_route({To, Node}, Delivery) when Node =:= node() ->
{Node, To, dispatch(To, Delivery)}; {Node, To, dispatch(To, Delivery)};
do_route({To, Node}, Delivery) when is_atom(Node) -> do_route({To, Node}, Delivery) when is_atom(Node) ->
{Node, To, forward(Node, To, Delivery, emqx:get_config([rpc, mode]))}; {Node, To, forward(Node, To, Delivery, emqx:get_config([rpc, mode]))};
do_route({To, {external, _} = ExtDest}, Delivery) ->
{ExtDest, To, emqx_external_broker:forward(ExtDest, Delivery)};
do_route({To, Group}, Delivery) when is_tuple(Group); is_binary(Group) -> do_route({To, Group}, Delivery) when is_tuple(Group); is_binary(Group) ->
{share, To, emqx_shared_sub:dispatch(Group, To, Delivery)}. {share, To, emqx_shared_sub:dispatch(Group, To, Delivery)}.
aggre([]) -> aggre([]) ->
{[], []}; [];
aggre([#route{topic = To, dest = Node}]) when is_atom(Node) -> aggre([#route{topic = To, dest = Node}]) when is_atom(Node) ->
{[{To, Node}], []}; [{To, Node}];
aggre([#route{topic = To, dest = {external, _} = ExtDest}]) ->
{[], [{To, ExtDest}]};
aggre([#route{topic = To, dest = {Group, _Node}}]) -> aggre([#route{topic = To, dest = {Group, _Node}}]) ->
{[{To, Group}], []}; [{To, Group}];
aggre(Routes) -> aggre(Routes) ->
aggre(Routes, false, {[], []}). aggre(Routes, false, []).
aggre([#route{topic = To, dest = Node} | Rest], Dedup, {Acc, ExtAcc}) when is_atom(Node) -> aggre([#route{topic = To, dest = Node} | Rest], Dedup, Acc) when is_atom(Node) ->
aggre(Rest, Dedup, {[{To, Node} | Acc], ExtAcc}); aggre(Rest, Dedup, [{To, Node} | Acc]);
aggre([#route{topic = To, dest = {external, _} = ExtDest} | Rest], Dedup, {Acc, ExtAcc}) -> aggre([#route{topic = To, dest = {Group, _Node}} | Rest], _Dedup, Acc) ->
aggre(Rest, Dedup, {Acc, [{To, ExtDest} | ExtAcc]}); aggre(Rest, true, [{To, Group} | Acc]);
aggre([#route{topic = To, dest = {Group, _Node}} | Rest], _Dedup, {Acc, ExtAcc}) ->
aggre(Rest, true, {[{To, Group} | Acc], ExtAcc});
aggre([], false, Acc) -> aggre([], false, Acc) ->
Acc; Acc;
aggre([], true, {Acc, ExtAcc}) -> aggre([], true, Acc) ->
{lists:usort(Acc), lists:usort(ExtAcc)}. lists:usort(Acc).
maybe_add_ext_routes([] = _ExtRoutes, Routes, _Msg) -> ext_routes(Topic, Msg) ->
Routes;
maybe_add_ext_routes(ExtRoutes, Routes, Msg) ->
case emqx_external_broker:should_route_to_external_dests(Msg) of case emqx_external_broker:should_route_to_external_dests(Msg) of
true -> Routes ++ ExtRoutes; true -> emqx_external_broker:match_routes(Topic);
false -> Routes false -> []
end. end.
ext_route([], _Delivery, RouteRes) ->
RouteRes;
ext_route(ExtRoutes, Delivery, RouteRes) ->
lists:foldl(
fun(#route{topic = To, dest = ExtDest}, Acc) ->
[{ExtDest, To, emqx_external_broker:forward(ExtDest, Delivery)} | Acc]
end,
RouteRes,
ExtRoutes
).
%% @doc Forward message to another node. %% @doc Forward message to another node.
-spec forward( -spec forward(
node(), emqx_types:topic() | emqx_types:share(), emqx_types:delivery(), RpcMode :: sync | async node(), emqx_types:topic() | emqx_types:share(), emqx_types:delivery(), RpcMode :: sync | async

View File

@ -24,6 +24,10 @@
-callback maybe_add_route(emqx_types:topic()) -> ok. -callback maybe_add_route(emqx_types:topic()) -> ok.
-callback maybe_delete_route(emqx_types:topic()) -> ok. -callback maybe_delete_route(emqx_types:topic()) -> ok.
-callback match_routes(emqx_types:topic()) -> [emqx_types:route()].
-type dest() :: term().
-export([ -export([
provider/0, provider/0,
register_provider/1, register_provider/1,
@ -31,9 +35,12 @@
forward/2, forward/2,
should_route_to_external_dests/1, should_route_to_external_dests/1,
maybe_add_route/1, maybe_add_route/1,
maybe_delete_route/1 maybe_delete_route/1,
match_routes/1
]). ]).
-export_type([dest/0]).
-include("logger.hrl"). -include("logger.hrl").
-define(PROVIDER, {?MODULE, external_broker}). -define(PROVIDER, {?MODULE, external_broker}).
@ -106,6 +113,9 @@ maybe_add_route(Topic) ->
maybe_delete_route(Topic) -> maybe_delete_route(Topic) ->
?safe_with_provider(?FUNCTION_NAME(Topic), ok). ?safe_with_provider(?FUNCTION_NAME(Topic), ok).
match_routes(Topic) ->
?safe_with_provider(?FUNCTION_NAME(Topic), ok).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Internal functions %% Internal functions
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------

View File

@ -95,8 +95,7 @@
-export_type([schemavsn/0]). -export_type([schemavsn/0]).
-type group() :: binary(). -type group() :: binary().
-type external_dest() :: {external, term()}. -type dest() :: node() | {group(), node()}.
-type dest() :: node() | {group(), node()} | external_dest().
-type schemavsn() :: v1 | v2. -type schemavsn() :: v1 | v2.
%% Operation :: {add, ...} | {delete, ...}. %% Operation :: {add, ...} | {delete, ...}.

View File

@ -267,7 +267,7 @@
[ [
{node(), topic(), deliver_result()} {node(), topic(), deliver_result()}
| {share, topic(), deliver_result()} | {share, topic(), deliver_result()}
| {emqx_router:external_dest(), topic(), deliver_result()} | {emqx_external_broker:dest(), topic(), deliver_result()}
| persisted | persisted
] ]
| disconnect. | disconnect.

View File

@ -11,6 +11,7 @@
unregister_external_broker/0, unregister_external_broker/0,
maybe_add_route/1, maybe_add_route/1,
maybe_delete_route/1, maybe_delete_route/1,
match_routes/1,
forward/2, forward/2,
should_route_to_external_dests/1 should_route_to_external_dests/1
]). ]).
@ -38,15 +39,16 @@ unregister_external_broker() ->
emqx_external_broker:unregister_provider(?MODULE). emqx_external_broker:unregister_provider(?MODULE).
maybe_add_route(Topic) -> maybe_add_route(Topic) ->
emqx_cluster_link_coordinator:route_op(<<"add">>, Topic). maybe_push_route_op(add, Topic).
maybe_delete_route(_Topic) -> maybe_delete_route(Topic) ->
%% Not implemented yet maybe_push_route_op(delete, Topic).
%% emqx_cluster_link_coordinator:route_op(<<"delete">>, Topic).
ok.
forward(ExternalDest, Delivery) -> forward(DestCluster, Delivery) ->
emqx_cluster_link_mqtt:forward(ExternalDest, Delivery). emqx_cluster_link_mqtt:forward(DestCluster, Delivery).
match_routes(Topic) ->
emqx_cluster_link_extrouter:match_routes(Topic).
%% Do not forward any external messages to other links. %% Do not forward any external messages to other links.
%% Only forward locally originated messages to all the relevant links, i.e. no gossip message forwarding. %% Only forward locally originated messages to all the relevant links, i.e. no gossip message forwarding.
@ -105,6 +107,28 @@ delete_hook() ->
%% Internal functions %% Internal functions
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
maybe_push_route_op(Op, Topic) ->
lists:foreach(
fun(#{upstream := Cluster, topics := LinkFilters}) ->
case topic_intersect_any(Topic, LinkFilters) of
false ->
ok;
TopicIntersection ->
ID = Topic,
emqx_cluster_link_router_syncer:push(Cluster, Op, TopicIntersection, ID)
end
end,
emqx_cluster_link_config:enabled_links()
).
topic_intersect_any(Topic, [LinkFilter | T]) ->
case emqx_topic:intersection(Topic, LinkFilter) of
false -> topic_intersect_any(Topic, T);
TopicOrFilter -> TopicOrFilter
end;
topic_intersect_any(_Topic, []) ->
false.
actor_init(ClusterName, Actor, Incarnation) -> actor_init(ClusterName, Actor, Incarnation) ->
Env = #{timestamp => erlang:system_time(millisecond)}, Env = #{timestamp => erlang:system_time(millisecond)},
{ok, _} = emqx_cluster_link_extrouter:actor_init(ClusterName, Actor, Incarnation, Env). {ok, _} = emqx_cluster_link_extrouter:actor_init(ClusterName, Actor, Incarnation, Env).

View File

@ -16,6 +16,7 @@
-export([ -export([
%% General %% General
cluster/0, cluster/0,
enabled_links/0,
links/0, links/0,
link/1, link/1,
topic_filters/1, topic_filters/1,
@ -41,6 +42,9 @@ cluster() ->
links() -> links() ->
emqx:get_config(?LINKS_PATH, []). emqx:get_config(?LINKS_PATH, []).
enabled_links() ->
[L || L = #{enable := true} <- links()].
link(Name) -> link(Name) ->
case lists:dropwhile(fun(L) -> Name =/= upstream_name(L) end, links()) of case lists:dropwhile(fun(L) -> Name =/= upstream_name(L) end, links()) of
[LinkConf | _] -> LinkConf; [LinkConf | _] -> LinkConf;

View File

@ -64,6 +64,8 @@
%% Op4 | n2@ds delete client/42/# MCounter -= 1 bsl 1 = 0 route deleted %% Op4 | n2@ds delete client/42/# MCounter -= 1 bsl 1 = 0 route deleted
-type lane() :: non_neg_integer(). -type lane() :: non_neg_integer().
-include_lib("emqx/include/emqx.hrl").
-define(DEFAULT_ACTOR_TTL_MS, 30_000). -define(DEFAULT_ACTOR_TTL_MS, 30_000).
-define(EXTROUTE_SHARD, ?MODULE). -define(EXTROUTE_SHARD, ?MODULE).
@ -117,7 +119,8 @@ create_tables() ->
match_routes(Topic) -> match_routes(Topic) ->
Matches = emqx_topic_index:matches(Topic, ?EXTROUTE_TAB, [unique]), Matches = emqx_topic_index:matches(Topic, ?EXTROUTE_TAB, [unique]),
[match_to_route(M) || M <- Matches]. %% `unique` opt is not enough, since we keep the original Topic as a part of RouteID
lists:usort([match_to_route(M) || M <- Matches]).
lookup_routes(Topic) -> lookup_routes(Topic) ->
Pat = #extroute{entry = emqx_topic_index:make_key(Topic, '$1'), _ = '_'}, Pat = #extroute{entry = emqx_topic_index:make_key(Topic, '$1'), _ = '_'},
@ -128,7 +131,8 @@ topics() ->
[emqx_topic_index:get_topic(K) || [K] <- ets:match(?EXTROUTE_TAB, Pat)]. [emqx_topic_index:get_topic(K) || [K] <- ets:match(?EXTROUTE_TAB, Pat)].
match_to_route(M) -> match_to_route(M) ->
emqx_topic_index:get_topic(M). ?ROUTE_ID(Cluster, _) = emqx_topic_index:get_id(M),
#route{topic = emqx_topic_index:get_topic(M), dest = Cluster}.
%% %%

View File

@ -556,8 +556,8 @@ encode_field(route, {add, Route = {_Topic, _ID}}) ->
encode_field(route, {delete, {Topic, ID}}) -> encode_field(route, {delete, {Topic, ID}}) ->
{?ROUTE_DELETE, Topic, ID}. {?ROUTE_DELETE, Topic, ID}.
decode_field(route, {?ROUTE_DELETE, Route = {_Topic, _ID}}) -> decode_field(route, {?ROUTE_DELETE, Topic, ID}) ->
{delete, Route}; {delete, {Topic, ID}};
decode_field(route, Route = {_Topic, _ID}) -> decode_field(route, Route = {_Topic, _ID}) ->
{add, Route}. {add, Route}.
@ -565,7 +565,7 @@ decode_field(route, Route = {_Topic, _ID}) ->
%% emqx_external_broker %% emqx_external_broker
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
forward({external, {link, ClusterName}}, #delivery{message = #message{topic = Topic} = Msg}) -> forward(ClusterName, #delivery{message = #message{topic = Topic} = Msg}) ->
QueryOpts = #{pick_key => Topic}, QueryOpts = #{pick_key => Topic},
emqx_resource:query(?MSG_RES_ID(ClusterName), Msg, QueryOpts). emqx_resource:query(?MSG_RES_ID(ClusterName), Msg, QueryOpts).

View File

@ -123,7 +123,11 @@ refine_client_options(Options = #{clientid := ClientID}) ->
client_session_present(ClientPid) -> client_session_present(ClientPid) ->
Info = emqtt:info(ClientPid), Info = emqtt:info(ClientPid),
proplists:get_value(session_present, Info, false). %% FIXME: waitnig for emqtt release that fixes session_present type (must be a boolean)
case proplists:get_value(session_present, Info, 0) of
0 -> false;
1 -> true
end.
announce_client(TargetCluster, Pid) -> announce_client(TargetCluster, Pid) ->
true = gproc:reg_other(?CLIENT_NAME(TargetCluster), Pid), true = gproc:reg_other(?CLIENT_NAME(TargetCluster), Pid),
@ -272,11 +276,10 @@ terminate(_Reason, _State) ->
process_connect(St = #st{target = TargetCluster, actor = Actor, incarnation = Incr}) -> process_connect(St = #st{target = TargetCluster, actor = Actor, incarnation = Incr}) ->
case start_link_client(TargetCluster) of case start_link_client(TargetCluster) of
{ok, ClientPid} -> {ok, ClientPid} ->
%% TODO: error handling, handshake
{ok, _} = emqx_cluster_link_mqtt:publish_actor_init_sync(ClientPid, Actor, Incr),
ok = start_syncer(TargetCluster), ok = start_syncer(TargetCluster),
ok = announce_client(TargetCluster, ClientPid), ok = announce_client(TargetCluster, ClientPid),
%% TODO: error handling, handshake
{ok, _} = emqx_cluster_link_mqtt:publish_actor_init_sync(ClientPid, Actor, Incr),
process_bootstrap(St#st{client = ClientPid}); process_bootstrap(St#st{client = ClientPid});
{error, Reason} -> {error, Reason} ->
handle_connect_error(Reason, St) handle_connect_error(Reason, St)