%%%------------------------------------------------------------------- %%% @author anlicheng %%% @copyright (C) 2025, %%% @doc %%% %%% @end %%% Created : 28. 4月 2025 23:50 %%%------------------------------------------------------------------- -module(efka_subscription). -author("anlicheng"). -behaviour(gen_server). %% API -export([start_link/0]). -export([subscribe/2, unsubscribe_all/1, publish/3, debug_info/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 = [], %% 优先级 %% 1. 完全匹配的topic优先级别最高 %% 2. 带 * 的订阅 %% 3. 带 + 的订阅 order :: integer() }). -record(state, { subscribers = [], %% qos未1,并且未被消费的消息 remaining_messages = [] }). %%%=================================================================== %%% 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 publish(Topic :: binary(), Qos :: integer(), Content :: binary()) -> ok. publish(Topic, Qos, Content) when is_binary(Topic), is_integer(Qos), is_binary(Content) -> gen_server:cast(?SERVER, {publish, Topic, Qos, Content}). -spec unsubscribe_all(pid()) -> ok. unsubscribe_all(SubscriberPid) when is_pid(SubscriberPid) -> gen_server:call(?SERVER, {unsubscribe_all, SubscriberPid}). -spec debug_info() -> {ok, Info :: map()}. debug_info() -> gen_server:call(?SERVER, debug_info). %% @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, #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({subscribe, Topic, SubscriberPid}, _From, State = #state{subscribers = Subscribers, remaining_messages = RemainingMessages}) -> Components = of_components(Topic), case is_valid_components(Components) of true -> case has_subscription(Topic, SubscriberPid, Subscribers) of true -> {reply, ok, State}; false -> Sub = #subscriber{topic = Topic, subscriber_pid = SubscriberPid, components = Components, order = order_num(Components)}, %% 只有首次看到该pid时才建立monitor,避免重复monitor case has_subscriber_pid(SubscriberPid, Subscribers) of true -> ok; false -> erlang:monitor(process, SubscriberPid) end, %% 处理遗留的消息 RestRemainingMessages = dispatch_remaining_messages(Sub, RemainingMessages), {reply, ok, State#state{subscribers = Subscribers ++ [Sub], remaining_messages = RestRemainingMessages}} end; false -> {reply, {error, <<"invalid topic name">>}, State} end; handle_call({unsubscribe_all, SubscriberPid}, _From, State = #state{subscribers = Subscribers}) -> {reply, ok, State#state{subscribers = remove_subscriber_pid(SubscriberPid, Subscribers)}}; handle_call(debug_info, _From, State = #state{subscribers = Subscribers, remaining_messages = RemainingMessages}) -> Info = #{ subscribes => Subscribers, remaining_messages => RemainingMessages }, {reply, {ok, Info}, State}. %% @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, Topic, Qos, Content}, State = #state{subscribers = Subscribers, remaining_messages = RemainingMessages}) -> MatchedSubscribers = match_subscribers(Subscribers, Topic), logger:debug("[efka_subscription] topic: ~p, content: ~p, match subscribers: ~p", [Topic, Content, MatchedSubscribers]), case MatchedSubscribers of [_|_] -> broadcast(Topic, Content, MatchedSubscribers), {noreply, State}; [] when Qos =:= 0 -> {noreply, State}; [] -> {noreply, State#state{remaining_messages = [{Topic, Content}|RemainingMessages]}} end. %% @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 = remove_subscriber_pid(SubscriberPid, 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), lists:foldl(fun(S = #subscriber{components = Components0, subscriber_pid = Pid0}, Acc) -> case match_components(Components0, Components) andalso not contain_channel(Pid0, Acc) of true -> [S|Acc]; false -> Acc end end, [], Subscribers). -spec contain_channel(Pid :: pid(), Subscribers :: list()) -> boolean(). contain_channel(Pid, Subscribers) when is_pid(Pid), is_list(Subscribers) -> lists:search(fun(#subscriber{subscriber_pid = Pid0}) -> Pid == Pid0 end, Subscribers) /= false. -spec has_subscriber_pid(pid(), [#subscriber{}]) -> boolean(). has_subscriber_pid(SubscriberPid, Subscribers) when is_pid(SubscriberPid), is_list(Subscribers) -> lists:any(fun(#subscriber{subscriber_pid = SubscriberPid0}) -> SubscriberPid =:= SubscriberPid0 end, Subscribers). -spec has_subscription(binary(), pid(), [#subscriber{}]) -> boolean(). has_subscription(Topic, SubscriberPid, Subscribers) when is_binary(Topic), is_pid(SubscriberPid), is_list(Subscribers) -> lists:any(fun(#subscriber{topic = Topic0, subscriber_pid = SubscriberPid0}) -> Topic =:= Topic0 andalso SubscriberPid =:= SubscriberPid0 end, Subscribers). %% 开始对比订阅的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]). -spec is_valid_components([binary()]) -> boolean(). 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 broadcast(binary(), binary(), [#subscriber{}]) -> ok. broadcast(Topic, Content, MatchedSubscribers) -> lists:foreach(fun(#subscriber{subscriber_pid = SubscriberPid}) -> SubscriberPid ! {topic_broadcast, Topic, Content} end, MatchedSubscribers). -spec remove_subscriber_pid(pid(), [#subscriber{}]) -> [#subscriber{}]. remove_subscriber_pid(SubscriberPid, Subscribers) when is_pid(SubscriberPid), is_list(Subscribers) -> lists:filter(fun(#subscriber{subscriber_pid = SubscriberPid0}) -> SubscriberPid =/= SubscriberPid0 end, Subscribers). -spec dispatch_remaining_messages(Subscriber :: #subscriber{}, RemainingMessages :: list()) -> RestRemainingMessages :: list(). dispatch_remaining_messages(#subscriber{subscriber_pid = SubscriberPid, components = Components}, RemainingMessages) when is_list(RemainingMessages) -> %% 处理遗留的消息 lists:foldl(fun({Topic0, Content0}, Acc) -> Components0 = of_components(Topic0), case match_components(Components, Components0) of true -> SubscriberPid ! {topic_broadcast, Topic0, Content0}, Acc; false -> [{Topic0, Content0}|Acc] end end, [], RemainingMessages).