Merge pull request #3113 from emqx/master

Auto-pull-request-by-2019-12-16
This commit is contained in:
turtleDeng 2019-12-16 22:59:01 +08:00 committed by GitHub
commit a3c228e5e3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 1716 additions and 1060 deletions

View File

@ -1,5 +1,5 @@
##====================================================================
## EMQ X Configuration R3.0
## EMQ X Configuration R4.0
##====================================================================
##--------------------------------------------------------------------

View File

@ -69,9 +69,9 @@
topic :: binary(),
%% Message Payload
payload :: binary(),
%% Timestamp
timestamp :: erlang:timestamp()
}).
%% Timestamp (Unit: millisecond)
timestamp :: integer()
}).
-record(delivery, {
sender :: pid(), %% Sender of the delivery

View File

@ -2,8 +2,8 @@
{deps,
[{jsx, "2.10.0"},
{cowboy, "2.7.0"},
{gproc, "0.8.0"},
{cowboy, {git, "https://github.com/emqx/cowboy", {tag, "2.7.1"}}},
{esockd, {git, "https://github.com/emqx/esockd", {tag, "5.6.0"}}},
{ekka, {git, "https://github.com/emqx/ekka", {tag, "0.7.1"}}},
{gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.4.1"}}},

View File

@ -1,6 +1,6 @@
%%-*- mode: erlang -*-
%% .app.src.script
%% .app.src.script
Config = case os:getenv("EMQX_DESC") of
false -> CONFIG; % env var not defined
[] -> CONFIG; % env var set to empty string
@ -31,4 +31,4 @@ case os:getenv("EMQX_DEPS_DEFAULT_VSN") of
AppConf0 = lists:keystore(vsn, 1, AppConf, {vsn, RemoveLeadingV(Tag)}),
{application, App, AppConf0}
end || Conf = {application, App, AppConf} <- Config]
end.
end.

View File

@ -105,7 +105,7 @@ init(_) ->
{ok, []}.
handle_event({set_alarm, {AlarmId, AlarmDesc = #alarm{timestamp = undefined}}}, State) ->
handle_event({set_alarm, {AlarmId, AlarmDesc#alarm{timestamp = os:timestamp()}}}, State);
handle_event({set_alarm, {AlarmId, AlarmDesc#alarm{timestamp = erlang:system_time(second)}}}, State);
handle_event({set_alarm, Alarm = {AlarmId, AlarmDesc}}, State) ->
?LOG(warning, "New Alarm: ~p, Alarm Info: ~p", [AlarmId, AlarmDesc]),
case encode_alarm(Alarm) of
@ -158,7 +158,7 @@ encode_alarm({AlarmId, #alarm{severity = Severity,
{desc, [{severity, Severity},
{title, iolist_to_binary(Title)},
{summary, iolist_to_binary(Summary)},
{timestamp, emqx_misc:now_to_secs(Ts)}]}]);
{timestamp, Ts}]}]);
encode_alarm({AlarmId, undefined}) ->
emqx_json:safe_encode([{id, maybe_to_binary(AlarmId)}]);
encode_alarm({AlarmId, AlarmDesc}) ->
@ -194,5 +194,5 @@ clear_alarm_(Id) ->
set_alarm_history(Id, Desc) ->
His = #alarm_history{id = Id,
desc = Desc,
clear_at = os:timestamp()},
clear_at = erlang:system_time(second)},
mnesia:dirty_write(?ALARM_HISTORY_TAB, His).

View File

@ -191,20 +191,21 @@ do_unsubscribe(undefined, Topic, SubPid, SubOpts) ->
do_unsubscribe(Group, Topic, SubPid, _SubOpts) ->
emqx_shared_sub:unsubscribe(Group, Topic, SubPid).
%%------------------------------------------------------------------------------
%%--------------------------------------------------------------------
%% Publish
%%------------------------------------------------------------------------------
%%--------------------------------------------------------------------
-spec(publish(emqx_types:message()) -> emqx_types:publish_result()).
publish(Msg) when is_record(Msg, message) ->
_ = emqx_tracer:trace(publish, Msg),
Headers = Msg#message.headers,
case emqx_hooks:run_fold('message.publish', [], Msg#message{headers = Headers#{allow_publish => true}}) of
Msg1 = emqx_message:set_header(allow_publish, true,
emqx_message:clean_dup(Msg)),
case emqx_hooks:run_fold('message.publish', [], Msg1) of
#message{headers = #{allow_publish := false}} ->
?LOG(notice, "Publishing interrupted: ~s", [emqx_message:format(Msg)]),
?LOG(notice, "Stop publishing: ~s", [emqx_message:format(Msg1)]),
[];
#message{topic = Topic} = Msg1 ->
route(aggre(emqx_router:match_routes(Topic)), delivery(Msg1))
#message{topic = Topic} = Msg2 ->
route(aggre(emqx_router:match_routes(Topic)), delivery(Msg2))
end.
%% Called internally

View File

@ -118,6 +118,8 @@ info(Keys, Channel) when is_list(Keys) ->
[{Key, info(Key, Channel)} || Key <- Keys];
info(conninfo, #channel{conninfo = ConnInfo}) ->
ConnInfo;
info(zone, #channel{clientinfo = #{zone := Zone}}) ->
Zone;
info(clientid, #channel{clientinfo = #{clientid := ClientId}}) ->
ClientId;
info(clientinfo, #channel{clientinfo = ClientInfo}) ->
@ -147,11 +149,6 @@ stats(#channel{session = Session})->
caps(#channel{clientinfo = #{zone := Zone}}) ->
emqx_mqtt_caps:get_caps(Zone).
%% For tests
set_field(Name, Val, Channel) ->
Fields = record_info(fields, channel),
Pos = emqx_misc:index_of(Name, Fields),
setelement(Pos+1, Channel, Val).
%%--------------------------------------------------------------------
%% Init the channel
@ -324,9 +321,13 @@ handle_in(?AUTH_PACKET(), Channel) ->
handle_in({frame_error, Reason}, Channel = #channel{conn_state = idle}) ->
shutdown(Reason, Channel);
handle_in({frame_error, frame_too_large}, Channel = #channel{conn_state = connecting}) ->
shutdown(frame_too_large, ?CONNACK_PACKET(?RC_PACKET_TOO_LARGE), Channel);
handle_in({frame_error, Reason}, Channel = #channel{conn_state = connecting}) ->
shutdown(Reason, ?CONNACK_PACKET(?RC_MALFORMED_PACKET), Channel);
handle_in({frame_error, frame_too_large}, Channel = #channel{conn_state = connected}) ->
handle_out(disconnect, {?RC_PACKET_TOO_LARGE, frame_too_large}, Channel);
handle_in({frame_error, Reason}, Channel = #channel{conn_state = connected}) ->
handle_out(disconnect, {?RC_MALFORMED_PACKET, Reason}, Channel);
@ -375,7 +376,7 @@ process_publish(Packet = ?PUBLISH_PACKET(_QoS, Topic, PacketId), Channel) ->
fun check_pub_caps/2
], Packet, Channel) of
{ok, NPacket, NChannel} ->
Msg = pub_to_msg(NPacket, NChannel),
Msg = packet_to_msg(NPacket, NChannel),
do_publish(PacketId, Msg, NChannel);
{error, ReasonCode, NChannel} ->
?LOG(warning, "Cannot publish message to ~s due to ~s",
@ -383,9 +384,9 @@ process_publish(Packet = ?PUBLISH_PACKET(_QoS, Topic, PacketId), Channel) ->
handle_out(disconnect, ReasonCode, NChannel)
end.
pub_to_msg(Packet, #channel{conninfo = #{proto_ver := ProtoVer},
clientinfo = ClientInfo =
#{mountpoint := MountPoint}}) ->
packet_to_msg(Packet, #channel{conninfo = #{proto_ver := ProtoVer},
clientinfo = ClientInfo =
#{mountpoint := MountPoint}}) ->
emqx_mountpoint:mount(
MountPoint, emqx_packet:to_message(
ClientInfo, #{proto_ver => ProtoVer}, Packet)).
@ -417,8 +418,8 @@ do_publish(PacketId, Msg = #message{qos = ?QOS_2},
end.
-compile({inline, [puback_reason_code/1]}).
puback_reason_code([]) -> ?RC_NO_MATCHING_SUBSCRIBERS;
puback_reason_code(_) -> ?RC_SUCCESS.
puback_reason_code([]) -> ?RC_NO_MATCHING_SUBSCRIBERS;
puback_reason_code([_|_]) -> ?RC_SUCCESS.
%%--------------------------------------------------------------------
%% Process Subscribe
@ -1210,7 +1211,7 @@ maybe_resume_session(#channel{resuming = false}) ->
maybe_resume_session(#channel{session = Session,
resuming = true,
pendings = Pendings}) ->
{ok, Publishes, Session1} = emqx_session:redeliver(Session),
{ok, Publishes, Session1} = emqx_session:replay(Session),
case emqx_session:deliver(Pendings, Session1) of
{ok, Session2} ->
{ok, Publishes, Session2};
@ -1309,3 +1310,11 @@ sp(false) -> 0.
flag(true) -> 1;
flag(false) -> 0.
%%--------------------------------------------------------------------
%% For CT tests
%%--------------------------------------------------------------------
set_field(Name, Value, Channel) ->
Pos = emqx_misc:index_of(Name, record_info(fields, channel)),
setelement(Pos+1, Channel, Value).

View File

@ -51,7 +51,10 @@
]).
%% Internal callback
-export([wakeup_from_hib/3]).
-export([wakeup_from_hib/2]).
%% Export for CT
-export([set_field/3]).
-import(emqx_misc,
[ maybe_apply/2
@ -85,6 +88,8 @@
gc_state :: maybe(emqx_gc:gc_state()),
%% Stats Timer
stats_timer :: disabled | maybe(reference()),
%% Idle Timeout
idle_timeout :: integer(),
%% Idle Timer
idle_timer :: maybe(reference())
}).
@ -163,13 +168,13 @@ stop(Pid) ->
init(Parent, Transport, RawSocket, Options) ->
case Transport:wait(RawSocket) of
{ok, Socket} ->
do_init(Parent, Transport, Socket, Options);
run_loop(Parent, init_state(Transport, Socket, Options));
{error, Reason} ->
ok = Transport:fast_close(RawSocket),
exit_on_sock_error(Reason)
end.
do_init(Parent, Transport, Socket, Options) ->
init_state(Transport, Socket, Options) ->
{ok, Peername} = Transport:ensure_ok_or_exit(peername, [Socket]),
{ok, Sockname} = Transport:ensure_ok_or_exit(sockname, [Socket]),
Peercert = Transport:ensure_ok_or_exit(peercert, [Socket]),
@ -181,7 +186,8 @@ do_init(Parent, Transport, Socket, Options) ->
},
Zone = proplists:get_value(zone, Options),
ActiveN = proplists:get_value(active_n, Options, ?ACTIVE_N),
Limiter = emqx_limiter:init(Options),
PubLimit = emqx_zone:publish_limit(Zone),
Limiter = emqx_limiter:init([{pub_limit, PubLimit}|Options]),
FrameOpts = emqx_zone:mqtt_frame_options(Zone),
ParseState = emqx_frame:initial_parse_state(FrameOpts),
Serialize = emqx_frame:serialize_fun(),
@ -190,25 +196,31 @@ do_init(Parent, Transport, Socket, Options) ->
StatsTimer = emqx_zone:stats_timer(Zone),
IdleTimeout = emqx_zone:idle_timeout(Zone),
IdleTimer = start_timer(IdleTimeout, idle_timeout),
emqx_misc:tune_heap_size(emqx_zone:oom_policy(Zone)),
#state{transport = Transport,
socket = Socket,
peername = Peername,
sockname = Sockname,
sockstate = idle,
active_n = ActiveN,
limiter = Limiter,
parse_state = ParseState,
serialize = Serialize,
channel = Channel,
gc_state = GcState,
stats_timer = StatsTimer,
idle_timeout = IdleTimeout,
idle_timer = IdleTimer
}.
run_loop(Parent, State = #state{transport = Transport,
socket = Socket,
peername = Peername,
channel = Channel}) ->
emqx_logger:set_metadata_peername(esockd:format(Peername)),
State = #state{transport = Transport,
socket = Socket,
peername = Peername,
sockname = Sockname,
sockstate = idle,
active_n = ActiveN,
limiter = Limiter,
parse_state = ParseState,
serialize = Serialize,
channel = Channel,
gc_state = GcState,
stats_timer = StatsTimer,
idle_timer = IdleTimer
},
emqx_misc:tune_heap_size(emqx_zone:oom_policy(
emqx_channel:info(zone, Channel))),
case activate_socket(State) of
{ok, NState} ->
hibernate(Parent, NState, #{idle_timeout => IdleTimeout});
{ok, NState} -> hibernate(Parent, NState);
{error, Reason} ->
ok = Transport:fast_close(Socket),
exit_on_sock_error(Reason)
@ -226,28 +238,24 @@ exit_on_sock_error(Reason) ->
%%--------------------------------------------------------------------
%% Recv Loop
recvloop(Parent, State, Options = #{idle_timeout := IdleTimeout}) ->
recvloop(Parent, State = #state{idle_timeout = IdleTimeout}) ->
receive
{system, From, Request} ->
sys:handle_system_msg(Request, From, Parent,
?MODULE, [], {State, Options});
sys:handle_system_msg(Request, From, Parent, ?MODULE, [], State);
{'EXIT', Parent, Reason} ->
terminate(Reason, State);
Msg ->
NState = ensure_stats_timer(IdleTimeout, State),
process_msg([Msg], Parent, NState, Options)
process_msg([Msg], Parent, ensure_stats_timer(IdleTimeout, State))
after
IdleTimeout ->
NState = cancel_stats_timer(State),
hibernate(Parent, NState, Options)
hibernate(Parent, cancel_stats_timer(State))
end.
hibernate(Parent, State, Options) ->
proc_lib:hibernate(?MODULE, wakeup_from_hib, [Parent, State, Options]).
hibernate(Parent, State) ->
proc_lib:hibernate(?MODULE, wakeup_from_hib, [Parent, State]).
wakeup_from_hib(Parent, State, Options) ->
%% Maybe do something later here.
recvloop(Parent, State, Options).
%% Maybe do something here later.
wakeup_from_hib(Parent, State) -> recvloop(Parent, State).
%%--------------------------------------------------------------------
%% Ensure/cancel stats timer
@ -266,17 +274,16 @@ cancel_stats_timer(State) -> State.
%%--------------------------------------------------------------------
%% Process next Msg
process_msg([], Parent, State, Options) ->
recvloop(Parent, State, Options);
process_msg([], Parent, State) -> recvloop(Parent, State);
process_msg([Msg|More], Parent, State, Options) ->
process_msg([Msg|More], Parent, State) ->
case catch handle_msg(Msg, State) of
ok ->
process_msg(More, Parent, State, Options);
process_msg(More, Parent, State);
{ok, NState} ->
process_msg(More, Parent, NState, Options);
process_msg(More, Parent, NState);
{ok, Msgs, NState} ->
process_msg(append_msg(Msgs, More), Parent, NState, Options);
process_msg(append_msg(More, Msgs), Parent, NState);
{stop, Reason} ->
terminate(Reason, State);
{stop, Reason, NState} ->
@ -285,6 +292,15 @@ process_msg([Msg|More], Parent, State, Options) ->
terminate(Reason, State)
end.
-compile({inline, [append_msg/2]}).
append_msg([], Msgs) when is_list(Msgs) ->
Msgs;
append_msg([], Msg) -> [Msg];
append_msg(Q, Msgs) when is_list(Msgs) ->
lists:append(Q, Msgs);
append_msg(Q, Msg) ->
lists:append(Q, [Msg]).
%%--------------------------------------------------------------------
%% Handle a Msg
@ -343,11 +359,10 @@ handle_msg({Passive, _Sock}, State)
NState1 = check_oom(run_gc(InStats, NState)),
handle_info(activate_socket, NState1);
handle_msg(Deliver = {deliver, _Topic, _Msg}, State =
#state{active_n = ActiveN, channel = Channel}) ->
handle_msg(Deliver = {deliver, _Topic, _Msg},
State = #state{active_n = ActiveN}) ->
Delivers = [Deliver|emqx_misc:drain_deliver(ActiveN)],
Ret = emqx_channel:handle_deliver(Delivers, Channel),
handle_chan_return(Ret, State);
with_channel(handle_deliver, [Delivers], State);
%% Something sent
handle_msg({inet_reply, _Sock, ok}, State = #state{active_n = ActiveN}) ->
@ -407,18 +422,17 @@ terminate(Reason, State = #state{channel = Channel}) ->
%%--------------------------------------------------------------------
%% Sys callbacks
system_continue(Parent, _Deb, {State, Options}) ->
recvloop(Parent, State, Options).
system_continue(Parent, _Debug, State) ->
recvloop(Parent, State).
system_terminate(Reason, _Parent, _Deb, {State, _}) ->
system_terminate(Reason, _Parent, _Debug, State) ->
terminate(Reason, State).
system_code_change(Misc, _, _, _) ->
{ok, Misc}.
system_get_state({State, _Options}) ->
system_code_change(State, _Mod, _OldVsn, _Extra) ->
{ok, State}.
system_get_state(State) -> {ok, State}.
%%--------------------------------------------------------------------
%% Handle call
@ -467,9 +481,8 @@ handle_timeout(TRef, keepalive, State =
handle_info({sock_error, Reason}, State)
end;
handle_timeout(TRef, Msg, State = #state{channel = Channel}) ->
Ret = emqx_channel:handle_timeout(TRef, Msg, Channel),
handle_chan_return(Ret, State).
handle_timeout(TRef, Msg, State) ->
with_channel(handle_timeout, [TRef, Msg], State).
%%--------------------------------------------------------------------
%% Parse incoming data
@ -491,7 +504,7 @@ parse_incoming(Data, Packets, State = #state{parse_state = ParseState}) ->
parse_incoming(Rest, [Packet|Packets], NState)
catch
error:Reason:Stk ->
?LOG(error, "~nParse failed for ~p~nStacktrace: ~p~nFrame data:~p",
?LOG(error, "~nParse failed for ~p~n~p~nFrame data:~p",
[Reason, Stk, Data]),
{[{frame_error, Reason}|Packets], State}
end.
@ -505,30 +518,31 @@ next_incoming_msgs(Packets) ->
%%--------------------------------------------------------------------
%% Handle incoming packet
handle_incoming(Packet, State = #state{channel = Channel})
when is_record(Packet, mqtt_packet) ->
handle_incoming(Packet, State) when is_record(Packet, mqtt_packet) ->
ok = inc_incoming_stats(Packet),
?LOG(debug, "RECV ~s", [emqx_packet:format(Packet)]),
handle_chan_return(emqx_channel:handle_in(Packet, Channel), State);
with_channel(handle_in, [Packet], State);
handle_incoming(FrameError, State = #state{channel = Channel}) ->
handle_chan_return(emqx_channel:handle_in(FrameError, Channel), State).
handle_incoming(FrameError, State) ->
with_channel(handle_in, [FrameError], State).
%%--------------------------------------------------------------------
%% Handle channel return
%% With Channel
handle_chan_return(ok, State) ->
{ok, State};
handle_chan_return({ok, NChannel}, State) ->
{ok, State#state{channel = NChannel}};
handle_chan_return({ok, Replies, NChannel}, State) ->
{ok, next_msgs(Replies), State#state{channel = NChannel}};
handle_chan_return({shutdown, Reason, NChannel}, State) ->
shutdown(Reason, State#state{channel = NChannel});
handle_chan_return({shutdown, Reason, OutPacket, NChannel}, State) ->
NState = State#state{channel = NChannel},
ok = handle_outgoing(OutPacket, NState),
shutdown(Reason, NState).
with_channel(Fun, Args, State = #state{channel = Channel}) ->
case erlang:apply(emqx_channel, Fun, Args ++ [Channel]) of
ok -> {ok, State};
{ok, NChannel} ->
{ok, State#state{channel = NChannel}};
{ok, Replies, NChannel} ->
{ok, next_msgs(Replies), State#state{channel = NChannel}};
{shutdown, Reason, NChannel} ->
shutdown(Reason, State#state{channel = NChannel});
{shutdown, Reason, Packet, NChannel} ->
NState = State#state{channel = NChannel},
ok = handle_outgoing(Packet, NState),
shutdown(Reason, NState)
end.
%%--------------------------------------------------------------------
%% Handle outgoing packets
@ -585,9 +599,8 @@ handle_info({sock_error, Reason}, State) ->
?LOG(debug, "Socket error: ~p", [Reason]),
handle_info({sock_closed, Reason}, close_socket(State));
handle_info(Info, State = #state{channel = Channel}) ->
Ret = emqx_channel:handle_info(Info, Channel),
handle_chan_return(Ret, State).
handle_info(Info, State) ->
with_channel(handle_info, [Info], State).
%%--------------------------------------------------------------------
%% Ensure rate limit
@ -613,12 +626,12 @@ run_gc(Stats, State = #state{gc_state = GcSt}) ->
case ?ENABLED(GcSt) andalso emqx_gc:run(Stats, GcSt) of
false -> State;
{IsGC, GcSt1} ->
IsGC andalso emqx_metrics:inc('channel.gc.cnt'),
IsGC andalso emqx_metrics:inc('channel.gc'),
State#state{gc_state = GcSt1}
end.
check_oom(State = #state{channel = Channel}) ->
#{zone := Zone} = emqx_channel:info(clientinfo, Channel),
Zone = emqx_channel:info(zone, Channel),
OomPolicy = emqx_zone:oom_policy(Zone),
case ?ENABLED(OomPolicy) andalso emqx_misc:check_oom(OomPolicy) of
Shutdown = {shutdown, _Reason} ->
@ -679,18 +692,13 @@ inc_outgoing_stats(Packet = ?PACKET(Type)) ->
%%--------------------------------------------------------------------
%% Helper functions
-compile({inline, [append_msg/2]}).
append_msg(Msgs, Q) when is_list(Msgs) ->
lists:append(Msgs, Q);
append_msg(Msg, Q) -> [Msg|Q].
-compile({inline, [next_msgs/1]}).
next_msgs(Packet) when is_record(Packet, mqtt_packet) ->
{outgoing, Packet};
next_msgs(Action) when is_tuple(Action) ->
Action;
next_msgs(Actions) when is_list(Actions) ->
Actions.
next_msgs(Event) when is_tuple(Event) ->
Event;
next_msgs(More) when is_list(More) ->
More.
-compile({inline, [shutdown/2, shutdown/3]}).
shutdown(Reason, State) ->
@ -706,3 +714,11 @@ stop(Reason, State) ->
stop(Reason, Reply, State) ->
{stop, Reason, Reply, State}.
%%--------------------------------------------------------------------
%% For CT tests
%%--------------------------------------------------------------------
set_field(Name, Value, State) ->
Pos = emqx_misc:index_of(Name, record_info(fields, state)),
setelement(Pos+1, State, Value).

View File

@ -67,14 +67,7 @@ next(NPid, Seq) ->
bin({Ts, NPid, Seq}) ->
<<Ts:64, NPid:48, Seq:16>>.
ts() ->
case erlang:function_exported(erlang, system_time, 1) of
true -> %% R18
erlang:system_time(micro_seconds);
false ->
{MegaSeconds, Seconds, MicroSeconds} = os:timestamp(),
(MegaSeconds * 1000000 + Seconds) * 1000000 + MicroSeconds
end.
ts() -> erlang:system_time(micro_seconds).
%% Copied from https://github.com/okeuday/uuid.git.
npid() ->

View File

@ -37,8 +37,7 @@
-spec(init(proplists:proplist()) -> maybe(limiter())).
init(Options) ->
Zone = proplists:get_value(zone, Options),
Pl = emqx_zone:publish_limit(Zone),
Pl = proplists:get_value(pub_limit, Options),
Rl = proplists:get_value(rate_limit, Options),
case ?ENABLED(Pl) or ?ENABLED(Rl) of
true -> #limiter{pub_limit = init_limit(Pl),

View File

@ -16,6 +16,8 @@
-module(emqx_message).
-compile(inline).
-include("emqx.hrl").
-include("emqx_mqtt.hrl").
-include("types.hrl").
@ -36,7 +38,8 @@
]).
%% Flags
-export([ get_flag/2
-export([ clean_dup/1
, get_flag/2
, get_flag/3
, get_flags/1
, set_flag/2
@ -71,25 +74,25 @@
make(Topic, Payload) ->
make(undefined, Topic, Payload).
-spec(make(atom() | emqx_types:clientid(),
-spec(make(emqx_types:clientid(),
emqx_topic:topic(),
emqx_types:payload()) -> emqx_types:message()).
make(From, Topic, Payload) ->
make(From, ?QOS_0, Topic, Payload).
-spec(make(atom() | emqx_types:clientid(),
-spec(make(emqx_types:clientid(),
emqx_types:qos(),
emqx_topic:topic(),
emqx_types:payload()) -> emqx_types:message()).
make(From, QoS, Topic, Payload) when ?QOS_0 =< QoS, QoS =< ?QOS_2 ->
Now = erlang:system_time(millisecond),
#message{id = emqx_guid:gen(),
qos = QoS,
from = From,
flags = #{dup => false},
headers = #{},
topic = Topic,
payload = Payload,
timestamp = os:timestamp()}.
timestamp = Now
}.
-spec(id(emqx_types:message()) -> maybe(binary())).
id(#message{id = Id}) -> Id.
@ -106,9 +109,14 @@ topic(#message{topic = Topic}) -> Topic.
-spec(payload(emqx_types:message()) -> emqx_types:payload()).
payload(#message{payload = Payload}) -> Payload.
-spec(timestamp(emqx_types:message()) -> erlang:timestamp()).
-spec(timestamp(emqx_types:message()) -> integer()).
timestamp(#message{timestamp = TS}) -> TS.
-spec(clean_dup(emqx_types:message()) -> emqx_types:message()).
clean_dup(Msg = #message{flags = Flags = #{dup := true}}) ->
Msg#message{flags = Flags#{dup => false}};
clean_dup(Msg) -> Msg.
-spec(set_flags(map(), emqx_types:message()) -> emqx_types:message()).
set_flags(Flags, Msg = #message{flags = undefined}) when is_map(Flags) ->
Msg#message{flags = Flags};
@ -116,8 +124,13 @@ set_flags(New, Msg = #message{flags = Old}) when is_map(New) ->
Msg#message{flags = maps:merge(Old, New)}.
-spec(get_flag(flag(), emqx_types:message()) -> boolean()).
get_flag(_Flag, #message{flags = undefined}) ->
false;
get_flag(Flag, Msg) ->
get_flag(Flag, Msg, false).
get_flag(_Flag, #message{flags = undefined}, Default) ->
Default;
get_flag(Flag, #message{flags = Flags}, Default) ->
maps:get(Flag, Flags, Default).
@ -140,25 +153,27 @@ set_flag(Flag, Val, Msg = #message{flags = Flags}) when is_atom(Flag) ->
-spec(unset_flag(flag(), emqx_types:message()) -> emqx_types:message()).
unset_flag(Flag, Msg = #message{flags = Flags}) ->
case maps:is_key(Flag, Flags) of
true ->
Msg#message{flags = maps:remove(Flag, Flags)};
true -> Msg#message{flags = maps:remove(Flag, Flags)};
false -> Msg
end.
-spec(set_headers(undefined | map(), emqx_types:message()) -> emqx_types:message()).
-spec(set_headers(map(), emqx_types:message()) -> emqx_types:message()).
set_headers(Headers, Msg = #message{headers = undefined}) when is_map(Headers) ->
Msg#message{headers = Headers};
set_headers(New, Msg = #message{headers = Old}) when is_map(New) ->
Msg#message{headers = maps:merge(Old, New)}.
-spec(get_headers(emqx_types:message()) -> map()).
get_headers(Msg) ->
Msg#message.headers.
-spec(get_headers(emqx_types:message()) -> maybe(map())).
get_headers(Msg) -> Msg#message.headers.
-spec(get_header(term(), emqx_types:message()) -> term()).
get_header(_Hdr, #message{headers = undefined}) ->
undefined;
get_header(Hdr, Msg) ->
get_header(Hdr, Msg, undefined).
-spec(get_header(term(), emqx_types:message(), Default :: term()) -> term()).
-spec(get_header(term(), emqx_types:message(), term()) -> term()).
get_header(_Hdr, #message{headers = undefined}, Default) ->
Default;
get_header(Hdr, #message{headers = Headers}, Default) ->
maps:get(Hdr, Headers, Default).
@ -169,10 +184,11 @@ set_header(Hdr, Val, Msg = #message{headers = Headers}) ->
Msg#message{headers = maps:put(Hdr, Val, Headers)}.
-spec(remove_header(term(), emqx_types:message()) -> emqx_types:message()).
remove_header(_Hdr, Msg = #message{headers = undefined}) ->
Msg;
remove_header(Hdr, Msg = #message{headers = Headers}) ->
case maps:is_key(Hdr, Headers) of
true ->
Msg#message{headers = maps:remove(Hdr, Headers)};
true -> Msg#message{headers = maps:remove(Hdr, Headers)};
false -> Msg
end.
@ -180,15 +196,15 @@ remove_header(Hdr, Msg = #message{headers = Headers}) ->
is_expired(#message{headers = #{'Message-Expiry-Interval' := Interval},
timestamp = CreatedAt}) ->
elapsed(CreatedAt) > timer:seconds(Interval);
is_expired(_Msg) ->
false.
is_expired(_Msg) -> false.
-spec(update_expiry(emqx_types:message()) -> emqx_types:message()).
update_expiry(Msg = #message{headers = #{'Message-Expiry-Interval' := Interval},
timestamp = CreatedAt}) ->
case elapsed(CreatedAt) of
Elapsed when Elapsed > 0 ->
set_header('Message-Expiry-Interval', max(1, Interval - (Elapsed div 1000)), Msg);
Interval1 = max(1, Interval - (Elapsed div 1000)),
set_header('Message-Expiry-Interval', Interval1, Msg);
_ -> Msg
end;
update_expiry(Msg) -> Msg.
@ -196,30 +212,29 @@ update_expiry(Msg) -> Msg.
%% @doc Message to PUBLISH Packet.
-spec(to_packet(emqx_types:packet_id(), emqx_types:message())
-> emqx_types:packet()).
to_packet(PacketId, #message{qos = QoS, flags = Flags, headers = Headers,
topic = Topic, payload = Payload}) ->
Flags1 = if Flags =:= undefined -> #{};
true -> Flags
end,
Dup = maps:get(dup, Flags1, false),
Retain = maps:get(retain, Flags1, false),
Publish = #mqtt_packet_publish{topic_name = Topic,
packet_id = PacketId,
properties = publish_props(Headers)},
#mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH,
dup = Dup,
qos = QoS,
retain = Retain},
variable = Publish, payload = Payload}.
to_packet(PacketId, Msg = #message{qos = QoS, headers = Headers,
topic = Topic, payload = Payload}) ->
#mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH,
dup = get_flag(dup, Msg),
qos = QoS,
retain = get_flag(retain, Msg)
},
variable = #mqtt_packet_publish{topic_name = Topic,
packet_id = PacketId,
properties = props(Headers)
},
payload = Payload
}.
publish_props(Headers) ->
maps:with(['Payload-Format-Indicator',
'Response-Topic',
'Correlation-Data',
'User-Property',
'Subscription-Identifier',
'Content-Type',
'Message-Expiry-Interval'], Headers).
props(undefined) -> undefined;
props(Headers) -> maps:with(['Payload-Format-Indicator',
'Response-Topic',
'Correlation-Data',
'User-Property',
'Subscription-Identifier',
'Content-Type',
'Message-Expiry-Interval'
], Headers).
%% @doc Message to map
-spec(to_map(emqx_types:message()) -> map()).
@ -240,7 +255,8 @@ to_map(#message{
headers => Headers,
topic => Topic,
payload => Payload,
timestamp => Timestamp}.
timestamp => Timestamp
}.
%% @doc Message to tuple list
-spec(to_list(emqx_types:message()) -> map()).
@ -249,7 +265,7 @@ to_list(Msg) ->
%% MilliSeconds
elapsed(Since) ->
max(0, timer:now_diff(os:timestamp(), Since) div 1000).
max(0, erlang:system_time(millisecond) - Since).
format(#message{id = Id, qos = QoS, topic = Topic, from = From, flags = Flags, headers = Headers}) ->
io_lib:format("Message(Id=~s, QoS=~w, Topic=~s, From=~p, Flags=~s, Headers=~s)",

View File

@ -137,7 +137,7 @@
]).
-define(CHAN_METRICS, [
{counter, 'channel.gc.cnt'}
{counter, 'channel.gc'}
]).
-define(MQTT_METRICS, [
@ -377,7 +377,7 @@ handle_call({create, Type, Name}, _From, State = #state{next_idx = ?MAX_SIZE}) -
handle_call({create, Type, Name}, _From, State = #state{next_idx = NextIdx}) ->
case ets:lookup(?TAB, Name) of
[#metric{idx = Idx}] ->
?LOG(warning, "~s already exists.", [Name]),
?LOG(info, "~s already exists.", [Name]),
{reply, {ok, Idx}, State};
[] ->
Metric = #metric{name = Name, type = Type, idx = NextIdx},
@ -459,7 +459,7 @@ reserved_idx('messages.dropped') -> 49;
reserved_idx('messages.expired') -> 50;
reserved_idx('messages.forward') -> 51;
reserved_idx('auth.mqtt.anonymous') -> 52;
reserved_idx('channel.gc.cnt') -> 53;
reserved_idx('channel.gc') -> 53;
reserved_idx('packets.pubrec.inuse') -> 54;
reserved_idx('packets.pubcomp.inuse') -> 55;
reserved_idx(_) -> undefined.

View File

@ -72,7 +72,7 @@ pipeline([], Input, State) ->
{ok, Input, State};
pipeline([Fun|More], Input, State) ->
try apply_fun(Fun, Input, State) of
case apply_fun(Fun, Input, State) of
ok -> pipeline(More, Input, State);
{ok, NState} ->
pipeline(More, Input, NState);
@ -82,11 +82,6 @@ pipeline([Fun|More], Input, State) ->
{error, Reason, State};
{error, Reason, NState} ->
{error, Reason, NState}
catch
Error:Reason:Stacktrace ->
?LOG(error, "pipeline ~p failed: ~p,\nstacktrace: ~0p",
[{Fun, Input, State}, {Error, Reason}, Stacktrace]),
{error, Reason, State}
end.
-compile({inline, [apply_fun/3]}).

View File

@ -80,7 +80,7 @@
-export([ takeover/1
, resume/2
, redeliver/1
, replay/1
]).
-export([expire/2]).
@ -340,8 +340,7 @@ return_with(Msg, {ok, Publishes, Session}) ->
pubrec(PacketId, Session = #session{inflight = Inflight}) ->
case emqx_inflight:lookup(PacketId, Inflight) of
{value, {Msg, _Ts}} when is_record(Msg, message) ->
Inflight1 = emqx_inflight:update(
PacketId, {pubrel, os:timestamp()}, Inflight),
Inflight1 = emqx_inflight:update(PacketId, with_ts(pubrel), Inflight),
{ok, Msg, Session#session{inflight = Inflight1}};
{value, {pubrel, _Ts}} ->
{error, ?RC_PACKET_IDENTIFIER_IN_USE};
@ -418,6 +417,10 @@ acc_msg(Msg, Msgs) ->
-spec(deliver(list(emqx_types:deliver()), session())
-> {ok, session()} | {ok, replies(), session()}).
deliver([Deliver], Session) -> %% Optimize
Enrich = enrich_fun(Session),
deliver_msg(Enrich(Deliver), Session);
deliver(Delivers, Session) ->
Msgs = lists:map(enrich_fun(Session), Delivers),
deliver(Msgs, [], Session).
@ -425,12 +428,19 @@ deliver(Delivers, Session) ->
deliver([], Publishes, Session) ->
{ok, lists:reverse(Publishes), Session};
deliver([Msg = #message{qos = ?QOS_0}|More], Acc, Session) ->
Publish = {undefined, maybe_ack(Msg)},
deliver(More, [Publish|Acc], Session);
deliver([Msg|More], Acc, Session) ->
case deliver_msg(Msg, Session) of
{ok, Session1} ->
deliver(More, Acc, Session1);
{ok, [Publish], Session1} ->
deliver(More, [Publish|Acc], Session1)
end.
deliver([Msg = #message{qos = QoS}|More], Acc, Session =
#session{next_pkt_id = PacketId, inflight = Inflight})
deliver_msg(Msg = #message{qos = ?QOS_0}, Session) ->
{ok, [{undefined, maybe_ack(Msg)}], Session};
deliver_msg(Msg = #message{qos = QoS}, Session =
#session{next_pkt_id = PacketId, inflight = Inflight})
when QoS =:= ?QOS_1 orelse QoS =:= ?QOS_2 ->
case emqx_inflight:is_full(Inflight) of
true ->
@ -438,15 +448,19 @@ deliver([Msg = #message{qos = QoS}|More], Acc, Session =
true -> Session;
false -> enqueue(Msg, Session)
end,
deliver(More, Acc, Session1);
{ok, Session1};
false ->
Publish = {PacketId, maybe_ack(Msg)},
Session1 = await(PacketId, Msg, Session),
deliver(More, [Publish|Acc], next_pkt_id(Session1))
{ok, [Publish], next_pkt_id(Session1)}
end.
-spec(enqueue(list(emqx_types:deliver())|emqx_types:message(), session())
-> session()).
-spec(enqueue(list(emqx_types:deliver())|emqx_types:message(),
session()) -> session()).
enqueue([Deliver], Session) -> %% Optimize
Enrich = enrich_fun(Session),
enqueue(Enrich(Deliver), Session);
enqueue(Delivers, Session) when is_list(Delivers) ->
Msgs = lists:map(enrich_fun(Session), Delivers),
lists:foldl(fun enqueue/2, Session, Msgs);
@ -510,7 +524,7 @@ enrich_subopts([{subid, SubId}|Opts], Msg, Session) ->
%%--------------------------------------------------------------------
await(PacketId, Msg, Session = #session{inflight = Inflight}) ->
Inflight1 = emqx_inflight:insert(PacketId, {Msg, os:timestamp()}, Inflight),
Inflight1 = emqx_inflight:insert(PacketId, with_ts(Msg), Inflight),
Session#session{inflight = Inflight1}.
%%--------------------------------------------------------------------
@ -521,9 +535,8 @@ await(PacketId, Msg, Session = #session{inflight = Inflight}) ->
retry(Session = #session{inflight = Inflight}) ->
case emqx_inflight:is_empty(Inflight) of
true -> {ok, Session};
false ->
retry_delivery(emqx_inflight:to_list(sort_fun(), Inflight),
[], os:timestamp(), Session)
false -> retry_delivery(emqx_inflight:to_list(sort_fun(), Inflight),
[], erlang:system_time(millisecond), Session)
end.
retry_delivery([], Acc, _Now, Session = #session{retry_interval = Interval}) ->
@ -561,53 +574,48 @@ retry_delivery(PacketId, pubrel, Now, Acc, Inflight) ->
expire(awaiting_rel, Session = #session{awaiting_rel = AwaitingRel}) ->
case maps:size(AwaitingRel) of
0 -> {ok, Session};
_ -> expire_awaiting_rel(os:timestamp(), Session)
_ -> expire_awaiting_rel(erlang:system_time(millisecond), Session)
end.
expire_awaiting_rel(Now, Session = #session{awaiting_rel = AwaitingRel,
await_rel_timeout = Timeout}) ->
NotExpired = fun(_PacketId, Ts) -> age(Now, Ts) < Timeout end,
case maps:filter(NotExpired, AwaitingRel) of
[] -> {ok, Session};
AwaitingRel1 ->
{ok, Timeout, Session#session{awaiting_rel = AwaitingRel1}}
AwaitingRel1 = maps:filter(NotExpired, AwaitingRel),
NSession = Session#session{awaiting_rel = AwaitingRel1},
case maps:size(AwaitingRel1) of
0 -> {ok, NSession};
_ -> {ok, Timeout, NSession}
end.
%%--------------------------------------------------------------------
%% Takeover, Resume and Redeliver
%% Takeover, Resume and Replay
%%--------------------------------------------------------------------
-spec(takeover(session()) -> ok).
takeover(#session{subscriptions = Subs}) ->
lists:foreach(fun({TopicFilter, _SubOpts}) ->
ok = emqx_broker:unsubscribe(TopicFilter)
end, maps:to_list(Subs)).
lists:foreach(fun emqx_broker:unsubscribe/1, maps:keys(Subs)).
-spec(resume(emqx_types:clientid(), session()) -> ok).
resume(ClientId, #session{subscriptions = Subs}) ->
%% 1. Subscribe again.
lists:foreach(fun({TopicFilter, SubOpts}) ->
ok = emqx_broker:subscribe(TopicFilter, ClientId, SubOpts)
ok = emqx_broker:subscribe(TopicFilter, ClientId, SubOpts)
end, maps:to_list(Subs)).
%% 2. Run hooks.
%% ok = emqx_hooks:run('session.resumed', [#{clientid => ClientId}, attrs(Session)]),
%% TODO: 3. Redeliver: Replay delivery and Dequeue pending messages
%%Session.
-spec(redeliver(session()) -> {ok, replies(), session()}).
redeliver(Session = #session{inflight = Inflight}) ->
Pubs = lists:map(fun to_pub/1, emqx_inflight:to_list(Inflight)),
-spec(replay(session()) -> {ok, replies(), session()}).
replay(Session = #session{inflight = Inflight}) ->
Pubs = replay(Inflight),
case dequeue(Session) of
{ok, NSession} -> {ok, Pubs, NSession};
{ok, More, NSession} ->
{ok, lists:append(Pubs, More), NSession}
end.
end;
to_pub({PacketId, {pubrel, _Ts}}) ->
{pubrel, PacketId};
to_pub({PacketId, {Msg, _Ts}}) ->
{PacketId, emqx_message:set_flag(dup, true, Msg)}.
replay(Inflight) ->
lists:map(fun({PacketId, {pubrel, _Ts}}) ->
{pubrel, PacketId};
({PacketId, {Msg, _Ts}}) ->
{PacketId, emqx_message:set_flag(dup, true, Msg)}
end, emqx_inflight:to_list(Inflight)).
%%--------------------------------------------------------------------
%% Next Packet Id
@ -623,10 +631,10 @@ next_pkt_id(Session = #session{next_pkt_id = Id}) ->
%% Helper functions
%%--------------------------------------------------------------------
-compile({inline, [sort_fun/0, batch_n/1, age/2]}).
-compile({inline, [sort_fun/0, batch_n/1, with_ts/1, age/2]}).
sort_fun() ->
fun({_, {_, Ts1}}, {_, {_, Ts2}}) -> Ts1 < Ts2 end.
fun({_, {_, Ts1}}, {_, {_, Ts2}}) -> Ts1 =< Ts2 end.
batch_n(Inflight) ->
case emqx_inflight:max_size(Inflight) of
@ -634,14 +642,16 @@ batch_n(Inflight) ->
Sz -> Sz - emqx_inflight:size(Inflight)
end.
age(Now, Ts) -> timer:now_diff(Now, Ts) div 1000.
with_ts(Msg) ->
{Msg, erlang:system_time(millisecond)}.
age(Now, Ts) -> Now - Ts.
%%--------------------------------------------------------------------
%% For CT tests
%%--------------------------------------------------------------------
set_field(Name, Val, Channel) ->
Fields = record_info(fields, session),
Pos = emqx_misc:index_of(Name, Fields),
setelement(Pos+1, Channel, Val).
set_field(Name, Value, Session) ->
Pos = emqx_misc:index_of(Name, record_info(fields, session)),
setelement(Pos+1, Session, Value).

View File

@ -58,7 +58,7 @@
-record(update, {name, countdown, interval, func}).
-record(state, {
timer :: reference(),
timer :: reference(),
updates :: [#update{}],
tick_ms :: timeout()
}).
@ -66,48 +66,53 @@
-type(stats() :: list({atom(), non_neg_integer()})).
%% Connection stats
-define(CONNECTION_STATS, [
'connections.count', % current connections
'connections.max' % maximum connections connected
]).
-define(CONNECTION_STATS,
['connections.count', %% Count of Concurrent Connections
'connections.max' %% Maximum Number of Concurrent Connections
]).
%% Channel stats
-define(CHANNEL_STATS,
['channels.count', %% Count of Concurrent Channels
'channels.max' %% Maximum Number of Concurrent Channels
]).
%% Session stats
-define(SESSION_STATS, [
'sessions.count',
'sessions.max',
'sessions.persistent.count',
'sessions.persistent.max'
]).
-define(SESSION_STATS,
['sessions.count', %% Count of Concurrent Sessions
'sessions.max' %% Maximum Number of Concurrent Sessions
]).
%% Subscribers, Subscriptions stats
-define(PUBSUB_STATS, [
'topics.count',
'topics.max',
'suboptions.count',
'suboptions.max',
'subscribers.count',
'subscribers.max',
'subscriptions.count',
'subscriptions.max',
'subscriptions.shared.count',
'subscriptions.shared.max'
]).
%% PubSub stats
-define(PUBSUB_STATS,
['topics.count',
'topics.max',
'suboptions.count',
'suboptions.max',
'subscribers.count',
'subscribers.max',
'subscriptions.count',
'subscriptions.max',
'subscriptions.shared.count',
'subscriptions.shared.max'
]).
-define(ROUTE_STATS, [
'routes.count',
'routes.max'
]).
%% Route stats
-define(ROUTE_STATS,
['routes.count',
'routes.max'
]).
%% Retained stats
-define(RETAINED_STATS, [
'retained.count',
'retained.max'
]).
-define(RETAINED_STATS,
['retained.count',
'retained.max'
]).
-define(TAB, ?MODULE).
-define(SERVER, ?MODULE).
-type opts() :: #{tick_ms := timeout()}.
-type(opts() :: #{tick_ms := timeout()}).
%% @doc Start stats server
-spec(start_link() -> startlink_ret()).
@ -122,7 +127,7 @@ start_link(Opts) ->
stop() ->
gen_server:call(?SERVER, stop, infinity).
%% @doc Generate stats fun
%% @doc Generate stats fun.
-spec(statsfun(Stat :: atom()) -> fun()).
statsfun(Stat) ->
fun(Val) -> setstat(Stat, Val) end.
@ -131,7 +136,7 @@ statsfun(Stat) ->
statsfun(Stat, MaxStat) ->
fun(Val) -> setstat(Stat, MaxStat, Val) end.
%% @doc Get all statistics
%% @doc Get all statistics.
-spec(getstats() -> stats()).
getstats() ->
case ets:info(?TAB, name) of
@ -139,7 +144,7 @@ getstats() ->
_ -> ets:tab2list(?TAB)
end.
%% @doc Get stats by name
%% @doc Get stats by name.
-spec(getstat(atom()) -> maybe(non_neg_integer())).
getstat(Name) ->
case ets:lookup(?TAB, Name) of
@ -173,8 +178,7 @@ cancel_update(Name) ->
rec(Name, Secs, UpFun) ->
#update{name = Name, countdown = Secs, interval = Secs, func = UpFun}.
cast(Msg) ->
gen_server:cast(?SERVER, Msg).
cast(Msg) -> gen_server:cast(?SERVER, Msg).
%%--------------------------------------------------------------------
%% gen_server callbacks
@ -182,8 +186,13 @@ cast(Msg) ->
init(#{tick_ms := TickMs}) ->
ok = emqx_tables:new(?TAB, [public, set, {write_concurrency, true}]),
Stats = lists:append([?CONNECTION_STATS, ?SESSION_STATS, ?PUBSUB_STATS,
?ROUTE_STATS, ?RETAINED_STATS]),
Stats = lists:append([?CONNECTION_STATS,
?CHANNEL_STATS,
?SESSION_STATS,
?PUBSUB_STATS,
?ROUTE_STATS,
?RETAINED_STATS
]),
true = ets:insert(?TAB, [{Name, 0} || Name <- Stats]),
{ok, start_timer(#state{updates = [], tick_ms = TickMs}), hibernate}.
@ -211,16 +220,17 @@ handle_cast({setstat, Stat, MaxStat, Val}, State) ->
handle_cast({update_interval, Update = #update{name = Name}},
State = #state{updates = Updates}) ->
case lists:keyfind(Name, #update.name, Updates) of
#update{} ->
?LOG(warning, "Duplicated update: ~s", [Name]),
{noreply, State};
false ->
{noreply, State#state{updates = [Update | Updates]}}
end;
NState = case lists:keyfind(Name, #update.name, Updates) of
#update{} ->
?LOG(warning, "Duplicated update: ~s", [Name]),
State;
false -> State#state{updates = [Update|Updates]}
end,
{noreply, NState};
handle_cast({cancel_update, Name}, State = #state{updates = Updates}) ->
{noreply, State#state{updates = lists:keydelete(Name, #update.name, Updates)}};
Updates1 = lists:keydelete(Name, #update.name, Updates),
{noreply, State#state{updates = Updates1}};
handle_cast(Msg, State) ->
?LOG(error, "Unexpected cast: ~p", [Msg]),
@ -233,7 +243,7 @@ handle_info({timeout, TRef, tick}, State = #state{timer = TRef, updates = Update
try UpFun()
catch
_:Error ->
?LOG(error, "update ~s failed: ~p", [Name, Error])
?LOG(error, "Update ~s failed: ~p", [Name, Error])
end,
[Update#update{countdown = I} | Acc];
(Update = #update{countdown = C}, Acc) ->
@ -259,10 +269,9 @@ safe_update_element(Key, Val) ->
try ets:update_element(?TAB, Key, {2, Val}) of
false ->
ets:insert_new(?TAB, {Key, Val});
true ->
true
true -> true
catch
error:badarg ->
?LOG(warning, "Update ~p to ~p failed", [Key, Val])
?LOG(warning, "Failed to update ~p to ~p", [Key, Val])
end.

View File

@ -176,8 +176,8 @@
-type(deliver() :: {deliver, topic(), message()}).
-type(delivery() :: #delivery{}).
-type(deliver_result() :: ok | {error, term()}).
-type(publish_result() :: [ {node(), topic(), deliver_result()}
| {share, topic(), deliver_result()}]).
-type(publish_result() :: [{node(), topic(), deliver_result()} |
{share, topic(), deliver_result()}]).
-type(route() :: #route{}).
-type(sub_group() :: tuple() | binary()).
-type(route_entry() :: {topic(), node()} | {topic, sub_group()}).

View File

@ -18,7 +18,6 @@
-export([ schedulers/0
, scheduler_usage/1
, microsecs/0
, system_info_keys/0
, get_system_info/0
, get_system_info/1
@ -171,10 +170,6 @@
schedulers() ->
erlang:system_info(schedulers).
microsecs() ->
{Mega, Sec, Micro} = os:timestamp(),
(Mega * 1000000 + Sec) * 1000000 + Micro.
loads() ->
[{load1, ftos(avg1()/256)},
{load5, ftos(avg5()/256)},

View File

@ -45,13 +45,16 @@
, terminate/3
]).
%% Export for CT
-export([set_field/3]).
-import(emqx_misc,
[ maybe_apply/2
, start_timer/2
]).
-record(state, {
%% Peername of the ws connection.
%% Peername of the ws connection
peername :: emqx_types:peername(),
%% Sockname of the ws connection
sockname :: emqx_types:peername(),
@ -65,26 +68,26 @@
limit_timer :: maybe(reference()),
%% Parse State
parse_state :: emqx_frame:parse_state(),
%% Serialize function
%% Serialize Fun
serialize :: emqx_frame:serialize_fun(),
%% Channel
channel :: emqx_channel:channel(),
%% GC State
gc_state :: maybe(emqx_gc:gc_state()),
%% Out Pending Packets
pendings :: list(emqx_types:packet()),
%% Postponed Packets|Cmds|Events
postponed :: list(emqx_types:packet()|ws_cmd()|tuple()),
%% Stats Timer
stats_timer :: disabled | maybe(reference()),
%% Idle Timeout
idle_timeout :: timeout(),
%% Idle Timer
idle_timer :: reference(),
%% The stop reason
stop_reason :: term()
idle_timer :: reference()
}).
-type(state() :: #state{}).
-type(ws_cmd() :: {active, boolean()}|close).
-define(ACTIVE_N, 100).
-define(INFO_KEYS, [socktype, peername, sockname, sockstate, active_n]).
-define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt]).
@ -93,7 +96,7 @@
-define(ENABLED(X), (X =/= undefined)).
%%--------------------------------------------------------------------
%% API
%% Info, Stats
%%--------------------------------------------------------------------
-spec(info(pid()|state()) -> emqx_types:infos()).
@ -120,8 +123,16 @@ info(limiter, #state{limiter = Limiter}) ->
maybe_apply(fun emqx_limiter:info/1, Limiter);
info(channel, #state{channel = Channel}) ->
emqx_channel:info(Channel);
info(stop_reason, #state{stop_reason = Reason}) ->
Reason.
info(gc_state, #state{gc_state = GcSt}) ->
maybe_apply(fun emqx_gc:info/1, GcSt);
info(postponed, #state{postponed = Postponed}) ->
Postponed;
info(stats_timer, #state{stats_timer = TRef}) ->
TRef;
info(idle_timeout, #state{idle_timeout = Timeout}) ->
Timeout;
info(idle_timer, #state{idle_timer = TRef}) ->
TRef.
-spec(stats(pid()|state()) -> emqx_types:stats()).
stats(WsPid) when is_pid(WsPid) ->
@ -201,8 +212,9 @@ websocket_init([Req, Opts]) ->
conn_mod => ?MODULE
},
Zone = proplists:get_value(zone, Opts),
PubLimit = emqx_zone:publish_limit(Zone),
Limiter = emqx_limiter:init([{pub_limit, PubLimit}|Opts]),
ActiveN = proplists:get_value(active_n, Opts, ?ACTIVE_N),
Limiter = emqx_limiter:init(Opts),
FrameOpts = emqx_zone:mqtt_frame_options(Zone),
ParseState = emqx_frame:initial_parse_state(FrameOpts),
Serialize = emqx_frame:serialize_fun(),
@ -223,7 +235,7 @@ websocket_init([Req, Opts]) ->
serialize = Serialize,
channel = Channel,
gc_state = GcState,
pendings = [],
postponed = [],
stats_timer = StatsTimer,
idle_timeout = IdleTimeout,
idle_timer = IdleTimer
@ -235,145 +247,144 @@ websocket_handle({binary, Data}, State) when is_list(Data) ->
websocket_handle({binary, Data}, State) ->
?LOG(debug, "RECV ~p", [Data]),
ok = inc_recv_stats(1, iolist_size(Data)),
parse_incoming(Data, ensure_stats_timer(State));
NState = ensure_stats_timer(State),
return(parse_incoming(Data, NState));
%% Pings should be replied with pongs, cowboy does it automatically
%% Pongs can be safely ignored. Clause here simply prevents crash.
websocket_handle(Frame, State) when Frame =:= ping; Frame =:= pong ->
{ok, State};
return(State);
websocket_handle({FrameType, _}, State) when FrameType =:= ping;
FrameType =:= pong ->
{ok, State};
websocket_handle({Frame, _}, State) when Frame =:= ping; Frame =:= pong ->
return(State);
websocket_handle({FrameType, _}, State) ->
?LOG(error, "Unexpected frame - ~p", [FrameType]),
stop({shutdown, unexpected_ws_frame}, State).
websocket_handle({Frame, _}, State) ->
%% TODO: should not close the ws connection
?LOG(error, "Unexpected frame - ~p", [Frame]),
shutdown(unexpected_ws_frame, State).
websocket_info({call, From, Req}, State) ->
handle_call(From, Req, State);
websocket_info({cast, Msg}, State = #state{channel = Channel}) ->
handle_chan_return(emqx_channel:handle_info(Msg, Channel), State);
websocket_info({cast, rate_limit}, State) ->
Stats = #{cnt => emqx_pd:reset_counter(incoming_pubs),
oct => emqx_pd:reset_counter(incoming_bytes)
},
NState = postpone({check_gc, Stats}, State),
return(ensure_rate_limit(Stats, NState));
websocket_info({incoming, Packet = ?CONNECT_PACKET(ConnPkt)},
State = #state{idle_timer = IdleTimer}) ->
ok = emqx_misc:cancel_timer(IdleTimer),
websocket_info({cast, Msg}, State) ->
handle_info(Msg, State);
websocket_info({incoming, Packet = ?CONNECT_PACKET(ConnPkt)}, State) ->
Serialize = emqx_frame:serialize_fun(ConnPkt),
NState = State#state{serialize = Serialize,
idle_timer = undefined
},
handle_incoming(Packet, NState);
NState = State#state{serialize = Serialize},
handle_incoming(Packet, cancel_idle_timer(NState));
websocket_info({incoming, ?PACKET(?PINGREQ)}, State) ->
reply(?PACKET(?PINGRESP), State);
return(enqueue(?PACKET(?PINGRESP), State));
websocket_info({incoming, Packet}, State) ->
handle_incoming(Packet, State);
websocket_info(rate_limit, State) ->
InStats = #{cnt => emqx_pd:reset_counter(incoming_pubs),
oct => emqx_pd:reset_counter(incoming_bytes)
},
erlang:send(self(), {check_gc, InStats}),
ensure_rate_limit(InStats, State);
websocket_info({outgoing, Packets}, State) ->
return(enqueue(Packets, State));
websocket_info({check_gc, Stats}, State) ->
{ok, check_oom(run_gc(Stats, State))};
return(check_oom(run_gc(Stats, State)));
websocket_info(Deliver = {deliver, _Topic, _Msg},
State = #state{active_n = ActiveN, channel = Channel}) ->
State = #state{active_n = ActiveN}) ->
Delivers = [Deliver|emqx_misc:drain_deliver(ActiveN)],
Ret = emqx_channel:handle_deliver(Delivers, Channel),
handle_chan_return(Ret, State);
with_channel(handle_deliver, [Delivers], State);
websocket_info({timeout, TRef, limit_timeout}, State = #state{limit_timer = TRef}) ->
websocket_info({timeout, TRef, limit_timeout},
State = #state{limit_timer = TRef}) ->
NState = State#state{sockstate = running,
limit_timer = undefined
},
{reply, [{active, true}], NState};
return(enqueue({active, true}, NState));
websocket_info({timeout, TRef, Msg}, State) when is_reference(TRef) ->
handle_timeout(TRef, Msg, State);
websocket_info(Close = {close, _Reason}, State) ->
handle_info(Close, State);
websocket_info({shutdown, Reason}, State) ->
stop({shutdown, Reason}, State);
shutdown(Reason, State);
websocket_info({stop, Reason}, State) ->
stop(Reason, State);
shutdown(Reason, State);
websocket_info(Info, State) ->
handle_info(Info, State).
websocket_close(Reason, State) ->
?LOG(debug, "WebSocket closed due to ~p~n", [Reason]),
?LOG(debug, "Websocket closed due to ~p~n", [Reason]),
handle_info({sock_closed, Reason}, State).
terminate(SockError, _Req, #state{channel = Channel,
stop_reason = Reason}) ->
?LOG(debug, "Terminated for ~p, sockerror: ~p", [Reason, SockError]),
terminate(Reason, _Req, #state{channel = Channel}) ->
?LOG(debug, "Terminated due to ~p", [Reason]),
emqx_channel:terminate(Reason, Channel).
%%--------------------------------------------------------------------
%% Handle call
%%--------------------------------------------------------------------
handle_call(From, info, State) ->
gen_server:reply(From, info(State)),
{ok, State};
return(State);
handle_call(From, stats, State) ->
gen_server:reply(From, stats(State)),
{ok, State};
return(State);
handle_call(From, Req, State = #state{channel = Channel}) ->
case emqx_channel:handle_call(Req, Channel) of
{reply, Reply, NChannel} ->
_ = gen_server:reply(From, Reply),
{ok, State#state{channel = NChannel}};
{stop, Reason, Reply, NChannel} ->
_ = gen_server:reply(From, Reply),
stop(Reason, State#state{channel = NChannel});
{stop, Reason, Reply, OutPacket, NChannel} ->
gen_server:reply(From, Reply),
return(State#state{channel = NChannel});
{shutdown, Reason, Reply, NChannel} ->
gen_server:reply(From, Reply),
shutdown(Reason, State#state{channel = NChannel});
{shutdown, Reason, Reply, Packet, NChannel} ->
gen_server:reply(From, Reply),
NState = State#state{channel = NChannel},
stop(Reason, enqueue(OutPacket, NState))
shutdown(Reason, enqueue(Packet, NState))
end.
%%--------------------------------------------------------------------
%% Handle Info
%%--------------------------------------------------------------------
handle_info({connack, ConnAck}, State) ->
reply(enqueue(ConnAck, State));
return(enqueue(ConnAck, State));
handle_info({close, Reason}, State) ->
stop({shutdown, Reason}, State);
?LOG(debug, "Force to close the socket due to ~p", [Reason]),
return(enqueue(close, State));
handle_info({event, connected}, State = #state{channel = Channel}) ->
ClientId = emqx_channel:info(clientid, Channel),
emqx_cm:register_channel(ClientId, info(State), stats(State)),
reply(State);
ok = emqx_cm:register_channel(ClientId, info(State), stats(State)),
return(State);
handle_info({event, disconnected}, State = #state{channel = Channel}) ->
ClientId = emqx_channel:info(clientid, Channel),
emqx_cm:set_chan_info(ClientId, info(State)),
emqx_cm:connection_closed(ClientId),
reply(State);
return(State);
handle_info({event, _Other}, State = #state{channel = Channel}) ->
ClientId = emqx_channel:info(clientid, Channel),
emqx_cm:set_chan_info(ClientId, info(State)),
emqx_cm:set_chan_stats(ClientId, stats(State)),
reply(State);
return(State);
handle_info(Info, State = #state{channel = Channel}) ->
Ret = emqx_channel:handle_info(Info, Channel),
handle_chan_return(Ret, State).
handle_info(Info, State) ->
with_channel(handle_info, [Info], State).
%%--------------------------------------------------------------------
%% Handle timeout
%%--------------------------------------------------------------------
handle_timeout(TRef, idle_timeout, State = #state{idle_timer = TRef}) ->
shutdown(idle_timeout, State);
@ -382,33 +393,24 @@ handle_timeout(TRef, keepalive, State) when is_reference(TRef) ->
RecvOct = emqx_pd:get_counter(recv_oct),
handle_timeout(TRef, {keepalive, RecvOct}, State);
handle_timeout(TRef, emit_stats, State =
#state{channel = Channel, stats_timer = TRef}) ->
handle_timeout(TRef, emit_stats, State = #state{channel = Channel,
stats_timer = TRef}) ->
ClientId = emqx_channel:info(clientid, Channel),
emqx_cm:set_chan_stats(ClientId, stats(State)),
reply(State#state{stats_timer = undefined});
return(State#state{stats_timer = undefined});
handle_timeout(TRef, TMsg, State = #state{channel = Channel}) ->
Ret = emqx_channel:handle_timeout(TRef, TMsg, Channel),
handle_chan_return(Ret, State).
%%--------------------------------------------------------------------
%% Ensure stats timer
-compile({inline, [ensure_stats_timer/1]}).
ensure_stats_timer(State = #state{idle_timeout = Timeout,
stats_timer = undefined}) ->
State#state{stats_timer = start_timer(Timeout, emit_stats)};
ensure_stats_timer(State) -> State.
handle_timeout(TRef, TMsg, State) ->
with_channel(handle_timeout, [TRef, TMsg], State).
%%--------------------------------------------------------------------
%% Ensure rate limit
%%--------------------------------------------------------------------
ensure_rate_limit(Stats, State = #state{limiter = Limiter}) ->
case ?ENABLED(Limiter) andalso emqx_limiter:check(Stats, Limiter) of
false -> {ok, State};
false -> State;
{ok, Limiter1} ->
{ok, State#state{limiter = Limiter1}};
State#state{limiter = Limiter1};
{pause, Time, Limiter1} ->
?LOG(debug, "Pause ~pms due to rate limit", [Time]),
TRef = start_timer(Time, limit_timeout),
@ -416,102 +418,108 @@ ensure_rate_limit(Stats, State = #state{limiter = Limiter}) ->
limiter = Limiter1,
limit_timer = TRef
},
{reply, [{active, false}], NState}
enqueue({active, false}, NState)
end.
%%--------------------------------------------------------------------
%% Run GC and Check OOM
%% Run GC, Check OOM
%%--------------------------------------------------------------------
run_gc(Stats, State = #state{gc_state = GcSt}) ->
case ?ENABLED(GcSt) andalso emqx_gc:run(Stats, GcSt) of
false -> State;
{IsGC, GcSt1} ->
IsGC andalso emqx_metrics:inc('channel.gc.cnt'),
State#state{gc_state = GcSt1}
IsGC andalso emqx_metrics:inc('channel.gc'),
State#state{gc_state = GcSt1};
false -> State
end.
check_oom(State = #state{channel = Channel}) ->
#{zone := Zone} = emqx_channel:info(clientinfo, Channel),
OomPolicy = emqx_zone:oom_policy(Zone),
OomPolicy = emqx_zone:oom_policy(emqx_channel:info(zone, Channel)),
case ?ENABLED(OomPolicy) andalso emqx_misc:check_oom(OomPolicy) of
Shutdown = {shutdown, _Reason} ->
erlang:send(self(), Shutdown);
_Other -> ok
end,
State.
postpone(Shutdown, State);
_Other -> State
end.
%%--------------------------------------------------------------------
%% Parse incoming data
%%--------------------------------------------------------------------
parse_incoming(<<>>, State) ->
{ok, State};
State;
parse_incoming(Data, State = #state{parse_state = ParseState}) ->
try emqx_frame:parse(Data, ParseState) of
{more, NParseState} ->
{ok, State#state{parse_state = NParseState}};
State#state{parse_state = NParseState};
{ok, Packet, Rest, NParseState} ->
erlang:send(self(), {incoming, Packet}),
parse_incoming(Rest, State#state{parse_state = NParseState})
NState = State#state{parse_state = NParseState},
parse_incoming(Rest, postpone({incoming, Packet}, NState))
catch
error:Reason:Stk ->
?LOG(error, "~nParse failed for ~p~nStacktrace: ~p~nFrame data: ~p",
?LOG(error, "~nParse failed for ~p~n~p~nFrame data: ~p",
[Reason, Stk, Data]),
self() ! {incoming, {frame_error, Reason}},
{ok, State}
FrameError = {frame_error, Reason},
postpone({incoming, FrameError}, State)
end.
%%--------------------------------------------------------------------
%% Handle incoming packet
%%--------------------------------------------------------------------
handle_incoming(Packet, State = #state{active_n = ActiveN, channel = Channel})
handle_incoming(Packet, State = #state{active_n = ActiveN})
when is_record(Packet, mqtt_packet) ->
?LOG(debug, "RECV ~s", [emqx_packet:format(Packet)]),
ok = inc_incoming_stats(Packet),
(emqx_pd:get_counter(incoming_pubs) > ActiveN)
andalso erlang:send(self(), rate_limit),
Ret = emqx_channel:handle_in(Packet, Channel),
handle_chan_return(Ret, State);
NState = case emqx_pd:get_counter(incoming_pubs) > ActiveN of
true -> postpone({cast, rate_limit}, State);
false -> State
end,
with_channel(handle_in, [Packet], NState);
handle_incoming(FrameError, State = #state{channel = Channel}) ->
handle_chan_return(emqx_channel:handle_in(FrameError, Channel), State).
handle_incoming(FrameError, State) ->
with_channel(handle_in, [FrameError], State).
%%--------------------------------------------------------------------
%% Handle channel return
%% With Channel
%%--------------------------------------------------------------------
handle_chan_return(ok, State) ->
reply(State);
handle_chan_return({ok, NChannel}, State) ->
reply(State#state{channel= NChannel});
handle_chan_return({ok, Replies, NChannel}, State) ->
reply(Replies, State#state{channel= NChannel});
handle_chan_return({shutdown, Reason, NChannel}, State) ->
stop(Reason, State#state{channel = NChannel});
handle_chan_return({shutdown, Reason, OutPacket, NChannel}, State) ->
NState = State#state{channel = NChannel},
stop(Reason, enqueue(OutPacket, NState)).
with_channel(Fun, Args, State = #state{channel = Channel}) ->
case erlang:apply(emqx_channel, Fun, Args ++ [Channel]) of
ok -> return(State);
{ok, NChannel} ->
return(State#state{channel = NChannel});
{ok, Replies, NChannel} ->
return(postpone(Replies, State#state{channel= NChannel}));
{shutdown, Reason, NChannel} ->
shutdown(Reason, State#state{channel = NChannel});
{shutdown, Reason, Packet, NChannel} ->
NState = State#state{channel = NChannel},
shutdown(Reason, postpone(Packet, NState))
end.
%%--------------------------------------------------------------------
%% Handle outgoing packets
%%--------------------------------------------------------------------
handle_outgoing(Packets, State = #state{active_n = ActiveN}) ->
IoData = lists:map(serialize_and_inc_stats_fun(State), Packets),
Oct = iolist_size(IoData),
ok = inc_sent_stats(length(Packets), Oct),
case emqx_pd:get_counter(outgoing_pubs) > ActiveN of
true ->
OutStats = #{cnt => emqx_pd:reset_counter(outgoing_pubs),
oct => emqx_pd:reset_counter(outgoing_bytes)
},
erlang:send(self(), {check_gc, OutStats});
false -> ok
end,
{{binary, IoData}, ensure_stats_timer(State)}.
NState = case emqx_pd:get_counter(outgoing_pubs) > ActiveN of
true ->
Stats = #{cnt => emqx_pd:reset_counter(outgoing_pubs),
oct => emqx_pd:reset_counter(outgoing_bytes)
},
postpone({check_gc, Stats}, State);
false -> State
end,
{{binary, IoData}, ensure_stats_timer(NState)}.
serialize_and_inc_stats_fun(#state{serialize = Serialize}) ->
fun(Packet) ->
case Serialize(Packet) of
<<>> -> ?LOG(warning, "~s is discarded due to the frame is too large!",
<<>> -> ?LOG(warning, "~s is discarded due to the frame is too large.",
[emqx_packet:format(Packet)]),
<<>>;
Data -> ?LOG(debug, "SEND ~s", [emqx_packet:format(Packet)]),
@ -522,6 +530,7 @@ serialize_and_inc_stats_fun(#state{serialize = Serialize}) ->
%%--------------------------------------------------------------------
%% Inc incoming/outgoing stats
%%--------------------------------------------------------------------
-compile({inline,
[ inc_recv_stats/2
@ -561,46 +570,84 @@ inc_sent_stats(Cnt, Oct) ->
emqx_metrics:inc('bytes.sent', Oct).
%%--------------------------------------------------------------------
%% Reply or Stop
%% Helper functions
%%--------------------------------------------------------------------
reply(Packet, State) when is_record(Packet, mqtt_packet) ->
reply(enqueue(Packet, State));
reply({outgoing, Packets}, State) ->
reply(enqueue(Packets, State));
reply(Other, State) when is_tuple(Other) ->
self() ! Other,
reply(State);
-compile({inline, [cancel_idle_timer/1, ensure_stats_timer/1]}).
reply([], State) ->
reply(State);
reply([Packet|More], State) when is_record(Packet, mqtt_packet) ->
reply(More, enqueue(Packet, State));
reply([{outgoing, Packets}|More], State) ->
reply(More, enqueue(Packets, State));
reply([Other|More], State) ->
self() ! Other,
reply(More, State).
%%--------------------------------------------------------------------
%% Cancel idle timer
-compile({inline, [reply/1, enqueue/2]}).
cancel_idle_timer(State = #state{idle_timer = IdleTimer}) ->
ok = emqx_misc:cancel_timer(IdleTimer),
State#state{idle_timer = undefined}.
reply(State = #state{pendings = []}) ->
%%--------------------------------------------------------------------
%% Ensure stats timer
ensure_stats_timer(State = #state{idle_timeout = Timeout,
stats_timer = undefined}) ->
State#state{stats_timer = start_timer(Timeout, emit_stats)};
ensure_stats_timer(State) -> State.
-compile({inline, [postpone/2, enqueue/2, return/1, shutdown/2]}).
%%--------------------------------------------------------------------
%% Postpone the packet, cmd or event
postpone(Packet, State) when is_record(Packet, mqtt_packet) ->
enqueue(Packet, State);
postpone(Event, State) when is_tuple(Event) ->
enqueue(Event, State);
postpone(More, State) when is_list(More) ->
lists:foldl(fun postpone/2, State, More).
enqueue([Packet], State = #state{postponed = Postponed}) ->
State#state{postponed = [Packet|Postponed]};
enqueue(Packets, State = #state{postponed = Postponed})
when is_list(Packets) ->
State#state{postponed = lists:reverse(Packets) ++ Postponed};
enqueue(Other, State = #state{postponed = Postponed}) ->
State#state{postponed = [Other|Postponed]}.
shutdown(Reason, State = #state{postponed = Postponed}) ->
return(State#state{postponed = [{shutdown, Reason}|Postponed]}).
return(State = #state{postponed = []}) ->
{ok, State};
reply(State = #state{pendings = Pendings}) ->
{Reply, NState} = handle_outgoing(Pendings, State),
{reply, Reply, NState#state{pendings = []}}.
return(State = #state{postponed = Postponed}) ->
{Packets, Cmds, Events} = classify(Postponed, [], [], []),
ok = lists:foreach(fun trigger/1, Events),
State1 = State#state{postponed = []},
case {Packets, Cmds} of
{[], []} -> {ok, State1};
{[], Cmds} -> {Cmds, State1};
{Packets, Cmds} ->
{Frame, State2} = handle_outgoing(Packets, State1),
{[Frame|Cmds], State2}
end.
enqueue(Packet, State) when is_record(Packet, mqtt_packet) ->
enqueue([Packet], State);
enqueue(Packets, State = #state{pendings = Pendings}) ->
State#state{pendings = lists:append(Pendings, Packets)}.
classify([], Packets, Cmds, Events) ->
{Packets, Cmds, Events};
classify([Packet|More], Packets, Cmds, Events)
when is_record(Packet, mqtt_packet) ->
classify(More, [Packet|Packets], Cmds, Events);
classify([Cmd = {active, _}|More], Packets, Cmds, Events) ->
classify(More, Packets, [Cmd|Cmds], Events);
classify([Cmd = {shutdown, _Reason}|More], Packets, Cmds, Events) ->
classify(More, Packets, [Cmd|Cmds], Events);
classify([Cmd = close|More], Packets, Cmds, Events) ->
classify(More, Packets, [Cmd|Cmds], Events);
classify([Event|More], Packets, Cmds, Events) ->
classify(More, Packets, Cmds, [Event|Events]).
shutdown(Reason, State) ->
stop({shutdown, Reason}, State).
trigger(Event) -> erlang:send(self(), Event).
stop(Reason, State = #state{pendings = []}) ->
{stop, State#state{stop_reason = Reason}};
stop(Reason, State = #state{pendings = Pendings}) ->
{Reply, State1} = handle_outgoing(Pendings, State),
State2 = State1#state{pendings = [], stop_reason = Reason},
{reply, [Reply, close], State2}.
%%--------------------------------------------------------------------
%% For CT tests
%%--------------------------------------------------------------------
set_field(Name, Value, State) ->
Pos = emqx_misc:index_of(Name, record_info(fields, state)),
setelement(Pos+1, State, Value).

View File

@ -89,6 +89,7 @@ t_subscribers(_) ->
t_subscriptions(_) ->
emqx_broker:subscribe(<<"topic">>, <<"clientid">>, #{qos => 1}),
ok = timer:sleep(100),
?assertEqual(#{qos => 1, subid => <<"clientid">>},
proplists:get_value(<<"topic">>, emqx_broker:subscriptions(self()))),
?assertEqual(#{qos => 1, subid => <<"clientid">>},

View File

@ -65,3 +65,11 @@ t_shards_num(_) ->
t_get_sub_shard(_) ->
?assertEqual(0, emqx_broker_helper:get_sub_shard(self(), <<"topic">>)).
t_terminate(_) ->
?assertEqual(ok, gen_server:stop(emqx_broker_helper)).
t_uncovered_func(_) ->
gen_server:call(emqx_broker_helper, test),
gen_server:cast(emqx_broker_helper, test),
emqx_broker_helper ! test.

View File

@ -23,20 +23,6 @@
-include("emqx_mqtt.hrl").
-include_lib("eunit/include/eunit.hrl").
-define(DEFAULT_CONNINFO,
#{peername => {{127,0,0,1}, 3456},
sockname => {{127,0,0,1}, 1883},
conn_mod => emqx_connection,
proto_name => <<"MQTT">>,
proto_ver => ?MQTT_PROTO_V5,
clean_start => true,
keepalive => 30,
clientid => <<"clientid">>,
username => <<"username">>,
conn_props => #{},
receive_maximum => 100,
expiry_interval => 0
}).
all() -> emqx_ct:all(?MODULE).
@ -45,40 +31,40 @@ all() -> emqx_ct:all(?MODULE).
%%--------------------------------------------------------------------
init_per_suite(Config) ->
Config.
end_per_suite(_Config) ->
ok.
init_per_testcase(_TestCase, Config) ->
%% CM Meck
ok = meck:new(emqx_cm, [passthrough, no_history]),
ok = meck:new(emqx_cm, [passthrough, no_history, no_link]),
%% Access Control Meck
ok = meck:new(emqx_access_control, [passthrough, no_history]),
ok = meck:new(emqx_access_control, [passthrough, no_history, no_link]),
ok = meck:expect(emqx_access_control, authenticate,
fun(_) -> {ok, #{auth_result => success}} end),
ok = meck:expect(emqx_access_control, check_acl, fun(_, _, _) -> allow end),
%% Broker Meck
ok = meck:new(emqx_broker, [passthrough, no_history]),
ok = meck:new(emqx_broker, [passthrough, no_history, no_link]),
%% Hooks Meck
ok = meck:new(emqx_hooks, [passthrough, no_history]),
ok = meck:new(emqx_hooks, [passthrough, no_history, no_link]),
ok = meck:expect(emqx_hooks, run, fun(_Hook, _Args) -> ok end),
ok = meck:expect(emqx_hooks, run_fold, fun(_Hook, _Args, Acc) -> Acc end),
%% Session Meck
ok = meck:new(emqx_session, [passthrough, no_history]),
ok = meck:new(emqx_session, [passthrough, no_history, no_link]),
%% Metrics
ok = meck:new(emqx_metrics, [passthrough, no_history]),
ok = meck:new(emqx_metrics, [passthrough, no_history, no_link]),
ok = meck:expect(emqx_metrics, inc, fun(_) -> ok end),
ok = meck:expect(emqx_metrics, inc, fun(_, _) -> ok end),
Config.
end_per_testcase(_TestCase, Config) ->
end_per_suite(_Config) ->
ok = meck:unload(emqx_access_control),
ok = meck:unload(emqx_metrics),
ok = meck:unload(emqx_session),
ok = meck:unload(emqx_broker),
ok = meck:unload(emqx_hooks),
ok = meck:unload(emqx_cm),
ok.
init_per_testcase(_TestCase, Config) ->
Config.
end_per_testcase(_TestCase, Config) ->
Config.
%%--------------------------------------------------------------------
@ -92,17 +78,15 @@ t_chan_info(_) ->
?assertEqual(clientinfo(), ClientInfo).
t_chan_caps(_) ->
Caps = emqx_mqtt_caps:default(),
?assertEqual(Caps#{max_packet_size => 1048576},
emqx_channel:caps(channel())).
%%--------------------------------------------------------------------
%% Test cases for channel init
%%--------------------------------------------------------------------
%% TODO:
t_chan_init(_) ->
_Channel = channel().
#{max_clientid_len := 65535,
max_qos_allowed := 2,
max_topic_alias := 65535,
max_topic_levels := 0,
retain_available := true,
shared_subscription := true,
subscription_identifiers := true,
wildcard_subscription := true
} = emqx_channel:caps(channel()).
%%--------------------------------------------------------------------
%% Test cases for channel handle_in
@ -114,8 +98,8 @@ t_handle_in_connect_packet_sucess(_) ->
{ok, #{session => session(), present => false}}
end),
IdleChannel = channel(#{conn_state => idle}),
{ok, [{event, connected}, {connack, ?CONNACK_PACKET(?RC_SUCCESS, 0, _)}], Channel}
= emqx_channel:handle_in(?CONNECT_PACKET(connpkt()), IdleChannel),
{ok, [{event, connected}, {connack, ?CONNACK_PACKET(?RC_SUCCESS, 0, _)}], Channel} =
emqx_channel:handle_in(?CONNECT_PACKET(connpkt()), IdleChannel),
ClientInfo = emqx_channel:info(clientinfo, Channel),
?assertMatch(#{clientid := <<"clientid">>,
username := <<"username">>
@ -125,32 +109,47 @@ t_handle_in_connect_packet_sucess(_) ->
t_handle_in_unexpected_connect_packet(_) ->
Channel = emqx_channel:set_field(conn_state, connected, channel()),
Packet = ?DISCONNECT_PACKET(?RC_PROTOCOL_ERROR),
{ok, [{outgoing, Packet}, {close, protocol_error}], Channel}
= emqx_channel:handle_in(?CONNECT_PACKET(connpkt()), Channel).
{ok, [{outgoing, Packet}, {close, protocol_error}], Channel} =
emqx_channel:handle_in(?CONNECT_PACKET(connpkt()), Channel).
t_handle_in_qos0_publish(_) ->
ok = meck:expect(emqx_broker, publish, fun(_) -> ok end),
ok = meck:expect(emqx_broker, publish, fun(_) -> [] end),
Channel = channel(#{conn_state => connected}),
Publish = ?PUBLISH_PACKET(?QOS_0, <<"topic">>, undefined, <<"payload">>),
{ok, _NChannel} = emqx_channel:handle_in(Publish, Channel).
% ?assertEqual(#{publish_in => 1}, emqx_channel:info(pub_stats, NChannel)).
t_handle_in_qos1_publish(_) ->
ok = meck:expect(emqx_broker, publish, fun(_) -> ok end),
Channel = channel(#{conn_state => connected}),
ok = meck:expect(emqx_broker, publish, fun(_) -> [] end),
Publish = ?PUBLISH_PACKET(?QOS_1, <<"topic">>, 1, <<"payload">>),
{ok, ?PUBACK_PACKET(1, RC), _NChannel} = emqx_channel:handle_in(Publish, Channel),
?assert((RC == ?RC_SUCCESS) orelse (RC == ?RC_NO_MATCHING_SUBSCRIBERS)).
% ?assertEqual(#{publish_in => 1, puback_out => 1}, emqx_channel:info(pub_stats, NChannel)).
{ok, ?PUBACK_PACKET(1, ?RC_NO_MATCHING_SUBSCRIBERS), _Channel} =
emqx_channel:handle_in(Publish, channel(#{conn_state => connected})).
t_handle_in_qos2_publish(_) ->
ok = meck:expect(emqx_session, publish, fun(_, _Msg, Session) -> {ok, [], Session} end),
ok = meck:expect(emqx_session, info, fun(await_rel_timeout, _Session) -> 300 end),
Channel = channel(#{conn_state => connected}),
Publish = ?PUBLISH_PACKET(?QOS_2, <<"topic">>, 1, <<"payload">>),
{ok, ?PUBREC_PACKET(1, RC), _NChannel} = emqx_channel:handle_in(Publish, Channel),
?assert((RC == ?RC_SUCCESS) orelse (RC == ?RC_NO_MATCHING_SUBSCRIBERS)).
% ?assertEqual(#{publish_in => 1, pubrec_out => 1}, emqx_channel:info(pub_stats, NChannel)).
ok = meck:expect(emqx_broker, publish, fun(_) -> [{node(), <<"topic">>, 1}] end),
Channel = channel(#{conn_state => connected, session => session()}),
Publish1 = ?PUBLISH_PACKET(?QOS_2, <<"topic">>, 1, <<"payload">>),
{ok, ?PUBREC_PACKET(1, ?RC_SUCCESS), Channel1} =
emqx_channel:handle_in(Publish1, Channel),
ok = meck:expect(emqx_broker, publish, fun(_) -> [] end),
Publish2 = ?PUBLISH_PACKET(?QOS_2, <<"topic">>, 2, <<"payload">>),
{ok, ?PUBREC_PACKET(2, ?RC_NO_MATCHING_SUBSCRIBERS), Channel2} =
emqx_channel:handle_in(Publish2, Channel1),
?assertEqual(2, proplists:get_value(awaiting_rel_cnt, emqx_channel:stats(Channel2))).
t_handle_in_qos2_publish_with_error_return(_) ->
ok = meck:expect(emqx_metrics, inc, fun(_) -> ok end),
ok = meck:expect(emqx_broker, publish, fun(_) -> [] end),
Session = session(#{max_awaiting_rel => 2, awaiting_rel => #{1 => 1}}),
Channel = channel(#{conn_state => connected, session => Session}),
Publish1 = ?PUBLISH_PACKET(?QOS_2, <<"topic">>, 1, <<"payload">>),
{ok, ?PUBREC_PACKET(1, ?RC_PACKET_IDENTIFIER_IN_USE), Channel} =
emqx_channel:handle_in(Publish1, Channel),
Publish2 = ?PUBLISH_PACKET(?QOS_2, <<"topic">>, 2, <<"payload">>),
{ok, ?PUBREC_PACKET(2, ?RC_NO_MATCHING_SUBSCRIBERS), Channel1} =
emqx_channel:handle_in(Publish2, Channel),
Publish3 = ?PUBLISH_PACKET(?QOS_2, <<"topic">>, 3, <<"payload">>),
{ok, ?PUBREC_PACKET(3, ?RC_RECEIVE_MAXIMUM_EXCEEDED), Channel1} =
emqx_channel:handle_in(Publish3, Channel1).
t_handle_in_puback_ok(_) ->
Msg = emqx_message:make(<<"t">>, <<"payload">>),
@ -180,46 +179,38 @@ t_handle_in_pubrec_ok(_) ->
Msg = emqx_message:make(test,?QOS_2, <<"t">>, <<"payload">>),
ok = meck:expect(emqx_session, pubrec, fun(_, Session) -> {ok, Msg, Session} end),
Channel = channel(#{conn_state => connected}),
{ok, ?PUBREL_PACKET(1, ?RC_SUCCESS), _Channel1}
= emqx_channel:handle_in(?PUBREC_PACKET(1, ?RC_SUCCESS), Channel).
% ?assertEqual(#{pubrec_in => 1, pubrel_out => 1},
% emqx_channel:info(pub_stats, Channel1)).
{ok, ?PUBREL_PACKET(1, ?RC_SUCCESS), _Channel1} =
emqx_channel:handle_in(?PUBREC_PACKET(1, ?RC_SUCCESS), Channel).
t_handle_in_pubrec_id_in_use(_) ->
ok = meck:expect(emqx_session, pubrec,
fun(_, _Session) ->
{error, ?RC_PACKET_IDENTIFIER_IN_USE}
end),
{ok, ?PUBREL_PACKET(1, ?RC_PACKET_IDENTIFIER_IN_USE), _Channel}
= emqx_channel:handle_in(?PUBREC_PACKET(1, ?RC_SUCCESS), channel()).
% ?assertEqual(#{pubrec_in => 1, pubrel_out => 1},
% emqx_channel:info(pub_stats, Channel)).
{ok, ?PUBREL_PACKET(1, ?RC_PACKET_IDENTIFIER_IN_USE), _Channel} =
emqx_channel:handle_in(?PUBREC_PACKET(1, ?RC_SUCCESS), channel()).
t_handle_in_pubrec_id_not_found(_) ->
ok = meck:expect(emqx_session, pubrec,
fun(_, _Session) ->
{error, ?RC_PACKET_IDENTIFIER_NOT_FOUND}
end),
{ok, ?PUBREL_PACKET(1, ?RC_PACKET_IDENTIFIER_NOT_FOUND), _Channel}
= emqx_channel:handle_in(?PUBREC_PACKET(1, ?RC_SUCCESS), channel()).
% ?assertEqual(#{pubrec_in => 1, pubrel_out => 1},
% emqx_channel:info(pub_stats, Channel)).
{ok, ?PUBREL_PACKET(1, ?RC_PACKET_IDENTIFIER_NOT_FOUND), _Channel} =
emqx_channel:handle_in(?PUBREC_PACKET(1, ?RC_SUCCESS), channel()).
t_handle_in_pubrel_ok(_) ->
ok = meck:expect(emqx_session, pubrel, fun(_, Session) -> {ok, Session} end),
Channel = channel(#{conn_state => connected}),
{ok, ?PUBCOMP_PACKET(1, ?RC_SUCCESS), _Channel1}
= emqx_channel:handle_in(?PUBREL_PACKET(1, ?RC_SUCCESS), Channel).
% ?assertEqual(#{pubrel_in => 1, pubcomp_out => 1},
% emqx_channel:info(pub_stats, Channel1)).
{ok, ?PUBCOMP_PACKET(1, ?RC_SUCCESS), _Channel1} =
emqx_channel:handle_in(?PUBREL_PACKET(1, ?RC_SUCCESS), Channel).
t_handle_in_pubrel_not_found_error(_) ->
ok = meck:expect(emqx_session, pubrel,
fun(_PacketId, _Session) ->
{error, ?RC_PACKET_IDENTIFIER_NOT_FOUND}
end),
{ok, ?PUBCOMP_PACKET(1, ?RC_PACKET_IDENTIFIER_NOT_FOUND), _Channel}
= emqx_channel:handle_in(?PUBREL_PACKET(1, ?RC_SUCCESS), channel()).
{ok, ?PUBCOMP_PACKET(1, ?RC_PACKET_IDENTIFIER_NOT_FOUND), _Channel} =
emqx_channel:handle_in(?PUBREL_PACKET(1, ?RC_SUCCESS), channel()).
t_handle_in_pubcomp_ok(_) ->
ok = meck:expect(emqx_session, pubcomp, fun(_, Session) -> {ok, Session} end),
@ -233,7 +224,6 @@ t_handle_in_pubcomp_not_found_error(_) ->
end),
Channel = channel(#{conn_state => connected}),
{ok, _Channel1} = emqx_channel:handle_in(?PUBCOMP_PACKET(1, ?RC_SUCCESS), Channel).
% ?assertEqual(#{pubcomp_in => 1}, emqx_channel:info(pub_stats, Channel1)).
t_handle_in_subscribe(_) ->
ok = meck:expect(emqx_session, subscribe,
@ -250,12 +240,12 @@ t_handle_in_unsubscribe(_) ->
{ok, Session}
end),
Channel = channel(#{conn_state => connected}),
{ok, [{outgoing, ?UNSUBACK_PACKET(1)}, {event, updated}], _Chan}
= emqx_channel:handle_in(?UNSUBSCRIBE_PACKET(1, #{}, [<<"+">>]), Channel).
{ok, [{outgoing, ?UNSUBACK_PACKET(1)}, {event, updated}], _Chan} =
emqx_channel:handle_in(?UNSUBSCRIBE_PACKET(1, #{}, [<<"+">>]), Channel).
t_handle_in_pingreq(_) ->
{ok, ?PACKET(?PINGRESP), _Channel}
= emqx_channel:handle_in(?PACKET(?PINGREQ), channel()).
{ok, ?PACKET(?PINGRESP), _Channel} =
emqx_channel:handle_in(?PACKET(?PINGREQ), channel()).
t_handle_in_disconnect(_) ->
Packet = ?DISCONNECT_PACKET(?RC_SUCCESS),
@ -266,38 +256,37 @@ t_handle_in_disconnect(_) ->
t_handle_in_auth(_) ->
Channel = channel(#{conn_state => connected}),
Packet = ?DISCONNECT_PACKET(?RC_IMPLEMENTATION_SPECIFIC_ERROR),
{ok, [{outgoing, Packet},
{close, implementation_specific_error}], Channel}
= emqx_channel:handle_in(?AUTH_PACKET(), Channel).
{ok, [{outgoing, Packet}, {close, implementation_specific_error}], Channel} =
emqx_channel:handle_in(?AUTH_PACKET(), Channel).
t_handle_in_frame_error(_) ->
IdleChannel = channel(#{conn_state => idle}),
{shutdown, frame_too_large, _}
= emqx_channel:handle_in({frame_error, frame_too_large}, IdleChannel),
{shutdown, frame_too_large, _Chan} =
emqx_channel:handle_in({frame_error, frame_too_large}, IdleChannel),
ConnectingChan = channel(#{conn_state => connecting}),
ConnackPacket = ?CONNACK_PACKET(?RC_MALFORMED_PACKET),
{shutdown, frame_too_large, ConnackPacket, _}
= emqx_channel:handle_in({frame_error, frame_too_large}, ConnectingChan),
DisconnectPacket = ?DISCONNECT_PACKET(?RC_MALFORMED_PACKET),
ConnackPacket = ?CONNACK_PACKET(?RC_PACKET_TOO_LARGE),
{shutdown, frame_too_large, ConnackPacket, _} =
emqx_channel:handle_in({frame_error, frame_too_large}, ConnectingChan),
DisconnectPacket = ?DISCONNECT_PACKET(?RC_PACKET_TOO_LARGE),
ConnectedChan = channel(#{conn_state => connected}),
{ok, [{outgoing, DisconnectPacket}, {close, frame_too_large}], _}
= emqx_channel:handle_in({frame_error, frame_too_large}, ConnectedChan),
{ok, [{outgoing, DisconnectPacket}, {close, frame_too_large}], _} =
emqx_channel:handle_in({frame_error, frame_too_large}, ConnectedChan),
DisconnectedChan = channel(#{conn_state => disconnected}),
{ok, DisconnectedChan}
= emqx_channel:handle_in({frame_error, frame_too_large}, DisconnectedChan).
{ok, DisconnectedChan} =
emqx_channel:handle_in({frame_error, frame_too_large}, DisconnectedChan).
t_handle_in_expected_packet(_) ->
Packet = ?DISCONNECT_PACKET(?RC_PROTOCOL_ERROR),
{ok, [{outgoing, Packet}, {close, protocol_error}], _Chan}
= emqx_channel:handle_in(packet, channel()).
{ok, [{outgoing, Packet}, {close, protocol_error}], _Chan} =
emqx_channel:handle_in(packet, channel()).
t_process_connect(_) ->
ok = meck:expect(emqx_cm, open_session,
fun(true, _ClientInfo, _ConnInfo) ->
{ok, #{session => session(), present => false}}
end),
{ok, [{event, connected}, {connack, ?CONNACK_PACKET(?RC_SUCCESS)}], _Chan}
= emqx_channel:process_connect(connpkt(), channel(#{conn_state => idle})).
{ok, [{event, connected}, {connack, ?CONNACK_PACKET(?RC_SUCCESS)}], _Chan} =
emqx_channel:process_connect(connpkt(), channel(#{conn_state => idle})).
t_process_publish_qos0(_) ->
ok = meck:expect(emqx_broker, publish, fun(_) -> [] end),
@ -307,8 +296,8 @@ t_process_publish_qos0(_) ->
t_process_publish_qos1(_) ->
ok = meck:expect(emqx_broker, publish, fun(_) -> [] end),
Publish = ?PUBLISH_PACKET(?QOS_1, <<"t">>, 1, <<"payload">>),
{ok, ?PUBACK_PACKET(1, ?RC_NO_MATCHING_SUBSCRIBERS), _Channel}
= emqx_channel:process_publish(Publish, channel()).
{ok, ?PUBACK_PACKET(1, ?RC_NO_MATCHING_SUBSCRIBERS), _Channel} =
emqx_channel:process_publish(Publish, channel()).
t_process_subscribe(_) ->
ok = meck:expect(emqx_session, subscribe, fun(_, _, _, Session) -> {ok, Session} end),
@ -325,15 +314,6 @@ t_process_unsubscribe(_) ->
%%--------------------------------------------------------------------
t_handle_deliver(_) ->
WithPacketId = fun(Msgs) ->
lists:zip(lists:seq(1, length(Msgs)), Msgs)
end,
ok = meck:expect(emqx_session, deliver,
fun(Delivers, Session) ->
Publishes = WithPacketId([Msg || {deliver, _, Msg} <- Delivers]),
{ok, Publishes, Session}
end),
ok = meck:expect(emqx_session, info, fun(retry_interval, _Session) -> 20 end),
Msg0 = emqx_message:make(test, ?QOS_1, <<"t1">>, <<"qos1">>),
Msg1 = emqx_message:make(test, ?QOS_2, <<"t2">>, <<"qos2">>),
Delivers = [{deliver, <<"+">>, Msg0}, {deliver, <<"+">>, Msg1}],
@ -348,14 +328,14 @@ t_handle_out_publish(_) ->
Channel = channel(#{conn_state => connected}),
Pub0 = {undefined, emqx_message:make(<<"t">>, <<"qos0">>)},
Pub1 = {1, emqx_message:make(<<"c">>, ?QOS_1, <<"t">>, <<"qos1">>)},
{ok, {outgoing, Packets}, _NChannel}
= emqx_channel:handle_out(publish, [Pub0, Pub1], Channel),
{ok, {outgoing, Packets}, _NChannel} =
emqx_channel:handle_out(publish, [Pub0, Pub1], Channel),
?assertEqual(2, length(Packets)).
t_handle_out_publish_1(_) ->
Msg = emqx_message:make(<<"clientid">>, ?QOS_1, <<"t">>, <<"payload">>),
{ok, ?PUBLISH_PACKET(?QOS_1, <<"t">>, 1, <<"payload">>), _Chan}
= emqx_channel:handle_out(publish, [{1, Msg}], channel()).
{ok, ?PUBLISH_PACKET(?QOS_1, <<"t">>, 1, <<"payload">>), _Chan} =
emqx_channel:handle_out(publish, [{1, Msg}], channel()).
t_handle_out_publish_nl(_) ->
ClientInfo = clientinfo(#{clientid => <<"clientid">>}),
@ -365,50 +345,47 @@ t_handle_out_publish_nl(_) ->
{ok, Channel} = emqx_channel:handle_out(publish, Pubs, Channel).
t_handle_out_connack_sucess(_) ->
{ok, [{event, connected}, {connack, ?CONNACK_PACKET(?RC_SUCCESS, 0, _)}], Channel}
= emqx_channel:handle_out(connack, {?RC_SUCCESS, 0, connpkt()}, channel()),
{ok, [{event, connected}, {connack, ?CONNACK_PACKET(?RC_SUCCESS, 0, _)}], Channel} =
emqx_channel:handle_out(connack, {?RC_SUCCESS, 0, connpkt()}, channel()),
?assertEqual(connected, emqx_channel:info(conn_state, Channel)).
t_handle_out_connack_failure(_) ->
{shutdown, not_authorized, ?CONNACK_PACKET(?RC_NOT_AUTHORIZED), _Chan}
= emqx_channel:handle_out(connack, {?RC_NOT_AUTHORIZED, connpkt()}, channel()).
{shutdown, not_authorized, ?CONNACK_PACKET(?RC_NOT_AUTHORIZED), _Chan} =
emqx_channel:handle_out(connack, {?RC_NOT_AUTHORIZED, connpkt()}, channel()).
t_handle_out_puback(_) ->
Channel = channel(#{conn_state => connected}),
{ok, ?PUBACK_PACKET(1, ?RC_SUCCESS), _NChannel}
= emqx_channel:handle_out(puback, {1, ?RC_SUCCESS}, Channel).
% ?assertEqual(#{puback_out => 1}, emqx_channel:info(pub_stats, NChannel)).
{ok, ?PUBACK_PACKET(1, ?RC_SUCCESS), _NChannel} =
emqx_channel:handle_out(puback, {1, ?RC_SUCCESS}, Channel).
t_handle_out_pubrec(_) ->
Channel = channel(#{conn_state => connected}),
{ok, ?PUBREC_PACKET(1, ?RC_SUCCESS), _NChannel}
= emqx_channel:handle_out(pubrec, {1, ?RC_SUCCESS}, Channel).
{ok, ?PUBREC_PACKET(1, ?RC_SUCCESS), _NChannel} =
emqx_channel:handle_out(pubrec, {1, ?RC_SUCCESS}, Channel).
t_handle_out_pubrel(_) ->
Channel = channel(#{conn_state => connected}),
{ok, ?PUBREL_PACKET(1), Channel1}
= emqx_channel:handle_out(pubrel, {1, ?RC_SUCCESS}, Channel),
{ok, ?PUBREL_PACKET(2, ?RC_SUCCESS), _Channel2}
= emqx_channel:handle_out(pubrel, {2, ?RC_SUCCESS}, Channel1).
{ok, ?PUBREL_PACKET(1), Channel1} =
emqx_channel:handle_out(pubrel, {1, ?RC_SUCCESS}, Channel),
{ok, ?PUBREL_PACKET(2, ?RC_SUCCESS), _Channel2} =
emqx_channel:handle_out(pubrel, {2, ?RC_SUCCESS}, Channel1).
t_handle_out_pubcomp(_) ->
{ok, ?PUBCOMP_PACKET(1, ?RC_SUCCESS), _Channel}
= emqx_channel:handle_out(pubcomp, {1, ?RC_SUCCESS}, channel()).
{ok, ?PUBCOMP_PACKET(1, ?RC_SUCCESS), _Channel} =
emqx_channel:handle_out(pubcomp, {1, ?RC_SUCCESS}, channel()).
t_handle_out_suback(_) ->
Replies = [{outgoing, ?SUBACK_PACKET(1, [?QOS_2])}, {event, updated}],
{ok, Replies, _Channel}
= emqx_channel:handle_out(suback, {1, [?QOS_2]}, channel()).
{ok, Replies, _Chan} = emqx_channel:handle_out(suback, {1, [?QOS_2]}, channel()).
t_handle_out_unsuback(_) ->
Replies = [{outgoing, ?UNSUBACK_PACKET(1, [?RC_SUCCESS])}, {event, updated}],
{ok, Replies, _Channel}
= emqx_channel:handle_out(unsuback, {1, [?RC_SUCCESS]}, channel()).
{ok, Replies, _Chan} = emqx_channel:handle_out(unsuback, {1, [?RC_SUCCESS]}, channel()).
t_handle_out_disconnect(_) ->
Packet = ?DISCONNECT_PACKET(?RC_SUCCESS),
{ok, [{outgoing, Packet}, {close, normal}], _Chan}
= emqx_channel:handle_out(disconnect, ?RC_SUCCESS, channel()).
{ok, [{outgoing, Packet}, {close, normal}], _Chan} =
emqx_channel:handle_out(disconnect, ?RC_SUCCESS, channel()).
t_handle_out_unexpected(_) ->
{ok, _Channel} = emqx_channel:handle_out(unexpected, <<"data">>, channel()).
@ -422,20 +399,19 @@ t_handle_call_kick(_) ->
t_handle_call_discard(_) ->
Packet = ?DISCONNECT_PACKET(?RC_SESSION_TAKEN_OVER),
{shutdown, discarded, ok, Packet, _Channel}
= emqx_channel:handle_call(discard, channel()).
{shutdown, discarded, ok, Packet, _Channel} =
emqx_channel:handle_call(discard, channel()).
t_handle_call_takeover_begin(_) ->
{reply, undefined, _Channel}
= emqx_channel:handle_call({takeover, 'begin'}, channel()).
{reply, _Session, _Chan} = emqx_channel:handle_call({takeover, 'begin'}, channel()).
t_handle_call_takeover_end(_) ->
ok = meck:expect(emqx_session, takeover, fun(_) -> ok end),
{shutdown, takeovered, [], _Channel}
= emqx_channel:handle_call({takeover, 'end'}, channel()).
{shutdown, takeovered, [], _Chan} =
emqx_channel:handle_call({takeover, 'end'}, channel()).
t_handle_call_unexpected(_) ->
{reply, ignored, _Channel} = emqx_channel:handle_call(unexpected_req, channel()).
{reply, ignored, _Chan} = emqx_channel:handle_call(unexpected_req, channel()).
%%--------------------------------------------------------------------
%% Test cases for handle_info
@ -507,8 +483,8 @@ t_auth_connect(_) ->
t_process_alias(_) ->
Publish = #mqtt_packet_publish{topic_name = <<>>, properties = #{'Topic-Alias' => 1}},
Channel = emqx_channel:set_field(topic_aliases, #{1 => <<"t">>}, channel()),
{ok, #mqtt_packet{variable = #mqtt_packet_publish{topic_name = <<"t">>}}, _Chan}
= emqx_channel:process_alias(#mqtt_packet{variable = Publish}, Channel).
{ok, #mqtt_packet{variable = #mqtt_packet_publish{topic_name = <<"t">>}}, _Chan} =
emqx_channel:process_alias(#mqtt_packet{variable = Publish}, Channel).
t_check_pub_acl(_) ->
ok = meck:new(emqx_zone, [passthrough, no_history]),
@ -566,14 +542,27 @@ t_terminate(_) ->
channel() -> channel(#{}).
channel(InitFields) ->
ConnInfo = #{peername => {{127,0,0,1}, 3456},
sockname => {{127,0,0,1}, 1883},
conn_mod => emqx_connection,
proto_name => <<"MQTT">>,
proto_ver => ?MQTT_PROTO_V5,
clean_start => true,
keepalive => 30,
clientid => <<"clientid">>,
username => <<"username">>,
conn_props => #{},
receive_maximum => 100,
expiry_interval => 0
},
maps:fold(fun(Field, Value, Channel) ->
emqx_channel:set_field(Field, Value, Channel)
end, default_channel(), InitFields).
default_channel() ->
Channel = emqx_channel:init(?DEFAULT_CONNINFO, [{zone, zone}]),
Channel1 = emqx_channel:set_field(conn_state, connected, Channel),
emqx_channel:set_field(clientinfo, clientinfo(), Channel1).
end,
emqx_channel:init(ConnInfo, [{zone, zone}]),
maps:merge(#{clientinfo => clientinfo(),
session => session(),
conn_state => connected
}, InitFields)).
clientinfo() -> clientinfo(#{}).
clientinfo(InitProps) ->
@ -608,6 +597,6 @@ session(InitFields) when is_map(InitFields) ->
maps:fold(fun(Field, Value, Session) ->
emqx_session:set_field(Field, Value, Session)
end,
emqx_session:init(#{zone => zone}, #{receive_maximum => 0}),
emqx_session:init(#{zone => channel}, #{receive_maximum => 0}),
InitFields).

View File

@ -22,6 +22,8 @@
-include("emqx.hrl").
-include_lib("eunit/include/eunit.hrl").
-define(CM, emqx_cm).
%%--------------------------------------------------------------------
%% CT callbacks
%%--------------------------------------------------------------------
@ -81,10 +83,55 @@ t_open_session(_) ->
?assertEqual(100, emqx_session:info(inflight_max, Session2)).
t_discard_session(_) ->
ok = emqx_cm:discard_session(<<"clientid">>).
ok = meck:new(emqx_connection, [passthrough, no_history]),
ok = meck:expect(emqx_connection, call, fun(_, _) -> ok end),
ok = emqx_cm:discard_session(<<"clientid">>),
ok = emqx_cm:register_channel(<<"clientid">>),
ok = emqx_cm:discard_session(<<"clientid">>),
ok = emqx_cm:unregister_channel(<<"clientid">>),
ok = emqx_cm:register_channel(<<"clientid">>, #{conninfo => #{conn_mod => emqx_connection}}, []),
ok = emqx_cm:discard_session(<<"clientid">>),
ok = meck:expect(emqx_connection, call, fun(_, _) -> error(testing) end),
ok = emqx_cm:discard_session(<<"clientid">>),
ok = emqx_cm:unregister_channel(<<"clientid">>),
ok = meck:unload(emqx_connection).
t_takeover_session(_) ->
{error, not_found} = emqx_cm:takeover_session(<<"clientid">>).
ok = meck:new(emqx_connection, [passthrough, no_history]),
ok = meck:expect(emqx_connection, call, fun(_, _) -> test end),
{error, not_found} = emqx_cm:takeover_session(<<"clientid">>),
ok = emqx_cm:register_channel(<<"clientid">>),
{error, not_found} = emqx_cm:takeover_session(<<"clientid">>),
ok = emqx_cm:unregister_channel(<<"clientid">>),
ok = emqx_cm:register_channel(<<"clientid">>, #{conninfo => #{conn_mod => emqx_connection}}, []),
Pid = self(),
{ok, emqx_connection, Pid, test} = emqx_cm:takeover_session(<<"clientid">>),
erlang:spawn(fun() ->
ok = emqx_cm:register_channel(<<"clientid">>, #{conninfo => #{conn_mod => emqx_connection}}, []),
timer:sleep(1000)
end),
ct:sleep(100),
{ok, emqx_connection, _, test} = emqx_cm:takeover_session(<<"clientid">>),
ok = emqx_cm:unregister_channel(<<"clientid">>),
ok = meck:unload(emqx_connection).
t_kick_session(_) ->
ok = meck:new(emqx_connection, [passthrough, no_history]),
ok = meck:expect(emqx_connection, call, fun(_, _) -> test end),
{error, not_found} = emqx_cm:kick_session(<<"clientid">>),
ok = emqx_cm:register_channel(<<"clientid">>),
{error, not_found} = emqx_cm:kick_session(<<"clientid">>),
ok = emqx_cm:unregister_channel(<<"clientid">>),
ok = emqx_cm:register_channel(<<"clientid">>, #{conninfo => #{conn_mod => emqx_connection}}, []),
test = emqx_cm:kick_session(<<"clientid">>),
erlang:spawn(fun() ->
ok = emqx_cm:register_channel(<<"clientid">>, #{conninfo => #{conn_mod => emqx_connection}}, []),
timer:sleep(1000)
end),
ct:sleep(100),
test = emqx_cm:kick_session(<<"clientid">>),
ok = emqx_cm:unregister_channel(<<"clientid">>),
ok = meck:unload(emqx_connection).
t_lock_clientid(_) ->
{true, _Nodes} = emqx_cm_locker:lock(<<"clientid">>),
@ -92,27 +139,7 @@ t_lock_clientid(_) ->
{true, _Nodes} = emqx_cm_locker:unlock(<<"clientid">>),
{true, _Nodes} = emqx_cm_locker:unlock(<<"clientid">>).
% t_unregister_channel(_) ->
% error('TODO').
% t_get_chan_attrs(_) ->
% error('TODO').
% t_get_chan_stats(_) ->
% error('TODO').
% t_lookup_channels(_) ->
% error('TODO').
% t_set_chan_stats(_) ->
% error('TODO').
% t_set_chan_attrs(_) ->
% error('TODO').
% t_register_channel(_) ->
% error('TODO').
% t_stats_fun(_) ->
% error('TODO').
t_message(_) ->
?CM ! testing,
gen_server:cast(?CM, testing),
gen_server:call(?CM, testing).

View File

@ -22,43 +22,34 @@
-include("emqx_mqtt.hrl").
-include_lib("eunit/include/eunit.hrl").
-define(STATS_KYES, [recv_pkt, recv_msg, send_pkt, send_msg,
recv_oct, recv_cnt, send_oct, send_cnt,
send_pend
]).
all() -> emqx_ct:all(?MODULE) ++ [{group, real_client}].
groups() ->
[{real_client, [non_parallel_tests],
[
g_get_conn_stats,
g_handle_sock_passive
]}].
all() -> emqx_ct:all(?MODULE).
%%--------------------------------------------------------------------
%% CT callbacks
%%--------------------------------------------------------------------
init_per_suite(Config) ->
%% Meck Transport
ok = meck:new(emqx_transport, [non_strict, passthrough, no_history, no_link]),
%% Meck Channel
ok = meck:new(emqx_channel, [passthrough, no_history, no_link]),
%% Meck Cm
ok = meck:new(emqx_cm, [passthrough, no_history, no_link]),
%% Meck Metrics
ok = meck:new(emqx_metrics, [passthrough, no_history, no_link]),
ok = meck:expect(emqx_metrics, inc, fun(_, _) -> ok end),
ok = meck:expect(emqx_metrics, inc_recv, fun(_) -> ok end),
ok = meck:expect(emqx_metrics, inc_sent, fun(_) -> ok end),
Config.
end_per_suite(_Config) ->
ok = meck:unload(emqx_transport),
ok = meck:unload(emqx_channel),
ok = meck:unload(emqx_cm),
ok = meck:unload(emqx_metrics),
ok.
init_per_group(real_client, Config) ->
emqx_ct_helpers:boot_modules(all),
emqx_ct_helpers:start_apps([]),
Config;
init_per_group(_, Config) -> Config.
end_per_group(real_client, _Config) ->
emqx_ct_helpers:stop_apps([]);
end_per_group(_, Config) -> Config.
init_per_testcase(_TestCase, Config) ->
%% Meck Transport
ok = meck:new(emqx_transport, [non_strict, passthrough, no_history]),
ok = meck:expect(emqx_transport, wait, fun(Sock) -> {ok, Sock} end),
ok = meck:expect(emqx_transport, type, fun(_Sock) -> tcp end),
ok = meck:expect(emqx_transport, ensure_ok_or_exit,
@ -72,19 +63,9 @@ init_per_testcase(_TestCase, Config) ->
end),
ok = meck:expect(emqx_transport, async_send, fun(_Sock, _Data) -> ok end),
ok = meck:expect(emqx_transport, fast_close, fun(_Sock) -> ok end),
%% Meck Channel
ok = meck:new(emqx_channel, [passthrough, no_history]),
%% Meck Metrics
ok = meck:new(emqx_metrics, [passthrough, no_history]),
ok = meck:expect(emqx_metrics, inc, fun(_, _) -> ok end),
ok = meck:expect(emqx_metrics, inc_recv, fun(_) -> ok end),
ok = meck:expect(emqx_metrics, inc_sent, fun(_) -> ok end),
Config.
end_per_testcase(_TestCase, Config) ->
ok = meck:unload(emqx_transport),
ok = meck:unload(emqx_channel),
ok = meck:unload(emqx_metrics),
Config.
%%--------------------------------------------------------------------
@ -92,9 +73,7 @@ end_per_testcase(_TestCase, Config) ->
%%--------------------------------------------------------------------
t_start_link_ok(_) ->
with_connection(fun(CPid) ->
state = element(1, sys:get_state(CPid))
end).
with_conn(fun(CPid) -> state = element(1, sys:get_state(CPid)) end).
t_start_link_exit_on_wait(_) ->
ok = exit_on_wait_error(enotconn, normal),
@ -110,108 +89,114 @@ t_start_link_exit_on_activate(_) ->
ok = exit_on_activate_error(econnreset, {shutdown, econnreset}).
t_get_conn_info(_) ->
with_connection(fun(CPid) ->
#{sockinfo := SockInfo} = emqx_connection:info(CPid),
?assertEqual(#{active_n => 100,
peername => {{127,0,0,1},3456},
sockname => {{127,0,0,1},1883},
sockstate => running,
socktype => tcp}, SockInfo)
end).
g_get_conn_stats(_) ->
with_client(fun(CPid) ->
Stats = emqx_connection:stats(CPid),
ct:pal("==== stats: ~p", [Stats]),
[?assert(proplists:get_value(Key, Stats) >= 0) || Key <- ?STATS_KYES]
end, []).
with_conn(fun(CPid) ->
#{sockinfo := SockInfo} = emqx_connection:info(CPid),
?assertEqual(#{active_n => 100,
peername => {{127,0,0,1},3456},
sockname => {{127,0,0,1},1883},
sockstate => running,
socktype => tcp
}, SockInfo)
end).
t_handle_call_discard(_) ->
with_connection(fun(CPid) ->
ok = meck:expect(emqx_channel, handle_call,
fun(discard, Channel) ->
{shutdown, discarded, ok, Channel}
end),
ok = emqx_connection:call(CPid, discard),
timer:sleep(100),
ok = trap_exit(CPid, {shutdown, discarded})
end, #{trap_exit => true}).
with_conn(fun(CPid) ->
ok = meck:expect(emqx_channel, handle_call,
fun(discard, Channel) ->
{shutdown, discarded, ok, Channel}
end),
ok = emqx_connection:call(CPid, discard),
timer:sleep(100),
ok = trap_exit(CPid, {shutdown, discarded})
end, #{trap_exit => true}),
with_conn(fun(CPid) ->
ok = meck:expect(emqx_channel, handle_call,
fun(discard, Channel) ->
{shutdown, discarded, ok, ?DISCONNECT_PACKET(?RC_SESSION_TAKEN_OVER), Channel}
end),
ok = emqx_connection:call(CPid, discard),
timer:sleep(100),
ok = trap_exit(CPid, {shutdown, discarded})
end, #{trap_exit => true}).
t_handle_call_takeover(_) ->
with_connection(fun(CPid) ->
ok = meck:expect(emqx_channel, handle_call,
fun({takeover, 'begin'}, Channel) ->
{reply, session, Channel};
({takeover, 'end'}, Channel) ->
{shutdown, takeovered, [], Channel}
end),
session = emqx_connection:call(CPid, {takeover, 'begin'}),
[] = emqx_connection:call(CPid, {takeover, 'end'}),
timer:sleep(100),
ok = trap_exit(CPid, {shutdown, takeovered})
end, #{trap_exit => true}).
with_conn(fun(CPid) ->
ok = meck:expect(emqx_channel, handle_call,
fun({takeover, 'begin'}, Channel) ->
{reply, session, Channel};
({takeover, 'end'}, Channel) ->
{shutdown, takeovered, [], Channel}
end),
session = emqx_connection:call(CPid, {takeover, 'begin'}),
[] = emqx_connection:call(CPid, {takeover, 'end'}),
timer:sleep(100),
ok = trap_exit(CPid, {shutdown, takeovered})
end, #{trap_exit => true}).
t_handle_call_any(_) ->
with_connection(fun(CPid) ->
ok = meck:expect(emqx_channel, handle_call,
fun(_Req, Channel) -> {reply, ok, Channel} end),
ok = emqx_connection:call(CPid, req)
end).
with_conn(fun(CPid) ->
ok = meck:expect(emqx_channel, handle_call,
fun(_Req, Channel) -> {reply, ok, Channel} end),
ok = emqx_connection:call(CPid, req)
end).
t_handle_incoming_connect(_) ->
with_connection(fun(CPid) ->
ok = meck:expect(emqx_channel, handle_in, fun(_Packet, Channel) -> {ok, Channel} end),
ConnPkt = #mqtt_packet_connect{proto_ver = ?MQTT_PROTO_V5,
proto_name = <<"MQTT">>,
clientid = <<>>,
clean_start = true,
keepalive = 60
},
Frame = make_frame(?CONNECT_PACKET(ConnPkt)),
CPid ! {tcp, sock, Frame}
end).
with_conn(fun(CPid) ->
ok = meck:expect(emqx_channel, handle_in, fun(_Packet, Channel) -> {ok, Channel} end),
ConnPkt = #mqtt_packet_connect{proto_ver = ?MQTT_PROTO_V5,
proto_name = <<"MQTT">>,
clientid = <<>>,
clean_start = true,
keepalive = 60
},
Frame = make_frame(?CONNECT_PACKET(ConnPkt)),
CPid ! {tcp, sock, Frame}
end).
t_handle_incoming_publish(_) ->
with_connection(fun(CPid) ->
ok = meck:expect(emqx_channel, handle_in, fun(_Packet, Channel) -> {ok, Channel} end),
Frame = make_frame(?PUBLISH_PACKET(?QOS_1, <<"t">>, 1, <<"payload">>)),
CPid ! {tcp, sock, Frame}
end).
with_conn(fun(CPid) ->
ok = meck:expect(emqx_channel, handle_in, fun(_Packet, Channel) -> {ok, Channel} end),
Frame = make_frame(?PUBLISH_PACKET(?QOS_1, <<"t">>, 1, <<"payload">>)),
CPid ! {tcp, sock, Frame}
end).
t_handle_incoming_subscribe(_) ->
with_connection(fun(CPid) ->
ok = meck:expect(emqx_channel, handle_in, fun(_Packet, Channel) -> {ok, Channel} end),
Frame = <<?SUBSCRIBE:4,2:4,11,0,2,0,6,84,111,112,105,99,65,2>>,
CPid ! {tcp, sock, Frame}
end).
with_conn(fun(CPid) ->
ok = meck:expect(emqx_channel, handle_in, fun(_Packet, Channel) -> {ok, Channel} end),
Frame = <<?SUBSCRIBE:4,2:4,11,0,2,0,6,84,111,112,105,99,65,2>>,
CPid ! {tcp, sock, Frame}
end).
t_handle_incoming_unsubscribe(_) ->
with_connection(fun(CPid) ->
ok = meck:expect(emqx_channel, handle_in, fun(_Packet, Channel) -> {ok, Channel} end),
Frame = <<?UNSUBSCRIBE:4,2:4,10,0,2,0,6,84,111,112,105,99,65>>,
CPid ! {tcp, sock, Frame}
end).
with_conn(fun(CPid) ->
ok = meck:expect(emqx_channel, handle_in, fun(_Packet, Channel) -> {ok, Channel} end),
Frame = <<?UNSUBSCRIBE:4,2:4,10,0,2,0,6,84,111,112,105,99,65>>,
CPid ! {tcp, sock, Frame}
end).
t_handle_incoming_undefined(_) ->
with_conn(fun(CPid) ->
ok = meck:expect(emqx_channel, handle_in, fun(_Packet, Channel) -> {ok, Channel} end),
CPid ! {incoming, undefined}
end).
t_handle_sock_error(_) ->
with_connection(fun(CPid) ->
ok = meck:expect(emqx_channel, handle_info,
fun({_, Reason}, Channel) ->
{shutdown, Reason, Channel}
end),
with_conn(fun(CPid) ->
ok = meck:expect(emqx_channel, handle_info,
fun({_, Reason}, Channel) ->
{shutdown, Reason, Channel}
end),
%% TODO: fixme later
CPid ! {tcp_error, sock, econnreset},
timer:sleep(100),
trap_exit(CPid, {shutdown, econnreset})
end, #{trap_exit => true}).
g_handle_sock_passive(_) ->
with_client(fun(CPid) -> CPid ! {tcp_passive, sock} end, []).
end, #{trap_exit => true}).
t_handle_sock_activate(_) ->
with_connection(fun(CPid) -> CPid ! activate_socket end).
with_conn(fun(CPid) -> CPid ! activate_socket end).
t_handle_sock_closed(_) ->
with_connection(fun(CPid) ->
with_conn(fun(CPid) ->
ok = meck:expect(emqx_channel, handle_info,
fun({sock_closed, Reason}, Channel) ->
{shutdown, Reason, Channel}
@ -219,75 +204,156 @@ t_handle_sock_closed(_) ->
CPid ! {tcp_closed, sock},
timer:sleep(100),
trap_exit(CPid, {shutdown, tcp_closed})
end, #{trap_exit => true}).
end, #{trap_exit => true}),
with_conn(fun(CPid) ->
ok = meck:expect(emqx_channel, handle_info,
fun({sock_closed, Reason}, Channel) ->
{shutdown, Reason, ?DISCONNECT_PACKET(), Channel}
end),
CPid ! {tcp_closed, sock},
timer:sleep(100),
trap_exit(CPid, {shutdown, tcp_closed})
end, #{trap_exit => true}).
t_handle_outgoing(_) ->
with_connection(fun(CPid) ->
Publish = ?PUBLISH_PACKET(?QOS_2, <<"Topic">>, 1, <<>>),
CPid ! {outgoing, Publish},
CPid ! {outgoing, ?PUBREL_PACKET(1)},
CPid ! {outgoing, [?PUBCOMP_PACKET(1)]}
end).
with_conn(fun(CPid) ->
Publish = ?PUBLISH_PACKET(?QOS_2, <<"Topic">>, 1, <<>>),
CPid ! {outgoing, Publish},
CPid ! {outgoing, ?PUBREL_PACKET(1)},
CPid ! {outgoing, [?PUBCOMP_PACKET(1)]}
end).
t_conn_rate_limit(_) ->
with_connection(fun(CPid) ->
ok = meck:expect(emqx_channel, handle_in, fun(_, Channel) -> {ok, Channel} end),
lists:foreach(fun(I) ->
Publish = ?PUBLISH_PACKET(?QOS_0, <<"Topic">>, I, payload(2000)),
CPid ! {tcp, sock, make_frame(Publish)}
end, [1, 2])
%%#{sockinfo := #{sockstate := blocked}} = emqx_connection:info(CPid)
end, #{active_n => 1, rate_limit => {1, 1024}}).
with_conn(fun(CPid) ->
ok = meck:expect(emqx_channel, handle_in, fun(_, Channel) -> {ok, Channel} end),
lists:foreach(fun(I) ->
Publish = ?PUBLISH_PACKET(?QOS_0, <<"Topic">>, I, payload(2000)),
CPid ! {tcp, sock, make_frame(Publish)}
end, [1, 2])
end, #{active_n => 1, rate_limit => {1, 1024}}).
t_conn_pub_limit(_) ->
with_connection(fun(CPid) ->
ok = meck:expect(emqx_channel, handle_in, fun(_, Channel) -> {ok, Channel} end),
ok = lists:foreach(fun(I) ->
CPid ! {incoming, ?PUBLISH_PACKET(?QOS_0, <<"Topic">>, I, <<>>)}
end, lists:seq(1, 3))
%%#{sockinfo := #{sockstate := blocked}} = emqx_connection:info(CPid)
end, #{active_n => 1, publish_limit => {1, 2}}).
with_conn(fun(CPid) ->
ok = meck:expect(emqx_channel, handle_in, fun(_, Channel) -> {ok, Channel} end),
ok = lists:foreach(fun(I) ->
CPid ! {incoming, ?PUBLISH_PACKET(?QOS_0, <<"Topic">>, I, <<>>)}
end, lists:seq(1, 3))
%%#{sockinfo := #{sockstate := blocked}} = emqx_connection:info(CPid)
end, #{active_n => 1, publish_limit => {1, 2}}).
t_conn_pingreq(_) ->
with_conn(fun(CPid) -> CPid ! {incoming, ?PACKET(?PINGREQ)} end).
t_inet_reply(_) ->
ok = meck:new(emqx_pd, [passthrough, no_history]),
with_conn(fun(CPid) ->
ok = meck:expect(emqx_pd, get_counter, fun(_) -> 10 end),
CPid ! {inet_reply, for_testing, ok},
timer:sleep(100)
end, #{active_n => 1, trap_exit => true}),
ok = meck:unload(emqx_pd),
with_conn(fun(CPid) ->
CPid ! {inet_reply, for_testing, {error, for_testing}},
timer:sleep(100),
trap_exit(CPid, {shutdown, for_testing})
end, #{trap_exit => true}).
t_deliver(_) ->
with_conn(fun(CPid) ->
ok = meck:expect(emqx_channel, handle_deliver,
fun(_, Channel) -> {ok, Channel} end),
CPid ! {deliver, topic, msg}
end).
t_event_disconnected(_) ->
with_conn(fun(CPid) ->
ok = meck:expect(emqx_cm, set_chan_info, fun(_, _) -> ok end),
ok = meck:expect(emqx_cm, connection_closed, fun(_) -> ok end),
CPid ! {event, disconnected}
end).
t_event_undefined(_) ->
with_conn(fun(CPid) ->
ok = meck:expect(emqx_channel, stats, fun(_Channel) -> [] end),
ok = meck:expect(emqx_cm, set_chan_info, fun(_, _) -> ok end),
ok = meck:expect(emqx_cm, set_chan_stats, fun(_, _) -> true end),
CPid ! {event, undefined}
end).
t_cloes(_) ->
with_conn(fun(CPid) ->
CPid ! {close, normal},
timer:sleep(100),
trap_exit(CPid, {shutdown, normal})
end, #{trap_exit => true}).
t_oom_shutdown(_) ->
with_connection(fun(CPid) ->
CPid ! {shutdown, message_queue_too_long},
timer:sleep(100),
trap_exit(CPid, {shutdown, message_queue_too_long})
end, #{trap_exit => true}).
with_conn(fun(CPid) ->
CPid ! {shutdown, message_queue_too_long},
timer:sleep(100),
trap_exit(CPid, {shutdown, message_queue_too_long})
end, #{trap_exit => true}).
t_handle_idle_timeout(_) ->
ok = emqx_zone:set_env(external, idle_timeout, 10),
with_connection(fun(CPid) ->
timer:sleep(100),
trap_exit(CPid, {shutdown, idle_timeout})
end, #{zone => external, trap_exit => true}).
with_conn(fun(CPid) ->
timer:sleep(100),
trap_exit(CPid, {shutdown, idle_timeout})
end, #{zone => external, trap_exit => true}).
t_handle_emit_stats(_) ->
with_connection(fun(CPid) ->
ok = meck:expect(emqx_channel, handle_timeout,
fun(_TRef, _TMsg, Channel) ->
{ok, Channel}
end),
CPid ! {timeout, make_ref(), emit_stats}
end).
ok = emqx_zone:set_env(external, idle_timeout, 1000),
with_conn(fun(CPid) ->
ok = meck:expect(emqx_channel, stats, fun(_Channel) -> [] end),
ok = meck:expect(emqx_channel, handle_in, fun(_Packet, Channel) -> {ok, Channel} end),
ok = meck:expect(emqx_cm, set_chan_stats, fun(_, _) -> true end),
CPid ! {incoming, ?CONNECT_PACKET(#{strict_mode => false,
max_size => ?MAX_PACKET_SIZE,
version => ?MQTT_PROTO_V4
})},
timer:sleep(1000)
end,#{zone => external, trap_exit => true}).
t_handle_limit_timeout(_) ->
with_conn(fun(CPid) ->
CPid ! {timeout, undefined, limit_timeout},
timer:sleep(100),
true = erlang:is_process_alive(CPid)
end).
t_handle_keepalive_timeout(_) ->
with_connection(fun(CPid) ->
ok = meck:expect(emqx_channel, handle_timeout,
fun(_TRef, _TMsg, Channel) ->
{shutdown, keepalive_timeout, Channel}
end),
CPid ! {timeout, make_ref(), keepalive},
timer:sleep(100),
trap_exit(CPid, {shutdown, keepalive_timeout})
end, #{trap_exit => true}).
with_conn(fun(CPid) ->
ok = meck:expect(emqx_channel, handle_timeout,
fun(_TRef, _TMsg, Channel) ->
{shutdown, keepalive_timeout, Channel}
end),
CPid ! {timeout, make_ref(), keepalive},
timer:sleep(100),
trap_exit(CPid, {shutdown, keepalive_timeout})
end, #{trap_exit => true}),
with_conn(fun(CPid) ->
ok = meck:expect(emqx_transport, getstat, fun(_Sock, _Options) -> {error, for_testing} end),
ok = meck:expect(emqx_channel, handle_timeout,
fun(_TRef, _TMsg, Channel) ->
{shutdown, keepalive_timeout, Channel}
end),
CPid ! {timeout, make_ref(), keepalive},
timer:sleep(100),
false = erlang:is_process_alive(CPid)
end, #{trap_exit => true}).
t_handle_shutdown(_) ->
with_connection(fun(CPid) ->
CPid ! Shutdown = {shutdown, reason},
timer:sleep(100),
trap_exit(CPid, Shutdown)
end, #{trap_exit => true}).
with_conn(fun(CPid) ->
CPid ! Shutdown = {shutdown, reason},
timer:sleep(100),
trap_exit(CPid, Shutdown)
end, #{trap_exit => true}).
t_exit_message(_) ->
with_conn(fun(CPid) ->
CPid ! {'EXIT', CPid, for_testing},
timer:sleep(1000)
end, #{trap_exit => true}).
%%--------------------------------------------------------------------
%% Helper functions
@ -298,27 +364,28 @@ exit_on_wait_error(SockErr, Reason) ->
fun(_Sock) ->
{error, SockErr}
end),
with_connection(fun(CPid) ->
timer:sleep(100),
trap_exit(CPid, Reason)
end, #{trap_exit => true}).
with_conn(fun(CPid) ->
timer:sleep(100),
trap_exit(CPid, Reason)
end, #{trap_exit => true}).
exit_on_activate_error(SockErr, Reason) ->
ok = meck:expect(emqx_transport, setopts,
fun(_Sock, _Opts) ->
{error, SockErr}
end),
with_connection(fun(CPid) ->
timer:sleep(100),
trap_exit(CPid, Reason)
end, #{trap_exit => true}).
with_conn(fun(CPid) ->
timer:sleep(100),
trap_exit(CPid, Reason)
end, #{trap_exit => true}).
with_connection(TestFun) ->
with_connection(TestFun, #{trap_exit => false}).
with_conn(TestFun) ->
with_conn(TestFun, #{trap_exit => false}).
with_connection(TestFun, Options) when is_map(Options) ->
with_connection(TestFun, maps:to_list(Options));
with_connection(TestFun, Options) ->
with_conn(TestFun, Options) when is_map(Options) ->
with_conn(TestFun, maps:to_list(Options));
with_conn(TestFun, Options) ->
TrapExit = proplists:get_value(trap_exit, Options, false),
process_flag(trap_exit, TrapExit),
{ok, CPid} = emqx_connection:start_link(emqx_transport, sock, Options),
@ -326,18 +393,6 @@ with_connection(TestFun, Options) ->
TrapExit orelse emqx_connection:stop(CPid),
ok.
with_client(TestFun, _Options) ->
ClientId = <<"t_conn">>,
{ok, C} = emqtt:start_link([{clientid, ClientId}]),
{ok, _} = emqtt:connect(C),
timer:sleep(50),
case emqx_cm:lookup_channels(ClientId) of
[] -> ct:fail({client_not_started, ClientId});
[ChanPid] ->
TestFun(ChanPid),
emqtt:stop(C)
end.
trap_exit(Pid, Reason) ->
receive
{'EXIT', Pid, Reason} -> ok;

View File

@ -53,5 +53,10 @@ t_detect_check(_) ->
true = emqx_banned:check(ClientInfo),
timer:sleep(200),
false = emqx_banned:check(ClientInfo),
Childrens = supervisor:which_children(emqx_cm_sup),
{flapping, Pid, _, _} = lists:keyfind(flapping, 1, Childrens),
gen_server:call(Pid, test),
gen_server:cast(Pid, test),
Pid ! test,
ok = emqx_flapping:stop().

View File

@ -99,6 +99,14 @@ t_run_hooks(_) ->
ok = emqx_hooks:stop().
t_uncovered_func(_) ->
{ok, _} = emqx_hooks:start_link(),
Pid = erlang:whereis(emqx_hooks),
gen_server:call(Pid, test),
gen_server:cast(Pid, test),
Pid ! test,
ok = emqx_hooks:stop().
%%--------------------------------------------------------------------
%% Hook fun
%%--------------------------------------------------------------------

View File

@ -0,0 +1,58 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2019 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_limiter_SUITE).
-compile(export_all).
-compile(nowarn_export_all).
-include_lib("eunit/include/eunit.hrl").
all() -> emqx_ct:all(?MODULE).
init_per_testcase(_TestCase, Config) ->
Config.
end_per_testcase(_TestCase, _Config) ->
ok.
t_info(_) ->
#{pub_limit := #{rate := 1,
burst := 10,
tokens := 10
},
rate_limit := #{rate := 100,
burst := 1000,
tokens := 1000
}
} = emqx_limiter:info(limiter()).
t_check(_) ->
lists:foreach(fun(I) ->
{ok, Limiter} = emqx_limiter:check(#{cnt => I, oct => I*100}, limiter()),
#{pub_limit := #{tokens := Cnt},
rate_limit := #{tokens := Oct}
} = emqx_limiter:info(Limiter),
?assertEqual({10 - I, 1000 - I*100}, {Cnt, Oct})
end, lists:seq(1, 10)).
t_check_pause(_) ->
{pause, 1000, _} = emqx_limiter:check(#{cnt => 11, oct => 2000}, limiter()),
{pause, 2000, _} = emqx_limiter:check(#{cnt => 10, oct => 1200}, limiter()).
limiter() ->
emqx_limiter:init([{pub_limit, {1, 10}}, {rate_limit, {100, 1000}}]).

View File

@ -43,17 +43,56 @@ t_make(_) ->
?assertEqual(<<"topic">>, emqx_message:topic(Msg2)),
?assertEqual(<<"payload">>, emqx_message:payload(Msg2)).
t_id(_) ->
Msg = emqx_message:make(<<"topic">>, <<"payload">>),
?assert(is_binary(emqx_message:id(Msg))).
t_qos(_) ->
Msg = emqx_message:make(<<"topic">>, <<"payload">>),
?assertEqual(?QOS_0, emqx_message:qos(Msg)),
Msg1 = emqx_message:make(id, ?QOS_1, <<"t">>, <<"payload">>),
?assertEqual(?QOS_1, emqx_message:qos(Msg1)),
Msg2 = emqx_message:make(id, ?QOS_2, <<"t">>, <<"payload">>),
?assertEqual(?QOS_2, emqx_message:qos(Msg2)).
t_topic(_) ->
Msg = emqx_message:make(<<"t">>, <<"payload">>),
?assertEqual(<<"t">>, emqx_message:topic(Msg)).
t_payload(_) ->
Msg = emqx_message:make(<<"t">>, <<"payload">>),
?assertEqual(<<"payload">>, emqx_message:payload(Msg)).
t_timestamp(_) ->
Msg = emqx_message:make(<<"t">>, <<"payload">>),
timer:sleep(1),
?assert(erlang:system_time(millisecond) > emqx_message:timestamp(Msg)).
t_clean_dup(_) ->
Msg = emqx_message:make(<<"topic">>, <<"payload">>),
?assertNot(emqx_message:get_flag(dup, Msg)),
Msg = emqx_message:clean_dup(Msg),
Msg1 = emqx_message:set_flag(dup, Msg),
?assert(emqx_message:get_flag(dup, Msg1)),
Msg2 = emqx_message:clean_dup(Msg1),
?assertNot(emqx_message:get_flag(dup, Msg2)).
t_get_set_flags(_) ->
Msg = #message{id = <<"id">>, qos = ?QOS_1, flags = undefined},
Msg1 = emqx_message:set_flags(#{retain => true}, Msg),
?assertEqual(#{retain => true}, emqx_message:get_flags(Msg1)).
?assertEqual(#{retain => true}, emqx_message:get_flags(Msg1)),
Msg2 = emqx_message:set_flags(#{dup => true}, Msg1),
?assertEqual(#{retain => true, dup => true}, emqx_message:get_flags(Msg2)).
t_get_set_flag(_) ->
Msg = emqx_message:make(<<"clientid">>, <<"topic">>, <<"payload">>),
Msg2 = emqx_message:set_flag(retain, false, Msg),
?assertNot(emqx_message:get_flag(dup, Msg)),
?assertNot(emqx_message:get_flag(retain, Msg)),
Msg1 = emqx_message:set_flag(dup, true, Msg),
Msg2 = emqx_message:set_flag(retain, true, Msg1),
Msg3 = emqx_message:set_flag(dup, Msg2),
?assert(emqx_message:get_flag(dup, Msg3)),
?assertNot(emqx_message:get_flag(retain, Msg3)),
?assert(emqx_message:get_flag(retain, Msg3)),
Msg4 = emqx_message:unset_flag(dup, Msg3),
Msg5 = emqx_message:unset_flag(retain, Msg4),
Msg5 = emqx_message:unset_flag(badflag, Msg5),
@ -76,6 +115,8 @@ t_get_set_headers(_) ->
t_get_set_header(_) ->
Msg = emqx_message:make(<<"clientid">>, <<"topic">>, <<"payload">>),
Msg = emqx_message:remove_header(x, Msg),
?assertEqual(undefined, emqx_message:get_header(a, Msg)),
Msg1 = emqx_message:set_header(a, 1, Msg),
Msg2 = emqx_message:set_header(b, 2, Msg1),
Msg3 = emqx_message:set_header(c, 3, Msg2),
@ -95,11 +136,8 @@ t_undefined_headers(_) ->
t_format(_) ->
Msg = emqx_message:make(<<"clientid">>, <<"topic">>, <<"payload">>),
io:format("~s~n", [emqx_message:format(Msg)]),
Msg1 = #message{id = <<"id">>,
qos = ?QOS_0,
flags = undefined,
headers = undefined
},
Msg1 = emqx_message:set_header('Subscription-Identifier', 1,
emqx_message:set_flag(dup, Msg)),
io:format("~s~n", [emqx_message:format(Msg1)]).
t_is_expired(_) ->
@ -117,28 +155,49 @@ t_is_expired(_) ->
% t_to_list(_) ->
% error('TODO').
t_to_packet(_) ->
Pkt = #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH,
qos = ?QOS_0,
retain = false,
dup = false},
Pkt = #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH,
qos = ?QOS_0,
retain = false,
dup = false
},
variable = #mqtt_packet_publish{topic_name = <<"topic">>,
packet_id = 10,
properties = #{}},
payload = <<"payload">>},
properties = undefined
},
payload = <<"payload">>
},
Msg = emqx_message:make(<<"clientid">>, ?QOS_0, <<"topic">>, <<"payload">>),
?assertEqual(Pkt, emqx_message:to_packet(10, Msg)).
t_to_packet_with_props(_) ->
Props = #{'Subscription-Identifier' => 1},
Pkt = #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH,
qos = ?QOS_0,
retain = false,
dup = false
},
variable = #mqtt_packet_publish{topic_name = <<"topic">>,
packet_id = 10,
properties = Props
},
payload = <<"payload">>
},
Msg = emqx_message:make(<<"clientid">>, ?QOS_0, <<"topic">>, <<"payload">>),
Msg1 = emqx_message:set_header('Subscription-Identifier', 1, Msg),
?assertEqual(Pkt, emqx_message:to_packet(10, Msg1)).
t_to_map(_) ->
Msg = emqx_message:make(<<"clientid">>, ?QOS_1, <<"topic">>, <<"payload">>),
List = [{id, emqx_message:id(Msg)},
{qos, ?QOS_1},
{from, <<"clientid">>},
{flags, #{dup => false}},
{headers, #{}},
{flags, undefined},
{headers, undefined},
{topic, <<"topic">>},
{payload, <<"payload">>},
{timestamp, emqx_message:timestamp(Msg)}],
?assertEqual(List, emqx_message:to_list(Msg)),
?assertEqual(maps:from_list(List), emqx_message:to_map(Msg)).

View File

@ -66,8 +66,7 @@ t_pipeline(_) ->
fun(I, St) -> {ok, I*2, St*2} end],
?assertEqual({ok, 4, 6}, emqx_misc:pipeline(Funs, 1, 1)),
?assertEqual({error, undefined, 1}, emqx_misc:pipeline([fun(_I) -> {error, undefined} end], 1, 1)),
?assertEqual({error, undefined, 2}, emqx_misc:pipeline([fun(_I, _St) -> {error, undefined, 2} end], 1, 1)),
?assertEqual({error, undefined, 1}, emqx_misc:pipeline([fun(_I, _St) -> erlang:error(undefined) end], 1, 1)).
?assertEqual({error, undefined, 2}, emqx_misc:pipeline([fun(_I, _St) -> {error, undefined, 2} end], 1, 1)).
t_start_timer(_) ->
TRef = emqx_misc:start_timer(1, tmsg),

View File

@ -32,10 +32,8 @@ init_per_suite(Config) ->
end_per_suite(_Config) ->
emqx_ct_helpers:stop_apps([emqx]).
t_load(_) ->
?assertEqual(ok, emqx_mod_subscription:load([{<<"connected/%c/%u">>, ?QOS_0}])).
t_on_client_connected(_) ->
?assertEqual(ok, emqx_mod_subscription:load([{<<"connected/%c/%u">>, ?QOS_0}])),
{ok, C} = emqtt:start_link([{host, "localhost"},
{clientid, "myclient"},
{username, "admin"}]),
@ -44,11 +42,20 @@ t_on_client_connected(_) ->
{ok, #{topic := Topic, payload := Payload}} = receive_publish(100),
?assertEqual(<<"connected/myclient/admin">>, Topic),
?assertEqual(<<"Hello world">>, Payload),
ok = emqtt:disconnect(C).
t_unload(_) ->
ok = emqtt:disconnect(C),
?assertEqual(ok, emqx_mod_subscription:unload([{<<"connected/%c/%u">>, ?QOS_0}])).
t_on_undefined_client_connected(_) ->
?assertEqual(ok, emqx_mod_subscription:load([{<<"connected/undefined">>, ?QOS_0}])),
{ok, C} = emqtt:start_link([{host, "localhost"}]),
{ok, _} = emqtt:connect(C),
emqtt:publish(C, <<"connected/undefined">>, <<"Hello world">>, ?QOS_0),
{ok, #{topic := Topic, payload := Payload}} = receive_publish(100),
?assertEqual(<<"connected/undefined">>, Topic),
?assertEqual(<<"Hello world">>, Payload),
ok = emqtt:disconnect(C),
?assertEqual(ok, emqx_mod_subscription:unload([{<<"connected/undefined">>, ?QOS_0}])).
%%--------------------------------------------------------------------
%% Internal functions
%%--------------------------------------------------------------------

View File

@ -14,23 +14,40 @@
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_msg_expiry_interval_SUITE).
-module(emqx_mqtt_SUITE).
-compile(export_all).
-compile(nowarn_export_all).
-include("emqx.hrl").
-include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl").
-define(STATS_KYES, [recv_pkt, recv_msg, send_pkt, send_msg,
recv_oct, recv_cnt, send_oct, send_cnt,
send_pend
]).
all() -> emqx_ct:all(?MODULE).
init_per_suite(Config) ->
emqx_ct_helpers:boot_modules(all),
emqx_ct_helpers:boot_modules(all),
emqx_ct_helpers:start_apps([]),
Config.
end_per_suite(_Config) ->
emqx_ct_helpers:stop_apps([]).
t_conn_stats(_) ->
with_client(fun(CPid) ->
Stats = emqx_connection:stats(CPid),
ct:pal("==== stats: ~p", [Stats]),
[?assert(proplists:get_value(Key, Stats) >= 0) || Key <- ?STATS_KYES]
end, []).
t_tcp_sock_passive(_) ->
with_client(fun(CPid) -> CPid ! {tcp_passive, sock} end, []).
t_message_expiry_interval_1(_) ->
ClientA = message_expiry_interval_init(),
[message_expiry_interval_exipred(ClientA, QoS) || QoS <- [0,1,2]],
@ -92,3 +109,16 @@ message_expiry_interval_not_exipred(ClientA, QoS) ->
ct:fail(no_publish_received)
end,
emqtt:stop(ClientB1).
with_client(TestFun, _Options) ->
ClientId = <<"t_conn">>,
{ok, C} = emqtt:start_link([{clientid, ClientId}]),
{ok, _} = emqtt:connect(C),
timer:sleep(50),
case emqx_cm:lookup_channels(ClientId) of
[] -> ct:fail({client_not_started, ClientId});
[ChanPid] ->
TestFun(ChanPid),
emqtt:stop(C)
end.

View File

@ -24,12 +24,6 @@
all() -> emqx_ct:all(?MODULE).
% t_get_caps(_) ->
% error('TODO').
% t_default(_) ->
% error('TODO').
t_check_pub(_) ->
PubCaps = #{max_qos_allowed => ?QOS_1,
retain_available => false

View File

@ -153,7 +153,7 @@ t_check_connect(_) ->
t_from_to_message(_) ->
ExpectedMsg = emqx_message:make(<<"clientid">>, ?QOS_0, <<"topic">>, <<"payload">>),
ExpectedMsg1 = emqx_message:set_flag(retain, false, ExpectedMsg),
ExpectedMsg1 = emqx_message:set_flags(#{dup => false, retain => false}, ExpectedMsg),
ExpectedMsg2 = emqx_message:set_headers(#{peerhost => {127,0,0,1},
protocol => mqtt,
username => <<"test">>

View File

@ -21,20 +21,30 @@
-include_lib("eunit/include/eunit.hrl").
-define(ROUTER_HELPER, emqx_router_helper).
all() -> emqx_ct:all(?MODULE).
init_per_testcase(_TestCase, Config) ->
emqx_ct_helpers:start_apps([emqx]),
Config.
end_per_testcase(_TestCase, Config) ->
Config.
% t_mnesia(_) ->
% error('TODO').
t_monitor(_) ->
ok = emqx_router_helper:monitor({undefined, node()}),
emqx_router_helper:monitor(undefined).
% t_monitor(_) ->
% error('TODO').
% t_stats_fun(_) ->
% error('TODO').
t_mnesia(_) ->
?ROUTER_HELPER ! {mnesia_table_event, {delete, {emqx_routing_node, node()}, undefined}},
?ROUTER_HELPER ! {mnesia_table_event, testing},
?ROUTER_HELPER ! {mnesia_table_event, {write, {emqx_routing_node, node()}, undefined}},
?ROUTER_HELPER ! {membership, testing},
?ROUTER_HELPER ! {membership, {mnesia, down, node()}},
ct:sleep(200).
t_message(_) ->
?ROUTER_HELPER ! testing,
gen_server:cast(?ROUTER_HELPER, testing),
gen_server:call(?ROUTER_HELPER, testing).

View File

@ -23,24 +23,27 @@
-include_lib("proper/include/proper.hrl").
-include_lib("eunit/include/eunit.hrl").
-import(emqx_session, [set_field/3]).
all() -> emqx_ct:all(?MODULE).
%%--------------------------------------------------------------------
%% CT callbacks
%%--------------------------------------------------------------------
init_per_testcase(_TestCase, Config) ->
%% Meck Broker
ok = meck:new(emqx_broker, [passthrough, no_history]),
ok = meck:new(emqx_hooks, [passthrough, no_history]),
init_per_suite(Config) ->
%% Broker
ok = meck:new(emqx_broker, [passthrough, no_history, no_link]),
ok = meck:new(emqx_hooks, [passthrough, no_history, no_link]),
ok = meck:expect(emqx_hooks, run, fun(_Hook, _Args) -> ok end),
Config.
end_per_testcase(_TestCase, Config) ->
end_per_suite(_Config) ->
ok = meck:unload(emqx_broker),
ok = meck:unload(emqx_hooks),
ok = meck:unload(emqx_hooks).
init_per_testcase(_TestCase, Config) ->
Config.
end_per_testcase(_TestCase, Config) ->
Config.
%%--------------------------------------------------------------------
@ -57,6 +60,7 @@ t_session_init(_) ->
?assertEqual(64, emqx_session:info(inflight_max, Session)),
?assertEqual(1, emqx_session:info(next_pkt_id, Session)),
?assertEqual(0, emqx_session:info(retry_interval, Session)),
?assertEqual(0, emqx_mqueue:len(emqx_session:info(mqueue, Session))),
?assertEqual(0, emqx_session:info(awaiting_rel_cnt, Session)),
?assertEqual(100, emqx_session:info(awaiting_rel_max, Session)),
?assertEqual(300, emqx_session:info(await_rel_timeout, Session)),
@ -86,12 +90,11 @@ t_session_stats(_) ->
}, maps:from_list(Stats)).
%%--------------------------------------------------------------------
%% Test cases for pub/sub
%% Test cases for sub/unsub
%%--------------------------------------------------------------------
t_subscribe(_) ->
ok = meck:expect(emqx_broker, subscribe, fun(_, _, _) -> ok end),
ok = meck:expect(emqx_broker, set_subopts, fun(_, _) -> ok end),
{ok, Session} = emqx_session:subscribe(
clientinfo(), <<"#">>, subopts(), session()),
?assertEqual(1, emqx_session:info(subscriptions_cnt, Session)).
@ -101,110 +104,217 @@ t_is_subscriptions_full_false(_) ->
?assertNot(emqx_session:is_subscriptions_full(Session)).
t_is_subscriptions_full_true(_) ->
ok = meck:expect(emqx_broker, subscribe, fun(_, _, _) -> ok end),
Session = session(#{max_subscriptions => 1}),
?assertNot(emqx_session:is_subscriptions_full(Session)),
Subs = #{<<"t1">> => subopts(), <<"t2">> => subopts()},
NSession = set_field(subscriptions, Subs, Session),
?assert(emqx_session:is_subscriptions_full(NSession)).
{ok, Session1} = emqx_session:subscribe(
clientinfo(), <<"t1">>, subopts(), Session),
?assert(emqx_session:is_subscriptions_full(Session1)),
{error, ?RC_QUOTA_EXCEEDED} =
emqx_session:subscribe(clientinfo(), <<"t2">>, subopts(), Session1).
t_unsubscribe(_) ->
ok = meck:expect(emqx_broker, unsubscribe, fun(_) -> ok end),
Session = session(#{subscriptions => #{<<"#">> => subopts()}}),
{ok, NSession} = emqx_session:unsubscribe(clientinfo(), <<"#">>, Session),
Error = emqx_session:unsubscribe(clientinfo(), <<"#">>, NSession),
?assertEqual({error, ?RC_NO_SUBSCRIPTION_EXISTED}, Error).
t_publish_qos2(_) ->
ok = meck:expect(emqx_broker, publish, fun(_) -> [] end),
Msg = emqx_message:make(test, ?QOS_2, <<"t">>, <<"payload">>),
{ok, [], Session} = emqx_session:publish(1, Msg, session()),
?assertEqual(1, emqx_session:info(awaiting_rel_cnt, Session)).
t_publish_qos1(_) ->
ok = meck:expect(emqx_broker, publish, fun(_) -> [] end),
Msg = emqx_message:make(test, ?QOS_1, <<"t">>, <<"payload">>),
{ok, [], _Session} = emqx_session:publish(1, Msg, session()).
{ok, Session1} = emqx_session:unsubscribe(clientinfo(), <<"#">>, Session),
{error, ?RC_NO_SUBSCRIPTION_EXISTED} =
emqx_session:unsubscribe(clientinfo(), <<"#">>, Session1).
t_publish_qos0(_) ->
ok = meck:expect(emqx_broker, publish, fun(_) -> [] end),
Msg = emqx_message:make(test, ?QOS_1, <<"t">>, <<"payload">>),
{ok, [], _Session} = emqx_session:publish(0, Msg, session()).
Msg = emqx_message:make(clientid, ?QOS_0, <<"t">>, <<"payload">>),
{ok, [], Session} = emqx_session:publish(1, Msg, Session = session()),
{ok, [], Session} = emqx_session:publish(undefined, Msg, Session).
t_publish_qos1(_) ->
ok = meck:expect(emqx_broker, publish, fun(_) -> [] end),
Msg = emqx_message:make(clientid, ?QOS_1, <<"t">>, <<"payload">>),
{ok, [], Session} = emqx_session:publish(1, Msg, Session = session()),
{ok, [], Session} = emqx_session:publish(2, Msg, Session).
t_publish_qos2(_) ->
ok = meck:expect(emqx_broker, publish, fun(_) -> [] end),
Msg = emqx_message:make(clientid, ?QOS_2, <<"t">>, <<"payload">>),
{ok, [], Session} = emqx_session:publish(1, Msg, session()),
?assertEqual(1, emqx_session:info(awaiting_rel_cnt, Session)),
{ok, Session1} = emqx_session:pubrel(1, Session),
?assertEqual(0, emqx_session:info(awaiting_rel_cnt, Session1)),
{error, ?RC_PACKET_IDENTIFIER_NOT_FOUND} = emqx_session:pubrel(1, Session1).
t_publish_qos2_with_error_return(_) ->
ok = meck:expect(emqx_broker, publish, fun(_) -> [] end),
Session = session(#{max_awaiting_rel => 2,
awaiting_rel => #{1 => ts(millisecond)}
}),
Msg = emqx_message:make(clientid, ?QOS_2, <<"t">>, <<"payload">>),
{error, ?RC_PACKET_IDENTIFIER_IN_USE} = emqx_session:publish(1, Msg, Session),
{ok, [], Session1} = emqx_session:publish(2, Msg, Session),
?assertEqual(2, emqx_session:info(awaiting_rel_cnt, Session1)),
{error, ?RC_RECEIVE_MAXIMUM_EXCEEDED} = emqx_session:publish(3, Msg, Session1).
t_is_awaiting_full_false(_) ->
?assertNot(emqx_session:is_awaiting_full(session(#{max_awaiting_rel => 0}))).
Session = session(#{max_awaiting_rel => 0}),
?assertNot(emqx_session:is_awaiting_full(Session)).
t_is_awaiting_full_true(_) ->
Session = session(#{max_awaiting_rel => 1,
awaiting_rel => #{1 => 1}
awaiting_rel => #{1 => ts(millisecond)}
}),
?assert(emqx_session:is_awaiting_full(Session)).
t_puback(_) ->
Msg = emqx_message:make(test, ?QOS_1, <<"t">>, <<>>),
Inflight = emqx_inflight:insert(1, {Msg, os:timestamp()}, emqx_inflight:new()),
Session = set_field(inflight, Inflight, session()),
{ok, Msg, NSession} = emqx_session:puback(1, Session),
?assertEqual(0, emqx_session:info(inflight_cnt, NSession)).
Inflight = emqx_inflight:insert(1, {Msg, ts(millisecond)}, emqx_inflight:new()),
Session = session(#{inflight => Inflight, mqueue => mqueue()}),
{ok, Msg, Session1} = emqx_session:puback(1, Session),
?assertEqual(0, emqx_session:info(inflight_cnt, Session1)).
t_puback_with_dequeue(_) ->
Msg1 = emqx_message:make(clientid, ?QOS_1, <<"t1">>, <<"payload1">>),
Inflight = emqx_inflight:insert(1, {Msg1, ts(millisecond)}, emqx_inflight:new()),
Msg2 = emqx_message:make(clientid, ?QOS_1, <<"t2">>, <<"payload2">>),
{_, Q} = emqx_mqueue:in(Msg2, mqueue(#{max_len => 10})),
Session = session(#{inflight => Inflight, mqueue => Q}),
{ok, Msg1, [{_, Msg3}], Session1} = emqx_session:puback(1, Session),
?assertEqual(1, emqx_session:info(inflight_cnt, Session1)),
?assertEqual(0, emqx_session:info(mqueue_len, Session1)),
?assertEqual(<<"t2">>, emqx_message:topic(Msg3)).
t_puback_error_packet_id_in_use(_) ->
Inflight = emqx_inflight:insert(1, {pubrel, os:timestamp()}, emqx_inflight:new()),
Session = set_field(inflight, Inflight, session()),
{error, ?RC_PACKET_IDENTIFIER_IN_USE} = emqx_session:puback(1, Session).
Inflight = emqx_inflight:insert(1, {pubrel, ts(millisecond)}, emqx_inflight:new()),
{error, ?RC_PACKET_IDENTIFIER_IN_USE} =
emqx_session:puback(1, session(#{inflight => Inflight})).
t_puback_error_packet_id_not_found(_) ->
{error, ?RC_PACKET_IDENTIFIER_NOT_FOUND} = emqx_session:puback(1, session()).
t_pubrec(_) ->
Msg = emqx_message:make(test, ?QOS_2, <<"t">>, <<>>),
Inflight = emqx_inflight:insert(2, {Msg, os:timestamp()}, emqx_inflight:new()),
Session = set_field(inflight, Inflight, session()),
{ok, Msg, NSession} = emqx_session:pubrec(2, Session),
?assertMatch([{pubrel, _}], emqx_inflight:values(emqx_session:info(inflight, NSession))).
Inflight = emqx_inflight:insert(2, {Msg, ts(millisecond)}, emqx_inflight:new()),
Session = session(#{inflight => Inflight}),
{ok, Msg, Session1} = emqx_session:pubrec(2, Session),
?assertMatch([{pubrel, _}], emqx_inflight:values(emqx_session:info(inflight, Session1))).
t_pubrec_packet_id_in_use_error(_) ->
Inflight = emqx_inflight:insert(1, {pubrel, ts()}, emqx_inflight:new()),
Session = set_field(inflight, Inflight, session()),
{error, ?RC_PACKET_IDENTIFIER_IN_USE} = emqx_session:puback(1, Session).
Inflight = emqx_inflight:insert(1, {pubrel, ts(millisecond)}, emqx_inflight:new()),
{error, ?RC_PACKET_IDENTIFIER_IN_USE} =
emqx_session:pubrec(1, session(#{inflight => Inflight})).
t_pubrec_packet_id_not_found_error(_) ->
{error, ?RC_PACKET_IDENTIFIER_NOT_FOUND} = emqx_session:pubrec(1, session()).
t_pubrel(_) ->
Session = set_field(awaiting_rel, #{1 => os:timestamp()}, session()),
{ok, NSession} = emqx_session:pubrel(1, Session),
?assertEqual(#{}, emqx_session:info(awaiting_rel, NSession)).
Session = session(#{awaiting_rel => #{1 => ts(millisecond)}}),
{ok, Session1} = emqx_session:pubrel(1, Session),
?assertEqual(#{}, emqx_session:info(awaiting_rel, Session1)).
t_pubrel_id_not_found(_) ->
t_pubrel_error_packetid_not_found(_) ->
{error, ?RC_PACKET_IDENTIFIER_NOT_FOUND} = emqx_session:pubrel(1, session()).
t_pubcomp(_) ->
Inflight = emqx_inflight:insert(2, {pubrel, os:timestamp()}, emqx_inflight:new()),
Session = emqx_session:set_field(inflight, Inflight, session()),
{ok, NSession} = emqx_session:pubcomp(2, Session),
?assertEqual(0, emqx_session:info(inflight_cnt, NSession)).
Inflight = emqx_inflight:insert(1, {pubrel, ts(millisecond)}, emqx_inflight:new()),
Session = session(#{inflight => Inflight}),
{ok, Session1} = emqx_session:pubcomp(1, Session),
?assertEqual(0, emqx_session:info(inflight_cnt, Session1)).
t_pubcomp_id_not_found(_) ->
{error, ?RC_PACKET_IDENTIFIER_NOT_FOUND} = emqx_session:pubcomp(2, session()).
t_pubcomp_error_packetid_in_use(_) ->
Msg = emqx_message:make(test, ?QOS_2, <<"t">>, <<>>),
Inflight = emqx_inflight:insert(1, {Msg, ts(millisecond)}, emqx_inflight:new()),
Session = session(#{inflight => Inflight}),
{error, ?RC_PACKET_IDENTIFIER_IN_USE} = emqx_session:pubcomp(1, Session).
t_pubcomp_error_packetid_not_found(_) ->
{error, ?RC_PACKET_IDENTIFIER_NOT_FOUND} = emqx_session:pubcomp(1, session()).
%%--------------------------------------------------------------------
%% Test cases for deliver/retry
%%--------------------------------------------------------------------
t_dequeue(_) ->
{ok, _Session} = emqx_session:dequeue(session()).
Q = mqueue(#{store_qos0 => true}),
{ok, Session} = emqx_session:dequeue(session(#{mqueue => Q})),
Msgs = [emqx_message:make(clientid, ?QOS_0, <<"t0">>, <<"payload">>),
emqx_message:make(clientid, ?QOS_1, <<"t1">>, <<"payload">>),
emqx_message:make(clientid, ?QOS_2, <<"t2">>, <<"payload">>)
],
Session1 = lists:foldl(fun emqx_session:enqueue/2, Session, Msgs),
{ok, [{undefined, Msg0}, {1, Msg1}, {2, Msg2}], Session2} =
emqx_session:dequeue(Session1),
?assertEqual(0, emqx_session:info(mqueue_len, Session2)),
?assertEqual(2, emqx_session:info(inflight_cnt, Session2)),
?assertEqual(<<"t0">>, emqx_message:topic(Msg0)),
?assertEqual(<<"t1">>, emqx_message:topic(Msg1)),
?assertEqual(<<"t2">>, emqx_message:topic(Msg2)).
t_deliver(_) ->
t_deliver_qos0(_) ->
ok = meck:expect(emqx_broker, subscribe, fun(_, _, _) -> ok end),
{ok, Session} = emqx_session:subscribe(
clientinfo(), <<"t0">>, subopts(), session()),
{ok, Session1} = emqx_session:subscribe(
clientinfo(), <<"t1">>, subopts(), Session),
Deliveries = [delivery(?QOS_0, T) || T <- [<<"t0">>, <<"t1">>]],
{ok, [{undefined, Msg1}, {undefined, Msg2}], Session1} =
emqx_session:deliver(Deliveries, Session1),
?assertEqual(<<"t0">>, emqx_message:topic(Msg1)),
?assertEqual(<<"t1">>, emqx_message:topic(Msg2)).
t_deliver_qos1(_) ->
ok = meck:expect(emqx_broker, subscribe, fun(_, _, _) -> ok end),
{ok, Session} = emqx_session:subscribe(
clientinfo(), <<"t1">>, subopts(#{qos => ?QOS_1}), session()),
Delivers = [delivery(?QOS_1, T) || T <- [<<"t1">>, <<"t2">>]],
{ok, [{1, Msg1}, {2, Msg2}], Session1} = emqx_session:deliver(Delivers, Session),
?assertEqual(2, emqx_session:info(inflight_cnt, Session1)),
?assertEqual(<<"t1">>, emqx_message:topic(Msg1)),
?assertEqual(<<"t2">>, emqx_message:topic(Msg2)),
{ok, Msg1, Session2} = emqx_session:puback(1, Session1),
?assertEqual(1, emqx_session:info(inflight_cnt, Session2)),
{ok, Msg2, Session3} = emqx_session:puback(2, Session2),
?assertEqual(0, emqx_session:info(inflight_cnt, Session3)).
t_deliver_qos2(_) ->
ok = meck:expect(emqx_broker, subscribe, fun(_, _, _) -> ok end),
Delivers = [delivery(?QOS_2, <<"t0">>), delivery(?QOS_2, <<"t1">>)],
{ok, [{1, Msg1}, {2, Msg2}], Session} =
emqx_session:deliver(Delivers, session()),
?assertEqual(2, emqx_session:info(inflight_cnt, Session)),
?assertEqual(<<"t0">>, emqx_message:topic(Msg1)),
?assertEqual(<<"t1">>, emqx_message:topic(Msg2)).
t_deliver_one_msg(_) ->
{ok, [{1, Msg}], Session} =
emqx_session:deliver([delivery(?QOS_1, <<"t1">>)], session()),
?assertEqual(1, emqx_session:info(inflight_cnt, Session)),
?assertEqual(<<"t1">>, emqx_message:topic(Msg)).
t_deliver_when_inflight_is_full(_) ->
Delivers = [delivery(?QOS_1, <<"t1">>), delivery(?QOS_2, <<"t2">>)],
{ok, Publishes, _Session} = emqx_session:deliver(Delivers, session()),
?assertEqual(2, length(Publishes)).
Session = session(#{inflight => emqx_inflight:new(1)}),
{ok, Publishes, Session1} = emqx_session:deliver(Delivers, Session),
?assertEqual(1, length(Publishes)),
?assertEqual(1, emqx_session:info(inflight_cnt, Session1)),
?assertEqual(1, emqx_session:info(mqueue_len, Session1)),
{ok, Msg1, [{2, Msg2}], Session2} = emqx_session:puback(1, Session1),
?assertEqual(1, emqx_session:info(inflight_cnt, Session2)),
?assertEqual(0, emqx_session:info(mqueue_len, Session2)),
?assertEqual(<<"t1">>, emqx_message:topic(Msg1)),
?assertEqual(<<"t2">>, emqx_message:topic(Msg2)).
t_enqueue(_) ->
Delivers = [delivery(?QOS_1, <<"t1">>), delivery(?QOS_2, <<"t2">>)],
Session = emqx_session:enqueue(Delivers, session()),
?assertEqual(2, emqx_session:info(mqueue_len, Session)).
%% store_qos0 = true
Session = emqx_session:enqueue([delivery(?QOS_0, <<"t0">>)], session()),
Session1 = emqx_session:enqueue([delivery(?QOS_1, <<"t1">>),
delivery(?QOS_2, <<"t2">>)], Session),
?assertEqual(3, emqx_session:info(mqueue_len, Session1)).
t_retry(_) ->
{ok, _Session} = emqx_session:retry(session()).
Delivers = [delivery(?QOS_1, <<"t1">>), delivery(?QOS_2, <<"t2">>)],
Session = session(#{retry_interval => 100}),
{ok, Pubs, Session1} = emqx_session:deliver(Delivers, Session),
ok = timer:sleep(200),
Msgs1 = [{I, emqx_message:set_flag(dup, Msg)} || {I, Msg} <- Pubs],
{ok, Msgs1, 100, Session2} = emqx_session:retry(Session1),
?assertEqual(2, emqx_session:info(inflight_cnt, Session2)).
%%--------------------------------------------------------------------
%% Test cases for takeover/resume
@ -220,22 +330,53 @@ t_resume(_) ->
Session = session(#{subscriptions => #{<<"t">> => ?DEFAULT_SUBOPTS}}),
ok = emqx_session:resume(<<"clientid">>, Session).
t_redeliver(_) ->
{ok, [], _Session} = emqx_session:redeliver(session()).
t_replay(_) ->
Delivers = [delivery(?QOS_1, <<"t1">>), delivery(?QOS_2, <<"t2">>)],
{ok, Pubs, Session1} = emqx_session:deliver(Delivers, session()),
Msg = emqx_message:make(clientid, ?QOS_1, <<"t1">>, <<"payload">>),
Session2 = emqx_session:enqueue(Msg, Session1),
Pubs1 = [{I, emqx_message:set_flag(dup, M)} || {I, M} <- Pubs],
{ok, ReplayPubs, Session3} = emqx_session:replay(Session2),
?assertEqual(Pubs1 ++ [{3, Msg}], ReplayPubs),
?assertEqual(3, emqx_session:info(inflight_cnt, Session3)).
t_expire(_) ->
{ok, _Session} = emqx_session:expire(awaiting_rel, session()).
t_expire_awaiting_rel(_) ->
{ok, Session} = emqx_session:expire(awaiting_rel, session()),
Timeout = emqx_session:info(await_rel_timeout, Session) * 1000,
Session1 = emqx_session:set_field(awaiting_rel, #{1 => Ts = ts(millisecond)}, Session),
{ok, Timeout, Session2} = emqx_session:expire(awaiting_rel, Session1),
?assertEqual(#{1 => Ts}, emqx_session:info(awaiting_rel, Session2)).
t_expire_awaiting_rel_all(_) ->
Session = session(#{awaiting_rel => #{1 => 1, 2 => 2}}),
{ok, Session1} = emqx_session:expire(awaiting_rel, Session),
?assertEqual(#{}, emqx_session:info(awaiting_rel, Session1)).
%%--------------------------------------------------------------------
%% CT for utility functions
%%--------------------------------------------------------------------
t_next_pakt_id(_) ->
Session = session(#{next_pkt_id => 16#FFFF}),
Session1 = emqx_session:next_pkt_id(Session),
?assertEqual(1, emqx_session:info(next_pkt_id, Session1)),
Session2 = emqx_session:next_pkt_id(Session1),
?assertEqual(2, emqx_session:info(next_pkt_id, Session2)).
%%--------------------------------------------------------------------
%% Helper functions
%%--------------------------------------------------------------------
mqueue() -> mqueue(#{}).
mqueue(Opts) ->
emqx_mqueue:init(maps:merge(#{max_len => 0, store_qos0 => false}, Opts)).
session() -> session(#{}).
session(InitFields) when is_map(InitFields) ->
maps:fold(fun(Field, Value, Session) ->
emqx_session:set_field(Field, Value, Session)
end,
emqx_session:init(#{zone => zone}, #{receive_maximum => 0}),
emqx_session:init(#{zone => channel}, #{receive_maximum => 0}),
InitFields).
@ -252,5 +393,8 @@ subopts(Init) ->
delivery(QoS, Topic) ->
{deliver, Topic, emqx_message:make(test, QoS, Topic, <<"payload">>)}.
ts() -> erlang:system_time(second).
ts(second) ->
erlang:system_time(second);
ts(millisecond) ->
erlang:system_time(millisecond).

View File

@ -41,7 +41,7 @@ init_per_suite(Config) ->
end_per_suite(_Config) ->
emqx_ct_helpers:stop_apps([]).
t_is_ack_required(_) ->
?assertEqual(false, emqx_shared_sub:is_ack_required(#message{headers = #{}})).

View File

@ -95,9 +95,6 @@ t_scheduler_usage(_Config) ->
t_get_memory(_Config) ->
emqx_vm:get_memory().
t_microsecs(_Config) ->
emqx_vm:microsecs().
t_schedulers(_Config) ->
emqx_vm:schedulers().

View File

@ -16,6 +16,7 @@
-module(emqx_ws_connection_SUITE).
-include("emqx.hrl").
-include("emqx_mqtt.hrl").
-include_lib("eunit/include/eunit.hrl").
@ -25,12 +26,15 @@
-import(emqx_ws_connection,
[ websocket_handle/2
, websocket_info/2
, websocket_close/2
]).
-define(STATS_KEYS, [recv_oct, recv_cnt, send_oct, send_cnt,
recv_pkt, recv_msg, send_pkt, send_msg
]).
-define(ws_conn, emqx_ws_connection).
all() -> emqx_ct:all(?MODULE).
%%--------------------------------------------------------------------
@ -38,184 +42,355 @@ all() -> emqx_ct:all(?MODULE).
%%--------------------------------------------------------------------
init_per_suite(Config) ->
Config.
end_per_suite(_Config) ->
ok.
init_per_testcase(_TestCase, Config) ->
%% Meck CowboyReq
ok = meck:new(cowboy_req, [passthrough, no_history]),
%% Mock cowboy_req
ok = meck:new(cowboy_req, [passthrough, no_history, no_link]),
ok = meck:expect(cowboy_req, peer, fun(_) -> {{127,0,0,1}, 3456} end),
ok = meck:expect(cowboy_req, sock, fun(_) -> {{127,0,0,1}, 8883} end),
ok = meck:expect(cowboy_req, sock, fun(_) -> {{127,0,0,1}, 18083} end),
ok = meck:expect(cowboy_req, cert, fun(_) -> undefined end),
ok = meck:expect(cowboy_req, parse_cookies, fun(_) -> undefined end),
%% Meck Channel
ok = meck:new(emqx_channel, [passthrough, no_history]),
%% Meck Metrics
ok = meck:new(emqx_metrics, [passthrough, no_history]),
ok = meck:expect(cowboy_req, parse_cookies, fun(_) -> error(badarg) end),
%% Mock emqx_zone
ok = meck:new(emqx_zone, [passthrough, no_history, no_link]),
ok = meck:expect(emqx_zone, oom_policy,
fun(_) -> #{max_heap_size => 838860800,
message_queue_len => 8000
}
end),
%% Mock emqx_access_control
ok = meck:new(emqx_access_control, [passthrough, no_history, no_link]),
ok = meck:expect(emqx_access_control, check_acl, fun(_, _, _) -> allow end),
%% Mock emqx_hooks
ok = meck:new(emqx_hooks, [passthrough, no_history, no_link]),
ok = meck:expect(emqx_hooks, run, fun(_Hook, _Args) -> ok end),
ok = meck:expect(emqx_hooks, run_fold, fun(_Hook, _Args, Acc) -> Acc end),
%% Mock emqx_broker
ok = meck:new(emqx_broker, [passthrough, no_history, no_link]),
ok = meck:expect(emqx_broker, subscribe, fun(_, _, _) -> ok end),
ok = meck:expect(emqx_broker, publish, fun(#message{topic = Topic}) ->
[{node(), Topic, 1}]
end),
ok = meck:expect(emqx_broker, unsubscribe, fun(_) -> ok end),
%% Mock emqx_metrics
ok = meck:new(emqx_metrics, [passthrough, no_history, no_link]),
ok = meck:expect(emqx_metrics, inc, fun(_) -> ok end),
ok = meck:expect(emqx_metrics, inc, fun(_, _) -> ok end),
ok = meck:expect(emqx_metrics, inc_recv, fun(_) -> ok end),
ok = meck:expect(emqx_metrics, inc_sent, fun(_) -> ok end),
Config.
end_per_suite(_Config) ->
lists:foreach(fun meck:unload/1,
[cowboy_req,
emqx_zone,
emqx_access_control,
emqx_broker,
emqx_hooks,
emqx_metrics
]).
init_per_testcase(_TestCase, Config) ->
Config.
end_per_testcase(_TestCase, Config) ->
ok = meck:unload(cowboy_req),
ok = meck:unload(emqx_channel),
ok = meck:unload(emqx_metrics),
Config.
%%--------------------------------------------------------------------
%% Test Cases
%%--------------------------------------------------------------------
%%TODO:...
t_ws_conn_init(_) ->
with_ws_conn(fun(_WsConn) -> ok end).
t_info(_) ->
WsPid = spawn(fun() ->
receive {call, From, info} ->
gen_server:reply(From, ?ws_conn:info(st()))
end
end),
#{sockinfo := SockInfo} = ?ws_conn:call(WsPid, info),
#{socktype := ws,
active_n := 100,
peername := {{127,0,0,1}, 3456},
sockname := {{127,0,0,1}, 18083},
sockstate := running
} = SockInfo.
t_ws_conn_info(_) ->
with_ws_conn(fun(WsConn) ->
#{sockinfo := SockInfo} = emqx_ws_connection:info(WsConn),
#{socktype := ws,
peername := {{127,0,0,1}, 3456},
sockname := {{127,0,0,1}, 8883},
sockstate := running} = SockInfo
end).
t_info_limiter(_) ->
St = st(#{limiter => emqx_limiter:init([])}),
?assertEqual(undefined, ?ws_conn:info(limiter, St)).
t_websocket_init(_) ->
with_ws_conn(fun(WsConn) ->
#{sockinfo := SockInfo} = emqx_ws_connection:info(WsConn),
#{socktype := ws,
peername := {{127,0,0,1}, 3456},
sockname := {{127,0,0,1}, 8883},
sockstate := running
} = SockInfo
end).
t_info_channel(_) ->
#{conn_state := connected} = ?ws_conn:info(channel, st()).
t_info_gc_state(_) ->
GcSt = emqx_gc:init(#{count => 10, bytes => 1000}),
GcInfo = ?ws_conn:info(gc_state, st(#{gc_state => GcSt})),
?assertEqual(#{cnt => {10,10}, oct => {1000,1000}}, GcInfo).
t_info_postponed(_) ->
?assertEqual([], ?ws_conn:info(postponed, st())),
St = ?ws_conn:postpone({active, false}, st()),
?assertEqual([{active, false}], ?ws_conn:info(postponed, St)).
t_stats(_) ->
WsPid = spawn(fun() ->
receive {call, From, stats} ->
gen_server:reply(From, ?ws_conn:stats(st()))
end
end),
Stats = ?ws_conn:call(WsPid, stats),
[{recv_oct, 0}, {recv_cnt, 0}, {send_oct, 0}, {send_cnt, 0},
{recv_pkt, 0}, {recv_msg, 0}, {send_pkt, 0}, {send_msg, 0}|_] = Stats.
t_call(_) ->
Info = ?ws_conn:info(st()),
WsPid = spawn(fun() ->
receive {call, From, info} -> gen_server:reply(From, Info) end
end),
?assertEqual(Info, ?ws_conn:call(WsPid, info)).
t_init(_) ->
Opts = [{idle_timeout, 300000}],
WsOpts = #{compress => false,
deflate_opts => #{},
max_frame_size => infinity,
idle_timeout => 300000
},
ok = meck:expect(cowboy_req, parse_header, fun(_, req) -> undefined end),
{cowboy_websocket, req, [req, Opts], WsOpts} = ?ws_conn:init(req, Opts),
ok = meck:expect(cowboy_req, parse_header, fun(_, req) -> [<<"mqtt">>] end),
ok = meck:expect(cowboy_req, set_resp_header, fun(_, <<"mqtt">>, req) -> resp end),
{cowboy_websocket, resp, [req, Opts], WsOpts} = ?ws_conn:init(req, Opts).
t_websocket_handle_binary(_) ->
with_ws_conn(fun(WsConn) ->
{ok, _} = websocket_handle({binary, [<<>>]}, WsConn)
end).
{ok, _} = websocket_handle({binary, <<>>}, st()),
{ok, _} = websocket_handle({binary, [<<>>]}, st()),
{ok, _} = websocket_handle({binary, <<192,0>>}, st()),
receive {incoming, ?PACKET(?PINGREQ)} -> ok
after 0 -> error(expect_incoming_pingreq)
end.
t_websocket_handle_ping_pong(_) ->
with_ws_conn(fun(WsConn) ->
{ok, WsConn} = websocket_handle(ping, WsConn),
{ok, WsConn} = websocket_handle(pong, WsConn),
{ok, WsConn} = websocket_handle({ping, <<>>}, WsConn),
{ok, WsConn} = websocket_handle({pong, <<>>}, WsConn)
end).
t_websocket_handle_ping(_) ->
{ok, St} = websocket_handle(ping, St = st()),
{ok, St} = websocket_handle({ping, <<>>}, St).
t_websocket_handle_pong(_) ->
{ok, St} = websocket_handle(pong, St = st()),
{ok, St} = websocket_handle({pong, <<>>}, St).
t_websocket_handle_bad_frame(_) ->
with_ws_conn(fun(WsConn) ->
{stop, WsConn1} = websocket_handle({badframe, <<>>}, WsConn),
?assertEqual({shutdown, unexpected_ws_frame}, stop_reason(WsConn1))
end).
{[{shutdown, unexpected_ws_frame}], _St} = websocket_handle({badframe, <<>>}, st()).
t_websocket_info_call(_) ->
with_ws_conn(fun(WsConn) ->
From = {make_ref(), self()},
Call = {call, From, badreq},
websocket_info(Call, WsConn)
end).
From = {make_ref(), self()},
Call = {call, From, badreq},
{ok, _St} = websocket_info(Call, st()).
t_websocket_info_rate_limit(_) ->
{ok, _} = websocket_info({cast, rate_limit}, st()),
ok = timer:sleep(1),
receive
{check_gc, Stats} ->
?assertEqual(#{cnt => 0, oct => 0}, Stats)
after 0 -> error(expect_check_gc)
end.
t_websocket_info_cast(_) ->
ok = meck:expect(emqx_channel, handle_info, fun(_Msg, Channel) -> {ok, Channel} end),
with_ws_conn(fun(WsConn) -> websocket_info({cast, msg}, WsConn) end).
{ok, _St} = websocket_info({cast, msg}, st()).
t_websocket_info_incoming(_) ->
ok = meck:expect(emqx_channel, handle_in, fun(_Packet, Channel) -> {ok, Channel} end),
with_ws_conn(fun(WsConn) ->
Connect = ?CONNECT_PACKET(
#mqtt_packet_connect{proto_ver = ?MQTT_PROTO_V5,
proto_name = <<"MQTT">>,
clientid = <<>>,
clean_start = true,
keepalive = 60}),
{ok, WsConn1} = websocket_info({incoming, Connect}, WsConn),
Publish = ?PUBLISH_PACKET(?QOS_1, <<"t">>, 1, <<"payload">>),
{ok, _WsConn2} = websocket_info({incoming, Publish}, WsConn1)
end).
ConnPkt = #mqtt_packet_connect{
proto_name = <<"MQTT">>,
proto_ver = ?MQTT_PROTO_V5,
is_bridge = false,
clean_start = true,
keepalive = 60,
properties = undefined,
clientid = <<"clientid">>,
username = <<"username">>,
password = <<"passwd">>
},
{ok, St1} = websocket_info({incoming, ?CONNECT_PACKET(ConnPkt)}, st()),
% ?assertEqual(<<224,2,130,0>>, iolist_to_binary(IoData1)),
%% PINGREQ
{[{binary, IoData2}], St2} =
websocket_info({incoming, ?PACKET(?PINGREQ)}, St1),
?assertEqual(<<208,0>>, iolist_to_binary(IoData2)),
%% PUBLISH
Publish = ?PUBLISH_PACKET(?QOS_1, <<"t">>, 1, <<"payload">>),
{[{binary, IoData3}], _St3} =
websocket_info({incoming, Publish}, St2),
?assertEqual(<<64,4,0,1,0,0>>, iolist_to_binary(IoData3)).
t_websocket_info_check_gc(_) ->
Stats = #{cnt => 10, oct => 1000},
{ok, _St} = websocket_info({check_gc, Stats}, st()).
t_websocket_info_deliver(_) ->
with_ws_conn(fun(WsConn) ->
ok = meck:expect(emqx_channel, handle_deliver,
fun(Delivers, Channel) ->
Packets = [emqx_message:to_packet(1, Msg) || {deliver, _, Msg} <- Delivers],
{ok, {outgoing, Packets}, Channel}
end),
Deliver = {deliver, <<"#">>, emqx_message:make(<<"topic">>, <<"payload">>)},
{reply, {binary, _Data}, _WsConn1} = websocket_info(Deliver, WsConn)
end).
Msg0 = emqx_message:make(clientid, ?QOS_0, <<"t">>, <<"">>),
Msg1 = emqx_message:make(clientid, ?QOS_1, <<"t">>, <<"">>),
self() ! {deliver, <<"#">>, Msg1},
{ok, _St} = websocket_info({deliver, <<"#">>, Msg0}, st()).
% ?assertEqual(<<48,3,0,1,116,50,5,0,1,116,0,1>>, iolist_to_binary(IoData)).
t_websocket_info_timeout(_) ->
with_ws_conn(fun(WsConn) ->
websocket_info({timeout, make_ref(), keepalive}, WsConn),
websocket_info({timeout, make_ref(), emit_stats}, WsConn),
websocket_info({timeout, make_ref(), retry_delivery}, WsConn)
end).
t_websocket_info_timeout_limiter(_) ->
Ref = make_ref(),
Event = {timeout, Ref, limit_timeout},
{[{active, true}], St} = websocket_info(Event, st(#{limit_timer => Ref})),
?assertEqual([], ?ws_conn:info(postponed, St)).
t_websocket_info_timeout_keepalive(_) ->
{ok, _St} = websocket_info({timeout, make_ref(), keepalive}, st()).
t_websocket_info_timeout_emit_stats(_) ->
Ref = make_ref(),
St = st(#{stats_timer => Ref}),
{ok, St1} = websocket_info({timeout, Ref, emit_stats}, St),
?assertEqual(undefined, ?ws_conn:info(stats_timer, St1)).
t_websocket_info_timeout_retry(_) ->
{ok, _St} = websocket_info({timeout, make_ref(), retry_delivery}, st()).
t_websocket_info_close(_) ->
with_ws_conn(fun(WsConn) ->
{stop, WsConn1} = websocket_info({close, sock_error}, WsConn),
?assertEqual({shutdown, sock_error}, stop_reason(WsConn1))
end).
{[close], _St} = websocket_info({close, sock_error}, st()).
t_websocket_info_shutdown(_) ->
with_ws_conn(fun(WsConn) ->
{stop, WsConn1} = websocket_info({shutdown, reason}, WsConn),
?assertEqual({shutdown, reason}, stop_reason(WsConn1))
end).
{[{shutdown, reason}], _St} = websocket_info({shutdown, reason}, st()).
t_websocket_info_stop(_) ->
with_ws_conn(fun(WsConn) ->
{stop, WsConn1} = websocket_info({stop, normal}, WsConn),
?assertEqual(normal, stop_reason(WsConn1))
end).
{[{shutdown, normal}], _St} = websocket_info({stop, normal}, st()).
t_websocket_close(_) ->
ok = meck:expect(emqx_channel, handle_info,
fun({sock_closed, badframe}, Channel) ->
{shutdown, sock_closed, Channel}
end),
with_ws_conn(fun(WsConn) ->
{stop, WsConn1} = emqx_ws_connection:websocket_close(badframe, WsConn),
?assertEqual(sock_closed, stop_reason(WsConn1))
end).
{[{shutdown, badframe}], _St} = websocket_close(badframe, st()).
t_handle_call(_) ->
with_ws_conn(fun(WsConn) -> ok end).
t_handle_info_connack(_) ->
ConnAck = ?CONNACK_PACKET(?RC_SUCCESS),
{[{binary, IoData}], _St} =
?ws_conn:handle_info({connack, ConnAck}, st()),
?assertEqual(<<32,2,0,0>>, iolist_to_binary(IoData)).
t_handle_info(_) ->
with_ws_conn(fun(WsConn) -> ok end).
t_handle_info_close(_) ->
{[close], _St} = ?ws_conn:handle_info({close, protocol_error}, st()).
t_handle_timeout(_) ->
with_ws_conn(fun(WsConn) -> ok end).
t_handle_info_event(_) ->
ok = meck:new(emqx_cm, [passthrough, no_history]),
ok = meck:expect(emqx_cm, register_channel, fun(_,_,_) -> ok end),
ok = meck:expect(emqx_cm, connection_closed, fun(_) -> true end),
{ok, _} = ?ws_conn:handle_info({event, connected}, st()),
{ok, _} = ?ws_conn:handle_info({event, disconnected}, st()),
{ok, _} = ?ws_conn:handle_info({event, updated}, st()),
ok = meck:unload(emqx_cm).
t_handle_timeout_idle_timeout(_) ->
TRef = make_ref(),
St = st(#{idle_timer => TRef}),
{[{shutdown, idle_timeout}], _St} = ?ws_conn:handle_timeout(TRef, idle_timeout, St).
t_handle_timeout_keepalive(_) ->
{ok, _St} = ?ws_conn:handle_timeout(make_ref(), keepalive, st()).
t_handle_timeout_emit_stats(_) ->
TRef = make_ref(),
{ok, St} = ?ws_conn:handle_timeout(
TRef, emit_stats, st(#{stats_timer => TRef})),
?assertEqual(undefined, ?ws_conn:info(stats_timer, St)).
t_ensure_rate_limit(_) ->
Limiter = emqx_limiter:init([{pub_limit, {1, 10}},
{rate_limit, {100, 1000}}
]),
St = st(#{limiter => Limiter}),
St1 = ?ws_conn:ensure_rate_limit(#{cnt => 0, oct => 0}, St),
St2 = ?ws_conn:ensure_rate_limit(#{cnt => 11, oct => 1200}, St1),
?assertEqual(blocked, ?ws_conn:info(sockstate, St2)),
?assertEqual([{active, false}], ?ws_conn:info(postponed, St2)).
t_parse_incoming(_) ->
with_ws_conn(fun(WsConn) -> ok end).
St = ?ws_conn:parse_incoming(<<48,3>>, st()),
St1 = ?ws_conn:parse_incoming(<<0,1,116>>, St),
Packet = ?PUBLISH_PACKET(?QOS_0, <<"t">>, undefined, <<>>),
[{incoming, Packet}] = ?ws_conn:info(postponed, St1).
t_handle_incoming(_) ->
with_ws_conn(fun(WsConn) -> ok end).
t_parse_incoming_frame_error(_) ->
St = ?ws_conn:parse_incoming(<<3,2,1,0>>, st()),
FrameError = {frame_error, function_clause},
[{incoming, FrameError}] = ?ws_conn:info(postponed, St).
t_handle_return(_) ->
with_ws_conn(fun(WsConn) -> ok end).
t_handle_incomming_frame_error(_) ->
FrameError = {frame_error, bad_qos},
Serialize = emqx_frame:serialize_fun(#{version => 5, max_size => 16#FFFF}),
{ok, _St} = ?ws_conn:handle_incoming(FrameError, st(#{serialize => Serialize})).
% ?assertEqual(<<224,2,129,0>>, iolist_to_binary(IoData)).
t_handle_outgoing(_) ->
with_ws_conn(fun(WsConn) -> ok end).
Packets = [?PUBLISH_PACKET(?QOS_1, <<"t1">>, 1, <<"payload">>),
?PUBLISH_PACKET(?QOS_2, <<"t2">>, 2, <<"payload">>)
],
{{binary, IoData}, _St} = ?ws_conn:handle_outgoing(Packets, st()),
?assert(is_binary(iolist_to_binary(IoData))).
t_run_gc(_) ->
GcSt = emqx_gc:init(#{count => 10, bytes => 100}),
WsSt = st(#{gc_state => GcSt}),
?ws_conn:run_gc(#{cnt => 100, oct => 10000}, WsSt).
t_check_oom(_) ->
%%Policy = #{max_heap_size => 10, message_queue_len => 10},
%%meck:expect(emqx_zone, oom_policy, fun(_) -> Policy end),
_St = ?ws_conn:check_oom(st()),
ok = timer:sleep(10).
%%receive {shutdown, proc_heap_too_large} -> ok
%%after 0 -> error(expect_shutdown)
%%end.
t_enqueue(_) ->
Packet = ?PUBLISH_PACKET(?QOS_0),
St = ?ws_conn:enqueue(Packet, st()),
[Packet] = ?ws_conn:info(postponed, St).
t_shutdown(_) ->
{[{shutdown, closed}], _St} = ?ws_conn:shutdown(closed, st()).
%%--------------------------------------------------------------------
%% Helper functions
%%--------------------------------------------------------------------
with_ws_conn(TestFun) ->
with_ws_conn(TestFun, []).
st() -> st(#{}).
st(InitFields) when is_map(InitFields) ->
{ok, St, _} = ?ws_conn:websocket_init([req, [{zone, external}]]),
maps:fold(fun(N, V, S) -> ?ws_conn:set_field(N, V, S) end,
?ws_conn:set_field(channel, channel(), St),
InitFields
).
with_ws_conn(TestFun, Opts) ->
{ok, WsConn, _} = emqx_ws_connection:websocket_init(
[req, emqx_misc:merge_opts([{zone, external}], Opts)]),
TestFun(WsConn).
stop_reason(WsConn) ->
emqx_ws_connection:info(stop_reason, WsConn).
channel() -> channel(#{}).
channel(InitFields) ->
ConnInfo = #{peername => {{127,0,0,1}, 3456},
sockname => {{127,0,0,1}, 18083},
conn_mod => emqx_ws_connection,
proto_name => <<"MQTT">>,
proto_ver => ?MQTT_PROTO_V5,
clean_start => true,
keepalive => 30,
clientid => <<"clientid">>,
username => <<"username">>,
receive_maximum => 100,
expiry_interval => 0
},
ClientInfo = #{zone => zone,
protocol => mqtt,
peerhost => {127,0,0,1},
clientid => <<"clientid">>,
username => <<"username">>,
is_superuser => false,
peercert => undefined,
mountpoint => undefined
},
Session = emqx_session:init(#{zone => external},
#{receive_maximum => 0}
),
maps:fold(fun(Field, Value, Channel) ->
emqx_channel:set_field(Field, Value, Channel)
end,
emqx_channel:init(ConnInfo, [{zone, zone}]),
maps:merge(#{clientinfo => ClientInfo,
session => Session,
conn_state => connected
}, InitFields)).