%%%------------------------------------------------------------------- %%% @author anlicheng %%% @copyright (C) 2025, %%% @doc %%% %%% @end %%% Created : 07. 11月 2025 16:27 %%%------------------------------------------------------------------- -module(endpoint_subscription). -author("anlicheng"). -behaviour(gen_server). %% API -export([start_link/0]). -export([subscribe/2, unsubscribe/2, publish/2, get_subscribers/0]). -export([match_components/2, is_valid_components/1, of_components/1]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -define(SERVER, ?MODULE). %% 定义订阅者 -record(subscriber, { topic :: binary(), subscriber_pid :: pid(), components = [], monitor_ref :: undefined | reference(), %% 优先级 %% 1. 完全匹配的topic优先级别最高 %% 2. 带 * 的订阅 %% 3. 带 + 的订阅 order :: integer() }). -record(state, { subscribers = [] }). %%%=================================================================== %%% API %%%=================================================================== -spec subscribe(Topic :: binary(), SubscriberPid :: pid()) -> ok | {error, Reason :: binary()}. subscribe(Topic, SubscriberPid) when is_binary(Topic), is_pid(SubscriberPid) -> gen_server:call(?SERVER, {subscribe, Topic, SubscriberPid}). -spec unsubscribe(Topic :: binary(), SubscriberPid :: pid()) -> ok. unsubscribe(Topic, SubscriberPid) when is_binary(Topic), is_pid(SubscriberPid) -> gen_server:call(?SERVER, {unsubscribe, Topic, SubscriberPid}). -spec get_subscribers() -> {ok, Subscribers :: map()}. get_subscribers() -> gen_server:call(?SERVER, get_subscribers). -spec publish(RouteKey :: binary(), Content :: binary()) -> no_return(). publish(RouteKey, Content) when is_binary(RouteKey), is_binary(Content) -> gen_server:cast(?SERVER, {publish, RouteKey, Content}). %% @doc Spawns the server and registers the local name (unique) -spec(start_link() -> {ok, Pid :: pid()} | ignore | {error, Reason :: term()}). start_link() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). %%%=================================================================== %%% gen_server callbacks %%%=================================================================== %% @private %% @doc Initializes the server -spec(init(Args :: term()) -> {ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} | {stop, Reason :: term()} | ignore). init([]) -> ok = iot_log:set_metadata(), {ok, #state{}}. %% @private %% @doc Handling call messages -spec(handle_call(Request :: term(), From :: {pid(), Tag :: term()}, State :: #state{}) -> {reply, Reply :: term(), NewState :: #state{}} | {reply, Reply :: term(), NewState :: #state{}, timeout() | hibernate} | {noreply, NewState :: #state{}} | {noreply, NewState :: #state{}, timeout() | hibernate} | {stop, Reason :: term(), Reply :: term(), NewState :: #state{}} | {stop, Reason :: term(), NewState :: #state{}}). %% 同一个SubscriberPid只能订阅同一个topic一次 handle_call(get_subscribers, _From, State = #state{subscribers = Subscribers}) -> {reply, {ok, Subscribers}, State}; handle_call({subscribe, Topic, SubscriberPid}, _From, State = #state{subscribers = Subscribers}) -> Components = of_components(Topic), case is_valid_components(Components) of true -> case has_subscription(Topic, SubscriberPid, Subscribers) of true -> {reply, ok, State}; false -> %% 建立到SubscriberPid的monitor,进程退出需要清理订阅 MonitorRef = erlang:monitor(process, SubscriberPid), Sub = #subscriber{ topic = Topic, subscriber_pid = SubscriberPid, components = Components, monitor_ref = MonitorRef, order = order_num(Components) }, {reply, ok, State#state{subscribers = [Sub | Subscribers]}} end; false -> {reply, {error, <<"invalid topic name">>}, State} end; handle_call({unsubscribe, Topic, SubscriberPid}, _From, State = #state{subscribers = Subscribers}) -> {Removed, Reserved} = lists:partition(fun(#subscriber{topic = Topic0, subscriber_pid = SubscriberPid0}) -> Topic =:= Topic0 andalso SubscriberPid =:= SubscriberPid0 end, Subscribers), lists:foreach(fun(#subscriber{monitor_ref = MonitorRef}) when is_reference(MonitorRef) -> erlang:demonitor(MonitorRef, [flush]); (_) -> ok end, Removed), {reply, ok, State#state{subscribers = Reserved}}. %% @private %% @doc Handling cast messages -spec(handle_cast(Request :: term(), State :: #state{}) -> {noreply, NewState :: #state{}} | {noreply, NewState :: #state{}, timeout() | hibernate} | {stop, Reason :: term(), NewState :: #state{}}). %% 发布消息 handle_cast({publish, RouteKey, Metric}, State = #state{subscribers = Subscribers}) -> MatchedSubscribers = match_subscribers(Subscribers, RouteKey), lists:foreach(fun(#subscriber{subscriber_pid = SubscriberPid}) -> endpoint:forward(SubscriberPid, Metric) end, MatchedSubscribers), logger:debug("[efka_subscription] route_key: ~p, metric: ~p, match subscribers: ~p", [RouteKey, Metric, MatchedSubscribers]), {noreply, State}. %% @private %% @doc Handling all non call/cast messages -spec(handle_info(Info :: timeout() | term(), State :: #state{}) -> {noreply, NewState :: #state{}} | {noreply, NewState :: #state{}, timeout() | hibernate} | {stop, Reason :: term(), NewState :: #state{}}). handle_info({'DOWN', _Ref, process, SubscriberPid, Reason}, State = #state{subscribers = Subscribers}) -> logger:debug("[efka_subscription] subscriber: ~p, down with reason: ~p", [SubscriberPid, Reason]), NSubscribers = lists:filter(fun(#subscriber{subscriber_pid = Pid0}) -> SubscriberPid /= Pid0 end, Subscribers), {noreply, State#state{subscribers = NSubscribers}}; handle_info(Info, State = #state{}) -> logger:debug("[efka_subscription] get unknown info: ~p", [Info]), {noreply, State}. %% @private %% @doc This function is called by a gen_server when it is about to %% terminate. It should be the opposite of Module:init/1 and do any %% necessary cleaning up. When it returns, the gen_server terminates %% with Reason. The return value is ignored. -spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()), State :: #state{}) -> term()). terminate(_Reason, _State = #state{}) -> ok. %% @private %% @doc Convert process state when code is changed -spec(code_change(OldVsn :: term() | {down, term()}, State :: #state{}, Extra :: term()) -> {ok, NewState :: #state{}} | {error, Reason :: term()}). code_change(_OldVsn, State = #state{}, _Extra) -> {ok, State}. %%%=================================================================== %%% Internal functions %%%=================================================================== %% 查找满足条件订阅者 -spec match_subscribers(Subscribers :: [#subscriber{}], Topic :: binary()) -> [#subscriber{}]. match_subscribers(Subscribers, Topic) when is_list(Subscribers), is_binary(Topic) -> Components = of_components(Topic), Matched = lists:filter(fun(#subscriber{components = Components0}) -> match_components(Components0, Components) end, Subscribers), Sorted = lists:sort(fun compare_subscriber/2, Matched), dedupe_subscribers(Sorted). %% 开始对比订阅的topic和发布的topic的Components信息 %% *表示单级匹配,+表示多级匹配;+只能出现一次,并且只能在末尾 -spec match_components(list(), list()) -> boolean(). match_components(A, B) when is_list(A), is_list(B) -> match_components(A, B, false). match_components([<<"+">>], [_|_], _) -> true; match_components([], [], _) -> true; match_components([<<"*">>|T0], [_|T1], _) -> match_components(T0, T1, false); match_components([C0|T0], [C0|T1], _) -> match_components(T0, T1, false); match_components(_, _, _) -> false. -spec of_components(Topic :: binary()) -> [binary()]. of_components(Topic) when is_binary(Topic) -> binary:split(Topic, <<$/>>, [global]). is_valid_components([]) -> true; is_valid_components([<<$+>>|T]) -> length(T) =:= 0; is_valid_components([<<$*>>|T]) -> is_valid_components(T); is_valid_components([_|T]) -> is_valid_components(T). -spec order_num(Components :: list()) -> integer(). order_num([]) -> 1; order_num([<<$*>>|_]) -> 2; order_num([<<$+>>|_]) -> 3; order_num([_|Tail]) -> order_num(Tail). -spec has_subscription(binary(), pid(), [#subscriber{}]) -> boolean(). has_subscription(Topic, SubscriberPid, Subscribers) -> lists:any(fun(#subscriber{topic = Topic0, subscriber_pid = SubscriberPid0}) -> Topic =:= Topic0 andalso SubscriberPid =:= SubscriberPid0 end, Subscribers). -spec compare_subscriber(#subscriber{}, #subscriber{}) -> boolean(). compare_subscriber(#subscriber{order = Order0, topic = Topic0}, #subscriber{order = Order1, topic = Topic1}) -> case Order0 =:= Order1 of true -> Topic0 =< Topic1; false -> Order0 < Order1 end. -spec dedupe_subscribers([#subscriber{}]) -> [#subscriber{}]. dedupe_subscribers(Subscribers) -> {_, Result} = lists:foldl(fun(S = #subscriber{subscriber_pid = SubscriberPid}, {Seen, Acc}) -> case sets:is_element(SubscriberPid, Seen) of true -> {Seen, Acc}; false -> {sets:add_element(SubscriberPid, Seen), [S | Acc]} end end, {sets:new(), []}, Subscribers), lists:reverse(Result).