session, finally count down

This commit is contained in:
Feng Lee 2015-06-17 01:25:08 +08:00
parent 08a64ee97b
commit 38e0ba08d2
4 changed files with 278 additions and 296 deletions

View File

@ -1,71 +0,0 @@
%%%-----------------------------------------------------------------------------
%%% Copyright (c) 2012-2015 eMQTT.IO, All Rights Reserved.
%%%
%%% 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.
%%%-----------------------------------------------------------------------------
%%% @doc
%%% Inflight window of message queue. Wrap a list with len.
%%%
%%% @end
%%%-----------------------------------------------------------------------------
-module(emqttd_inflight).
-author("Feng Lee <feng@emqtt.io>").
-include("emqttd.hrl").
-export([new/2, is_full/1, len/1, in/2, ack/2]).
-define(MAX_SIZE, 100).
-record(inflight, {name, q = [], len = 0, size = ?MAX_SIZE}).
-type inflight() :: #inflight{}.
-export_type([inflight/0]).
new(Name, Max) ->
#inflight{name = Name, size = Max}.
is_full(#inflight{size = 0}) ->
false;
is_full(#inflight{len = Len, size = Size}) when Len < Size ->
false;
is_full(_Inflight) ->
true.
len(#inflight{len = Len}) ->
Len.
in(_Msg, #inflight{len = Len, size = Size})
when Len =:= Size -> {error, full};
in(Msg = #mqtt_message{msgid = MsgId}, Inflight = #inflight{q = Q, len = Len}) ->
{ok, Inflight#inflight{q = [{MsgId, Msg}|Q], len = Len +1}}.
ack(MsgId, Inflight = #inflight{q = Q, len = Len}) ->
case lists:keyfind(MsgId, 1, Q) of
false ->
lager:error("Inflight(~s) cannot find msgid: ~p", [MsgId]),
Inflight;
_Msg ->
Inflight#inflight{q = lists:keydelete(MsgId, 1, Q), len = Len - 1}
end.

View File

@ -112,28 +112,21 @@ len(#mqueue{len = Len}) -> Len.
%% @end
%%------------------------------------------------------------------------------
-spec in({new | old, mqtt_message()}, mqueue()) -> mqueue().
-spec in({newcome | pending, mqtt_message()}, mqueue()) -> mqueue().
%% drop qos0
in({_, #mqtt_message{qos = ?QOS_0}}, MQ = #mqueue{qos0 = false}) ->
MQ;
%% simply drop the oldest one if queue is full, improve later
in({new, Msg}, MQ = #mqueue{name = Name, q = Q, len = Len, max_len = MaxLen})
in(Msg, MQ = #mqueue{name = Name, q = Q, len = Len, max_len = MaxLen})
when Len =:= MaxLen ->
{{value, OldMsg}, Q2} = queue:out(Q),
lager:error("queue(~s) drop message: ~p", [Name, OldMsg]),
lager:error("MQueue(~s) drop message: ~p", [Name, OldMsg]),
MQ#mqueue{q = queue:in(Msg, Q2)};
in({old, Msg}, MQ = #mqueue{name = Name, len = Len, max_len = MaxLen})
when Len =:= MaxLen ->
lager:error("queue(~s) drop message: ~p", [Name, Msg]), MQ;
in({new, Msg}, MQ = #mqueue{q = Q, len = Len}) ->
maybe_set_alarm(MQ#mqueue{q = queue:in(Msg, Q), len = Len + 1});
in({old, Msg}, MQ = #mqueue{q = Q, len = Len}) ->
MQ#mqueue{q = queue:in_r(Msg, Q), len = Len + 1}.
in(Msg, MQ = #mqueue{q = Q, len = Len}) ->
maybe_set_alarm(MQ#mqueue{q = queue:in(Msg, Q), len = Len + 1}).
out(MQ = #mqueue{len = 0}) ->
{empty, MQ};

View File

@ -123,7 +123,7 @@ redeliver(Topic, CPid) when is_binary(Topic) andalso is_pid(CPid) ->
dispatch(_CPid, []) ->
ignore;
dispatch(CPid, Msgs) when is_list(Msgs) ->
CPid ! {dispatch, [Msg || Msg <- Msgs]};
[CPid ! {dispatch, Msg} || Msg <- Msgs];
dispatch(CPid, Msg) when is_record(Msg, mqtt_message) ->
CPid ! {dispatch, Msg}.

View File

@ -40,6 +40,8 @@
%%%
%%% 5. Optionally, QoS 0 messages pending transmission to the Client.
%%%
%%% State of Message: newcome, inflight, pending
%%%
%%% @end
%%%-----------------------------------------------------------------------------
@ -86,13 +88,15 @@
%% QoS 1 and QoS 2 messages which have been sent to the Client,
%% but have not been completely acknowledged.
%% Client <- Broker
inflight_queue :: emqttd_inflight:inflight(),
inflight_queue :: list(),
max_inflight = 0,
%% All qos1, qos2 messages published to when client is disconnected.
%% QoS 1 and QoS 2 messages pending transmission to the Client.
%%
%% Optionally, QoS 0 messages pending transmission to the Client.
pending_queue :: emqttd_mqueue:mqueue(),
message_queue :: emqttd_mqueue:mqueue(),
%% Inflight qos2 messages received from client and waiting for pubrel.
%% QoS 2 messages which have been received from the Client,
@ -100,25 +104,26 @@
%% Client -> Broker
awaiting_rel :: map(),
%% Awaiting PUBREL timeout
await_rel_timeout = 8,
%% Max Packets that Awaiting PUBREL
max_awaiting_rel = 100,
%% Awaiting timers for ack, rel and comp.
awaiting_ack :: map(),
awaiting_comp :: map(),
%% Retries to resend the unacked messages
unack_retries = 3,
%% 4, 8, 16 seconds if 3 retries:)
unack_timeout = 4,
%% Awaiting PUBREL timeout
await_rel_timeout = 8,
%% Max Packets that Awaiting PUBREL
max_awaiting_rel = 100,
%% Awaiting for PUBCOMP
awaiting_comp :: map(),
%% session expired after 48 hours
expired_after = 48,
expired_after = 172800,
expired_timer,
@ -128,7 +133,7 @@
%% @doc Start a session.
%% @end
%%------------------------------------------------------------------------------
-spec start_link(boolean(), binary(), pid()) -> {ok, pid()} | {error, any()}.
-spec start_link(boolean(), mqtt_clientid(), pid()) -> {ok, pid()} | {error, any()}.
start_link(CleanSess, ClientId, ClientPid) ->
gen_server:start_link(?MODULE, [CleanSess, ClientId, ClientPid], []).
@ -136,7 +141,7 @@ start_link(CleanSess, ClientId, ClientPid) ->
%% @doc Resume a session.
%% @end
%%------------------------------------------------------------------------------
-spec resume(pid(), binary(), pid()) -> ok.
-spec resume(pid(), mqtt_clientid(), pid()) -> ok.
resume(Session, ClientId, ClientPid) ->
gen_server:cast(Session, {resume, ClientId, ClientPid}).
@ -144,7 +149,7 @@ resume(Session, ClientId, ClientPid) ->
%% @doc Destroy a session.
%% @end
%%------------------------------------------------------------------------------
-spec destroy(Session:: pid(), ClientId :: binary()) -> ok.
-spec destroy(pid(), mqtt_clientid()) -> ok.
destroy(Session, ClientId) ->
gen_server:call(Session, {destroy, ClientId}).
@ -160,7 +165,7 @@ subscribe(Session, TopicTable) ->
%% @doc Publish message
%% @end
%%------------------------------------------------------------------------------
-spec publish(Session :: pid(), {mqtt_qos(), mqtt_message()}) -> ok.
-spec publish(pid(), mqtt_message()) -> ok.
publish(_Session, Msg = #mqtt_message{qos = ?QOS_0}) ->
%% publish qos0 directly
emqttd_pubsub:publish(Msg);
@ -210,24 +215,23 @@ init([CleanSess, ClientId, ClientPid]) ->
true = link(ClientPid),
QEnv = emqttd:env(mqtt, queue),
SessEnv = emqttd:env(mqtt, session),
PendingQ = emqttd_mqueue:new(ClientId, QEnv),
InflightQ = emqttd_inflight:new(ClientId, emqttd_opts:g(max_inflight, SessEnv)),
Session = #session{
clean_sess = CleanSess,
clientid = ClientId,
client_pid = ClientPid,
subscriptions = [],
inflight_queue = InflightQ,
pending_queue = PendingQ,
awaiting_rel = #{},
awaiting_ack = #{},
awaiting_comp = #{},
unack_retries = emqttd_opts:g(unack_retries, SessEnv),
unack_timeout = emqttd_opts:g(unack_timeout, SessEnv),
await_rel_timeout = emqttd_opts:g(await_rel_timeout, SessEnv),
max_awaiting_rel = emqttd_opts:g(max_awaiting_rel, SessEnv),
expired_after = emqttd_opts:g(expired_after, SessEnv) * 3600,
timestamp = os:timestamp()},
clean_sess = CleanSess,
clientid = ClientId,
client_pid = ClientPid,
subscriptions = [],
inflight_queue = [],
max_inflight = emqttd_opts:g(max_inflight, SessEnv, 0),
message_queue = emqttd_mqueue:new(ClientId, QEnv),
awaiting_rel = #{},
awaiting_ack = #{},
awaiting_comp = #{},
unack_retries = emqttd_opts:g(unack_retries, SessEnv),
unack_timeout = emqttd_opts:g(unack_timeout, SessEnv),
await_rel_timeout = emqttd_opts:g(await_rel_timeout, SessEnv),
max_awaiting_rel = emqttd_opts:g(max_awaiting_rel, SessEnv),
expired_after = emqttd_opts:g(expired_after, SessEnv) * 3600,
timestamp = os:timestamp()},
{ok, Session, hibernate}.
handle_call({subscribe, Topics}, _From, Session = #session{clientid = ClientId,
@ -237,33 +241,36 @@ handle_call({subscribe, Topics}, _From, Session = #session{clientid = ClientId,
{ok, GrantedQos} = emqttd_pubsub:subscribe(Topics),
lager:info([{client, ClientId}], "Session ~s subscribe ~p, Granted QoS: ~p",
[ClientId, Topics, GrantedQos]),
[ClientId, Topics, GrantedQos]),
Subscriptions1 =
lists:foldl(fun({Topic, Qos}, Acc) ->
case lists:keyfind(Topic, 1, Acc) of
{Topic, Qos} ->
lager:warning([{client, ClientId}], "Session ~s resubscribe ~p: qos = ~p", [ClientId, Topic, Qos]), Acc;
{Topic, Old} ->
lager:warning([{client, ClientId}], "Session ~s resubscribe ~p: old qos=~p, new qos=~p",
[ClientId, Topic, Old, Qos]),
lists:keyreplace(Topic, 1, Acc, {Topic, Qos});
false ->
%%TODO: the design is ugly, rewrite later...:(
%% <MQTT V3.1.1>: 3.8.4
%% Where the Topic Filter is not identical to any existing Subscriptions filter,
%% a new Subscription is created and all matching retained messages are sent.
emqttd_msg_store:redeliver(Topic, self()),
[{Topic, Qos} | Acc]
end
end, Subscriptions, Topics),
case lists:keyfind(Topic, 1, Acc) of
{Topic, Qos} ->
lager:warning([{client, ClientId}], "Session ~s "
"resubscribe ~p: qos = ~p", [ClientId, Topic, Qos]), Acc;
{Topic, OldQos} ->
lager:warning([{client, ClientId}], "Session ~s "
"resubscribe ~p: old qos=~p, new qos=~p", [ClientId, Topic, OldQos, Qos]),
lists:keyreplace(Topic, 1, Acc, {Topic, Qos});
false ->
%%TODO: the design is ugly, rewrite later...:(
%% <MQTT V3.1.1>: 3.8.4
%% Where the Topic Filter is not identical to any existing Subscriptions filter,
%% a new Subscription is created and all matching retained messages are sent.
emqttd_msg_store:redeliver(Topic, self()),
[{Topic, Qos} | Acc]
end
end, Subscriptions, Topics),
{reply, {ok, GrantedQos}, Session#session{subscriptions = Subscriptions1}};
handle_call({unsubscribe, Topics}, _From, Session = #session{clientid = ClientId, subscriptions = Subscriptions}) ->
handle_call({unsubscribe, Topics}, _From, Session = #session{clientid = ClientId,
subscriptions = Subscriptions}) ->
%%unsubscribe from topic tree
%% unsubscribe from topic tree
ok = emqttd_pubsub:unsubscribe(Topics),
lager:info([{client, ClientId}], "Session ~s unsubscribe ~p.", [ClientId, Topics]),
lager:info([{client, ClientId}], "Session ~s unsubscribe ~p", [ClientId, Topics]),
Subscriptions1 =
lists:foldl(fun(Topic, Acc) ->
@ -271,21 +278,24 @@ handle_call({unsubscribe, Topics}, _From, Session = #session{clientid = ClientId
{Topic, _Qos} ->
lists:keydelete(Topic, 1, Acc);
false ->
lager:warning([{client, ClientId}], "~s not subscribe ~s", [ClientId, Topic]), Acc
lager:warning([{client, ClientId}], "Session ~s not subscribe ~s", [ClientId, Topic]), Acc
end
end, Subscriptions, Topics),
{reply, ok, Session#session{subscriptions = Subscriptions1}};
handle_call({publish, Message = #mqtt_message{qos = ?QOS_2, msgid = MsgId}}, _From,
Session = #session{clientid = ClientId, awaiting_rel = AwaitingRel, await_rel_timeout = Timeout}) ->
handle_call({publish, Msg = #mqtt_message{qos = ?QOS_2, msgid = MsgId}}, _From,
Session = #session{clientid = ClientId,
awaiting_rel = AwaitingRel,
await_rel_timeout = Timeout}) ->
case check_awaiting_rel(Session) of
true ->
true ->
TRef = timer(Timeout, {timeout, awaiting_rel, MsgId}),
{reply, ok, Session#session{awaiting_rel = maps:put(MsgId, {Message, TRef}, AwaitingRel)}};
AwaitingRel1 = maps:put(MsgId, {Msg, TRef}, AwaitingRel),
{reply, ok, Session#session{awaiting_rel = AwaitingRel1}};
false ->
lager:error([{clientid, ClientId}], "Session ~s "
" dropped Qos2 message for too many awaiting_rel: ~p", [ClientId, Message]),
lager:critical([{clientid, ClientId}], "Session ~s dropped Qos2 message "
"for too many awaiting_rel: ~p", [ClientId, Msg]),
{reply, {error, dropped}, Session}
end;
@ -297,59 +307,52 @@ handle_call(Req, _From, State) ->
lager:critical("Unexpected Request: ~p", [Req]),
{reply, {error, badreq}, State}.
handle_cast({resume, ClientId, ClientPid}, State = #session{
clientid = ClientId,
client_pid = OldClientPid,
pending_queue = Queue,
awaiting_ack = AwaitingAck,
awaiting_comp = AwaitingComp,
expired_timer = ETimer}) ->
handle_cast({resume, ClientId, ClientPid}, Session) ->
#session{clientid = ClientId,
client_pid = OldClientPid,
inflight_queue = InflightQ,
awaiting_ack = AwaitingAck,
awaiting_comp = AwaitingComp,
expired_timer = ETimer} = Session,
lager:info([{client, ClientId}], "Session ~s resumed by ~p",[ClientId, ClientPid]),
%% kick old client...
if
OldClientPid =:= undefined ->
ok;
OldClientPid =:= ClientPid ->
ok;
true ->
lager:error("Session '~s' is duplicated: pid=~p, oldpid=~p", [ClientId, ClientPid, OldClientPid]),
unlink(OldClientPid),
OldClientPid ! {stop, duplicate_id, ClientPid}
end,
%% cancel expired timer
cancel_timer(ETimer),
%% cancel timeout timer
emqttd_util:cancel_timer(ETimer),
kick(ClientId, ClientPid, OldClientPid),
%% redelivery PUBREL
lists:foreach(fun(MsgId) ->
ClientPid ! {redeliver, {?PUBREL, MsgId}}
end, maps:keys(AwaitingComp)),
%% Redeliver PUBREL
[ClientPid ! {redeliver, {?PUBREL, MsgId}} || MsgId <- maps:keys(AwaitingComp)],
%% redelivery messages that awaiting PUBACK or PUBREC
Dup = fun(Msg) -> Msg#mqtt_message{dup = true} end,
lists:foreach(fun(Msg) ->
ClientPid ! {dispatch, {self(), Dup(Msg)}}
end, maps:values(AwaitingAck)),
%% Clear awaiting_ack timers
[cancel_timer(TRef) || {_, TRef} <- maps:values(AwaitingAck)],
%% send offline messages
lists:foreach(fun(Msg) ->
ClientPid ! {dispatch, {self(), Msg}}
end, emqttd_queue:all(Queue)),
%% Clear awaiting_comp timers
[cancel_timer(TRef) || TRef <- maps:values(AwaitingComp)],
{noreply, State#session{client_pid = ClientPid,
%%TODO:
pending_queue = emqttd_queue:clear(Queue),
expired_timer = undefined}, hibernate};
Session1 = Session#session{client_pid = ClientPid,
awaiting_ack = #{},
awaiting_comp = #{},
expired_timer = undefined},
%% Redeliver inflight messages
Session2 =
lists:foldl(fun({_Id, Msg}, Sess) ->
redeliver(Msg#mqtt_message{dup = true}, Sess)
end, Session1, lists:reverse(InflightQ)),
handle_cast({puback, MsgId}, Session = #session{clientid = ClientId, inflight_queue = Q, awaiting_ack = Awaiting}) ->
%% Dequeue pending messages
{noreply, dequeue(Session2), hibernate};
%% PUBRAC
handle_cast({puback, MsgId}, Session = #session{clientid = ClientId, awaiting_ack = Awaiting}) ->
case maps:find(MsgId, Awaiting) of
{ok, {_, TRef}} ->
catch erlang:cancel_timer(TRef),
{noreply, dispatch(Session#session{inflight_queue = emqttd_inflight:ack(MsgId, Q),
awaiting_ack = maps:remove(MsgId, Awaiting)})};
cancel_timer(TRef),
Session1 = acked(MsgId, Session),
{noreply, dequeue(Session1)};
error ->
lager:error("Session ~s cannot find PUBACK '~p'!", [ClientId, MsgId]),
{noreply, Session}
@ -362,33 +365,36 @@ handle_cast({pubrec, MsgId}, Session = #session{clientid = ClientId,
await_rel_timeout = Timeout}) ->
case maps:find(MsgId, AwaitingAck) of
{ok, {_, TRef}} ->
catch erlang:cancel_timer(TRef),
cancel_timer(TRef),
TRef1 = timer(Timeout, {timeout, awaiting_comp, MsgId}),
{noreply, dispatch(Session#session{awaiting_ack = maps:remove(MsgId, AwaitingAck),
awaiting_comp = maps:put(MsgId, TRef1, AwaitingComp)})};
Session1 = acked(MsgId, Session#session{awaiting_comp = maps:put(MsgId, TRef1, AwaitingComp)}),
{noreply, dequeue(Session1)};
error ->
lager:error("Session ~s cannot find PUBREC '~p'!", [ClientId, MsgId]),
{noreply, Session}
end;
handle_cast({pubrel, MsgId}, Session = #session{clientid = ClientId, awaiting_rel = AwaitingRel}) ->
%% PUBREL
handle_cast({pubrel, MsgId}, Session = #session{clientid = ClientId,
awaiting_rel = AwaitingRel}) ->
case maps:find(MsgId, AwaitingRel) of
{ok, {Msg, TRef}} ->
catch erlang:cancel_timer(TRef),
cancel_timer(TRef),
emqttd_pubsub:publish(Msg),
{noreply, Session#session{awaiting_rel = maps:remove(MsgId, AwaitingRel)}};
error ->
lager:error("Session ~s cannot find PUBREL '~p'!", [ClientId, MsgId]),
lager:error("Session ~s cannot find PUBREL: msgid=~p!", [ClientId, MsgId]),
{noreply, Session}
end;
%% PUBCOMP
handle_cast({pubcomp, MsgId}, Session = #session{clientid = ClientId, awaiting_comp = AwaitingComp}) ->
case maps:is_key(MsgId, AwaitingComp) of
true ->
case maps:find(MsgId, AwaitingComp) of
{ok, TRef} ->
cancel_timer(TRef),
{noreply, Session#session{awaiting_comp = maps:remove(MsgId, AwaitingComp)}};
false ->
lager:error("Session ~s cannot find PUBREC MsgId '~p'", [ClientId, MsgId]),
error ->
lager:error("Session ~s cannot find PUBCOMP: MsgId=~p", [ClientId, MsgId]),
{noreply, Session}
end;
@ -396,61 +402,103 @@ handle_cast(Msg, State) ->
lager:critical("Unexpected Msg: ~p, State: ~p", [Msg, State]),
{noreply, State}.
handle_info({dispatch, MsgList}, Session) when is_list(MsgList) ->
NewSession = lists:foldl(fun(Msg, S) ->
dispatch({new, Msg}, S)
end, Session, MsgList),
{noreply, NewSession};
%% Queue messages when client is offline
handle_info({dispatch, Msg}, Session = #session{client_pid = undefined,
message_queue = Q})
when is_record(Msg, mqtt_message) ->
{noreply, Session#session{message_queue = emqttd_mqueue:in(Msg, Q)}};
handle_info({dispatch, {old, Msg}}, Session) when is_record(Msg, mqtt_message) ->
{noreply, dispatch({old, Msg}, Session)};
%% Dispatch qos0 message directly to client
handle_info({dispatch, Msg = #mqtt_message{qos = ?QOS_0}},
Session = #session{client_pid = ClientPid}) ->
ClientPid ! {deliver, Msg},
{noreply, Session};
handle_info({dispatch, Msg}, Session) when is_record(Msg, mqtt_message) ->
{noreply, dispatch({new, Msg}, Session)};
handle_info({dispatch, Msg = #mqtt_message{qos = QoS}},
Session = #session{clientid = ClientId, message_queue = MsgQ})
when QoS =:= ?QOS_1 orelse QoS =:= ?QOS_2 ->
case check_inflight(Session) of
true ->
{noreply, deliver(Msg, Session)};
false ->
lager:warning([{client, ClientId}], "Session ~s inflight queue is full!", [ClientId]),
{noreply, Session#session{message_queue = emqttd_mqueue:in(Msg, MsgQ)}}
end;
handle_info({timeout, awaiting_ack, MsgId}, Session = #session{client_pid = undefined,
awaiting_ack = AwaitingAck}) ->
%% just remove awaiting
{noreply, Session#session{awaiting_ack = maps:remove(MsgId, AwaitingAck)}};
handle_info({timeout, awaiting_ack, MsgId}, Session = #session{clientid = ClientId,
inflight_queue = InflightQ,
awaiting_ack = AwaitingAck}) ->
case maps:find(MsgId, AwaitingAck) of
{ok, {{0, _Timeout}, _TRef}} ->
Session1 = Session#session{inflight_queue = lists:keydelete(MsgId, 1, InflightQ),
awaiting_ack = maps:remove(MsgId, AwaitingAck)},
{noreply, dequeue(Session1)};
{ok, {{Retries, Timeout}, _TRef}} ->
TRef = timer(Timeout, {timeout, awaiting_ack, MsgId}),
AwaitingAck1 = maps:put(MsgId, {{Retries-1, Timeout*2}, TRef}, AwaitingAck),
{noreply, Session#session{awaiting_ack = AwaitingAck1}};
error ->
lager:error([{client, ClientId}], "Session ~s "
"cannot find Awaiting Ack:~p", [ClientId, MsgId]),
{noreply, Session}
end;
handle_info({timeout, awaiting_rel, MsgId}, Session = #session{clientid = ClientId,
awaiting_rel = AwaitingRel}) ->
case maps:find(MsgId, AwaitingRel) of
{ok, {Msg, _TRef}} ->
lager:error([{client, ClientId}], "Session ~s AwaitingRel Timout!~n"
"Drop Message:~p", [ClientId, Msg]),
{noreply, Session#session{awaiting_rel = maps:remove(MsgId, AwaitingRel)}};
error ->
lager:error([{client, ClientId}], "Session ~s Cannot find AwaitingRel: MsgId=~p", [ClientId, MsgId]),
{noreply, Session}
end;
handle_info({timeout, awaiting_comp, MsgId}, Session = #session{clientid = ClientId,
awaiting_comp = Awaiting}) ->
case maps:find(MsgId, Awaiting) of
{ok, _TRef} ->
lager:error([{client, ClientId}], "Session ~s "
"Awaiting PUBCOMP Timout: MsgId=~p!", [ClientId, MsgId]),
{noreply, Session#session{awaiting_comp = maps:remove(MsgId, Awaiting)}};
error ->
lager:error([{client, ClientId}], "Session ~s "
"Cannot find Awaiting PUBCOMP: MsgId=~p", [ClientId, MsgId]),
{noreply, Session}
end;
handle_info({'EXIT', ClientPid, _Reason}, Session = #session{clean_sess = true,
client_pid = ClientPid}) ->
{stop, normal, Session};
handle_info({'EXIT', ClientPid, Reason}, Session = #session{clean_sess = false,
clientid = ClientId,
client_pid = ClientPid,
expired_after = Expires}) ->
%%TODO: Clean puback, pubrel, pubcomp timers
lager:info("Session ~s: client ~p exited for ~p", [ClientId, ClientPid, Reason]),
TRef = timer(Expires * 1000, session_expired),
{noreply, Session#session{expired_timer = TRef}};
lager:info("Session ~s unlink with client ~p: reason=~p", [ClientId, ClientPid, Reason]),
TRef = timer(Expires, session_expired),
{noreply, Session#session{expired_timer = TRef}, hibernate};
handle_info({'EXIT', ClientPid, _Reason}, Session = #session{clean_sess = true, client_pid = ClientPid}) ->
%%TODO: reason...
{stop, normal, Session};
handle_info({'EXIT', Pid, _Reason}, Session = #session{clientid = ClientId,
client_pid = ClientPid}) ->
lager:error("Session ~s received unexpected EXIT:"
" client_pid=~p, exit_pid=~p", [ClientId, ClientPid, Pid]),
{noreply, Session};
handle_info({'EXIT', ClientPid0, _Reason}, State = #session{client_pid = ClientPid}) ->
lager:critical("Unexpected Client EXIT: pid=~p, pid(state): ~p", [ClientPid0, ClientPid]),
{noreply, State};
handle_info(session_expired, State = #session{clientid = ClientId}) ->
handle_info(session_expired, Session = #session{clientid = ClientId}) ->
lager:error("Session ~s expired, shutdown now!", [ClientId]),
{stop, {shutdown, expired}, State};
{stop, {shutdown, expired}, Session};
handle_info({timeout, awaiting_rel, MsgId}, Session = #session{clientid = ClientId, awaiting_rel = Awaiting}) ->
case maps:find(MsgId, Awaiting) of
{ok, {Msg, _TRef}} ->
lager:error([{client, ClientId}], "Session ~s Awaiting Rel Timout!~nDrop Message:~p", [ClientId, Msg]),
{noreply, Session#session{awaiting_rel = maps:remove(MsgId, Awaiting)}};
error ->
lager:error([{client, ClientId}], "Session ~s Cannot find Awaiting Rel: MsgId=~p", [ClientId, MsgId]),
{noreply, Session}
end;
handle_info({timeout, awaiting_comp, MsgId}, Session = #session{clientid = ClientId, awaiting_comp = Awaiting}) ->
case maps:find(MsgId, Awaiting) of
{ok, _TRef} ->
lager:error([{client, ClientId}], "Session ~s Awaiting PUBCOMP Timout: MsgId=~p!", [ClientId, MsgId]),
{noreply, Session#session{awaiting_comp = maps:remove(MsgId, Awaiting)}};
error ->
lager:error([{client, ClientId}], "Session ~s Cannot find Awaiting PUBCOMP: MsgId=~p", [ClientId, MsgId]),
{noreply, Session}
end;
handle_info(Info, Session) ->
lager:critical("Unexpected Info: ~p, Session: ~p", [Info, Session]),
handle_info(Info, Session = #session{clientid = ClientId}) ->
lager:critical("Session ~s received unexpected info: ~p", [ClientId, Info]),
{noreply, Session}.
terminate(_Reason, _Session) ->
@ -464,11 +512,26 @@ code_change(_OldVsn, Session, _Extra) ->
%%%=============================================================================
%%------------------------------------------------------------------------------
%% @private
%% @doc Plubish Qos2 message from client -> broker, and then wait for pubrel.
%% @end
%% Kick duplicated client
%%------------------------------------------------------------------------------
kick(_ClientId, _ClientPid, undefined) ->
ok;
kick(_ClientId, ClientPid, ClientPid) ->
ok;
kick(ClientId, ClientPid, OldClientPid) ->
lager:error("Session '~s' is duplicated: pid=~p, oldpid=~p", [ClientId, ClientPid, OldClientPid]),
unlink(OldClientPid),
OldClientPid ! {stop, duplicate_id, ClientPid}.
%%------------------------------------------------------------------------------
%% Check inflight and awaiting_rel
%%------------------------------------------------------------------------------
check_inflight(#session{max_inflight = 0}) ->
true;
check_inflight(#session{max_inflight = Max, inflight_queue = Q}) ->
Max > length(Q).
check_awaiting_rel(#session{max_awaiting_rel = 0}) ->
true;
@ -476,72 +539,61 @@ check_awaiting_rel(#session{awaiting_rel = AwaitingRel,
max_awaiting_rel = MaxLen}) ->
maps:size(AwaitingRel) < MaxLen.
%%%=============================================================================
%%% Dispatch message from broker -> client.
%%%=============================================================================
%%------------------------------------------------------------------------------
%% Dequeue and Deliver
%%------------------------------------------------------------------------------
dispatch(Session = #session{client_pid = undefined}) ->
%% do nothing
dequeue(Session = #session{client_pid = undefined}) ->
%% do nothing if client is disconnected
Session;
dispatch(Session = #session{pending_queue = PendingQ}) ->
case emqttd_mqueue:out(PendingQ) of
{empty, _Q} ->
Session;
{{value, Msg}, Q1} ->
self() ! {dispatch, {old, Msg}},
Session#session{pending_queue = Q1}
end.
%% queued the message if client is offline
dispatch({Type, Msg}, Session = #session{client_pid = undefined,
pending_queue= PendingQ}) ->
Session#session{pending_queue = emqttd_mqueue:in({Type, Msg}, PendingQ)};
%% dispatch qos0 directly to client process
dispatch({_Type, Msg} = #mqtt_message{qos = ?QOS_0}, Session = #session{client_pid = ClientPid}) ->
ClientPid ! {deliver, Msg}, Session;
%% dispatch qos1/2 message and wait for puback
dispatch({Type, Msg = #mqtt_message{qos = Qos}}, Session = #session{clientid = ClientId,
client_pid = ClientPid,
message_id = MsgId,
pending_queue = PendingQ,
inflight_queue= InflightQ})
when Qos =:= ?QOS_1 orelse Qos =:= ?QOS_2 ->
%% assign id first
Msg1 = Msg#mqtt_message{msgid = MsgId},
Msg2 =
if
Qos =:= ?QOS_1 -> Msg1;
Qos =:= ?QOS_2 -> Msg1#mqtt_message{dup = false}
end,
case emqttd_inflight:in(Msg1, InflightQ) of
{error, full} ->
lager:error("Session ~s inflight queue is full!", [ClientId]),
Session#session{pending_queue = emqttd_mqueue:in({Type, Msg}, PendingQ)};
{ok, InflightQ1} ->
ClientPid ! {deliver, Msg1},
await_ack(Msg1, next_msgid(Session#session{inflight_queue = InflightQ1}))
dequeue(Session) ->
case check_inflight(Session) of
true -> dequeue2(Session);
false -> Session
end.
deliver(Msg, Session) ->
ok.
dequeue2(Session = #session{message_queue = Q}) ->
case emqttd_mqueue:out(Q) of
{empty, _Q} -> Session;
{{value, Msg}, Q1} ->
Session1 = deliver(Msg, Session#session{message_queue = Q1}),
dequeue(Session1) %% dequeue more
end.
await(Msg, Session) ->
ok.
deliver(Msg = #mqtt_message{qos = ?QOS_0}, Session = #session{client_pid = ClientPid}) ->
ClientPid ! {deliver, Msg}, Session;
% message(qos1/2) is awaiting ack
await_ack(Msg = #mqtt_message{msgid = MsgId}, Session = #session{awaiting_ack = Awaiting,
unack_retries = Retries,
unack_timeout = Timeout}) ->
TRef = timer(Timeout * 1000, {retry, MsgId}),
deliver(Msg = #mqtt_message{qos = QoS}, Session = #session{message_id = MsgId,
client_pid = ClientPid,
inflight_queue = InflightQ})
when QoS =:= ?QOS_1 orelse QoS =:= ?QOS_2 ->
Msg1 = Msg#mqtt_message{msgid = MsgId, dup = false},
ClientPid ! {deliver, Msg1},
await(Msg1, next_msgid(Session#session{inflight_queue = [{MsgId, Msg1}|InflightQ]})).
redeliver(Msg = #mqtt_message{qos = ?QOS_0}, Session) ->
deliver(Msg, Session);
redeliver(Msg = #mqtt_message{qos = QoS}, Session = #session{client_pid = ClientPid})
when QoS =:= ?QOS_1 orelse QoS =:= ?QOS_2 ->
ClientPid ! {deliver, Msg},
await(Msg, Session).
%%------------------------------------------------------------------------------
%% Awaiting ack for qos1, qos2 message
%%------------------------------------------------------------------------------
await(#mqtt_message{msgid = MsgId}, Session = #session{awaiting_ack = Awaiting,
unack_retries = Retries,
unack_timeout = Timeout}) ->
TRef = timer(Timeout, {timeout, awaiting_ack, MsgId}),
Awaiting1 = maps:put(MsgId, {{Retries, Timeout}, TRef}, Awaiting),
Session#session{awaiting_ack = Awaiting1}.
timer(Timeout, TimeoutMsg) ->
erlang:send_after(Timeout * 1000, self(), TimeoutMsg).
acked(MsgId, Session = #session{inflight_queue = InflightQ,
awaiting_ack = Awaiting}) ->
Session#session{inflight_queue = lists:keydelete(MsgId, 1, InflightQ),
awaiting_ack = maps:remove(MsgId, Awaiting)}.
next_msgid(Session = #session{message_id = 16#ffff}) ->
Session#session{message_id = 1};
@ -549,3 +601,11 @@ next_msgid(Session = #session{message_id = 16#ffff}) ->
next_msgid(Session = #session{message_id = MsgId}) ->
Session#session{message_id = MsgId + 1}.
timer(Timeout, TimeoutMsg) ->
erlang:send_after(Timeout * 1000, self(), TimeoutMsg).
cancel_timer(undefined) ->
undefined;
cancel_timer(Ref) ->
catch erlang:cancel_timer(Ref).