%%----------------------------------------------------------------------------- %% Copyright (c) 2014, Feng Lee %% %% Permission is hereby granted, free of charge, to any person obtaining a copy %% of this software and associated documentation files (the "Software"), to deal %% in the Software without restriction, including without limitation the rights %% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell %% copies of the Software, and to permit persons to whom the Software is %% furnished to do so, subject to the following conditions: %% %% The above copyright notice and this permission notice shall be included in all %% copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, %% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE %% SOFTWARE. %%------------------------------------------------------------------------------ -module(emqtt_pubsub). -author('feng@slimchat.io'). -include("emqtt.hrl"). -include("emqtt_log.hrl"). -include("emqtt_topic.hrl"). -include_lib("stdlib/include/qlc.hrl"). %% ------------------------------------------------------------------ %% API Function Exports %% ------------------------------------------------------------------ -export([start_link/0]). -export([topics/0, subscribe/2, unsubscribe/2, publish/1, publish/2, %local node dispatch/2, match/1]). %% ------------------------------------------------------------------ %% gen_server Function Exports %% ------------------------------------------------------------------ -behaviour(gen_server). -define(SERVER, ?MODULE). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -record(state, {}). %% ------------------------------------------------------------------ %% API Function Definitions %% ------------------------------------------------------------------ %% %% @doc Start Pubsub. %% start_link() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). %% %% @doc All topics %% -spec topics() -> list(topic()). topics() -> mnesia:dirty_all_keys(topic). %% %% @doc Subscribe Topic %% -spec subscribe({Topic :: binary(), Qos :: qos()}, SubPid :: pid()) -> any(). subscribe({Topic, Qos}, SubPid) when is_binary(Topic) and is_pid(SubPid) -> gen_server:call(?SERVER, {subscribe, {Topic, Qos}, SubPid}). %% %% @doc Unsubscribe Topic %% -spec unsubscribe(Topic :: binary(), SubPid :: pid()) -> any(). unsubscribe(Topic, SubPid) when is_binary(Topic) and is_pid(SubPid) -> gen_server:cast(?SERVER, {unsubscribe, Topic, SubPid}). %% %% @doc Publish to cluster node. %% -spec publish(Msg :: mqtt_msg()) -> ok. publish(Msg=#mqtt_msg{topic=Topic}) -> publish(Topic, Msg). -spec publish(Topic :: binary(), Msg :: mqtt_msg()) -> any(). publish(Topic, Msg) when is_binary(Topic) -> lists:foreach(fun(#topic{name=Name, node=Node}) -> case Node =:= node() of true -> dispatch(Name, Msg); false -> rpc:call(Node, ?MODULE, dispatch, [Name, Msg]) end end, match(Topic)). %dispatch locally, should only be called by publish dispatch(Topic, Msg) when is_binary(Topic) -> [SubPid ! {dispatch, Msg} || #topic_subscriber{subpid=SubPid} <- ets:lookup(topic_subscriber, Topic)]. -spec match(Topic :: binary()) -> [topic()]. match(Topic) when is_binary(Topic) -> TrieNodes = mnesia:async_dirty(fun trie_match/1, [emqtt_topic:words(Topic)]), Names = [Name || #topic_trie_node{topic=Name} <- TrieNodes, Name=/= undefined], lists:flatten([mnesia:dirty_read(topic, Name) || Name <- Names]). %% ------------------------------------------------------------------ %% gen_server Function Definitions %% ------------------------------------------------------------------ init([]) -> mnesia:create_table(topic_trie, [ {ram_copies, [node()]}, {attributes, record_info(fields, topic_trie)}]), mnesia:create_table(topic_trie_node, [ {ram_copies, [node()]}, {attributes, record_info(fields, topic_trie_node)}]), mnesia:create_table(topic, [ {type, bag}, {record_name, topic}, {ram_copies, [node()]}, {attributes, record_info(fields, topic)}]), mnesia:add_table_copy(topic_trie, node(), ram_copies), mnesia:add_table_copy(topic_trie_node, node(), ram_copies), mnesia:add_table_copy(topic, node(), ram_copies), ets:new(topic_subscriber, [bag, named_table, {keypos, 2}]), {ok, #state{}}. handle_call({subscribe, {Topic, Qos}, SubPid}, _From, State) -> case mnesia:transaction(fun trie_add/1, [Topic]) of {atomic, _} -> case get({subscriber, SubPid}) of undefined -> MonRef = erlang:monitor(process, SubPid), put({subcriber, SubPid}, MonRef), put({submon, MonRef}, SubPid); _ -> already_monitored end, ets:insert(topic_subscriber, #topic_subscriber{topic=Topic, qos = Qos, subpid=SubPid}), {reply, ok, State}; {aborted, Reason} -> {reply, {error, Reason}, State} end; handle_call(Req, _From, State) -> {stop, {badreq, Req}, State}. handle_cast({unsubscribe, Topic, SubPid}, State) -> ets:match_delete(topic_subscriber, #topic_subscriber{topic=Topic, qos ='_', subpid=SubPid}), try_remove_topic(Topic), {noreply, State}; handle_cast(Msg, State) -> {stop, {badmsg, Msg}, State}. handle_info({'DOWN', Mon, _Type, _Object, _Info}, State) -> case get({submon, Mon}) of undefined -> ?ERROR("unexpected 'DOWN': ~p", [Mon]); SubPid -> erase({submon, Mon}), erase({subscriber, SubPid}), Subs = ets:match_object(topic_subscriber, #topic_subscriber{subpid=SubPid, _='_'}), [ets:delete_object(topic_subscriber, Sub) || Sub <- Subs], [try_remove_topic(Topic) || #topic_subscriber{topic=Topic} <- Subs] end, {noreply, State}; handle_info(Info, State) -> {stop, {badinfo, Info}, State}. terminate(_Reason, _State) -> ok. code_change(_OldVsn, State, _Extra) -> {ok, State}. %% ------------------------------------------------------------------ %% Internal Function Definitions %% ------------------------------------------------------------------ try_remove_topic(Name) when is_binary(Name) -> case ets:member(topic_subscriber, Name) of false -> Topic = emqtt_topic:new(Name), Fun = fun() -> mnesia:delete_object(Topic), case mnesia:read(topic, Name) of [] -> trie_delete(Name); _ -> ignore end end, mnesia:transaction(Fun); true -> ok end. trie_add(Topic) when is_binary(Topic) -> mnesia:write(emqtt_topic:new(Topic)), case mnesia:read(topic_trie_node, Topic) of [TrieNode=#topic_trie_node{topic=undefined}] -> mnesia:write(TrieNode#topic_trie_node{topic=Topic}); [#topic_trie_node{topic=Topic}] -> ignore; [] -> %add trie path [trie_add_path(Triple) || Triple <- emqtt_topic:triples(Topic)], %add last node mnesia:write(#topic_trie_node{node_id=Topic, topic=Topic}) end. trie_delete(Topic) when is_binary(Topic) -> case mnesia:read(topic_trie_node, Topic) of [#topic_trie_node{edge_count=0}] -> mnesia:delete({topic_trie_node, Topic}), trie_delete_path(lists:reverse(emqtt_topic:triples(Topic))); [TrieNode] -> mnesia:write(TrieNode#topic_trie_node{topic=Topic}); [] -> ignore end. trie_match(Words) -> trie_match(root, Words, []). trie_match(NodeId, [], ResAcc) -> mnesia:read(topic_trie_node, NodeId) ++ 'trie_match_#'(NodeId, ResAcc); trie_match(NodeId, [W|Words], ResAcc) -> lists:foldl(fun(WArg, Acc) -> case mnesia:read(topic_trie, #topic_trie_edge{node_id=NodeId, word=WArg}) of [#topic_trie{node_id=ChildId}] -> trie_match(ChildId, Words, Acc); [] -> Acc end end, 'trie_match_#'(NodeId, ResAcc), [W, "+"]). 'trie_match_#'(NodeId, ResAcc) -> case mnesia:read(topic_trie, #topic_trie_edge{node_id=NodeId, word="#"}) of [#topic_trie{node_id=ChildId}] -> mnesia:read(topic_trie_node, ChildId) ++ ResAcc; [] -> ResAcc end. trie_add_path({Node, Word, Child}) -> Edge = #topic_trie_edge{node_id=Node, word=Word}, case mnesia:read(topic_trie_node, Node) of [TrieNode = #topic_trie_node{edge_count=Count}] -> case mnesia:read(topic_trie, Edge) of [] -> mnesia:write(TrieNode#topic_trie_node{edge_count=Count+1}), mnesia:write(#topic_trie{edge=Edge, node_id=Child}); [_] -> ok end; [] -> mnesia:write(#topic_trie_node{node_id=Node, edge_count=1}), mnesia:write(#topic_trie{edge=Edge, node_id=Child}) end. trie_delete_path([]) -> ok; trie_delete_path([{NodeId, Word, _} | RestPath]) -> Edge = #topic_trie_edge{node_id=NodeId, word=Word}, mnesia:delete({topic_trie, Edge}), case mnesia:read(topic_trie_node, NodeId) of [#topic_trie_node{edge_count=1, topic=undefined}] -> mnesia:delete({topic_trie_node, NodeId}), trie_delete_path(RestPath); [TrieNode=#topic_trie_node{edge_count=1, topic=_}] -> mnesia:write(TrieNode#topic_trie_node{edge_count=0}); [TrieNode=#topic_trie_node{edge_count=C}] -> mnesia:write(TrieNode#topic_trie_node{edge_count=C-1}); [] -> throw({notfound, NodeId}) end.