From d386b27e8a4be237202fafeea9eb37471d1c4a09 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 11 Jun 2019 23:18:38 +0800 Subject: [PATCH 01/33] Adopt channel architecture and improve the MQTT frame parser --- include/emqx_mqtt.hrl | 4 +- src/{emqx_connection.erl => emqx_channel.erl} | 152 ++++++------ src/emqx_frame.erl | 121 ++++++---- src/emqx_listeners.erl | 4 +- src/emqx_protocol.erl | 25 +- src/emqx_session.erl | 4 +- ..._ws_connection.erl => emqx_ws_channel.erl} | 100 ++++---- test/emqx_frame_SUITE.erl | 225 +++++++++--------- 8 files changed, 341 insertions(+), 294 deletions(-) rename src/{emqx_connection.erl => emqx_channel.erl} (82%) rename src/{emqx_ws_connection.erl => emqx_ws_channel.erl} (79%) diff --git a/include/emqx_mqtt.hrl b/include/emqx_mqtt.hrl index 1c2ce1a27..655b8e755 100644 --- a/include/emqx_mqtt.hrl +++ b/include/emqx_mqtt.hrl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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. +%%-------------------------------------------------------------------- -ifndef(EMQ_X_MQTT_HRL). -define(EMQ_X_MQTT_HRL, true). diff --git a/src/emqx_connection.erl b/src/emqx_channel.erl similarity index 82% rename from src/emqx_connection.erl rename to src/emqx_channel.erl index 2e8d2ac69..7e7b7f4c1 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_channel.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,8 +12,9 @@ %% 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_connection). +-module(emqx_channel). -behaviour(gen_statem). @@ -23,11 +25,10 @@ -export([start_link/3]). %% APIs --export([info/1]). - --export([attrs/1]). - --export([stats/1]). +-export([ info/1 + , attrs/1 + , stats/1 + ]). -export([kick/1]). @@ -45,7 +46,6 @@ ]). -record(state, { - zone, transport, socket, peername, @@ -56,10 +56,12 @@ parse_state, gc_state, keepalive, - stats_timer, rate_limit, pub_limit, - limit_timer + limit_timer, + enable_stats, + stats_timer, + idle_timeout }). -define(ACTIVE_N, 100). @@ -69,11 +71,12 @@ start_link(Transport, Socket, Options) -> {ok, proc_lib:spawn_link(?MODULE, init, [{Transport, Socket, Options}])}. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% API -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% For debug +-spec(info(pid() | #state{}) -> map()). info(CPid) when is_pid(CPid) -> call(CPid, info); @@ -92,7 +95,8 @@ info(#state{transport = Transport, conn_state => ConnState, active_n => ActiveN, rate_limit => rate_limit_info(RateLimit), - pub_limit => rate_limit_info(PubLimit)}, + pub_limit => rate_limit_info(PubLimit) + }, ProtoInfo = emqx_protocol:info(ProtoState), maps:merge(ConnInfo, ProtoInfo). @@ -137,9 +141,9 @@ session(CPid) -> call(CPid, Req) -> gen_statem:call(CPid, Req, infinity). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% gen_statem callbacks -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- init({Transport, RawSocket, Options}) -> {ok, Socket} = Transport:wait(RawSocket), @@ -151,12 +155,10 @@ init({Transport, RawSocket, Options}) -> RateLimit = init_limiter(proplists:get_value(rate_limit, Options)), PubLimit = init_limiter(emqx_zone:get_env(Zone, publish_limit)), ActiveN = proplists:get_value(active_n, Options, ?ACTIVE_N), - IdleTimout = emqx_zone:get_env(Zone, idle_timeout, 30000), - SendFun = fun(Packet, SeriaOpts) -> - Data = emqx_frame:serialize(Packet, SeriaOpts), + SendFun = fun(Packet, Opts) -> + Data = emqx_frame:serialize(Packet, Opts), case Transport:async_send(Socket, Data) of - ok -> - {ok, Data}; + ok -> {ok, Data}; {error, Reason} -> {error, Reason} end @@ -166,11 +168,13 @@ init({Transport, RawSocket, Options}) -> peercert => Peercert, sendfun => SendFun, conn_mod => ?MODULE}, Options), - ParseState = emqx_protocol:parser(ProtoState), + MaxSize = emqx_zone:get_env(Zone, max_packet_size, ?MAX_PACKET_SIZE), + ParseState = emqx_frame:initial_parse_state(#{max_size => MaxSize}), GcPolicy = emqx_zone:get_env(Zone, force_gc_policy, false), GcState = emqx_gc:init(GcPolicy), - State = #state{zone = Zone, - transport = Transport, + EnableStats = emqx_zone:get_env(Zone, enable_stats, true), + IdleTimout = emqx_zone:get_env(Zone, idle_timeout, 30000), + State = #state{transport = Transport, socket = Socket, peername = Peername, conn_state = running, @@ -179,7 +183,10 @@ init({Transport, RawSocket, Options}) -> pub_limit = PubLimit, proto_state = ProtoState, parse_state = ParseState, - gc_state = GcState}, + gc_state = GcState, + enable_stats = EnableStats, + idle_timeout = IdleTimout + }, ok = emqx_misc:init_proc_mng_policy(Zone), gen_statem:enter_loop(?MODULE, [{hibernate_after, 2 * IdleTimout}], idle, State, self(), [IdleTimout]). @@ -192,8 +199,8 @@ init_limiter({Rate, Burst}) -> callback_mode() -> [state_functions, state_enter]. -%%------------------------------------------------------------------------------ -%% Idle state +%%-------------------------------------------------------------------- +%% Idle State idle(enter, _, State) -> ok = activate_socket(State), @@ -203,15 +210,15 @@ idle(timeout, _Timeout, State) -> {stop, idle_timeout, State}; idle(cast, {incoming, Packet}, State) -> - handle_packet(Packet, fun(NState) -> - {next_state, connected, reset_parser(NState)} - end, State); + handle_incoming(Packet, fun(NState) -> + {next_state, connected, NState} + end, State); idle(EventType, Content, State) -> ?HANDLE(EventType, Content, State). -%%------------------------------------------------------------------------------ -%% Connected state +%%-------------------------------------------------------------------- +%% Connected State connected(enter, _, _State) -> %% What to do? @@ -221,9 +228,7 @@ connected(enter, _, _State) -> connected(cast, {incoming, Packet = ?PACKET(Type)}, State) -> ok = emqx_metrics:inc_recv(Packet), (Type == ?PUBLISH) andalso emqx_pd:update_counter(incoming_pubs, 1), - handle_packet(Packet, fun(NState) -> - {keep_state, NState} - end, State); + handle_incoming(Packet, fun(NState) -> {keep_state, NState} end, State); %% Handle Output connected(info, {deliver, PubOrAck}, State = #state{proto_state = ProtoState}) -> @@ -283,18 +288,18 @@ handle({call, From}, session, State = #state{proto_state = ProtoState}) -> reply(From, emqx_protocol:session(ProtoState), State); handle({call, From}, Req, State) -> - ?LOG(error, "[Connection] Unexpected call: ~p", [Req]), + ?LOG(error, "[Channel] Unexpected call: ~p", [Req]), reply(From, ignored, State); %% Handle cast handle(cast, Msg, State) -> - ?LOG(error, "[Connection] Unexpected cast: ~p", [Msg]), + ?LOG(error, "[Channel] Unexpected cast: ~p", [Msg]), {keep_state, State}; %% Handle Incoming handle(info, {Inet, _Sock, Data}, State) when Inet == tcp; Inet == ssl -> Oct = iolist_size(Data), - ?LOG(debug, "[Connection] RECV ~p", [Data]), + ?LOG(debug, "[Channel] RECV ~p", [Data]), emqx_pd:update_counter(incoming_bytes, Oct), ok = emqx_metrics:inc('bytes.received', Oct), NState = ensure_stats_timer(maybe_gc({1, Oct}, State)), @@ -308,13 +313,8 @@ handle(info, {Closed, _Sock}, State) when Closed == tcp_closed; Closed == ssl_closed -> shutdown(closed, State); -handle(info, {tcp_passive, _Sock}, State) -> - %% Rate limit here:) - NState = ensure_rate_limit(State), - ok = activate_socket(NState), - {keep_state, NState}; - -handle(info, {ssl_passive, _Sock}, State) -> +handle(info, {Passive, _Sock}, State) when Passive == tcp_passive; + Passive == ssl_passive -> %% Rate limit here:) NState = ensure_rate_limit(State), ok = activate_socket(NState), @@ -336,7 +336,8 @@ handle(info, {timeout, Timer, emit_stats}, State = #state{stats_timer = Timer, proto_state = ProtoState, gc_state = GcState}) -> - emqx_cm:set_conn_stats(emqx_protocol:client_id(ProtoState), stats(State)), + ClientId = emqx_protocol:client_id(ProtoState), + emqx_cm:set_conn_stats(ClientId, stats(State)), NState = State#state{stats_timer = undefined}, Limits = erlang:get(force_shutdown_policy), case emqx_misc:conn_proc_mng_policy(Limits) of @@ -347,23 +348,23 @@ handle(info, {timeout, Timer, emit_stats}, GcState1 = emqx_gc:reset(GcState), {keep_state, NState#state{gc_state = GcState1}, hibernate}; {shutdown, Reason} -> - ?LOG(error, "[Connection] Shutdown exceptionally due to ~p", [Reason]), + ?LOG(error, "[Channel] Shutdown exceptionally due to ~p", [Reason]), shutdown(Reason, NState) end; handle(info, {shutdown, discard, {ClientId, ByPid}}, State) -> - ?LOG(error, "[Connection] Discarded by ~s:~p", [ClientId, ByPid]), + ?LOG(error, "[Channel] Discarded by ~s:~p", [ClientId, ByPid]), shutdown(discard, State); handle(info, {shutdown, conflict, {ClientId, NewPid}}, State) -> - ?LOG(warning, "[Connection] Clientid '~s' conflict with ~p", [ClientId, NewPid]), + ?LOG(warning, "[Channel] Clientid '~s' conflict with ~p", [ClientId, NewPid]), shutdown(conflict, State); handle(info, {shutdown, Reason}, State) -> shutdown(Reason, State); handle(info, Info, State) -> - ?LOG(error, "[Connection] Unexpected info: ~p", [Info]), + ?LOG(error, "[Channel] Unexpected info: ~p", [Info]), {keep_state, State}. code_change(_Vsn, State, Data, _Extra) -> @@ -373,7 +374,7 @@ terminate(Reason, _StateName, #state{transport = Transport, socket = Socket, keepalive = KeepAlive, proto_state = ProtoState}) -> - ?LOG(debug, "[Connection] Terminated for ~p", [Reason]), + ?LOG(debug, "[Channel] Terminated for ~p", [Reason]), Transport:fast_close(Socket), emqx_keepalive:cancel(KeepAlive), case {ProtoState, Reason} of @@ -384,7 +385,7 @@ terminate(Reason, _StateName, #state{transport = Transport, emqx_protocol:terminate(Reason, ProtoState) end. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Process incoming data process_incoming(<<>>, Packets, State) -> @@ -392,30 +393,30 @@ process_incoming(<<>>, Packets, State) -> process_incoming(Data, Packets, State = #state{parse_state = ParseState}) -> try emqx_frame:parse(Data, ParseState) of - {ok, Packet, Rest} -> - process_incoming(Rest, [Packet|Packets], reset_parser(State)); - {more, NewParseState} -> - {keep_state, State#state{parse_state = NewParseState}, next_events(Packets)}; + {ok, NParseState} -> + NState = State#state{parse_state = NParseState}, + {keep_state, NState, next_events(Packets)}; + {ok, Packet, Rest, NParseState} -> + NState = State#state{parse_state = NParseState}, + process_incoming(Rest, [Packet|Packets], NState); {error, Reason} -> shutdown(Reason, State) catch - _:Error:Stk-> - ?LOG(error, "[Connection] Parse failed for ~p~nStacktrace:~p~nError data:~p", [Error, Stk, Data]), - shutdown(Error, State) + error:Reason:Stk -> + ?LOG(error, "[Channel] Parse failed for ~p~n\ + Stacktrace:~p~nError data:~p", [Reason, Stk, Data]), + shutdown(parse_error, State) end. -reset_parser(State = #state{proto_state = ProtoState}) -> - State#state{parse_state = emqx_protocol:parser(ProtoState)}. - next_events(Packets) when is_list(Packets) -> [next_events(Packet) || Packet <- lists:reverse(Packets)]; next_events(Packet) -> {next_event, cast, {incoming, Packet}}. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Handle incoming packet -handle_packet(Packet, SuccFun, State = #state{proto_state = ProtoState}) -> +handle_incoming(Packet, SuccFun, State = #state{proto_state = ProtoState}) -> case emqx_protocol:received(Packet, ProtoState) of {ok, NProtoState} -> SuccFun(State#state{proto_state = NProtoState}); @@ -427,7 +428,7 @@ handle_packet(Packet, SuccFun, State = #state{proto_state = ProtoState}) -> stop(Error, State#state{proto_state = NProtoState}) end. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Ensure rate limit ensure_rate_limit(State = #state{rate_limit = Rl, pub_limit = Pl}) -> @@ -444,12 +445,12 @@ ensure_rate_limit([{Rl, Pos, Cnt}|Limiters], State) -> {0, Rl1} -> ensure_rate_limit(Limiters, setelement(Pos, State, Rl1)); {Pause, Rl1} -> - ?LOG(debug, "[Connection] Rate limit pause connection ~pms", [Pause]), + ?LOG(debug, "[Channel] Rate limit pause connection ~pms", [Pause]), TRef = erlang:send_after(Pause, self(), activate_socket), setelement(Pos, State#state{conn_state = blocked, limit_timer = TRef}, Rl1) end. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Activate socket activate_socket(#state{conn_state = blocked}) -> @@ -463,20 +464,16 @@ activate_socket(#state{transport = Transport, socket = Socket, active_n = N}) -> ok end. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Ensure stats timer -ensure_stats_timer(State = #state{zone = Zone, stats_timer = undefined}) -> - case emqx_zone:get_env(Zone, enable_stats, true) of - true -> - IdleTimeout = emqx_zone:get_env(Zone, idle_timeout, 30000), - State#state{stats_timer = emqx_misc:start_timer(IdleTimeout, emit_stats)}; - false -> - State - end; +ensure_stats_timer(State = #state{enable_stats = true, + stats_timer = undefined, + idle_timeout = IdleTimeout}) -> + State#state{stats_timer = emqx_misc:start_timer(IdleTimeout, emit_stats)}; ensure_stats_timer(State) -> State. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Maybe GC maybe_gc(_, State = #state{gc_state = undefined}) -> @@ -494,7 +491,7 @@ maybe_gc({Cnt, Oct}, State = #state{gc_state = GCSt}) -> State#state{gc_state = GCSt1}; maybe_gc(_, State) -> State. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Helper functions reply(From, Reply, State) -> @@ -505,3 +502,4 @@ shutdown(Reason, State) -> stop(Reason, State) -> {stop, Reason, State}. + diff --git a/src/emqx_frame.erl b/src/emqx_frame.erl index 79c8ed3c8..a4ef34840 100644 --- a/src/emqx_frame.erl +++ b/src/emqx_frame.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,79 +12,95 @@ %% 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_frame). -include("emqx.hrl"). -include("emqx_mqtt.hrl"). --export([ initial_state/0 - , initial_state/1 +-export([ initial_parse_state/0 + , initial_parse_state/1 ]). --export([ parse/2 +-export([ parse/1 + , parse/2 , serialize/1 , serialize/2 ]). --type(options() :: #{max_packet_size => 1..?MAX_PACKET_SIZE, - version => emqx_mqtt_types:version()}). +-type(options() :: #{max_size => 1..?MAX_PACKET_SIZE, + version => emqx_mqtt_types:version() + }). --opaque(parse_state() :: {none, options()} | cont_fun(binary())). +-opaque(parse_state() :: {none, options()} | {more, cont_fun()}). --type(cont_fun(Bin) :: fun((Bin) -> {ok, emqx_mqtt_types:packet(), binary()} - | {more, cont_fun(Bin)})). +-opaque(parse_result() :: {ok, parse_state()} + | {ok, emqx_mqtt_types:packet(), binary(), parse_state()}). --export_type([options/0, parse_state/0]). +-type(cont_fun() :: fun((binary()) -> parse_result())). --define(DEFAULT_OPTIONS, #{max_packet_size => ?MAX_PACKET_SIZE, - version => ?MQTT_PROTO_V4}). +-export_type([ options/0 + , parse_state/0 + , parse_result/0 + ]). -%%------------------------------------------------------------------------------ -%% Init parse state -%%------------------------------------------------------------------------------ +-define(none(Opts), {none, Opts}). +-define(more(Cont), {more, Cont}). +-define(DEFAULT_OPTIONS, + #{max_size => ?MAX_PACKET_SIZE, + version => ?MQTT_PROTO_V4 + }). --spec(initial_state() -> {none, options()}). -initial_state() -> - initial_state(#{}). +%%-------------------------------------------------------------------- +%% Init Parse State +%%-------------------------------------------------------------------- --spec(initial_state(options()) -> {none, options()}). -initial_state(Options) when is_map(Options) -> - {none, merge_opts(Options)}. +-spec(initial_parse_state() -> {none, options()}). +initial_parse_state() -> + initial_parse_state(#{}). +-spec(initial_parse_state(options()) -> {none, options()}). +initial_parse_state(Options) when is_map(Options) -> + ?none(merge_opts(Options)). + +%% @pivate merge_opts(Options) -> maps:merge(?DEFAULT_OPTIONS, Options). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Parse MQTT Frame -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- --spec(parse(binary(), parse_state()) -> {ok, emqx_mqtt_types:packet(), binary()} | - {more, cont_fun(binary())}). +-spec(parse(binary()) -> parse_result()). +parse(Bin) -> + parse(Bin, initial_parse_state()). + +-spec(parse(binary(), parse_state()) -> parse_result()). parse(<<>>, {none, Options}) -> - {more, fun(Bin) -> parse(Bin, {none, Options}) end}; + {ok, ?more(fun(Bin) -> parse(Bin, {none, Options}) end)}; parse(<>, {none, Options}) -> parse_remaining_len(Rest, #mqtt_packet_header{type = Type, dup = bool(Dup), qos = fixqos(Type, QoS), retain = bool(Retain)}, Options); -parse(Bin, Cont) when is_binary(Bin), is_function(Cont) -> +parse(Bin, {more, Cont}) when is_binary(Bin), is_function(Cont) -> Cont(Bin). parse_remaining_len(<<>>, Header, Options) -> - {more, fun(Bin) -> parse_remaining_len(Bin, Header, Options) end}; + {ok, ?more(fun(Bin) -> parse_remaining_len(Bin, Header, Options) end)}; parse_remaining_len(Rest, Header, Options) -> parse_remaining_len(Rest, Header, 1, 0, Options). -parse_remaining_len(_Bin, _Header, _Multiplier, Length, - #{max_packet_size := MaxSize}) - when Length > MaxSize -> +parse_remaining_len(_Bin, _Header, _Multiplier, Length, #{max_size := MaxSize}) + when Length > MaxSize -> error(mqtt_frame_too_large); parse_remaining_len(<<>>, Header, Multiplier, Length, Options) -> - {more, fun(Bin) -> parse_remaining_len(Bin, Header, Multiplier, Length, Options) end}; + {ok, ?more(fun(Bin) -> parse_remaining_len(Bin, Header, Multiplier, Length, Options) end)}; %% Match DISCONNECT without payload -parse_remaining_len(<<0:8, Rest/binary>>, Header = #mqtt_packet_header{type = ?DISCONNECT}, 1, 0, _Options) -> - wrap(Header, #mqtt_packet_disconnect{reason_code = ?RC_SUCCESS}, Rest); +parse_remaining_len(<<0:8, Rest/binary>>, Header = #mqtt_packet_header{type = ?DISCONNECT}, 1, 0, Options) -> + Packet = packet(Header, #mqtt_packet_disconnect{reason_code = ?RC_SUCCESS}), + {ok, Packet, Rest, ?none(Options)}; %% Match PINGREQ. parse_remaining_len(<<0:8, Rest/binary>>, Header, 1, 0, Options) -> parse_frame(Rest, Header, 0, Options); @@ -92,38 +109,40 @@ parse_remaining_len(<<0:1, 2:7, Rest/binary>>, Header, 1, 0, Options) -> parse_frame(Rest, Header, 2, Options); parse_remaining_len(<<1:1, Len:7, Rest/binary>>, Header, Multiplier, Value, Options) -> parse_remaining_len(Rest, Header, Multiplier * ?HIGHBIT, Value + Len * Multiplier, Options); -parse_remaining_len(<<0:1, Len:7, Rest/binary>>, Header, Multiplier, Value, - Options = #{max_packet_size:= MaxSize}) -> +parse_remaining_len(<<0:1, Len:7, Rest/binary>>, Header, Multiplier, Value, + Options = #{max_size := MaxSize}) -> FrameLen = Value + Len * Multiplier, if FrameLen > MaxSize -> error(mqtt_frame_too_large); true -> parse_frame(Rest, Header, FrameLen, Options) end. -parse_frame(Bin, Header, 0, _Options) -> - wrap(Header, Bin); +parse_frame(Bin, Header, 0, Options) -> + {ok, packet(Header), Bin, ?none(Options)}; parse_frame(Bin, Header, Length, Options) -> case Bin of <> -> case parse_packet(Header, FrameBin, Options) of {Variable, Payload} -> - wrap(Header, Variable, Payload, Rest); + {ok, packet(Header, Variable, Payload), Rest, ?none(Options)}; + Variable = #mqtt_packet_connect{proto_ver = Ver} -> + {ok, packet(Header, Variable), Rest, ?none(Options#{version := Ver})}; Variable -> - wrap(Header, Variable, Rest) + {ok, packet(Header, Variable), Rest, ?none(Options)} end; TooShortBin -> - {more, fun(BinMore) -> - parse_frame(<>, Header, Length, Options) - end} + {ok, ?more(fun(BinMore) -> + parse_frame(<>, Header, Length, Options) + end)} end. -wrap(Header, Variable, Payload, Rest) -> - {ok, #mqtt_packet{header = Header, variable = Variable, payload = Payload}, Rest}. -wrap(Header, Variable, Rest) -> - {ok, #mqtt_packet{header = Header, variable = Variable}, Rest}. -wrap(Header, Rest) -> - {ok, #mqtt_packet{header = Header}, Rest}. +packet(Header) -> + #mqtt_packet{header = Header}. +packet(Header, Variable) -> + #mqtt_packet{header = Header, variable = Variable}. +packet(Header, Variable, Payload) -> + #mqtt_packet{header = Header, variable = Variable, payload = Payload}. parse_packet(#mqtt_packet_header{type = ?CONNECT}, FrameBin, _Options) -> {ProtoName, Rest} = parse_utf8_string(FrameBin), @@ -362,9 +381,9 @@ parse_utf8_string(<>) -> parse_binary_data(<>) -> {Data, Rest}. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Serialize MQTT Packet -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- -spec(serialize(emqx_mqtt_types:packet()) -> iodata()). serialize(Packet) -> diff --git a/src/emqx_listeners.erl b/src/emqx_listeners.erl index 65515a8d3..745daf000 100644 --- a/src/emqx_listeners.erl +++ b/src/emqx_listeners.erl @@ -73,7 +73,7 @@ start_listener(Proto, ListenOn, Options) when Proto == https; Proto == wss -> start_mqtt_listener(Name, ListenOn, Options) -> SockOpts = esockd:parse_opt(Options), esockd:open(Name, ListenOn, merge_default(SockOpts), - {emqx_connection, start_link, [Options -- SockOpts]}). + {emqx_channel, start_link, [Options -- SockOpts]}). start_http_listener(Start, Name, ListenOn, RanchOpts, ProtoOpts) -> Start(Name, with_port(ListenOn, RanchOpts), ProtoOpts). @@ -82,7 +82,7 @@ mqtt_path(Options) -> proplists:get_value(mqtt_path, Options, "/mqtt"). ws_opts(Options) -> - Dispatch = cowboy_router:compile([{'_', [{mqtt_path(Options), emqx_ws_connection, Options}]}]), + Dispatch = cowboy_router:compile([{'_', [{mqtt_path(Options), emqx_ws_channel, Options}]}]), #{env => #{dispatch => Dispatch}, proxy_header => proplists:get_value(proxy_protocol, Options, false)}. ranch_opts(Options) -> diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 57f60c8c7..d8e6da46f 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_protocol). @@ -18,16 +20,18 @@ -include("emqx_mqtt.hrl"). -include("logger.hrl"). --export([ init/2 - , info/1 +-export([ info/1 , attrs/1 , attr/2 , caps/1 + , caps/2 , stats/1 , client_id/1 , credentials/1 - , parser/1 , session/1 + ]). + +-export([ init/2 , received/2 , process/2 , deliver/2 @@ -35,8 +39,6 @@ , terminate/2 ]). --export_type([state/0]). - -record(pstate, { zone, sendfun, @@ -70,6 +72,8 @@ -opaque(state() :: #pstate{}). +-export_type([state/0]). + -ifdef(TEST). -compile(export_all). -compile(nowarn_export_all). @@ -168,6 +172,8 @@ attrs(#pstate{zone = Zone, , credentials => Credentials }. +attr(proto_ver, #pstate{proto_ver = ProtoVer}) -> + ProtoVer; attr(max_inflight, #pstate{proto_ver = ?MQTT_PROTO_V5, conn_props = ConnProps}) -> get_property('Receive-Maximum', ConnProps, 65535); attr(max_inflight, #pstate{zone = Zone}) -> @@ -190,6 +196,9 @@ attr(Name, PState) -> false -> undefined end. +caps(Name, PState) -> + maps:get(Name, caps(PState)). + caps(#pstate{zone = Zone}) -> emqx_mqtt_caps:get_caps(Zone). @@ -232,10 +241,6 @@ stats(#pstate{recv_stats = #{pkt := RecvPkt, msg := RecvMsg}, session(#pstate{session = SPid}) -> SPid. -parser(#pstate{zone = Zone, proto_ver = Ver}) -> - Size = emqx_zone:get_env(Zone, max_packet_size), - emqx_frame:initial_state(#{max_packet_size => Size, version => Ver}). - %%------------------------------------------------------------------------------ %% Packet Received %%------------------------------------------------------------------------------ diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 56f29bd4c..85d2c1947 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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. +%%-------------------------------------------------------------------- %% @doc %% A stateful interaction between a Client and a Server. Some Sessions diff --git a/src/emqx_ws_connection.erl b/src/emqx_ws_channel.erl similarity index 79% rename from src/emqx_ws_connection.erl rename to src/emqx_ws_channel.erl index d635a0caa..0d7420b9a 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_channel.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,8 +12,9 @@ %% 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_ws_connection). +-module(emqx_ws_channel). -include("emqx.hrl"). -include("emqx_mqtt.hrl"). @@ -38,20 +40,20 @@ options, peername, sockname, - idle_timeout, proto_state, parse_state, keepalive, enable_stats, stats_timer, + idle_timeout, shutdown }). -define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt]). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% API -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% for debug info(WSPid) when is_pid(WSPid) -> @@ -108,9 +110,9 @@ call(WSPid, Req) when is_pid(WSPid) -> exit(timeout) end. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% WebSocket callbacks -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- init(Req, Opts) -> IdleTimeout = proplists:get_value(idle_timeout, Opts, 7200000), @@ -141,11 +143,11 @@ websocket_init(#state{request = Req, options = Options}) -> WsCookie = try cowboy_req:parse_cookies(Req) catch error:badarg -> - ?LOG(error, "[WS Connection] Illegal cookie"), + ?LOG(error, "[WS Channel] Illegal cookie"), undefined; Error:Reason -> ?LOG(error, - "[WS Connection] Cookie is parsed failed, Error: ~p, Reason ~p", + "[WS Channel] Cookie is parsed failed, Error: ~p, Reason ~p", [Error, Reason]), undefined end, @@ -155,15 +157,16 @@ websocket_init(#state{request = Req, options = Options}) -> sendfun => send_fun(self()), ws_cookie => WsCookie, conn_mod => ?MODULE}, Options), - ParserState = emqx_protocol:parser(ProtoState), Zone = proplists:get_value(zone, Options), + MaxSize = emqx_zone:get_env(Zone, max_packet_size, ?MAX_PACKET_SIZE), + ParseState = emqx_frame:initial_parse_state(#{max_size => MaxSize}), EnableStats = emqx_zone:get_env(Zone, enable_stats, true), IdleTimout = emqx_zone:get_env(Zone, idle_timeout, 30000), emqx_logger:set_metadata_peername(esockd_net:format(Peername)), ok = emqx_misc:init_proc_mng_policy(Zone), {ok, #state{peername = Peername, sockname = Sockname, - parse_state = ParserState, + parse_state = ParseState, proto_state = ProtoState, enable_stats = EnableStats, idle_timeout = IdleTimout}}. @@ -185,35 +188,28 @@ websocket_handle({binary, <<>>}, State) -> {ok, ensure_stats_timer(State)}; websocket_handle({binary, [<<>>]}, State) -> {ok, ensure_stats_timer(State)}; -websocket_handle({binary, Data}, State = #state{parse_state = ParseState, - proto_state = ProtoState}) -> - ?LOG(debug, "[WS Connection] RECV ~p", [Data]), +websocket_handle({binary, Data}, State = #state{parse_state = ParseState}) -> + ?LOG(debug, "[WS Channel] RECV ~p", [Data]), BinSize = iolist_size(Data), emqx_pd:update_counter(recv_oct, BinSize), ok = emqx_metrics:inc('bytes.received', BinSize), try emqx_frame:parse(iolist_to_binary(Data), ParseState) of - {more, ParseState1} -> - {ok, State#state{parse_state = ParseState1}}; - {ok, Packet, Rest} -> + {ok, NParseState} -> + {ok, State#state{parse_state = NParseState}}; + {ok, Packet, Rest, NParseState} -> ok = emqx_metrics:inc_recv(Packet), emqx_pd:update_counter(recv_cnt, 1), - case emqx_protocol:received(Packet, ProtoState) of - {ok, ProtoState1} -> - websocket_handle({binary, Rest}, reset_parser(State#state{proto_state = ProtoState1})); - {error, Error} -> - ?LOG(error, "[WS Connection] Protocol error: ~p", [Error]), - shutdown(Error, State); - {error, Reason, ProtoState1} -> - shutdown(Reason, State#state{proto_state = ProtoState1}); - {stop, Error, ProtoState1} -> - shutdown(Error, State#state{proto_state = ProtoState1}) - end; - {error, Error} -> - ?LOG(error, "[WS Connection] Frame error: ~p", [Error]), - shutdown(Error, State) + handle_incoming(Packet, fun(NState) -> + websocket_handle({binary, Rest}, NState) + end, + State#state{parse_state = NParseState}); + {error, Reason} -> + ?LOG(error, "[WS Channel] Frame error: ~p", [Reason]), + shutdown(Reason, State) catch - _:Error -> - ?LOG(error, "[WS Connection] Frame error:~p~nFrame data: ~p", [Error, Data]), + error:Reason:Stk -> + ?LOG(error, "[WS Channel] Parse failed for ~p~n\ + Stacktrace:~p~nFrame data: ~p", [Reason, Stk, Data]), shutdown(parse_error, State) end; %% Pings should be replied with pongs, cowboy does it automatically @@ -259,12 +255,12 @@ websocket_info({timeout, Timer, emit_stats}, {ok, State#state{stats_timer = undefined}, hibernate}; websocket_info({keepalive, start, Interval}, State) -> - ?LOG(debug, "[WS Connection] Keepalive at the interval of ~p", [Interval]), + ?LOG(debug, "[WS Channel] Keepalive at the interval of ~p", [Interval]), case emqx_keepalive:start(stat_fun(), Interval, {keepalive, check}) of {ok, KeepAlive} -> {ok, State#state{keepalive = KeepAlive}}; {error, Error} -> - ?LOG(warning, "[WS Connection] Keepalive error: ~p", [Error]), + ?LOG(warning, "[WS Channel] Keepalive error: ~p", [Error]), shutdown(Error, State) end; @@ -273,19 +269,19 @@ websocket_info({keepalive, check}, State = #state{keepalive = KeepAlive}) -> {ok, KeepAlive1} -> {ok, State#state{keepalive = KeepAlive1}}; {error, timeout} -> - ?LOG(debug, "[WS Connection] Keepalive Timeout!"), + ?LOG(debug, "[WS Channel] Keepalive Timeout!"), shutdown(keepalive_timeout, State); {error, Error} -> - ?LOG(error, "[WS Connection] Keepalive error: ~p", [Error]), + ?LOG(error, "[WS Channel] Keepalive error: ~p", [Error]), shutdown(keepalive_error, State) end; websocket_info({shutdown, discard, {ClientId, ByPid}}, State) -> - ?LOG(warning, "[WS Connection] Discarded by ~s:~p", [ClientId, ByPid]), + ?LOG(warning, "[WS Channel] Discarded by ~s:~p", [ClientId, ByPid]), shutdown(discard, State); websocket_info({shutdown, conflict, {ClientId, NewPid}}, State) -> - ?LOG(warning, "[WS Connection] Clientid '~s' conflict with ~p", [ClientId, NewPid]), + ?LOG(warning, "[WS Channel] Clientid '~s' conflict with ~p", [ClientId, NewPid]), shutdown(conflict, State); websocket_info({binary, Data}, State) -> @@ -295,14 +291,15 @@ websocket_info({shutdown, Reason}, State) -> shutdown(Reason, State); websocket_info(Info, State) -> - ?LOG(error, "[WS Connection] Unexpected info: ~p", [Info]), + ?LOG(error, "[WS Channel] Unexpected info: ~p", [Info]), {ok, State}. terminate(SockError, _Req, #state{keepalive = Keepalive, proto_state = ProtoState, shutdown = Shutdown}) -> - ?LOG(debug, "[WS Connection] Terminated for ~p, sockerror: ~p", [Shutdown, SockError]), + ?LOG(debug, "[WS Channel] Terminated for ~p, sockerror: ~p", + [Shutdown, SockError]), emqx_keepalive:cancel(Keepalive), case {ProtoState, Shutdown} of {undefined, _} -> ok; @@ -312,12 +309,24 @@ terminate(SockError, _Req, #state{keepalive = Keepalive, emqx_protocol:terminate(Error, ProtoState) end. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Internal functions -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- + +handle_incoming(Packet, SuccFun, State = #state{proto_state = ProtoState}) -> + case emqx_protocol:received(Packet, ProtoState) of + {ok, NProtoState} -> + SuccFun(State#state{proto_state = NProtoState}); + {error, Reason} -> + ?LOG(error, "[WS Channel] Protocol error: ~p", [Reason]), + shutdown(Reason, State); + {error, Reason, NProtoState} -> + shutdown(Reason, State#state{proto_state = NProtoState}); + {stop, Error, NProtoState} -> + shutdown(Error, State#state{proto_state = NProtoState}) + end. + -reset_parser(State = #state{proto_state = ProtoState}) -> - State#state{parse_state = emqx_protocol:parser(ProtoState)}. ensure_stats_timer(State = #state{enable_stats = true, stats_timer = undefined, @@ -331,3 +340,4 @@ shutdown(Reason, State) -> wsock_stats() -> [{Key, emqx_pd:get_counter(Key)} || Key <- ?SOCK_STATS]. + diff --git a/test/emqx_frame_SUITE.erl b/test/emqx_frame_SUITE.erl index 86505a566..8de05e734 100644 --- a/test/emqx_frame_SUITE.erl +++ b/test/emqx_frame_SUITE.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_frame_SUITE). @@ -18,11 +20,8 @@ -compile(nowarn_export_all). -include("emqx_mqtt.hrl"). - -include_lib("eunit/include/eunit.hrl"). --import(emqx_frame, [serialize/1, serialize/2]). - all() -> [{group, connect}, {group, connack}, @@ -44,15 +43,18 @@ groups() -> serialize_parse_v5_connect, serialize_parse_connect_without_clientid, serialize_parse_connect_with_will, - serialize_parse_bridge_connect]}, + serialize_parse_bridge_connect + ]}, {connack, [parallel], [serialize_parse_connack, - serialize_parse_connack_v5]}, + serialize_parse_connack_v5 + ]}, {publish, [parallel], [serialize_parse_qos0_publish, serialize_parse_qos1_publish, serialize_parse_qos2_publish, - serialize_parse_publish_v5]}, + serialize_parse_publish_v5 + ]}, {puback, [parallel], [serialize_parse_puback, serialize_parse_puback_v5, @@ -61,27 +63,35 @@ groups() -> serialize_parse_pubrel, serialize_parse_pubrel_v5, serialize_parse_pubcomp, - serialize_parse_pubcomp_v5]}, + serialize_parse_pubcomp_v5 + ]}, {subscribe, [parallel], [serialize_parse_subscribe, - serialize_parse_subscribe_v5]}, + serialize_parse_subscribe_v5 + ]}, {suback, [parallel], [serialize_parse_suback, - serialize_parse_suback_v5]}, + serialize_parse_suback_v5 + ]}, {unsubscribe, [parallel], [serialize_parse_unsubscribe, - serialize_parse_unsubscribe_v5]}, + serialize_parse_unsubscribe_v5 + ]}, {unsuback, [parallel], [serialize_parse_unsuback, - serialize_parse_unsuback_v5]}, + serialize_parse_unsuback_v5 + ]}, {ping, [parallel], [serialize_parse_pingreq, - serialize_parse_pingresp]}, + serialize_parse_pingresp + ]}, {disconnect, [parallel], [serialize_parse_disconnect, - serialize_parse_disconnect_v5]}, + serialize_parse_disconnect_v5 + ]}, {auth, [parallel], - [serialize_parse_auth_v5]}]. + [serialize_parse_auth_v5] + }]. init_per_suite(Config) -> Config. @@ -97,7 +107,7 @@ end_per_group(_Group, _Config) -> serialize_parse_connect(_) -> Packet1 = ?CONNECT_PACKET(#mqtt_packet_connect{}), - ?assertEqual({ok, Packet1, <<>>}, parse_serialize(Packet1)), + ?assertEqual(Packet1, parse_serialize(Packet1)), Packet2 = ?CONNECT_PACKET(#mqtt_packet_connect{ client_id = <<"clientId">>, will_qos = ?QOS_1, @@ -105,8 +115,9 @@ serialize_parse_connect(_) -> will_retain = true, will_topic = <<"will">>, will_payload = <<"bye">>, - clean_start = true}), - ?assertEqual({ok, Packet2, <<>>}, parse_serialize(Packet2)). + clean_start = true + }), + ?assertEqual(Packet2, parse_serialize(Packet2)). serialize_parse_v3_connect(_) -> Bin = <<16,37,0,6,77,81,73,115,100,112,3,2,0,60,0,23,109,111,115, @@ -117,8 +128,9 @@ serialize_parse_v3_connect(_) -> proto_name = <<"MQIsdp">>, client_id = <<"mosqpub/10451-iMac.loca">>, clean_start = true, - keepalive = 60}), - ?assertEqual({ok, Packet, <<>>}, parse(Bin)). + keepalive = 60 + }), + ?assertMatch({ok, Packet, <<>>, _}, emqx_frame:parse(Bin)). serialize_parse_v4_connect(_) -> Bin = <<16,35,0,4,77,81,84,84,4,2,0,60,0,23,109,111,115,113,112,117, @@ -128,8 +140,8 @@ serialize_parse_v4_connect(_) -> client_id = <<"mosqpub/10451-iMac.loca">>, clean_start = true, keepalive = 60}), - ?assertEqual(Bin, iolist_to_binary(serialize(Packet))), - ?assertEqual({ok, Packet, <<>>}, parse(Bin)). + ?assertEqual(Bin, serialize_to_binary(Packet)), + ?assertMatch({ok, Packet, <<>>, _}, emqx_frame:parse(Bin)). serialize_parse_v5_connect(_) -> Props = #{'Session-Expiry-Interval' => 60, @@ -141,7 +153,8 @@ serialize_parse_v5_connect(_) -> 'Request-Response-Information' => 1, 'Request-Problem-Information' => 1, 'Authentication-Method' => <<"oauth2">>, - 'Authentication-Data' => <<"33kx93k">>}, + 'Authentication-Data' => <<"33kx93k">> + }, WillProps = #{'Will-Delay-Interval' => 60, 'Payload-Format-Indicator' => 1, @@ -149,7 +162,8 @@ serialize_parse_v5_connect(_) -> 'Content-Type' => <<"text/json">>, 'Response-Topic' => <<"topic">>, 'Correlation-Data' => <<"correlateid">>, - 'User-Property' => [{<<"k">>, <<"v">>}]}, + 'User-Property' => [{<<"k">>, <<"v">>}] + }, Packet = ?CONNECT_PACKET( #mqtt_packet_connect{proto_name = <<"MQTT">>, proto_ver = ?MQTT_PROTO_V5, @@ -165,18 +179,21 @@ serialize_parse_v5_connect(_) -> will_topic = <<"topic">>, will_payload = <<>>, username = <<"device:1">>, - password = <<"passwd">>}), - ?assertEqual({ok, Packet, <<>>}, parse_serialize(Packet)). + password = <<"passwd">> + }), + ?assertEqual(Packet, parse_serialize(Packet)). serialize_parse_connect_without_clientid(_) -> Bin = <<16,12,0,4,77,81,84,84,4,2,0,60,0,0>>, - Packet = ?CONNECT_PACKET(#mqtt_packet_connect{proto_ver = 4, - proto_name = <<"MQTT">>, - client_id = <<>>, - clean_start = true, - keepalive = 60}), - ?assertEqual(Bin, iolist_to_binary(serialize(Packet))), - ?assertEqual({ok, Packet, <<>>}, parse(Bin)). + Packet = ?CONNECT_PACKET( + #mqtt_packet_connect{proto_ver = 4, + proto_name = <<"MQTT">>, + client_id = <<>>, + clean_start = true, + keepalive = 60 + }), + ?assertEqual(Bin, serialize_to_binary(Packet)), + ?assertMatch({ok, Packet, <<>>, _}, emqx_frame:parse(Bin)). serialize_parse_connect_with_will(_) -> Bin = <<16,67,0,6,77,81,73,115,100,112,3,206,0,60,0,23,109,111,115,113,112, @@ -195,9 +212,10 @@ serialize_parse_connect_with_will(_) -> will_topic = <<"/will">>, will_payload = <<"willmsg">>, username = <<"test">>, - password = <<"public">>}}, - ?assertEqual(Bin, iolist_to_binary(serialize(Packet))), - ?assertEqual({ok, Packet, <<>>}, parse(Bin)). + password = <<"public">> + }}, + ?assertEqual(Bin, serialize_to_binary(Packet)), + ?assertMatch({ok, Packet, <<>>, _}, emqx_frame:parse(Bin)). serialize_parse_bridge_connect(_) -> Bin = <<16,86,0,6,77,81,73,115,100,112,131,44,0,60,0,19,67,95,48,48,58,48,67, @@ -216,14 +234,15 @@ serialize_parse_bridge_connect(_) -> clean_start = false, keepalive = 60, will_topic = Topic, - will_payload = <<"0">>}}, - ?assertEqual(Bin, iolist_to_binary(serialize(Packet))), - ?assertEqual({ok, Packet, <<>>}, parse(Bin)). + will_payload = <<"0">> + }}, + ?assertEqual(Bin, serialize_to_binary(Packet)), + ?assertMatch({ok, Packet, <<>>, _}, emqx_frame:parse(Bin)). serialize_parse_connack(_) -> Packet = ?CONNACK_PACKET(?RC_SUCCESS), - ?assertEqual(<<32,2,0,0>>, iolist_to_binary(serialize(Packet))), - ?assertEqual({ok, Packet, <<>>}, parse_serialize(Packet)). + ?assertEqual(<<32,2,0,0>>, serialize_to_binary(Packet)), + ?assertEqual(Packet, parse_serialize(Packet)). serialize_parse_connack_v5(_) -> Props = #{'Session-Expiry-Interval' => 60, @@ -241,10 +260,10 @@ serialize_parse_connack_v5(_) -> 'Response-Information' => <<"response">>, 'Server-Reference' => <<"192.168.1.10">>, 'Authentication-Method' => <<"oauth2">>, - 'Authentication-Data' => <<"33kx93k">>}, + 'Authentication-Data' => <<"33kx93k">> + }, Packet = ?CONNACK_PACKET(?RC_SUCCESS, 0, Props), - ?assertEqual({ok, Packet, <<>>}, - parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). + ?assertEqual(Packet, parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). serialize_parse_qos0_publish(_) -> Bin = <<48,14,0,7,120,120,120,47,121,121,121,104,101,108,108,111>>, @@ -255,8 +274,8 @@ serialize_parse_qos0_publish(_) -> variable = #mqtt_packet_publish{topic_name = <<"xxx/yyy">>, packet_id = undefined}, payload = <<"hello">>}, - ?assertEqual(Bin, iolist_to_binary(serialize(Packet))), - ?assertEqual({ok, Packet, <<>>}, parse(Bin)). + ?assertEqual(Bin, serialize_to_binary(Packet)), + ?assertMatch({ok, Packet, <<>>, _}, emqx_frame:parse(Bin)). serialize_parse_qos1_publish(_) -> Bin = <<50,13,0,5,97,47,98,47,99,0,1,104,97,104,97>>, @@ -267,12 +286,12 @@ serialize_parse_qos1_publish(_) -> variable = #mqtt_packet_publish{topic_name = <<"a/b/c">>, packet_id = 1}, payload = <<"haha">>}, - ?assertEqual(Bin, iolist_to_binary(serialize(Packet))), - ?assertEqual({ok, Packet, <<>>}, parse(Bin)). + ?assertEqual(Bin, serialize_to_binary(Packet)), + ?assertMatch({ok, Packet, <<>>, _}, emqx_frame:parse(Bin)). serialize_parse_qos2_publish(_) -> Packet = ?PUBLISH_PACKET(?QOS_2, <<"Topic">>, 1, payload()), - ?assertEqual({ok, Packet, <<>>}, parse_serialize(Packet)). + ?assertEqual(Packet, parse_serialize(Packet)). serialize_parse_publish_v5(_) -> Props = #{'Payload-Format-Indicator' => 1, @@ -283,147 +302,139 @@ serialize_parse_publish_v5(_) -> 'Subscription-Identifier' => 1, 'Content-Type' => <<"text/json">>}, Packet = ?PUBLISH_PACKET(?QOS_1, <<"$share/group/topic">>, 1, Props, <<"payload">>), - ?assertEqual({ok, Packet, <<>>}, - parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). + ?assertEqual(Packet, parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). serialize_parse_puback(_) -> Packet = ?PUBACK_PACKET(1), - ?assertEqual(<<64,2,0,1>>, iolist_to_binary(serialize(Packet))), - ?assertEqual({ok, Packet, <<>>}, parse_serialize(Packet)). + ?assertEqual(<<64,2,0,1>>, serialize_to_binary(Packet)), + ?assertEqual(Packet, parse_serialize(Packet)). serialize_parse_puback_v5(_) -> Packet = ?PUBACK_PACKET(16, ?RC_SUCCESS, #{'Reason-String' => <<"success">>}), - ?assertEqual({ok, Packet, <<>>}, - parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). + ?assertEqual(Packet, parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). serialize_parse_pubrec(_) -> Packet = ?PUBREC_PACKET(1), - ?assertEqual(<<5:4,0:4,2,0,1>>, iolist_to_binary(serialize(Packet))), - ?assertEqual({ok, Packet, <<>>}, parse_serialize(Packet)). + ?assertEqual(<<5:4,0:4,2,0,1>>, serialize_to_binary(Packet)), + ?assertEqual(Packet, parse_serialize(Packet)). serialize_parse_pubrec_v5(_) -> Packet = ?PUBREC_PACKET(16, ?RC_SUCCESS, #{'Reason-String' => <<"success">>}), - ?assertEqual({ok, Packet, <<>>}, - parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). + ?assertEqual(Packet, parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). serialize_parse_pubrel(_) -> Packet = ?PUBREL_PACKET(1), - ?assertEqual(<<6:4,2:4,2,0,1>>, iolist_to_binary(serialize(Packet))), - ?assertEqual({ok, Packet, <<>>}, parse_serialize(Packet)). + Bin = serialize_to_binary(Packet), + ?assertEqual(<<6:4,2:4,2,0,1>>, Bin), + ?assertEqual(Packet, parse_serialize(Packet)). serialize_parse_pubrel_v5(_) -> Packet = ?PUBREL_PACKET(16, ?RC_SUCCESS, #{'Reason-String' => <<"success">>}), - ?assertEqual({ok, Packet, <<>>}, - parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). + ?assertEqual(Packet, parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). serialize_parse_pubcomp(_) -> Packet = ?PUBCOMP_PACKET(1), - ?assertEqual(<<7:4,0:4,2,0,1>>, iolist_to_binary(serialize(Packet))), - ?assertEqual({ok, Packet, <<>>}, parse_serialize(Packet)). + Bin = serialize_to_binary(Packet), + ?assertEqual(<<7:4,0:4,2,0,1>>, Bin), + ?assertEqual(Packet, parse_serialize(Packet)). serialize_parse_pubcomp_v5(_) -> Packet = ?PUBCOMP_PACKET(16, ?RC_SUCCESS, #{'Reason-String' => <<"success">>}), - ?assertEqual({ok, Packet, <<>>}, - parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). + ?assertEqual(Packet, parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). serialize_parse_subscribe(_) -> %% SUBSCRIBE(Q1, R0, D0, PacketId=2, TopicTable=[{<<"TopicA">>,2}]) Bin = <<130,11,0,2,0,6,84,111,112,105,99,65,2>>, - TopicOpts = #{ nl => 0 , rap => 0, rc => 0, - rh => 0, qos => 2 }, + TopicOpts = #{nl => 0 , rap => 0, rc => 0, rh => 0, qos => 2}, TopicFilters = [{<<"TopicA">>, TopicOpts}], Packet = ?SUBSCRIBE_PACKET(2, TopicFilters), - ?assertEqual(Bin, iolist_to_binary(serialize(Packet))), - ct:log("Bin: ~p, Packet: ~p ~n", [Packet, parse(Bin)]), - ?assertEqual({ok, Packet, <<>>}, parse(Bin)). + ?assertEqual(Bin, serialize_to_binary(Packet)), + %%ct:log("Bin: ~p, Packet: ~p ~n", [Packet, parse(Bin)]), + ?assertMatch({ok, Packet, <<>>, _}, emqx_frame:parse(Bin)). serialize_parse_subscribe_v5(_) -> TopicFilters = [{<<"TopicQos0">>, #{rh => 1, qos => ?QOS_2, rap => 0, nl => 0, rc => 0}}, {<<"TopicQos1">>, #{rh => 1, qos => ?QOS_2, rap => 0, nl => 0, rc => 0}}], - Packet = ?SUBSCRIBE_PACKET(3, #{'Subscription-Identifier' => 16#FFFFFFF}, - TopicFilters), - ?assertEqual({ok, Packet, <<>>}, - parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). + Packet = ?SUBSCRIBE_PACKET(3, #{'Subscription-Identifier' => 16#FFFFFFF}, TopicFilters), + ?assertEqual(Packet, parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). serialize_parse_suback(_) -> Packet = ?SUBACK_PACKET(10, [?QOS_0, ?QOS_1, 128]), - ?assertEqual({ok, Packet, <<>>}, parse_serialize(Packet)). + ?assertEqual(Packet, parse_serialize(Packet)). serialize_parse_suback_v5(_) -> Packet = ?SUBACK_PACKET(1, #{'Reason-String' => <<"success">>, 'User-Property' => [{<<"key">>, <<"value">>}]}, [?QOS_0, ?QOS_1, 128]), - ?assertEqual({ok, Packet, <<>>}, - parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). - + ?assertEqual(Packet, parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). serialize_parse_unsubscribe(_) -> %% UNSUBSCRIBE(Q1, R0, D0, PacketId=2, TopicTable=[<<"TopicA">>]) Packet = ?UNSUBSCRIBE_PACKET(2, [<<"TopicA">>]), Bin = <<162,10,0,2,0,6,84,111,112,105,99,65>>, - ?assertEqual(Bin, iolist_to_binary(serialize(Packet))), - ?assertEqual({ok, Packet, <<>>}, parse(Bin)). + ?assertEqual(Bin, serialize_to_binary(Packet)), + ?assertMatch({ok, Packet, <<>>, _}, emqx_frame:parse(Bin)). serialize_parse_unsubscribe_v5(_) -> Props = #{'User-Property' => [{<<"key">>, <<"val">>}]}, Packet = ?UNSUBSCRIBE_PACKET(10, Props, [<<"Topic1">>, <<"Topic2">>]), - ?assertEqual({ok, Packet, <<>>}, - parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). + ?assertEqual(Packet, parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). serialize_parse_unsuback(_) -> Packet = ?UNSUBACK_PACKET(10), - ?assertEqual({ok, Packet, <<>>}, parse_serialize(Packet)). + ?assertEqual(Packet, parse_serialize(Packet)). serialize_parse_unsuback_v5(_) -> Packet = ?UNSUBACK_PACKET(10, #{'Reason-String' => <<"Not authorized">>, 'User-Property' => [{<<"key">>, <<"val">>}]}, [16#87, 16#87, 16#87]), - ?assertEqual({ok, Packet, <<>>}, - parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). + ?assertEqual(Packet, parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). serialize_parse_pingreq(_) -> PingReq = ?PACKET(?PINGREQ), - ?assertEqual({ok, PingReq, <<>>}, parse_serialize(PingReq)). + ?assertEqual(PingReq, parse_serialize(PingReq)). serialize_parse_pingresp(_) -> PingResp = ?PACKET(?PINGRESP), - ?assertEqual({ok, PingResp, <<>>}, parse_serialize(PingResp)). + ?assertEqual(PingResp, parse_serialize(PingResp)). parse_disconnect(_) -> - ?assertEqual({ok, ?DISCONNECT_PACKET(?RC_SUCCESS), <<>>}, parse(<<224, 0>>)). + Packet = ?DISCONNECT_PACKET(?RC_SUCCESS), + ?assertMatch({ok, Packet, <<>>, _}, emqx_frame:parse(<<224, 0>>)). serialize_parse_disconnect(_) -> Packet = ?DISCONNECT_PACKET(?RC_SUCCESS), - ?assertEqual({ok, Packet, <<>>}, parse_serialize(Packet)). + ?assertEqual(Packet, parse_serialize(Packet)). serialize_parse_disconnect_v5(_) -> Packet = ?DISCONNECT_PACKET(?RC_SUCCESS, #{'Session-Expiry-Interval' => 60, - 'Reason-String' => <<"server_moved">>, - 'Server-Reference' => <<"192.168.1.10">>}), - ?assertEqual({ok, Packet, <<>>}, - parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). + 'Reason-String' => <<"server_moved">>, + 'Server-Reference' => <<"192.168.1.10">> + }), + ?assertEqual(Packet, parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). serialize_parse_auth_v5(_) -> Packet = ?AUTH_PACKET(?RC_SUCCESS, #{'Authentication-Method' => <<"oauth2">>, - 'Authentication-Data' => <<"3zekkd">>, - 'Reason-String' => <<"success">>, - 'User-Property' => [{<<"key">>, <<"val">>}]}), - ?assertEqual({ok, Packet, <<>>}, - parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). + 'Authentication-Data' => <<"3zekkd">>, + 'Reason-String' => <<"success">>, + 'User-Property' => [{<<"key">>, <<"val">>}] + }), + ?assertEqual(Packet, parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). parse_serialize(Packet) -> - parse(iolist_to_binary(serialize(Packet))). + parse_serialize(Packet, #{}). parse_serialize(Packet, Opts) when is_map(Opts) -> - parse(iolist_to_binary(serialize(Packet, Opts)), Opts). + Bin = iolist_to_binary(emqx_frame:serialize(Packet, Opts)), + ParseState = emqx_frame:initial_parse_state(Opts), + {ok, NPacket, <<>>, _} = emqx_frame:parse(Bin, ParseState), + NPacket. -parse(Bin) -> - parse(Bin, #{}). - -parse(Bin, Opts) when is_map(Opts) -> - emqx_frame:parse(Bin, emqx_frame:initial_state(Opts)). +serialize_to_binary(Packet) -> + iolist_to_binary(emqx_frame:serialize(Packet)). payload() -> iolist_to_binary(["payload." || _I <- lists:seq(1, 1000)]). + From de978d4771979cdf2226188860da073252990487 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 12 Jun 2019 10:31:44 +0800 Subject: [PATCH 02/33] Update the test cases for emqx_channel, emqx_protocol - Improve emqx_client to Use the new emqx_frame:parse/2 - Update the ct suites for emqx_channel, emqx_ws_channel --- Makefile | 4 +- src/emqx_client.erl | 55 ++++++++------- test/emqx_SUITE.erl | 22 +++--- test/emqx_alarm_handler_SUITE.erl | 15 ++-- ...ction_SUITE.erl => emqx_channel_SUITE.erl} | 20 +++--- test/emqx_protocol_SUITE.erl | 68 ++++++++++--------- ...on_SUITE.erl => emqx_ws_channel_SUITE.erl} | 56 +++++++-------- 7 files changed, 124 insertions(+), 116 deletions(-) rename test/{emqx_connection_SUITE.erl => emqx_channel_SUITE.erl} (83%) rename test/{emqx_ws_connection_SUITE.erl => emqx_ws_channel_SUITE.erl} (68%) diff --git a/Makefile b/Makefile index 0aa6a03c8..acea3f331 100644 --- a/Makefile +++ b/Makefile @@ -11,8 +11,8 @@ CT_SUITES = emqx emqx_client emqx_zone emqx_banned emqx_session \ emqx_mqtt_props emqx_mqueue emqx_net emqx_pqueue emqx_router emqx_sm \ emqx_tables emqx_time emqx_topic emqx_trie emqx_vm emqx_mountpoint \ emqx_listeners emqx_protocol emqx_pool emqx_shared_sub emqx_bridge \ - emqx_hooks emqx_batch emqx_sequence emqx_pmon emqx_pd emqx_gc emqx_ws_connection \ - emqx_packet emqx_connection emqx_tracer emqx_sys_mon emqx_message emqx_os_mon \ + emqx_hooks emqx_batch emqx_sequence emqx_pmon emqx_pd emqx_gc emqx_ws_channel \ + emqx_packet emqx_channel emqx_tracer emqx_sys_mon emqx_message emqx_os_mon \ emqx_vm_mon emqx_alarm_handler emqx_rpc emqx_flapping CT_NODE_NAME = emqxct@127.0.0.1 diff --git a/src/emqx_client.erl b/src/emqx_client.erl index cd83e61ad..e6dbea27b 100644 --- a/src/emqx_client.erl +++ b/src/emqx_client.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_client). @@ -20,7 +22,9 @@ -include("types.hrl"). -include("emqx_client.hrl"). --export([start_link/0, start_link/1]). +-export([ start_link/0 + , start_link/1 + ]). -export([ connect/1 , disconnect/1 @@ -175,7 +179,8 @@ retry_timer :: reference(), session_present :: boolean(), last_packet_id :: packet_id(), - parse_state :: emqx_frame:state()}). + parse_state :: emqx_frame:state() + }). -record(call, {id, from, req, ts}). @@ -202,9 +207,9 @@ -type(subscribe_ret() :: {ok, properties(), [reason_code()]} | {error, term()}). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% API -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- -spec(start_link() -> gen_statem:start_ret()). start_link() -> start_link([]). @@ -352,9 +357,9 @@ disconnect(Client, ReasonCode) -> disconnect(Client, ReasonCode, Properties) -> gen_statem:call(Client, {disconnect, ReasonCode, Properties}). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% For test cases -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- puback(Client, PacketId) when is_integer(PacketId) -> puback(Client, PacketId, ?RC_SUCCESS). @@ -407,9 +412,9 @@ pause(Client) -> resume(Client) -> gen_statem:call(Client, resume). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% gen_statem callbacks -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- init([Options]) -> process_flag(trap_exit, true), @@ -443,7 +448,8 @@ init([Options]) -> ack_timeout = ?DEFAULT_ACK_TIMEOUT, retry_interval = 0, connect_timeout = ?DEFAULT_CONNECT_TIMEOUT, - last_packet_id = 1}), + last_packet_id = 1 + }), {ok, initialized, init_parse_state(State)}. random_client_id() -> @@ -563,9 +569,10 @@ init_will_msg({qos, QoS}, WillMsg) -> WillMsg#mqtt_msg{qos = ?QOS_I(QoS)}. init_parse_state(State = #state{proto_ver = Ver, properties = Properties}) -> - Size = maps:get('Maximum-Packet-Size', Properties, ?MAX_PACKET_SIZE), - State#state{parse_state = emqx_frame:initial_state( - #{max_packet_size => Size, version => Ver})}. + MaxSize = maps:get('Maximum-Packet-Size', Properties, ?MAX_PACKET_SIZE), + ParseState = emqx_frame:initial_parse_state( + #{max_size => MaxSize, version => Ver}), + State#state{parse_state = ParseState}. callback_mode() -> state_functions. @@ -955,9 +962,9 @@ terminate(Reason, _StateName, State = #state{socket = Socket}) -> code_change(_Vsn, State, Data, _Extra) -> {ok, State, Data}. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Internal functions -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- should_ping(Sock) -> case emqx_client_sock:getstat(Sock, [send_oct]) of @@ -1010,7 +1017,8 @@ assign_id(?NO_CLIENT_ID, Props) -> assign_id(Id, _Props) -> Id. -publish_process(?QOS_1, Packet = ?PUBLISH_PACKET(?QOS_1, PacketId), State0 = #state{auto_ack = AutoAck}) -> +publish_process(?QOS_1, Packet = ?PUBLISH_PACKET(?QOS_1, PacketId), + State0 = #state{auto_ack = AutoAck}) -> State = deliver(packet_to_msg(Packet), State0), case AutoAck of true -> send_puback(?PUBACK_PACKET(PacketId), State); @@ -1161,7 +1169,7 @@ msg_to_packet(#mqtt_msg{qos = QoS, dup = Dup, retain = Retain, packet_id = Packe properties = Props}, payload = Payload}. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Socket Connect/Send sock_connect(Hosts, SockOpts, Timeout) -> @@ -1201,7 +1209,7 @@ send(Packet, State = #state{socket = Sock, proto_ver = Ver}) run_sock(State = #state{socket = Sock}) -> emqx_client_sock:setopts(Sock, [{active, once}]), State. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Process incomming process_incoming(<<>>, Packets, State) -> @@ -1209,10 +1217,10 @@ process_incoming(<<>>, Packets, State) -> process_incoming(Bytes, Packets, State = #state{parse_state = ParseState}) -> try emqx_frame:parse(Bytes, ParseState) of - {ok, Packet, Rest} -> - process_incoming(Rest, [Packet|Packets], init_parse_state(State)); - {more, NewParseState} -> - {keep_state, State#state{parse_state = NewParseState}, next_events(Packets)}; + {ok, Packet, Rest, NParseState} -> + process_incoming(Rest, [Packet|Packets], State#state{parse_state = NParseState}); + {ok, NParseState} -> + {keep_state, State#state{parse_state = NParseState}, next_events(Packets)}; {error, Reason} -> {stop, Reason} catch @@ -1227,7 +1235,7 @@ next_events([Packet]) -> next_events(Packets) -> [{next_event, cast, Packet} || Packet <- lists:reverse(Packets)]. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% packet_id generation bump_last_packet_id(State = #state{last_packet_id = Id}) -> @@ -1236,3 +1244,4 @@ bump_last_packet_id(State = #state{last_packet_id = Id}) -> -spec next_packet_id(packet_id()) -> packet_id(). next_packet_id(?MAX_PACKET_ID) -> 1; next_packet_id(Id) -> Id + 1. + diff --git a/test/emqx_SUITE.erl b/test/emqx_SUITE.erl index bcc0a6b6b..a74e16552 100644 --- a/test/emqx_SUITE.erl +++ b/test/emqx_SUITE.erl @@ -110,7 +110,7 @@ mqtt_connect_with_tcp(_) -> Packet = raw_send_serialize(?CLIENT2), emqx_client_sock:send(Sock, Packet), {ok, Data} = gen_tcp:recv(Sock, 0), - {ok, ?CONNACK_PACKET(?CONNACK_INVALID_ID), _} = raw_recv_pase(Data), + {ok, ?CONNACK_PACKET(?CONNACK_INVALID_ID), <<>>, _} = raw_recv_pase(Data), emqx_client_sock:close(Sock). mqtt_connect_with_will_props(_) -> @@ -133,7 +133,7 @@ mqtt_connect_with_ssl_oneway(_) -> emqx_client_sock:send(Sock, Packet), ?assert( receive {ssl, _, ConAck}-> - {ok, ?CONNACK_PACKET(?CONNACK_ACCEPT), _} = raw_recv_pase(ConAck), true + {ok, ?CONNACK_PACKET(?CONNACK_ACCEPT), <<>>, _} = raw_recv_pase(ConAck), true after 1000 -> false end), @@ -152,7 +152,7 @@ mqtt_connect_with_ssl_twoway(_Config) -> timer:sleep(500), ?assert( receive {ssl, _, Data}-> - {ok, ?CONNACK_PACKET(?CONNACK_ACCEPT), _} = raw_recv_pase(Data), true + {ok, ?CONNACK_PACKET(?CONNACK_ACCEPT), <<>>, _} = raw_recv_pase(Data), true after 1000 -> false end), @@ -167,19 +167,19 @@ mqtt_connect_with_ws(_Config) -> Packet = raw_send_serialize(?CLIENT), ok = rfc6455_client:send_binary(WS, Packet), {binary, CONACK} = rfc6455_client:recv(WS), - {ok, ?CONNACK_PACKET(?CONNACK_ACCEPT), _} = raw_recv_pase(CONACK), + {ok, ?CONNACK_PACKET(?CONNACK_ACCEPT), <<>>, _} = raw_recv_pase(CONACK), %% Sub Packet SubPacket = raw_send_serialize(?SUBPACKET), rfc6455_client:send_binary(WS, SubPacket), {binary, SubAck} = rfc6455_client:recv(WS), - {ok, ?SUBACK_PACKET(?PACKETID, ?SUBCODE), _} = raw_recv_pase(SubAck), + {ok, ?SUBACK_PACKET(?PACKETID, ?SUBCODE), <<>>, _} = raw_recv_pase(SubAck), %% Pub Packet QoS 1 PubPacket = raw_send_serialize(?PUBPACKET), rfc6455_client:send_binary(WS, PubPacket), {binary, PubAck} = rfc6455_client:recv(WS), - {ok, ?PUBACK_PACKET(?PACKETID), _} = raw_recv_pase(PubAck), + {ok, ?PUBACK_PACKET(?PACKETID), <<>>, _} = raw_recv_pase(PubAck), {close, _} = rfc6455_client:close(WS), ok. @@ -189,18 +189,18 @@ packet_size(_Config) -> Packet = raw_send_serialize(?CLIENT), emqx_client_sock:send(Sock, Packet), {ok, Data} = gen_tcp:recv(Sock, 0), - {ok, ?CONNACK_PACKET(?CONNACK_ACCEPT), _} = raw_recv_pase(Data), + {ok, ?CONNACK_PACKET(?CONNACK_ACCEPT), <<>>, _} = raw_recv_pase(Data), %% Pub Packet QoS 1 PubPacket = raw_send_serialize(?BIG_PUBPACKET), emqx_client_sock:send(Sock, PubPacket), {ok, Data1} = gen_tcp:recv(Sock, 0), - {ok, ?PUBACK_PACKET(?PACKETID), _} = raw_recv_pase(Data1), + {ok, ?PUBACK_PACKET(?PACKETID), <<>>, _} = raw_recv_pase(Data1), emqx_client_sock:close(Sock). raw_send_serialize(Packet) -> emqx_frame:serialize(Packet). -raw_recv_pase(P) -> - emqx_frame:parse(P, {none, #{max_packet_size => ?MAX_PACKET_SIZE, - version => ?MQTT_PROTO_V4} }). +raw_recv_pase(Bin) -> + emqx_frame:parse(Bin). + diff --git a/test/emqx_alarm_handler_SUITE.erl b/test/emqx_alarm_handler_SUITE.erl index d50c7fb5f..25a8303e6 100644 --- a/test/emqx_alarm_handler_SUITE.erl +++ b/test/emqx_alarm_handler_SUITE.erl @@ -60,7 +60,7 @@ t_alarm_handler(_) -> #{version => ?MQTT_PROTO_V5} )), {ok, Data} = gen_tcp:recv(Sock, 0), - {ok, ?CONNACK_PACKET(?RC_SUCCESS), _} = raw_recv_parse(Data, ?MQTT_PROTO_V5), + {ok, ?CONNACK_PACKET(?RC_SUCCESS), <<>>, _} = raw_recv_parse(Data, ?MQTT_PROTO_V5), Topic1 = emqx_topic:systop(<<"alarms/alarm_for_test/alert">>), Topic2 = emqx_topic:systop(<<"alarms/alarm_for_test/clear">>), @@ -74,7 +74,7 @@ t_alarm_handler(_) -> #{version => ?MQTT_PROTO_V5})), {ok, Data2} = gen_tcp:recv(Sock, 0), - {ok, ?SUBACK_PACKET(1, #{}, [2, 2]), _} = raw_recv_parse(Data2, ?MQTT_PROTO_V5), + {ok, ?SUBACK_PACKET(1, #{}, [2, 2]), <<>>, _} = raw_recv_parse(Data2, ?MQTT_PROTO_V5), alarm_handler:set_alarm({alarm_for_test, #alarm{id = alarm_for_test, severity = error, @@ -83,7 +83,7 @@ t_alarm_handler(_) -> {ok, Data3} = gen_tcp:recv(Sock, 0), - {ok, ?PUBLISH_PACKET(?QOS_0, Topic1, _, _), _} = raw_recv_parse(Data3, ?MQTT_PROTO_V5), + {ok, ?PUBLISH_PACKET(?QOS_0, Topic1, _, _), <<>>, _} = raw_recv_parse(Data3, ?MQTT_PROTO_V5), ?assertEqual(true, lists:keymember(alarm_for_test, 1, emqx_alarm_handler:get_alarms())), @@ -91,7 +91,7 @@ t_alarm_handler(_) -> {ok, Data4} = gen_tcp:recv(Sock, 0), - {ok, ?PUBLISH_PACKET(?QOS_0, Topic2, _, _), _} = raw_recv_parse(Data4, ?MQTT_PROTO_V5), + {ok, ?PUBLISH_PACKET(?QOS_0, Topic2, _, _), <<>>, _} = raw_recv_parse(Data4, ?MQTT_PROTO_V5), ?assertEqual(false, lists:keymember(alarm_for_test, 1, emqx_alarm_handler:get_alarms())) @@ -119,6 +119,7 @@ raw_send_serialize(Packet) -> raw_send_serialize(Packet, Opts) -> emqx_frame:serialize(Packet, Opts). -raw_recv_parse(P, ProtoVersion) -> - emqx_frame:parse(P, {none, #{max_packet_size => ?MAX_PACKET_SIZE, - version => ProtoVersion}}). +raw_recv_parse(Bin, ProtoVer) -> + emqx_frame:parse(Bin, {none, #{max_size => ?MAX_PACKET_SIZE, + version => ProtoVer}}). + diff --git a/test/emqx_connection_SUITE.erl b/test/emqx_channel_SUITE.erl similarity index 83% rename from test/emqx_connection_SUITE.erl rename to test/emqx_channel_SUITE.erl index 2c124fd44..7d8b216f2 100644 --- a/test/emqx_connection_SUITE.erl +++ b/test/emqx_channel_SUITE.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,18 +12,19 @@ %% 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_connection_SUITE). +-module(emqx_channel_SUITE). -compile(export_all). -compile(nowarn_export_all). +-include("emqx_mqtt.hrl"). + -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). --include("emqx_mqtt.hrl"). - all() -> [t_connect_api]. @@ -40,13 +42,13 @@ t_connect_api(_Config) -> {password, <<"pass1">>}]), {ok, _} = emqx_client:connect(T1), CPid = emqx_cm:lookup_conn_pid(<<"client1">>), - ConnStats = emqx_connection:stats(CPid), + ConnStats = emqx_channel:stats(CPid), ok = t_stats(ConnStats), - ConnAttrs = emqx_connection:attrs(CPid), + ConnAttrs = emqx_channel:attrs(CPid), ok = t_attrs(ConnAttrs), - ConnInfo = emqx_connection:info(CPid), + ConnInfo = emqx_channel:info(CPid), ok = t_info(ConnInfo), - SessionPid = emqx_connection:session(CPid), + SessionPid = emqx_channel:session(CPid), true = is_pid(SessionPid), emqx_client:disconnect(T1). @@ -59,7 +61,7 @@ t_info(ConnInfo) -> t_attrs(AttrsData) -> ?assertEqual(<<"client1">>, maps:get(client_id, AttrsData)), - ?assertEqual(emqx_connection, maps:get(conn_mod, AttrsData)), + ?assertEqual(emqx_channel, maps:get(conn_mod, AttrsData)), ?assertEqual(<<"testuser1">>, maps:get(username, AttrsData)). t_stats(StatsData) -> diff --git a/test/emqx_protocol_SUITE.erl b/test/emqx_protocol_SUITE.erl index 9e8107665..60d298008 100644 --- a/test/emqx_protocol_SUITE.erl +++ b/test/emqx_protocol_SUITE.erl @@ -139,7 +139,7 @@ connect_v4(_) -> })), emqx_client_sock:send(Sock, ConnectPacket), {ok, Data} = gen_tcp:recv(Sock, 0), - {ok, ?CONNACK_PACKET(?CONNACK_ACCEPT), _} = raw_recv_parse(Data, ?MQTT_PROTO_V4), + {ok, ?CONNACK_PACKET(?CONNACK_ACCEPT), <<>>, _} = raw_recv_parse(Data, ?MQTT_PROTO_V4), emqx_client_sock:send(Sock, ConnectPacket), {error, closed} = gen_tcp:recv(Sock, 0) @@ -156,7 +156,7 @@ connect_v5(_) -> properties = #{'Request-Response-Information' => -1}}))), {ok, Data} = gen_tcp:recv(Sock, 0), - {ok, ?DISCONNECT_PACKET(?RC_PROTOCOL_ERROR), _} = raw_recv_parse(Data, ?MQTT_PROTO_V5) + {ok, ?DISCONNECT_PACKET(?RC_PROTOCOL_ERROR), <<>>, _} = raw_recv_parse(Data, ?MQTT_PROTO_V5) end), with_connection(fun([Sock]) -> @@ -168,7 +168,7 @@ connect_v5(_) -> properties = #{'Request-Problem-Information' => 2}}))), {ok, Data} = gen_tcp:recv(Sock, 0), - {ok, ?DISCONNECT_PACKET(?RC_PROTOCOL_ERROR), _} = raw_recv_parse(Data, ?MQTT_PROTO_V5) + {ok, ?DISCONNECT_PACKET(?RC_PROTOCOL_ERROR), <<>>, _} = raw_recv_parse(Data, ?MQTT_PROTO_V5) end), with_connection(fun([Sock]) -> @@ -181,7 +181,7 @@ connect_v5(_) -> #{'Request-Response-Information' => 1}}) )), {ok, Data} = gen_tcp:recv(Sock, 0), - {ok, ?CONNACK_PACKET(?RC_SUCCESS, 0, Props), _} = + {ok, ?CONNACK_PACKET(?RC_SUCCESS, 0, Props), <<>>, _} = raw_recv_parse(Data, ?MQTT_PROTO_V5), ?assertNot(maps:is_key('Response-Information', Props)) end), @@ -202,7 +202,7 @@ connect_v5(_) -> )), {ok, Data} = gen_tcp:recv(Sock, 0), {ok, ?CONNACK_PACKET(?RC_SUCCESS, 0, - #{'Topic-Alias-Maximum' := 20}), _} = + #{'Topic-Alias-Maximum' := 20}), <<>>, _} = raw_recv_parse(Data, ?MQTT_PROTO_V5), emqx_client_sock:send(Sock, raw_send_serialize( @@ -211,7 +211,7 @@ connect_v5(_) -> )), {ok, Data2} = gen_tcp:recv(Sock, 0), - {ok, ?DISCONNECT_PACKET(?RC_TOPIC_ALIAS_INVALID), _} = raw_recv_parse(Data2, ?MQTT_PROTO_V5) + {ok, ?DISCONNECT_PACKET(?RC_TOPIC_ALIAS_INVALID), <<>>, _} = raw_recv_parse(Data2, ?MQTT_PROTO_V5) end), % topic alias maximum @@ -227,7 +227,7 @@ connect_v5(_) -> )), {ok, Data} = gen_tcp:recv(Sock, 0), {ok, ?CONNACK_PACKET(?RC_SUCCESS, 0, - #{'Topic-Alias-Maximum' := 20}), _} = + #{'Topic-Alias-Maximum' := 20}), <<>>, _} = raw_recv_parse(Data, ?MQTT_PROTO_V5), emqx_client_sock:send(Sock, raw_send_serialize(?SUBSCRIBE_PACKET(1, [{<<"TopicA">>, #{rh => 1, @@ -237,7 +237,7 @@ connect_v5(_) -> rc => 0}}]), #{version => ?MQTT_PROTO_V5})), {ok, Data2} = gen_tcp:recv(Sock, 0), - {ok, ?SUBACK_PACKET(1, #{}, [2]), _} = raw_recv_parse(Data2, ?MQTT_PROTO_V5), + {ok, ?SUBACK_PACKET(1, #{}, [2]), <<>>, _} = raw_recv_parse(Data2, ?MQTT_PROTO_V5), emqx_client_sock:send(Sock, raw_send_serialize( @@ -247,11 +247,11 @@ connect_v5(_) -> {ok, Data3} = gen_tcp:recv(Sock, 0), - {ok, ?PUBACK_PACKET(1, 0), _} = raw_recv_parse(Data3, ?MQTT_PROTO_V5), + {ok, ?PUBACK_PACKET(1, 0), <<>>, _} = raw_recv_parse(Data3, ?MQTT_PROTO_V5), {ok, Data4} = gen_tcp:recv(Sock, 0), - {ok, ?PUBLISH_PACKET(?QOS_1, <<"TopicA">>, _, <<"hello">>), _} = raw_recv_parse(Data4, ?MQTT_PROTO_V5), + {ok, ?PUBLISH_PACKET(?QOS_1, <<"TopicA">>, _, <<"hello">>), <<>>, _} = raw_recv_parse(Data4, ?MQTT_PROTO_V5), emqx_client_sock:send(Sock, raw_send_serialize( @@ -260,7 +260,7 @@ connect_v5(_) -> )), {ok, Data5} = gen_tcp:recv(Sock, 0), - {ok, ?DISCONNECT_PACKET(?RC_TOPIC_ALIAS_INVALID), _} = raw_recv_parse(Data5, ?MQTT_PROTO_V5) + {ok, ?DISCONNECT_PACKET(?RC_TOPIC_ALIAS_INVALID), <<>>, _} = raw_recv_parse(Data5, ?MQTT_PROTO_V5) end), % test clean start @@ -276,7 +276,7 @@ connect_v5(_) -> #{'Session-Expiry-Interval' => 10}}) )), {ok, Data} = gen_tcp:recv(Sock, 0), - {ok, ?CONNACK_PACKET(?RC_SUCCESS, 0), _} = raw_recv_parse(Data, ?MQTT_PROTO_V5), + {ok, ?CONNACK_PACKET(?RC_SUCCESS, 0), <<>>, _} = raw_recv_parse(Data, ?MQTT_PROTO_V5), emqx_client_sock:send(Sock, raw_send_serialize( ?DISCONNECT_PACKET(?RC_SUCCESS) )) @@ -296,7 +296,7 @@ connect_v5(_) -> #{'Session-Expiry-Interval' => 10}}) )), {ok, Data} = gen_tcp:recv(Sock, 0), - {ok, ?CONNACK_PACKET(?RC_SUCCESS, 1), _} = raw_recv_parse(Data, ?MQTT_PROTO_V5) + {ok, ?CONNACK_PACKET(?RC_SUCCESS, 1), <<>>, _} = raw_recv_parse(Data, ?MQTT_PROTO_V5) end), % test will message publish and cancel @@ -320,7 +320,7 @@ connect_v5(_) -> ) ), {ok, Data} = gen_tcp:recv(Sock, 0), - {ok, ?CONNACK_PACKET(?RC_SUCCESS, 0), _} = raw_recv_parse(Data, ?MQTT_PROTO_V5), + {ok, ?CONNACK_PACKET(?RC_SUCCESS, 0), <<>>, _} = raw_recv_parse(Data, ?MQTT_PROTO_V5), {ok, Sock2} = emqx_client_sock:connect({127, 0, 0, 1}, 1883, [binary, {packet, raw}, @@ -335,7 +335,7 @@ connect_v5(_) -> rc => 0}}]), #{version => ?MQTT_PROTO_V5})), {ok, SubData} = gen_tcp:recv(Sock2, 0), - {ok, ?SUBACK_PACKET(1, #{}, [2]), _} = raw_recv_parse(SubData, ?MQTT_PROTO_V5), + {ok, ?SUBACK_PACKET(1, #{}, [2]), <<>>, _} = raw_recv_parse(SubData, ?MQTT_PROTO_V5), emqx_client_sock:send(Sock, raw_send_serialize( ?DISCONNECT_PACKET(?RC_SUCCESS))), @@ -367,7 +367,7 @@ connect_v5(_) -> ) ), {ok, Data3} = gen_tcp:recv(Sock3, 0), - {ok, ?CONNACK_PACKET(?RC_SUCCESS, 0), _} = raw_recv_parse(Data3, ?MQTT_PROTO_V5), + {ok, ?CONNACK_PACKET(?RC_SUCCESS, 0), <<>>, _} = raw_recv_parse(Data3, ?MQTT_PROTO_V5), emqx_client_sock:send(Sock3, raw_send_serialize( ?DISCONNECT_PACKET(?RC_DISCONNECT_WITH_WILL_MESSAGE), @@ -376,7 +376,8 @@ connect_v5(_) -> ), {ok, WillData} = gen_tcp:recv(Sock2, 0, 5000), - {ok, ?PUBLISH_PACKET(?QOS_1, <<"TopicA">>, _, <<"will message 2">>), _} = raw_recv_parse(WillData, ?MQTT_PROTO_V5) + {ok, ?PUBLISH_PACKET(?QOS_1, <<"TopicA">>, _, <<"will message 2">>), <<>>, _} + = raw_recv_parse(WillData, ?MQTT_PROTO_V5) end), % duplicate client id @@ -393,7 +394,7 @@ connect_v5(_) -> #{'Session-Expiry-Interval' => 10}}) )), {ok, Data} = gen_tcp:recv(Sock, 0), - {ok, ?CONNACK_PACKET(?RC_SUCCESS, 0), _} = raw_recv_parse(Data, ?MQTT_PROTO_V5), + {ok, ?CONNACK_PACKET(?RC_SUCCESS, 0), <<>>, _} = raw_recv_parse(Data, ?MQTT_PROTO_V5), emqx_client_sock:send(Sock1, raw_send_serialize( @@ -408,7 +409,7 @@ connect_v5(_) -> #{'Session-Expiry-Interval' => 10}}) )), {ok, Data1} = gen_tcp:recv(Sock1, 0), - {ok, ?CONNACK_PACKET(?RC_SUCCESS, 0), _} = raw_recv_parse(Data1, ?MQTT_PROTO_V5), + {ok, ?CONNACK_PACKET(?RC_SUCCESS, 0), <<>>, _} = raw_recv_parse(Data1, ?MQTT_PROTO_V5), emqx_client_sock:send(Sock, raw_send_serialize(?SUBSCRIBE_PACKET(1, [{<<"TopicA">>, #{rh => 1, qos => ?QOS_2, @@ -418,7 +419,7 @@ connect_v5(_) -> #{version => ?MQTT_PROTO_V5})), {ok, SubData} = gen_tcp:recv(Sock, 0), - {ok, ?SUBACK_PACKET(1, #{}, [2]), _} = raw_recv_parse(SubData, ?MQTT_PROTO_V5), + {ok, ?SUBACK_PACKET(1, #{}, [2]), <<>>, _} = raw_recv_parse(SubData, ?MQTT_PROTO_V5), emqx_client_sock:send(Sock1, raw_send_serialize(?SUBSCRIBE_PACKET(1, [{<<"TopicA">>, #{rh => 1, qos => ?QOS_2, @@ -428,7 +429,7 @@ connect_v5(_) -> #{version => ?MQTT_PROTO_V5})), {ok, SubData1} = gen_tcp:recv(Sock1, 0), - {ok, ?SUBACK_PACKET(1, #{}, [2]), _} = raw_recv_parse(SubData1, ?MQTT_PROTO_V5) + {ok, ?SUBACK_PACKET(1, #{}, [2]), <<>>, _} = raw_recv_parse(SubData1, ?MQTT_PROTO_V5) end, 2), ok. @@ -441,7 +442,7 @@ do_connect(Sock, ProtoVer) -> proto_ver = ProtoVer }))), {ok, Data} = gen_tcp:recv(Sock, 0), - {ok, ?CONNACK_PACKET(?CONNACK_ACCEPT), _} = raw_recv_parse(Data, ProtoVer). + {ok, ?CONNACK_PACKET(?CONNACK_ACCEPT), <<>>, _} = raw_recv_parse(Data, ProtoVer). subscribe_v4(_) -> with_connection(fun([Sock]) -> @@ -455,7 +456,7 @@ subscribe_v4(_) -> rc => 0}}])), emqx_client_sock:send(Sock, SubPacket), {ok, Data} = gen_tcp:recv(Sock, 0), - {ok, ?SUBACK_PACKET(15, _), _} = raw_recv_parse(Data, ?MQTT_PROTO_V4) + {ok, ?SUBACK_PACKET(15, _), <<>>, _} = raw_recv_parse(Data, ?MQTT_PROTO_V4) end), ok. @@ -466,7 +467,7 @@ subscribe_v5(_) -> #{version => ?MQTT_PROTO_V5}), emqx_client_sock:send(Sock, SubPacket), {ok, DisConnData} = gen_tcp:recv(Sock, 0), - {ok, ?DISCONNECT_PACKET(?RC_TOPIC_FILTER_INVALID), _} = + {ok, ?DISCONNECT_PACKET(?RC_TOPIC_FILTER_INVALID), <<>>, _} = raw_recv_parse(DisConnData, ?MQTT_PROTO_V5) end), with_connection(fun([Sock]) -> @@ -479,8 +480,9 @@ subscribe_v5(_) -> #{version => ?MQTT_PROTO_V5}), emqx_client_sock:send(Sock, SubPacket), {ok, DisConnData} = gen_tcp:recv(Sock, 0), - {ok, ?DISCONNECT_PACKET(?RC_MALFORMED_PACKET), _} = - raw_recv_parse(DisConnData, ?MQTT_PROTO_V5) + ?assertMatch( + {ok, ?DISCONNECT_PACKET(?RC_MALFORMED_PACKET), <<>>, _}, + raw_recv_parse(DisConnData, ?MQTT_PROTO_V5)) end), with_connection(fun([Sock]) -> do_connect(Sock, ?MQTT_PROTO_V5), @@ -493,8 +495,9 @@ subscribe_v5(_) -> #{version => ?MQTT_PROTO_V5}), emqx_client_sock:send(Sock, SubPacket), {ok, DisConnData} = gen_tcp:recv(Sock, 0), - {ok, ?DISCONNECT_PACKET(?RC_SUBSCRIPTION_IDENTIFIERS_NOT_SUPPORTED), _} = - raw_recv_parse(DisConnData, ?MQTT_PROTO_V5) + ?assertMatch( + {ok, ?DISCONNECT_PACKET(?RC_SUBSCRIPTION_IDENTIFIERS_NOT_SUPPORTED), <<>>, _}, + raw_recv_parse(DisConnData, ?MQTT_PROTO_V5)) end), with_connection(fun([Sock]) -> do_connect(Sock, ?MQTT_PROTO_V5), @@ -507,8 +510,8 @@ subscribe_v5(_) -> #{version => ?MQTT_PROTO_V5}), emqx_client_sock:send(Sock, SubPacket), {ok, SubData} = gen_tcp:recv(Sock, 0), - {ok, ?SUBACK_PACKET(1, #{}, [2]), _} = - raw_recv_parse(SubData, ?MQTT_PROTO_V5) + {ok, ?SUBACK_PACKET(1, #{}, [2]), <<>>, _} + = raw_recv_parse(SubData, ?MQTT_PROTO_V5) end), ok. @@ -524,9 +527,8 @@ raw_send_serialize(Packet) -> raw_send_serialize(Packet, Opts) -> emqx_frame:serialize(Packet, Opts). -raw_recv_parse(P, ProtoVersion) -> - emqx_frame:parse(P, {none, #{max_packet_size => ?MAX_PACKET_SIZE, - version => ProtoVersion}}). +raw_recv_parse(Bin, ProtoVer) -> + emqx_frame:parse(Bin, emqx_frame:initial_parse_state(#{version => ProtoVer})). acl_deny_action_ct(_) -> emqx_zone:set_env(external, acl_deny_action, disconnect), diff --git a/test/emqx_ws_connection_SUITE.erl b/test/emqx_ws_channel_SUITE.erl similarity index 68% rename from test/emqx_ws_connection_SUITE.erl rename to test/emqx_ws_channel_SUITE.erl index c086ef6b7..4edbc3ab5 100644 --- a/test/emqx_ws_connection_SUITE.erl +++ b/test/emqx_ws_channel_SUITE.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,29 +12,16 @@ %% 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_ws_connection_SUITE). +-module(emqx_ws_channel_SUITE). -compile(export_all). -compile(nowarn_export_all). --include_lib("eunit/include/eunit.hrl"). - --include_lib("common_test/include/ct.hrl"). - -include("emqx_mqtt.hrl"). - - --define(CLIENT, ?CONNECT_PACKET(#mqtt_packet_connect{ - client_id = <<"mqtt_client">>, - username = <<"admin">>, - password = <<"public">>})). - --define(SUBCODE, [0]). - --define(PACKETID, 1). - --define(PUBQOS, 1). +-include_lib("eunit/include/eunit.hrl"). +-include_lib("common_test/include/ct.hrl"). all() -> [t_ws_connect_api]. @@ -48,29 +36,34 @@ end_per_suite(_Config) -> t_ws_connect_api(_Config) -> WS = rfc6455_client:new("ws://127.0.0.1:8083" ++ "/mqtt", self()), {ok, _} = rfc6455_client:open(WS), - Packet = raw_send_serialize(?CLIENT), - ok = rfc6455_client:send_binary(WS, Packet), - {binary, CONACK} = rfc6455_client:recv(WS), - {ok, ?CONNACK_PACKET(?CONNACK_ACCEPT), _} = raw_recv_pase(CONACK), + Connect = ?CONNECT_PACKET( + #mqtt_packet_connect{ + client_id = <<"mqtt_client">>, + username = <<"admin">>, + password = <<"public">> + }), + ok = rfc6455_client:send_binary(WS, raw_send_serialize(Connect)), + {binary, Bin} = rfc6455_client:recv(WS), + Connack = ?CONNACK_PACKET(?CONNACK_ACCEPT), + {ok, Connack, <<>>, _} = raw_recv_pase(Bin), Pid = emqx_cm:lookup_conn_pid(<<"mqtt_client">>), - ConnInfo = emqx_ws_connection:info(Pid), + ConnInfo = emqx_ws_channel:info(Pid), ok = t_info(ConnInfo), - ConnAttrs = emqx_ws_connection:attrs(Pid), + ConnAttrs = emqx_ws_channel:attrs(Pid), ok = t_attrs(ConnAttrs), - ConnStats = emqx_ws_connection:stats(Pid), + ConnStats = emqx_ws_channel:stats(Pid), ok = t_stats(ConnStats), - SessionPid = emqx_ws_connection:session(Pid), + SessionPid = emqx_ws_channel:session(Pid), true = is_pid(SessionPid), - ok = emqx_ws_connection:kick(Pid), + ok = emqx_ws_channel:kick(Pid), {close, _} = rfc6455_client:close(WS), ok. raw_send_serialize(Packet) -> emqx_frame:serialize(Packet). -raw_recv_pase(P) -> - emqx_frame:parse(P, {none, #{max_packet_size => ?MAX_PACKET_SIZE, - version => ?MQTT_PROTO_V4} }). +raw_recv_pase(Packet) -> + emqx_frame:parse(Packet). t_info(InfoData) -> ?assertEqual(websocket, maps:get(socktype, InfoData)), @@ -81,7 +74,7 @@ t_info(InfoData) -> t_attrs(AttrsData) -> ?assertEqual(<<"mqtt_client">>, maps:get(client_id, AttrsData)), - ?assertEqual(emqx_ws_connection, maps:get(conn_mod, AttrsData)), + ?assertEqual(emqx_ws_channel, maps:get(conn_mod, AttrsData)), ?assertEqual(<<"admin">>, maps:get(username, AttrsData)). t_stats(StatsData) -> @@ -92,3 +85,4 @@ t_stats(StatsData) -> ?assertEqual(true, proplists:get_value(recv_pkt, StatsData) =:=1), ?assertEqual(true, proplists:get_value(recv_msg, StatsData) >=0), ?assertEqual(true, proplists:get_value(send_pkt, StatsData) =:=1). + From d0658476657fce46c3c9810fe763a5e61c0dce07 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sun, 16 Jun 2019 10:28:23 +0800 Subject: [PATCH 03/33] Remove session record --- ...x_sm_registry.erl => emqx_cm_registry.erl} | 0 src/emqx_cm_sup.erl | 49 ++-- src/emqx_session_sup.erl | 265 ------------------ src/emqx_sm_sup.erl | 64 ----- 4 files changed, 32 insertions(+), 346 deletions(-) rename src/{emqx_sm_registry.erl => emqx_cm_registry.erl} (100%) delete mode 100644 src/emqx_session_sup.erl delete mode 100644 src/emqx_sm_sup.erl diff --git a/src/emqx_sm_registry.erl b/src/emqx_cm_registry.erl similarity index 100% rename from src/emqx_sm_registry.erl rename to src/emqx_cm_registry.erl diff --git a/src/emqx_cm_sup.erl b/src/emqx_cm_sup.erl index 19940da05..65702b26f 100644 --- a/src/emqx_cm_sup.erl +++ b/src/emqx_cm_sup.erl @@ -12,7 +12,7 @@ %% See the License for the specific language governing permissions and %% limitations under the License. --module(emqx_cm_sup). +-module(emqx_sm_sup). -behaviour(supervisor). @@ -24,26 +24,41 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). init([]) -> - Banned = #{id => banned, - start => {emqx_banned, start_link, []}, + %% Session locker + Locker = #{id => locker, + start => {emqx_sm_locker, start_link, []}, restart => permanent, - shutdown => 1000, + shutdown => 5000, type => worker, - modules => [emqx_banned]}, - FlappingOption = emqx_config:get_env(flapping_clean_interval, 3600000), - Flapping = #{id => flapping, - start => {emqx_flapping, start_link, [FlappingOption]}, + modules => [emqx_sm_locker] + }, + %% Session registry + Registry = #{id => registry, + start => {emqx_sm_registry, start_link, []}, restart => permanent, - shutdown => 1000, + shutdown => 5000, type => worker, - modules => [emqx_flapping]}, + modules => [emqx_sm_registry] + }, + %% Session Manager Manager = #{id => manager, - start => {emqx_cm, start_link, []}, + start => {emqx_sm, start_link, []}, restart => permanent, - shutdown => 2000, + shutdown => 5000, type => worker, - modules => [emqx_cm]}, - SupFlags = #{strategy => one_for_one, - intensity => 100, - period => 10}, - {ok, {SupFlags, [Banned, Manager, Flapping]}}. + modules => [emqx_sm] + }, + %% Session Sup + SessSpec = #{start => {emqx_session, start_link, []}, + shutdown => brutal_kill, + clean_down => fun emqx_sm:clean_down/1 + }, + SessionSup = #{id => session_sup, + start => {emqx_session_sup, start_link, [SessSpec ]}, + restart => transient, + shutdown => infinity, + type => supervisor, + modules => [emqx_session_sup] + }, + {ok, {{rest_for_one, 10, 3600}, [Locker, Registry, Manager, SessionSup]}}. + diff --git a/src/emqx_session_sup.erl b/src/emqx_session_sup.erl deleted file mode 100644 index ad721d5fd..000000000 --- a/src/emqx_session_sup.erl +++ /dev/null @@ -1,265 +0,0 @@ -%% Copyright (c) 2013-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_session_sup). - --behaviour(gen_server). - --include("logger.hrl"). --include("types.hrl"). - --export([start_link/1]). - --export([ start_session/1 - , count_sessions/0 - ]). - -%% gen_server callbacks --export([ init/1 - , handle_call/3 - , handle_cast/2 - , handle_info/2 - , terminate/2 - , code_change/3 - ]). - --type(shutdown() :: brutal_kill | infinity | pos_integer()). - --record(state, - { sessions :: #{pid() => emqx_types:client_id()} - , mfargs :: mfa() - , shutdown :: shutdown() - , clean_down :: fun() - }). - --define(SUP, ?MODULE). --define(BATCH_EXIT, 100000). - -%% @doc Start session supervisor. --spec(start_link(map()) -> startlink_ret()). -start_link(SessSpec) when is_map(SessSpec) -> - gen_server:start_link({local, ?SUP}, ?MODULE, [SessSpec], []). - -%%------------------------------------------------------------------------------ -%% API -%%------------------------------------------------------------------------------ - -%% @doc Start a session. --spec(start_session(map()) -> startlink_ret()). -start_session(SessAttrs) -> - gen_server:call(?SUP, {start_session, SessAttrs}, infinity). - -%% @doc Count sessions. --spec(count_sessions() -> non_neg_integer()). -count_sessions() -> - gen_server:call(?SUP, count_sessions, infinity). - -%%------------------------------------------------------------------------------ -%% gen_server callbacks -%%------------------------------------------------------------------------------ - -init([Spec]) -> - process_flag(trap_exit, true), - MFA = maps:get(start, Spec), - Shutdown = maps:get(shutdown, Spec, brutal_kill), - CleanDown = maps:get(clean_down, Spec, undefined), - State = #state{sessions = #{}, - mfargs = MFA, - shutdown = Shutdown, - clean_down = CleanDown - }, - {ok, State}. - -handle_call({start_session, SessAttrs = #{client_id := ClientId}}, _From, - State = #state{sessions = SessMap, mfargs = {M, F, Args}}) -> - try erlang:apply(M, F, [SessAttrs | Args]) of - {ok, Pid} -> - reply({ok, Pid}, State#state{sessions = maps:put(Pid, ClientId, SessMap)}); - ignore -> - reply(ignore, State); - {error, Reason} -> - reply({error, Reason}, State) - catch - _:Error:Stk -> - ?LOG(error, "[Session Supervisor] Failed to start session ~p: ~p, stacktrace:~n~p", - [ClientId, Error, Stk]), - reply({error, Error}, State) - end; - -handle_call(count_sessions, _From, State = #state{sessions = SessMap}) -> - {reply, maps:size(SessMap), State}; - -handle_call(Req, _From, State) -> - ?LOG(error, "[Session Supervisor] Unexpected call: ~p", [Req]), - {reply, ignored, State}. - -handle_cast(Msg, State) -> - ?LOG(error, "[Session Supervisor] Unexpected cast: ~p", [Msg]), - {noreply, State}. - -handle_info({'EXIT', Pid, _Reason}, State = #state{sessions = SessMap, clean_down = CleanDown}) -> - SessPids = [Pid | drain_exit(?BATCH_EXIT, [])], - {SessItems, SessMap1} = erase_all(SessPids, SessMap), - (CleanDown =:= undefined) - orelse emqx_pool:async_submit( - fun lists:foreach/2, [CleanDown, SessItems]), - {noreply, State#state{sessions = SessMap1}}; - -handle_info(Info, State) -> - ?LOG(notice, "[Session Supervisor] Unexpected info: ~p", [Info]), - {noreply, State}. - -terminate(_Reason, State) -> - terminate_children(State). - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -%%------------------------------------------------------------------------------ -%% Internal functions -%%------------------------------------------------------------------------------ - -drain_exit(0, Acc) -> - lists:reverse(Acc); -drain_exit(Cnt, Acc) -> - receive - {'EXIT', Pid, _Reason} -> - drain_exit(Cnt - 1, [Pid|Acc]) - after 0 -> - lists:reverse(Acc) - end. - -erase_all(Pids, Map) -> - lists:foldl( - fun(Pid, {Acc, M}) -> - case maps:take(Pid, M) of - {Val, M1} -> - {[{Val, Pid}|Acc], M1}; - error -> - {Acc, M} - end - end, {[], Map}, Pids). - -terminate_children(State = #state{sessions = SessMap, shutdown = Shutdown}) -> - {Pids, EStack0} = monitor_children(SessMap), - Sz = sets:size(Pids), - EStack = - case Shutdown of - brutal_kill -> - sets:fold(fun(P, _) -> exit(P, kill) end, ok, Pids), - wait_children(Shutdown, Pids, Sz, undefined, EStack0); - infinity -> - sets:fold(fun(P, _) -> exit(P, shutdown) end, ok, Pids), - wait_children(Shutdown, Pids, Sz, undefined, EStack0); - Time when is_integer(Time) -> - sets:fold(fun(P, _) -> exit(P, shutdown) end, ok, Pids), - TRef = erlang:start_timer(Time, self(), kill), - wait_children(Shutdown, Pids, Sz, TRef, EStack0) - end, - %% Unroll stacked errors and report them - dict:fold(fun(Reason, Pid, _) -> - report_error(connection_shutdown_error, Reason, Pid, State) - end, ok, EStack). - -monitor_children(SessMap) -> - lists:foldl( - fun(Pid, {Pids, EStack}) -> - case monitor_child(Pid) of - ok -> - {sets:add_element(Pid, Pids), EStack}; - {error, normal} -> - {Pids, EStack}; - {error, Reason} -> - {Pids, dict:append(Reason, Pid, EStack)} - end - end, {sets:new(), dict:new()}, maps:keys(SessMap)). - -%% Help function to shutdown/2 switches from link to monitor approach -monitor_child(Pid) -> - %% Do the monitor operation first so that if the child dies - %% before the monitoring is done causing a 'DOWN'-message with - %% reason noproc, we will get the real reason in the 'EXIT'-message - %% unless a naughty child has already done unlink... - erlang:monitor(process, Pid), - unlink(Pid), - - receive - %% If the child dies before the unlik we must empty - %% the mail-box of the 'EXIT'-message and the 'DOWN'-message. - {'EXIT', Pid, Reason} -> - receive - {'DOWN', _, process, Pid, _} -> - {error, Reason} - end - after 0 -> - %% If a naughty child did unlink and the child dies before - %% monitor the result will be that shutdown/2 receives a - %% 'DOWN'-message with reason noproc. - %% If the child should die after the unlink there - %% will be a 'DOWN'-message with a correct reason - %% that will be handled in shutdown/2. - ok - end. - -wait_children(_Shutdown, _Pids, 0, undefined, EStack) -> - EStack; -wait_children(_Shutdown, _Pids, 0, TRef, EStack) -> - %% If the timer has expired before its cancellation, we must empty the - %% mail-box of the 'timeout'-message. - erlang:cancel_timer(TRef), - receive - {timeout, TRef, kill} -> - EStack - after 0 -> - EStack - end; - -%%TODO: Copied from supervisor.erl, rewrite it later. -wait_children(brutal_kill, Pids, Sz, TRef, EStack) -> - receive - {'DOWN', _MRef, process, Pid, killed} -> - wait_children(brutal_kill, sets:del_element(Pid, Pids), Sz-1, TRef, EStack); - - {'DOWN', _MRef, process, Pid, Reason} -> - wait_children(brutal_kill, sets:del_element(Pid, Pids), - Sz-1, TRef, dict:append(Reason, Pid, EStack)) - end; - -wait_children(Shutdown, Pids, Sz, TRef, EStack) -> - receive - {'DOWN', _MRef, process, Pid, shutdown} -> - wait_children(Shutdown, sets:del_element(Pid, Pids), Sz-1, TRef, EStack); - {'DOWN', _MRef, process, Pid, normal} -> - wait_children(Shutdown, sets:del_element(Pid, Pids), Sz-1, TRef, EStack); - {'DOWN', _MRef, process, Pid, Reason} -> - wait_children(Shutdown, sets:del_element(Pid, Pids), Sz-1, - TRef, dict:append(Reason, Pid, EStack)); - {timeout, TRef, kill} -> - sets:fold(fun(P, _) -> exit(P, kill) end, ok, Pids), - wait_children(Shutdown, Pids, Sz-1, undefined, EStack) - end. - -report_error(Error, Reason, Pid, #state{mfargs = MFA}) -> - SupName = list_to_atom("esockd_connection_sup - " ++ pid_to_list(self())), - ErrorMsg = [{supervisor, SupName}, - {errorContext, Error}, - {reason, Reason}, - {offender, [{pid, Pid}, - {name, connection}, - {mfargs, MFA}]}], - error_logger:error_report(supervisor_report, ErrorMsg). - -reply(Repy, State) -> - {reply, Repy, State}. - diff --git a/src/emqx_sm_sup.erl b/src/emqx_sm_sup.erl deleted file mode 100644 index 65702b26f..000000000 --- a/src/emqx_sm_sup.erl +++ /dev/null @@ -1,64 +0,0 @@ -%% Copyright (c) 2013-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_sm_sup). - --behaviour(supervisor). - --export([start_link/0]). - --export([init/1]). - -start_link() -> - supervisor:start_link({local, ?MODULE}, ?MODULE, []). - -init([]) -> - %% Session locker - Locker = #{id => locker, - start => {emqx_sm_locker, start_link, []}, - restart => permanent, - shutdown => 5000, - type => worker, - modules => [emqx_sm_locker] - }, - %% Session registry - Registry = #{id => registry, - start => {emqx_sm_registry, start_link, []}, - restart => permanent, - shutdown => 5000, - type => worker, - modules => [emqx_sm_registry] - }, - %% Session Manager - Manager = #{id => manager, - start => {emqx_sm, start_link, []}, - restart => permanent, - shutdown => 5000, - type => worker, - modules => [emqx_sm] - }, - %% Session Sup - SessSpec = #{start => {emqx_session, start_link, []}, - shutdown => brutal_kill, - clean_down => fun emqx_sm:clean_down/1 - }, - SessionSup = #{id => session_sup, - start => {emqx_session_sup, start_link, [SessSpec ]}, - restart => transient, - shutdown => infinity, - type => supervisor, - modules => [emqx_session_sup] - }, - {ok, {{rest_for_one, 10, 3600}, [Locker, Registry, Manager, SessionSup]}}. - From 21162f88b89e591823c507d6c7dd15087cd15850 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 18 Jun 2019 14:27:06 +0800 Subject: [PATCH 04/33] Update copyright --- include/types.hrl | 4 +- src/emqx.erl | 33 +- src/emqx_access_control.erl | 15 +- src/emqx_access_rule.erl | 8 +- src/emqx_acl_cache.erl | 5 +- src/emqx_alarm_handler.erl | 28 +- src/emqx_app.erl | 10 +- src/emqx_base62.erl | 12 +- src/emqx_batch.erl | 43 +- src/emqx_bridge.erl | 5 + src/emqx_bridge_connect.erl | 4 +- src/emqx_bridge_mqtt.erl | 7 +- src/emqx_bridge_msg.erl | 4 +- src/emqx_bridge_rpc.erl | 5 +- src/emqx_bridge_sup.erl | 5 +- src/emqx_broker.erl | 4 +- src/emqx_broker_helper.erl | 12 +- src/emqx_broker_sup.erl | 28 +- src/emqx_cli.erl | 5 +- src/emqx_client_sock.erl | 5 +- src/emqx_config.erl | 4 +- src/emqx_ctl.erl | 4 +- src/emqx_gc.erl | 12 +- src/emqx_gen_mod.erl | 4 +- src/emqx_guid.erl | 4 +- src/emqx_inflight.erl | 11 +- src/emqx_json.erl | 12 +- src/emqx_kernel_sup.erl | 4 +- src/emqx_listeners.erl | 9 +- src/emqx_logger.erl | 14 +- src/emqx_logger_handler.erl | 9 +- src/emqx_message.erl | 4 +- src/emqx_metrics.erl | 20 +- src/emqx_misc.erl | 4 +- src/emqx_mod_acl_internal.erl | 16 +- src/emqx_mod_presence.erl | 8 +- src/emqx_mod_rewrite.erl | 4 +- src/emqx_mod_subscription.erl | 4 +- src/emqx_mod_sup.erl | 21 +- src/emqx_mountpoint.erl | 8 +- src/emqx_mqtt_caps.erl | 4 +- src/emqx_mqtt_props.erl | 4 +- src/emqx_mqtt_types.erl | 4 +- src/emqx_mqueue.erl | 6 +- src/emqx_os_mon.erl | 104 +-- src/emqx_packet.erl | 8 +- src/emqx_pd.erl | 4 +- src/emqx_plugins.erl | 12 +- src/emqx_pmon.erl | 8 +- src/emqx_pool.erl | 16 +- src/emqx_pool_sup.erl | 16 +- src/emqx_protocol.erl | 138 ++-- src/emqx_psk.erl | 6 +- src/emqx_reason_codes.erl | 4 +- src/emqx_router.erl | 24 +- src/emqx_router_helper.erl | 20 +- src/emqx_router_sup.erl | 4 +- src/emqx_rpc.erl | 6 +- src/emqx_sequence.erl | 8 +- src/emqx_session.erl | 1192 ++++++++++++--------------------- src/emqx_shared_sub.erl | 23 +- src/emqx_sm.erl | 295 -------- src/emqx_sm_locker.erl | 68 -- src/emqx_stats.erl | 22 +- src/emqx_sup.erl | 70 +- src/emqx_sys.erl | 4 +- src/emqx_sys_mon.erl | 14 +- src/emqx_sys_sup.erl | 33 +- src/emqx_tables.erl | 5 +- src/emqx_time.erl | 5 +- src/emqx_topic.erl | 8 +- src/emqx_tracer.erl | 4 +- src/emqx_trie.erl | 16 +- src/emqx_types.erl | 10 +- src/emqx_vm.erl | 4 +- src/emqx_vm_mon.erl | 78 ++- src/emqx_zone.erl | 16 +- 77 files changed, 1118 insertions(+), 1563 deletions(-) delete mode 100644 src/emqx_sm.erl delete mode 100644 src/emqx_sm_locker.erl diff --git a/include/types.hrl b/include/types.hrl index 85a9aadf0..d8e96a20e 100644 --- a/include/types.hrl +++ b/include/types.hrl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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. +%%-------------------------------------------------------------------- -type(maybe(T) :: undefined | T). diff --git a/src/emqx.erl b/src/emqx.erl index c126b262c..8012b2eec 100644 --- a/src/emqx.erl +++ b/src/emqx.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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). @@ -59,9 +61,13 @@ -define(APP, ?MODULE). -%%------------------------------------------------------------------------------ +-define(COPYRIGHT, "Copyright (c) 2019 EMQ Technologies Co., Ltd"). + +-define(LICENSE_MESSAGE, "Licensed under the Apache License, Version 2.0"). + +%%-------------------------------------------------------------------- %% Bootstrap, is_running... -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% @doc Start emqx application -spec(start() -> {ok, list(atom())} | {error, term()}). @@ -93,9 +99,9 @@ is_running(Node) -> Pid when is_pid(Pid) -> true end. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% PubSub API -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- -spec(subscribe(emqx_topic:topic() | string()) -> ok). subscribe(Topic) -> @@ -120,9 +126,9 @@ publish(Msg) -> unsubscribe(Topic) -> emqx_broker:unsubscribe(iolist_to_binary(Topic)). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% PubSub management API -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- -spec(topics() -> list(emqx_topic:topic())). topics() -> emqx_router:topics(). @@ -141,9 +147,9 @@ subscribed(SubPid, Topic) when is_pid(SubPid) -> subscribed(SubId, Topic) when is_atom(SubId); is_binary(SubId) -> emqx_broker:subscribed(SubId, iolist_to_binary(Topic)). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Hooks API -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- -spec(hook(emqx_hooks:hookpoint(), emqx_hooks:action()) -> ok | {error, already_exists}). hook(HookPoint, Action) -> @@ -175,9 +181,9 @@ run_hook(HookPoint, Args) -> run_fold_hook(HookPoint, Args, Acc) -> emqx_hooks:run_fold(HookPoint, Args, Acc). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Shutdown and reboot -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- shutdown() -> shutdown(normal). @@ -191,12 +197,13 @@ shutdown(Reason) -> reboot() -> lists:foreach(fun application:start/1, [gproc, esockd, ranch, cowboy, ekka, emqx]). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Internal functions -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- reload_config(ConfFile) -> {ok, [Conf]} = file:consult(ConfFile), lists:foreach(fun({App, Vals}) -> [application:set_env(App, Par, Val) || {Par, Val} <- Vals] end, Conf). + diff --git a/src/emqx_access_control.erl b/src/emqx_access_control.erl index d6aab26d4..ae20c4489 100644 --- a/src/emqx_access_control.erl +++ b/src/emqx_access_control.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_access_control). @@ -22,9 +24,10 @@ , reload_acl/0 ]). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% APIs -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- + -spec(authenticate(emqx_types:credentials()) -> {ok, emqx_types:credentials()} | {error, term()}). authenticate(Credentials) -> @@ -36,7 +39,8 @@ authenticate(Credentials) -> end. %% @doc Check ACL --spec(check_acl(emqx_types:credentials(), emqx_types:pubsub(), emqx_types:topic()) -> allow | deny). +-spec(check_acl(emqx_types:credentials(), emqx_types:pubsub(), emqx_types:topic()) + -> allow | deny). check_acl(Credentials, PubSub, Topic) -> case emqx_acl_cache:is_enabled() of false -> @@ -47,8 +51,7 @@ check_acl(Credentials, PubSub, Topic) -> AclResult = do_check_acl(Credentials, PubSub, Topic), emqx_acl_cache:put_acl_cache(PubSub, Topic, AclResult), AclResult; - AclResult -> - AclResult + AclResult -> AclResult end end. diff --git a/src/emqx_access_rule.erl b/src/emqx_access_rule.erl index 5d40341cc..2f57059cb 100644 --- a/src/emqx_access_rule.erl +++ b/src/emqx_access_rule.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_access_rule). @@ -38,9 +40,9 @@ -define(ALLOW_DENY(A), ((A =:= allow) orelse (A =:= deny))). -define(PUBSUB(A), ((A =:= subscribe) orelse (A =:= publish) orelse (A =:= pubsub))). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% APIs -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% @doc Compile Access Rule. compile({A, all}) when ?ALLOW_DENY(A) -> diff --git a/src/emqx_acl_cache.erl b/src/emqx_acl_cache.erl index 92d4d8328..b417f16fa 100644 --- a/src/emqx_acl_cache.erl +++ b/src/emqx_acl_cache.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_acl_cache). @@ -124,6 +126,7 @@ map_acl_cache(Fun) -> %%-------------------------------------------------------------------- %% Internal functions %%-------------------------------------------------------------------- + add_acl(PubSub, Topic, AclResult) -> K = cache_k(PubSub, Topic), V = cache_v(AclResult), diff --git a/src/emqx_alarm_handler.erl b/src/emqx_alarm_handler.erl index e7d74eee8..ca74f3186 100644 --- a/src/emqx_alarm_handler.erl +++ b/src/emqx_alarm_handler.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_alarm_handler). @@ -44,9 +46,9 @@ -define(ALARM_TAB, emqx_alarm). -define(ALARM_HISTORY_TAB, emqx_alarm_history). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Mnesia bootstrap -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- mnesia(boot) -> ok = ekka_mnesia:create_table(?ALARM_TAB, [ @@ -61,13 +63,14 @@ mnesia(boot) -> {local_content, true}, {record_name, alarm_history}, {attributes, record_info(fields, alarm_history)}]); + mnesia(copy) -> ok = ekka_mnesia:copy_table(?ALARM_TAB), ok = ekka_mnesia:copy_table(?ALARM_HISTORY_TAB). -%%---------------------------------------------------------------------- +%%-------------------------------------------------------------------- %% API -%%---------------------------------------------------------------------- +%%-------------------------------------------------------------------- load() -> gen_event:swap_handler(alarm_handler, {alarm_handler, swap}, {?MODULE, []}). @@ -79,13 +82,14 @@ unload() -> get_alarms() -> gen_event:call(alarm_handler, ?MODULE, get_alarms). -%%---------------------------------------------------------------------- +%%-------------------------------------------------------------------- %% gen_event callbacks -%%---------------------------------------------------------------------- +%%-------------------------------------------------------------------- init({_Args, {alarm_handler, ExistingAlarms}}) -> init_tables(ExistingAlarms), {ok, []}; + init(_) -> init_tables([]), {ok, []}. @@ -131,9 +135,9 @@ init_tables(ExistingAlarms) -> set_alarm_history(Id) end, ExistingAlarms). -encode_alarm({AlarmId, #alarm{severity = Severity, +encode_alarm({AlarmId, #alarm{severity = Severity, title = Title, - summary = Summary, + summary = Summary, timestamp = Ts}}) -> emqx_json:safe_encode([{id, maybe_to_binary(AlarmId)}, {desc, [{severity, Severity}, @@ -141,7 +145,7 @@ encode_alarm({AlarmId, #alarm{severity = Severity, {summary, iolist_to_binary(Summary)}, {ts, emqx_time:now_secs(Ts)}]}]); encode_alarm({AlarmId, AlarmDesc}) -> - emqx_json:safe_encode([{id, maybe_to_binary(AlarmId)}, + emqx_json:safe_encode([{id, maybe_to_binary(AlarmId)}, {desc, maybe_to_binary(AlarmDesc)}]). alarm_msg(Topic, Payload) -> @@ -171,6 +175,6 @@ get_alarms_() -> [{Id, Desc} || #common_alarm{id = Id, desc = Desc} <- Alarms]. set_alarm_history(Id) -> - mnesia:dirty_write(?ALARM_HISTORY_TAB, #alarm_history{id = Id, - clear_at = undefined}). + His = #alarm_history{id = Id, clear_at = undefined}, + mnesia:dirty_write(?ALARM_HISTORY_TAB, His). diff --git a/src/emqx_app.erl b/src/emqx_app.erl index 8e8ce4c1b..57916a7fc 100644 --- a/src/emqx_app.erl +++ b/src/emqx_app.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_app). @@ -30,10 +32,10 @@ start(_Type, _Args) -> print_banner(), ekka:start(), {ok, Sup} = emqx_sup:start_link(), - emqx_modules:load(), - emqx_plugins:init(), + ok = emqx_modules:load(), + ok = emqx_plugins:init(), emqx_plugins:load(), - emqx_listeners:start(), + ok = emqx_listeners:start(), start_autocluster(), register(emqx, self()), diff --git a/src/emqx_base62.erl b/src/emqx_base62.erl index 77d04fc4d..6d1f5a882 100644 --- a/src/emqx_base62.erl +++ b/src/emqx_base62.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_base62). @@ -21,9 +23,9 @@ , decode/2 ]). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% APIs -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% @doc Encode any data to base62 binary -spec encode(string() | integer() | binary()) -> binary(). @@ -43,9 +45,9 @@ decode(L) when is_list(L) -> decode(B) when is_binary(B) -> decode(B, <<>>). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Interval Functions -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- encode(D, string) -> binary_to_list(encode(D)); diff --git a/src/emqx_batch.erl b/src/emqx_batch.erl index b7e341367..6d1df24ca 100644 --- a/src/emqx_batch.erl +++ b/src/emqx_batch.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_batch). @@ -22,18 +24,18 @@ , items/1 ]). --record(batch, - { batch_size :: non_neg_integer() - , batch_q :: list(any()) - , linger_ms :: pos_integer() - , linger_timer :: reference() | undefined - , commit_fun :: function() - }). +-record(batch, { + batch_size :: non_neg_integer(), + batch_q :: list(any()), + linger_ms :: pos_integer(), + linger_timer :: reference() | undefined, + commit_fun :: function() + }). --type(options() :: - #{ batch_size => non_neg_integer() - , linger_ms => pos_integer() - , commit_fun := function() +-type(options() :: #{ + batch_size => non_neg_integer(), + linger_ms => pos_integer(), + commit_fun := function() }). -opaque(batch() :: #batch{}). @@ -42,26 +44,31 @@ -export_type([batch/0]). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% APIs -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- -spec(init(options()) -> batch()). init(Opts) when is_map(Opts) -> #batch{batch_size = maps:get(batch_size, Opts, 1000), batch_q = [], - linger_ms = maps:get(linger_ms, Opts, 1000), + linger_ms = maps:get(linger_ms, Opts, 1000), commit_fun = maps:get(commit_fun, Opts)}. -spec(push(any(), batch()) -> batch()). -push(El, Batch = #batch{batch_q = Q, linger_ms = Ms, linger_timer = undefined}) when length(Q) == 0 -> - Batch#batch{batch_q = [El], linger_timer = erlang:send_after(Ms, self(), batch_linger_expired)}; +push(El, Batch = #batch{batch_q = Q, + linger_ms = Ms, + linger_timer = undefined}) + when length(Q) == 0 -> + TRef = erlang:send_after(Ms, self(), batch_linger_expired), + Batch#batch{batch_q = [El], linger_timer = TRef}; %% no limit. push(El, Batch = #batch{batch_size = 0, batch_q = Q}) -> Batch#batch{batch_q = [El|Q]}; -push(El, Batch = #batch{batch_size = MaxSize, batch_q = Q}) when length(Q) >= MaxSize -> +push(El, Batch = #batch{batch_size = MaxSize, batch_q = Q}) + when length(Q) >= MaxSize -> commit(Batch#batch{batch_q = [El|Q]}); push(El, Batch = #batch{batch_q = Q}) -> diff --git a/src/emqx_bridge.erl b/src/emqx_bridge.erl index a01837659..e56db99cd 100644 --- a/src/emqx_bridge.erl +++ b/src/emqx_bridge.erl @@ -1,3 +1,4 @@ +%%-------------------------------------------------------------------- %% Copyright (c) 2019 EMQ Technologies Co., Ltd. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); @@ -11,7 +12,9 @@ %% 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. +%%-------------------------------------------------------------------- +%%-------------------------------------------------------------------- %% @doc Bridge works in two layers (1) batching layer (2) transport layer %% The `bridge' batching layer collects local messages in batches and sends over %% to remote MQTT node/cluster via `connetion' transport layer. @@ -56,8 +59,10 @@ %% %% NOTES: %% * Local messages are all normalised to QoS-1 when exporting to remote +%%-------------------------------------------------------------------- -module(emqx_bridge). + -behaviour(gen_statem). %% APIs diff --git a/src/emqx_bridge_connect.erl b/src/emqx_bridge_connect.erl index 8685451ae..401ea771d 100644 --- a/src/emqx_bridge_connect.erl +++ b/src/emqx_bridge_connect.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_bridge_connect). diff --git a/src/emqx_bridge_mqtt.erl b/src/emqx_bridge_mqtt.erl index 8a66f77a0..45c8cd47d 100644 --- a/src/emqx_bridge_mqtt.erl +++ b/src/emqx_bridge_mqtt.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,8 +12,10 @@ %% 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. +%%-------------------------------------------------------------------- -%% @doc This module implements EMQX Bridge transport layer on top of MQTT protocol +%% @doc This module implements EMQX Bridge transport layer on top of +%% MQTT protocol. -module(emqx_bridge_mqtt). diff --git a/src/emqx_bridge_msg.erl b/src/emqx_bridge_msg.erl index 6633027f9..8a70aeaa0 100644 --- a/src/emqx_bridge_msg.erl +++ b/src/emqx_bridge_msg.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_bridge_msg). diff --git a/src/emqx_bridge_rpc.erl b/src/emqx_bridge_rpc.erl index 9674fdcf1..9be3955bc 100644 --- a/src/emqx_bridge_rpc.erl +++ b/src/emqx_bridge_rpc.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,10 +12,12 @@ %% 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. +%%-------------------------------------------------------------------- %% @doc This module implements EMQX Bridge transport layer based on gen_rpc. -module(emqx_bridge_rpc). + -behaviour(emqx_bridge_connect). %% behaviour callbacks diff --git a/src/emqx_bridge_sup.erl b/src/emqx_bridge_sup.erl index a40e7b2e3..92bfc5ba9 100644 --- a/src/emqx_bridge_sup.erl +++ b/src/emqx_bridge_sup.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,8 +12,10 @@ %% 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_bridge_sup). + -behavior(supervisor). -include("logger.hrl"). diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index cccc0bcd7..0274c6829 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_broker). diff --git a/src/emqx_broker_helper.erl b/src/emqx_broker_helper.erl index 8ece1805b..c08078da5 100644 --- a/src/emqx_broker_helper.erl +++ b/src/emqx_broker_helper.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_broker_helper). @@ -90,9 +92,9 @@ create_seq(Topic) -> reclaim_seq(Topic) -> emqx_sequence:reclaim(?SUBSEQ, Topic). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% gen_server callbacks -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- init([]) -> %% Helper table @@ -140,9 +142,9 @@ terminate(_Reason, _State) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Internal functions -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- clean_down(SubPid) -> case ets:lookup(?SUBMON, SubPid) of diff --git a/src/emqx_broker_sup.erl b/src/emqx_broker_sup.erl index 05154f393..325019ef6 100644 --- a/src/emqx_broker_sup.erl +++ b/src/emqx_broker_sup.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_broker_sup). @@ -23,9 +25,9 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Supervisor callbacks -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- init([]) -> %% Broker pool @@ -34,20 +36,20 @@ init([]) -> {emqx_broker, start_link, []}]), %% Shared subscription - SharedSub = #{id => shared_sub, - start => {emqx_shared_sub, start_link, []}, - restart => permanent, + SharedSub = #{id => shared_sub, + start => {emqx_shared_sub, start_link, []}, + restart => permanent, shutdown => 2000, - type => worker, - modules => [emqx_shared_sub]}, + type => worker, + modules => [emqx_shared_sub]}, %% Broker helper - Helper = #{id => helper, - start => {emqx_broker_helper, start_link, []}, - restart => permanent, + Helper = #{id => helper, + start => {emqx_broker_helper, start_link, []}, + restart => permanent, shutdown => 2000, - type => worker, - modules => [emqx_broker_helper]}, + type => worker, + modules => [emqx_broker_helper]}, {ok, {{one_for_all, 0, 1}, [BrokerPool, SharedSub, Helper]}}. diff --git a/src/emqx_cli.erl b/src/emqx_cli.erl index e681936c3..3deb50b07 100644 --- a/src/emqx_cli.erl +++ b/src/emqx_cli.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_cli). @@ -35,3 +37,4 @@ usage(CmdList) -> usage(Format, Args) -> usage([{Format, Args}]). + diff --git a/src/emqx_client_sock.erl b/src/emqx_client_sock.erl index ae9b03305..f40064f17 100644 --- a/src/emqx_client_sock.erl +++ b/src/emqx_client_sock.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_client_sock). @@ -105,3 +107,4 @@ default_ciphers(TlsVersions) -> fun(TlsVer, Ciphers) -> Ciphers ++ ssl:cipher_suites(all, TlsVer) end, [], TlsVersions). + diff --git a/src/emqx_config.erl b/src/emqx_config.erl index fd80de0c2..3d4c33369 100644 --- a/src/emqx_config.erl +++ b/src/emqx_config.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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. +%%-------------------------------------------------------------------- %% @doc Hot Configuration %% diff --git a/src/emqx_ctl.erl b/src/emqx_ctl.erl index 4746175b3..861f92162 100644 --- a/src/emqx_ctl.erl +++ b/src/emqx_ctl.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_ctl). diff --git a/src/emqx_gc.erl b/src/emqx_gc.erl index fa0d3d6ed..256a5c4ff 100644 --- a/src/emqx_gc.erl +++ b/src/emqx_gc.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,9 +12,10 @@ %% 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. +%%-------------------------------------------------------------------- -%% @doc This module manages an opaque collection of statistics data used to -%% force garbage collection on `self()' process when hitting thresholds. +%% @doc This module manages an opaque collection of statistics data used +%% to force garbage collection on `self()' process when hitting thresholds. %% Namely: %% (1) Total number of messages passed through %% (2) Total data volume passed through @@ -85,9 +87,9 @@ reset(?GCS(St)) -> reset(undefined) -> undefined. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Internal functions -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- -spec(dec(cnt | oct, pos_integer(), st()) -> {boolean(), st()}). dec(Key, Num, St) -> diff --git a/src/emqx_gen_mod.erl b/src/emqx_gen_mod.erl index dc9304f98..9ae4d5b6f 100644 --- a/src/emqx_gen_mod.erl +++ b/src/emqx_gen_mod.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_gen_mod). diff --git a/src/emqx_guid.erl b/src/emqx_guid.erl index 2e54e9aaa..2a3715b9e 100644 --- a/src/emqx_guid.erl +++ b/src/emqx_guid.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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. +%%-------------------------------------------------------------------- %% @doc Generate global unique id for mqtt message. %% diff --git a/src/emqx_inflight.erl b/src/emqx_inflight.erl index 7a41e4ec7..4522b3510 100644 --- a/src/emqx_inflight.erl +++ b/src/emqx_inflight.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_inflight). @@ -38,17 +40,18 @@ -opaque(inflight() :: {?MODULE, max_size(), gb_trees:tree()}). -define(Inflight(Tree), {?MODULE, _MaxSize, Tree}). + -define(Inflight(MaxSize, Tree), {?MODULE, MaxSize, (Tree)}). -export_type([inflight/0]). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% APIs -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- -spec(new(non_neg_integer()) -> inflight()). new(MaxSize) when MaxSize >= 0 -> - {?MODULE, MaxSize, gb_trees:empty()}. + ?Inflight(MaxSize, gb_trees:empty()). -spec(contain(key(), inflight()) -> boolean()). contain(Key, ?Inflight(Tree)) -> diff --git a/src/emqx_json.erl b/src/emqx_json.erl index 07a2e9a23..c9130646f 100644 --- a/src/emqx_json.erl +++ b/src/emqx_json.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_json). @@ -35,12 +37,12 @@ encode(Term, Opts) -> jsx:encode(Term, Opts). -spec(safe_encode(jsx:json_term()) - -> {ok, jsx:json_text()} | {error, term()}). + -> {ok, jsx:json_text()} | {error, Reason :: term()}). safe_encode(Term) -> safe_encode(Term, []). -spec(safe_encode(jsx:json_term(), jsx_to_json:config()) - -> {ok, jsx:json_text()} | {error, term()}). + -> {ok, jsx:json_text()} | {error, Reason :: term()}). safe_encode(Term, Opts) -> try encode(Term, Opts) of Json -> {ok, Json} @@ -58,12 +60,12 @@ decode(Json, Opts) -> jsx:decode(Json, Opts). -spec(safe_decode(jsx:json_text()) - -> {ok, jsx:json_term()} | {error, term()}). + -> {ok, jsx:json_term()} | {error, Reason :: term()}). safe_decode(Json) -> safe_decode(Json, []). -spec(safe_decode(jsx:json_text(), jsx_to_json:config()) - -> {ok, jsx:json_term()} | {error, term()}). + -> {ok, jsx:json_term()} | {error, Reason :: term()}). safe_decode(Json, Opts) -> try decode(Json, Opts) of Term -> {ok, Term} diff --git a/src/emqx_kernel_sup.erl b/src/emqx_kernel_sup.erl index cfed226cd..486942b53 100644 --- a/src/emqx_kernel_sup.erl +++ b/src/emqx_kernel_sup.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_kernel_sup). diff --git a/src/emqx_listeners.erl b/src/emqx_listeners.erl index 745daf000..043b55fcc 100644 --- a/src/emqx_listeners.erl +++ b/src/emqx_listeners.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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. +%%-------------------------------------------------------------------- %% @doc Start/Stop MQTT listeners. -module(emqx_listeners). @@ -33,9 +35,9 @@ -type(listener() :: {esockd:proto(), esockd:listen_on(), [esockd:option()]}). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% APIs -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% @doc Start all listeners. -spec(start() -> ok). @@ -167,3 +169,4 @@ format({Addr, Port}) when is_list(Addr) -> io_lib:format("~s:~w", [Addr, Port]); format({Addr, Port}) when is_tuple(Addr) -> io_lib:format("~s:~w", [esockd_net:ntoab(Addr), Port]). + diff --git a/src/emqx_logger.erl b/src/emqx_logger.erl index b77fa2d70..ce6ccfa97 100644 --- a/src/emqx_logger.erl +++ b/src/emqx_logger.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,10 +12,11 @@ %% 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_logger). --compile({no_auto_import,[error/1]}). +-compile({no_auto_import, [error/1]}). %% Logs -export([ debug/1 @@ -48,9 +50,9 @@ , get_log_handler/1 ]). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% APIs -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- debug(Msg) -> logger:debug(Msg). @@ -120,9 +122,9 @@ set_log_level(Level) -> {error, Error} -> {error, {primary_logger_level, Error}} end. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Internal Functions -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- log_hanlder_info(#{id := Id, level := Level, module := logger_std_h, config := #{type := Type}}) when Type =:= standard_io; diff --git a/src/emqx_logger_handler.erl b/src/emqx_logger_handler.erl index e0f5d9af4..c6492447a 100644 --- a/src/emqx_logger_handler.erl +++ b/src/emqx_logger_handler.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_logger_handler). @@ -19,8 +21,8 @@ -export([init/0]). init() -> - logger:add_handler(emqx_logger_handler, - emqx_logger_handler, + logger:add_handler(emqx_logger_handler, + emqx_logger_handler, #{level => error, filters => [{easy_filter, {fun filter_by_level/2, []}}], filters_default => stop}). @@ -41,3 +43,4 @@ filter_by_level(LogEvent = #{level := error}, _Extra) -> LogEvent; filter_by_level(_LogEvent, _Extra) -> stop. + diff --git a/src/emqx_message.erl b/src/emqx_message.erl index 23ddd69d4..a00928af8 100644 --- a/src/emqx_message.erl +++ b/src/emqx_message.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_message). diff --git a/src/emqx_metrics.erl b/src/emqx_metrics.erl index cf5939d38..2bcbb9eb5 100644 --- a/src/emqx_metrics.erl +++ b/src/emqx_metrics.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_metrics). @@ -143,9 +145,9 @@ start_link() -> stop() -> gen_server:stop(?SERVER). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Metrics API -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- -spec(new(metric_name()) -> ok). new(Name) -> @@ -249,9 +251,9 @@ update_counter(Name, Value) -> end, counters:add(CRef, CIdx, Value). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Inc Received/Sent metrics -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% @doc Inc packets received. -spec(inc_recv(emqx_mqtt_types:packet()) -> ok). @@ -337,9 +339,9 @@ do_inc_sent(?PACKET(?AUTH)) -> do_inc_sent(_Packet) -> ignore. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% gen_server callbacks -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- init([]) -> % Create counters array @@ -389,9 +391,9 @@ terminate(_Reason, _State) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Internal functions -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- reserved_idx('bytes.received') -> 01; reserved_idx('bytes.sent') -> 02; diff --git a/src/emqx_misc.erl b/src/emqx_misc.erl index e236b560f..26c97c92f 100644 --- a/src/emqx_misc.erl +++ b/src/emqx_misc.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_misc). diff --git a/src/emqx_mod_acl_internal.erl b/src/emqx_mod_acl_internal.erl index 953666f31..370bb986f 100644 --- a/src/emqx_mod_acl_internal.erl +++ b/src/emqx_mod_acl_internal.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_mod_acl_internal). @@ -35,9 +37,9 @@ -type(acl_rules() :: #{publish => [emqx_access_rule:rule()], subscribe => [emqx_access_rule:rule()]}). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% API -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- load(_Env) -> Rules = rules_from_file(acl_file()), @@ -52,9 +54,9 @@ unload(_Env) -> all_rules() -> rules_from_file(acl_file()). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% ACL callbacks -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% @doc Check ACL -spec(check_acl(emqx_types:credentials(), emqx_types:pubsub(), emqx_topic:topic(), @@ -71,9 +73,9 @@ check_acl(Credentials, PubSub, Topic, _AclResult, Rules) -> reload_acl() -> unload([]), load([]). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Internal Functions -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- acl_file() -> emqx_config:get_env(acl_file). diff --git a/src/emqx_mod_presence.erl b/src/emqx_mod_presence.erl index 9789474d7..dc6552954 100644 --- a/src/emqx_mod_presence.erl +++ b/src/emqx_mod_presence.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_mod_presence). @@ -31,9 +33,9 @@ -define(ATTR_KEYS, [clean_start, proto_ver, proto_name, keepalive]). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% APIs -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- load(Env) -> emqx_hooks:add('client.connected', fun ?MODULE:on_client_connected/4, [Env]), diff --git a/src/emqx_mod_rewrite.erl b/src/emqx_mod_rewrite.erl index b06428e9f..0b0285ddf 100644 --- a/src/emqx_mod_rewrite.erl +++ b/src/emqx_mod_rewrite.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_mod_rewrite). diff --git a/src/emqx_mod_subscription.erl b/src/emqx_mod_subscription.erl index c674adf97..cc4d5cda7 100644 --- a/src/emqx_mod_subscription.erl +++ b/src/emqx_mod_subscription.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_mod_subscription). diff --git a/src/emqx_mod_sup.erl b/src/emqx_mod_sup.erl index 54a77feda..cb6e86130 100644 --- a/src/emqx_mod_sup.erl +++ b/src/emqx_mod_sup.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,11 +12,14 @@ %% 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_mod_sup). -behaviour(supervisor). +-include("types.hrl"). + -export([ start_link/0 , start_child/1 , start_child/2 @@ -25,8 +29,14 @@ -export([init/1]). %% Helper macro for declaring children of supervisor --define(CHILD(Mod, Type), {Mod, {Mod, start_link, []}, permanent, 5000, Type, [Mod]}). +-define(CHILD(Mod, Type), #{id => Mod, + start => {Mod, start_link, []}, + restart => permanent, + shutdown => 5000, + type => Type, + modules => [Mod]}). +-spec(start_link() -> startlink_ret()). start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). @@ -39,13 +49,14 @@ start_child(Mod, Type) when is_atom(Mod) andalso is_atom(Type) -> -spec(stop_child(any()) -> ok | {error, term()}). stop_child(ChildId) -> case supervisor:terminate_child(?MODULE, ChildId) of - ok -> supervisor:delete_child(?MODULE, ChildId); + ok -> supervisor:delete_child(?MODULE, ChildId); Error -> Error end. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Supervisor callbacks -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- init([]) -> {ok, {{one_for_one, 10, 100}, []}}. + diff --git a/src/emqx_mountpoint.erl b/src/emqx_mountpoint.erl index b98348395..7c77d76e1 100644 --- a/src/emqx_mountpoint.erl +++ b/src/emqx_mountpoint.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_mountpoint). @@ -27,9 +29,9 @@ -export_type([mountpoint/0]). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% APIs -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- mount(undefined, Any) -> Any; diff --git a/src/emqx_mqtt_caps.erl b/src/emqx_mqtt_caps.erl index ff38f687e..abd14ee3c 100644 --- a/src/emqx_mqtt_caps.erl +++ b/src/emqx_mqtt_caps.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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. +%%-------------------------------------------------------------------- %% @doc MQTTv5 capabilities -module(emqx_mqtt_caps). diff --git a/src/emqx_mqtt_props.erl b/src/emqx_mqtt_props.erl index 686bb5bc8..47a368714 100644 --- a/src/emqx_mqtt_props.erl +++ b/src/emqx_mqtt_props.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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. +%%-------------------------------------------------------------------- %% @doc MQTT5 Properties -module(emqx_mqtt_props). diff --git a/src/emqx_mqtt_types.erl b/src/emqx_mqtt_types.erl index 0274b6ac3..2fe174d11 100644 --- a/src/emqx_mqtt_types.erl +++ b/src/emqx_mqtt_types.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_mqtt_types). diff --git a/src/emqx_mqueue.erl b/src/emqx_mqueue.erl index f13ccdd34..12154c184 100644 --- a/src/emqx_mqueue.erl +++ b/src/emqx_mqueue.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,7 +12,9 @@ %% 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. +%%-------------------------------------------------------------------- +%%-------------------------------------------------------------------- %% @doc A Simple in-memory message queue. %% %% Notice that MQTT is not a (on-disk) persistent messaging queue. @@ -42,6 +45,7 @@ %% unless `max_len' is set to `0' which implies (`infinity'). %% %% @end +%%-------------------------------------------------------------------- -module(emqx_mqueue). diff --git a/src/emqx_os_mon.erl b/src/emqx_os_mon.erl index 2a8eb3ca3..0bde8d118 100644 --- a/src/emqx_os_mon.erl +++ b/src/emqx_os_mon.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_os_mon). @@ -20,15 +22,6 @@ -export([start_link/1]). -%% gen_server callbacks --export([ init/1 - , handle_call/3 - , handle_cast/2 - , handle_info/2 - , terminate/2 - , code_change/3 - ]). - -export([ get_cpu_check_interval/0 , set_cpu_check_interval/1 , get_cpu_high_watermark/0 @@ -43,15 +36,24 @@ , set_procmem_high_watermark/1 ]). --define(OS_MON, ?MODULE). +%% gen_server callbacks +-export([ init/1 + , handle_call/3 + , handle_cast/2 + , handle_info/2 + , terminate/2 + , code_change/3 + ]). -%%------------------------------------------------------------------------------ -%% API -%%------------------------------------------------------------------------------ +-define(OS_MON, ?MODULE). start_link(Opts) -> gen_server:start_link({local, ?OS_MON}, ?MODULE, [Opts], []). +%%-------------------------------------------------------------------- +%% API +%%-------------------------------------------------------------------- + get_cpu_check_interval() -> call(get_cpu_check_interval). @@ -88,9 +90,12 @@ get_procmem_high_watermark() -> set_procmem_high_watermark(Float) -> memsup:set_procmem_high_watermark(Float). -%%------------------------------------------------------------------------------ +call(Req) -> + gen_server:call(?OS_MON, Req, infinity). + +%%-------------------------------------------------------------------- %% gen_server callbacks -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- init([Opts]) -> _ = cpu_sup:util(), @@ -105,47 +110,58 @@ init([Opts]) -> handle_call(get_cpu_check_interval, _From, State) -> {reply, maps:get(cpu_check_interval, State, undefined), State}; + handle_call({set_cpu_check_interval, Seconds}, _From, State) -> {reply, ok, State#{cpu_check_interval := Seconds}}; handle_call(get_cpu_high_watermark, _From, State) -> {reply, maps:get(cpu_high_watermark, State, undefined), State}; + handle_call({set_cpu_high_watermark, Float}, _From, State) -> {reply, ok, State#{cpu_high_watermark := Float}}; handle_call(get_cpu_low_watermark, _From, State) -> {reply, maps:get(cpu_low_watermark, State, undefined), State}; + handle_call({set_cpu_low_watermark, Float}, _From, State) -> {reply, ok, State#{cpu_low_watermark := Float}}; -handle_call(_Request, _From, State) -> - {reply, ok, State}. +handle_call(Req, _From, State) -> + ?LOG(error, "[OS_MON] Unexpected call: ~p", [Req]), + {reply, ignored, State}. -handle_cast(_Request, State) -> +handle_cast(Msg, State) -> + ?LOG(error, "[OS_MON] Unexpected cast: ~p", [Msg]), {noreply, State}. -handle_info({timeout, Timer, check}, State = #{timer := Timer, - cpu_high_watermark := CPUHighWatermark, - cpu_low_watermark := CPULowWatermark, - is_cpu_alarm_set := IsCPUAlarmSet}) -> - case cpu_sup:util() of - 0 -> - {noreply, State#{timer := undefined}}; - {error, Reason} -> - ?LOG(error, "[OS Monitor] Failed to get cpu utilization: ~p", [Reason]), - {noreply, ensure_check_timer(State)}; - Busy when Busy / 100 >= CPUHighWatermark -> - alarm_handler:set_alarm({cpu_high_watermark, Busy}), - {noreply, ensure_check_timer(State#{is_cpu_alarm_set := true})}; - Busy when Busy / 100 < CPULowWatermark -> - case IsCPUAlarmSet of - true -> alarm_handler:clear_alarm(cpu_high_watermark); - false -> ok - end, - {noreply, ensure_check_timer(State#{is_cpu_alarm_set := false})}; - _Busy -> - {noreply, ensure_check_timer(State)} - end. +handle_info({timeout, Timer, check}, + State = #{timer := Timer, + cpu_high_watermark := CPUHighWatermark, + cpu_low_watermark := CPULowWatermark, + is_cpu_alarm_set := IsCPUAlarmSet}) -> + NState = case cpu_sup:util() of + 0 -> + State#{timer := undefined}; + {error, Reason} -> + ?LOG(error, "[OS_MON] Failed to get cpu utilization: ~p", [Reason]), + ensure_check_timer(State); + Busy when (Busy / 100) >= CPUHighWatermark -> + alarm_handler:set_alarm({cpu_high_watermark, Busy}), + ensure_check_timer(State#{is_cpu_alarm_set := true}); + Busy when (Busy / 100) < CPULowWatermark -> + case IsCPUAlarmSet of + true -> alarm_handler:clear_alarm(cpu_high_watermark); + false -> ok + end, + ensure_check_timer(State#{is_cpu_alarm_set := false}); + _Busy -> + ensure_check_timer(State) + end, + {noreply, NState}; + +handle_info(Info, State) -> + ?LOG(error, "[OS_MON] Unexpected info: ~p", [Info]), + {noreply, State}. terminate(_Reason, #{timer := Timer}) -> emqx_misc:cancel_timer(Timer). @@ -153,11 +169,9 @@ terminate(_Reason, #{timer := Timer}) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Internal functions -%%------------------------------------------------------------------------------ -call(Req) -> - gen_server:call(?OS_MON, Req, infinity). +%%-------------------------------------------------------------------- ensure_check_timer(State = #{cpu_check_interval := Interval}) -> State#{timer := emqx_misc:start_timer(timer:seconds(Interval), check)}. diff --git a/src/emqx_packet.erl b/src/emqx_packet.erl index ba28bddac..a231ecd45 100644 --- a/src/emqx_packet.erl +++ b/src/emqx_packet.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_packet). @@ -40,9 +42,9 @@ protocol_name(?MQTT_PROTO_V5) -> type_name(Type) when Type > ?RESERVED andalso Type =< ?AUTH -> lists:nth(Type, ?TYPE_NAMES). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Validate MQTT Packet -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- validate(?SUBSCRIBE_PACKET(_PacketId, _Properties, [])) -> error(topic_filters_invalid); diff --git a/src/emqx_pd.erl b/src/emqx_pd.erl index 04b4d6075..5d1277833 100644 --- a/src/emqx_pd.erl +++ b/src/emqx_pd.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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. +%%-------------------------------------------------------------------- %% @doc The utility functions for erlang process dictionary. -module(emqx_pd). diff --git a/src/emqx_plugins.erl b/src/emqx_plugins.erl index 1c1185bf0..d2ce8b8a0 100644 --- a/src/emqx_plugins.erl +++ b/src/emqx_plugins.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_plugins). @@ -28,9 +30,9 @@ , load_expand_plugin/1 ]). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% APIs -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% @doc Init plugins' config -spec(init() -> ok). @@ -45,8 +47,8 @@ init() -> init_config(CfgFile) -> {ok, [AppsEnv]} = file:consult(CfgFile), - lists:foreach(fun({AppName, Envs}) -> - [application:set_env(AppName, Par, Val) || {Par, Val} <- Envs] + lists:foreach(fun({App, Envs}) -> + [application:set_env(App, Par, Val) || {Par, Val} <- Envs] end, AppsEnv). %% @doc Load all plugins when the broker started. diff --git a/src/emqx_pmon.erl b/src/emqx_pmon.erl index 6aa880f90..411eb8ce6 100644 --- a/src/emqx_pmon.erl +++ b/src/emqx_pmon.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_pmon). @@ -33,9 +35,9 @@ -type(pmon() :: {?MODULE, map()}). -export_type([pmon/0]). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% APIs -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- -spec(new() -> pmon()). new() -> diff --git a/src/emqx_pool.erl b/src/emqx_pool.erl index b78479f5c..64729e929 100644 --- a/src/emqx_pool.erl +++ b/src/emqx_pool.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_pool). @@ -45,9 +47,9 @@ -type(task() :: fun() | mfa() | {fun(), Args :: list(any())}). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% APIs -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% @doc Start pool. -spec(start_link(atom(), pos_integer()) -> startlink_ret()). @@ -85,9 +87,9 @@ cast(Msg) -> worker() -> gproc_pool:pick_worker(?POOL). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% gen_server callbacks -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- init([Pool, Id]) -> true = gproc_pool:connect_worker(Pool, {Pool, Id}), @@ -121,9 +123,9 @@ terminate(_Reason, #{pool := Pool, id := Id}) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Internal functions -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- run({M, F, A}) -> erlang:apply(M, F, A); diff --git a/src/emqx_pool_sup.erl b/src/emqx_pool_sup.erl index 1e884bafe..77278807d 100644 --- a/src/emqx_pool_sup.erl +++ b/src/emqx_pool_sup.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_pool_sup). @@ -44,7 +46,8 @@ spec(ChildId, Args) -> start_link() -> start_link(?POOL, random, {?POOL, start_link, []}). --spec(start_link(atom() | tuple(), atom(), mfa()) -> {ok, pid()} | {error, term()}). +-spec(start_link(atom() | tuple(), atom(), mfa()) + -> {ok, pid()} | {error, term()}). start_link(Pool, Type, MFA) -> start_link(Pool, Type, emqx_vm:schedulers(), MFA). @@ -54,11 +57,16 @@ start_link(Pool, Type, Size, MFA) -> supervisor:start_link(?MODULE, [Pool, Type, Size, MFA]). init([Pool, Type, Size, {M, F, Args}]) -> - ensure_pool(Pool, Type, [{size, Size}]), + ok = ensure_pool(Pool, Type, [{size, Size}]), {ok, {{one_for_one, 10, 3600}, [ begin ensure_pool_worker(Pool, {Pool, I}, I), - {{M, I}, {M, F, [Pool, I | Args]}, transient, 5000, worker, [M]} + #{id => {M, I}, + start => {M, F, [Pool, I | Args]}, + restart => transient, + shutdown => 5000, + type => worker, + modules => [M]} end || I <- lists:seq(1, Size)]}}. ensure_pool(Pool, Type, Opts) -> diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index d8e6da46f..bfb7194af 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -49,7 +49,6 @@ proto_name, client_id, is_assigned, - conn_pid, conn_props, ack_props, username, @@ -81,15 +80,15 @@ -define(NO_PROPS, undefined). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Init -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- -spec(init(map(), list()) -> state()). -init(SocketOpts = #{ sockname := Sockname - , peername := Peername - , peercert := Peercert - , sendfun := SendFun}, Options) -> +init(SocketOpts = #{sockname := Sockname, + peername := Peername, + peercert := Peercert, + sendfun := SendFun}, Options) -> Zone = proplists:get_value(zone, Options), #pstate{zone = Zone, sendfun = SendFun, @@ -100,7 +99,6 @@ init(SocketOpts = #{ sockname := Sockname proto_name = <<"MQTT">>, client_id = <<>>, is_assigned = false, - conn_pid = self(), username = init_username(Peercert, Options), clean_start = false, topic_aliases = #{}, @@ -108,6 +106,7 @@ init(SocketOpts = #{ sockname := Sockname recv_stats = #{msg => 0, pkt => 0}, send_stats = #{msg => 0, pkt => 0}, connected = false, + %% TODO: ...? topic_alias_maximum = #{to_client => 0, from_client => 0}, conn_mod = maps:get(conn_mod, SocketOpts, undefined), credentials = #{}, @@ -126,9 +125,9 @@ set_username(Username, PState = #pstate{username = undefined}) -> set_username(_Username, PState) -> PState. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% API -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- info(PState = #pstate{zone = Zone, conn_props = ConnProps, @@ -157,19 +156,19 @@ attrs(#pstate{zone = Zone, connected_at = ConnectedAt, conn_mod = ConnMod, credentials = Credentials}) -> - #{ zone => Zone - , client_id => ClientId - , username => Username - , peername => Peername - , peercert => Peercert - , proto_ver => ProtoVer - , proto_name => ProtoName - , clean_start => CleanStart - , keepalive => Keepalive - , is_bridge => IsBridge - , connected_at => ConnectedAt - , conn_mod => ConnMod - , credentials => Credentials + #{zone => Zone, + client_id => ClientId, + username => Username, + peername => Peername, + peercert => Peercert, + proto_ver => ProtoVer, + proto_name => ProtoName, + clean_start => CleanStart, + keepalive => Keepalive, + is_bridge => IsBridge, + connected_at => ConnectedAt, + conn_mod => ConnMod, + credentials => Credentials }. attr(proto_ver, #pstate{proto_ver = ProtoVer}) -> @@ -238,8 +237,8 @@ stats(#pstate{recv_stats = #{pkt := RecvPkt, msg := RecvMsg}, {send_pkt, SendPkt}, {send_msg, SendMsg}]. -session(#pstate{session = SPid}) -> - SPid. +session(#pstate{session = Session}) -> + Session. %%------------------------------------------------------------------------------ %% Packet Received @@ -364,6 +363,7 @@ preprocess_properties(Packet, PState) -> %%------------------------------------------------------------------------------ %% Process MQTT Packet %%------------------------------------------------------------------------------ + process(?CONNECT_PACKET( #mqtt_packet_connect{proto_name = ProtoName, proto_ver = ProtoVer, @@ -403,11 +403,11 @@ process(?CONNECT_PACKET( %% Open session SessAttrs = #{will_msg => make_will_msg(ConnPkt)}, case try_open_session(SessAttrs, PState3) of - {ok, SPid, SP} -> - PState4 = PState3#pstate{session = SPid, connected = true, + {ok, Session, SP} -> + PState4 = PState3#pstate{session = Session, connected = true, credentials = keepsafety(Credentials0)}, - ok = emqx_cm:register_connection(client_id(PState4)), - true = emqx_cm:set_conn_attrs(client_id(PState4), attrs(PState4)), + ok = emqx_cm:register_channel(client_id(PState4)), + ok = emqx_cm:set_conn_attrs(client_id(PState4), attrs(PState4)), %% Start keepalive start_keepalive(Keepalive, PState4), %% Success @@ -470,36 +470,43 @@ process(Packet = ?PUBLISH_PACKET(?QOS_2, Topic, PacketId, _Payload), end end; -process(?PUBACK_PACKET(PacketId, ReasonCode), PState = #pstate{session = SPid}) -> - {ok = emqx_session:puback(SPid, PacketId, ReasonCode), PState}; +process(?PUBACK_PACKET(PacketId, ReasonCode), PState = #pstate{session = Session}) -> + NSession = emqx_session:puback(PacketId, ReasonCode, Session), + {ok, PState#pstate{session = NSession}}; -process(?PUBREC_PACKET(PacketId, ReasonCode), PState = #pstate{session = SPid}) -> - case emqx_session:pubrec(SPid, PacketId, ReasonCode) of - ok -> - send(?PUBREL_PACKET(PacketId), PState); +process(?PUBREC_PACKET(PacketId, ReasonCode), PState = #pstate{session = Session}) -> + case emqx_session:pubrec(PacketId, ReasonCode, Session) of + {ok, NSession} -> + send(?PUBREL_PACKET(PacketId), PState#pstate{session = NSession}); {error, NotFound} -> send(?PUBREL_PACKET(PacketId, NotFound), PState) end; -process(?PUBREL_PACKET(PacketId, ReasonCode), PState = #pstate{session = SPid}) -> - case emqx_session:pubrel(SPid, PacketId, ReasonCode) of - ok -> - send(?PUBCOMP_PACKET(PacketId), PState); +process(?PUBREL_PACKET(PacketId, ReasonCode), PState = #pstate{session = Session}) -> + case emqx_session:pubrel(PacketId, ReasonCode, Session) of + {ok, NSession} -> + send(?PUBCOMP_PACKET(PacketId), PState#pstate{session = NSession}); {error, NotFound} -> send(?PUBCOMP_PACKET(PacketId, NotFound), PState) end; -process(?PUBCOMP_PACKET(PacketId, ReasonCode), PState = #pstate{session = SPid}) -> - {ok = emqx_session:pubcomp(SPid, PacketId, ReasonCode), PState}; +process(?PUBCOMP_PACKET(PacketId, ReasonCode), PState = #pstate{session = Session}) -> + case emqx_session:pubcomp(PacketId, ReasonCode, Session) of + {ok, NSession} -> + {ok, PState#pstate{session = NSession}}; + {error, _NotFound} -> + %% TODO: How to handle NotFound? + {ok, PState} + end; process(Packet = ?SUBSCRIBE_PACKET(PacketId, Properties, RawTopicFilters), - PState = #pstate{zone = Zone, proto_ver = ProtoVer, session = SPid, credentials = Credentials}) -> + PState = #pstate{zone = Zone, proto_ver = ProtoVer, session = Session, credentials = Credentials}) -> case check_subscribe(parse_topic_filters(?SUBSCRIBE, raw_topic_filters(PState, RawTopicFilters)), PState) of {ok, TopicFilters} -> TopicFilters0 = emqx_hooks:run_fold('client.subscribe', [Credentials], TopicFilters), TopicFilters1 = emqx_mountpoint:mount(mountpoint(Credentials), TopicFilters0), - ok = emqx_session:subscribe(SPid, PacketId, Properties, TopicFilters1), - {ok, PState}; + {ok, ReasonCodes, NSession} = emqx_session:subscribe(TopicFilters1, Session), + deliver({suback, PacketId, ReasonCodes}, PState#pstate{session = NSession}); {error, TopicFilters} -> {SubTopics, ReasonCodes} = lists:foldr(fun({Topic, #{rc := ?RC_SUCCESS}}, {Topics, Codes}) -> @@ -520,26 +527,26 @@ process(Packet = ?SUBSCRIBE_PACKET(PacketId, Properties, RawTopicFilters), end; process(?UNSUBSCRIBE_PACKET(PacketId, Properties, RawTopicFilters), - PState = #pstate{session = SPid, credentials = Credentials}) -> + PState = #pstate{session = Session, credentials = Credentials}) -> TopicFilters = emqx_hooks:run_fold('client.unsubscribe', [Credentials], parse_topic_filters(?UNSUBSCRIBE, RawTopicFilters)), - ok = emqx_session:unsubscribe(SPid, PacketId, Properties, - emqx_mountpoint:mount(mountpoint(Credentials), TopicFilters)), - {ok, PState}; + TopicFilters1 = emqx_mountpoint:mount(mountpoint(Credentials), TopicFilters), + {ok, ReasonCodes, NSession} = emqx_session:unsubscribe(TopicFilters1, Session), + deliver({unsuback, PacketId, ReasonCodes}, PState#pstate{session = NSession}); process(?PACKET(?PINGREQ), PState) -> send(?PACKET(?PINGRESP), PState); process(?DISCONNECT_PACKET(?RC_SUCCESS, #{'Session-Expiry-Interval' := Interval}), - PState = #pstate{session = SPid, conn_props = #{'Session-Expiry-Interval' := OldInterval}}) -> + PState = #pstate{session = Session, conn_props = #{'Session-Expiry-Interval' := OldInterval}}) -> case Interval =/= 0 andalso OldInterval =:= 0 of true -> deliver({disconnect, ?RC_PROTOCOL_ERROR}, PState), {error, protocol_error, PState#pstate{will_msg = undefined}}; false -> - emqx_session:update_expiry_interval(SPid, Interval), + NSession = emqx_session:update_expiry_interval(Interval, Session), %% Clean willmsg - {stop, normal, PState#pstate{will_msg = undefined}} + {stop, normal, PState#pstate{will_msg = undefined, session = NSession}} end; process(?DISCONNECT_PACKET(?RC_SUCCESS), PState) -> @@ -562,19 +569,27 @@ connack({ReasonCode, PState = #pstate{proto_ver = ProtoVer, credentials = Creden _ = deliver({connack, ReasonCode1}, PState), {error, emqx_reason_codes:name(ReasonCode1, ProtoVer), PState}. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Publish Message -> Broker -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- do_publish(Packet = ?PUBLISH_PACKET(QoS, PacketId), - PState = #pstate{session = SPid, credentials = Credentials}) -> + PState = #pstate{session = Session, credentials = Credentials}) -> Msg = emqx_mountpoint:mount(mountpoint(Credentials), emqx_packet:to_message(Credentials, Packet)), - puback(QoS, PacketId, emqx_session:publish(SPid, PacketId, emqx_message:set_flag(dup, false, Msg)), PState). + Msg1 = emqx_message:set_flag(dup, false, Msg), + case emqx_session:publish(PacketId, Msg1, Session) of + {ok, Result} -> + puback(QoS, PacketId, {ok, Result}, PState); + {ok, Result, NSession} -> + puback(QoS, PacketId, {ok, Result}, PState#pstate{session = NSession}); + {error, ReasonCode} -> + puback(QoS, PacketId, {error, ReasonCode}, PState) + end. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Puback -> Client -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- puback(?QOS_0, _PacketId, _Result, PState) -> {ok, PState}; @@ -734,21 +749,19 @@ maybe_assign_client_id(PState) -> try_open_session(SessAttrs, PState = #pstate{zone = Zone, client_id = ClientId, - conn_pid = ConnPid, username = Username, clean_start = CleanStart}) -> - case emqx_sm:open_session( + case emqx_cm:open_session( maps:merge(#{zone => Zone, client_id => ClientId, - conn_pid => ConnPid, username => Username, clean_start => CleanStart, max_inflight => attr(max_inflight, PState), expiry_interval => attr(expiry_interval, PState), topic_alias_maximum => attr(topic_alias_maximum, PState)}, SessAttrs)) of - {ok, SPid} -> - {ok, SPid, false}; + {ok, Session} -> + {ok, Session, false}; Other -> Other end. @@ -1035,3 +1048,4 @@ do_acl_check(Action, Credentials, Topic, AllowTerm, DenyTerm) -> allow -> AllowTerm; deny -> DenyTerm end. + diff --git a/src/emqx_psk.erl b/src/emqx_psk.erl index 3b2407b1c..eb369e872 100644 --- a/src/emqx_psk.erl +++ b/src/emqx_psk.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_psk). @@ -35,4 +37,4 @@ lookup(psk, ClientPSKID, _UserState) -> Except:Error:Stacktrace -> ?LOG(error, "[PSK] Lookup PSK failed, ~p: ~p", [{Except,Error}, Stacktrace]), error - end. \ No newline at end of file + end. diff --git a/src/emqx_reason_codes.erl b/src/emqx_reason_codes.erl index b8a9b9e4b..a99406f53 100644 --- a/src/emqx_reason_codes.erl +++ b/src/emqx_reason_codes.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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. +%%-------------------------------------------------------------------- %% @doc MQTT5 reason codes -module(emqx_reason_codes). diff --git a/src/emqx_router.erl b/src/emqx_router.erl index 8eb65169c..8f3965969 100644 --- a/src/emqx_router.erl +++ b/src/emqx_router.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_router). @@ -66,9 +68,9 @@ -define(ROUTE, emqx_route). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Mnesia bootstrap -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- mnesia(boot) -> ok = ekka_mnesia:create_table(?ROUTE, [ @@ -81,18 +83,18 @@ mnesia(boot) -> mnesia(copy) -> ok = ekka_mnesia:copy_table(?ROUTE). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Start a router -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- -spec(start_link(atom(), pos_integer()) -> startlink_ret()). start_link(Pool, Id) -> gen_server:start_link({local, emqx_misc:proc_name(?MODULE, Id)}, ?MODULE, [Pool, Id], [{hibernate_after, 1000}]). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Route APIs -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- -spec(add_route(emqx_topic:topic()) -> ok | {error, term()}). add_route(Topic) when is_binary(Topic) -> @@ -181,9 +183,9 @@ call(Router, Msg) -> pick(Topic) -> gproc_pool:pick_worker(router_pool, Topic). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% gen_server callbacks -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- init([Pool, Id]) -> true = gproc_pool:connect_worker(Pool, {Pool, Id}), @@ -215,9 +217,9 @@ terminate(_Reason, #{pool := Pool, id := Id}) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Internal functions -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- insert_direct_route(Route) -> mnesia:async_dirty(fun mnesia:write/3, [?ROUTE, Route, sticky_write]). diff --git a/src/emqx_router_helper.erl b/src/emqx_router_helper.erl index f272b1236..9c9ae4e12 100644 --- a/src/emqx_router_helper.erl +++ b/src/emqx_router_helper.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_router_helper). @@ -49,9 +51,9 @@ -define(ROUTING_NODE, emqx_routing_node). -define(LOCK, {?MODULE, cleanup_routes}). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Mnesia bootstrap -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- mnesia(boot) -> ok = ekka_mnesia:create_table(?ROUTING_NODE, [ @@ -64,9 +66,9 @@ mnesia(boot) -> mnesia(copy) -> ok = ekka_mnesia:copy_table(?ROUTING_NODE). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% API -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% @doc Starts the router helper -spec(start_link() -> startlink_ret()). @@ -84,9 +86,9 @@ monitor(Node) when is_atom(Node) -> false -> mnesia:dirty_write(?ROUTING_NODE, #routing_node{name = Node}) end. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% gen_server callbacks -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- init([]) -> ok = ekka:monitor(membership), @@ -152,9 +154,9 @@ terminate(_Reason, _State) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Internal functions -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- stats_fun() -> case ets:info(?ROUTE, size) of diff --git a/src/emqx_router_sup.erl b/src/emqx_router_sup.erl index 486d44b69..c4944e3c1 100644 --- a/src/emqx_router_sup.erl +++ b/src/emqx_router_sup.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_router_sup). diff --git a/src/emqx_rpc.erl b/src/emqx_rpc.erl index 4d01b6229..eeb1c298e 100644 --- a/src/emqx_rpc.erl +++ b/src/emqx_rpc.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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. +%%-------------------------------------------------------------------- %% @doc wrap gen_rpc? -module(emqx_rpc). @@ -32,7 +34,7 @@ cast(Node, Mod, Fun, Args) -> filter_result(?RPC:cast(Node, Mod, Fun, Args)). filter_result(Delivery) -> - case Delivery of + case Delivery of {badrpc, Reason} -> {badrpc, Reason}; {badtcp, Reason} -> {badrpc, Reason}; _ -> Delivery diff --git a/src/emqx_sequence.erl b/src/emqx_sequence.erl index f02165ea6..29381bf6c 100644 --- a/src/emqx_sequence.erl +++ b/src/emqx_sequence.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_sequence). @@ -29,9 +31,9 @@ -export_type([seqid/0]). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% APIs -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% @doc Create a sequence. -spec(create(name()) -> ok). diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 85d2c1947..dfbc6720b 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -14,6 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- +%%-------------------------------------------------------------------- %% @doc %% A stateful interaction between a Client and a Server. Some Sessions %% last only as long as the Network Connection, others can span multiple @@ -37,32 +38,27 @@ %% If the Session is currently not connected, the time at which the Session %% will end and Session State will be discarded. %% @end +%%-------------------------------------------------------------------- -module(emqx_session). --behaviour(gen_server). - -include("emqx.hrl"). -include("emqx_mqtt.hrl"). -include("logger.hrl"). -include("types.hrl"). --export([start_link/1]). +-export([ new/1 + , handle/2 + , close/1 + ]). -export([ info/1 , attrs/1 , stats/1 ]). --export([ resume/2 - , discard/2 - , update_expiry_interval/2 - ]). - -export([ subscribe/2 - , subscribe/4 , unsubscribe/2 - , unsubscribe/4 , publish/3 , puback/2 , puback/3 @@ -72,59 +68,43 @@ , pubcomp/3 ]). --export([close/1]). - -%% gen_server callbacks --export([ init/1 - , handle_call/3 - , handle_cast/2 - , handle_info/2 - , terminate/2 - , code_change/3 - ]). - --import(emqx_zone, [get_env/2, get_env/3]). - --record(state, { - %% zone - zone :: atom(), - - %% Idle timeout - idle_timeout :: pos_integer(), +-record(session, { + %% ClientId: Identifier of Session + client_id :: binary(), %% Clean Start Flag clean_start = false :: boolean(), + %% Username + username :: maybe(binary()), + %% Conn Binding: local | remote %% binding = local :: local | remote, %% Deliver fun deliver_fun :: function(), - %% ClientId: Identifier of Session - client_id :: binary(), - - %% Username - username :: maybe(binary()), - - %% Connection pid binding with session - conn_pid :: pid(), - - %% Old Connection Pid that has been kickout - old_conn_pid :: pid(), - %% Next packet id of the session next_pkt_id = 1 :: emqx_mqtt_types:packet_id(), + %% Max subscriptions + max_subscriptions :: non_neg_integer(), + %% Client’s Subscriptions. subscriptions :: map(), + %% Upgrade QoS? + upgrade_qos = false :: boolean(), + %% Client <- Broker: Inflight QoS1, QoS2 messages sent to the client but unacked. inflight :: emqx_inflight:inflight(), %% Max Inflight Size. DEPRECATED: Get from inflight %% max_inflight = 32 :: non_neg_integer(), + %% Retry interval for redelivering QoS1/2 messages + retry_interval = 20000 :: timeout(), + %% Retry Timer retry_timer :: maybe(reference()), @@ -134,334 +114,134 @@ %% Optionally, QoS 0 messages pending transmission to the Client. mqueue :: emqx_mqueue:mqueue(), + %% Max Packets Awaiting PUBREL + max_awaiting_rel = 100 :: non_neg_integer(), + + %% Awaiting PUBREL Timeout + await_rel_timeout = 20000 :: timeout(), + %% Client -> Broker: Inflight QoS2 messages received from client and waiting for pubrel. awaiting_rel :: map(), %% Awaiting PUBREL Timer await_rel_timer :: maybe(reference()), + will_msg :: emqx:message(), + + will_delay_timer :: maybe(reference()), + %% Session Expiry Interval expiry_interval = 7200 :: timeout(), %% Expired Timer expiry_timer :: maybe(reference()), - %% Stats timer - stats_timer :: maybe(reference()), - - %% GC State - gc_state, - %% Created at - created_at :: erlang:timestamp(), - - will_msg :: emqx:message(), - - will_delay_timer :: maybe(reference()) - + created_at :: erlang:timestamp() }). --type(spid() :: pid()). --type(attr() :: {atom(), term()}). +-opaque(session() :: #session{}). --export_type([attr/0]). +-export_type([session/0]). --define(DEFAULT_BATCH_N, 1000). +%% @doc Create a session. +-spec(new(Attrs :: map()) -> session()). +new(#{zone := Zone, + client_id := ClientId, + clean_start := CleanStart, + username := Username, + expiry_interval := ExpiryInterval, + max_inflight := MaxInflight, + will_msg := WillMsg}) -> + emqx_logger:set_metadata_client_id(ClientId), + #session{client_id = ClientId, + clean_start = CleanStart, + username = Username, + subscriptions = #{}, + inflight = emqx_inflight:new(MaxInflight), + mqueue = init_mqueue(Zone), + awaiting_rel = #{}, + expiry_interval = ExpiryInterval, + created_at = os:timestamp(), + will_msg = WillMsg}. -%% @doc Start a session proc. --spec(start_link(SessAttrs :: map()) -> {ok, pid()}). -start_link(SessAttrs) -> - proc_lib:start_link(?MODULE, init, [[self(), SessAttrs]]). +init_mqueue(Zone) -> + emqx_mqueue:init(#{max_len => emqx_zone:get_env(Zone, max_mqueue_len, 1000), + store_qos0 => emqx_zone:get_env(Zone, mqueue_store_qos0, true), + priorities => emqx_zone:get_env(Zone, mqueue_priorities), + default_priority => emqx_zone:get_env(Zone, mqueue_default_priority) + }). %% @doc Get session info --spec(info(spid() | #state{}) -> list({atom(), term()})). -info(SPid) when is_pid(SPid) -> - gen_server:call(SPid, info, infinity); - -info(State = #state{zone = Zone, - conn_pid = ConnPid, - next_pkt_id = PktId, - subscriptions = Subscriptions, - inflight = Inflight, - mqueue = MQueue, - awaiting_rel = AwaitingRel}) -> - attrs(State) ++ [{conn_pid, ConnPid}, - {next_pkt_id, PktId}, - {max_subscriptions, get_env(Zone, max_subscriptions, 0)}, - {subscriptions, Subscriptions}, - {upgrade_qos, get_env(Zone, upgrade_qos, false)}, - {inflight, Inflight}, - {retry_interval, get_env(Zone, retry_interval, 0)}, - {mqueue_len, emqx_mqueue:len(MQueue)}, - {awaiting_rel, AwaitingRel}, - {max_awaiting_rel, get_env(Zone, max_awaiting_rel)}, - {await_rel_timeout, get_env(Zone, await_rel_timeout)}]. +-spec(info(session()) -> list({atom(), term()})). +info(Session = #session{next_pkt_id = PktId, + max_subscriptions = MaxSubscriptions, + subscriptions = Subscriptions, + upgrade_qos = UpgradeQoS, + inflight = Inflight, + retry_interval = RetryInterval, + mqueue = MQueue, + max_awaiting_rel = MaxAwaitingRel, + awaiting_rel = AwaitingRel, + await_rel_timeout = AwaitRelTimeout}) -> + attrs(Session) ++ [{next_pkt_id, PktId}, + {max_subscriptions, MaxSubscriptions}, + {subscriptions, Subscriptions}, + {upgrade_qos, UpgradeQoS}, + {inflight, Inflight}, + {retry_interval, RetryInterval}, + {mqueue_len, emqx_mqueue:len(MQueue)}, + {awaiting_rel, AwaitingRel}, + {max_awaiting_rel, MaxAwaitingRel}, + {await_rel_timeout, AwaitRelTimeout}]. %% @doc Get session attrs --spec(attrs(spid() | #state{}) -> list({atom(), term()})). -attrs(SPid) when is_pid(SPid) -> - gen_server:call(SPid, attrs, infinity); - -attrs(#state{clean_start = CleanStart, - client_id = ClientId, - conn_pid = ConnPid, - username = Username, - expiry_interval = ExpiryInterval, - created_at = CreatedAt}) -> - [{clean_start, CleanStart}, - {binding, binding(ConnPid)}, - {client_id, ClientId}, +-spec(attrs(session()) -> list({atom(), term()})). +attrs(#session{client_id = ClientId, + clean_start = CleanStart, + username = Username, + expiry_interval = ExpiryInterval, + created_at = CreatedAt}) -> + [{client_id, ClientId}, + {clean_start, CleanStart}, {username, Username}, {expiry_interval, ExpiryInterval div 1000}, {created_at, CreatedAt}]. --spec(stats(spid() | #state{}) -> list({atom(), non_neg_integer()})). -stats(SPid) when is_pid(SPid) -> - gen_server:call(SPid, stats, infinity); - -stats(#state{zone = Zone, - subscriptions = Subscriptions, - inflight = Inflight, - mqueue = MQueue, - awaiting_rel = AwaitingRel}) -> +%% @doc Get session stats +-spec(stats(session()) -> list({atom(), non_neg_integer()})). +stats(#session{max_subscriptions = MaxSubscriptions, + subscriptions = Subscriptions, + inflight = Inflight, + mqueue = MQueue, + max_awaiting_rel = MaxAwaitingRel, + awaiting_rel = AwaitingRel}) -> lists:append(emqx_misc:proc_stats(), - [{max_subscriptions, get_env(Zone, max_subscriptions, 0)}, + [{max_subscriptions, MaxSubscriptions}, {subscriptions_count, maps:size(Subscriptions)}, {max_inflight, emqx_inflight:max_size(Inflight)}, {inflight_len, emqx_inflight:size(Inflight)}, {max_mqueue, emqx_mqueue:max_len(MQueue)}, {mqueue_len, emqx_mqueue:len(MQueue)}, {mqueue_dropped, emqx_mqueue:dropped(MQueue)}, - {max_awaiting_rel, get_env(Zone, max_awaiting_rel)}, + {max_awaiting_rel, MaxAwaitingRel}, {awaiting_rel_len, maps:size(AwaitingRel)}, {deliver_msg, emqx_pd:get_counter(deliver_stats)}, {enqueue_msg, emqx_pd:get_counter(enqueue_stats)}]). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% PubSub API -%%------------------------------------------------------------------------------ - --spec(subscribe(spid(), list({emqx_topic:topic(), emqx_types:subopts()})) -> ok). -subscribe(SPid, RawTopicFilters) when is_list(RawTopicFilters) -> - TopicFilters = [emqx_topic:parse(RawTopic, maps:merge(?DEFAULT_SUBOPTS, SubOpts)) - || {RawTopic, SubOpts} <- RawTopicFilters], - subscribe(SPid, undefined, #{}, TopicFilters). - --spec(subscribe(spid(), emqx_mqtt_types:packet_id(), - emqx_mqtt_types:properties(), emqx_mqtt_types:topic_filters()) -> ok). -subscribe(SPid, PacketId, Properties, TopicFilters) -> - SubReq = {PacketId, Properties, TopicFilters}, - gen_server:cast(SPid, {subscribe, self(), SubReq}). - -%% @doc Called by connection processes when publishing messages --spec(publish(spid(), emqx_mqtt_types:packet_id(), emqx_types:message()) - -> {ok, emqx_types:deliver_results()} | {error, term()}). -publish(_SPid, _PacketId, Msg = #message{qos = ?QOS_0}) -> - %% Publish QoS0 message directly - {ok, emqx_broker:publish(Msg)}; - -publish(_SPid, _PacketId, Msg = #message{qos = ?QOS_1}) -> - %% Publish QoS1 message directly - {ok, emqx_broker:publish(Msg)}; - -publish(SPid, PacketId, Msg = #message{qos = ?QOS_2, timestamp = Ts}) -> - %% Register QoS2 message packet ID (and timestamp) to session, then publish - case gen_server:call(SPid, {register_publish_packet_id, PacketId, Ts}, infinity) of - ok -> {ok, emqx_broker:publish(Msg)}; - {error, Reason} -> {error, Reason} - end. - --spec(puback(spid(), emqx_mqtt_types:packet_id()) -> ok). -puback(SPid, PacketId) -> - gen_server:cast(SPid, {puback, PacketId, ?RC_SUCCESS}). - --spec(puback(spid(), emqx_mqtt_types:packet_id(), emqx_mqtt_types:reason_code()) -> ok). -puback(SPid, PacketId, ReasonCode) -> - gen_server:cast(SPid, {puback, PacketId, ReasonCode}). - --spec(pubrec(spid(), emqx_mqtt_types:packet_id()) -> ok | {error, emqx_mqtt_types:reason_code()}). -pubrec(SPid, PacketId) -> - pubrec(SPid, PacketId, ?RC_SUCCESS). - --spec(pubrec(spid(), emqx_mqtt_types:packet_id(), emqx_mqtt_types:reason_code()) - -> ok | {error, emqx_mqtt_types:reason_code()}). -pubrec(SPid, PacketId, ReasonCode) -> - gen_server:call(SPid, {pubrec, PacketId, ReasonCode}, infinity). - --spec(pubrel(spid(), emqx_mqtt_types:packet_id(), emqx_mqtt_types:reason_code()) - -> ok | {error, emqx_mqtt_types:reason_code()}). -pubrel(SPid, PacketId, ReasonCode) -> - gen_server:call(SPid, {pubrel, PacketId, ReasonCode}, infinity). - --spec(pubcomp(spid(), emqx_mqtt_types:packet_id(), emqx_mqtt_types:reason_code()) -> ok). -pubcomp(SPid, PacketId, ReasonCode) -> - gen_server:cast(SPid, {pubcomp, PacketId, ReasonCode}). - --spec(unsubscribe(spid(), emqx_types:topic_table()) -> ok). -unsubscribe(SPid, RawTopicFilters) when is_list(RawTopicFilters) -> - TopicFilters = lists:map(fun({RawTopic, Opts}) -> - emqx_topic:parse(RawTopic, Opts); - (RawTopic) when is_binary(RawTopic) -> - emqx_topic:parse(RawTopic) - end, RawTopicFilters), - unsubscribe(SPid, undefined, #{}, TopicFilters). - --spec(unsubscribe(spid(), emqx_mqtt_types:packet_id(), - emqx_mqtt_types:properties(), emqx_mqtt_types:topic_filters()) -> ok). -unsubscribe(SPid, PacketId, Properties, TopicFilters) -> - UnsubReq = {PacketId, Properties, TopicFilters}, - gen_server:cast(SPid, {unsubscribe, self(), UnsubReq}). - --spec(resume(spid(), map()) -> ok). -resume(SPid, SessAttrs) -> - gen_server:cast(SPid, {resume, SessAttrs}). - -%% @doc Discard the session --spec(discard(spid(), ByPid :: pid()) -> ok). -discard(SPid, ByPid) -> - gen_server:call(SPid, {discard, ByPid}, infinity). - --spec(update_expiry_interval(spid(), timeout()) -> ok). -update_expiry_interval(SPid, Interval) -> - gen_server:cast(SPid, {update_expiry_interval, Interval}). - --spec(close(spid()) -> ok). -close(SPid) -> - gen_server:call(SPid, close, infinity). - -%%------------------------------------------------------------------------------ -%% gen_server callbacks -%%------------------------------------------------------------------------------ - -init([Parent, #{zone := Zone, - client_id := ClientId, - username := Username, - conn_pid := ConnPid, - clean_start := CleanStart, - expiry_interval := ExpiryInterval, - max_inflight := MaxInflight, - will_msg := WillMsg}]) -> - process_flag(trap_exit, true), - true = link(ConnPid), - emqx_logger:set_metadata_client_id(ClientId), - GcPolicy = emqx_zone:get_env(Zone, force_gc_policy, false), - IdleTimout = get_env(Zone, idle_timeout, 30000), - State = #state{zone = Zone, - idle_timeout = IdleTimout, - clean_start = CleanStart, - deliver_fun = deliver_fun(ConnPid), - client_id = ClientId, - username = Username, - conn_pid = ConnPid, - subscriptions = #{}, - inflight = emqx_inflight:new(MaxInflight), - mqueue = init_mqueue(Zone), - awaiting_rel = #{}, - expiry_interval = ExpiryInterval, - gc_state = emqx_gc:init(GcPolicy), - created_at = os:timestamp(), - will_msg = WillMsg - }, - ok = emqx_sm:register_session(ClientId, self()), - true = emqx_sm:set_session_attrs(ClientId, attrs(State)), - true = emqx_sm:set_session_stats(ClientId, stats(State)), - ok = emqx_hooks:run('session.created', [#{client_id => ClientId}, info(State)]), - ok = emqx_misc:init_proc_mng_policy(Zone), - ok = proc_lib:init_ack(Parent, {ok, self()}), - gen_server:enter_loop(?MODULE, [{hibernate_after, IdleTimout}], State). - -init_mqueue(Zone) -> - emqx_mqueue:init(#{max_len => get_env(Zone, max_mqueue_len, 1000), - store_qos0 => get_env(Zone, mqueue_store_qos0, true), - priorities => get_env(Zone, mqueue_priorities), - default_priority => get_env(Zone, mqueue_default_priority) - }). - -binding(undefined) -> undefined; -binding(ConnPid) -> - case node(ConnPid) =:= node() of true -> local; false -> remote end. - -deliver_fun(ConnPid) when node(ConnPid) == node() -> - fun(Packet) -> ConnPid ! {deliver, Packet}, ok end; -deliver_fun(ConnPid) -> - Node = node(ConnPid), - fun(Packet) -> - true = emqx_rpc:cast(Node, erlang, send, [ConnPid, {deliver, Packet}]), ok - end. - -handle_call(info, _From, State) -> - reply(info(State), State); - -handle_call(attrs, _From, State) -> - reply(attrs(State), State); - -handle_call(stats, _From, State) -> - reply(stats(State), State); - -handle_call({discard, ByPid}, _From, State = #state{conn_pid = undefined}) -> - ?LOG(warning, "[Session] Discarded by ~p", [ByPid]), - {stop, {shutdown, discarded}, ok, State}; - -handle_call({discard, ByPid}, _From, State = #state{client_id = ClientId, conn_pid = ConnPid}) -> - ?LOG(warning, "[Session] Conn ~p is discarded by ~p", [ConnPid, ByPid]), - ConnPid ! {shutdown, discard, {ClientId, ByPid}}, - {stop, {shutdown, discarded}, ok, State}; - -%% PUBLISH: This is only to register packetId to session state. -%% The actual message dispatching should be done by the caller (e.g. connection) process. -handle_call({register_publish_packet_id, PacketId, Ts}, _From, - State = #state{zone = Zone, awaiting_rel = AwaitingRel}) -> - MaxAwaitingRel = get_env(Zone, max_awaiting_rel), - reply( - case is_awaiting_full(MaxAwaitingRel, AwaitingRel) of - false -> - case maps:is_key(PacketId, AwaitingRel) of - true -> - {{error, ?RC_PACKET_IDENTIFIER_IN_USE}, State}; - false -> - State1 = State#state{awaiting_rel = maps:put(PacketId, Ts, AwaitingRel)}, - {ok, ensure_stats_timer(ensure_await_rel_timer(State1))} - end; - true -> - ?LOG(warning, "[Session] Dropped qos2 packet ~w for too many awaiting_rel", [PacketId]), - ok = emqx_metrics:inc('messages.qos2.dropped'), - {{error, ?RC_RECEIVE_MAXIMUM_EXCEEDED}, State} - end); - -%% PUBREC: -handle_call({pubrec, PacketId, _ReasonCode}, _From, State = #state{inflight = Inflight}) -> - reply( - case emqx_inflight:contain(PacketId, Inflight) of - true -> - {ok, ensure_stats_timer(acked(pubrec, PacketId, State))}; - false -> - ?LOG(warning, "[Session] The PUBREC PacketId ~w is not found.", [PacketId]), - ok = emqx_metrics:inc('packets.pubrec.missed'), - {{error, ?RC_PACKET_IDENTIFIER_NOT_FOUND}, State} - end); - -%% PUBREL: -handle_call({pubrel, PacketId, _ReasonCode}, _From, State = #state{awaiting_rel = AwaitingRel}) -> - reply( - case maps:take(PacketId, AwaitingRel) of - {_Ts, AwaitingRel1} -> - {ok, ensure_stats_timer(State#state{awaiting_rel = AwaitingRel1})}; - error -> - ?LOG(warning, "[Session] The PUBREL PacketId ~w is not found", [PacketId]), - ok = emqx_metrics:inc('packets.pubrel.missed'), - {{error, ?RC_PACKET_IDENTIFIER_NOT_FOUND}, State} - end); - -handle_call(close, _From, State) -> - {stop, normal, ok, State}; - -handle_call(Req, _From, State) -> - ?LOG(error, "[Session] Unexpected call: ~p", [Req]), - {reply, ignored, State}. +%%-------------------------------------------------------------------- %% SUBSCRIBE: -handle_cast({subscribe, FromPid, {PacketId, _Properties, TopicFilters}}, - State = #state{client_id = ClientId, username = Username, subscriptions = Subscriptions}) -> +-spec(subscribe(list({emqx_topic:topic(), emqx_types:subopts()}), session()) + -> {ok, list(emqx_mqtt_types:reason_code()), session()}). +subscribe(RawTopicFilters, Session = #session{client_id = ClientId, + username = Username, + subscriptions = Subscriptions}) + when is_list(RawTopicFilters) -> + TopicFilters = [emqx_topic:parse(RawTopic, maps:merge(?DEFAULT_SUBOPTS, SubOpts)) + || {RawTopic, SubOpts} <- RawTopicFilters], {ReasonCodes, Subscriptions1} = lists:foldr( fun ({Topic, SubOpts = #{qos := QoS, rc := RC}}, {RcAcc, SubMap}) when @@ -470,12 +250,173 @@ handle_cast({subscribe, FromPid, {PacketId, _Properties, TopicFilters}}, ({_Topic, #{rc := RC}}, {RcAcc, SubMap}) -> {[RC|RcAcc], SubMap} end, {[], Subscriptions}, TopicFilters), - suback(FromPid, PacketId, ReasonCodes), - noreply(ensure_stats_timer(State#state{subscriptions = Subscriptions1})); + {ok, ReasonCodes, Session#session{subscriptions = Subscriptions1}}. + +%% TODO: improve later. +do_subscribe(ClientId, Username, Topic, SubOpts, SubMap) -> + case maps:find(Topic, SubMap) of + {ok, SubOpts} -> + ok = emqx_hooks:run('session.subscribed', [#{client_id => ClientId, username => Username}, Topic, SubOpts#{first => false}]), + SubMap; + {ok, _SubOpts} -> + emqx_broker:set_subopts(Topic, SubOpts), + %% Why??? + ok = emqx_hooks:run('session.subscribed', [#{client_id => ClientId, username => Username}, Topic, SubOpts#{first => false}]), + maps:put(Topic, SubOpts, SubMap); + error -> + emqx_broker:subscribe(Topic, ClientId, SubOpts), + ok = emqx_hooks:run('session.subscribed', [#{client_id => ClientId, username => Username}, Topic, SubOpts#{first => true}]), + maps:put(Topic, SubOpts, SubMap) + end. + +%% PUBLISH: +-spec(publish(emqx_mqtt_types:packet_id(), emqx_types:message(), session()) + -> {ok, emqx_types:deliver_results()} | {error, term()}). +publish(_PacketId, Msg = #message{qos = ?QOS_0}, _Session) -> + %% Publish QoS0 message directly + {ok, emqx_broker:publish(Msg)}; + +publish(_PacketId, Msg = #message{qos = ?QOS_1}, _Session) -> + %% Publish QoS1 message directly + {ok, emqx_broker:publish(Msg)}; + +%% PUBLISH: This is only to register packetId to session state. +%% The actual message dispatching should be done by the caller. +publish(PacketId, Msg = #message{qos = ?QOS_2, timestamp = Ts}, + Session = #session{awaiting_rel = AwaitingRel, + max_awaiting_rel = MaxAwaitingRel}) -> + %% Register QoS2 message packet ID (and timestamp) to session, then publish + case is_awaiting_full(MaxAwaitingRel, AwaitingRel) of + false -> + case maps:is_key(PacketId, AwaitingRel) of + false -> + NewAwaitingRel = maps:put(PacketId, Ts, AwaitingRel), + NSession = Session#session{awaiting_rel = NewAwaitingRel}, + {ok, emqx_broker:publish(Msg), ensure_await_rel_timer(NSession)}; + true -> + {error, ?RC_PACKET_IDENTIFIER_IN_USE} + end; + true -> + ?LOG(warning, "[Session] Dropped qos2 packet ~w for too many awaiting_rel", [PacketId]), + ok = emqx_metrics:inc('messages.qos2.dropped'), + {error, ?RC_RECEIVE_MAXIMUM_EXCEEDED} + end. + +%% PUBACK: +-spec(puback(emqx_mqtt_types:packet_id(), session()) -> session()). +puback(PacketId, Session) -> + puback(PacketId, ?RC_SUCCESS, Session). + +-spec(puback(emqx_mqtt_types:packet_id(), emqx_mqtt_types:reason_code(), session()) + -> session()). +puback(PacketId, ReasonCode, Session = #session{inflight = Inflight}) -> + case emqx_inflight:contain(PacketId, Inflight) of + true -> + dequeue(acked(puback, PacketId, Session)); + false -> + ?LOG(warning, "[Session] The PUBACK PacketId ~w is not found", [PacketId]), + ok = emqx_metrics:inc('packets.puback.missed'), + Session + end. + +%% PUBREC: +-spec(pubrec(emqx_mqtt_types:packet_id(), session()) + -> ok | {error, emqx_mqtt_types:reason_code()}). +pubrec(PacketId, Session) -> + pubrec(PacketId, ?RC_SUCCESS, Session). + +-spec(pubrec(emqx_mqtt_types:packet_id(), emqx_mqtt_types:reason_code(), session()) + -> {ok, session()} | {error, emqx_mqtt_types:reason_code()}). +pubrec(PacketId, ReasonCode, Session = #session{inflight = Inflight}) -> + case emqx_inflight:contain(PacketId, Inflight) of + true -> + {ok, acked(pubrec, PacketId, Session)}; + false -> + ?LOG(warning, "[Session] The PUBREC PacketId ~w is not found.", [PacketId]), + ok = emqx_metrics:inc('packets.pubrec.missed'), + {error, ?RC_PACKET_IDENTIFIER_NOT_FOUND} + end. + +%% PUBREL: +-spec(pubrel(emqx_mqtt_types:packet_id(), emqx_mqtt_types:reason_code(), session()) + -> {ok, session()} | {error, emqx_mqtt_types:reason_code()}). +pubrel(PacketId, ReasonCode, Session = #session{awaiting_rel = AwaitingRel}) -> + case maps:take(PacketId, AwaitingRel) of + {_Ts, AwaitingRel1} -> + {ok, Session#session{awaiting_rel = AwaitingRel1}}; + error -> + ?LOG(warning, "[Session] The PUBREL PacketId ~w is not found", [PacketId]), + ok = emqx_metrics:inc('packets.pubrel.missed'), + {error, ?RC_PACKET_IDENTIFIER_NOT_FOUND} + end. + +%% PUBCOMP: +-spec(pubcomp(emqx_mqtt_types:packet_id(), emqx_mqtt_types:reason_code(), session()) + -> {ok, session()} | {error, emqx_mqtt_types:reason_code()}). +pubcomp(PacketId, ReasonCode, Session = #session{inflight = Inflight}) -> + case emqx_inflight:contain(PacketId, Inflight) of + true -> + {ok, dequeue(acked(pubcomp, PacketId, Session))}; + false -> + ?LOG(warning, "[Session] The PUBCOMP PacketId ~w is not found", [PacketId]), + ok = emqx_metrics:inc('packets.pubcomp.missed'), + {error, ?RC_PACKET_IDENTIFIER_NOT_FOUND} + end. + +%%------------------------------------------------------------------------------ +%% Awaiting ACK for QoS1/QoS2 Messages +%%------------------------------------------------------------------------------ + +await(PacketId, Msg, Session = #session{inflight = Inflight}) -> + Inflight1 = emqx_inflight:insert( + PacketId, {publish, {PacketId, Msg}, os:timestamp()}, Inflight), + ensure_retry_timer(Session#session{inflight = Inflight1}). + +acked(puback, PacketId, Session = #session{client_id = ClientId, username = Username, inflight = Inflight}) -> + case emqx_inflight:lookup(PacketId, Inflight) of + {value, {publish, {_, Msg}, _Ts}} -> + ok = emqx_hooks:run('message.acked', [#{client_id => ClientId, username => Username}, Msg]), + Session#session{inflight = emqx_inflight:delete(PacketId, Inflight)}; + none -> + ?LOG(warning, "[Session] Duplicated PUBACK PacketId ~w", [PacketId]), + Session + end; + +acked(pubrec, PacketId, Session = #session{client_id = ClientId, username = Username, inflight = Inflight}) -> + case emqx_inflight:lookup(PacketId, Inflight) of + {value, {publish, {_, Msg}, _Ts}} -> + ok = emqx_hooks:run('message.acked', [#{client_id => ClientId, username => Username}, Msg]), + Inflight1 = emqx_inflight:update(PacketId, {pubrel, PacketId, os:timestamp()}, Inflight), + Session#session{inflight = Inflight1}; + {value, {pubrel, PacketId, _Ts}} -> + ?LOG(warning, "[Session] Duplicated PUBREC PacketId ~w", [PacketId]), + Session; + none -> + ?LOG(warning, "[Session] Unexpected PUBREC PacketId ~w", [PacketId]), + Session + end; + +acked(pubcomp, PacketId, Session = #session{inflight = Inflight}) -> + case emqx_inflight:contain(PacketId, Inflight) of + true -> + Session#session{inflight = emqx_inflight:delete(PacketId, Inflight)}; + false -> + ?LOG(warning, "[Session] PUBCOMP PacketId ~w is not found", [PacketId]), + Session + end. %% UNSUBSCRIBE: -handle_cast({unsubscribe, From, {PacketId, _Properties, TopicFilters}}, - State = #state{client_id = ClientId, username = Username, subscriptions = Subscriptions}) -> +-spec(unsubscribe(emqx_mqtt_types:topic_filters(), session()) + -> {ok, list(emqx_mqtt_types:reason_code()), session()}). +unsubscribe(RawTopicFilters, Session = #session{client_id = ClientId, + username = Username, + subscriptions = Subscriptions}) + when is_list(RawTopicFilters) -> + TopicFilters = lists:map(fun({RawTopic, Opts}) -> + emqx_topic:parse(RawTopic, Opts); + (RawTopic) when is_binary(RawTopic) -> + emqx_topic:parse(RawTopic) + end, RawTopicFilters), {ReasonCodes, Subscriptions1} = lists:foldr(fun({Topic, _SubOpts}, {Acc, SubMap}) -> case maps:find(Topic, SubMap) of @@ -487,239 +428,91 @@ handle_cast({unsubscribe, From, {PacketId, _Properties, TopicFilters}}, {[?RC_NO_SUBSCRIPTION_EXISTED|Acc], SubMap} end end, {[], Subscriptions}, TopicFilters), - unsuback(From, PacketId, ReasonCodes), - noreply(ensure_stats_timer(State#state{subscriptions = Subscriptions1})); + {ok, ReasonCodes, Session#session{subscriptions = Subscriptions1}}. -%% PUBACK: -handle_cast({puback, PacketId, _ReasonCode}, State = #state{inflight = Inflight}) -> - noreply( - case emqx_inflight:contain(PacketId, Inflight) of - true -> - ensure_stats_timer(dequeue(acked(puback, PacketId, State))); - false -> - ?LOG(warning, "[Session] The PUBACK PacketId ~w is not found", [PacketId]), - ok = emqx_metrics:inc('packets.puback.missed'), - State - end); +-spec(resume(map(), session()) -> session()). +resume(#{will_msg := WillMsg, + expiry_interval := ExpiryInterval, + max_inflight := MaxInflight}, + Session = #session{client_id = ClientId, + clean_start = CleanStart, + retry_timer = RetryTimer, + await_rel_timer = AwaitTimer, + expiry_timer = ExpireTimer, + will_delay_timer = WillDelayTimer}) -> -%% PUBCOMP: -handle_cast({pubcomp, PacketId, _ReasonCode}, State = #state{inflight = Inflight}) -> - noreply( - case emqx_inflight:contain(PacketId, Inflight) of - true -> - ensure_stats_timer(dequeue(acked(pubcomp, PacketId, State))); - false -> - ?LOG(warning, "[Session] The PUBCOMP PacketId ~w is not found", [PacketId]), - ok = emqx_metrics:inc('packets.pubcomp.missed'), - State - end); - -%% RESUME: -handle_cast({resume, #{conn_pid := ConnPid, - will_msg := WillMsg, - expiry_interval := ExpiryInterval, - max_inflight := MaxInflight}}, - State = #state{client_id = ClientId, - conn_pid = OldConnPid, - clean_start = CleanStart, - retry_timer = RetryTimer, - await_rel_timer = AwaitTimer, - expiry_timer = ExpireTimer, - will_delay_timer = WillDelayTimer}) -> - - ?LOG(info, "[Session] Resumed by connection ~p ", [ConnPid]), + %% ?LOG(info, "[Session] Resumed by ~p ", [self()]), %% Cancel Timers lists:foreach(fun emqx_misc:cancel_timer/1, [RetryTimer, AwaitTimer, ExpireTimer, WillDelayTimer]), - case kick(ClientId, OldConnPid, ConnPid) of - ok -> ?LOG(warning, "[Session] Connection ~p kickout ~p", [ConnPid, OldConnPid]); - ignore -> ok - end, + %% case kick(ClientId, OldConnPid, ConnPid) of + %% ok -> ?LOG(warning, "[Session] Connection ~p kickout ~p", [ConnPid, OldConnPid]); + %% ignore -> ok + %% end, - true = link(ConnPid), + Inflight = emqx_inflight:update_size(MaxInflight, Session#session.inflight), - State1 = State#state{conn_pid = ConnPid, - deliver_fun = deliver_fun(ConnPid), - old_conn_pid = OldConnPid, - clean_start = false, - retry_timer = undefined, - awaiting_rel = #{}, - await_rel_timer = undefined, - expiry_timer = undefined, - expiry_interval = ExpiryInterval, - inflight = emqx_inflight:update_size(MaxInflight, State#state.inflight), - will_delay_timer = undefined, - will_msg = WillMsg}, + Session1 = Session#session{clean_start = false, + retry_timer = undefined, + awaiting_rel = #{}, + await_rel_timer = undefined, + expiry_timer = undefined, + expiry_interval = ExpiryInterval, + inflight = Inflight, + will_delay_timer = undefined, + will_msg = WillMsg + }, %% Clean Session: true -> false??? - CleanStart andalso emqx_sm:set_session_attrs(ClientId, attrs(State1)), + CleanStart andalso emqx_cm:set_session_attrs(ClientId, attrs(Session1)), - ok = emqx_hooks:run('session.resumed', [#{client_id => ClientId}, attrs(State)]), + %%ok = emqx_hooks:run('session.resumed', [#{client_id => ClientId}, attrs(Session1)]), %% Replay delivery and Dequeue pending messages - noreply(ensure_stats_timer(dequeue(retry_delivery(true, State1)))); + dequeue(retry_delivery(true, Session1)). -handle_cast({update_expiry_interval, Interval}, State) -> - {noreply, State#state{expiry_interval = Interval}}; +-spec(update_expiry_interval(timeout(), session()) -> session()). +update_expiry_interval(Interval, Session) -> + Session#session{expiry_interval = Interval}. -handle_cast(Msg, State) -> - ?LOG(error, "[Session] Unexpected cast: ~p", [Msg]), - {noreply, State}. - -handle_info({dispatch, Topic, Msg}, State) when is_record(Msg, message) -> - handle_dispatch([{Topic, Msg}], State); - -handle_info({dispatch, Topic, Msgs}, State) when is_list(Msgs) -> - handle_dispatch([{Topic, Msg} || Msg <- Msgs], State); - -%% Do nothing if the client has been disconnected. -handle_info({timeout, Timer, retry_delivery}, State = #state{conn_pid = undefined, retry_timer = Timer}) -> - noreply(State#state{retry_timer = undefined}); - -handle_info({timeout, Timer, retry_delivery}, State = #state{retry_timer = Timer}) -> - noreply(retry_delivery(false, State#state{retry_timer = undefined})); - -handle_info({timeout, Timer, check_awaiting_rel}, State = #state{await_rel_timer = Timer}) -> - State1 = State#state{await_rel_timer = undefined}, - noreply(ensure_stats_timer(expire_awaiting_rel(State1))); - -handle_info({timeout, Timer, emit_stats}, - State = #state{client_id = ClientId, - stats_timer = Timer, - gc_state = GcState}) -> - _ = emqx_sm:set_session_stats(ClientId, stats(State)), - NewState = State#state{stats_timer = undefined}, - Limits = erlang:get(force_shutdown_policy), - case emqx_misc:conn_proc_mng_policy(Limits) of - continue -> - {noreply, NewState}; - hibernate -> - %% going to hibernate, reset gc stats - GcState1 = emqx_gc:reset(GcState), - {noreply, NewState#state{gc_state = GcState1}, hibernate}; - {shutdown, Reason} -> - ?LOG(warning, "[Session] Shutdown exceptionally due to ~p", [Reason]), - shutdown(Reason, NewState) - end; - -handle_info({timeout, Timer, expired}, State = #state{expiry_timer = Timer}) -> - ?LOG(info, "[Session] Expired, shutdown now.", []), - shutdown(expired, State); - -handle_info({timeout, Timer, will_delay}, State = #state{will_msg = WillMsg, will_delay_timer = Timer}) -> - send_willmsg(WillMsg), - {noreply, State#state{will_msg = undefined}}; - -%% ConnPid is shutting down by the supervisor. -handle_info({'EXIT', ConnPid, Reason}, #state{conn_pid = ConnPid}) - when Reason =:= killed; Reason =:= shutdown -> - exit(Reason); - -handle_info({'EXIT', ConnPid, Reason}, State = #state{will_msg = WillMsg, expiry_interval = 0, conn_pid = ConnPid}) -> - case Reason of - normal -> - ignore; - _ -> - send_willmsg(WillMsg) - end, - {stop, Reason, State#state{will_msg = undefined, conn_pid = undefined}}; - -handle_info({'EXIT', ConnPid, Reason}, State = #state{conn_pid = ConnPid}) -> - State1 = case Reason of - normal -> - State#state{will_msg = undefined}; - _ -> - ensure_will_delay_timer(State) - end, - {noreply, ensure_expire_timer(State1#state{conn_pid = undefined})}; - -handle_info({'EXIT', OldPid, _Reason}, State = #state{old_conn_pid = OldPid}) -> - %% ignore - {noreply, State#state{old_conn_pid = undefined}}; - -handle_info({'EXIT', Pid, Reason}, State = #state{conn_pid = ConnPid}) -> - ?LOG(error, "[Session] Unexpected EXIT: conn_pid=~p, exit_pid=~p, reason=~p", - [ConnPid, Pid, Reason]), - {noreply, State}; - -handle_info(Info, State) -> - ?LOG(error, "[Session] Unexpected info: ~p", [Info]), - {noreply, State}. - -terminate(Reason, #state{will_msg = WillMsg, - client_id = ClientId, - username = Username, - conn_pid = ConnPid, - old_conn_pid = OldConnPid}) -> - send_willmsg(WillMsg), - [maybe_shutdown(Pid, Reason) || Pid <- [ConnPid, OldConnPid]], - ok = emqx_hooks:run('session.terminated', [#{client_id => ClientId, username => Username}, Reason]). - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -maybe_shutdown(undefined, _Reason) -> - ok; -maybe_shutdown(Pid, normal) -> - Pid ! {shutdown, normal}; -maybe_shutdown(Pid, Reason) -> - exit(Pid, Reason). +-spec(close(session()) -> ok). +close(_Session) -> ok. %%------------------------------------------------------------------------------ %% Internal functions %%------------------------------------------------------------------------------ -is_connection_alive(#state{conn_pid = Pid}) -> - is_pid(Pid) andalso is_process_alive(Pid). -%%------------------------------------------------------------------------------ -%% Suback and unsuback - -suback(_From, undefined, _ReasonCodes) -> - ignore; -suback(From, PacketId, ReasonCodes) -> - From ! {deliver, {suback, PacketId, ReasonCodes}}. - -unsuback(_From, undefined, _ReasonCodes) -> - ignore; -unsuback(From, PacketId, ReasonCodes) -> - From ! {deliver, {unsuback, PacketId, ReasonCodes}}. - -%%------------------------------------------------------------------------------ -%% Kickout old connection - -kick(_ClientId, undefined, _ConnPid) -> - ignore; -kick(_ClientId, ConnPid, ConnPid) -> - ignore; -kick(ClientId, OldConnPid, ConnPid) -> - unlink(OldConnPid), - OldConnPid ! {shutdown, conflict, {ClientId, ConnPid}}, - %% Clean noproc - receive {'EXIT', OldConnPid, _} -> ok after 1 -> ok end. +%%deliver_fun(ConnPid) when node(ConnPid) == node() -> +%% fun(Packet) -> ConnPid ! {deliver, Packet}, ok end; +%%deliver_fun(ConnPid) -> +%% Node = node(ConnPid), +%% fun(Packet) -> +%% true = emqx_rpc:cast(Node, erlang, send, [ConnPid, {deliver, Packet}]), ok +%% end. %%------------------------------------------------------------------------------ %% Replay or Retry Delivery %% Redeliver at once if force is true -retry_delivery(Force, State = #state{inflight = Inflight}) -> +retry_delivery(Force, Session = #session{inflight = Inflight}) -> case emqx_inflight:is_empty(Inflight) of - true -> State; + true -> Session; false -> SortFun = fun({_, _, Ts1}, {_, _, Ts2}) -> Ts1 < Ts2 end, Msgs = lists:sort(SortFun, emqx_inflight:values(Inflight)), - retry_delivery(Force, Msgs, os:timestamp(), State) + retry_delivery(Force, Msgs, os:timestamp(), Session) end. -retry_delivery(_Force, [], _Now, State) -> +retry_delivery(_Force, [], _Now, Session) -> %% Retry again... - ensure_retry_timer(State); + ensure_retry_timer(Session); retry_delivery(Force, [{Type, Msg0, Ts} | Msgs], Now, - State = #state{zone = Zone, inflight = Inflight}) -> - Interval = get_env(Zone, retry_interval, 0), + Session = #session{inflight = Inflight, + retry_interval = Interval}) -> %% Microseconds -> MilliSeconds Age = timer:now_diff(Now, Ts) div 1000, if @@ -731,16 +524,16 @@ retry_delivery(Force, [{Type, Msg0, Ts} | Msgs], Now, ok = emqx_metrics:inc('messages.expired'), emqx_inflight:delete(PacketId, Inflight); false -> - redeliver({PacketId, Msg}, State), + redeliver({PacketId, Msg}, Session), emqx_inflight:update(PacketId, {publish, {PacketId, Msg}, Now}, Inflight) end; {pubrel, PacketId} -> - redeliver({pubrel, PacketId}, State), + redeliver({pubrel, PacketId}, Session), emqx_inflight:update(PacketId, {pubrel, PacketId, Now}, Inflight) end, - retry_delivery(Force, Msgs, Now, State#state{inflight = Inflight1}); + retry_delivery(Force, Msgs, Now, Session#session{inflight = Inflight1}); true -> - ensure_retry_timer(Interval - max(0, Age), State) + ensure_retry_timer(Interval - max(0, Age), Session) end. %%------------------------------------------------------------------------------ @@ -756,25 +549,26 @@ send_willmsg(WillMsg) -> %% Expire Awaiting Rel %%------------------------------------------------------------------------------ -expire_awaiting_rel(State = #state{awaiting_rel = AwaitingRel}) -> +expire_awaiting_rel(Session = #session{awaiting_rel = AwaitingRel}) -> case maps:size(AwaitingRel) of - 0 -> State; - _ -> expire_awaiting_rel(lists:keysort(2, maps:to_list(AwaitingRel)), os:timestamp(), State) + 0 -> Session; + _ -> expire_awaiting_rel(lists:keysort(2, maps:to_list(AwaitingRel)), os:timestamp(), Session) end. -expire_awaiting_rel([], _Now, State) -> - State#state{await_rel_timer = undefined}; +expire_awaiting_rel([], _Now, Session) -> + Session#session{await_rel_timer = undefined}; expire_awaiting_rel([{PacketId, Ts} | More], Now, - State = #state{zone = Zone, awaiting_rel = AwaitingRel}) -> - Timeout = get_env(Zone, await_rel_timeout), + Session = #session{awaiting_rel = AwaitingRel, + await_rel_timeout = Timeout}) -> case (timer:now_diff(Now, Ts) div 1000) of Age when Age >= Timeout -> ok = emqx_metrics:inc('messages.qos2.expired'), ?LOG(warning, "[Session] Dropped qos2 packet ~s for await_rel_timeout", [PacketId]), - expire_awaiting_rel(More, Now, State#state{awaiting_rel = maps:remove(PacketId, AwaitingRel)}); + NSession = Session#session{awaiting_rel = maps:remove(PacketId, AwaitingRel)}, + expire_awaiting_rel(More, Now, NSession); Age -> - ensure_await_rel_timer(Timeout - max(0, Age), State) + ensure_await_rel_timer(Timeout - max(0, Age), Session) end. %%------------------------------------------------------------------------------ @@ -790,69 +584,46 @@ is_awaiting_full(MaxAwaitingRel, AwaitingRel) -> %% Dispatch messages %%------------------------------------------------------------------------------ -handle_dispatch(Msgs, State = #state{inflight = Inflight, - client_id = ClientId, - username = Username, - subscriptions = SubMap}) -> +handle(Msgs, Session = #session{inflight = Inflight, + client_id = ClientId, + username = Username, + subscriptions = SubMap}) -> SessProps = #{client_id => ClientId, username => Username}, %% Drain the mailbox and batch deliver - Msgs1 = drain_m(batch_n(Inflight), Msgs), + Msgs1 = Msgs, %% drain_m(batch_n(Inflight), Msgs), %% Ack the messages for shared subscription - Msgs2 = maybe_ack_shared(Msgs1, State), + Msgs2 = maybe_ack_shared(Msgs1, Session), %% Process suboptions Msgs3 = lists:foldr( fun({Topic, Msg}, Acc) -> SubOpts = find_subopts(Topic, SubMap), - case process_subopts(SubOpts, Msg, State) of + case process_subopts(SubOpts, Msg, Session) of {ok, Msg1} -> [Msg1|Acc]; ignore -> emqx_hooks:run('message.dropped', [SessProps, Msg]), Acc end end, [], Msgs2), - NState = batch_process(Msgs3, State), - noreply(ensure_stats_timer(NState)). - -batch_n(Inflight) -> - case emqx_inflight:max_size(Inflight) of - 0 -> ?DEFAULT_BATCH_N; - Sz -> Sz - emqx_inflight:size(Inflight) - end. - -drain_m(Cnt, Msgs) when Cnt =< 0 -> - lists:reverse(Msgs); -drain_m(Cnt, Msgs) -> - receive - {dispatch, Topic, Msg} when is_record(Msg, message)-> - drain_m(Cnt-1, [{Topic, Msg} | Msgs]); - {dispatch, Topic, InMsgs} when is_list(InMsgs) -> - Msgs1 = lists:foldl( - fun(Msg, Acc) -> - [{Topic, Msg} | Acc] - end, Msgs, InMsgs), - drain_m(Cnt-length(InMsgs), Msgs1) - after 0 -> - lists:reverse(Msgs) - end. + batch_process(Msgs3, Session). %% Ack or nack the messages of shared subscription? -maybe_ack_shared(Msgs, State) when is_list(Msgs) -> +maybe_ack_shared(Msgs, Session) when is_list(Msgs) -> lists:foldr( fun({Topic, Msg}, Acc) -> - case maybe_ack_shared(Msg, State) of + case maybe_ack_shared(Msg, Session) of ok -> Acc; Msg1 -> [{Topic, Msg1}|Acc] end end, [], Msgs); -maybe_ack_shared(Msg, State) -> +maybe_ack_shared(Msg, Session) -> case emqx_shared_sub:is_ack_required(Msg) of - true -> do_ack_shared(Msg, State); + true -> do_ack_shared(Msg, Session); false -> Msg end. -do_ack_shared(Msg, State = #state{inflight = Inflight}) -> - case {is_connection_alive(State), +do_ack_shared(Msg, Session = #session{inflight = Inflight}) -> + case {true, %% is_connection_alive(Session), emqx_inflight:is_full(Inflight)} of {false, _} -> %% Require ack, but we do not have connection @@ -871,25 +642,26 @@ do_ack_shared(Msg, State = #state{inflight = Inflight}) -> emqx_shared_sub:maybe_ack(Msg) end. -process_subopts([], Msg, _State) -> +process_subopts([], Msg, _Session) -> {ok, Msg}; -process_subopts([{nl, 1}|_Opts], #message{from = ClientId}, #state{client_id = ClientId}) -> +process_subopts([{nl, 1}|_Opts], #message{from = ClientId}, #session{client_id = ClientId}) -> ignore; -process_subopts([{nl, _}|Opts], Msg, State) -> - process_subopts(Opts, Msg, State); -process_subopts([{qos, SubQoS}|Opts], Msg = #message{qos = PubQoS}, State = #state{zone = Zone}) -> - case get_env(Zone, upgrade_qos, false) of - true -> process_subopts(Opts, Msg#message{qos = max(SubQoS, PubQoS)}, State); - false -> process_subopts(Opts, Msg#message{qos = min(SubQoS, PubQoS)}, State) - end; -process_subopts([{rap, _Rap}|Opts], Msg = #message{flags = Flags, headers = #{retained := true}}, State = #state{}) -> - process_subopts(Opts, Msg#message{flags = maps:put(retain, true, Flags)}, State); -process_subopts([{rap, 0}|Opts], Msg = #message{flags = Flags}, State = #state{}) -> - process_subopts(Opts, Msg#message{flags = maps:put(retain, false, Flags)}, State); -process_subopts([{rap, _}|Opts], Msg, State) -> - process_subopts(Opts, Msg, State); -process_subopts([{subid, SubId}|Opts], Msg, State) -> - process_subopts(Opts, emqx_message:set_header('Subscription-Identifier', SubId, Msg), State). +process_subopts([{nl, _}|Opts], Msg, Session) -> + process_subopts(Opts, Msg, Session); +process_subopts([{qos, SubQoS}|Opts], Msg = #message{qos = PubQoS}, + Session = #session{upgrade_qos= true}) -> + process_subopts(Opts, Msg#message{qos = max(SubQoS, PubQoS)}, Session); +process_subopts([{qos, SubQoS}|Opts], Msg = #message{qos = PubQoS}, + Session = #session{upgrade_qos= false}) -> + process_subopts(Opts, Msg#message{qos = min(SubQoS, PubQoS)}, Session); +process_subopts([{rap, _Rap}|Opts], Msg = #message{flags = Flags, headers = #{retained := true}}, Session = #session{}) -> + process_subopts(Opts, Msg#message{flags = maps:put(retain, true, Flags)}, Session); +process_subopts([{rap, 0}|Opts], Msg = #message{flags = Flags}, Session = #session{}) -> + process_subopts(Opts, Msg#message{flags = maps:put(retain, false, Flags)}, Session); +process_subopts([{rap, _}|Opts], Msg, Session) -> + process_subopts(Opts, Msg, Session); +process_subopts([{subid, SubId}|Opts], Msg, Session) -> + process_subopts(Opts, emqx_message:set_header('Subscription-Identifier', SubId, Msg), Session). find_subopts(Topic, SubMap) -> case maps:find(Topic, SubMap) of @@ -900,43 +672,43 @@ find_subopts(Topic, SubMap) -> error -> [] end. -batch_process(Msgs, State) -> - {ok, Publishes, NState} = process_msgs(Msgs, [], State), - ok = batch_deliver(Publishes, NState), - maybe_gc(msg_cnt(Msgs), NState). +batch_process(Msgs, Session) -> + {ok, Publishes, NSession} = process_msgs(Msgs, [], Session), + ok = batch_deliver(Publishes, NSession), + NSession. -process_msgs([], Publishes, State) -> - {ok, lists:reverse(Publishes), State}; +process_msgs([], Publishes, Session) -> + {ok, lists:reverse(Publishes), Session}; -process_msgs([Msg|Msgs], Publishes, State) -> - case process_msg(Msg, State) of - {ok, Publish, NState} -> - process_msgs(Msgs, [Publish|Publishes], NState); - {ignore, NState} -> - process_msgs(Msgs, Publishes, NState) +process_msgs([Msg|Msgs], Publishes, Session) -> + case process_msg(Msg, Session) of + {ok, Publish, NSession} -> + process_msgs(Msgs, [Publish|Publishes], NSession); + {ignore, NSession} -> + process_msgs(Msgs, Publishes, NSession) end. %% Enqueue message if the client has been disconnected -process_msg(Msg, State = #state{conn_pid = undefined}) -> - {ignore, enqueue_msg(Msg, State)}; +%% process_msg(Msg, Session = #session{conn_pid = undefined}) -> +%% {ignore, enqueue_msg(Msg, Session)}; %% Prepare the qos0 message delivery -process_msg(Msg = #message{qos = ?QOS_0}, State) -> - {ok, {publish, undefined, Msg}, State}; +process_msg(Msg = #message{qos = ?QOS_0}, Session) -> + {ok, {publish, undefined, Msg}, Session}; process_msg(Msg = #message{qos = QoS}, - State = #state{next_pkt_id = PacketId, inflight = Inflight}) + Session = #session{next_pkt_id = PacketId, inflight = Inflight}) when QoS =:= ?QOS_1 orelse QoS =:= ?QOS_2 -> case emqx_inflight:is_full(Inflight) of true -> - {ignore, enqueue_msg(Msg, State)}; + {ignore, enqueue_msg(Msg, Session)}; false -> Publish = {publish, PacketId, Msg}, - NState = await(PacketId, Msg, State), - {ok, Publish, next_pkt_id(NState)} + NSession = await(PacketId, Msg, Session), + {ok, Publish, next_pkt_id(NSession)} end. -enqueue_msg(Msg, State = #state{mqueue = Q, client_id = ClientId, username = Username}) -> +enqueue_msg(Msg, Session = #session{mqueue = Q, client_id = ClientId, username = Username}) -> emqx_pd:update_counter(enqueue_stats, 1), {Dropped, NewQ} = emqx_mqueue:in(Msg, Q), if @@ -945,77 +717,41 @@ enqueue_msg(Msg, State = #state{mqueue = Q, client_id = ClientId, username = Use ok = emqx_hooks:run('message.dropped', [SessProps, Dropped]); true -> ok end, - State#state{mqueue = NewQ}. + Session#session{mqueue = NewQ}. %%------------------------------------------------------------------------------ %% Deliver %%------------------------------------------------------------------------------ -redeliver({PacketId, Msg = #message{qos = QoS}}, State) when QoS =/= ?QOS_0 -> +redeliver({PacketId, Msg = #message{qos = QoS}}, Session) when QoS =/= ?QOS_0 -> Msg1 = emqx_message:set_flag(dup, Msg), - do_deliver(PacketId, Msg1, State); + do_deliver(PacketId, Msg1, Session); -redeliver({pubrel, PacketId}, #state{deliver_fun = DeliverFun}) -> +redeliver({pubrel, PacketId}, #session{deliver_fun = DeliverFun}) -> DeliverFun({pubrel, PacketId}). -do_deliver(PacketId, Msg, #state{deliver_fun = DeliverFun}) -> +do_deliver(PacketId, Msg, #session{deliver_fun = DeliverFun}) -> emqx_pd:update_counter(deliver_stats, 1), DeliverFun({publish, PacketId, Msg}). -batch_deliver(Publishes, #state{deliver_fun = DeliverFun}) -> +batch_deliver(Publishes, #session{deliver_fun = DeliverFun}) -> emqx_pd:update_counter(deliver_stats, length(Publishes)), DeliverFun(Publishes). -%%------------------------------------------------------------------------------ -%% Awaiting ACK for QoS1/QoS2 Messages -%%------------------------------------------------------------------------------ - -await(PacketId, Msg, State = #state{inflight = Inflight}) -> - Inflight1 = emqx_inflight:insert( - PacketId, {publish, {PacketId, Msg}, os:timestamp()}, Inflight), - ensure_retry_timer(State#state{inflight = Inflight1}). - -acked(puback, PacketId, State = #state{client_id = ClientId, username = Username, inflight = Inflight}) -> - case emqx_inflight:lookup(PacketId, Inflight) of - {value, {publish, {_, Msg}, _Ts}} -> - ok = emqx_hooks:run('message.acked', [#{client_id => ClientId, username => Username}, Msg]), - State#state{inflight = emqx_inflight:delete(PacketId, Inflight)}; - none -> - ?LOG(warning, "[Session] Duplicated PUBACK PacketId ~w", [PacketId]), - State - end; - -acked(pubrec, PacketId, State = #state{client_id = ClientId, username = Username, inflight = Inflight}) -> - case emqx_inflight:lookup(PacketId, Inflight) of - {value, {publish, {_, Msg}, _Ts}} -> - ok = emqx_hooks:run('message.acked', [#{client_id => ClientId, username => Username}, Msg]), - State#state{inflight = emqx_inflight:update(PacketId, {pubrel, PacketId, os:timestamp()}, Inflight)}; - {value, {pubrel, PacketId, _Ts}} -> - ?LOG(warning, "[Session] Duplicated PUBREC PacketId ~w", [PacketId]), - State; - none -> - ?LOG(warning, "[Session] Unexpected PUBREC PacketId ~w", [PacketId]), - State - end; - -acked(pubcomp, PacketId, State = #state{inflight = Inflight}) -> - State#state{inflight = emqx_inflight:delete(PacketId, Inflight)}. - %%------------------------------------------------------------------------------ %% Dequeue %%------------------------------------------------------------------------------ -%% Do nothing if client is disconnected -dequeue(State = #state{conn_pid = undefined}) -> - State; - -dequeue(State = #state{inflight = Inflight, mqueue = Q}) -> +dequeue(Session = #session{inflight = Inflight, mqueue = Q}) -> case emqx_mqueue:is_empty(Q) orelse emqx_inflight:is_full(Inflight) of - true -> State; + true -> Session; false -> - {Msgs, Q1} = drain_q(batch_n(Inflight), [], Q), - batch_process(lists:reverse(Msgs), State#state{mqueue = Q1}) + %% TODO: + Msgs = [], + Q1 = Q, + %% {Msgs, Q1} = drain_q(batch_n(Inflight), [], Q), + batch_process(lists:reverse(Msgs), Session#session{mqueue = Q1}) end. drain_q(Cnt, Msgs, Q) when Cnt =< 0 -> @@ -1031,107 +767,45 @@ drain_q(Cnt, Msgs, Q) -> %%------------------------------------------------------------------------------ %% Ensure timers -ensure_await_rel_timer(State = #state{zone = Zone, - await_rel_timer = undefined}) -> - Timeout = get_env(Zone, await_rel_timeout), - ensure_await_rel_timer(Timeout, State); -ensure_await_rel_timer(State) -> - State. +ensure_await_rel_timer(Session = #session{await_rel_timeout = Timeout, + await_rel_timer = undefined}) -> + ensure_await_rel_timer(Timeout, Session); +ensure_await_rel_timer(Session) -> + Session. -ensure_await_rel_timer(Timeout, State = #state{await_rel_timer = undefined}) -> - State#state{await_rel_timer = emqx_misc:start_timer(Timeout, check_awaiting_rel)}; -ensure_await_rel_timer(_Timeout, State) -> - State. +ensure_await_rel_timer(Timeout, Session = #session{await_rel_timer = undefined}) -> + Session#session{await_rel_timer = emqx_misc:start_timer(Timeout, check_awaiting_rel)}; +ensure_await_rel_timer(_Timeout, Session) -> + Session. -ensure_retry_timer(State = #state{zone = Zone, retry_timer = undefined}) -> - Interval = get_env(Zone, retry_interval, 0), - ensure_retry_timer(Interval, State); -ensure_retry_timer(State) -> - State. +ensure_retry_timer(Session = #session{retry_interval = Interval, retry_timer = undefined}) -> + ensure_retry_timer(Interval, Session); +ensure_retry_timer(Session) -> + Session. -ensure_retry_timer(Interval, State = #state{retry_timer = undefined}) -> - State#state{retry_timer = emqx_misc:start_timer(Interval, retry_delivery)}; -ensure_retry_timer(_Timeout, State) -> - State. +ensure_retry_timer(Interval, Session = #session{retry_timer = undefined}) -> + Session#session{retry_timer = emqx_misc:start_timer(Interval, retry_delivery)}; +ensure_retry_timer(_Timeout, Session) -> + Session. -ensure_expire_timer(State = #state{expiry_interval = Interval}) +ensure_expire_timer(Session = #session{expiry_interval = Interval}) when Interval > 0 andalso Interval =/= 16#ffffffff -> - State#state{expiry_timer = emqx_misc:start_timer(Interval * 1000, expired)}; -ensure_expire_timer(State) -> - State. + Session#session{expiry_timer = emqx_misc:start_timer(Interval * 1000, expired)}; +ensure_expire_timer(Session) -> + Session. -ensure_will_delay_timer(State = #state{will_msg = #message{headers = #{'Will-Delay-Interval' := WillDelayInterval}}}) -> - State#state{will_delay_timer = emqx_misc:start_timer(WillDelayInterval * 1000, will_delay)}; -ensure_will_delay_timer(State = #state{will_msg = WillMsg}) -> +ensure_will_delay_timer(Session = #session{will_msg = #message{headers = #{'Will-Delay-Interval' := WillDelayInterval}}}) -> + Session#session{will_delay_timer = emqx_misc:start_timer(WillDelayInterval * 1000, will_delay)}; +ensure_will_delay_timer(Session = #session{will_msg = WillMsg}) -> send_willmsg(WillMsg), - State#state{will_msg = undefined}. + Session#session{will_msg = undefined}. -ensure_stats_timer(State = #state{zone = Zone, - stats_timer = undefined, - idle_timeout = IdleTimeout}) -> - case get_env(Zone, enable_stats, true) of - true -> State#state{stats_timer = emqx_misc:start_timer(IdleTimeout, emit_stats)}; - _Other -> State - end; -ensure_stats_timer(State) -> - State. - -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Next Packet Id -next_pkt_id(State = #state{next_pkt_id = 16#FFFF}) -> - State#state{next_pkt_id = 1}; +next_pkt_id(Session = #session{next_pkt_id = 16#FFFF}) -> + Session#session{next_pkt_id = 1}; -next_pkt_id(State = #state{next_pkt_id = Id}) -> - State#state{next_pkt_id = Id + 1}. +next_pkt_id(Session = #session{next_pkt_id = Id}) -> + Session#session{next_pkt_id = Id + 1}. -%%------------------------------------------------------------------------------ -%% Maybe GC - -msg_cnt(Msgs) -> - lists:foldl(fun(Msg, {Cnt, Oct}) -> - {Cnt+1, Oct+msg_size(Msg)} - end, {0, 0}, Msgs). - -%% Take only the payload size into account, add other fields if necessary -msg_size(#message{payload = Payload}) -> payload_size(Payload). - -%% Payload should be binary(), but not 100% sure. Need dialyzer! -payload_size(Payload) -> erlang:iolist_size(Payload). - -maybe_gc(_, State = #state{gc_state = undefined}) -> - State; -maybe_gc({Cnt, Oct}, State = #state{gc_state = GCSt}) -> - {_, GCSt1} = emqx_gc:run(Cnt, Oct, GCSt), - State#state{gc_state = GCSt1}. - -%%------------------------------------------------------------------------------ -%% Helper functions - -reply({Reply, State}) -> - reply(Reply, State). - -reply(Reply, State) -> - {reply, Reply, State}. - -noreply(State) -> - {noreply, State}. - -shutdown(Reason, State) -> - {stop, {shutdown, Reason}, State}. - -do_subscribe(ClientId, Username, Topic, SubOpts, SubMap) -> - case maps:find(Topic, SubMap) of - {ok, SubOpts} -> - ok = emqx_hooks:run('session.subscribed', [#{client_id => ClientId, username => Username}, Topic, SubOpts#{first => false}]), - SubMap; - {ok, _SubOpts} -> - emqx_broker:set_subopts(Topic, SubOpts), - %% Why??? - ok = emqx_hooks:run('session.subscribed', [#{client_id => ClientId, username => Username}, Topic, SubOpts#{first => false}]), - maps:put(Topic, SubOpts, SubMap); - error -> - emqx_broker:subscribe(Topic, ClientId, SubOpts), - ok = emqx_hooks:run('session.subscribed', [#{client_id => ClientId, username => Username}, Topic, SubOpts#{first => true}]), - maps:put(Topic, SubOpts, SubMap) - end. diff --git a/src/emqx_shared_sub.erl b/src/emqx_shared_sub.erl index 48eba293a..8b786dacd 100644 --- a/src/emqx_shared_sub.erl +++ b/src/emqx_shared_sub.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_shared_sub). @@ -65,11 +67,12 @@ -define(no_ack, no_ack). -record(state, {pmon}). + -record(emqx_shared_subscription, {group, topic, subpid}). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Mnesia bootstrap -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- mnesia(boot) -> ok = ekka_mnesia:create_table(?TAB, [ @@ -81,9 +84,9 @@ mnesia(boot) -> mnesia(copy) -> ok = ekka_mnesia:copy_table(?TAB). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% API -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- -spec(start_link() -> startlink_ret()). start_link() -> @@ -273,12 +276,12 @@ do_pick_subscriber(Group, Topic, round_robin, _ClientId, Count) -> subscribers(Group, Topic) -> ets:select(?TAB, [{{emqx_shared_subscription, Group, Topic, '$1'}, [], ['$1']}]). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% gen_server callbacks -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- init([]) -> - mnesia:subscribe({table, ?TAB, simple}), + {ok, _} = mnesia:subscribe({table, ?TAB, simple}), {atomic, PMon} = mnesia:transaction(fun init_monitors/0), ok = emqx_tables:new(?SHARED_SUBS, [protected, bag]), ok = emqx_tables:new(?ALIVE_SUBS, [protected, set, {read_concurrency, true}]), @@ -343,9 +346,9 @@ terminate(_Reason, _State) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Internal functions -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% keep track of alive remote pids maybe_insert_alive_tab(Pid) when ?IS_LOCAL_PID(Pid) -> ok; diff --git a/src/emqx_sm.erl b/src/emqx_sm.erl deleted file mode 100644 index a122a05fe..000000000 --- a/src/emqx_sm.erl +++ /dev/null @@ -1,295 +0,0 @@ -%% Copyright (c) 2013-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_sm). - --behaviour(gen_server). - --include("emqx.hrl"). --include("logger.hrl"). --include("types.hrl"). - -%% APIs --export([start_link/0]). - --export([ open_session/1 - , close_session/1 - , resume_session/2 - , discard_session/1 - , discard_session/2 - , register_session/1 - , register_session/2 - , unregister_session/1 - , unregister_session/2 - ]). - --export([ get_session_attrs/1 - , get_session_attrs/2 - , set_session_attrs/2 - , set_session_attrs/3 - , get_session_stats/1 - , get_session_stats/2 - , set_session_stats/2 - , set_session_stats/3 - ]). - --export([lookup_session_pids/1]). - -%% Internal functions for rpc --export([dispatch/3]). - -%% Internal function for stats --export([stats_fun/0]). - -%% Internal function for emqx_session_sup --export([clean_down/1]). - -%% gen_server callbacks --export([ init/1 - , handle_call/3 - , handle_cast/2 - , handle_info/2 - , terminate/2 - , code_change/3 - ]). - --define(SM, ?MODULE). - -%% ETS Tables for session management. --define(SESSION_TAB, emqx_session). --define(SESSION_P_TAB, emqx_session_p). --define(SESSION_ATTRS_TAB, emqx_session_attrs). --define(SESSION_STATS_TAB, emqx_session_stats). - --define(BATCH_SIZE, 100000). - -%%------------------------------------------------------------------------------ -%% APIs -%%------------------------------------------------------------------------------ - --spec(start_link() -> startlink_ret()). -start_link() -> - gen_server:start_link({local, ?SM}, ?MODULE, [], []). - -%% @doc Open a session. --spec(open_session(map()) -> {ok, pid()} | {ok, pid(), boolean()} | {error, term()}). -open_session(SessAttrs = #{clean_start := true, client_id := ClientId, conn_pid := ConnPid}) -> - CleanStart = fun(_) -> - ok = discard_session(ClientId, ConnPid), - emqx_session_sup:start_session(SessAttrs) - end, - emqx_sm_locker:trans(ClientId, CleanStart); - -open_session(SessAttrs = #{clean_start := false, client_id := ClientId}) -> - ResumeStart = fun(_) -> - case resume_session(ClientId, SessAttrs) of - {ok, SessPid} -> - {ok, SessPid, true}; - {error, not_found} -> - emqx_session_sup:start_session(SessAttrs) - end - end, - emqx_sm_locker:trans(ClientId, ResumeStart). - -%% @doc Discard all the sessions identified by the ClientId. --spec(discard_session(emqx_types:client_id()) -> ok). -discard_session(ClientId) when is_binary(ClientId) -> - discard_session(ClientId, self()). - --spec(discard_session(emqx_types:client_id(), pid()) -> ok). -discard_session(ClientId, ConnPid) when is_binary(ClientId) -> - lists:foreach( - fun(SessPid) -> - try emqx_session:discard(SessPid, ConnPid) - catch - _:Error:_Stk -> - ?LOG(warning, "[SM] Failed to discard ~p: ~p", [SessPid, Error]) - end - end, lookup_session_pids(ClientId)). - -%% @doc Try to resume a session. --spec(resume_session(emqx_types:client_id(), map()) -> {ok, pid()} | {error, term()}). -resume_session(ClientId, SessAttrs = #{conn_pid := ConnPid}) -> - case lookup_session_pids(ClientId) of - [] -> {error, not_found}; - [SessPid] -> - ok = emqx_session:resume(SessPid, SessAttrs), - {ok, SessPid}; - SessPids -> - [SessPid|StalePids] = lists:reverse(SessPids), - ?LOG(error, "[SM] More than one session found: ~p", [SessPids]), - lists:foreach(fun(StalePid) -> - catch emqx_session:discard(StalePid, ConnPid) - end, StalePids), - ok = emqx_session:resume(SessPid, SessAttrs), - {ok, SessPid} - end. - -%% @doc Close a session. --spec(close_session(emqx_types:client_id() | pid()) -> ok). -close_session(ClientId) when is_binary(ClientId) -> - case lookup_session_pids(ClientId) of - [] -> ok; - [SessPid] -> close_session(SessPid); - SessPids -> lists:foreach(fun close_session/1, SessPids) - end; - -close_session(SessPid) when is_pid(SessPid) -> - emqx_session:close(SessPid). - -%% @doc Register a session. --spec(register_session(emqx_types:client_id()) -> ok). -register_session(ClientId) when is_binary(ClientId) -> - register_session(ClientId, self()). - --spec(register_session(emqx_types:client_id(), pid()) -> ok). -register_session(ClientId, SessPid) when is_binary(ClientId), is_pid(SessPid) -> - Session = {ClientId, SessPid}, - true = ets:insert(?SESSION_TAB, Session), - emqx_sm_registry:register_session(Session). - -%% @doc Unregister a session --spec(unregister_session(emqx_types:client_id()) -> ok). -unregister_session(ClientId) when is_binary(ClientId) -> - unregister_session(ClientId, self()). - --spec(unregister_session(emqx_types:client_id(), pid()) -> ok). -unregister_session(ClientId, SessPid) when is_binary(ClientId), is_pid(SessPid) -> - Session = {ClientId, SessPid}, - true = ets:delete(?SESSION_STATS_TAB, Session), - true = ets:delete(?SESSION_ATTRS_TAB, Session), - true = ets:delete_object(?SESSION_P_TAB, Session), - true = ets:delete_object(?SESSION_TAB, Session), - emqx_sm_registry:unregister_session(Session). - -%% @doc Get session attrs --spec(get_session_attrs(emqx_types:client_id()) -> list(emqx_session:attr())). -get_session_attrs(ClientId) when is_binary(ClientId) -> - case lookup_session_pids(ClientId) of - [] -> []; - [SessPid|_] -> get_session_attrs(ClientId, SessPid) - end. - --spec(get_session_attrs(emqx_types:client_id(), pid()) -> list(emqx_session:attr())). -get_session_attrs(ClientId, SessPid) when is_binary(ClientId), is_pid(SessPid) -> - emqx_tables:lookup_value(?SESSION_ATTRS_TAB, {ClientId, SessPid}, []). - -%% @doc Set session attrs --spec(set_session_attrs(emqx_types:client_id(), list(emqx_session:attr())) -> true). -set_session_attrs(ClientId, SessAttrs) when is_binary(ClientId) -> - set_session_attrs(ClientId, self(), SessAttrs). - --spec(set_session_attrs(emqx_types:client_id(), pid(), list(emqx_session:attr())) -> true). -set_session_attrs(ClientId, SessPid, SessAttrs) when is_binary(ClientId), is_pid(SessPid) -> - Session = {ClientId, SessPid}, - true = ets:insert(?SESSION_ATTRS_TAB, {Session, SessAttrs}), - proplists:get_value(clean_start, SessAttrs, true) orelse ets:insert(?SESSION_P_TAB, Session). - -%% @doc Get session stats --spec(get_session_stats(emqx_types:client_id()) -> list(emqx_stats:stats())). -get_session_stats(ClientId) when is_binary(ClientId) -> - case lookup_session_pids(ClientId) of - [] -> []; - [SessPid|_] -> - get_session_stats(ClientId, SessPid) - end. - --spec(get_session_stats(emqx_types:client_id(), pid()) -> list(emqx_stats:stats())). -get_session_stats(ClientId, SessPid) when is_binary(ClientId) -> - emqx_tables:lookup_value(?SESSION_STATS_TAB, {ClientId, SessPid}, []). - -%% @doc Set session stats --spec(set_session_stats(emqx_types:client_id(), emqx_stats:stats()) -> true). -set_session_stats(ClientId, Stats) when is_binary(ClientId) -> - set_session_stats(ClientId, self(), Stats). - --spec(set_session_stats(emqx_types:client_id(), pid(), emqx_stats:stats()) -> true). -set_session_stats(ClientId, SessPid, Stats) when is_binary(ClientId), is_pid(SessPid) -> - ets:insert(?SESSION_STATS_TAB, {{ClientId, SessPid}, Stats}). - -%% @doc Lookup session pid. --spec(lookup_session_pids(emqx_types:client_id()) -> list(pid())). -lookup_session_pids(ClientId) -> - case emqx_sm_registry:is_enabled() of - true -> emqx_sm_registry:lookup_session(ClientId); - false -> - case emqx_tables:lookup_value(?SESSION_TAB, ClientId) of - undefined -> []; - SessPid when is_pid(SessPid) -> [SessPid] - end - end. - -%% @doc Dispatch a message to the session. --spec(dispatch(emqx_types:client_id(), emqx_topic:topic(), emqx_types:message()) -> any()). -dispatch(ClientId, Topic, Msg) -> - case lookup_session_pids(ClientId) of - [SessPid|_] when is_pid(SessPid) -> - SessPid ! {dispatch, Topic, Msg}; - [] -> - emqx_hooks:run('message.dropped', [#{client_id => ClientId}, Msg]) - end. - -%%------------------------------------------------------------------------------ -%% gen_server callbacks -%%------------------------------------------------------------------------------ - -init([]) -> - TabOpts = [public, set, {write_concurrency, true}], - ok = emqx_tables:new(?SESSION_TAB, [{read_concurrency, true} | TabOpts]), - ok = emqx_tables:new(?SESSION_P_TAB, TabOpts), - ok = emqx_tables:new(?SESSION_ATTRS_TAB, TabOpts), - ok = emqx_tables:new(?SESSION_STATS_TAB, TabOpts), - ok = emqx_stats:update_interval(sess_stats, fun ?MODULE:stats_fun/0), - {ok, #{}}. - -handle_call(Req, _From, State) -> - ?LOG(error, "[SM] Unexpected call: ~p", [Req]), - {reply, ignored, State}. - -handle_cast(Msg, State) -> - ?LOG(error, "[SM] Unexpected cast: ~p", [Msg]), - {noreply, State}. - -handle_info(Info, State) -> - ?LOG(error, "[SM] Unexpected info: ~p", [Info]), - {noreply, State}. - -terminate(_Reason, _State) -> - emqx_stats:cancel_update(sess_stats). - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -%%------------------------------------------------------------------------------ -%% Internal functions -%%------------------------------------------------------------------------------ - -clean_down(Session = {ClientId, SessPid}) -> - case ets:member(?SESSION_TAB, ClientId) - orelse ets:member(?SESSION_ATTRS_TAB, Session) of - true -> - unregister_session(ClientId, SessPid); - false -> ok - end. - -stats_fun() -> - safe_update_stats(?SESSION_TAB, 'sessions.count', 'sessions.max'), - safe_update_stats(?SESSION_P_TAB, 'sessions.persistent.count', 'sessions.persistent.max'). - -safe_update_stats(Tab, Stat, MaxStat) -> - case ets:info(Tab, size) of - undefined -> ok; - Size -> emqx_stats:setstat(Stat, MaxStat, Size) - end. - diff --git a/src/emqx_sm_locker.erl b/src/emqx_sm_locker.erl deleted file mode 100644 index 854b95653..000000000 --- a/src/emqx_sm_locker.erl +++ /dev/null @@ -1,68 +0,0 @@ -%% Copyright (c) 2013-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_sm_locker). - --include("emqx.hrl"). --include("types.hrl"). - --export([start_link/0]). - --export([ trans/2 - , trans/3 - , lock/1 - , lock/2 - , unlock/1 - ]). - -%%------------------------------------------------------------------------------ -%% APIs -%%------------------------------------------------------------------------------ - --spec(start_link() -> startlink_ret()). -start_link() -> - ekka_locker:start_link(?MODULE). - --spec(trans(emqx_types:client_id(), fun(([node()]) -> any())) -> any()). -trans(ClientId, Fun) -> - trans(ClientId, Fun, undefined). - --spec(trans(maybe(emqx_types:client_id()), - fun(([node()])-> any()), ekka_locker:piggyback()) -> any()). -trans(undefined, Fun, _Piggyback) -> - Fun([]); -trans(ClientId, Fun, Piggyback) -> - case lock(ClientId, Piggyback) of - {true, Nodes} -> - try Fun(Nodes) after unlock(ClientId) end; - {false, _Nodes} -> - {error, client_id_unavailable} - end. - --spec(lock(emqx_types:client_id()) -> ekka_locker:lock_result()). -lock(ClientId) -> - ekka_locker:acquire(?MODULE, ClientId, strategy()). - --spec(lock(emqx_types:client_id(), ekka_locker:piggyback()) -> ekka_locker:lock_result()). -lock(ClientId, Piggyback) -> - ekka_locker:acquire(?MODULE, ClientId, strategy(), Piggyback). - --spec(unlock(emqx_types:client_id()) -> {boolean(), [node()]}). -unlock(ClientId) -> - ekka_locker:release(?MODULE, ClientId, strategy()). - --spec(strategy() -> local | one | quorum | all). -strategy() -> - emqx_config:get_env(session_locking_strategy, quorum). - diff --git a/src/emqx_stats.erl b/src/emqx_stats.erl index c75bd742d..17b42f667 100644 --- a/src/emqx_stats.erl +++ b/src/emqx_stats.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_stats). @@ -49,7 +51,11 @@ -record(update, {name, countdown, interval, func}). --record(state, {timer, updates :: [#update{}], tick_ms :: timeout()}). +-record(state, { + timer :: reference(), + updates :: [#update{}], + tick_ms :: timeout() + }). -type(stats() :: list({atom(), non_neg_integer()})). @@ -166,9 +172,9 @@ rec(Name, Secs, UpFun) -> cast(Msg) -> gen_server:cast(?SERVER, Msg). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% gen_server callbacks -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- init(#{tick_ms := TickMs}) -> ok = emqx_tables:new(?TAB, [public, set, {write_concurrency, true}]), @@ -199,7 +205,8 @@ handle_cast({setstat, Stat, MaxStat, Val}, State) -> safe_update_element(Stat, Val), {noreply, State}; -handle_cast({update_interval, Update = #update{name = Name}}, State = #state{updates = Updates}) -> +handle_cast({update_interval, Update = #update{name = Name}}, + State = #state{updates = Updates}) -> case lists:keyfind(Name, #update.name, Updates) of #update{} -> ?LOG(warning, "[Stats] Duplicated update: ~s", [Name]), @@ -240,9 +247,9 @@ terminate(_Reason, #state{timer = TRef}) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Internal functions -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- safe_update_element(Key, Val) -> try ets:update_element(?TAB, Key, {2, Val}) of @@ -254,3 +261,4 @@ safe_update_element(Key, Val) -> error:badarg -> ?LOG(warning, "[Stats] Update ~p to ~p failed", [Key, Val]) end. + diff --git a/src/emqx_sup.erl b/src/emqx_sup.erl index dc30f4af1..15877502f 100644 --- a/src/emqx_sup.erl +++ b/src/emqx_sup.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,11 +12,14 @@ %% 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_sup). -behaviour(supervisor). +-include("types.hrl"). + -export([ start_link/0 , start_child/1 , start_child/2 @@ -28,29 +32,28 @@ | {ok, supervisor:child(), term()} | {error, term()}). --define(SUPERVISOR, ?MODULE). +-define(SUP, ?MODULE). %%-------------------------------------------------------------------- %% API %%-------------------------------------------------------------------- +-spec(start_link() -> startlink_ret()). start_link() -> - supervisor:start_link({local, ?SUPERVISOR}, ?MODULE, []). + supervisor:start_link({local, ?SUP}, ?MODULE, []). -spec(start_child(supervisor:child_spec()) -> startchild_ret()). start_child(ChildSpec) when is_tuple(ChildSpec) -> - supervisor:start_child(?SUPERVISOR, ChildSpec). + supervisor:start_child(?SUP, ChildSpec). -spec(start_child(module(), worker | supervisor) -> startchild_ret()). -start_child(Mod, worker) -> - start_child(worker_spec(Mod)); -start_child(Mod, supervisor) -> - start_child(supervisor_spec(Mod)). +start_child(Mod, Type) -> + start_child(child_spec(Mod, Type)). -spec(stop_child(supervisor:child_id()) -> ok | {error, term()}). stop_child(ChildId) -> - case supervisor:terminate_child(?SUPERVISOR, ChildId) of - ok -> supervisor:delete_child(?SUPERVISOR, ChildId); + case supervisor:terminate_child(?SUP, ChildId) of + ok -> supervisor:delete_child(?SUP, ChildId); Error -> Error end. @@ -60,32 +63,39 @@ stop_child(ChildId) -> init([]) -> %% Kernel Sup - KernelSup = supervisor_spec(emqx_kernel_sup), + KernelSup = child_spec(emqx_kernel_sup, supervisor), %% Router Sup - RouterSup = supervisor_spec(emqx_router_sup), + RouterSup = child_spec(emqx_router_sup, supervisor), %% Broker Sup - BrokerSup = supervisor_spec(emqx_broker_sup), - BridgeSup = supervisor_spec(emqx_bridge_sup), - %% Session Manager - SMSup = supervisor_spec(emqx_sm_sup), - %% Connection Manager - CMSup = supervisor_spec(emqx_cm_sup), + BrokerSup = child_spec(emqx_broker_sup, supervisor), + %% Bridge Sup + BridgeSup = child_spec(emqx_bridge_sup, supervisor), + %% CM Sup + CMSup = child_spec(emqx_cm_sup, supervisor), %% Sys Sup - SysSup = supervisor_spec(emqx_sys_sup), + SysSup = child_spec(emqx_sys_sup, supervisor), {ok, {{one_for_all, 0, 1}, - [KernelSup, - RouterSup, - BrokerSup, - BridgeSup, - SMSup, - CMSup, - SysSup]}}. + [KernelSup, RouterSup, BrokerSup, BridgeSup, CMSup, SysSup]}}. %%-------------------------------------------------------------------- %% Internal functions %%-------------------------------------------------------------------- -worker_spec(M) -> - {M, {M, start_link, []}, permanent, 30000, worker, [M]}. -supervisor_spec(M) -> - {M, {M, start_link, []}, permanent, infinity, supervisor, [M]}. +child_spec(Mod, supervisor) -> + #{id => Mod, + start => {Mod, start_link, []}, + restart => permanent, + shutdown => infinity, + type => supervisor, + modules => [Mod] + }; + +child_spec(Mod, worker) -> + #{id => Mod, + start => {Mod, start_link, []}, + restart => permanent, + shutdown => 15000, + type => worker, + modules => [Mod] + }. + diff --git a/src/emqx_sys.erl b/src/emqx_sys.erl index 90b24e122..dbab44e59 100644 --- a/src/emqx_sys.erl +++ b/src/emqx_sys.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_sys). diff --git a/src/emqx_sys_mon.erl b/src/emqx_sys_mon.erl index b75503b53..36ea7cfb3 100644 --- a/src/emqx_sys_mon.erl +++ b/src/emqx_sys_mon.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_sys_mon). @@ -41,18 +43,14 @@ -define(SYSMON, ?MODULE). -%%------------------------------------------------------------------------------ -%% APIs -%%------------------------------------------------------------------------------ - -%% @doc Start system monitor +%% @doc Start the system monitor. -spec(start_link(list(option())) -> startlink_ret()). start_link(Opts) -> gen_server:start_link({local, ?SYSMON}, ?MODULE, [Opts], []). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% gen_server callbacks -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- init([Opts]) -> erlang:system_monitor(self(), parse_opt(Opts)), diff --git a/src/emqx_sys_sup.erl b/src/emqx_sys_sup.erl index 9341b8528..cbc07650e 100644 --- a/src/emqx_sys_sup.erl +++ b/src/emqx_sys_sup.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_sys_sup). @@ -24,23 +26,28 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). init([]) -> - {ok, {{one_for_one, 10, 100}, [child_spec(emqx_sys, worker), - child_spec(emqx_sys_mon, worker, [emqx_config:get_env(sysmon, [])]), - child_spec(emqx_os_mon, worker, [emqx_config:get_env(os_mon, [])]), - child_spec(emqx_vm_mon, worker, [emqx_config:get_env(vm_mon, [])])]}}. + Childs = [child_spec(emqx_sys), + child_spec(emqx_sys_mon, [config(sysmon)]), + child_spec(emqx_os_mon, [config(os_mon)]), + child_spec(emqx_vm_mon, [config(vm_mon)])], + {ok, {{one_for_one, 10, 100}, Childs}}. %%-------------------------------------------------------------------- %% Internal functions %%-------------------------------------------------------------------- -child_spec(M, worker) -> - child_spec(M, worker, []). +child_spec(Mod) -> + child_spec(Mod, []). -child_spec(M, worker, A) -> - #{id => M, - start => {M, start_link, A}, - restart => permanent, +child_spec(Mod, Args) -> + #{id => Mod, + start => {Mod, start_link, Args}, + restart => permanent, shutdown => 5000, - type => worker, - modules => [M]}. + type => worker, + modules => [Mod] + }. + +config(Name) -> + emqx_config:get_env(Name, []). diff --git a/src/emqx_tables.erl b/src/emqx_tables.erl index 16812036a..106af13c2 100644 --- a/src/emqx_tables.erl +++ b/src/emqx_tables.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_tables). @@ -52,3 +54,4 @@ lookup_value(Tab, Key, Def) -> catch error:badarg -> Def end. + diff --git a/src/emqx_time.erl b/src/emqx_time.erl index e0ef8e5fb..e1c5e8527 100644 --- a/src/emqx_time.erl +++ b/src/emqx_time.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_time). @@ -39,3 +41,4 @@ now_ms({MegaSecs, Secs, MicroSecs}) -> ts_from_ms(Ms) -> {Ms div 1000000, Ms rem 1000000, 0}. + diff --git a/src/emqx_topic.erl b/src/emqx_topic.erl index f1d15d8e2..f3fccd70b 100644 --- a/src/emqx_topic.erl +++ b/src/emqx_topic.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_topic). @@ -43,9 +45,9 @@ -define(MAX_TOPIC_LEN, 4096). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% APIs -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% @doc Is wildcard topic? -spec(wildcard(topic() | words()) -> true | false). diff --git a/src/emqx_tracer.erl b/src/emqx_tracer.erl index c85aa5007..3a4d72afc 100644 --- a/src/emqx_tracer.erl +++ b/src/emqx_tracer.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_tracer). diff --git a/src/emqx_trie.erl b/src/emqx_trie.erl index 1fb5e0ead..d437fec9d 100644 --- a/src/emqx_trie.erl +++ b/src/emqx_trie.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_trie). @@ -35,9 +37,9 @@ -define(TRIE, emqx_trie). -define(TRIE_NODE, emqx_trie_node). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Mnesia bootstrap -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% @doc Create or replicate trie tables. -spec(mnesia(boot | copy) -> ok). @@ -64,9 +66,9 @@ mnesia(copy) -> %% Copy trie_node table ok = ekka_mnesia:copy_table(?TRIE_NODE). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Trie APIs -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% @doc Insert a topic filter into the trie. -spec(insert(emqx_topic:topic()) -> ok). @@ -111,9 +113,9 @@ delete(Topic) when is_binary(Topic) -> empty() -> ets:info(?TRIE, size) == 0. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Internal functions -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% @private %% @doc Add a path to the trie. diff --git a/src/emqx_types.erl b/src/emqx_types.erl index 50e7f78c0..57dfe7e8c 100644 --- a/src/emqx_types.erl +++ b/src/emqx_types.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_types). @@ -32,9 +34,7 @@ , protocol/0 ]). --export_type([ credentials/0 - , session/0 - ]). +-export_type([credentials/0]). -export_type([ subscription/0 , subscriber/0 @@ -65,7 +65,6 @@ share => binary(), atom() => term() }). --type(session() :: #session{}). -type(client_id() :: binary() | atom()). -type(username() :: maybe(binary())). -type(password() :: maybe(binary())). @@ -105,3 +104,4 @@ -type(alarm() :: #alarm{}). -type(plugin() :: #plugin{}). -type(command() :: #command{}). + diff --git a/src/emqx_vm.erl b/src/emqx_vm.erl index fcc9b3067..0a2489264 100644 --- a/src/emqx_vm.erl +++ b/src/emqx_vm.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_vm). diff --git a/src/emqx_vm_mon.erl b/src/emqx_vm_mon.erl index 10be9d71b..e42ceb2ee 100644 --- a/src/emqx_vm_mon.erl +++ b/src/emqx_vm_mon.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,11 +12,14 @@ %% 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_vm_mon). -behaviour(gen_server). +-include("logger.hrl"). + %% APIs -export([start_link/1]). @@ -38,13 +42,13 @@ -define(VM_MON, ?MODULE). -%%---------------------------------------------------------------------- -%% API -%%---------------------------------------------------------------------- - start_link(Opts) -> gen_server:start_link({local, ?VM_MON}, ?MODULE, [Opts], []). +%%-------------------------------------------------------------------- +%% API +%%-------------------------------------------------------------------- + get_check_interval() -> call(get_check_interval). @@ -63,9 +67,12 @@ get_process_low_watermark() -> set_process_low_watermark(Float) -> call({set_process_low_watermark, Float}). -%%---------------------------------------------------------------------- +call(Req) -> + gen_server:call(?VM_MON, Req, infinity). + +%%-------------------------------------------------------------------- %% gen_server callbacks -%%---------------------------------------------------------------------- +%%-------------------------------------------------------------------- init([Opts]) -> {ok, ensure_check_timer(#{check_interval => proplists:get_value(check_interval, Opts, 30), @@ -76,43 +83,53 @@ init([Opts]) -> handle_call(get_check_interval, _From, State) -> {reply, maps:get(check_interval, State, undefined), State}; + handle_call({set_check_interval, Seconds}, _From, State) -> {reply, ok, State#{check_interval := Seconds}}; handle_call(get_process_high_watermark, _From, State) -> {reply, maps:get(process_high_watermark, State, undefined), State}; + handle_call({set_process_high_watermark, Float}, _From, State) -> {reply, ok, State#{process_high_watermark := Float}}; handle_call(get_process_low_watermark, _From, State) -> {reply, maps:get(process_low_watermark, State, undefined), State}; + handle_call({set_process_low_watermark, Float}, _From, State) -> {reply, ok, State#{process_low_watermark := Float}}; -handle_call(_Request, _From, State) -> - {reply, ok, State}. +handle_call(Req, _From, State) -> + ?LOG(error, "[VM_MON] Unexpected call: ~p", [Req]), + {reply, ignored, State}. -handle_cast(_Request, State) -> +handle_cast(Msg, State) -> + ?LOG(error, "[VM_MON] Unexpected cast: ~p", [Msg]), {noreply, State}. -handle_info({timeout, Timer, check}, State = #{timer := Timer, - process_high_watermark := ProcHighWatermark, - process_low_watermark := ProcLowWatermark, - is_process_alarm_set := IsProcessAlarmSet}) -> +handle_info({timeout, Timer, check}, + State = #{timer := Timer, + process_high_watermark := ProcHighWatermark, + process_low_watermark := ProcLowWatermark, + is_process_alarm_set := IsProcessAlarmSet}) -> ProcessCount = erlang:system_info(process_count), - case ProcessCount / erlang:system_info(process_limit) of - Percent when Percent >= ProcHighWatermark -> - alarm_handler:set_alarm({too_many_processes, ProcessCount}), - {noreply, ensure_check_timer(State#{is_process_alarm_set := true})}; - Percent when Percent < ProcLowWatermark -> - case IsProcessAlarmSet of - true -> alarm_handler:clear_alarm(too_many_processes); - false -> ok - end, - {noreply, ensure_check_timer(State#{is_process_alarm_set := false})}; - _Precent -> - {noreply, ensure_check_timer(State)} - end. + NState = case ProcessCount / erlang:system_info(process_limit) of + Percent when Percent >= ProcHighWatermark -> + alarm_handler:set_alarm({too_many_processes, ProcessCount}), + State#{is_process_alarm_set := true}; + Percent when Percent < ProcLowWatermark -> + case IsProcessAlarmSet of + true -> alarm_handler:clear_alarm(too_many_processes); + false -> ok + end, + State#{is_process_alarm_set := false}; + _Precent -> State + end, + {noreply, ensure_check_timer(NState)}; + +handle_info(Info, State) -> + ?LOG(error, "[VM_MON] Unexpected info: ~p", [Info]), + {noreply, State}. terminate(_Reason, #{timer := Timer}) -> emqx_misc:cancel_timer(Timer). @@ -120,11 +137,10 @@ terminate(_Reason, #{timer := Timer}) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -%%---------------------------------------------------------------------- +%%-------------------------------------------------------------------- %% Internal functions -%%---------------------------------------------------------------------- -call(Req) -> - gen_server:call(?VM_MON, Req, infinity). +%%-------------------------------------------------------------------- ensure_check_timer(State = #{check_interval := Interval}) -> State#{timer := emqx_misc:start_timer(timer:seconds(Interval), check)}. + diff --git a/src/emqx_zone.erl b/src/emqx_zone.erl index 65deeea24..52aa91dc1 100644 --- a/src/emqx_zone.erl +++ b/src/emqx_zone.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_zone). @@ -48,9 +50,9 @@ -define(SERVER, ?MODULE). -define(KEY(Zone, Key), {?MODULE, Zone, Key}). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% APIs -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- -spec(start_link() -> startlink_ret()). start_link() -> @@ -83,9 +85,9 @@ force_reload() -> stop() -> gen_server:stop(?SERVER, normal, infinity). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% gen_server callbacks -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- init([]) -> _ = do_reload(), @@ -117,9 +119,9 @@ terminate(_Reason, _State) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Internal functions -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- do_reload() -> [ persistent_term:put(?KEY(Zone, Key), Val) From 7774b85f813d8d82ac974d49abb2779b0dd669af Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 18 Jun 2019 15:03:51 +0800 Subject: [PATCH 05/33] Implement the channel architecture --- include/emqx.hrl | 20 +- src/emqx_banned.erl | 46 ++-- src/emqx_channel.erl | 15 +- src/emqx_cm.erl | 418 +++++++++++++++++++++++++--------- src/emqx_cm_locker.erl | 66 ++++++ src/emqx_cm_registry.erl | 95 ++++---- src/emqx_cm_sup.erl | 52 +++-- src/emqx_flapping.erl | 41 ++-- src/emqx_hooks.erl | 35 +-- src/emqx_keepalive.erl | 18 +- src/emqx_logger_formatter.erl | 26 ++- src/emqx_modules.erl | 27 ++- 12 files changed, 603 insertions(+), 256 deletions(-) create mode 100644 src/emqx_cm_locker.erl diff --git a/include/emqx.hrl b/include/emqx.hrl index fba079b91..e52df41f6 100644 --- a/include/emqx.hrl +++ b/include/emqx.hrl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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. +%%-------------------------------------------------------------------- -ifndef(EMQ_X_HRL). -define(EMQ_X_HRL, true). @@ -19,10 +21,6 @@ %% Banner %%-------------------------------------------------------------------- --define(COPYRIGHT, "Copyright (c) 2013-2019 EMQ Technologies Co., Ltd"). - --define(LICENSE_MESSAGE, "Licensed under the Apache License, Version 2.0"). - -define(PROTOCOL_VERSION, "MQTT/5.0"). -define(ERTS_MINIMUM_REQUIRED, "10.0"). @@ -47,8 +45,6 @@ %% Message and Delivery %%-------------------------------------------------------------------- --record(session, {sid, pid}). - -record(subscription, {topic, subid, subopts}). %% See 'Application Message' in MQTT Version 5.0 @@ -72,9 +68,12 @@ }). -record(delivery, { - sender :: pid(), %% Sender of the delivery - message :: #message{}, %% The message delivered - results :: list() %% Dispatches of the message + %% Sender of the delivery + sender :: pid(), + %% The message delivered + message :: #message{}, + %% Dispatches of the message + results :: list() }). %%-------------------------------------------------------------------- @@ -151,6 +150,7 @@ %%-------------------------------------------------------------------- %% Banned %%-------------------------------------------------------------------- + -type(banned_who() :: {client_id, binary()} | {username, binary()} | {ip_address, inet:ip_address()}). diff --git a/src/emqx_banned.erl b/src/emqx_banned.erl index 126562401..7d1b2c773 100644 --- a/src/emqx_banned.erl +++ b/src/emqx_banned.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_banned). @@ -42,14 +44,14 @@ , code_change/3 ]). --define(TAB, ?MODULE). +-define(BANNED_TAB, ?MODULE). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Mnesia bootstrap -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- mnesia(boot) -> - ok = ekka_mnesia:create_table(?TAB, [ + ok = ekka_mnesia:create_table(?BANNED_TAB, [ {type, set}, {disc_copies, [node()]}, {record_name, banned}, @@ -57,7 +59,7 @@ mnesia(boot) -> {storage_properties, [{ets, [{read_concurrency, true}]}]}]); mnesia(copy) -> - ok = ekka_mnesia:copy_table(?TAB). + ok = ekka_mnesia:copy_table(?BANNED_TAB). %% @doc Start the banned server. -spec(start_link() -> startlink_ret()). @@ -66,41 +68,42 @@ start_link() -> -spec(check(emqx_types:credentials()) -> boolean()). check(#{client_id := ClientId, username := Username, peername := {IPAddr, _}}) -> - ets:member(?TAB, {client_id, ClientId}) - orelse ets:member(?TAB, {username, Username}) - orelse ets:member(?TAB, {ipaddr, IPAddr}). + ets:member(?BANNED_TAB, {client_id, ClientId}) + orelse ets:member(?BANNED_TAB, {username, Username}) + orelse ets:member(?BANNED_TAB, {ipaddr, IPAddr}). -spec(add(emqx_types:banned()) -> ok). add(Banned) when is_record(Banned, banned) -> - mnesia:dirty_write(?TAB, Banned). + mnesia:dirty_write(?BANNED_TAB, Banned). -spec(delete({client_id, emqx_types:client_id()} | {username, emqx_types:username()} | {peername, emqx_types:peername()}) -> ok). delete(Key) -> - mnesia:dirty_delete(?TAB, Key). + mnesia:dirty_delete(?BANNED_TAB, Key). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% gen_server callbacks -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- init([]) -> {ok, ensure_expiry_timer(#{expiry_timer => undefined})}. handle_call(Req, _From, State) -> - ?LOG(error, "[Banned] unexpected call: ~p", [Req]), + ?LOG(error, "[Banned] Unexpected call: ~p", [Req]), {reply, ignored, State}. handle_cast(Msg, State) -> - ?LOG(error, "[Banned] unexpected msg: ~p", [Msg]), + ?LOG(error, "[Banned] Unexpected msg: ~p", [Msg]), {noreply, State}. handle_info({timeout, TRef, expire}, State = #{expiry_timer := TRef}) -> - mnesia:async_dirty(fun expire_banned_items/1, [erlang:system_time(second)]), + mnesia:async_dirty(fun expire_banned_items/1, + [erlang:system_time(second)]), {noreply, ensure_expiry_timer(State), hibernate}; handle_info(Info, State) -> - ?LOG(error, "[Banned] unexpected info: ~p", [Info]), + ?LOG(error, "[Banned] Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, #{expiry_timer := TRef}) -> @@ -109,9 +112,9 @@ terminate(_Reason, #{expiry_timer := TRef}) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Internal functions -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- -ifdef(TEST). ensure_expiry_timer(State) -> @@ -124,6 +127,7 @@ ensure_expiry_timer(State) -> expire_banned_items(Now) -> mnesia:foldl( fun(B = #banned{until = Until}, _Acc) when Until < Now -> - mnesia:delete_object(?TAB, B, sticky_write); + mnesia:delete_object(?BANNED_TAB, B, sticky_write); (_, _Acc) -> ok - end, ok, ?TAB). + end, ok, ?BANNED_TAB). + diff --git a/src/emqx_channel.erl b/src/emqx_channel.erl index 7e7b7f4c1..89c65f45f 100644 --- a/src/emqx_channel.erl +++ b/src/emqx_channel.erl @@ -30,7 +30,10 @@ , stats/1 ]). --export([kick/1]). +-export([ kick/1 + , discard/1 + , takeover/1 + ]). -export([session/1]). @@ -135,6 +138,12 @@ stats(#state{transport = Transport, kick(CPid) -> call(CPid, kick). +discard(CPid) -> + call(CPid, discard). + +takeover(CPid) -> + call(CPid, takeover). + session(CPid) -> call(CPid, session). @@ -284,6 +293,10 @@ handle({call, From}, kick, State) -> ok = gen_statem:reply(From, ok), shutdown(kicked, State); +handle({call, From}, discard, State) -> + ok = gen_statem:reply(From, ok), + shutdown(discard, State); + handle({call, From}, session, State = #state{proto_state = ProtoState}) -> reply(From, emqx_protocol:session(ProtoState), State); diff --git a/src/emqx_cm.erl b/src/emqx_cm.erl index d78e99465..d4982f80b 100644 --- a/src/emqx_cm.erl +++ b/src/emqx_cm.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,7 +12,9 @@ %% 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. +%%-------------------------------------------------------------------- +%% Channel Manager -module(emqx_cm). -behaviour(gen_server). @@ -22,25 +25,39 @@ -export([start_link/0]). --export([ register_connection/1 - , register_connection/2 - , unregister_connection/1 - , unregister_connection/2 +-export([ register_channel/1 + , unregister_channel/1 + , unregister_channel/2 ]). -export([ get_conn_attrs/1 , get_conn_attrs/2 , set_conn_attrs/2 - , set_conn_attrs/3 ]). -export([ get_conn_stats/1 , get_conn_stats/2 , set_conn_stats/2 - , set_conn_stats/3 ]). --export([lookup_conn_pid/1]). +-export([ open_session/1 + , discard_session/1 + , resume_session/1 + ]). + +-export([ get_session_attrs/1 + , get_session_attrs/2 + , set_session_attrs/2 + ]). + +-export([ get_session_stats/1 + , get_session_stats/2 + , set_session_stats/2 + ]). + +-export([ lookup_channels/1 + , lookup_channels/2 + ]). %% gen_server callbacks -export([ init/1 @@ -51,159 +68,350 @@ , code_change/3 ]). -%% internal export +%% Internal export -export([stats_fun/0]). --define(CM, ?MODULE). +-type(chan_pid() :: pid()). -%% ETS tables for connection management. --define(CONN_TAB, emqx_conn). --define(CONN_ATTRS_TAB, emqx_conn_attrs). --define(CONN_STATS_TAB, emqx_conn_stats). +-opaque(attrs() :: #{atom() => term()}). +-opaque(stats() :: #{atom() => integer()}). + +-export_type([attrs/0, stats/0]). + +%% Tables for channel management. +-define(CHAN_TAB, emqx_channel). + +-define(CONN_TAB, emqx_connection). + +-define(SESSION_TAB, emqx_session). + +-define(SESSION_P_TAB, emqx_session_p). + +%% Chan stats +-define(CHAN_STATS, + [{?CHAN_TAB, 'channels.count', 'channels.max'}, + {?CONN_TAB, 'connections.count', 'connections.max'}, + {?SESSION_TAB, 'sessions.count', 'sessions.max'}, + {?SESSION_P_TAB, 'sessions.persistent.count', 'sessions.persistent.max'} + ]). + +%% Batch drain -define(BATCH_SIZE, 100000). -%% @doc Start the connection manager. +%% Server name +-define(CM, ?MODULE). + +%% @doc Start the channel manager. -spec(start_link() -> startlink_ret()). start_link() -> gen_server:start_link({local, ?CM}, ?MODULE, [], []). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% API -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- -%% @doc Register a connection. --spec(register_connection(emqx_types:client_id()) -> ok). -register_connection(ClientId) when is_binary(ClientId) -> - register_connection(ClientId, self()). +%% @doc Register a channel. +-spec(register_channel(emqx_types:client_id()) -> ok). +register_channel(ClientId) when is_binary(ClientId) -> + register_channel(ClientId, self()). --spec(register_connection(emqx_types:client_id(), pid()) -> ok). -register_connection(ClientId, ConnPid) when is_binary(ClientId), is_pid(ConnPid) -> - true = ets:insert(?CONN_TAB, {ClientId, ConnPid}), - notify({registered, ClientId, ConnPid}). +-spec(register_channel(emqx_types:client_id(), chan_pid()) -> ok). +register_channel(ClientId, ChanPid) -> + Chan = {ClientId, ChanPid}, + true = ets:insert(?CHAN_TAB, Chan), + ok = emqx_cm_registry:register_channel(Chan), + cast({registered, Chan}). -%% @doc Unregister a connection. --spec(unregister_connection(emqx_types:client_id()) -> ok). -unregister_connection(ClientId) when is_binary(ClientId) -> - unregister_connection(ClientId, self()). +%% @doc Unregister a channel. +-spec(unregister_channel(emqx_types:client_id()) -> ok). +unregister_channel(ClientId) when is_binary(ClientId) -> + unregister_channel(ClientId, self()). --spec(unregister_connection(emqx_types:client_id(), pid()) -> ok). -unregister_connection(ClientId, ConnPid) when is_binary(ClientId), is_pid(ConnPid) -> - true = do_unregister_connection({ClientId, ConnPid}), - notify({unregistered, ConnPid}). +-spec(unregister_channel(emqx_types:client_id(), chan_pid()) -> ok). +unregister_channel(ClientId, ChanPid) -> + Chan = {ClientId, ChanPid}, + true = do_unregister_channel(Chan), + cast({unregistered, Chan}). -do_unregister_connection(Conn) -> - true = ets:delete(?CONN_STATS_TAB, Conn), - true = ets:delete(?CONN_ATTRS_TAB, Conn), - true = ets:delete_object(?CONN_TAB, Conn). +%% @private +do_unregister_channel(Chan) -> + ok = emqx_cm_registry:unregister_channel(Chan), + true = ets:delete_object(?SESSION_P_TAB, Chan), + true = ets:delete(?SESSION_TAB, Chan), + true = ets:delete(?CONN_TAB, Chan), + ets:delete_object(?CHAN_TAB, Chan). -%% @doc Get conn attrs --spec(get_conn_attrs(emqx_types:client_id()) -> list()). -get_conn_attrs(ClientId) when is_binary(ClientId) -> - ConnPid = lookup_conn_pid(ClientId), - get_conn_attrs(ClientId, ConnPid). +%% @doc Get conn attrs. +-spec(get_conn_attrs(emqx_types:client_id()) -> maybe(attrs())). +get_conn_attrs(ClientId) -> + with_channel(ClientId, fun(ChanPid) -> + get_conn_attrs(ClientId, ChanPid) + end). --spec(get_conn_attrs(emqx_types:client_id(), pid()) -> list()). -get_conn_attrs(ClientId, ConnPid) when is_binary(ClientId) -> - emqx_tables:lookup_value(?CONN_ATTRS_TAB, {ClientId, ConnPid}, []). +-spec(get_conn_attrs(emqx_types:client_id(), chan_pid()) -> maybe(attrs())). +get_conn_attrs(ClientId, ChanPid) when node(ChanPid) == node() -> + Chan = {ClientId, ChanPid}, + try ets:lookup_element(?CONN_TAB, Chan, 2) of + Attrs -> Attrs + catch + error:badarg -> undefined + end; +get_conn_attrs(ClientId, ChanPid) -> + rpc_call(node(ChanPid), get_conn_attrs, [ClientId, ChanPid]). -%% @doc Set conn attrs --spec(set_conn_attrs(emqx_types:client_id(), list()) -> true). -set_conn_attrs(ClientId, Attrs) when is_binary(ClientId) -> - set_conn_attrs(ClientId, self(), Attrs). +%% @doc Set conn attrs. +-spec(set_conn_attrs(emqx_types:client_id(), attrs()) -> ok). +set_conn_attrs(ClientId, Attrs) when is_map(Attrs) -> + Chan = {ClientId, self()}, + case ets:update_element(?CONN_TAB, Chan, {2, Attrs}) of + true -> ok; + false -> true = ets:insert(?CONN_TAB, {Chan, Attrs, #{}}), + ok + end. --spec(set_conn_attrs(emqx_types:client_id(), pid(), list()) -> true). -set_conn_attrs(ClientId, ConnPid, Attrs) when is_binary(ClientId), is_pid(ConnPid) -> - Conn = {ClientId, ConnPid}, - ets:insert(?CONN_ATTRS_TAB, {Conn, Attrs}). +%% @doc Get conn stats. +-spec(get_conn_stats(emqx_types:client_id()) -> maybe(stats())). +get_conn_stats(ClientId) -> + with_channel(ClientId, fun(ChanPid) -> + get_conn_stats(ClientId, ChanPid) + end). -%% @doc Get conn stats --spec(get_conn_stats(emqx_types:client_id()) -> list(emqx_stats:stats())). -get_conn_stats(ClientId) when is_binary(ClientId) -> - ConnPid = lookup_conn_pid(ClientId), - get_conn_stats(ClientId, ConnPid). - --spec(get_conn_stats(emqx_types:client_id(), pid()) -> list(emqx_stats:stats())). -get_conn_stats(ClientId, ConnPid) when is_binary(ClientId) -> - Conn = {ClientId, ConnPid}, - emqx_tables:lookup_value(?CONN_STATS_TAB, Conn, []). +-spec(get_conn_stats(emqx_types:client_id(), chan_pid()) -> maybe(stats())). +get_conn_stats(ClientId, ChanPid) when node(ChanPid) == node() -> + Chan = {ClientId, ChanPid}, + try ets:lookup_element(?CONN_TAB, Chan, 3) of + Stats -> Stats + catch + error:badarg -> undefined + end; +get_conn_stats(ClientId, ChanPid) -> + rpc_call(node(ChanPid), get_conn_stats, [ClientId, ChanPid]). %% @doc Set conn stats. --spec(set_conn_stats(emqx_types:client_id(), list(emqx_stats:stats())) -> true). +-spec(set_conn_stats(emqx_types:client_id(), stats()) -> ok). set_conn_stats(ClientId, Stats) when is_binary(ClientId) -> set_conn_stats(ClientId, self(), Stats). --spec(set_conn_stats(emqx_types:client_id(), pid(), list(emqx_stats:stats())) -> true). -set_conn_stats(ClientId, ConnPid, Stats) when is_binary(ClientId), is_pid(ConnPid) -> - Conn = {ClientId, ConnPid}, - ets:insert(?CONN_STATS_TAB, {Conn, Stats}). +-spec(set_conn_stats(emqx_types:client_id(), chan_pid(), stats()) -> ok). +set_conn_stats(ClientId, ChanPid, Stats) -> + Chan = {ClientId, ChanPid}, + _ = ets:update_element(?CONN_TAB, Chan, {3, Stats}), + ok. -%% @doc Lookup connection pid. --spec(lookup_conn_pid(emqx_types:client_id()) -> maybe(pid())). -lookup_conn_pid(ClientId) when is_binary(ClientId) -> - emqx_tables:lookup_value(?CONN_TAB, ClientId). +%% @doc Open a session. +-spec(open_session(map()) -> {ok, emqx_session:session()} + | {error, Reason :: term()}). +open_session(Attrs = #{clean_start := true, + client_id := ClientId}) -> + CleanStart = fun(_) -> + ok = discard_session(ClientId), + {ok, emqx_session:new(Attrs)} + end, + emqx_cm_locker:trans(ClientId, CleanStart); -notify(Msg) -> - gen_server:cast(?CM, {notify, Msg}). +open_session(Attrs = #{clean_start := false, + client_id := ClientId}) -> + ResumeStart = fun(_) -> + case resume_session(ClientId) of + {ok, Session} -> + {ok, Session, true}; + {error, not_found} -> + {ok, emqx_session:new(Attrs)} + end + end, + emqx_cm_locker:trans(ClientId, ResumeStart). -%%----------------------------------------------------------------------------- +%% @doc Try to resume a session. +-spec(resume_session(emqx_types:client_id()) + -> {ok, emqx_session:session()} | {error, Reason :: term()}). +resume_session(ClientId) -> + case lookup_channels(ClientId) of + [] -> {error, not_found}; + [ChanPid] -> + emqx_channel:resume(ChanPid); + ChanPids -> + [ChanPid|StalePids] = lists:reverse(ChanPids), + ?LOG(error, "[SM] More than one channel found: ~p", [ChanPids]), + lists:foreach(fun(StalePid) -> + catch emqx_channel:discard(StalePid) + end, StalePids), + emqx_channel:resume(ChanPid) + end. + +%% @doc Discard all the sessions identified by the ClientId. +-spec(discard_session(emqx_types:client_id()) -> ok). +discard_session(ClientId) when is_binary(ClientId) -> + case lookup_channels(ClientId) of + [] -> ok; + ChanPids -> + lists:foreach( + fun(ChanPid) -> + try emqx_channel:discard(ChanPid) + catch + _:Error:_Stk -> + ?LOG(warning, "[SM] Failed to discard ~p: ~p", [ChanPid, Error]) + end + end, ChanPids) + end. + +%% @doc Get session attrs. +-spec(get_session_attrs(emqx_types:client_id()) -> attrs()). +get_session_attrs(ClientId) -> + with_channel(ClientId, fun(ChanPid) -> + get_session_attrs(ClientId, ChanPid) + end). + +-spec(get_session_attrs(emqx_types:client_id(), chan_pid()) -> maybe(attrs())). +get_session_attrs(ClientId, ChanPid) when node(ChanPid) == node() -> + Chan = {ClientId, ChanPid}, + try ets:lookup_element(?SESSION_TAB, Chan, 2) of + Attrs -> Attrs + catch + error:badarg -> undefined + end; +get_session_attrs(ClientId, ChanPid) -> + rpc_call(node(ChanPid), get_session_attrs, [ClientId, ChanPid]). + +%% @doc Set session attrs. +-spec(set_session_attrs(emqx_types:client_id(), attrs()) -> ok). +set_session_attrs(ClientId, Attrs) when is_binary(ClientId) -> + Chan = {ClientId, self()}, + case ets:update_element(?SESSION_TAB, Chan, {2, Attrs}) of + true -> ok; + false -> + true = ets:insert(?SESSION_TAB, {Chan, Attrs, #{}}), + is_clean_start(Attrs) orelse ets:insert(?SESSION_P_TAB, Chan), + ok + end. + +%% @doc Is clean start? +is_clean_start(#{clean_start := false}) -> false; +is_clean_start(_Attrs) -> true. + +%% @doc Get session stats. +-spec(get_session_stats(emqx_types:client_id()) -> stats()). +get_session_stats(ClientId) -> + with_channel(ClientId, fun(ChanPid) -> + get_session_stats(ClientId, ChanPid) + end). + +-spec(get_session_stats(emqx_types:client_id(), chan_pid()) -> maybe(stats())). +get_session_stats(ClientId, ChanPid) when node(ChanPid) == node() -> + Chan = {ClientId, ChanPid}, + try ets:lookup_element(?SESSION_TAB, Chan, 3) of + Stats -> Stats + catch + error:badarg -> undefined + end; +get_session_stats(ClientId, ChanPid) -> + rpc_call(node(ChanPid), get_session_stats, [ClientId, ChanPid]). + +%% @doc Set session stats. +-spec(set_session_stats(emqx_types:client_id(), stats()) -> ok). +set_session_stats(ClientId, Stats) when is_binary(ClientId) -> + set_session_stats(ClientId, self(), Stats). + +-spec(set_session_stats(emqx_types:client_id(), chan_pid(), stats()) -> ok). +set_session_stats(ClientId, ChanPid, Stats) -> + Chan = {ClientId, ChanPid}, + _ = ets:update_element(?SESSION_TAB, Chan, {3, Stats}), + ok. + +with_channel(ClientId, Fun) -> + case lookup_channels(ClientId) of + [] -> undefined; + [Pid] -> Fun(Pid); + Pids -> Fun(lists:last(Pids)) + end. + +%% @doc Lookup channels. +-spec(lookup_channels(emqx_types:client_id()) -> list(chan_pid())). +lookup_channels(ClientId) -> + lookup_channels(global, ClientId). + +%% @doc Lookup local or global channels. +-spec(lookup_channels(local | global, emqx_types:client_id()) -> list(chan_pid())). +lookup_channels(global, ClientId) -> + case emqx_cm_registry:is_enabled() of + true -> + emqx_cm_registry:lookup_channels(ClientId); + false -> + lookup_channels(local, ClientId) + end; + +lookup_channels(local, ClientId) -> + [ChanPid || {_, ChanPid} <- ets:lookup(?CHAN_TAB, ClientId)]. + +%% @private +rpc_call(Node, Fun, Args) -> + case rpc:call(Node, ?MODULE, Fun, Args) of + {badrpc, Reason} -> error(Reason); + Res -> Res + end. + +%% @private +cast(Msg) -> gen_server:cast(?CM, Msg). + +%%-------------------------------------------------------------------- %% gen_server callbacks -%%----------------------------------------------------------------------------- +%%-------------------------------------------------------------------- init([]) -> - TabOpts = [public, set, {write_concurrency, true}], - ok = emqx_tables:new(?CONN_TAB, [{read_concurrency, true} | TabOpts]), - ok = emqx_tables:new(?CONN_ATTRS_TAB, TabOpts), - ok = emqx_tables:new(?CONN_STATS_TAB, TabOpts), - ok = emqx_stats:update_interval(conn_stats, fun ?MODULE:stats_fun/0), - {ok, #{conn_pmon => emqx_pmon:new()}}. + TabOpts = [public, {write_concurrency, true}], + ok = emqx_tables:new(?CHAN_TAB, [bag, {read_concurrency, true} | TabOpts]), + ok = emqx_tables:new(?CONN_TAB, [set, compressed | TabOpts]), + ok = emqx_tables:new(?SESSION_TAB, [set, compressed | TabOpts]), + ok = emqx_tables:new(?SESSION_P_TAB, [bag | TabOpts]), + ok = emqx_stats:update_interval(chan_stats, fun ?MODULE:stats_fun/0), + {ok, #{chan_pmon => emqx_pmon:new()}}. handle_call(Req, _From, State) -> ?LOG(error, "[CM] Unexpected call: ~p", [Req]), {reply, ignored, State}. -handle_cast({notify, {registered, ClientId, ConnPid}}, State = #{conn_pmon := PMon}) -> - {noreply, State#{conn_pmon := emqx_pmon:monitor(ConnPid, ClientId, PMon)}}; +handle_cast({registered, {ClientId, ChanPid}}, State = #{chan_pmon := PMon}) -> + PMon1 = emqx_pmon:monitor(ChanPid, ClientId, PMon), + {noreply, State#{chan_pmon := PMon1}}; -handle_cast({notify, {unregistered, ConnPid}}, State = #{conn_pmon := PMon}) -> - {noreply, State#{conn_pmon := emqx_pmon:demonitor(ConnPid, PMon)}}; +handle_cast({unregistered, {_ClientId, ChanPid}}, State = #{chan_pmon := PMon}) -> + PMon1 = emqx_pmon:demonitor(ChanPid, PMon), + {noreply, State#{chan_pmon := PMon1}}; handle_cast(Msg, State) -> ?LOG(error, "[CM] Unexpected cast: ~p", [Msg]), {noreply, State}. -handle_info({'DOWN', _MRef, process, Pid, _Reason}, State = #{conn_pmon := PMon}) -> - ConnPids = [Pid | emqx_misc:drain_down(?BATCH_SIZE)], - {Items, PMon1} = emqx_pmon:erase_all(ConnPids, PMon), - ok = emqx_pool:async_submit( - fun lists:foreach/2, [fun clean_down/1, Items]), - {noreply, State#{conn_pmon := PMon1}}; +handle_info({'DOWN', _MRef, process, Pid, _Reason}, State = #{chan_pmon := PMon}) -> + ChanPids = [Pid | emqx_misc:drain_down(?BATCH_SIZE)], + {Items, PMon1} = emqx_pmon:erase_all(ChanPids, PMon), + ok = emqx_pool:async_submit(fun lists:foreach/2, [fun clean_down/1, Items]), + {noreply, State#{chan_pmon := PMon1}}; handle_info(Info, State) -> ?LOG(error, "[CM] Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, _State) -> - emqx_stats:cancel_update(conn_stats). + emqx_stats:cancel_update(chan_stats). code_change(_OldVsn, State, _Extra) -> {ok, State}. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Internal functions -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- -clean_down({Pid, ClientId}) -> - Conn = {ClientId, Pid}, - case ets:member(?CONN_TAB, ClientId) - orelse ets:member(?CONN_ATTRS_TAB, Conn) of - true -> - do_unregister_connection(Conn); - false -> false - end. +clean_down({ChanPid, ClientId}) -> + Chan = {ClientId, ChanPid}, + do_unregister_channel(Chan). stats_fun() -> - case ets:info(?CONN_TAB, size) of + lists:foreach(fun update_stats/1, ?CHAN_STATS). + +update_stats({Tab, Stat, MaxStat}) -> + case ets:info(Tab, size) of undefined -> ok; - Size -> emqx_stats:setstat('connections.count', 'connections.max', Size) + Size -> emqx_stats:setstat(Stat, MaxStat, Size) end. + diff --git a/src/emqx_cm_locker.erl b/src/emqx_cm_locker.erl new file mode 100644 index 000000000..c5cd9c2f6 --- /dev/null +++ b/src/emqx_cm_locker.erl @@ -0,0 +1,66 @@ +%%-------------------------------------------------------------------- +%% 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_cm_locker). + +-include("emqx.hrl"). +-include("types.hrl"). + +-export([start_link/0]). + +-export([ trans/2 + , trans/3 + , lock/1 + , lock/2 + , unlock/1 + ]). + +-spec(start_link() -> startlink_ret()). +start_link() -> + ekka_locker:start_link(?MODULE). + +-spec(trans(emqx_types:client_id(), fun(([node()]) -> any())) -> any()). +trans(ClientId, Fun) -> + trans(ClientId, Fun, undefined). + +-spec(trans(maybe(emqx_types:client_id()), + fun(([node()])-> any()), ekka_locker:piggyback()) -> any()). +trans(undefined, Fun, _Piggyback) -> + Fun([]); +trans(ClientId, Fun, Piggyback) -> + case lock(ClientId, Piggyback) of + {true, Nodes} -> + try Fun(Nodes) after unlock(ClientId) end; + {false, _Nodes} -> + {error, client_id_unavailable} + end. + +-spec(lock(emqx_types:client_id()) -> ekka_locker:lock_result()). +lock(ClientId) -> + ekka_locker:acquire(?MODULE, ClientId, strategy()). + +-spec(lock(emqx_types:client_id(), ekka_locker:piggyback()) -> ekka_locker:lock_result()). +lock(ClientId, Piggyback) -> + ekka_locker:acquire(?MODULE, ClientId, strategy(), Piggyback). + +-spec(unlock(emqx_types:client_id()) -> {boolean(), [node()]}). +unlock(ClientId) -> + ekka_locker:release(?MODULE, ClientId, strategy()). + +-spec(strategy() -> local | one | quorum | all). +strategy() -> + emqx_config:get_env(session_locking_strategy, quorum). + diff --git a/src/emqx_cm_registry.erl b/src/emqx_cm_registry.erl index ccacc9907..ffdb6661a 100644 --- a/src/emqx_cm_registry.erl +++ b/src/emqx_cm_registry.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,8 +12,10 @@ %% 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_sm_registry). +%% Global Channel Registry +-module(emqx_cm_registry). -behaviour(gen_server). @@ -22,12 +25,14 @@ -export([start_link/0]). --export([ is_enabled/0 - , register_session/1 - , lookup_session/1 - , unregister_session/1 +-export([is_enabled/0]). + +-export([ register_channel/1 + , unregister_channel/1 ]). +-export([lookup_channels/1]). + %% gen_server callbacks -export([ init/1 , handle_call/3 @@ -38,57 +43,67 @@ ]). -define(REGISTRY, ?MODULE). --define(TAB, emqx_session_registry). --define(LOCK, {?MODULE, cleanup_sessions}). +-define(TAB, emqx_channel_registry). +-define(LOCK, {?MODULE, cleanup_down}). --record(global_session, {sid, pid}). +-record(channel, {chid, pid}). --type(session_pid() :: pid()). - -%%------------------------------------------------------------------------------ -%% APIs -%%------------------------------------------------------------------------------ - -%% @doc Start the global session manager. +%% @doc Start the global channel registry. -spec(start_link() -> startlink_ret()). start_link() -> gen_server:start_link({local, ?REGISTRY}, ?MODULE, [], []). +%%-------------------------------------------------------------------- +%% APIs +%%-------------------------------------------------------------------- + +%% @doc Is the global registry enabled? -spec(is_enabled() -> boolean()). is_enabled() -> - emqx_config:get_env(enable_session_registry, true). + emqx_config:get_env(enable_channel_registry, true). --spec(lookup_session(emqx_types:client_id()) -> list(session_pid())). -lookup_session(ClientId) -> - [SessPid || #global_session{pid = SessPid} <- mnesia:dirty_read(?TAB, ClientId)]. +%% @doc Register a global channel. +-spec(register_channel(emqx_types:client_id() + | {emqx_types:client_id(), pid()}) -> ok). +register_channel(ClientId) when is_binary(ClientId) -> + register_channel({ClientId, self()}); --spec(register_session({emqx_types:client_id(), session_pid()}) -> ok). -register_session({ClientId, SessPid}) when is_binary(ClientId), is_pid(SessPid) -> +register_channel({ClientId, ChanPid}) when is_binary(ClientId), is_pid(ChanPid) -> case is_enabled() of - true -> mnesia:dirty_write(?TAB, record(ClientId, SessPid)); + true -> mnesia:dirty_write(?TAB, record(ClientId, ChanPid)); false -> ok end. --spec(unregister_session({emqx_types:client_id(), session_pid()}) -> ok). -unregister_session({ClientId, SessPid}) when is_binary(ClientId), is_pid(SessPid) -> +%% @doc Unregister a global channel. +-spec(unregister_channel(emqx_types:client_id() + | {emqx_types:client_id(), pid()}) -> ok). +unregister_channel(ClientId) when is_binary(ClientId) -> + unregister_channel({ClientId, self()}); + +unregister_channel({ClientId, ChanPid}) when is_binary(ClientId), is_pid(ChanPid) -> case is_enabled() of - true -> mnesia:dirty_delete_object(?TAB, record(ClientId, SessPid)); + true -> mnesia:dirty_delete_object(?TAB, record(ClientId, ChanPid)); false -> ok end. -record(ClientId, SessPid) -> - #global_session{sid = ClientId, pid = SessPid}. +%% @doc Lookup the global channels. +-spec(lookup_channels(emqx_types:client_id()) -> list(pid())). +lookup_channels(ClientId) -> + [ChanPid || #channel{pid = ChanPid} <- mnesia:dirty_read(?TAB, ClientId)]. -%%------------------------------------------------------------------------------ +record(ClientId, ChanPid) -> + #channel{chid = ClientId, pid = ChanPid}. + +%%-------------------------------------------------------------------- %% gen_server callbacks -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- init([]) -> ok = ekka_mnesia:create_table(?TAB, [ {type, bag}, {ram_copies, [node()]}, - {record_name, global_session}, - {attributes, record_info(fields, global_session)}, + {record_name, channel}, + {attributes, record_info(fields, channel)}, {storage_properties, [{ets, [{read_concurrency, true}, {write_concurrency, true}]}]}]), ok = ekka_mnesia:copy_table(?TAB), @@ -106,7 +121,7 @@ handle_cast(Msg, State) -> handle_info({membership, {mnesia, down, Node}}, State) -> global:trans({?LOCK, self()}, fun() -> - mnesia:transaction(fun cleanup_sessions/1, [Node]) + mnesia:transaction(fun cleanup_channels/1, [Node]) end), {noreply, State}; @@ -123,14 +138,14 @@ terminate(_Reason, _State) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Internal functions -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- -cleanup_sessions(Node) -> - Pat = [{#global_session{pid = '$1', _ = '_'}, [{'==', {node, '$1'}, Node}], ['$_']}], - lists:foreach(fun delete_session/1, mnesia:select(?TAB, Pat, write)). +cleanup_channels(Node) -> + Pat = [{#channel{pid = '$1', _ = '_'}, [{'==', {node, '$1'}, Node}], ['$_']}], + lists:foreach(fun delete_channel/1, mnesia:select(?TAB, Pat, write)). -delete_session(Session) -> - mnesia:delete_object(?TAB, Session, write). +delete_channel(Chan) -> + mnesia:delete_object(?TAB, Chan, write). diff --git a/src/emqx_cm_sup.erl b/src/emqx_cm_sup.erl index 65702b26f..6cbf8d432 100644 --- a/src/emqx_cm_sup.erl +++ b/src/emqx_cm_sup.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,8 +12,9 @@ %% 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_sm_sup). +-module(emqx_cm_sup). -behaviour(supervisor). @@ -24,41 +26,45 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). init([]) -> - %% Session locker + Banned = #{id => banned, + start => {emqx_banned, start_link, []}, + restart => permanent, + shutdown => 1000, + type => worker, + modules => [emqx_banned]}, + Flapping = #{id => flapping, + start => {emqx_flapping, start_link, []}, + restart => permanent, + shutdown => 1000, + type => worker, + modules => [emqx_flapping]}, + %% Channel locker Locker = #{id => locker, - start => {emqx_sm_locker, start_link, []}, + start => {emqx_cm_locker, start_link, []}, restart => permanent, shutdown => 5000, type => worker, - modules => [emqx_sm_locker] + modules => [emqx_cm_locker] }, - %% Session registry + %% Channel registry Registry = #{id => registry, - start => {emqx_sm_registry, start_link, []}, + start => {emqx_cm_registry, start_link, []}, restart => permanent, shutdown => 5000, type => worker, - modules => [emqx_sm_registry] + modules => [emqx_cm_registry] }, - %% Session Manager + %% Channel Manager Manager = #{id => manager, - start => {emqx_sm, start_link, []}, + start => {emqx_cm, start_link, []}, restart => permanent, shutdown => 5000, type => worker, - modules => [emqx_sm] + modules => [emqx_cm] }, - %% Session Sup - SessSpec = #{start => {emqx_session, start_link, []}, - shutdown => brutal_kill, - clean_down => fun emqx_sm:clean_down/1 + SupFlags = #{strategy => one_for_one, + intensity => 100, + period => 10 }, - SessionSup = #{id => session_sup, - start => {emqx_session_sup, start_link, [SessSpec ]}, - restart => transient, - shutdown => infinity, - type => supervisor, - modules => [emqx_session_sup] - }, - {ok, {{rest_for_one, 10, 3600}, [Locker, Registry, Manager, SessionSup]}}. + {ok, {SupFlags, [Banned, Flapping, Locker, Registry, Manager]}}. diff --git a/src/emqx_flapping.erl b/src/emqx_flapping.erl index 099bf3910..94e1bcc78 100644 --- a/src/emqx_flapping.erl +++ b/src/emqx_flapping.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,9 @@ %% 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. +%%-------------------------------------------------------------------- + +%% @doc This module is used to garbage clean the flapping records. -module(emqx_flapping). @@ -19,31 +23,29 @@ -behaviour(gen_statem). --export([start_link/1]). - -%% This module is used to garbage clean the flapping records +-export([start_link/0]). %% gen_statem callbacks --export([ terminate/3 - , code_change/4 - , init/1 +-export([ init/1 , initialized/3 , callback_mode/0 + , terminate/3 + , code_change/4 ]). -define(FLAPPING_TAB, ?MODULE). -export([check/3]). --record(flapping, - { client_id :: binary() - , check_count :: integer() - , timestamp :: integer() - }). +-record(flapping, { + client_id :: binary(), + check_count :: integer(), + timestamp :: integer() + }). -type(flapping_record() :: #flapping{}). --type(flapping_state() :: flapping | ok). +-type(flapping_state() :: flapping | ok). %% @doc This function is used to initialize flapping records %% the expiry time unit is minutes. @@ -96,18 +98,20 @@ check_flapping(Action, CheckCount, _Threshold = {TimesThreshold, TimeInterval}, %%-------------------------------------------------------------------- %% gen_statem callbacks %%-------------------------------------------------------------------- --spec(start_link(TimerInterval :: [integer()]) -> startlink_ret()). -start_link(TimerInterval) -> - gen_statem:start_link({local, ?MODULE}, ?MODULE, [TimerInterval], []). -init([TimerInterval]) -> +-spec(start_link() -> startlink_ret()). +start_link() -> + gen_statem:start_link({local, ?MODULE}, ?MODULE, [], []). + +init([]) -> + Interval = emqx_config:get_env(flapping_clean_interval, 3600000), TabOpts = [ public , set , {keypos, 2} , {write_concurrency, true} , {read_concurrency, true}], ok = emqx_tables:new(?FLAPPING_TAB, TabOpts), - {ok, initialized, #{timer_interval => TimerInterval}}. + {ok, initialized, #{timer_interval => Interval}}. callback_mode() -> [state_functions, state_enter]. @@ -134,3 +138,4 @@ clean_expired_records() -> NowTime = emqx_time:now_secs(), MatchSpec = [{{'$1', '$2', '$3'},[{'<', '$3', NowTime}], [true]}], ets:select_delete(?FLAPPING_TAB, MatchSpec). + diff --git a/src/emqx_hooks.erl b/src/emqx_hooks.erl index 6d448d2f3..c789ae482 100644 --- a/src/emqx_hooks.erl +++ b/src/emqx_hooks.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_hooks). @@ -19,7 +21,9 @@ -include("logger.hrl"). -include("types.hrl"). --export([start_link/0, stop/0]). +-export([ start_link/0 + , stop/0 + ]). %% Hooks API -export([ add/2 @@ -52,11 +56,16 @@ -type(action() :: function() | mfa()). -type(filter() :: function() | mfa()). --record(callback, {action :: action(), - filter :: filter(), - priority :: integer()}). +-record(callback, { + action :: action(), + filter :: filter(), + priority :: integer() + }). --record(hook, {name :: hookpoint(), callbacks :: list(#callback{})}). +-record(hook, { + name :: hookpoint(), + callbacks :: list(#callback{}) + }). -export_type([hookpoint/0, action/0, filter/0]). @@ -65,15 +74,16 @@ -spec(start_link() -> startlink_ret()). start_link() -> - gen_server:start_link({local, ?SERVER}, ?MODULE, [], [{hibernate_after, 1000}]). + gen_server:start_link({local, ?SERVER}, + ?MODULE, [], [{hibernate_after, 1000}]). -spec(stop() -> ok). stop() -> gen_server:stop(?SERVER, normal, infinity). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Hooks API -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% @doc Register a callback -spec(add(hookpoint(), action() | #callback{}) -> ok_or_error(already_exists)). @@ -111,7 +121,6 @@ run(HookPoint, Args) -> run_fold(HookPoint, Args, Acc) -> do_run_fold(lookup(HookPoint), Args, Acc). - do_run([#callback{action = Action, filter = Filter} | Callbacks], Args) -> case filter_passed(Filter, Args) andalso execute(Action, Args) of %% stop the hook chain and return @@ -163,12 +172,12 @@ lookup(HookPoint) -> [] -> [] end. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% gen_server callbacks -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- init([]) -> - ok = emqx_tables:new(?TAB, [{keypos, #hook.name}, {read_concurrency, true}, protected]), + ok = emqx_tables:new(?TAB, [{keypos, #hook.name}, {read_concurrency, true}]), {ok, #{}}. handle_call({add, HookPoint, Callback = #callback{action = Action}}, _From, State) -> diff --git a/src/emqx_keepalive.erl b/src/emqx_keepalive.erl index ebd5e18d4..36cb6b335 100644 --- a/src/emqx_keepalive.erl +++ b/src/emqx_keepalive.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_keepalive). @@ -20,15 +22,22 @@ , cancel/1 ]). --record(keepalive, {statfun, statval, tsec, tmsg, tref, repeat = 0}). +-record(keepalive, { + statfun, + statval, + tsec, + tmsg, + tref, + repeat = 0 + }). -opaque(keepalive() :: #keepalive{}). -export_type([keepalive/0]). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% APIs -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% @doc Start a keepalive -spec(start(fun(), integer(), any()) -> {ok, keepalive()} | {error, term()}). @@ -79,3 +88,4 @@ cancel(_) -> timer(Secs, Msg) -> erlang:send_after(timer:seconds(Secs), self(), Msg). + diff --git a/src/emqx_logger_formatter.erl b/src/emqx_logger_formatter.erl index 92c194883..6cfe1ca58 100644 --- a/src/emqx_logger_formatter.erl +++ b/src/emqx_logger_formatter.erl @@ -34,18 +34,22 @@ -define(IS_STRING(String), (is_list(String) orelse is_binary(String))). -%%%----------------------------------------------------------------- -%%% Types --type config() :: #{chars_limit => pos_integer() | unlimited, - depth => pos_integer() | unlimited, - max_size => pos_integer() | unlimited, - report_cb => logger:report_cb(), - quit => template()}. --type template() :: [metakey() | {metakey(),template(),template()} | string()]. --type metakey() :: atom() | [atom()]. +%%-------------------------------------------------------------------- +%% Types + +-type(config() :: #{chars_limit => pos_integer() | unlimited, + depth => pos_integer() | unlimited, + max_size => pos_integer() | unlimited, + report_cb => logger:report_cb(), + quit => template()}). + +-type(template() :: [metakey() | {metakey(),template(),template()} | string()]). + +-type(metakey() :: atom() | [atom()]). + +%%-------------------------------------------------------------------- +%% API -%%%----------------------------------------------------------------- -%%% API -spec format(LogEvent,Config) -> unicode:chardata() when LogEvent :: logger:log_event(), Config :: config(). diff --git a/src/emqx_modules.erl b/src/emqx_modules.erl index f9d74dc81..38ad29b86 100644 --- a/src/emqx_modules.erl +++ b/src/emqx_modules.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_modules). @@ -20,20 +22,25 @@ , unload/0 ]). +%% @doc Load all the extended modules. -spec(load() -> ok). load() -> ok = emqx_mod_acl_internal:load([]), - lists:foreach( - fun({Mod, Env}) -> - ok = Mod:load(Env), - ?LOG(info, "[Modules] Load ~s module successfully.", [Mod]) - end, emqx_config:get_env(modules, [])). + lists:foreach(fun load/1, modules()). +load({Mod, Env}) -> + ok = Mod:load(Env), + ?LOG(info, "[Modules] Load ~s module successfully.", [Mod]). + +modules() -> + emqx_config:get_env(modules, []). + +%% @doc Unload all the extended modules. -spec(unload() -> ok). unload() -> ok = emqx_mod_acl_internal:unload([]), - lists:foreach( - fun({Mod, Env}) -> - Mod:unload(Env) end, - emqx_config:get_env(modules, [])). + lists:foreach(fun unload/1, modules()). + +unload({Mod, Env}) -> + Mod:unload(Env). From 795fe4e0bc26d879c98b258f4b88ad5102cd3bbd Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 25 Jun 2019 14:29:26 +0800 Subject: [PATCH 06/33] Add 'PUBLISH_PACKET(QoS, Topic, PacketId)' macro --- include/emqx_mqtt.hrl | 7 +++++++ src/{emqx_channel.erl => emqx_connection.erl} | 0 src/{emqx_ws_channel.erl => emqx_ws_connection.erl} | 0 3 files changed, 7 insertions(+) rename src/{emqx_channel.erl => emqx_connection.erl} (100%) rename src/{emqx_ws_channel.erl => emqx_ws_connection.erl} (100%) diff --git a/include/emqx_mqtt.hrl b/include/emqx_mqtt.hrl index 655b8e755..52b5d4313 100644 --- a/include/emqx_mqtt.hrl +++ b/include/emqx_mqtt.hrl @@ -351,6 +351,13 @@ variable = #mqtt_packet_publish{packet_id = PacketId} }). +-define(PUBLISH_PACKET(QoS, Topic, PacketId), + #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, + qos = QoS}, + variable = #mqtt_packet_publish{topic_name = Topic, + packet_id = PacketId} + }). + -define(PUBLISH_PACKET(QoS, Topic, PacketId, Payload), #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, qos = QoS}, diff --git a/src/emqx_channel.erl b/src/emqx_connection.erl similarity index 100% rename from src/emqx_channel.erl rename to src/emqx_connection.erl diff --git a/src/emqx_ws_channel.erl b/src/emqx_ws_connection.erl similarity index 100% rename from src/emqx_ws_channel.erl rename to src/emqx_ws_connection.erl From fb54fe0fad9041dcce8eaf3f371dd71f24f50b39 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 25 Jun 2019 14:45:23 +0800 Subject: [PATCH 07/33] Improve export_type attr --- src/emqx_access_rule.erl | 4 ++-- src/emqx_batch.erl | 6 ++---- src/emqx_client_sock.erl | 4 ++-- src/emqx_cm.erl | 6 +++--- src/emqx_gc.erl | 6 ++++-- src/emqx_hooks.erl | 7 +++++-- src/emqx_keepalive.erl | 4 ++-- src/emqx_metrics.erl | 4 ++-- src/emqx_mountpoint.erl | 4 ++-- src/emqx_mqtt_caps.erl | 5 +++-- src/emqx_pmon.erl | 3 ++- src/emqx_pqueue.erl | 4 ++-- src/emqx_sequence.erl | 4 ++-- src/emqx_stats.erl | 6 +++--- src/emqx_topic.erl | 9 +++++++-- src/emqx_zone.erl | 10 +++++++--- 16 files changed, 50 insertions(+), 36 deletions(-) diff --git a/src/emqx_access_rule.erl b/src/emqx_access_rule.erl index 2f57059cb..d56710a21 100644 --- a/src/emqx_access_rule.erl +++ b/src/emqx_access_rule.erl @@ -23,6 +23,8 @@ , compile/1 ]). +-export_type([rule/0]). + -type(acl_result() :: allow | deny). -type(who() :: all | binary() | @@ -35,8 +37,6 @@ -type(rule() :: {acl_result(), all} | {acl_result(), who(), access(), list(emqx_topic:topic())}). --export_type([rule/0]). - -define(ALLOW_DENY(A), ((A =:= allow) orelse (A =:= deny))). -define(PUBSUB(A), ((A =:= subscribe) orelse (A =:= publish) orelse (A =:= pubsub))). diff --git a/src/emqx_batch.erl b/src/emqx_batch.erl index 6d1df24ca..c5ea3c598 100644 --- a/src/emqx_batch.erl +++ b/src/emqx_batch.erl @@ -24,6 +24,8 @@ , items/1 ]). +-export_type([options/0, batch/0]). + -record(batch, { batch_size :: non_neg_integer(), batch_q :: list(any()), @@ -40,10 +42,6 @@ -opaque(batch() :: #batch{}). --export_type([options/0]). - --export_type([batch/0]). - %%-------------------------------------------------------------------- %% APIs %%-------------------------------------------------------------------- diff --git a/src/emqx_client_sock.erl b/src/emqx_client_sock.erl index f40064f17..eb938910c 100644 --- a/src/emqx_client_sock.erl +++ b/src/emqx_client_sock.erl @@ -26,6 +26,8 @@ , getstat/2 ]). +-export_type([socket/0, option/0]). + -record(ssl_socket, {tcp, ssl}). -type(socket() :: inet:socket() | #ssl_socket{}). @@ -34,8 +36,6 @@ -type(option() :: gen_tcp:connect_option() | {ssl_opts, [ssl:ssl_option()]}). --export_type([socket/0, option/0]). - -define(DEFAULT_TCP_OPTIONS, [binary, {packet, raw}, {active, false}, {nodelay, true}, {reuseaddr, true}]). diff --git a/src/emqx_cm.erl b/src/emqx_cm.erl index 2a42064d8..39b19d9ba 100644 --- a/src/emqx_cm.erl +++ b/src/emqx_cm.erl @@ -73,14 +73,14 @@ %% Internal export -export([stats_fun/0]). +-export_type([attrs/0, stats/0]). + -type(chan_pid() :: pid()). -opaque(attrs() :: #{atom() => term()}). -opaque(stats() :: #{atom() => integer()}). --export_type([attrs/0, stats/0]). - %% Tables for channel management. -define(CHAN_TAB, emqx_channel). @@ -164,7 +164,7 @@ get_conn_attrs(ClientId, ChanPid) -> %% @doc Set conn attrs. -spec(set_conn_attrs(emqx_types:client_id(), attrs()) -> ok). -set_conn_attrs(ClientId, Attrs) when is_map(Attrs) -> +set_conn_attrs(ClientId, Attrs) when is_binary(ClientId), is_map(Attrs) -> Chan = {ClientId, self()}, case ets:update_element(?CONN_TAB, Chan, {2, Attrs}) of true -> ok; diff --git a/src/emqx_gc.erl b/src/emqx_gc.erl index 256a5c4ff..83264ecee 100644 --- a/src/emqx_gc.erl +++ b/src/emqx_gc.erl @@ -14,12 +14,14 @@ %% limitations under the License. %%-------------------------------------------------------------------- +%%-------------------------------------------------------------------- %% @doc This module manages an opaque collection of statistics data used %% to force garbage collection on `self()' process when hitting thresholds. %% Namely: %% (1) Total number of messages passed through %% (2) Total data volume passed through %% @end +%%-------------------------------------------------------------------- -module(emqx_gc). @@ -31,6 +33,8 @@ , reset/1 ]). +-export_type([gc_state/0]). + -type(opts() :: #{count => integer(), bytes => integer()}). @@ -39,8 +43,6 @@ -opaque(gc_state() :: {?MODULE, st()}). --export_type([gc_state/0]). - -define(GCS(St), {?MODULE, St}). -define(disabled, disabled). diff --git a/src/emqx_hooks.erl b/src/emqx_hooks.erl index 220383ebb..39c692b4a 100644 --- a/src/emqx_hooks.erl +++ b/src/emqx_hooks.erl @@ -46,6 +46,11 @@ , code_change/3 ]). +-export_type([ hookpoint/0 + , action/0 + , filter/0 + ]). + %% Multiple callbacks can be registered on a hookpoint. %% The execution order depends on the priority value: %% - Callbacks with greater priority values will be run before @@ -69,8 +74,6 @@ callbacks :: list(#callback{}) }). --export_type([hookpoint/0, action/0, filter/0]). - -define(TAB, ?MODULE). -define(SERVER, ?MODULE). diff --git a/src/emqx_keepalive.erl b/src/emqx_keepalive.erl index 36cb6b335..25170067d 100644 --- a/src/emqx_keepalive.erl +++ b/src/emqx_keepalive.erl @@ -22,6 +22,8 @@ , cancel/1 ]). +-export_type([keepalive/0]). + -record(keepalive, { statfun, statval, @@ -33,8 +35,6 @@ -opaque(keepalive() :: #keepalive{}). --export_type([keepalive/0]). - %%-------------------------------------------------------------------- %% APIs %%-------------------------------------------------------------------- diff --git a/src/emqx_metrics.erl b/src/emqx_metrics.erl index 7a40eaf4e..f8582ca9a 100644 --- a/src/emqx_metrics.erl +++ b/src/emqx_metrics.erl @@ -60,12 +60,12 @@ , code_change/3 ]). +-export_type([metric_idx/0]). + -opaque(metric_idx() :: 1..1024). -type(metric_name() :: atom() | string() | binary()). --export_type([metric_idx/0]). - -define(MAX_SIZE, 1024). -define(RESERVED_IDX, 256). -define(TAB, ?MODULE). diff --git a/src/emqx_mountpoint.erl b/src/emqx_mountpoint.erl index da8cf1f55..1e52d9d25 100644 --- a/src/emqx_mountpoint.erl +++ b/src/emqx_mountpoint.erl @@ -27,10 +27,10 @@ -export([replvar/2]). --type(mountpoint() :: binary()). - -export_type([mountpoint/0]). +-type(mountpoint() :: binary()). + %%-------------------------------------------------------------------- %% APIs %%-------------------------------------------------------------------- diff --git a/src/emqx_mqtt_caps.erl b/src/emqx_mqtt_caps.erl index abd14ee3c..dc748c832 100644 --- a/src/emqx_mqtt_caps.erl +++ b/src/emqx_mqtt_caps.erl @@ -26,6 +26,8 @@ , get_caps/2 ]). +-export_type([caps/0]). + -type(caps() :: #{max_packet_size => integer(), max_clientid_len => integer(), max_topic_alias => integer(), @@ -35,9 +37,8 @@ mqtt_shared_subscription => boolean(), mqtt_wildcard_subscription => boolean()}). --export_type([caps/0]). - -define(UNLIMITED, 0). + -define(DEFAULT_CAPS, [{max_packet_size, ?MAX_PACKET_SIZE}, {max_clientid_len, ?MAX_CLIENTID_LEN}, {max_topic_alias, ?UNLIMITED}, diff --git a/src/emqx_pmon.erl b/src/emqx_pmon.erl index 411eb8ce6..52d7a2619 100644 --- a/src/emqx_pmon.erl +++ b/src/emqx_pmon.erl @@ -32,9 +32,10 @@ -export([count/1]). --type(pmon() :: {?MODULE, map()}). -export_type([pmon/0]). +-opaque(pmon() :: {?MODULE, map()}). + %%-------------------------------------------------------------------- %% APIs %%-------------------------------------------------------------------- diff --git a/src/emqx_pqueue.erl b/src/emqx_pqueue.erl index 06ea192fb..85c89866d 100644 --- a/src/emqx_pqueue.erl +++ b/src/emqx_pqueue.erl @@ -57,6 +57,8 @@ , highest/1 ]). +-export_type([q/0]). + %%---------------------------------------------------------------------------- -type(priority() :: integer() | 'infinity'). @@ -64,8 +66,6 @@ -type(pqueue() :: squeue() | {pqueue, [{priority(), squeue()}]}). -type(q() :: pqueue()). --export_type([q/0]). - %%---------------------------------------------------------------------------- -spec(new() -> pqueue()). diff --git a/src/emqx_sequence.erl b/src/emqx_sequence.erl index 29381bf6c..8aa763b6f 100644 --- a/src/emqx_sequence.erl +++ b/src/emqx_sequence.erl @@ -23,14 +23,14 @@ , delete/1 ]). +-export_type([seqid/0]). + -type(key() :: term()). -type(name() :: atom()). -type(seqid() :: non_neg_integer()). --export_type([seqid/0]). - %%-------------------------------------------------------------------- %% APIs %%-------------------------------------------------------------------- diff --git a/src/emqx_stats.erl b/src/emqx_stats.erl index 8db14753e..d3454c6f2 100644 --- a/src/emqx_stats.erl +++ b/src/emqx_stats.erl @@ -51,18 +51,18 @@ , code_change/3 ]). +-export_type([stats/0]). + -record(update, {name, countdown, interval, func}). -record(state, { - timer :: reference(), + timer :: reference(), updates :: [#update{}], tick_ms :: timeout() }). -type(stats() :: list({atom(), non_neg_integer()})). --export_type([stats/0]). - %% Connection stats -define(CONNECTION_STATS, [ 'connections.count', % current connections diff --git a/src/emqx_topic.erl b/src/emqx_topic.erl index f3fccd70b..86cbcf547 100644 --- a/src/emqx_topic.erl +++ b/src/emqx_topic.erl @@ -35,14 +35,18 @@ , parse/2 ]). +-export_type([ group/0 + , topic/0 + , word/0 + , triple/0 + ]). + -type(group() :: binary()). -type(topic() :: binary()). -type(word() :: '' | '+' | '#' | binary()). -type(words() :: list(word())). -opaque(triple() :: {root | binary(), word(), binary()}). --export_type([group/0, topic/0, word/0, triple/0]). - -define(MAX_TOPIC_LEN, 4096). %%-------------------------------------------------------------------- @@ -232,3 +236,4 @@ parse(Topic, Options = #{qos := QoS}) -> {Topic, Options#{rc => QoS}}; parse(Topic, Options) -> {Topic, Options}. + diff --git a/src/emqx_zone.erl b/src/emqx_zone.erl index c0ad66312..c4721485b 100644 --- a/src/emqx_zone.erl +++ b/src/emqx_zone.erl @@ -45,9 +45,13 @@ , code_change/3 ]). +-export_type([zone/0]). + %% dummy state -record(state, {}). +-type(zone() :: atom()). + -define(TAB, ?MODULE). -define(SERVER, ?MODULE). -define(KEY(Zone, Key), {?MODULE, Zone, Key}). @@ -60,13 +64,13 @@ start_link() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). --spec(get_env(maybe(emqx_types:zone()), atom()) -> maybe(term())). +-spec(get_env(maybe(zone()), atom()) -> maybe(term())). get_env(undefined, Key) -> emqx_config:get_env(Key); get_env(Zone, Key) -> get_env(Zone, Key, undefined). --spec(get_env(maybe(emqx_types:zone()), atom(), term()) -> maybe(term())). +-spec(get_env(maybe(zone()), atom(), term()) -> maybe(term())). get_env(undefined, Key, Def) -> emqx_config:get_env(Key, Def); get_env(Zone, Key, Def) -> @@ -75,7 +79,7 @@ get_env(Zone, Key, Def) -> emqx_config:get_env(Key, Def) end. --spec(set_env(emqx_types:zone(), atom(), term()) -> ok). +-spec(set_env(zone(), atom(), term()) -> ok). set_env(Zone, Key, Val) -> gen_server:cast(?SERVER, {set_env, Zone, Key, Val}). From 72331d8ad0b0875f34c5beb5c1734e50d98c7f19 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 25 Jun 2019 14:46:21 +0800 Subject: [PATCH 08/33] Move emqx_mqtt_types module to emqx_mqtt --- src/{emqx_mqtt_types.erl => emqx_mqtt.erl} | 27 +++++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) rename src/{emqx_mqtt_types.erl => emqx_mqtt.erl} (78%) diff --git a/src/emqx_mqtt_types.erl b/src/emqx_mqtt.erl similarity index 78% rename from src/emqx_mqtt_types.erl rename to src/emqx_mqtt.erl index 2fe174d11..bd10bce40 100644 --- a/src/emqx_mqtt_types.erl +++ b/src/emqx_mqtt.erl @@ -14,15 +14,30 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_mqtt_types). +%% MQTT Types +-module(emqx_mqtt). -include("emqx_mqtt.hrl"). --export_type([version/0, qos/0, qos_name/0]). --export_type([connack/0, reason_code/0]). --export_type([properties/0, subopts/0]). +-export_type([ version/0 + , qos/0 + , qos_name/0 + ]). + +-export_type([ connack/0 + , reason_code/0 + ]). + +-export_type([ properties/0 + , subopts/0 + ]). + -export_type([topic_filters/0]). --export_type([packet_id/0, packet_type/0, packet/0]). + +-export_type([ packet_id/0 + , packet_type/0 + , packet/0 + ]). -type(qos() :: ?QOS_0 | ?QOS_1 | ?QOS_2). -type(version() :: ?MQTT_PROTO_V3 | ?MQTT_PROTO_V4 | ?MQTT_PROTO_V5). @@ -40,6 +55,6 @@ qos := qos(), rc => reason_code() }). --type(topic_filters() :: [{emqx_topic:topic(), subopts()}]). +-type(topic_filters() :: list({emqx_topic:topic(), subopts()})). -type(packet() :: #mqtt_packet{}). From 242a1ae45325515a6a6f24482121495c59919117 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 25 Jun 2019 14:47:42 +0800 Subject: [PATCH 09/33] Use emqx_mqtt:qos() type --- src/emqx_bridge.erl | 2 +- src/emqx_bridge_connect.erl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/emqx_bridge.erl b/src/emqx_bridge.erl index 37791ac3a..ee912d993 100644 --- a/src/emqx_bridge.erl +++ b/src/emqx_bridge.erl @@ -109,7 +109,7 @@ ]). -type id() :: atom() | string() | pid(). --type qos() :: emqx_mqtt_types:qos(). +-type qos() :: emqx_mqtt:qos(). -type config() :: map(). -type batch() :: [emqx_bridge_msg:exp_msg()]. -type ack_ref() :: term(). diff --git a/src/emqx_bridge_connect.erl b/src/emqx_bridge_connect.erl index 28fd585b9..29e22c36b 100644 --- a/src/emqx_bridge_connect.erl +++ b/src/emqx_bridge_connect.erl @@ -29,7 +29,7 @@ -type batch() :: emqx_protal:batch(). -type ack_ref() :: emqx_bridge:ack_ref(). -type topic() :: emqx_topic:topic(). --type qos() :: emqx_mqtt_types:qos(). +-type qos() :: emqx_mqtt:qos(). -include("logger.hrl"). From c4eb28351702f615856cc2e574b4f5447e31736c Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 25 Jun 2019 14:50:51 +0800 Subject: [PATCH 10/33] Add spec for validate/1 function --- src/emqx_packet.erl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/emqx_packet.erl b/src/emqx_packet.erl index a231ecd45..ea917c8bf 100644 --- a/src/emqx_packet.erl +++ b/src/emqx_packet.erl @@ -46,6 +46,7 @@ type_name(Type) when Type > ?RESERVED andalso Type =< ?AUTH -> %% Validate MQTT Packet %%-------------------------------------------------------------------- +-spec(validate(emqx_mqtt_types:packet()) -> true). validate(?SUBSCRIBE_PACKET(_PacketId, _Properties, [])) -> error(topic_filters_invalid); validate(?SUBSCRIBE_PACKET(PacketId, Properties, TopicFilters)) -> @@ -112,7 +113,8 @@ validate_qos(QoS) when ?QOS_0 =< QoS, QoS =< ?QOS_2 -> validate_qos(_) -> error(bad_qos). %% @doc From message to packet --spec(from_message(emqx_mqtt_types:packet_id(), emqx_types:message()) -> emqx_mqtt_types:packet()). +-spec(from_message(emqx_mqtt_types:packet_id(), emqx_types:message()) + -> emqx_mqtt_types:packet()). from_message(PacketId, #message{qos = QoS, flags = Flags, headers = Headers, topic = Topic, payload = Payload}) -> Flags1 = if Flags =:= undefined -> From 4974eab20e2fd91b8ef7c57e8aa4b1834c17655b Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 25 Jun 2019 14:53:45 +0800 Subject: [PATCH 11/33] Improve the channel design --- src/emqx_broker.erl | 2 +- src/{emqx_connection.erl => emqx_channel.erl} | 187 ++-- src/emqx_frame.erl | 22 +- src/emqx_inflight.erl | 4 +- src/emqx_protocol.erl | 574 +++++------ src/emqx_session.erl | 971 +++++++----------- src/emqx_shared_sub.erl | 10 +- ..._ws_connection.erl => emqx_ws_channel.erl} | 31 +- 8 files changed, 757 insertions(+), 1044 deletions(-) rename src/{emqx_connection.erl => emqx_channel.erl} (77%) rename src/{emqx_ws_connection.erl => emqx_ws_channel.erl} (91%) diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index 6c3b6163a..6e74bcfa4 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -286,7 +286,7 @@ dispatch(Topic, Delivery = #delivery{message = Msg, results = Results}) -> dispatch(SubPid, Topic, Msg) when is_pid(SubPid) -> case erlang:is_process_alive(SubPid) of true -> - SubPid ! {dispatch, Topic, Msg}, + SubPid ! {deliver, Topic, Msg}, 1; false -> 0 end; diff --git a/src/emqx_connection.erl b/src/emqx_channel.erl similarity index 77% rename from src/emqx_connection.erl rename to src/emqx_channel.erl index 65612b0a2..be192f22e 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_channel.erl @@ -14,6 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- +%% MQTT TCP/SSL Channel -module(emqx_channel). -behaviour(gen_statem). @@ -21,6 +22,7 @@ -include("emqx.hrl"). -include("emqx_mqtt.hrl"). -include("logger.hrl"). +-include("types.hrl"). -logger_header("[Channel]"). @@ -32,16 +34,10 @@ , stats/1 ]). --export([ kick/1 - , discard/1 - , takeover/1 - ]). - --export([session/1]). - %% gen_statem callbacks -export([ idle/3 , connected/3 + , disconnected/3 ]). -export([ init/1 @@ -51,28 +47,32 @@ ]). -record(state, { - transport, - socket, - peername, - sockname, - conn_state, - active_n, - proto_state, - parse_state, - gc_state, - keepalive, - rate_limit, - pub_limit, - limit_timer, - enable_stats, - stats_timer, - idle_timeout + transport :: esockd:transport(), + socket :: esockd:sock(), + peername :: {inet:ip_address(), inet:port_number()}, + sockname :: {inet:ip_address(), inet:port_number()}, + conn_state :: running | blocked, + active_n :: pos_integer(), + rate_limit :: maybe(esockd_rate_limit:bucket()), + pub_limit :: maybe(esockd_rate_limit:bucket()), + limit_timer :: maybe(reference()), + serializer :: emqx_frame:serializer(), %% TODO: remove it later. + parse_state :: emqx_frame:parse_state(), + proto_state :: emqx_protocol:protocol(), + gc_state :: emqx_gc:gc_state(), + keepalive :: maybe(reference()), + enable_stats :: boolean(), + stats_timer :: maybe(reference()), + idle_timeout :: timeout() }). -define(ACTIVE_N, 100). -define(HANDLE(T, C, D), handle((T), (C), (D))). +-define(CHAN_STATS, [recv_pkt, recv_msg, send_pkt, send_msg]). -define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt, send_pend]). +-spec(start_link(esockd:transport(), esockd:sock(), proplists:proplist()) + -> {ok, pid()}). start_link(Transport, Socket, Options) -> {ok, proc_lib:spawn_link(?MODULE, init, [{Transport, Socket, Options}])}. @@ -126,28 +126,13 @@ attrs(#state{peername = Peername, stats(CPid) when is_pid(CPid) -> call(CPid, stats); -stats(#state{transport = Transport, - socket = Socket, - proto_state = ProtoState}) -> +stats(#state{transport = Transport, socket = Socket}) -> SockStats = case Transport:getstat(Socket, ?SOCK_STATS) of {ok, Ss} -> Ss; {error, _} -> [] end, - lists:append([SockStats, - emqx_misc:proc_stats(), - emqx_protocol:stats(ProtoState)]). - -kick(CPid) -> - call(CPid, kick). - -discard(CPid) -> - call(CPid, discard). - -takeover(CPid) -> - call(CPid, takeover). - -session(CPid) -> - call(CPid, session). + ChanStats = [{Name, emqx_pd:get_counter(Name)} || Name <- ?CHAN_STATS], + lists:append([SockStats, ChanStats, emqx_misc:proc_stats()]). call(CPid, Req) -> gen_statem:call(CPid, Req, infinity). @@ -166,23 +151,15 @@ init({Transport, RawSocket, Options}) -> RateLimit = init_limiter(proplists:get_value(rate_limit, Options)), PubLimit = init_limiter(emqx_zone:get_env(Zone, publish_limit)), ActiveN = proplists:get_value(active_n, Options, ?ACTIVE_N), - SendFun = fun(Packet, Opts) -> - Data = emqx_frame:serialize(Packet, Opts), - case Transport:async_send(Socket, Data) of - ok -> {ok, Data}; - {error, Reason} -> - {error, Reason} - end - end, + MaxSize = emqx_zone:get_env(Zone, max_packet_size, ?MAX_PACKET_SIZE), + ParseState = emqx_frame:initial_parse_state(#{max_size => MaxSize}), ProtoState = emqx_protocol:init(#{peername => Peername, sockname => Sockname, peercert => Peercert, - sendfun => SendFun, conn_mod => ?MODULE}, Options), - MaxSize = emqx_zone:get_env(Zone, max_packet_size, ?MAX_PACKET_SIZE), - ParseState = emqx_frame:initial_parse_state(#{max_size => MaxSize}), GcPolicy = emqx_zone:get_env(Zone, force_gc_policy, false), GcState = emqx_gc:init(GcPolicy), + ok = emqx_misc:init_proc_mng_policy(Zone), EnableStats = emqx_zone:get_env(Zone, enable_stats, true), IdleTimout = emqx_zone:get_env(Zone, idle_timeout, 30000), State = #state{transport = Transport, @@ -192,13 +169,12 @@ init({Transport, RawSocket, Options}) -> active_n = ActiveN, rate_limit = RateLimit, pub_limit = PubLimit, - proto_state = ProtoState, parse_state = ParseState, + proto_state = ProtoState, gc_state = GcState, enable_stats = EnableStats, idle_timeout = IdleTimout }, - ok = emqx_misc:init_proc_mng_policy(Zone), gen_statem:enter_loop(?MODULE, [{hibernate_after, 2 * IdleTimout}], idle, State, self(), [IdleTimout]). @@ -218,12 +194,17 @@ idle(enter, _, State) -> keep_state_and_data; idle(timeout, _Timeout, State) -> - {stop, idle_timeout, State}; + stop(idle_timeout, State); + +idle(cast, {incoming, Packet = ?CONNECT_PACKET(ConnVar)}, State) -> + #mqtt_packet_connect{proto_ver = ProtoVer} = ConnVar, + Serializer = emqx_frame:init_serializer(#{version => ProtoVer}), + NState = State#state{serializer = Serializer}, + handle_incoming(Packet, fun(St) -> {next_state, connected, St} end, NState); idle(cast, {incoming, Packet}, State) -> - handle_incoming(Packet, fun(NState) -> - {next_state, connected, NState} - end, State); + ?LOG(warning, "Unexpected incoming: ~p", [Packet]), + shutdown(unexpected_incoming_packet, State); idle(EventType, Content, State) -> ?HANDLE(EventType, Content, State). @@ -235,18 +216,23 @@ connected(enter, _, _State) -> %% What to do? keep_state_and_data; -%% Handle Input +connected(cast, {incoming, Packet = ?PACKET(?CONNECT)}, State) -> + ?LOG(warning, "Unexpected connect: ~p", [Packet]), + shutdown(unexpected_incoming_connect, State); + connected(cast, {incoming, Packet = ?PACKET(Type)}, State) -> ok = emqx_metrics:inc_recv(Packet), (Type == ?PUBLISH) andalso emqx_pd:update_counter(incoming_pubs, 1), - handle_incoming(Packet, fun(NState) -> {keep_state, NState} end, State); + handle_incoming(Packet, fun(St) -> {keep_state, St} end, State); -%% Handle Output -connected(info, {deliver, PubOrAck}, State = #state{proto_state = ProtoState}) -> - case emqx_protocol:deliver(PubOrAck, ProtoState) of +%% Handle delivery +connected(info, Devliery = {deliver, _Topic, Msg}, State = #state{proto_state = ProtoState}) -> + case emqx_protocol:handle_out(Devliery, ProtoState) of {ok, NProtoState} -> + {keep_state, State#state{proto_state = NProtoState}}; + {ok, Packet, NProtoState} -> NState = State#state{proto_state = NProtoState}, - {keep_state, maybe_gc(PubOrAck, NState)}; + handle_outgoing(Packet, fun(St) -> {keep_state, St} end, NState); {error, Reason} -> shutdown(Reason, State) end; @@ -281,6 +267,16 @@ connected(info, {keepalive, check}, State = #state{keepalive = KeepAlive}) -> connected(EventType, Content, State) -> ?HANDLE(EventType, Content, State). +%%-------------------------------------------------------------------- +%% Disconnected State + +disconnected(enter, _, _State) -> + %% TODO: What to do? + keep_state_and_data; + +disconnected(EventType, Content, State) -> + ?HANDLE(EventType, Content, State). + %% Handle call handle({call, From}, info, State) -> reply(From, info(State), State); @@ -299,9 +295,6 @@ handle({call, From}, discard, State) -> ok = gen_statem:reply(From, ok), shutdown(discard, State); -handle({call, From}, session, State = #state{proto_state = ProtoState}) -> - reply(From, emqx_protocol:session(ProtoState), State); - handle({call, From}, Req, State) -> ?LOG(error, "Unexpected call: ~p", [Req]), reply(From, ignored, State); @@ -312,7 +305,8 @@ handle(cast, Msg, State) -> {keep_state, State}; %% Handle Incoming -handle(info, {Inet, _Sock, Data}, State) when Inet == tcp; Inet == ssl -> +handle(info, {Inet, _Sock, Data}, State) when Inet == tcp; + Inet == ssl -> Oct = iolist_size(Data), ?LOG(debug, "RECV ~p", [Data]), emqx_pd:update_counter(incoming_bytes, Oct), @@ -350,7 +344,7 @@ handle(info, {inet_reply, _Sock, {error, Reason}}, State) -> handle(info, {timeout, Timer, emit_stats}, State = #state{stats_timer = Timer, proto_state = ProtoState, - gc_state = GcState}) -> + gc_state = GcState}) -> ClientId = emqx_protocol:client_id(ProtoState), emqx_cm:set_conn_stats(ClientId, stats(State)), NState = State#state{stats_timer = undefined}, @@ -390,15 +384,9 @@ terminate(Reason, _StateName, #state{transport = Transport, keepalive = KeepAlive, proto_state = ProtoState}) -> ?LOG(debug, "Terminated for ~p", [Reason]), - Transport:fast_close(Socket), - emqx_keepalive:cancel(KeepAlive), - case {ProtoState, Reason} of - {undefined, _} -> ok; - {_, {shutdown, Error}} -> - emqx_protocol:terminate(Error, ProtoState); - {_, Reason} -> - emqx_protocol:terminate(Reason, ProtoState) - end. + ok = Transport:fast_close(Socket), + ok = emqx_keepalive:cancel(KeepAlive), + emqx_protocol:terminate(Reason, ProtoState). %%-------------------------------------------------------------------- %% Process incoming data @@ -431,10 +419,16 @@ next_events(Packet) -> %%-------------------------------------------------------------------- %% Handle incoming packet -handle_incoming(Packet, SuccFun, State = #state{proto_state = ProtoState}) -> - case emqx_protocol:received(Packet, ProtoState) of +handle_incoming(Packet = ?PACKET(Type), SuccFun, + State = #state{proto_state = ProtoState}) -> + _ = inc_incoming_stats(Type), + ?LOG(debug, "RECV ~s", [emqx_packet:format(Packet)]), + case emqx_protocol:handle_in(Packet, ProtoState) of {ok, NProtoState} -> SuccFun(State#state{proto_state = NProtoState}); + {ok, OutPacket, NProtoState} -> + handle_outgoing(OutPacket, SuccFun, + State#state{proto_state = NProtoState}); {error, Reason} -> shutdown(Reason, State); {error, Reason, NProtoState} -> @@ -443,6 +437,22 @@ handle_incoming(Packet, SuccFun, State = #state{proto_state = ProtoState}) -> stop(Error, State#state{proto_state = NProtoState}) end. +%%-------------------------------------------------------------------- +%% Handle outgoing packet + +handle_outgoing(Packet = ?PACKET(Type), SuccFun, + State = #state{transport = Transport, + socket = Socket, + serializer = Serializer}) -> + _ = inc_outgoing_stats(Type), + ?LOG(debug, "SEND ~s", [emqx_packet:format(Packet)]), + Data = Serializer(Packet), + case Transport:async_send(Socket, Data) of + ok -> SuccFun(State); + {error, Reason} -> + shutdown(Reason, State) + end. + %%-------------------------------------------------------------------- %% Ensure rate limit @@ -465,6 +475,12 @@ ensure_rate_limit([{Rl, Pos, Cnt}|Limiters], State) -> setelement(Pos, State#state{conn_state = blocked, limit_timer = TRef}, Rl1) end. +%% start_keepalive(0, _PState) -> +%% ignore; +%% start_keepalive(Secs, #pstate{zone = Zone}) when Secs > 0 -> +%% Backoff = emqx_zone:get_env(Zone, keepalive_backoff, 0.75), +%% self() ! {keepalive, start, round(Secs * Backoff)}. + %%-------------------------------------------------------------------- %% Activate socket @@ -479,6 +495,17 @@ activate_socket(#state{transport = Transport, socket = Socket, active_n = N}) -> ok end. +%%-------------------------------------------------------------------- +%% Inc incoming/outgoing stats + +inc_incoming_stats(Type) -> + emqx_pd:update_counter(recv_pkt, 1), + Type =:= ?PUBLISH andalso emqx_pd:update_counter(recv_msg, 1). + +inc_outgoing_stats(Type) -> + emqx_pd:update_counter(send_pkt, 1), + Type =:= ?PUBLISH andalso emqx_pd:update_counter(send_msg, 1). + %%-------------------------------------------------------------------- %% Ensure stats timer diff --git a/src/emqx_frame.erl b/src/emqx_frame.erl index a4ef34840..3c46e50db 100644 --- a/src/emqx_frame.erl +++ b/src/emqx_frame.erl @@ -21,6 +21,7 @@ -export([ initial_parse_state/0 , initial_parse_state/1 + , init_serializer/1 ]). -export([ parse/1 @@ -29,22 +30,22 @@ , serialize/2 ]). +-export_type([ options/0 + , parse_state/0 + , parse_result/0 + ]). + -type(options() :: #{max_size => 1..?MAX_PACKET_SIZE, - version => emqx_mqtt_types:version() + version => emqx_mqtt:version() }). -opaque(parse_state() :: {none, options()} | {more, cont_fun()}). -opaque(parse_result() :: {ok, parse_state()} - | {ok, emqx_mqtt_types:packet(), binary(), parse_state()}). + | {ok, emqx_mqtt:packet(), binary(), parse_state()}). -type(cont_fun() :: fun((binary()) -> parse_result())). --export_type([ options/0 - , parse_state/0 - , parse_result/0 - ]). - -define(none(Opts), {none, Opts}). -define(more(Cont), {more, Cont}). -define(DEFAULT_OPTIONS, @@ -385,11 +386,14 @@ parse_binary_data(<>) -> %% Serialize MQTT Packet %%-------------------------------------------------------------------- --spec(serialize(emqx_mqtt_types:packet()) -> iodata()). +init_serializer(Options) -> + fun(Packet) -> serialize(Packet, Options) end. + +-spec(serialize(emqx_mqtt:packet()) -> iodata()). serialize(Packet) -> serialize(Packet, ?DEFAULT_OPTIONS). --spec(serialize(emqx_mqtt_types:packet(), options()) -> iodata()). +-spec(serialize(emqx_mqtt:packet(), options()) -> iodata()). serialize(#mqtt_packet{header = Header, variable = Variable, payload = Payload}, Options) when is_map(Options) -> diff --git a/src/emqx_inflight.erl b/src/emqx_inflight.erl index 4522b3510..88c4796c4 100644 --- a/src/emqx_inflight.erl +++ b/src/emqx_inflight.erl @@ -33,6 +33,8 @@ , window/1 ]). +-export_type([inflight/0]). + -type(key() :: term()). -type(max_size() :: pos_integer()). @@ -43,8 +45,6 @@ -define(Inflight(MaxSize, Tree), {?MODULE, MaxSize, (Tree)}). --export_type([inflight/0]). - %%-------------------------------------------------------------------- %% APIs %%-------------------------------------------------------------------- diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index f7a337541..699963644 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -14,11 +14,13 @@ %% limitations under the License. %%-------------------------------------------------------------------- +%% MQTT Protocol -module(emqx_protocol). -include("emqx.hrl"). -include("emqx_mqtt.hrl"). -include("logger.hrl"). +-include("types.hrl"). -logger_header("[Protocol]"). @@ -27,53 +29,49 @@ , attr/2 , caps/1 , caps/2 - , stats/1 , client_id/1 , credentials/1 , session/1 ]). -export([ init/2 - , received/2 - , process/2 - , deliver/2 - , send/2 + , handle_in/2 + , handle_out/2 + , handle_timeout/3 , terminate/2 ]). --record(pstate, { - zone, +-export_type([protocol/0]). + +-record(protocol, { + zone :: emqx_zone:zone(), + conn_mod :: module(), sendfun, sockname, peername, peercert, - proto_ver, + proto_ver :: emqx_mqtt:version(), proto_name, - client_id, + client_id :: maybe(emqx_types:client_id()), is_assigned, + username :: maybe(emqx_types:username()), conn_props, ack_props, - username, - session, + credentials :: map(), + session :: maybe(emqx_session:session()), clean_start, topic_aliases, will_topic, will_msg, keepalive, - is_bridge, - recv_stats, - send_stats, - connected, - connected_at, + is_bridge :: boolean(), + connected :: boolean(), + connected_at :: erlang:timestamp(), topic_alias_maximum, - conn_mod, - credentials, ws_cookie - }). + }). --opaque(state() :: #pstate{}). - --export_type([state/0]). +-opaque(protocol() :: #protocol{}). -ifdef(TEST). -compile(export_all). @@ -86,33 +84,31 @@ %% Init %%-------------------------------------------------------------------- --spec(init(map(), list()) -> state()). +-spec(init(map(), list()) -> protocol()). init(SocketOpts = #{sockname := Sockname, peername := Peername, - peercert := Peercert, - sendfun := SendFun}, Options) -> + peercert := Peercert}, Options) -> Zone = proplists:get_value(zone, Options), - #pstate{zone = Zone, - sendfun = SendFun, - sockname = Sockname, - peername = Peername, - peercert = Peercert, - proto_ver = ?MQTT_PROTO_V4, - proto_name = <<"MQTT">>, - client_id = <<>>, - is_assigned = false, - username = init_username(Peercert, Options), - clean_start = false, - topic_aliases = #{}, - is_bridge = false, - recv_stats = #{msg => 0, pkt => 0}, - send_stats = #{msg => 0, pkt => 0}, - connected = false, - %% TODO: ...? - topic_alias_maximum = #{to_client => 0, from_client => 0}, - conn_mod = maps:get(conn_mod, SocketOpts, undefined), - credentials = #{}, - ws_cookie = maps:get(ws_cookie, SocketOpts, undefined)}. + #protocol{zone = Zone, + %%sendfun = SendFun, + sockname = Sockname, + peername = Peername, + peercert = Peercert, + proto_ver = ?MQTT_PROTO_V4, + proto_name = <<"MQTT">>, + client_id = <<>>, + is_assigned = false, + %%conn_pid = self(), + username = init_username(Peercert, Options), + clean_start = false, + topic_aliases = #{}, + is_bridge = false, + connected = false, + topic_alias_maximum = #{to_client => 0, from_client => 0}, + conn_mod = maps:get(conn_mod, SocketOpts, undefined), + credentials = #{}, + ws_cookie = maps:get(ws_cookie, SocketOpts, undefined) + }. init_username(Peercert, Options) -> case proplists:get_value(peer_cert_as_username, Options) of @@ -122,8 +118,8 @@ init_username(Peercert, Options) -> _ -> undefined end. -set_username(Username, PState = #pstate{username = undefined}) -> - PState#pstate{username = Username}; +set_username(Username, PState = #protocol{username = undefined}) -> + PState#protocol{username = Username}; set_username(_Username, PState) -> PState. @@ -131,12 +127,12 @@ set_username(_Username, PState) -> %% API %%-------------------------------------------------------------------- -info(PState = #pstate{zone = Zone, - conn_props = ConnProps, - ack_props = AckProps, - session = Session, - topic_aliases = Aliases, - will_msg = WillMsg}) -> +info(PState = #protocol{zone = Zone, + conn_props = ConnProps, + ack_props = AckProps, + session = Session, + topic_aliases = Aliases, + will_msg = WillMsg}) -> maps:merge(attrs(PState), #{conn_props => ConnProps, ack_props => AckProps, session => Session, @@ -145,19 +141,19 @@ info(PState = #pstate{zone = Zone, enable_acl => emqx_zone:get_env(Zone, enable_acl, false) }). -attrs(#pstate{zone = Zone, - client_id = ClientId, - username = Username, - peername = Peername, - peercert = Peercert, - clean_start = CleanStart, - proto_ver = ProtoVer, - proto_name = ProtoName, - keepalive = Keepalive, - is_bridge = IsBridge, - connected_at = ConnectedAt, - conn_mod = ConnMod, - credentials = Credentials}) -> +attrs(#protocol{zone = Zone, + client_id = ClientId, + username = Username, + peername = Peername, + peercert = Peercert, + clean_start = CleanStart, + proto_ver = ProtoVer, + proto_name = ProtoName, + keepalive = Keepalive, + is_bridge = IsBridge, + connected_at = ConnectedAt, + conn_mod = ConnMod, + credentials = Credentials}) -> #{zone => Zone, client_id => ClientId, username => Username, @@ -173,25 +169,25 @@ attrs(#pstate{zone = Zone, credentials => Credentials }. -attr(proto_ver, #pstate{proto_ver = ProtoVer}) -> +attr(proto_ver, #protocol{proto_ver = ProtoVer}) -> ProtoVer; -attr(max_inflight, #pstate{proto_ver = ?MQTT_PROTO_V5, conn_props = ConnProps}) -> +attr(max_inflight, #protocol{proto_ver = ?MQTT_PROTO_V5, conn_props = ConnProps}) -> get_property('Receive-Maximum', ConnProps, 65535); -attr(max_inflight, #pstate{zone = Zone}) -> +attr(max_inflight, #protocol{zone = Zone}) -> emqx_zone:get_env(Zone, max_inflight, 65535); -attr(expiry_interval, #pstate{proto_ver = ?MQTT_PROTO_V5, conn_props = ConnProps}) -> +attr(expiry_interval, #protocol{proto_ver = ?MQTT_PROTO_V5, conn_props = ConnProps}) -> get_property('Session-Expiry-Interval', ConnProps, 0); -attr(expiry_interval, #pstate{zone = Zone, clean_start = CleanStart}) -> +attr(expiry_interval, #protocol{zone = Zone, clean_start = CleanStart}) -> case CleanStart of true -> 0; false -> emqx_zone:get_env(Zone, session_expiry_interval, 16#ffffffff) end; -attr(topic_alias_maximum, #pstate{proto_ver = ?MQTT_PROTO_V5, conn_props = ConnProps}) -> +attr(topic_alias_maximum, #protocol{proto_ver = ?MQTT_PROTO_V5, conn_props = ConnProps}) -> get_property('Topic-Alias-Maximum', ConnProps, 0); -attr(topic_alias_maximum, #pstate{zone = Zone}) -> +attr(topic_alias_maximum, #protocol{zone = Zone}) -> emqx_zone:get_env(Zone, max_topic_alias, 0); attr(Name, PState) -> - Attrs = lists:zip(record_info(fields, pstate), tl(tuple_to_list(PState))), + Attrs = lists:zip(record_info(fields, protocol), tl(tuple_to_list(PState))), case lists:keyfind(Name, 1, Attrs) of {_, Value} -> Value; false -> undefined @@ -200,13 +196,13 @@ attr(Name, PState) -> caps(Name, PState) -> maps:get(Name, caps(PState)). -caps(#pstate{zone = Zone}) -> +caps(#protocol{zone = Zone}) -> emqx_mqtt_caps:get_caps(Zone). -client_id(#pstate{client_id = ClientId}) -> +client_id(#protocol{client_id = ClientId}) -> ClientId. -credentials(#pstate{zone = Zone, +credentials(#protocol{zone = Zone, client_id = ClientId, username = Username, sockname = Sockname, @@ -232,79 +228,61 @@ keepsafety(Credentials) -> (cn, _) -> false; (_, _) -> true end, Credentials). -stats(#pstate{recv_stats = #{pkt := RecvPkt, msg := RecvMsg}, - send_stats = #{pkt := SendPkt, msg := SendMsg}}) -> - [{recv_pkt, RecvPkt}, - {recv_msg, RecvMsg}, - {send_pkt, SendPkt}, - {send_msg, SendMsg}]. - -session(#pstate{session = Session}) -> +session(#protocol{session = Session}) -> Session. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Packet Received -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- set_protover(?CONNECT_PACKET(#mqtt_packet_connect{proto_ver = ProtoVer}), PState) -> - PState#pstate{proto_ver = ProtoVer}; + PState#protocol{proto_ver = ProtoVer}; set_protover(_Packet, PState) -> PState. --spec(received(emqx_mqtt_types:packet(), state()) - -> {ok, state()} - | {error, term()} - | {error, term(), state()} - | {stop, term(), state()}). -received(?PACKET(Type), PState = #pstate{connected = false}) when Type =/= ?CONNECT -> +handle_in(?PACKET(Type), PState = #protocol{connected = false}) when Type =/= ?CONNECT -> {error, proto_not_connected, PState}; -received(?PACKET(?CONNECT), PState = #pstate{connected = true}) -> +handle_in(?PACKET(?CONNECT), PState = #protocol{connected = true}) -> {error, proto_unexpected_connect, PState}; -received(Packet = ?PACKET(Type), PState) -> - trace(recv, Packet), +handle_in(Packet = ?PACKET(_Type), PState) -> PState1 = set_protover(Packet, PState), try emqx_packet:validate(Packet) of true -> case preprocess_properties(Packet, PState1) of {ok, Packet1, PState2} -> - process(Packet1, inc_stats(recv, Type, PState2)); + process(Packet1, PState2); {error, ReasonCode} -> - {error, ReasonCode, PState1} + handle_out({disconnect, ReasonCode}, PState1) end catch error:protocol_error -> - deliver({disconnect, ?RC_PROTOCOL_ERROR}, PState1), - {error, protocol_error, PState}; + handle_out({disconnect, ?RC_PROTOCOL_ERROR}, PState1); error:subscription_identifier_invalid -> - deliver({disconnect, ?RC_SUBSCRIPTION_IDENTIFIERS_NOT_SUPPORTED}, PState1), - {error, subscription_identifier_invalid, PState1}; + handle_out({disconnect, ?RC_SUBSCRIPTION_IDENTIFIERS_NOT_SUPPORTED}, PState1); error:topic_alias_invalid -> - deliver({disconnect, ?RC_TOPIC_ALIAS_INVALID}, PState1), - {error, topic_alias_invalid, PState1}; + handle_out({disconnect, ?RC_TOPIC_ALIAS_INVALID}, PState1); error:topic_filters_invalid -> - deliver({disconnect, ?RC_TOPIC_FILTER_INVALID}, PState1), - {error, topic_filters_invalid, PState1}; + handle_out({disconnect, ?RC_TOPIC_FILTER_INVALID}, PState1); error:topic_name_invalid -> - deliver({disconnect, ?RC_TOPIC_FILTER_INVALID}, PState1), - {error, topic_filters_invalid, PState1}; - error:Reason -> - deliver({disconnect, ?RC_MALFORMED_PACKET}, PState1), - {error, Reason, PState1} + handle_out({disconnect, ?RC_TOPIC_FILTER_INVALID}, PState1); + error:_Reason -> + %% TODO: {error, Reason, PState1} + handle_out({disconnect, ?RC_MALFORMED_PACKET}, PState1) end. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Preprocess MQTT Properties -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- preprocess_properties(Packet = #mqtt_packet{ variable = #mqtt_packet_connect{ properties = #{'Topic-Alias-Maximum' := ToClient} } }, - PState = #pstate{topic_alias_maximum = TopicAliasMaximum}) -> - {ok, Packet, PState#pstate{topic_alias_maximum = TopicAliasMaximum#{to_client => ToClient}}}; + PState = #protocol{topic_alias_maximum = TopicAliasMaximum}) -> + {ok, Packet, PState#protocol{topic_alias_maximum = TopicAliasMaximum#{to_client => ToClient}}}; %% Subscription Identifier preprocess_properties(Packet = #mqtt_packet{ @@ -313,7 +291,7 @@ preprocess_properties(Packet = #mqtt_packet{ topic_filters = TopicFilters } }, - PState = #pstate{proto_ver = ?MQTT_PROTO_V5}) -> + PState = #protocol{proto_ver = ?MQTT_PROTO_V5}) -> TopicFilters1 = [{Topic, SubOpts#{subid => SubId}} || {Topic, SubOpts} <- TopicFilters], {ok, Packet#mqtt_packet{variable = Subscribe#mqtt_packet_subscribe{topic_filters = TopicFilters1}}, PState}; @@ -323,7 +301,6 @@ preprocess_properties(#mqtt_packet{ properties = #{'Topic-Alias' := 0}} }, PState) -> - deliver({disconnect, ?RC_TOPIC_ALIAS_INVALID}, PState), {error, ?RC_TOPIC_ALIAS_INVALID}; preprocess_properties(Packet = #mqtt_packet{ @@ -331,7 +308,7 @@ preprocess_properties(Packet = #mqtt_packet{ topic_name = <<>>, properties = #{'Topic-Alias' := AliasId}} }, - PState = #pstate{proto_ver = ?MQTT_PROTO_V5, + PState = #protocol{proto_ver = ?MQTT_PROTO_V5, topic_aliases = Aliases, topic_alias_maximum = #{from_client := TopicAliasMaximum}}) -> case AliasId =< TopicAliasMaximum of @@ -339,7 +316,6 @@ preprocess_properties(Packet = #mqtt_packet{ {ok, Packet#mqtt_packet{variable = Publish#mqtt_packet_publish{ topic_name = maps:get(AliasId, Aliases, <<>>)}}, PState}; false -> - deliver({disconnect, ?RC_TOPIC_ALIAS_INVALID}, PState), {error, ?RC_TOPIC_ALIAS_INVALID} end; @@ -348,23 +324,22 @@ preprocess_properties(Packet = #mqtt_packet{ topic_name = Topic, properties = #{'Topic-Alias' := AliasId}} }, - PState = #pstate{proto_ver = ?MQTT_PROTO_V5, + PState = #protocol{proto_ver = ?MQTT_PROTO_V5, topic_aliases = Aliases, topic_alias_maximum = #{from_client := TopicAliasMaximum}}) -> case AliasId =< TopicAliasMaximum of true -> - {ok, Packet, PState#pstate{topic_aliases = maps:put(AliasId, Topic, Aliases)}}; + {ok, Packet, PState#protocol{topic_aliases = maps:put(AliasId, Topic, Aliases)}}; false -> - deliver({disconnect, ?RC_TOPIC_ALIAS_INVALID}, PState), {error, ?RC_TOPIC_ALIAS_INVALID} end; preprocess_properties(Packet, PState) -> {ok, Packet, PState}. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Process MQTT Packet -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- process(?CONNECT_PACKET( #mqtt_packet_connect{proto_name = ProtoName, @@ -381,7 +356,7 @@ process(?CONNECT_PACKET( %% Msg -> emqx_mountpoint:mount(MountPoint, Msg) PState0 = maybe_use_username_as_clientid(ClientId, set_username(Username, - PState#pstate{proto_ver = ProtoVer, + PState#protocol{proto_ver = ProtoVer, proto_name = ProtoName, clean_start = CleanStart, keepalive = Keepalive, @@ -389,120 +364,115 @@ process(?CONNECT_PACKET( is_bridge = IsBridge, connected_at = os:timestamp()})), - NewClientId = PState0#pstate.client_id, + NewClientId = PState0#protocol.client_id, emqx_logger:set_metadata_client_id(NewClientId), Credentials = credentials(PState0), - PState1 = PState0#pstate{credentials = Credentials}, + PState1 = PState0#protocol{credentials = Credentials}, connack( case check_connect(ConnPkt, PState1) of ok -> case emqx_access_control:authenticate(Credentials#{password => Password}) of {ok, Credentials0} -> PState3 = maybe_assign_client_id(PState1), - emqx_logger:set_metadata_client_id(PState3#pstate.client_id), + emqx_logger:set_metadata_client_id(PState3#protocol.client_id), %% Open session SessAttrs = #{will_msg => make_will_msg(ConnPkt)}, case try_open_session(SessAttrs, PState3) of {ok, Session, SP} -> - PState4 = PState3#pstate{session = Session, connected = true, + PState4 = PState3#protocol{session = Session, connected = true, credentials = keepsafety(Credentials0)}, ok = emqx_cm:register_channel(client_id(PState4)), - ok = emqx_cm:set_conn_attrs(client_id(PState4), attrs(PState4)), + true = emqx_cm:set_conn_attrs(client_id(PState4), attrs(PState4)), %% Start keepalive start_keepalive(Keepalive, PState4), %% Success {?RC_SUCCESS, SP, PState4}; {error, Error} -> ?LOG(error, "Failed to open session: ~p", [Error]), - {?RC_UNSPECIFIED_ERROR, PState1#pstate{credentials = Credentials0}} + {?RC_UNSPECIFIED_ERROR, PState1#protocol{credentials = Credentials0}} end; {error, Reason} -> ?LOG(warning, "Client ~s (Username: '~s') login failed for ~p", [NewClientId, Username, Reason]), - {emqx_reason_codes:connack_error(Reason), PState1#pstate{credentials = Credentials}} + {emqx_reason_codes:connack_error(Reason), PState1#protocol{credentials = Credentials}} end; {error, ReasonCode} -> {ReasonCode, PState1} end); -process(Packet = ?PUBLISH_PACKET(?QOS_0, Topic, _PacketId, _Payload), PState = #pstate{zone = Zone}) -> +process(Packet = ?PUBLISH_PACKET(?QOS_0, Topic, _PacketId, _Payload), PState = #protocol{zone = Zone}) -> case check_publish(Packet, PState) of ok -> do_publish(Packet, PState); {error, ReasonCode} -> ?LOG(warning, "Cannot publish qos0 message to ~s for ~s", [Topic, emqx_reason_codes:text(ReasonCode)]), + %% TODO: ... AclDenyAction = emqx_zone:get_env(Zone, acl_deny_action, ignore), do_acl_deny_action(AclDenyAction, Packet, ReasonCode, PState) end; -process(Packet = ?PUBLISH_PACKET(?QOS_1, Topic, PacketId, _Payload), PState = #pstate{zone = Zone}) -> +process(Packet = ?PUBLISH_PACKET(?QOS_1, Topic, PacketId, _Payload), PState = #protocol{zone = Zone}) -> case check_publish(Packet, PState) of ok -> do_publish(Packet, PState); {error, ReasonCode} -> - ?LOG(warning, "Cannot publish qos1 message to ~s for ~s", [Topic, emqx_reason_codes:text(ReasonCode)]), - case deliver({puback, PacketId, ReasonCode}, PState) of - {ok, PState1} -> - AclDenyAction = emqx_zone:get_env(Zone, acl_deny_action, ignore), - do_acl_deny_action(AclDenyAction, Packet, ReasonCode, PState1); - Error -> Error - end + ?LOG(warning, "Cannot publish qos1 message to ~s for ~s", + [Topic, emqx_reason_codes:text(ReasonCode)]), + handle_out({puback, PacketId, ReasonCode}, PState) end; -process(Packet = ?PUBLISH_PACKET(?QOS_2, Topic, PacketId, _Payload), PState = #pstate{zone = Zone}) -> +process(Packet = ?PUBLISH_PACKET(?QOS_2, Topic, PacketId, _Payload), PState = #protocol{zone = Zone}) -> case check_publish(Packet, PState) of ok -> do_publish(Packet, PState); {error, ReasonCode} -> ?LOG(warning, "Cannot publish qos2 message to ~s for ~s", [Topic, emqx_reason_codes:text(ReasonCode)]), - case deliver({pubrec, PacketId, ReasonCode}, PState) of - {ok, PState1} -> - AclDenyAction = emqx_zone:get_env(Zone, acl_deny_action, ignore), - do_acl_deny_action(AclDenyAction, Packet, ReasonCode, PState1); - Error -> Error - end + handle_out({pubrec, PacketId, ReasonCode}, PState) end; -process(?PUBACK_PACKET(PacketId, ReasonCode), PState = #pstate{session = Session}) -> - NSession = emqx_session:puback(PacketId, ReasonCode, Session), - {ok, PState#pstate{session = NSession}}; +process(?PUBACK_PACKET(PacketId, ReasonCode), PState = #protocol{session = Session}) -> + case emqx_session:puback(PacketId, ReasonCode, Session) of + {ok, NSession} -> + {ok, PState#protocol{session = NSession}}; + {error, _NotFound} -> + {ok, PState} %% TODO: Fixme later + end; -process(?PUBREC_PACKET(PacketId, ReasonCode), PState = #pstate{session = Session}) -> +process(?PUBREC_PACKET(PacketId, ReasonCode), PState = #protocol{session = Session}) -> case emqx_session:pubrec(PacketId, ReasonCode, Session) of {ok, NSession} -> - send(?PUBREL_PACKET(PacketId), PState#pstate{session = NSession}); + {ok, ?PUBREL_PACKET(PacketId), PState#protocol{session = NSession}}; {error, NotFound} -> - send(?PUBREL_PACKET(PacketId, NotFound), PState) + {ok, ?PUBREL_PACKET(PacketId, NotFound), PState} end; -process(?PUBREL_PACKET(PacketId, ReasonCode), PState = #pstate{session = Session}) -> +process(?PUBREL_PACKET(PacketId, ReasonCode), PState = #protocol{session = Session}) -> case emqx_session:pubrel(PacketId, ReasonCode, Session) of {ok, NSession} -> - send(?PUBCOMP_PACKET(PacketId), PState#pstate{session = NSession}); + {ok, ?PUBCOMP_PACKET(PacketId), PState#protocol{session = NSession}}; {error, NotFound} -> - send(?PUBCOMP_PACKET(PacketId, NotFound), PState) + {ok, ?PUBCOMP_PACKET(PacketId, NotFound), PState} end; -process(?PUBCOMP_PACKET(PacketId, ReasonCode), PState = #pstate{session = Session}) -> +process(?PUBCOMP_PACKET(PacketId, ReasonCode), PState = #protocol{session = Session}) -> case emqx_session:pubcomp(PacketId, ReasonCode, Session) of {ok, NSession} -> - {ok, PState#pstate{session = NSession}}; - {error, _NotFound} -> + {ok, PState#protocol{session = NSession}}; + {error, _NotFound} -> ok %% TODO: How to handle NotFound? - {ok, PState} end; process(Packet = ?SUBSCRIBE_PACKET(PacketId, Properties, RawTopicFilters), - PState = #pstate{zone = Zone, proto_ver = ProtoVer, session = Session, credentials = Credentials}) -> + PState = #protocol{zone = Zone, session = Session, credentials = Credentials}) -> case check_subscribe(parse_topic_filters(?SUBSCRIBE, raw_topic_filters(PState, RawTopicFilters)), PState) of {ok, TopicFilters} -> TopicFilters0 = emqx_hooks:run_fold('client.subscribe', [Credentials], TopicFilters), TopicFilters1 = emqx_mountpoint:mount(mountpoint(Credentials), TopicFilters0), {ok, ReasonCodes, NSession} = emqx_session:subscribe(TopicFilters1, Session), - deliver({suback, PacketId, ReasonCodes}, PState#pstate{session = NSession}); + handle_out({suback, PacketId, ReasonCodes}, PState#protocol{session = NSession}); {error, TopicFilters} -> {SubTopics, ReasonCodes} = lists:foldr(fun({Topic, #{rc := ?RC_SUCCESS}}, {Topics, Codes}) -> @@ -512,119 +482,102 @@ process(Packet = ?SUBSCRIBE_PACKET(PacketId, Properties, RawTopicFilters), end, {[], []}, TopicFilters), ?LOG(warning, "Cannot subscribe ~p for ~p", [SubTopics, [emqx_reason_codes:text(R) || R <- ReasonCodes]]), - case deliver({suback, PacketId, ReasonCodes}, PState) of - {ok, PState1} -> - AclDenyAction = emqx_zone:get_env(Zone, acl_deny_action, ignore), - do_acl_deny_action(AclDenyAction, Packet, ReasonCodes, PState1); - Error -> - Error - end + handle_out({suback, PacketId, ReasonCodes}, PState) end; process(?UNSUBSCRIBE_PACKET(PacketId, Properties, RawTopicFilters), - PState = #pstate{session = Session, credentials = Credentials}) -> + PState = #protocol{session = Session, credentials = Credentials}) -> TopicFilters = emqx_hooks:run_fold('client.unsubscribe', [Credentials], parse_topic_filters(?UNSUBSCRIBE, RawTopicFilters)), TopicFilters1 = emqx_mountpoint:mount(mountpoint(Credentials), TopicFilters), {ok, ReasonCodes, NSession} = emqx_session:unsubscribe(TopicFilters1, Session), - deliver({unsuback, PacketId, ReasonCodes}, PState#pstate{session = NSession}); + handle_out({unsuback, PacketId, ReasonCodes}, PState#protocol{session = NSession}); process(?PACKET(?PINGREQ), PState) -> - send(?PACKET(?PINGRESP), PState); + {ok, ?PACKET(?PINGRESP), PState}; process(?DISCONNECT_PACKET(?RC_SUCCESS, #{'Session-Expiry-Interval' := Interval}), - PState = #pstate{session = Session, conn_props = #{'Session-Expiry-Interval' := OldInterval}}) -> + PState = #protocol{session = Session, conn_props = #{'Session-Expiry-Interval' := OldInterval}}) -> case Interval =/= 0 andalso OldInterval =:= 0 of true -> - deliver({disconnect, ?RC_PROTOCOL_ERROR}, PState), - {error, protocol_error, PState#pstate{will_msg = undefined}}; + handle_out({disconnect, ?RC_PROTOCOL_ERROR}, PState#protocol{will_msg = undefined}); false -> - NSession = emqx_session:update_expiry_interval(Interval, Session), + %% TODO: + %% emqx_session:update_expiry_interval(SPid, Interval), %% Clean willmsg - {stop, normal, PState#pstate{will_msg = undefined, session = NSession}} + {stop, normal, PState#protocol{will_msg = undefined}} end; process(?DISCONNECT_PACKET(?RC_SUCCESS), PState) -> - {stop, normal, PState#pstate{will_msg = undefined}}; + {stop, normal, PState#protocol{will_msg = undefined}}; process(?DISCONNECT_PACKET(_), PState) -> - {stop, {shutdown, abnormal_disconnet}, PState}. + {stop, {shutdown, abnormal_disconnet}, PState}; -%%------------------------------------------------------------------------------ +process(?AUTH_PACKET(), State) -> + %%TODO: implement later. + {ok, State}. + +%%-------------------------------------------------------------------- %% ConnAck --> Client -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- -connack({?RC_SUCCESS, SP, PState = #pstate{credentials = Credentials}}) -> +connack({?RC_SUCCESS, SP, PState = #protocol{credentials = Credentials}}) -> ok = emqx_hooks:run('client.connected', [Credentials, ?RC_SUCCESS, attrs(PState)]), - deliver({connack, ?RC_SUCCESS, sp(SP)}, PState); + handle_out({connack, ?RC_SUCCESS, sp(SP)}, PState); -connack({ReasonCode, PState = #pstate{proto_ver = ProtoVer, credentials = Credentials}}) -> +connack({ReasonCode, PState = #protocol{proto_ver = ProtoVer, credentials = Credentials}}) -> ok = emqx_hooks:run('client.connected', [Credentials, ReasonCode, attrs(PState)]), [ReasonCode1] = reason_codes_compat(connack, [ReasonCode], ProtoVer), - _ = deliver({connack, ReasonCode1}, PState), - {error, emqx_reason_codes:name(ReasonCode1, ProtoVer), PState}. + handle_out({connack, ReasonCode1}, PState). -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Publish Message -> Broker -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ do_publish(Packet = ?PUBLISH_PACKET(QoS, PacketId), - PState = #pstate{session = Session, credentials = Credentials}) -> + PState = #protocol{session = Session, credentials = Credentials}) -> Msg = emqx_mountpoint:mount(mountpoint(Credentials), emqx_packet:to_message(Credentials, Packet)), Msg1 = emqx_message:set_flag(dup, false, Msg), case emqx_session:publish(PacketId, Msg1, Session) of - {ok, Result} -> - puback(QoS, PacketId, {ok, Result}, PState); - {ok, Result, NSession} -> - puback(QoS, PacketId, {ok, Result}, PState#pstate{session = NSession}); - {error, ReasonCode} -> - puback(QoS, PacketId, {error, ReasonCode}, PState) + {ok, Results} -> + puback(QoS, PacketId, Results, PState); + {ok, Results, NSession} -> + puback(QoS, PacketId, Results, PState#protocol{session = NSession}); + {error, Reason} -> + puback(QoS, PacketId, {error, Reason}, PState) end. -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% Puback -> Client -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ puback(?QOS_0, _PacketId, _Result, PState) -> {ok, PState}; puback(?QOS_1, PacketId, {ok, []}, PState) -> - deliver({puback, PacketId, ?RC_NO_MATCHING_SUBSCRIBERS}, PState); + handle_out({puback, PacketId, ?RC_NO_MATCHING_SUBSCRIBERS}, PState); %%TODO: calc the deliver count? puback(?QOS_1, PacketId, {ok, _Result}, PState) -> - deliver({puback, PacketId, ?RC_SUCCESS}, PState); + handle_out({puback, PacketId, ?RC_SUCCESS}, PState); puback(?QOS_1, PacketId, {error, ReasonCode}, PState) -> - deliver({puback, PacketId, ReasonCode}, PState); + handle_out({puback, PacketId, ReasonCode}, PState); puback(?QOS_2, PacketId, {ok, []}, PState) -> - deliver({pubrec, PacketId, ?RC_NO_MATCHING_SUBSCRIBERS}, PState); + handle_out({pubrec, PacketId, ?RC_NO_MATCHING_SUBSCRIBERS}, PState); puback(?QOS_2, PacketId, {ok, _Result}, PState) -> - deliver({pubrec, PacketId, ?RC_SUCCESS}, PState); + handle_out({pubrec, PacketId, ?RC_SUCCESS}, PState); puback(?QOS_2, PacketId, {error, ReasonCode}, PState) -> - deliver({pubrec, PacketId, ReasonCode}, PState). + handle_out({pubrec, PacketId, ReasonCode}, PState). -%%------------------------------------------------------------------------------ -%% Deliver Packet -> Client -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- +%% Handle outgoing +%%-------------------------------------------------------------------- --spec(deliver(list(tuple()) | tuple(), state()) -> {ok, state()} | {error, term()}). -deliver([], PState) -> - {ok, PState}; -deliver([Pub|More], PState) -> - case deliver(Pub, PState) of - {ok, PState1} -> - deliver(More, PState1); - {error, _} = Error -> - Error - end; - -deliver({connack, ReasonCode}, PState) -> - send(?CONNACK_PACKET(ReasonCode), PState); - -deliver({connack, ?RC_SUCCESS, SP}, PState = #pstate{zone = Zone, - proto_ver = ?MQTT_PROTO_V5, - client_id = ClientId, - is_assigned = IsAssigned, - topic_alias_maximum = TopicAliasMaximum}) -> +handle_out({connack, ?RC_SUCCESS, SP}, PState = #protocol{zone = Zone, + proto_ver = ?MQTT_PROTO_V5, + client_id = ClientId, + is_assigned = IsAssigned, + topic_alias_maximum = TopicAliasMaximum}) -> #{max_packet_size := MaxPktSize, max_qos_allowed := MaxQoS, mqtt_retain_available := Retain, @@ -668,81 +621,76 @@ deliver({connack, ?RC_SUCCESS, SP}, PState = #pstate{zone = Zone, Keepalive -> Props2#{'Server-Keep-Alive' => Keepalive} end, - PState1 = PState#pstate{topic_alias_maximum = TopicAliasMaximum#{from_client => MaxAlias}}, + PState1 = PState#protocol{topic_alias_maximum = TopicAliasMaximum#{from_client => MaxAlias}}, - send(?CONNACK_PACKET(?RC_SUCCESS, SP, Props3), PState1); + {ok, ?CONNACK_PACKET(?RC_SUCCESS, SP, Props3), PState1}; -deliver({connack, ReasonCode, SP}, PState) -> - send(?CONNACK_PACKET(ReasonCode, SP), PState); +handle_out({connack, ?RC_SUCCESS, SP}, PState) -> + {ok, ?CONNACK_PACKET(?RC_SUCCESS, SP), PState}; -deliver({publish, PacketId, Msg}, PState = #pstate{credentials = Credentials}) -> - Msg0 = emqx_hooks:run_fold('message.deliver', [Credentials], Msg), - Msg1 = emqx_message:update_expiry(Msg0), - Msg2 = emqx_mountpoint:unmount(mountpoint(Credentials), Msg1), - send(emqx_packet:from_message(PacketId, Msg2), PState); +handle_out({connack, ReasonCode}, PState = #protocol{proto_ver = ProtoVer}) -> + Reason = emqx_reason_codes:name(ReasonCode, ProtoVer), + {error, Reason, ?CONNACK_PACKET(ReasonCode), PState}; -deliver({puback, PacketId, ReasonCode}, PState) -> - send(?PUBACK_PACKET(PacketId, ReasonCode), PState); +handle_out({puback, PacketId, ReasonCode}, PState) -> + {ok, ?PUBACK_PACKET(PacketId, ReasonCode), PState}; + %% TODO: + %% AclDenyAction = emqx_zone:get_env(Zone, acl_deny_action, ignore), + %% do_acl_deny_action(AclDenyAction, Packet, ReasonCode, PState1); -deliver({pubrel, PacketId}, PState) -> - send(?PUBREL_PACKET(PacketId), PState); +handle_out({pubrel, PacketId}, PState) -> + {ok, ?PUBREL_PACKET(PacketId), PState}; -deliver({pubrec, PacketId, ReasonCode}, PState) -> - send(?PUBREC_PACKET(PacketId, ReasonCode), PState); +handle_out({pubrec, PacketId, ReasonCode}, PState) -> + %% TODO: + %% AclDenyAction = emqx_zone:get_env(Zone, acl_deny_action, ignore), + %% do_acl_deny_action(AclDenyAction, Packet, ReasonCode, PState1); + {ok, ?PUBREC_PACKET(PacketId, ReasonCode), PState}; -deliver({suback, PacketId, ReasonCodes}, PState = #pstate{proto_ver = ProtoVer}) -> - send(?SUBACK_PACKET(PacketId, reason_codes_compat(suback, ReasonCodes, ProtoVer)), PState); +%%handle_out({pubrec, PacketId, ReasonCode}, PState) -> +%% {ok, ?PUBREC_PACKET(PacketId, ReasonCode), PState}; -deliver({unsuback, PacketId, ReasonCodes}, PState = #pstate{proto_ver = ProtoVer}) -> - send(?UNSUBACK_PACKET(PacketId, reason_codes_compat(unsuback, ReasonCodes, ProtoVer)), PState); +handle_out({suback, PacketId, ReasonCodes}, PState = #protocol{proto_ver = ProtoVer}) -> + %% TODO: ACL Deny + {ok, ?SUBACK_PACKET(PacketId, reason_codes_compat(suback, ReasonCodes, ProtoVer)), PState}; + +handle_out({unsuback, PacketId, ReasonCodes}, PState = #protocol{proto_ver = ProtoVer}) -> + {ok, ?UNSUBACK_PACKET(PacketId, reason_codes_compat(unsuback, ReasonCodes, ProtoVer)), PState}; %% Deliver a disconnect for mqtt 5.0 -deliver({disconnect, ReasonCode}, PState = #pstate{proto_ver = ?MQTT_PROTO_V5}) -> - send(?DISCONNECT_PACKET(ReasonCode), PState); +handle_out({disconnect, RC}, PState = #protocol{proto_ver = ?MQTT_PROTO_V5}) -> + {error, emqx_reason_codes:name(RC), ?DISCONNECT_PACKET(RC), PState}; -deliver({disconnect, _ReasonCode}, PState) -> +handle_out({disconnect, RC}, PState) -> + {error, emqx_reason_codes:name(RC), PState}. + +handle_timeout(Timer, Name, PState) -> {ok, PState}. -%%------------------------------------------------------------------------------ -%% Send Packet to Client - --spec(send(emqx_mqtt_types:packet(), state()) -> {ok, state()} | {error, term()}). -send(Packet = ?PACKET(Type), PState = #pstate{proto_ver = Ver, sendfun = Send}) -> - case Send(Packet, #{version => Ver}) of - ok -> - trace(send, Packet), - {ok, PState}; - {ok, Data} -> - trace(send, Packet), - emqx_metrics:inc_sent(Packet), - ok = emqx_metrics:inc('bytes.sent', iolist_size(Data)), - {ok, inc_stats(send, Type, PState)}; - {error, Reason} -> - {error, Reason} - end. - %%------------------------------------------------------------------------------ %% Maybe use username replace client id -maybe_use_username_as_clientid(ClientId, PState = #pstate{username = undefined}) -> - PState#pstate{client_id = ClientId}; -maybe_use_username_as_clientid(ClientId, PState = #pstate{username = Username, zone = Zone}) -> +maybe_use_username_as_clientid(ClientId, PState = #protocol{username = undefined}) -> + PState#protocol{client_id = ClientId}; +maybe_use_username_as_clientid(ClientId, PState = #protocol{username = Username, zone = Zone}) -> case emqx_zone:get_env(Zone, use_username_as_clientid, false) of - true -> PState#pstate{client_id = Username}; - false -> PState#pstate{client_id = ClientId} + true -> + PState#protocol{client_id = Username}; + false -> + PState#protocol{client_id = ClientId} end. %%------------------------------------------------------------------------------ %% Assign a clientId -maybe_assign_client_id(PState = #pstate{client_id = <<>>, ack_props = AckProps}) -> +maybe_assign_client_id(PState = #protocol{client_id = <<>>, ack_props = AckProps}) -> ClientId = emqx_guid:to_base62(emqx_guid:gen()), AckProps1 = set_property('Assigned-Client-Identifier', ClientId, AckProps), - PState#pstate{client_id = ClientId, is_assigned = true, ack_props = AckProps1}; + PState#protocol{client_id = ClientId, is_assigned = true, ack_props = AckProps1}; maybe_assign_client_id(PState) -> PState. -try_open_session(SessAttrs, PState = #pstate{zone = Zone, +try_open_session(SessAttrs, PState = #protocol{zone = Zone, client_id = ClientId, username = Username, clean_start = CleanStart}) -> @@ -782,9 +730,9 @@ make_will_msg(#mqtt_packet_connect{proto_ver = ProtoVer, ConnPkt end). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Check Packet -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- check_connect(Packet, PState) -> run_check_steps([fun check_proto_ver/2, @@ -815,7 +763,7 @@ check_client_id(#mqtt_packet_connect{client_id = <<>>, clean_start = true}, _PState) -> ok; -check_client_id(#mqtt_packet_connect{client_id = ClientId}, #pstate{zone = Zone}) -> +check_client_id(#mqtt_packet_connect{client_id = ClientId}, #protocol{zone = Zone}) -> Len = byte_size(ClientId), MaxLen = emqx_zone:get_env(Zone, max_clientid_len), case (1 =< Len) andalso (Len =< MaxLen) of @@ -827,7 +775,7 @@ check_flapping(#mqtt_packet_connect{}, PState) -> do_flapping_detect(connect, PState). check_banned(#mqtt_packet_connect{client_id = ClientId, username = Username}, - #pstate{zone = Zone, peername = Peername}) -> + #protocol{zone = Zone, peername = Peername}) -> Credentials = #{client_id => ClientId, username => Username, peername => Peername}, @@ -845,7 +793,7 @@ check_will_topic(#mqtt_packet_connect{will_topic = WillTopic} = ConnPkt, PState) check_will_retain(#mqtt_packet_connect{will_retain = false, proto_ver = ?MQTT_PROTO_V5}, _PState) -> ok; -check_will_retain(#mqtt_packet_connect{will_retain = true, proto_ver = ?MQTT_PROTO_V5}, #pstate{zone = Zone}) -> +check_will_retain(#mqtt_packet_connect{will_retain = true, proto_ver = ?MQTT_PROTO_V5}, #protocol{zone = Zone}) -> case emqx_zone:get_env(Zone, mqtt_retain_available, true) of true -> {error, ?RC_RETAIN_NOT_SUPPORTED}; false -> ok @@ -854,7 +802,7 @@ check_will_retain(_Packet, _PState) -> ok. check_will_acl(#mqtt_packet_connect{will_topic = WillTopic}, - #pstate{zone = Zone, credentials = Credentials}) -> + #protocol{zone = Zone, credentials = Credentials}) -> EnableAcl = emqx_zone:get_env(Zone, enable_acl, false), case do_acl_check(EnableAcl, publish, Credentials, WillTopic) of ok -> ok; @@ -869,14 +817,14 @@ check_publish(Packet, PState) -> check_pub_caps(#mqtt_packet{header = #mqtt_packet_header{qos = QoS, retain = Retain}, variable = #mqtt_packet_publish{properties = _Properties}}, - #pstate{zone = Zone}) -> + #protocol{zone = Zone}) -> emqx_mqtt_caps:check_pub(Zone, #{qos => QoS, retain => Retain}). -check_pub_acl(_Packet, #pstate{credentials = #{is_superuser := IsSuper}}) +check_pub_acl(_Packet, #protocol{credentials = #{is_superuser := IsSuper}}) when IsSuper -> ok; check_pub_acl(#mqtt_packet{variable = #mqtt_packet_publish{topic_name = Topic}}, - #pstate{zone = Zone, credentials = Credentials}) -> + #protocol{zone = Zone, credentials = Credentials}) -> EnableAcl = emqx_zone:get_env(Zone, enable_acl, false), do_acl_check(EnableAcl, publish, Credentials, Topic). @@ -890,7 +838,7 @@ run_check_steps([Check|Steps], Packet, PState) -> Error end. -check_subscribe(TopicFilters, PState = #pstate{zone = Zone}) -> +check_subscribe(TopicFilters, PState = #protocol{zone = Zone}) -> case emqx_mqtt_caps:check_sub(Zone, TopicFilters) of {ok, TopicFilter1} -> check_sub_acl(TopicFilter1, PState); @@ -898,10 +846,10 @@ check_subscribe(TopicFilters, PState = #pstate{zone = Zone}) -> {error, TopicFilter1} end. -check_sub_acl(TopicFilters, #pstate{credentials = #{is_superuser := IsSuper}}) +check_sub_acl(TopicFilters, #protocol{credentials = #{is_superuser := IsSuper}}) when IsSuper -> {ok, TopicFilters}; -check_sub_acl(TopicFilters, #pstate{zone = Zone, credentials = Credentials}) -> +check_sub_acl(TopicFilters, #protocol{zone = Zone, credentials = Credentials}) -> EnableAcl = emqx_zone:get_env(Zone, enable_acl, false), lists:foldr( fun({Topic, SubOpts}, {Ok, Acc}) when EnableAcl -> @@ -912,26 +860,9 @@ check_sub_acl(TopicFilters, #pstate{zone = Zone, credentials = Credentials}) -> {ok, [TopicFilter | Acc]} end, {ok, []}, TopicFilters). -trace(recv, Packet) -> - ?LOG(debug, "RECV ~s", [emqx_packet:format(Packet)]); -trace(send, Packet) -> - ?LOG(debug, "SEND ~s", [emqx_packet:format(Packet)]). - -inc_stats(recv, Type, PState = #pstate{recv_stats = Stats}) -> - PState#pstate{recv_stats = inc_stats(Type, Stats)}; - -inc_stats(send, Type, PState = #pstate{send_stats = Stats}) -> - PState#pstate{send_stats = inc_stats(Type, Stats)}. - -inc_stats(Type, Stats = #{pkt := PktCnt, msg := MsgCnt}) -> - Stats#{pkt := PktCnt + 1, msg := case Type =:= ?PUBLISH of - true -> MsgCnt + 1; - false -> MsgCnt - end}. - -terminate(_Reason, #pstate{client_id = undefined}) -> +terminate(_Reason, #protocol{client_id = undefined}) -> ok; -terminate(_Reason, PState = #pstate{connected = false}) -> +terminate(_Reason, PState = #protocol{connected = false}) -> do_flapping_detect(disconnect, PState), ok; terminate(Reason, PState) when Reason =:= conflict; @@ -939,20 +870,20 @@ terminate(Reason, PState) when Reason =:= conflict; do_flapping_detect(disconnect, PState), ok; -terminate(Reason, PState = #pstate{credentials = Credentials}) -> +terminate(Reason, PState = #protocol{credentials = Credentials}) -> do_flapping_detect(disconnect, PState), ?LOG(info, "Shutdown for ~p", [Reason]), ok = emqx_hooks:run('client.disconnected', [Credentials, Reason]). start_keepalive(0, _PState) -> ignore; -start_keepalive(Secs, #pstate{zone = Zone}) when Secs > 0 -> +start_keepalive(Secs, #protocol{zone = Zone}) when Secs > 0 -> Backoff = emqx_zone:get_env(Zone, keepalive_backoff, 0.75), self() ! {keepalive, start, round(Secs * Backoff)}. -%%----------------------------------------------------------------------------- +%%-------------------------------------------------------------------- %% Parse topic filters -%%----------------------------------------------------------------------------- +%%-------------------------------------------------------------------- parse_topic_filters(?SUBSCRIBE, RawTopicFilters) -> [emqx_topic:parse(RawTopic, SubOpts) || {RawTopic, SubOpts} <- RawTopicFilters]; @@ -966,17 +897,17 @@ sp(false) -> 0. flag(false) -> 0; flag(true) -> 1. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Execute actions in case acl deny -do_flapping_detect(Action, #pstate{zone = Zone, +do_flapping_detect(Action, #protocol{zone = Zone, client_id = ClientId}) -> ok = case emqx_zone:get_env(Zone, enable_flapping_detect, false) of true -> Threshold = emqx_zone:get_env(Zone, flapping_threshold, {10, 60}), case emqx_flapping:check(Action, ClientId, Threshold) of flapping -> - BanExpiryInterval = emqx_zone:get_env(Zone, flapping_ban_expiry_interval, 3600000), + BanExpiryInterval = emqx_zone:get_env(Zone, flapping_banned_expiry_interval, 3600000), Until = erlang:system_time(second) + BanExpiryInterval, emqx_banned:add(#banned{who = {client_id, ClientId}, reason = <<"flapping">>, @@ -990,13 +921,14 @@ do_flapping_detect(Action, #pstate{zone = Zone, end. do_acl_deny_action(disconnect, ?PUBLISH_PACKET(?QOS_0, _Topic, _PacketId, _Payload), - ?RC_NOT_AUTHORIZED, PState = #pstate{proto_ver = ProtoVer}) -> + ?RC_NOT_AUTHORIZED, PState = #protocol{proto_ver = ProtoVer}) -> {error, emqx_reason_codes:name(?RC_NOT_AUTHORIZED, ProtoVer), PState}; do_acl_deny_action(disconnect, ?PUBLISH_PACKET(QoS, _Topic, _PacketId, _Payload), - ?RC_NOT_AUTHORIZED, PState = #pstate{proto_ver = ProtoVer}) + ?RC_NOT_AUTHORIZED, PState = #protocol{proto_ver = ProtoVer}) when QoS =:= ?QOS_1; QoS =:= ?QOS_2 -> - deliver({disconnect, ?RC_NOT_AUTHORIZED}, PState), + %% TODO:... + %% deliver({disconnect, ?RC_NOT_AUTHORIZED}, PState), {error, emqx_reason_codes:name(?RC_NOT_AUTHORIZED, ProtoVer), PState}; do_acl_deny_action(Action, ?SUBSCRIBE_PACKET(_PacketId, _Properties, _RawTopicFilters), ReasonCodes, PState) @@ -1004,18 +936,18 @@ do_acl_deny_action(Action, ?SUBSCRIBE_PACKET(_PacketId, _Properties, _RawTopicFi traverse_reason_codes(ReasonCodes, Action, PState); do_acl_deny_action(_OtherAction, _PubSubPacket, ?RC_NOT_AUTHORIZED, PState) -> {ok, PState}; -do_acl_deny_action(_OtherAction, _PubSubPacket, ReasonCode, PState = #pstate{proto_ver = ProtoVer}) -> +do_acl_deny_action(_OtherAction, _PubSubPacket, ReasonCode, PState = #protocol{proto_ver = ProtoVer}) -> {error, emqx_reason_codes:name(ReasonCode, ProtoVer), PState}. traverse_reason_codes([], _Action, PState) -> {ok, PState}; traverse_reason_codes([?RC_SUCCESS | LeftReasonCodes], Action, PState) -> traverse_reason_codes(LeftReasonCodes, Action, PState); -traverse_reason_codes([?RC_NOT_AUTHORIZED | _LeftReasonCodes], disconnect, PState = #pstate{proto_ver = ProtoVer}) -> +traverse_reason_codes([?RC_NOT_AUTHORIZED | _LeftReasonCodes], disconnect, PState = #protocol{proto_ver = ProtoVer}) -> {error, emqx_reason_codes:name(?RC_NOT_AUTHORIZED, ProtoVer), PState}; traverse_reason_codes([?RC_NOT_AUTHORIZED | LeftReasonCodes], Action, PState) -> traverse_reason_codes(LeftReasonCodes, Action, PState); -traverse_reason_codes([OtherCode | _LeftReasonCodes], _Action, PState = #pstate{proto_ver = ProtoVer}) -> +traverse_reason_codes([OtherCode | _LeftReasonCodes], _Action, PState = #protocol{proto_ver = ProtoVer}) -> {error, emqx_reason_codes:name(OtherCode, ProtoVer), PState}. %% Reason code compat @@ -1026,7 +958,7 @@ reason_codes_compat(unsuback, _ReasonCodes, _ProtoVer) -> reason_codes_compat(PktType, ReasonCodes, _ProtoVer) -> [emqx_reason_codes:compat(PktType, RC) || RC <- ReasonCodes]. -raw_topic_filters(#pstate{zone = Zone, proto_ver = ProtoVer, is_bridge = IsBridge}, RawTopicFilters) -> +raw_topic_filters(#protocol{zone = Zone, proto_ver = ProtoVer, is_bridge = IsBridge}, RawTopicFilters) -> IgnoreLoop = emqx_zone:get_env(Zone, ignore_loop_deliver, false), case ProtoVer < ?MQTT_PROTO_V5 of true -> diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 74cbee9d0..f5e7414d5 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -40,6 +40,7 @@ %% @end %%-------------------------------------------------------------------- +%% MQTT Session -module(emqx_session). -include("emqx.hrl"). @@ -49,91 +50,86 @@ -logger_header("[Session]"). --export([ new/1 - , handle/2 - , close/1 - ]). +-export([new/1]). -export([ info/1 , attrs/1 , stats/1 ]). --export([ subscribe/2 - , unsubscribe/2 - , publish/3 - , puback/2 - , puback/3 - , pubrec/2 +-export([ subscribe/3 + , unsubscribe/3 + ]). + +-export([publish/3]). + +-export([ puback/3 , pubrec/3 , pubrel/3 , pubcomp/3 ]). +-export([ deliver/3 + , await/3 + , enqueue/2 + ]). + +-export_type([ session/0 + , puback_ret/0 + ]). + +-import(emqx_zone, + [ get_env/2 + , get_env/3 + ]). + -record(session, { - %% ClientId: Identifier of Session - client_id :: binary(), - %% Clean Start Flag - clean_start = false :: boolean(), + clean_start :: boolean(), - %% Username - username :: maybe(binary()), - - %% Conn Binding: local | remote - %% binding = local :: local | remote, - - %% Deliver fun - deliver_fun :: function(), - - %% Next packet id of the session - next_pkt_id = 1 :: emqx_mqtt_types:packet_id(), - - %% Max subscriptions + %% Max subscriptions allowed max_subscriptions :: non_neg_integer(), %% Client’s Subscriptions. subscriptions :: map(), %% Upgrade QoS? - upgrade_qos = false :: boolean(), + upgrade_qos :: boolean(), - %% Client <- Broker: Inflight QoS1, QoS2 messages sent to the client but unacked. + %% Client <- Broker: + %% Inflight QoS1, QoS2 messages sent to the client but unacked. inflight :: emqx_inflight:inflight(), - %% Max Inflight Size. DEPRECATED: Get from inflight - %% max_inflight = 32 :: non_neg_integer(), - - %% Retry interval for redelivering QoS1/2 messages - retry_interval = 20000 :: timeout(), - - %% Retry Timer - retry_timer :: maybe(reference()), - %% All QoS1, QoS2 messages published to when client is disconnected. %% QoS 1 and QoS 2 messages pending transmission to the Client. %% %% Optionally, QoS 0 messages pending transmission to the Client. mqueue :: emqx_mqueue:mqueue(), - %% Max Packets Awaiting PUBREL - max_awaiting_rel = 100 :: non_neg_integer(), + %% Next packet id of the session + next_pkt_id = 1 :: emqx_mqtt:packet_id(), - %% Awaiting PUBREL Timeout - await_rel_timeout = 20000 :: timeout(), + %% Retry interval for redelivering QoS1/2 messages + retry_interval :: timeout(), - %% Client -> Broker: Inflight QoS2 messages received from client and waiting for pubrel. + %% Retry delivery timer + retry_timer :: maybe(reference()), + + %% Client -> Broker: + %% Inflight QoS2 messages received from client and waiting for pubrel. awaiting_rel :: map(), %% Awaiting PUBREL Timer await_rel_timer :: maybe(reference()), - will_msg :: emqx:message(), + %% Max Packets Awaiting PUBREL + max_awaiting_rel :: non_neg_integer(), - will_delay_timer :: maybe(reference()), + %% Awaiting PUBREL Timeout + await_rel_timeout :: timeout(), %% Session Expiry Interval - expiry_interval = 7200 :: timeout(), + expiry_interval :: timeout(), %% Expired Timer expiry_timer :: maybe(reference()), @@ -144,287 +140,155 @@ -opaque(session() :: #session{}). --export_type([session/0]). +-type(puback_ret() :: {ok, session()} + | {ok, emqx_types:message(), session()} + | {error, emqx_mqtt:reason_code()}). %% @doc Create a session. -spec(new(Attrs :: map()) -> session()). new(#{zone := Zone, - client_id := ClientId, clean_start := CleanStart, - username := Username, - expiry_interval := ExpiryInterval, max_inflight := MaxInflight, - will_msg := WillMsg}) -> - emqx_logger:set_metadata_client_id(ClientId), - #session{client_id = ClientId, - clean_start = CleanStart, - username = Username, - subscriptions = #{}, - inflight = emqx_inflight:new(MaxInflight), - mqueue = init_mqueue(Zone), - awaiting_rel = #{}, - expiry_interval = ExpiryInterval, - created_at = os:timestamp(), - will_msg = WillMsg}. + expiry_interval := ExpiryInterval}) -> + %% emqx_logger:set_metadata_client_id(ClientId), + #session{clean_start = CleanStart, + max_subscriptions = get_env(Zone, max_subscriptions, 0), + subscriptions = #{}, + upgrade_qos = get_env(Zone, upgrade_qos, false), + inflight = emqx_inflight:new(MaxInflight), + mqueue = init_mqueue(Zone), + next_pkt_id = 1, + retry_interval = get_env(Zone, retry_interval, 0), + awaiting_rel = #{}, + max_awaiting_rel = get_env(Zone, max_awaiting_rel, 100), + await_rel_timeout = get_env(Zone, await_rel_timeout), + expiry_interval = ExpiryInterval, + created_at = os:timestamp() + }. init_mqueue(Zone) -> - emqx_mqueue:init(#{max_len => emqx_zone:get_env(Zone, max_mqueue_len, 1000), - store_qos0 => emqx_zone:get_env(Zone, mqueue_store_qos0, true), - priorities => emqx_zone:get_env(Zone, mqueue_priorities), - default_priority => emqx_zone:get_env(Zone, mqueue_default_priority) + emqx_mqueue:init(#{max_len => get_env(Zone, max_mqueue_len, 1000), + store_qos0 => get_env(Zone, mqueue_store_qos0, true), + priorities => get_env(Zone, mqueue_priorities), + default_priority => get_env(Zone, mqueue_default_priority) }). -%% @doc Get session info --spec(info(session()) -> list({atom(), term()})). -info(Session = #session{next_pkt_id = PktId, - max_subscriptions = MaxSubscriptions, - subscriptions = Subscriptions, - upgrade_qos = UpgradeQoS, - inflight = Inflight, - retry_interval = RetryInterval, - mqueue = MQueue, - max_awaiting_rel = MaxAwaitingRel, - awaiting_rel = AwaitingRel, - await_rel_timeout = AwaitRelTimeout}) -> - attrs(Session) ++ [{next_pkt_id, PktId}, - {max_subscriptions, MaxSubscriptions}, - {subscriptions, Subscriptions}, - {upgrade_qos, UpgradeQoS}, - {inflight, Inflight}, - {retry_interval, RetryInterval}, - {mqueue_len, emqx_mqueue:len(MQueue)}, - {awaiting_rel, AwaitingRel}, - {max_awaiting_rel, MaxAwaitingRel}, - {await_rel_timeout, AwaitRelTimeout}]. +%%------------------------------------------------------------------------------ +%% Info, Attrs, Stats +%%------------------------------------------------------------------------------ -%% @doc Get session attrs --spec(attrs(session()) -> list({atom(), term()})). -attrs(#session{client_id = ClientId, - clean_start = CleanStart, - username = Username, +%% @doc Get session info +-spec(info(session()) -> map()). +info(#session{clean_start = CleanStart, + max_subscriptions = MaxSubscriptions, + subscriptions = Subscriptions, + upgrade_qos = UpgradeQoS, + inflight = Inflight, + retry_interval = RetryInterval, + mqueue = MQueue, + next_pkt_id = PktId, + max_awaiting_rel = MaxAwaitingRel, + awaiting_rel = AwaitingRel, + await_rel_timeout = AwaitRelTimeout, + expiry_interval = ExpiryInterval, + created_at = CreatedAt}) -> + #{clean_start => CleanStart, + max_subscriptions => MaxSubscriptions, + subscriptions => Subscriptions, + upgrade_qos => UpgradeQoS, + inflight => Inflight, + retry_interval => RetryInterval, + mqueue_len => emqx_mqueue:len(MQueue), + next_pkt_id => PktId, + awaiting_rel => AwaitingRel, + max_awaiting_rel => MaxAwaitingRel, + await_rel_timeout => AwaitRelTimeout, + expiry_interval => ExpiryInterval div 1000, + created_at => CreatedAt + }. + +%% @doc Get session attrs. +-spec(attrs(session()) -> map()). +attrs(#session{clean_start = CleanStart, expiry_interval = ExpiryInterval, created_at = CreatedAt}) -> - [{client_id, ClientId}, - {clean_start, CleanStart}, - {username, Username}, - {expiry_interval, ExpiryInterval div 1000}, - {created_at, CreatedAt}]. + #{clean_start => CleanStart, + expiry_interval => ExpiryInterval div 1000, + created_at => CreatedAt + }. -%% @doc Get session stats --spec(stats(session()) -> list({atom(), non_neg_integer()})). +%% @doc Get session stats. +-spec(stats(session()) -> #{atom() => non_neg_integer()}). stats(#session{max_subscriptions = MaxSubscriptions, subscriptions = Subscriptions, inflight = Inflight, mqueue = MQueue, max_awaiting_rel = MaxAwaitingRel, awaiting_rel = AwaitingRel}) -> - lists:append(emqx_misc:proc_stats(), - [{max_subscriptions, MaxSubscriptions}, - {subscriptions_count, maps:size(Subscriptions)}, - {max_inflight, emqx_inflight:max_size(Inflight)}, - {inflight_len, emqx_inflight:size(Inflight)}, - {max_mqueue, emqx_mqueue:max_len(MQueue)}, - {mqueue_len, emqx_mqueue:len(MQueue)}, - {mqueue_dropped, emqx_mqueue:dropped(MQueue)}, - {max_awaiting_rel, MaxAwaitingRel}, - {awaiting_rel_len, maps:size(AwaitingRel)}, - {deliver_msg, emqx_pd:get_counter(deliver_stats)}, - {enqueue_msg, emqx_pd:get_counter(enqueue_stats)}]). + #{max_subscriptions => MaxSubscriptions, + subscriptions_count => maps:size(Subscriptions), + max_inflight => emqx_inflight:max_size(Inflight), + inflight_len => emqx_inflight:size(Inflight), + max_mqueue => emqx_mqueue:max_len(MQueue), + mqueue_len => emqx_mqueue:len(MQueue), + mqueue_dropped => emqx_mqueue:dropped(MQueue), + max_awaiting_rel => MaxAwaitingRel, + awaiting_rel_len => maps:size(AwaitingRel) + }. %%-------------------------------------------------------------------- %% PubSub API %%-------------------------------------------------------------------- -%% SUBSCRIBE: --spec(subscribe(list({emqx_topic:topic(), emqx_types:subopts()}), session()) - -> {ok, list(emqx_mqtt_types:reason_code()), session()}). -subscribe(RawTopicFilters, Session = #session{client_id = ClientId, - username = Username, - subscriptions = Subscriptions}) +%% Client -> Broker: SUBSCRIBE +-spec(subscribe(emqx_types:credentials(), emqx_mqtt:topic_filters(), session()) + -> {ok, list(emqx_mqtt:reason_code()), session()}). +subscribe(Credentials, RawTopicFilters, Session = #session{subscriptions = Subscriptions}) when is_list(RawTopicFilters) -> TopicFilters = [emqx_topic:parse(RawTopic, maps:merge(?DEFAULT_SUBOPTS, SubOpts)) || {RawTopic, SubOpts} <- RawTopicFilters], {ReasonCodes, Subscriptions1} = lists:foldr( - fun ({Topic, SubOpts = #{qos := QoS, rc := RC}}, {RcAcc, SubMap}) when - RC == ?QOS_0; RC == ?QOS_1; RC == ?QOS_2 -> - {[QoS|RcAcc], do_subscribe(ClientId, Username, Topic, SubOpts, SubMap)}; - ({_Topic, #{rc := RC}}, {RcAcc, SubMap}) -> - {[RC|RcAcc], SubMap} - end, {[], Subscriptions}, TopicFilters), + fun({Topic, SubOpts = #{qos := QoS, rc := RC}}, {RcAcc, SubMap}) + when RC == ?QOS_0; RC == ?QOS_1; RC == ?QOS_2 -> + {[QoS|RcAcc], do_subscribe(Credentials, Topic, SubOpts, SubMap)}; + ({_Topic, #{rc := RC}}, {RcAcc, SubMap}) -> + {[RC|RcAcc], SubMap} + end, {[], Subscriptions}, TopicFilters), {ok, ReasonCodes, Session#session{subscriptions = Subscriptions1}}. -%% TODO: improve later. -do_subscribe(ClientId, Username, Topic, SubOpts, SubMap) -> +do_subscribe(Credentials = #{client_id := ClientId}, Topic, SubOpts, SubMap) -> case maps:find(Topic, SubMap) of {ok, SubOpts} -> - ok = emqx_hooks:run('session.subscribed', [#{client_id => ClientId, username => Username}, Topic, SubOpts#{first => false}]), + ok = emqx_hooks:run('session.subscribed', [Credentials, Topic, SubOpts#{first => false}]), SubMap; {ok, _SubOpts} -> emqx_broker:set_subopts(Topic, SubOpts), %% Why??? - ok = emqx_hooks:run('session.subscribed', [#{client_id => ClientId, username => Username}, Topic, SubOpts#{first => false}]), + ok = emqx_hooks:run('session.subscribed', [Credentials, Topic, SubOpts#{first => false}]), maps:put(Topic, SubOpts, SubMap); error -> - emqx_broker:subscribe(Topic, ClientId, SubOpts), - ok = emqx_hooks:run('session.subscribed', [#{client_id => ClientId, username => Username}, Topic, SubOpts#{first => true}]), + ok = emqx_broker:subscribe(Topic, ClientId, SubOpts), + ok = emqx_hooks:run('session.subscribed', [Credentials, Topic, SubOpts#{first => true}]), maps:put(Topic, SubOpts, SubMap) end. -%% PUBLISH: --spec(publish(emqx_mqtt_types:packet_id(), emqx_types:message(), session()) - -> {ok, emqx_types:deliver_results()} | {error, term()}). -publish(_PacketId, Msg = #message{qos = ?QOS_0}, _Session) -> - %% Publish QoS0 message directly - {ok, emqx_broker:publish(Msg)}; - -publish(_PacketId, Msg = #message{qos = ?QOS_1}, _Session) -> - %% Publish QoS1 message directly - {ok, emqx_broker:publish(Msg)}; - -%% PUBLISH: This is only to register packetId to session state. -%% The actual message dispatching should be done by the caller. -publish(PacketId, Msg = #message{qos = ?QOS_2, timestamp = Ts}, - Session = #session{awaiting_rel = AwaitingRel, - max_awaiting_rel = MaxAwaitingRel}) -> - %% Register QoS2 message packet ID (and timestamp) to session, then publish - case is_awaiting_full(MaxAwaitingRel, AwaitingRel) of - false -> - case maps:is_key(PacketId, AwaitingRel) of - false -> - NewAwaitingRel = maps:put(PacketId, Ts, AwaitingRel), - NSession = Session#session{awaiting_rel = NewAwaitingRel}, - {ok, emqx_broker:publish(Msg), ensure_await_rel_timer(NSession)}; - true -> - {error, ?RC_PACKET_IDENTIFIER_IN_USE} - end; - true -> - ?LOG(warning, "[Session] Dropped qos2 packet ~w for too many awaiting_rel", [PacketId]), - ok = emqx_metrics:inc('messages.qos2.dropped'), - {error, ?RC_RECEIVE_MAXIMUM_EXCEEDED} - end. - -%% PUBACK: --spec(puback(emqx_mqtt_types:packet_id(), session()) -> session()). -puback(PacketId, Session) -> - puback(PacketId, ?RC_SUCCESS, Session). - --spec(puback(emqx_mqtt_types:packet_id(), emqx_mqtt_types:reason_code(), session()) - -> session()). -puback(PacketId, ReasonCode, Session = #session{inflight = Inflight}) -> - case emqx_inflight:contain(PacketId, Inflight) of - true -> - dequeue(acked(puback, PacketId, Session)); - false -> - ?LOG(warning, "[Session] The PUBACK PacketId ~w is not found", [PacketId]), - ok = emqx_metrics:inc('packets.puback.missed'), - Session - end. - -%% PUBREC: --spec(pubrec(emqx_mqtt_types:packet_id(), session()) - -> ok | {error, emqx_mqtt_types:reason_code()}). -pubrec(PacketId, Session) -> - pubrec(PacketId, ?RC_SUCCESS, Session). - --spec(pubrec(emqx_mqtt_types:packet_id(), emqx_mqtt_types:reason_code(), session()) - -> {ok, session()} | {error, emqx_mqtt_types:reason_code()}). -pubrec(PacketId, ReasonCode, Session = #session{inflight = Inflight}) -> - case emqx_inflight:contain(PacketId, Inflight) of - true -> - {ok, acked(pubrec, PacketId, Session)}; - false -> - ?LOG(warning, "[Session] The PUBREC PacketId ~w is not found.", [PacketId]), - ok = emqx_metrics:inc('packets.pubrec.missed'), - {error, ?RC_PACKET_IDENTIFIER_NOT_FOUND} - end. - -%% PUBREL: --spec(pubrel(emqx_mqtt_types:packet_id(), emqx_mqtt_types:reason_code(), session()) - -> {ok, session()} | {error, emqx_mqtt_types:reason_code()}). -pubrel(PacketId, ReasonCode, Session = #session{awaiting_rel = AwaitingRel}) -> - case maps:take(PacketId, AwaitingRel) of - {_Ts, AwaitingRel1} -> - {ok, Session#session{awaiting_rel = AwaitingRel1}}; - error -> - ?LOG(warning, "[Session] The PUBREL PacketId ~w is not found", [PacketId]), - ok = emqx_metrics:inc('packets.pubrel.missed'), - {error, ?RC_PACKET_IDENTIFIER_NOT_FOUND} - end. - -%% PUBCOMP: --spec(pubcomp(emqx_mqtt_types:packet_id(), emqx_mqtt_types:reason_code(), session()) - -> {ok, session()} | {error, emqx_mqtt_types:reason_code()}). -pubcomp(PacketId, ReasonCode, Session = #session{inflight = Inflight}) -> - case emqx_inflight:contain(PacketId, Inflight) of - true -> - {ok, dequeue(acked(pubcomp, PacketId, Session))}; - false -> - ?LOG(warning, "[Session] The PUBCOMP PacketId ~w is not found", [PacketId]), - ok = emqx_metrics:inc('packets.pubcomp.missed'), - {error, ?RC_PACKET_IDENTIFIER_NOT_FOUND} - end. - -%%------------------------------------------------------------------------------ -%% Awaiting ACK for QoS1/QoS2 Messages -%%------------------------------------------------------------------------------ - -await(PacketId, Msg, Session = #session{inflight = Inflight}) -> - Inflight1 = emqx_inflight:insert( - PacketId, {publish, {PacketId, Msg}, os:timestamp()}, Inflight), - ensure_retry_timer(Session#session{inflight = Inflight1}). - -acked(puback, PacketId, Session = #session{client_id = ClientId, username = Username, inflight = Inflight}) -> - case emqx_inflight:lookup(PacketId, Inflight) of - {value, {publish, {_, Msg}, _Ts}} -> - ok = emqx_hooks:run('message.acked', [#{client_id => ClientId, username => Username}, Msg]), - Session#session{inflight = emqx_inflight:delete(PacketId, Inflight)}; - none -> - ?LOG(warning, "[Session] Duplicated PUBACK PacketId ~w", [PacketId]), - Session - end; - -acked(pubrec, PacketId, Session = #session{client_id = ClientId, username = Username, inflight = Inflight}) -> - case emqx_inflight:lookup(PacketId, Inflight) of - {value, {publish, {_, Msg}, _Ts}} -> - ok = emqx_hooks:run('message.acked', [#{client_id => ClientId, username => Username}, Msg]), - Inflight1 = emqx_inflight:update(PacketId, {pubrel, PacketId, os:timestamp()}, Inflight), - Session#session{inflight = Inflight1}; - {value, {pubrel, PacketId, _Ts}} -> - ?LOG(warning, "[Session] Duplicated PUBREC PacketId ~w", [PacketId]), - Session; - none -> - ?LOG(warning, "[Session] Unexpected PUBREC PacketId ~w", [PacketId]), - Session - end; - -acked(pubcomp, PacketId, Session = #session{inflight = Inflight}) -> - case emqx_inflight:contain(PacketId, Inflight) of - true -> - Session#session{inflight = emqx_inflight:delete(PacketId, Inflight)}; - false -> - ?LOG(warning, "PUBCOMP PacketId ~w is not found", [PacketId]), - Session - end. - -%% UNSUBSCRIBE: --spec(unsubscribe(emqx_mqtt_types:topic_filters(), session()) - -> {ok, list(emqx_mqtt_types:reason_code()), session()}). -unsubscribe(RawTopicFilters, Session = #session{client_id = ClientId, - username = Username, - subscriptions = Subscriptions}) +%% Client -> Broker: UNSUBSCRIBE +-spec(unsubscribe(emqx_types:credentials(), emqx_mqtt:topic_filters(), session()) + -> {ok, list(emqx_mqtt:reason_code()), session()}). +unsubscribe(Credentials, RawTopicFilters, Session = #session{subscriptions = Subscriptions}) when is_list(RawTopicFilters) -> - TopicFilters = lists:map(fun({RawTopic, Opts}) -> - emqx_topic:parse(RawTopic, Opts); - (RawTopic) when is_binary(RawTopic) -> - emqx_topic:parse(RawTopic) - end, RawTopicFilters), + TopicFilters = lists:map(fun({RawTopic, Opts}) -> + emqx_topic:parse(RawTopic, Opts); + (RawTopic) when is_binary(RawTopic) -> + emqx_topic:parse(RawTopic) + end, RawTopicFilters), {ReasonCodes, Subscriptions1} = lists:foldr(fun({Topic, _SubOpts}, {Acc, SubMap}) -> case maps:find(Topic, SubMap) of {ok, SubOpts} -> ok = emqx_broker:unsubscribe(Topic), - ok = emqx_hooks:run('session.unsubscribed', [#{client_id => ClientId, username => Username}, Topic, SubOpts]), + ok = emqx_hooks:run('session.unsubscribed', [Credentials, Topic, SubOpts]), {[?RC_SUCCESS|Acc], maps:remove(Topic, SubMap)}; error -> {[?RC_NO_SUBSCRIPTION_EXISTED|Acc], SubMap} @@ -432,124 +296,225 @@ unsubscribe(RawTopicFilters, Session = #session{client_id = ClientId, end, {[], Subscriptions}, TopicFilters), {ok, ReasonCodes, Session#session{subscriptions = Subscriptions1}}. --spec(resume(map(), session()) -> session()). -resume(#{will_msg := WillMsg, - expiry_interval := ExpiryInterval, - max_inflight := MaxInflight}, - Session = #session{client_id = ClientId, - clean_start = CleanStart, - retry_timer = RetryTimer, - await_rel_timer = AwaitTimer, - expiry_timer = ExpireTimer, - will_delay_timer = WillDelayTimer}) -> - - %% ?LOG(info, "[Session] Resumed by ~p ", [self()]), - - %% Cancel Timers - lists:foreach(fun emqx_misc:cancel_timer/1, - [RetryTimer, AwaitTimer, ExpireTimer, WillDelayTimer]), - - %% case kick(ClientId, OldConnPid, ConnPid) of - %% ok -> ?LOG(warning, "[Session] Connection ~p kickout ~p", [ConnPid, OldConnPid]); - %% ignore -> ok - %% end, - - Inflight = emqx_inflight:update_size(MaxInflight, Session#session.inflight), - - Session1 = Session#session{clean_start = false, - retry_timer = undefined, - awaiting_rel = #{}, - await_rel_timer = undefined, - expiry_timer = undefined, - expiry_interval = ExpiryInterval, - inflight = Inflight, - will_delay_timer = undefined, - will_msg = WillMsg - }, - - %% Clean Session: true -> false??? - CleanStart andalso emqx_cm:set_session_attrs(ClientId, attrs(Session1)), - - %%ok = emqx_hooks:run('session.resumed', [#{client_id => ClientId}, attrs(Session1)]), - - %% Replay delivery and Dequeue pending messages - dequeue(retry_delivery(true, Session1)). - --spec(update_expiry_interval(timeout(), session()) -> session()). -update_expiry_interval(Interval, Session) -> - Session#session{expiry_interval = Interval}. - --spec(close(session()) -> ok). -close(_Session) -> ok. - -%%------------------------------------------------------------------------------ -%% Internal functions -%%------------------------------------------------------------------------------ - - -%%deliver_fun(ConnPid) when node(ConnPid) == node() -> -%% fun(Packet) -> ConnPid ! {deliver, Packet}, ok end; -%%deliver_fun(ConnPid) -> -%% Node = node(ConnPid), -%% fun(Packet) -> -%% true = emqx_rpc:cast(Node, erlang, send, [ConnPid, {deliver, Packet}]), ok -%% end. - -%%------------------------------------------------------------------------------ -%% Replay or Retry Delivery - -%% Redeliver at once if force is true -retry_delivery(Force, Session = #session{inflight = Inflight}) -> - case emqx_inflight:is_empty(Inflight) of - true -> Session; +%% Client -> Broker: QoS2 PUBLISH +-spec(publish(emqx_mqtt:packet_id(), emqx_types:message(), session()) + -> {ok, emqx_types:deliver_results(), session()} | {error, emqx_mqtt:reason_code()}). +publish(PacketId, Msg = #message{qos = ?QOS_2, timestamp = Ts}, + Session = #session{awaiting_rel = AwaitingRel, + max_awaiting_rel = MaxAwaitingRel}) -> + case is_awaiting_full(MaxAwaitingRel, AwaitingRel) of false -> - SortFun = fun({_, _, Ts1}, {_, _, Ts2}) -> Ts1 < Ts2 end, - Msgs = lists:sort(SortFun, emqx_inflight:values(Inflight)), - retry_delivery(Force, Msgs, os:timestamp(), Session) - end. - -retry_delivery(_Force, [], _Now, Session) -> - %% Retry again... - ensure_retry_timer(Session); - -retry_delivery(Force, [{Type, Msg0, Ts} | Msgs], Now, - Session = #session{inflight = Inflight, - retry_interval = Interval}) -> - %% Microseconds -> MilliSeconds - Age = timer:now_diff(Now, Ts) div 1000, - if - Force orelse (Age >= Interval) -> - Inflight1 = case {Type, Msg0} of - {publish, {PacketId, Msg}} -> - case emqx_message:is_expired(Msg) of - true -> - ok = emqx_metrics:inc('messages.expired'), - emqx_inflight:delete(PacketId, Inflight); - false -> - redeliver({PacketId, Msg}, Session), - emqx_inflight:update(PacketId, {publish, {PacketId, Msg}, Now}, Inflight) - end; - {pubrel, PacketId} -> - redeliver({pubrel, PacketId}, Session), - emqx_inflight:update(PacketId, {pubrel, PacketId, Now}, Inflight) - end, - retry_delivery(Force, Msgs, Now, Session#session{inflight = Inflight1}); + case maps:is_key(PacketId, AwaitingRel) of + false -> + DeliverResults = emqx_broker:publish(Msg), + AwaitingRel1 = maps:put(PacketId, Ts, AwaitingRel), + NSession = Session#session{awaiting_rel = AwaitingRel1}, + {ok, DeliverResults, ensure_await_rel_timer(NSession)}; + true -> + {error, ?RC_PACKET_IDENTIFIER_IN_USE} + end; true -> - ensure_retry_timer(Interval - max(0, Age), Session) + ?LOG(warning, "Dropped qos2 packet ~w for too many awaiting_rel", [PacketId]), + ok = emqx_metrics:inc('messages.qos2.dropped'), + {error, ?RC_RECEIVE_MAXIMUM_EXCEEDED} + end; + +%% QoS0/1 +publish(_PacketId, Msg, Session) -> + {ok, emqx_broker:publish(Msg)}. + +%% Client -> Broker: PUBACK +-spec(puback(emqx_mqtt:packet_id(), emqx_mqtt:reason_code(), session()) + -> puback_ret()). +puback(PacketId, _ReasonCode, Session = #session{inflight = Inflight, mqueue = Q}) -> + case emqx_inflight:lookup(PacketId, Inflight) of + {value, {publish, {_, Msg}, _Ts}} -> + %% #{client_id => ClientId, username => Username} + %% ok = emqx_hooks:run('message.acked', [], Msg]), + Inflight1 = emqx_inflight:delete(PacketId, Inflight), + Session1 = Session#session{inflight = Inflight1}, + case (emqx_mqueue:is_empty(Q) orelse emqx_mqueue:out(Q)) of + true -> {ok, Session1}; + {{value, Msg}, Q1} -> + {ok, Msg, Session1#session{mqueue = Q1}} + end; + false -> + ?LOG(warning, "The PUBACK PacketId ~w is not found", [PacketId]), + ok = emqx_metrics:inc('packets.puback.missed'), + {error, ?RC_PACKET_IDENTIFIER_NOT_FOUND} end. -%%------------------------------------------------------------------------------ -%% Send Will Message -%%------------------------------------------------------------------------------ +%% Client -> Broker: PUBREC +-spec(pubrec(emqx_mqtt:packet_id(), emqx_mqtt:reason_code(), session()) + -> {ok, session()} | {error, emqx_mqtt:reason_code()}). +pubrec(PacketId, _ReasonCode, Session = #session{inflight = Inflight}) -> + case emqx_inflight:lookup(PacketId, Inflight) of + {value, {publish, {_, Msg}, _Ts}} -> + %% ok = emqx_hooks:run('message.acked', [#{client_id => ClientId, username => Username}, Msg]), + Inflight1 = emqx_inflight:update(PacketId, {pubrel, PacketId, os:timestamp()}, Inflight), + {ok, Session#session{inflight = Inflight1}}; + {value, {pubrel, PacketId, _Ts}} -> + ?LOG(warning, "The PUBREC ~w is duplicated", [PacketId]), + {error, ?RC_PACKET_IDENTIFIER_IN_USE}; + none -> + ?LOG(warning, "The PUBREC ~w is not found.", [PacketId]), + ok = emqx_metrics:inc('packets.pubrec.missed'), + {error, ?RC_PACKET_IDENTIFIER_NOT_FOUND} + end. -send_willmsg(undefined) -> - ignore; -send_willmsg(WillMsg) -> - emqx_broker:publish(WillMsg). +%% Client -> Broker: PUBREL +-spec(pubrel(emqx_mqtt:packet_id(), emqx_mqtt:reason_code(), session()) + -> {ok, session()} | {error, emqx_mqtt:reason_code()}). +pubrel(PacketId, ReasonCode, Session = #session{awaiting_rel = AwaitingRel}) -> + case maps:take(PacketId, AwaitingRel) of + {_Ts, AwaitingRel1} -> + {ok, Session#session{awaiting_rel = AwaitingRel1}}; + error -> + ?LOG(warning, "The PUBREL PacketId ~w is not found", [PacketId]), + ok = emqx_metrics:inc('packets.pubrel.missed'), + {error, ?RC_PACKET_IDENTIFIER_NOT_FOUND} + end. + +%% Client -> Broker: PUBCOMP +-spec(pubcomp(emqx_mqtt:packet_id(), emqx_mqtt:reason_code(), session()) -> puback_ret()). +pubcomp(PacketId, ReasonCode, Session = #session{inflight = Inflight, mqueue = Q}) -> + case emqx_inflight:contain(PacketId, Inflight) of + true -> + Inflight1 = emqx_inflight:delete(PacketId, Inflight), + Session1 = Session#session{inflight = Inflight1}, + case (emqx_mqueue:is_empty(Q) orelse emqx_mqueue:out(Q)) of + true -> {ok, Session1}; + {{value, Msg}, Q1} -> + {ok, Msg, Session1#session{mqueue = Q1}} + end; + false -> + ?LOG(warning, "The PUBCOMP PacketId ~w is not found", [PacketId]), + ok = emqx_metrics:inc('packets.pubcomp.missed'), + {error, ?RC_PACKET_IDENTIFIER_NOT_FOUND} + end. + +%%-------------------------------------------------------------------- +%% Handle delivery +%%-------------------------------------------------------------------- + +deliver(Topic, Msg, Session = #session{subscriptions = SubMap}) -> + SubOpts = get_subopts(Topic, SubMap), + case enrich(SubOpts, Msg, Session) of + {ok, Msg1} -> + deliver(Msg1, Session); + ignore -> ignore + end. + +%% Enqueue message if the client has been disconnected +%% process_msg(Msg, Session = #session{conn_pid = undefined}) -> +%% {ignore, enqueue_msg(Msg, Session)}; + +deliver(Msg = #message{qos = ?QOS_0}, Session) -> + {ok, {publish, undefined, Msg}, Session}; + +deliver(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 -> + {ignore, enqueue(Msg, Session)}; + false -> + Publish = {publish, PacketId, Msg}, + NSession = await(PacketId, Msg, Session), + {ok, Publish, next_pkt_id(NSession)} + end. + +enqueue(Msg, Session = #session{mqueue = Q}) -> + emqx_pd:update_counter(enqueue_stats, 1), + {Dropped, NewQ} = emqx_mqueue:in(Msg, Q), + if + Dropped =/= undefined -> + %% SessProps = #{client_id => ClientId, username => Username}, + ok; %% = emqx_hooks:run('message.dropped', [SessProps, Dropped]); + true -> ok + end, + Session#session{mqueue = NewQ}. %%------------------------------------------------------------------------------ +%% Awaiting ACK for QoS1/QoS2 Messages +%%------------------------------------------------------------------------------ + +await(PacketId, Msg, Session = #session{inflight = Inflight}) -> + Publish = {publish, {PacketId, Msg}, os:timestamp()}, + Inflight1 = emqx_inflight:insert(PacketId, Publish, Inflight), + ensure_retry_timer(Session#session{inflight = Inflight1}). + +get_subopts(Topic, SubMap) -> + case maps:find(Topic, SubMap) of + {ok, #{nl := Nl, qos := QoS, rap := Rap, subid := SubId}} -> + [{nl, Nl}, {qos, QoS}, {rap, Rap}, {subid, SubId}]; + {ok, #{nl := Nl, qos := QoS, rap := Rap}} -> + [{nl, Nl}, {qos, QoS}, {rap, Rap}]; + error -> [] + end. + +enrich([], Msg, _Session) -> + {ok, Msg}; +%%enrich([{nl, 1}|_Opts], #message{from = ClientId}, #session{client_id = ClientId}) -> +%% ignore; +enrich([{nl, _}|Opts], Msg, Session) -> + enrich(Opts, Msg, Session); +enrich([{qos, SubQoS}|Opts], Msg = #message{qos = PubQoS}, Session = #session{upgrade_qos= true}) -> + enrich(Opts, Msg#message{qos = max(SubQoS, PubQoS)}, Session); +enrich([{qos, SubQoS}|Opts], Msg = #message{qos = PubQoS}, Session = #session{upgrade_qos= false}) -> + enrich(Opts, Msg#message{qos = min(SubQoS, PubQoS)}, Session); +enrich([{rap, _Rap}|Opts], Msg = #message{flags = Flags, headers = #{retained := true}}, Session = #session{}) -> + enrich(Opts, Msg#message{flags = maps:put(retain, true, Flags)}, Session); +enrich([{rap, 0}|Opts], Msg = #message{flags = Flags}, Session) -> + enrich(Opts, Msg#message{flags = maps:put(retain, false, Flags)}, Session); +enrich([{rap, _}|Opts], Msg, Session) -> + enrich(Opts, Msg, Session); +enrich([{subid, SubId}|Opts], Msg, Session) -> + enrich(Opts, emqx_message:set_header('Subscription-Identifier', SubId, Msg), Session). + +%%-------------------------------------------------------------------- +%% Ensure retry timer +%%-------------------------------------------------------------------- + +ensure_retry_timer(Session = #session{retry_interval = Interval, retry_timer = undefined}) -> + ensure_retry_timer(Interval, Session); +ensure_retry_timer(Session) -> + Session. + +ensure_retry_timer(Interval, Session = #session{retry_timer = undefined}) -> + TRef = emqx_misc:start_timer(Interval, retry_delivery), + Session#session{retry_timer = TRef}; +ensure_retry_timer(_Interval, Session) -> + Session. + +%%-------------------------------------------------------------------- +%% Check awaiting rel +%%-------------------------------------------------------------------- + +is_awaiting_full(_MaxAwaitingRel = 0, _AwaitingRel) -> + false; +is_awaiting_full(MaxAwaitingRel, AwaitingRel) -> + maps:size(AwaitingRel) >= MaxAwaitingRel. + +%%-------------------------------------------------------------------- +%% Ensure await_rel timer +%%-------------------------------------------------------------------- + +ensure_await_rel_timer(Session = #session{await_rel_timeout = Timeout, + await_rel_timer = undefined}) -> + ensure_await_rel_timer(Timeout, Session); +ensure_await_rel_timer(Session) -> + Session. + +ensure_await_rel_timer(Timeout, Session = #session{await_rel_timer = undefined}) -> + TRef = emqx_misc:start_timer(Timeout, check_awaiting_rel), + Session#session{await_rel_timer = TRef}; +ensure_await_rel_timer(_Timeout, Session) -> + Session. + +%%-------------------------------------------------------------------- %% Expire Awaiting Rel -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- expire_awaiting_rel(Session = #session{awaiting_rel = AwaitingRel}) -> case maps:size(AwaitingRel) of @@ -573,237 +538,9 @@ expire_awaiting_rel([{PacketId, Ts} | More], Now, ensure_await_rel_timer(Timeout - max(0, Age), Session) end. -%%------------------------------------------------------------------------------ -%% Check awaiting rel -%%------------------------------------------------------------------------------ - -is_awaiting_full(_MaxAwaitingRel = 0, _AwaitingRel) -> - false; -is_awaiting_full(MaxAwaitingRel, AwaitingRel) -> - maps:size(AwaitingRel) >= MaxAwaitingRel. - -%%------------------------------------------------------------------------------ -%% Dispatch messages -%%------------------------------------------------------------------------------ - -handle(Msgs, Session = #session{inflight = Inflight, - client_id = ClientId, - username = Username, - subscriptions = SubMap}) -> - SessProps = #{client_id => ClientId, username => Username}, - %% Drain the mailbox and batch deliver - Msgs1 = Msgs, %% drain_m(batch_n(Inflight), Msgs), - %% Ack the messages for shared subscription - Msgs2 = maybe_ack_shared(Msgs1, Session), - %% Process suboptions - Msgs3 = lists:foldr( - fun({Topic, Msg}, Acc) -> - SubOpts = find_subopts(Topic, SubMap), - case process_subopts(SubOpts, Msg, Session) of - {ok, Msg1} -> [Msg1|Acc]; - ignore -> - emqx_hooks:run('message.dropped', [SessProps, Msg]), - Acc - end - end, [], Msgs2), - batch_process(Msgs3, Session). - -%% Ack or nack the messages of shared subscription? -maybe_ack_shared(Msgs, Session) when is_list(Msgs) -> - lists:foldr( - fun({Topic, Msg}, Acc) -> - case maybe_ack_shared(Msg, Session) of - ok -> Acc; - Msg1 -> [{Topic, Msg1}|Acc] - end - end, [], Msgs); - -maybe_ack_shared(Msg, Session) -> - case emqx_shared_sub:is_ack_required(Msg) of - true -> do_ack_shared(Msg, Session); - false -> Msg - end. - -do_ack_shared(Msg, Session = #session{inflight = Inflight}) -> - case {true, %% is_connection_alive(Session), - emqx_inflight:is_full(Inflight)} of - {false, _} -> - %% Require ack, but we do not have connection - %% negative ack the message so it can try the next subscriber in the group - emqx_shared_sub:nack_no_connection(Msg); - {_, true} -> - emqx_shared_sub:maybe_nack_dropped(Msg); - _ -> - %% Ack QoS1/QoS2 messages when message is delivered to connection. - %% NOTE: NOT to wait for PUBACK because: - %% The sender is monitoring this session process, - %% if the message is delivered to client but connection or session crashes, - %% sender will try to dispatch the message to the next shared subscriber. - %% This violates spec as QoS2 messages are not allowed to be sent to more - %% than one member in the group. - emqx_shared_sub:maybe_ack(Msg) - end. - -process_subopts([], Msg, _Session) -> - {ok, Msg}; -process_subopts([{nl, 1}|_Opts], #message{from = ClientId}, #session{client_id = ClientId}) -> - ignore; -process_subopts([{nl, _}|Opts], Msg, Session) -> - process_subopts(Opts, Msg, Session); -process_subopts([{qos, SubQoS}|Opts], Msg = #message{qos = PubQoS}, - Session = #session{upgrade_qos= true}) -> - process_subopts(Opts, Msg#message{qos = max(SubQoS, PubQoS)}, Session); -process_subopts([{qos, SubQoS}|Opts], Msg = #message{qos = PubQoS}, - Session = #session{upgrade_qos= false}) -> - process_subopts(Opts, Msg#message{qos = min(SubQoS, PubQoS)}, Session); -process_subopts([{rap, _Rap}|Opts], Msg = #message{flags = Flags, headers = #{retained := true}}, Session = #session{}) -> - process_subopts(Opts, Msg#message{flags = maps:put(retain, true, Flags)}, Session); -process_subopts([{rap, 0}|Opts], Msg = #message{flags = Flags}, Session = #session{}) -> - process_subopts(Opts, Msg#message{flags = maps:put(retain, false, Flags)}, Session); -process_subopts([{rap, _}|Opts], Msg, Session) -> - process_subopts(Opts, Msg, Session); -process_subopts([{subid, SubId}|Opts], Msg, Session) -> - process_subopts(Opts, emqx_message:set_header('Subscription-Identifier', SubId, Msg), Session). - -find_subopts(Topic, SubMap) -> - case maps:find(Topic, SubMap) of - {ok, #{nl := Nl, qos := QoS, rap := Rap, subid := SubId}} -> - [{nl, Nl}, {qos, QoS}, {rap, Rap}, {subid, SubId}]; - {ok, #{nl := Nl, qos := QoS, rap := Rap}} -> - [{nl, Nl}, {qos, QoS}, {rap, Rap}]; - error -> [] - end. - -batch_process(Msgs, Session) -> - {ok, Publishes, NSession} = process_msgs(Msgs, [], Session), - ok = batch_deliver(Publishes, NSession), - NSession. - -process_msgs([], Publishes, Session) -> - {ok, lists:reverse(Publishes), Session}; - -process_msgs([Msg|Msgs], Publishes, Session) -> - case process_msg(Msg, Session) of - {ok, Publish, NSession} -> - process_msgs(Msgs, [Publish|Publishes], NSession); - {ignore, NSession} -> - process_msgs(Msgs, Publishes, NSession) - end. - -%% Enqueue message if the client has been disconnected -%% process_msg(Msg, Session = #session{conn_pid = undefined}) -> -%% {ignore, enqueue_msg(Msg, Session)}; - -%% Prepare the qos0 message delivery -process_msg(Msg = #message{qos = ?QOS_0}, Session) -> - {ok, {publish, undefined, Msg}, Session}; - -process_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 -> - {ignore, enqueue_msg(Msg, Session)}; - false -> - Publish = {publish, PacketId, Msg}, - NSession = await(PacketId, Msg, Session), - {ok, Publish, next_pkt_id(NSession)} - end. - -enqueue_msg(Msg, Session = #session{mqueue = Q, client_id = ClientId, username = Username}) -> - emqx_pd:update_counter(enqueue_stats, 1), - {Dropped, NewQ} = emqx_mqueue:in(Msg, Q), - if - Dropped =/= undefined -> - SessProps = #{client_id => ClientId, username => Username}, - ok = emqx_hooks:run('message.dropped', [SessProps, Dropped]); - true -> ok - end, - Session#session{mqueue = NewQ}. - -%%------------------------------------------------------------------------------ -%% Deliver -%%------------------------------------------------------------------------------ - -redeliver({PacketId, Msg = #message{qos = QoS}}, Session) when QoS =/= ?QOS_0 -> - Msg1 = emqx_message:set_flag(dup, Msg), - do_deliver(PacketId, Msg1, Session); - -redeliver({pubrel, PacketId}, #session{deliver_fun = DeliverFun}) -> - DeliverFun({pubrel, PacketId}). - -do_deliver(PacketId, Msg, #session{deliver_fun = DeliverFun}) -> - emqx_pd:update_counter(deliver_stats, 1), - DeliverFun({publish, PacketId, Msg}). - -batch_deliver(Publishes, #session{deliver_fun = DeliverFun}) -> - emqx_pd:update_counter(deliver_stats, length(Publishes)), - DeliverFun(Publishes). - -%%------------------------------------------------------------------------------ -%% Dequeue -%%------------------------------------------------------------------------------ - -dequeue(Session = #session{inflight = Inflight, mqueue = Q}) -> - case emqx_mqueue:is_empty(Q) - orelse emqx_inflight:is_full(Inflight) of - true -> Session; - false -> - %% TODO: - Msgs = [], - Q1 = Q, - %% {Msgs, Q1} = drain_q(batch_n(Inflight), [], Q), - batch_process(lists:reverse(Msgs), Session#session{mqueue = Q1}) - end. - -drain_q(Cnt, Msgs, Q) when Cnt =< 0 -> - {Msgs, Q}; - -drain_q(Cnt, Msgs, Q) -> - case emqx_mqueue:out(Q) of - {empty, _Q} -> {Msgs, Q}; - {{value, Msg}, Q1} -> - drain_q(Cnt-1, [Msg|Msgs], Q1) - end. - -%%------------------------------------------------------------------------------ -%% Ensure timers - -ensure_await_rel_timer(Session = #session{await_rel_timeout = Timeout, - await_rel_timer = undefined}) -> - ensure_await_rel_timer(Timeout, Session); -ensure_await_rel_timer(Session) -> - Session. - -ensure_await_rel_timer(Timeout, Session = #session{await_rel_timer = undefined}) -> - Session#session{await_rel_timer = emqx_misc:start_timer(Timeout, check_awaiting_rel)}; -ensure_await_rel_timer(_Timeout, Session) -> - Session. - -ensure_retry_timer(Session = #session{retry_interval = Interval, retry_timer = undefined}) -> - ensure_retry_timer(Interval, Session); -ensure_retry_timer(Session) -> - Session. - -ensure_retry_timer(Interval, Session = #session{retry_timer = undefined}) -> - Session#session{retry_timer = emqx_misc:start_timer(Interval, retry_delivery)}; -ensure_retry_timer(_Timeout, Session) -> - Session. - -ensure_expire_timer(Session = #session{expiry_interval = Interval}) - when Interval > 0 andalso Interval =/= 16#ffffffff -> - Session#session{expiry_timer = emqx_misc:start_timer(Interval * 1000, expired)}; -ensure_expire_timer(Session) -> - Session. - -ensure_will_delay_timer(Session = #session{will_msg = #message{headers = #{'Will-Delay-Interval' := WillDelayInterval}}}) -> - Session#session{will_delay_timer = emqx_misc:start_timer(WillDelayInterval * 1000, will_delay)}; -ensure_will_delay_timer(Session = #session{will_msg = WillMsg}) -> - send_willmsg(WillMsg), - Session#session{will_msg = undefined}. - %%-------------------------------------------------------------------- %% Next Packet Id +%%-------------------------------------------------------------------- next_pkt_id(Session = #session{next_pkt_id = 16#FFFF}) -> Session#session{next_pkt_id = 1}; diff --git a/src/emqx_shared_sub.erl b/src/emqx_shared_sub.erl index 33924888c..e6c8deefc 100644 --- a/src/emqx_shared_sub.erl +++ b/src/emqx_shared_sub.erl @@ -135,7 +135,7 @@ ack_enabled() -> do_dispatch(SubPid, Topic, Msg, _Type) when SubPid =:= self() -> %% Deadlock otherwise - _ = erlang:send(SubPid, {dispatch, Topic, Msg}), + _ = erlang:send(SubPid, {deliver, Topic, Msg}), ok; do_dispatch(SubPid, Topic, Msg, Type) -> dispatch_per_qos(SubPid, Topic, Msg, Type). @@ -143,18 +143,18 @@ do_dispatch(SubPid, Topic, Msg, Type) -> %% return either 'ok' (when everything is fine) or 'error' dispatch_per_qos(SubPid, Topic, #message{qos = ?QOS_0} = Msg, _Type) -> %% For QoS 0 message, send it as regular dispatch - _ = erlang:send(SubPid, {dispatch, Topic, Msg}), + _ = erlang:send(SubPid, {deliver, Topic, Msg}), ok; dispatch_per_qos(SubPid, Topic, Msg, retry) -> %% Retry implies all subscribers nack:ed, send again without ack - _ = erlang:send(SubPid, {dispatch, Topic, Msg}), + _ = erlang:send(SubPid, {deliver, Topic, Msg}), ok; dispatch_per_qos(SubPid, Topic, Msg, fresh) -> case ack_enabled() of true -> dispatch_with_ack(SubPid, Topic, Msg); false -> - _ = erlang:send(SubPid, {dispatch, Topic, Msg}), + _ = erlang:send(SubPid, {deliver, Topic, Msg}), ok end. @@ -162,7 +162,7 @@ dispatch_with_ack(SubPid, Topic, Msg) -> %% For QoS 1/2 message, expect an ack Ref = erlang:monitor(process, SubPid), Sender = self(), - _ = erlang:send(SubPid, {dispatch, Topic, with_ack_ref(Msg, {Sender, Ref})}), + _ = erlang:send(SubPid, {deliver, Topic, with_ack_ref(Msg, {Sender, Ref})}), Timeout = case Msg#message.qos of ?QOS_1 -> timer:seconds(?SHARED_SUB_QOS1_DISPATCH_TIMEOUT_SECONDS); ?QOS_2 -> infinity diff --git a/src/emqx_ws_connection.erl b/src/emqx_ws_channel.erl similarity index 91% rename from src/emqx_ws_connection.erl rename to src/emqx_ws_channel.erl index fcf145341..e2ce4e238 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_channel.erl @@ -14,6 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- +%% MQTT WebSocket Channel -module(emqx_ws_channel). -include("emqx.hrl"). @@ -170,7 +171,8 @@ websocket_init(#state{request = Req, options = Options}) -> parse_state = ParseState, proto_state = ProtoState, enable_stats = EnableStats, - idle_timeout = IdleTimout}}. + idle_timeout = IdleTimout + }}. send_fun(WsPid) -> fun(Packet, Options) -> @@ -242,10 +244,13 @@ websocket_info({call, From, session}, State = #state{proto_state = ProtoState}) gen_server:reply(From, emqx_protocol:session(ProtoState)), {ok, State}; -websocket_info({deliver, PubOrAck}, State = #state{proto_state = ProtoState}) -> - case emqx_protocol:deliver(PubOrAck, ProtoState) of - {ok, ProtoState1} -> - {ok, ensure_stats_timer(State#state{proto_state = ProtoState1})}; +websocket_info(Delivery, State = #state{proto_state = ProtoState}) + when element(1, Delivery) =:= deliver -> + case emqx_protocol:handle_out(Delivery, ProtoState) of + {ok, NProtoState} -> + {ok, State#state{proto_state = NProtoState}}; + {ok, Packet, NProtoState} -> + handle_outgoing(Packet, State#state{proto_state = NProtoState}); {error, Reason} -> shutdown(Reason, State) end; @@ -285,8 +290,8 @@ websocket_info({shutdown, conflict, {ClientId, NewPid}}, State) -> ?LOG(warning, "Clientid '~s' conflict with ~p", [ClientId, NewPid]), shutdown(conflict, State); -websocket_info({binary, Data}, State) -> - {reply, {binary, Data}, State}; +%% websocket_info({binary, Data}, State) -> +%% {reply, {binary, Data}, State}; websocket_info({shutdown, Reason}, State) -> shutdown(Reason, State); @@ -317,9 +322,12 @@ terminate(SockError, _Req, #state{keepalive = Keepalive, %%-------------------------------------------------------------------- handle_incoming(Packet, SuccFun, State = #state{proto_state = ProtoState}) -> - case emqx_protocol:received(Packet, ProtoState) of + case emqx_protocol:handle_in(Packet, ProtoState) of {ok, NProtoState} -> SuccFun(State#state{proto_state = NProtoState}); + {ok, OutPacket, NProtoState} -> + %% TODO: How to call SuccFun??? + handle_outgoing(OutPacket, State#state{proto_state = NProtoState}); {error, Reason} -> ?LOG(error, "Protocol error: ~p", [Reason]), shutdown(Reason, State); @@ -329,7 +337,12 @@ handle_incoming(Packet, SuccFun, State = #state{proto_state = ProtoState}) -> shutdown(Error, State#state{proto_state = NProtoState}) end. - +handle_outgoing(Packet, State = #state{proto_state = _NProtoState}) -> + Data = emqx_frame:serialize(Packet), %% TODO:, Options), + BinSize = iolist_size(Data), + emqx_pd:update_counter(send_cnt, 1), + emqx_pd:update_counter(send_oct, BinSize), + {reply, {binary, Data}, ensure_stats_timer(State)}. ensure_stats_timer(State = #state{enable_stats = true, stats_timer = undefined, From 67b726643849a73b282d1d74bb3bd9c5ff01cf21 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 25 Jun 2019 15:58:09 +0800 Subject: [PATCH 12/33] Rename TimerInterval to Interval --- src/emqx_flapping.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emqx_flapping.erl b/src/emqx_flapping.erl index 9a8e4e43b..ba3f444b7 100644 --- a/src/emqx_flapping.erl +++ b/src/emqx_flapping.erl @@ -105,7 +105,7 @@ start_link() -> gen_statem:start_link({local, ?MODULE}, ?MODULE, [], []). init([]) -> - TimerInterval = emqx_config:get_env(flapping_clean_interval, ?default_flapping_clean_interval), + Interval = emqx_config:get_env(flapping_clean_interval, ?default_flapping_clean_interval), TabOpts = [ public , set , {keypos, 2} From 9ee0a4d17180944c7a8312830248bd8530dd3bff Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 25 Jun 2019 17:35:58 +0800 Subject: [PATCH 13/33] Fix the function_clause error when publishing QoS2 message --- src/emqx_protocol.erl | 28 +++++++++++++++++++++------- test/emqx_ws_channel_SUITE.erl | 8 +++++++- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 699963644..b1396db3c 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -384,7 +384,7 @@ process(?CONNECT_PACKET( PState4 = PState3#protocol{session = Session, connected = true, credentials = keepsafety(Credentials0)}, ok = emqx_cm:register_channel(client_id(PState4)), - true = emqx_cm:set_conn_attrs(client_id(PState4), attrs(PState4)), + ok = emqx_cm:set_conn_attrs(client_id(PState4), attrs(PState4)), %% Start keepalive start_keepalive(Keepalive, PState4), %% Success @@ -471,7 +471,7 @@ process(Packet = ?SUBSCRIBE_PACKET(PacketId, Properties, RawTopicFilters), {ok, TopicFilters} -> TopicFilters0 = emqx_hooks:run_fold('client.subscribe', [Credentials], TopicFilters), TopicFilters1 = emqx_mountpoint:mount(mountpoint(Credentials), TopicFilters0), - {ok, ReasonCodes, NSession} = emqx_session:subscribe(TopicFilters1, Session), + {ok, ReasonCodes, NSession} = emqx_session:subscribe(Credentials, TopicFilters1, Session), handle_out({suback, PacketId, ReasonCodes}, PState#protocol{session = NSession}); {error, TopicFilters} -> {SubTopics, ReasonCodes} = @@ -490,7 +490,7 @@ process(?UNSUBSCRIBE_PACKET(PacketId, Properties, RawTopicFilters), TopicFilters = emqx_hooks:run_fold('client.unsubscribe', [Credentials], parse_topic_filters(?UNSUBSCRIBE, RawTopicFilters)), TopicFilters1 = emqx_mountpoint:mount(mountpoint(Credentials), TopicFilters), - {ok, ReasonCodes, NSession} = emqx_session:unsubscribe(TopicFilters1, Session), + {ok, ReasonCodes, NSession} = emqx_session:unsubscribe(Credentials, TopicFilters1, Session), handle_out({unsuback, PacketId, ReasonCodes}, PState#protocol{session = NSession}); process(?PACKET(?PINGREQ), PState) -> @@ -555,16 +555,16 @@ do_publish(Packet = ?PUBLISH_PACKET(QoS, PacketId), puback(?QOS_0, _PacketId, _Result, PState) -> {ok, PState}; -puback(?QOS_1, PacketId, {ok, []}, PState) -> +puback(?QOS_1, PacketId, [], PState) -> handle_out({puback, PacketId, ?RC_NO_MATCHING_SUBSCRIBERS}, PState); %%TODO: calc the deliver count? -puback(?QOS_1, PacketId, {ok, _Result}, PState) -> +puback(?QOS_1, PacketId, Result, PState) when is_list(Result) -> handle_out({puback, PacketId, ?RC_SUCCESS}, PState); puback(?QOS_1, PacketId, {error, ReasonCode}, PState) -> handle_out({puback, PacketId, ReasonCode}, PState); -puback(?QOS_2, PacketId, {ok, []}, PState) -> +puback(?QOS_2, PacketId, [], PState) -> handle_out({pubrec, PacketId, ?RC_NO_MATCHING_SUBSCRIBERS}, PState); -puback(?QOS_2, PacketId, {ok, _Result}, PState) -> +puback(?QOS_2, PacketId, Result, PState) when is_list(Result) -> handle_out({pubrec, PacketId, ?RC_SUCCESS}, PState); puback(?QOS_2, PacketId, {error, ReasonCode}, PState) -> handle_out({pubrec, PacketId, ReasonCode}, PState). @@ -632,6 +632,20 @@ handle_out({connack, ReasonCode}, PState = #protocol{proto_ver = ProtoVer}) -> Reason = emqx_reason_codes:name(ReasonCode, ProtoVer), {error, Reason, ?CONNACK_PACKET(ReasonCode), PState}; +handle_out({deliver, Topic, Msg}, PState = #protocol{session = Session}) -> + case emqx_session:deliver(Topic, Msg, Session) of + {ok, Publish, NSession} -> + handle_out(Publish, PState#protocol{session = NSession}); + {ok, NSession} -> + {ok, PState#protocol{session = NSession}} + end; + +handle_out({publish, PacketId, Msg}, PState = #protocol{credentials = Credentials}) -> + Msg0 = emqx_hooks:run_fold('message.deliver', [Credentials], Msg), + Msg1 = emqx_message:update_expiry(Msg0), + Msg2 = emqx_mountpoint:unmount(mountpoint(Credentials), Msg1), + {ok, emqx_packet:from_message(PacketId, Msg2), PState}; + handle_out({puback, PacketId, ReasonCode}, PState) -> {ok, ?PUBACK_PACKET(PacketId, ReasonCode), PState}; %% TODO: diff --git a/test/emqx_ws_channel_SUITE.erl b/test/emqx_ws_channel_SUITE.erl index 979dc77e9..e92c55d48 100644 --- a/test/emqx_ws_channel_SUITE.erl +++ b/test/emqx_ws_channel_SUITE.erl @@ -66,7 +66,13 @@ t_ws_auth_failure(_Config) -> t_ws_other_type_frame(_Config) -> WS = rfc6455_client:new("ws://127.0.0.1:8083" ++ "/mqtt", self()), {ok, _} = rfc6455_client:open(WS), - ok = rfc6455_client:send_binary(WS, raw_send_serialize(?CLIENT)), + Connect = ?CONNECT_PACKET( + #mqtt_packet_connect{ + client_id = <<"mqtt_client">>, + username = <<"admin">>, + password = <<"public">> + }), + ok = rfc6455_client:send_binary(WS, raw_send_serialize(Connect)), {binary, Bin} = rfc6455_client:recv(WS), Connack = ?CONNACK_PACKET(?CONNACK_ACCEPT), {ok, Connack, <<>>, _} = raw_recv_pase(Bin), From 177dc441551f0bb4aa0336099c0d233ebf5d7ca2 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 2 Jul 2019 13:39:23 +0800 Subject: [PATCH 14/33] Define 'Otherwise' macro --- include/types.hrl | 2 ++ src/{emqx_channel.erl => emqx_connection.erl} | 0 src/{emqx_ws_channel.erl => emqx_ws_connection.erl} | 0 3 files changed, 2 insertions(+) rename src/{emqx_channel.erl => emqx_connection.erl} (100%) rename src/{emqx_ws_channel.erl => emqx_ws_connection.erl} (100%) diff --git a/include/types.hrl b/include/types.hrl index d8e96a20e..f37d11a66 100644 --- a/include/types.hrl +++ b/include/types.hrl @@ -14,6 +14,8 @@ %% limitations under the License. %%-------------------------------------------------------------------- +-define(Otherwise, true). + -type(maybe(T) :: undefined | T). -type(startlink_ret() :: {ok, pid()} | ignore | {error, term()}). diff --git a/src/emqx_channel.erl b/src/emqx_connection.erl similarity index 100% rename from src/emqx_channel.erl rename to src/emqx_connection.erl diff --git a/src/emqx_ws_channel.erl b/src/emqx_ws_connection.erl similarity index 100% rename from src/emqx_ws_channel.erl rename to src/emqx_ws_connection.erl From 42da51e1c5f7c8b20c401508922434bab2a917f1 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 2 Jul 2019 13:40:52 +0800 Subject: [PATCH 15/33] Implement batch delivery for the new channel design - Implement a new emqx_channel module - Improve the emqx_frame:serialize/2 API - Improve the emqx_connection, emqx_ws_connection modules --- include/emqx_client.hrl | 1 - src/emqx_channel.erl | 581 +++++++++++++++++++++ src/emqx_cm.erl | 4 +- src/emqx_connection.erl | 328 +++++++----- src/emqx_endpoint.erl | 95 ++++ src/emqx_frame.erl | 43 +- src/emqx_listeners.erl | 4 +- src/emqx_misc.erl | 19 +- src/emqx_mountpoint.erl | 8 +- src/emqx_pd.erl | 6 + src/emqx_protocol.erl | 1010 ------------------------------------ src/emqx_reason_codes.erl | 4 + src/emqx_session.erl | 32 +- src/emqx_ws_connection.erl | 180 ++++--- 14 files changed, 1023 insertions(+), 1292 deletions(-) create mode 100644 src/emqx_channel.erl create mode 100644 src/emqx_endpoint.erl delete mode 100644 src/emqx_protocol.erl diff --git a/include/emqx_client.hrl b/include/emqx_client.hrl index bf2f49283..b23a02f65 100644 --- a/include/emqx_client.hrl +++ b/include/emqx_client.hrl @@ -12,7 +12,6 @@ %% See the License for the specific language governing permissions and %% limitations under the License. - -ifndef(EMQX_CLIENT_HRL). -define(EMQX_CLIENT_HRL, true). -include("emqx_mqtt.hrl"). diff --git a/src/emqx_channel.erl b/src/emqx_channel.erl new file mode 100644 index 000000000..9799719b6 --- /dev/null +++ b/src/emqx_channel.erl @@ -0,0 +1,581 @@ +%%-------------------------------------------------------------------- +%% 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. +%%-------------------------------------------------------------------- + +%% MQTT Channel +-module(emqx_channel). + +-include("emqx.hrl"). +-include("emqx_mqtt.hrl"). +-include("logger.hrl"). +-include("types.hrl"). + +-logger_header("[Channel]"). + +-export([ attrs/1 ]). + +-export([ zone/1 + , client_id/1 + , conn_mod/1 + , endpoint/1 + , proto_ver/1 + , keepalive/1 + , session/1 + ]). + +-export([ init/2 + , handle_in/2 + , handle_out/2 + , handle_timeout/3 + , terminate/2 + ]). + +-export_type([channel/0]). + +-record(channel, { + conn_mod :: maybe(module()), + endpoint :: emqx_endpoint:endpoint(), + proto_name :: binary(), + proto_ver :: emqx_mqtt:version(), + keepalive :: non_neg_integer(), + session :: emqx_session:session(), + will_msg :: emqx_types:message(), + enable_acl :: boolean(), + is_bridge :: boolean(), + connected :: boolean(), + topic_aliases :: map(), + alias_maximum :: map(), + connected_at :: erlang:timestamp() + }). + +-opaque(channel() :: #channel{}). + +attrs(#channel{endpoint = Endpoint, session = Session}) -> + maps:merge(emqx_endpoint:to_map(Endpoint), + emqx_session:attrs(Session)). + +zone(#channel{endpoint = Endpoint}) -> + emqx_endpoint:zone(Endpoint). + +-spec(client_id(channel()) -> emqx_types:client_id()). +client_id(#channel{endpoint = Endpoint}) -> + emqx_endpoint:client_id(Endpoint). + +-spec(conn_mod(channel()) -> module()). +conn_mod(#channel{conn_mod = ConnMod}) -> + ConnMod. + +-spec(endpoint(channel()) -> emqx_endpoint:endpoint()). +endpoint(#channel{endpoint = Endpoint}) -> + Endpoint. + +-spec(proto_ver(channel()) -> emqx_mqtt:version()). +proto_ver(#channel{proto_ver = ProtoVer}) -> + ProtoVer. + +keepalive(#channel{keepalive = Keepalive}) -> + Keepalive. + +-spec(session(channel()) -> emqx_session:session()). +session(#channel{session = Session}) -> + Session. + +-spec(init(map(), proplists:proplist()) -> channel()). +init(ConnInfo = #{peername := Peername, + sockname := Sockname, + conn_mod := ConnMod}, Options) -> + Zone = proplists:get_value(zone, Options), + Peercert = maps:get(peercert, ConnInfo, nossl), + Username = peer_cert_as_username(Peercert, Options), + Mountpoint = emqx_zone:get_env(Zone, mountpoint), + WsCookie = maps:get(ws_cookie, ConnInfo, undefined), + Endpoint = emqx_endpoint:new(#{zone => Zone, + peername => Peername, + sockname => Sockname, + username => Username, + peercert => Peercert, + mountpoint => Mountpoint, + ws_cookie => WsCookie + }), + EnableAcl = emqx_zone:get_env(Zone, enable_acl, false), + #channel{conn_mod = ConnMod, + endpoint = Endpoint, + enable_acl = EnableAcl, + is_bridge = false, + connected = false + }. + +peer_cert_as_username(Peercert, Options) -> + case proplists:get_value(peer_cert_as_username, Options) of + cn -> esockd_peercert:common_name(Peercert); + dn -> esockd_peercert:subject(Peercert); + crt -> Peercert; + _ -> undefined + end. + +%%-------------------------------------------------------------------- +%% Handle incoming packet +%%-------------------------------------------------------------------- + +-spec(handle_in(emqx_mqtt:packet(), channel()) + -> {ok, channel()} + | {ok, emqx_mqtt:packet(), channel()} + | {error, Reason :: term(), channel()} + | {stop, Error :: atom(), channel()}). +handle_in(?CONNECT_PACKET( + #mqtt_packet_connect{proto_name = ProtoName, + proto_ver = ProtoVer, + is_bridge = IsBridge, + client_id = ClientId, + username = Username, + password = Password, + keepalive = Keepalive} = ConnPkt), + Channel = #channel{endpoint = Endpoint}) -> + Endpoint1 = emqx_endpoint:update(#{client_id => ClientId, + username => Username, + password => Password + }, Endpoint), + emqx_logger:set_metadata_client_id(ClientId), + WillMsg = emqx_packet:will_msg(ConnPkt), + Channel1 = Channel#channel{endpoint = Endpoint1, + proto_name = ProtoName, + proto_ver = ProtoVer, + is_bridge = IsBridge, + keepalive = Keepalive, + will_msg = WillMsg + }, + %% fun validate_packet/2, + case pipeline([fun check_connect/2, + fun handle_connect/2], ConnPkt, Channel1) of + {ok, SP, Channel2} -> + handle_out({connack, ?RC_SUCCESS, sp(SP)}, Channel2); + {error, ReasonCode} -> + handle_out({connack, ReasonCode}, Channel1); + {error, ReasonCode, Channel2} -> + handle_out({connack, ReasonCode}, Channel2) + end; + +handle_in(Packet = ?PUBLISH_PACKET(QoS, Topic, PacketId), Channel) -> + case pipeline([fun validate_packet/2, + fun check_pub_caps/2, + fun check_pub_acl/2, + fun handle_publish/2], Packet, Channel) of + {error, ReasonCode} -> + ?LOG(warning, "Cannot publish qos~w message to ~s due to ~s", + [QoS, Topic, emqx_reason_codes:text(ReasonCode)]), + handle_out(case QoS of + ?QOS_0 -> {puberr, ReasonCode}; + ?QOS_1 -> {puback, PacketId, ReasonCode}; + ?QOS_2 -> {pubrec, PacketId, ReasonCode} + end, Channel); + Result -> Result + end; + +handle_in(?PUBACK_PACKET(PacketId, ReasonCode), Channel = #channel{session = Session}) -> + case emqx_session:puback(PacketId, ReasonCode, Session) of + {ok, NSession} -> + {ok, Channel#channel{session = NSession}}; + {error, _NotFound} -> + %% TODO: metrics? error msg? + {ok, Channel} + end; + +handle_in(?PUBREC_PACKET(PacketId, ReasonCode), Channel = #channel{session = Session}) -> + case emqx_session:pubrec(PacketId, ReasonCode, Session) of + {ok, NSession} -> + handle_out({pubrel, PacketId}, Channel#channel{session = NSession}); + {error, ReasonCode} -> + handle_out({pubrel, PacketId, ReasonCode}, Channel) + end; + +handle_in(?PUBREL_PACKET(PacketId, ReasonCode), Channel = #channel{session = Session}) -> + case emqx_session:pubrel(PacketId, ReasonCode, Session) of + {ok, NSession} -> + handle_out({pubcomp, PacketId}, Channel#channel{session = NSession}); + {error, ReasonCode} -> + handle_out({pubcomp, PacketId, ReasonCode}, Channel) + end; + +handle_in(?PUBCOMP_PACKET(PacketId, ReasonCode), Channel = #channel{session = Session}) -> + case emqx_session:pubcomp(PacketId, ReasonCode, Session) of + {ok, NSession} -> + {ok, Channel#channel{session = NSession}}; + {error, _ReasonCode} -> + %% TODO: How to handle the reason code? + {ok, Channel} + end; + +handle_in(?SUBSCRIBE_PACKET(PacketId, Properties, RawTopicFilters), + Channel = #channel{endpoint = Endpoint, session = Session}) -> + case check_subscribe(parse_topic_filters(?SUBSCRIBE, RawTopicFilters), Channel) of + {ok, TopicFilters} -> + TopicFilters1 = preprocess_topic_filters(?SUBSCRIBE, Endpoint, + enrich_subopts(TopicFilters, Channel)), + {ok, ReasonCodes, NSession} = emqx_session:subscribe(emqx_endpoint:to_map(Endpoint), + TopicFilters1, Session), + handle_out({suback, PacketId, ReasonCodes}, Channel#channel{session = NSession}); + {error, TopicFilters} -> + {Topics, ReasonCodes} = lists:unzip([{Topic, RC} || {Topic, #{rc := RC}} <- TopicFilters]), + ?LOG(warning, "Cannot subscribe ~p due to ~p", + [Topics, [emqx_reason_codes:text(R) || R <- ReasonCodes]]), + %% Tell the client that all subscriptions has been failed. + ReasonCodes1 = lists:map(fun(?RC_SUCCESS) -> + ?RC_IMPLEMENTATION_SPECIFIC_ERROR; + (RC) -> RC + end, ReasonCodes), + handle_out({suback, PacketId, ReasonCodes1}, Channel) + end; + +handle_in(?UNSUBSCRIBE_PACKET(PacketId, Properties, RawTopicFilters), + Channel = #channel{endpoint = Endpoint, session = Session}) -> + TopicFilters = preprocess_topic_filters( + ?UNSUBSCRIBE, Endpoint, + parse_topic_filters(?UNSUBSCRIBE, RawTopicFilters)), + {ok, ReasonCodes, NSession} = emqx_session:unsubscribe(emqx_endpoint:to_map(Endpoint), TopicFilters, Session), + handle_out({unsuback, PacketId, ReasonCodes}, Channel#channel{session = NSession}); + +handle_in(?PACKET(?PINGREQ), Channel) -> + {ok, ?PACKET(?PINGRESP), Channel}; + +handle_in(?DISCONNECT_PACKET(?RC_SUCCESS), Channel) -> + %% Clear will msg + {stop, normal, Channel#channel{will_msg = undefined}}; + +handle_in(?DISCONNECT_PACKET(RC), Channel = #channel{proto_ver = Ver}) -> + %% TODO: + %% {stop, {shutdown, abnormal_disconnet}, Channel}; + {stop, {shutdown, emqx_reason_codes:name(RC, Ver)}, Channel}; + +handle_in(?AUTH_PACKET(), Channel) -> + %%TODO: implement later. + {ok, Channel}; + +handle_in(Packet, Channel) -> + io:format("In: ~p~n", [Packet]), + {ok, Channel}. + +%%-------------------------------------------------------------------- +%% Handle outgoing packet +%%-------------------------------------------------------------------- + +handle_out({connack, ?RC_SUCCESS, SP}, Channel = #channel{endpoint = Endpoint}) -> + ok = emqx_hooks:run('client.connected', + [emqx_endpoint:to_map(Endpoint), ?RC_SUCCESS, attrs(Channel)]), + Props = #{}, %% TODO: ... + {ok, ?CONNACK_PACKET(?RC_SUCCESS, SP, Props), Channel}; + +handle_out({connack, ReasonCode}, Channel = #channel{endpoint = Endpoint, + proto_ver = ProtoVer}) -> + ok = emqx_hooks:run('client.connected', + [emqx_endpoint:to_map(Endpoint), ReasonCode, attrs(Channel)]), + ReasonCode1 = if + ProtoVer == ?MQTT_PROTO_V5 -> ReasonCode; + true -> emqx_reason_codes:compat(connack, ReasonCode) + end, + Reason = emqx_reason_codes:name(ReasonCode1, ProtoVer), + {error, Reason, ?CONNACK_PACKET(ReasonCode1), Channel}; + +handle_out(Delivers, Channel = #channel{endpoint = Endpoint, session = Session}) + when is_list(Delivers) -> + case emqx_session:deliver([{Topic, Msg} || {deliver, Topic, Msg} <- Delivers], Session) of + {ok, Publishes, NSession} -> + Credentials = emqx_endpoint:credentials(Endpoint), + Packets = lists:map(fun({publish, PacketId, Msg}) -> + Msg0 = emqx_hooks:run_fold('message.deliver', [Credentials], Msg), + Msg1 = emqx_message:update_expiry(Msg0), + Msg2 = emqx_mountpoint:unmount(emqx_endpoint:mountpoint(Endpoint), Msg1), + emqx_packet:from_message(PacketId, Msg2) + end, Publishes), + {ok, Packets, Channel#channel{session = NSession}}; + {ok, NSession} -> + {ok, Channel#channel{session = NSession}} + end; + +handle_out({publish, PacketId, Msg}, Channel = #channel{endpoint = Endpoint}) -> + Credentials = emqx_endpoint:credentials(Endpoint), + Msg0 = emqx_hooks:run_fold('message.deliver', [Credentials], Msg), + Msg1 = emqx_message:update_expiry(Msg0), + Msg2 = emqx_mountpoint:unmount( + emqx_endpoint:mountpoint(Credentials), Msg1), + {ok, emqx_packet:from_message(PacketId, Msg2), Channel}; + +handle_out({puberr, ReasonCode}, Channel) -> + {ok, Channel}; + +handle_out({puback, PacketId, ReasonCode}, Channel) -> + {ok, ?PUBACK_PACKET(PacketId, ReasonCode), Channel}; + +handle_out({pubrel, PacketId}, Channel) -> + {ok, ?PUBREL_PACKET(PacketId), Channel}; +handle_out({pubrel, PacketId, ReasonCode}, Channel) -> + {ok, ?PUBREL_PACKET(PacketId, ReasonCode), Channel}; + +handle_out({pubrec, PacketId, ReasonCode}, Channel) -> + {ok, ?PUBREC_PACKET(PacketId, ReasonCode), Channel}; + +handle_out({pubcomp, PacketId}, Channel) -> + {ok, ?PUBCOMP_PACKET(PacketId), Channel}; +handle_out({pubcomp, PacketId, ReasonCode}, Channel) -> + {ok, ?PUBCOMP_PACKET(PacketId, ReasonCode), Channel}; + +handle_out({suback, PacketId, ReasonCodes}, Channel = #channel{proto_ver = ?MQTT_PROTO_V5}) -> + %% TODO: ACL Deny + {ok, ?SUBACK_PACKET(PacketId, ReasonCodes), Channel}; +handle_out({suback, PacketId, ReasonCodes}, Channel) -> + %% TODO: ACL Deny + ReasonCodes1 = [emqx_reason_codes:compat(suback, RC) || RC <- ReasonCodes], + {ok, ?SUBACK_PACKET(PacketId, ReasonCodes1), Channel}; + +handle_out({unsuback, PacketId, ReasonCodes}, Channel = #channel{proto_ver = ?MQTT_PROTO_V5}) -> + {ok, ?UNSUBACK_PACKET(PacketId, ReasonCodes), Channel}; +%% Ignore reason codes if not MQTT5 +handle_out({unsuback, PacketId, _ReasonCodes}, Channel) -> + {ok, ?UNSUBACK_PACKET(PacketId), Channel}; + +handle_out(Packet, State) -> + io:format("Out: ~p~n", [Packet]), + {ok, State}. + +handle_deliver(Msg, State) -> + io:format("Msg: ~p~n", [Msg]), + %% Msg -> Pub + {ok, State}. + +handle_timeout(Name, TRef, State) -> + io:format("Timeout: ~s ~p~n", [Name, TRef]), + {ok, State}. + +terminate(Reason, _State) -> + %%io:format("Terminated for ~p~n", [Reason]), + ok. + +%%-------------------------------------------------------------------- +%% Check Connect Packet +%%-------------------------------------------------------------------- + +check_connect(_ConnPkt, Channel) -> + {ok, Channel}. + +%%-------------------------------------------------------------------- +%% Handle Connect Packet +%%-------------------------------------------------------------------- + +handle_connect(#mqtt_packet_connect{proto_name = ProtoName, + proto_ver = ProtoVer, + is_bridge = IsBridge, + clean_start = CleanStart, + keepalive = Keepalive, + properties = ConnProps, + client_id = ClientId, + username = Username, + password = Password} = ConnPkt, + Channel = #channel{endpoint = Endpoint}) -> + Credentials = emqx_endpoint:credentials(Endpoint), + case emqx_access_control:authenticate( + Credentials#{password => Password}) of + {ok, Credentials1} -> + Endpoint1 = emqx_endpoint:update(Credentials1, Endpoint), + %% Open session + case open_session(ConnPkt, Channel) of + {ok, Session, SP} -> + Channel1 = Channel#channel{endpoint = Endpoint1, + session = Session, + connected = true, + connected_at = os:timestamp() + }, + ok = emqx_cm:register_channel(ClientId), + {ok, SP, Channel1}; + {error, Error} -> + ?LOG(error, "Failed to open session: ~p", [Error]), + {error, ?RC_UNSPECIFIED_ERROR, Channel#channel{endpoint = Endpoint1}} + end; + {error, Reason} -> + ?LOG(warning, "Client ~s (Username: '~s') login failed for ~p", + [ClientId, Username, Reason]), + {error, emqx_reason_codes:connack_error(Reason), Channel} + end. + +open_session(#mqtt_packet_connect{clean_start = CleanStart, + %%properties = ConnProps, + client_id = ClientId, + username = Username} = ConnPkt, + Channel = #channel{endpoint = Endpoint}) -> + emqx_cm:open_session(maps:merge(emqx_endpoint:to_map(Endpoint), + #{clean_start => CleanStart, + max_inflight => 0, + expiry_interval => 0})). + +%%-------------------------------------------------------------------- +%% Handle Publish Message: Client -> Broker +%%-------------------------------------------------------------------- + +handle_publish(Packet = ?PUBLISH_PACKET(QoS, Topic, PacketId), + Channel = #channel{endpoint = Endpoint}) -> + Credentials = emqx_endpoint:credentials(Endpoint), + %% TODO: ugly... publish_to_msg(...) + Msg = emqx_packet:to_message(Credentials, Packet), + Msg1 = emqx_mountpoint:mount( + emqx_endpoint:mountpoint(Endpoint), Msg), + Msg2 = emqx_message:set_flag(dup, false, Msg1), + handle_publish(PacketId, Msg2, Channel). + +handle_publish(_PacketId, Msg = #message{qos = ?QOS_0}, Channel) -> + _ = emqx_broker:publish(Msg), + {ok, Channel}; + +handle_publish(PacketId, Msg = #message{qos = ?QOS_1}, Channel) -> + Results = emqx_broker:publish(Msg), + ReasonCode = emqx_reason_codes:puback(Results), + handle_out({puback, PacketId, ReasonCode}, Channel); + +handle_publish(PacketId, Msg = #message{qos = ?QOS_2}, + Channel = #channel{session = Session}) -> + case emqx_session:publish(PacketId, Msg, Session) of + {ok, Results, NSession} -> + ReasonCode = emqx_reason_codes:puback(Results), + handle_out({pubrec, PacketId, ReasonCode}, + Channel#channel{session = NSession}); + {error, ReasonCode} -> + handle_out({pubrec, PacketId, ReasonCode}, Channel) + end. + +%%-------------------------------------------------------------------- +%% Validate Incoming Packet +%%-------------------------------------------------------------------- + +-spec(validate_packet(emqx_mqtt:packet(), channel()) -> ok). +validate_packet(Packet, _Channel) -> + try emqx_packet:validate(Packet) of + true -> ok + catch + error:protocol_error -> + {error, ?RC_PROTOCOL_ERROR}; + error:subscription_identifier_invalid -> + {error, ?RC_SUBSCRIPTION_IDENTIFIERS_NOT_SUPPORTED}; + error:topic_alias_invalid -> + {error, ?RC_TOPIC_ALIAS_INVALID}; + error:topic_filters_invalid -> + {error, ?RC_TOPIC_FILTER_INVALID}; + error:topic_name_invalid -> + {error, ?RC_TOPIC_FILTER_INVALID}; + error:_Reason -> + {error, ?RC_MALFORMED_PACKET} + end. + +%%-------------------------------------------------------------------- +%% Preprocess MQTT Properties +%%-------------------------------------------------------------------- + +%% TODO:... + +%%-------------------------------------------------------------------- +%% Check Publish +%%-------------------------------------------------------------------- + +check_pub_caps(#mqtt_packet{header = #mqtt_packet_header{qos = QoS, + retain = Retain}, + variable = #mqtt_packet_publish{}}, + #channel{endpoint = Endpoint}) -> + emqx_mqtt_caps:check_pub(emqx_endpoint:zone(Endpoint), + #{qos => QoS, retain => Retain}). + +check_pub_acl(_Packet, #channel{enable_acl = false}) -> + ok; +check_pub_acl(#mqtt_packet{variable = #mqtt_packet_publish{topic_name = Topic}}, + #channel{endpoint = Endpoint}) -> + case emqx_endpoint:is_superuser(Endpoint) of + true -> ok; + false -> + do_acl_check(Endpoint, publish, Topic) + end. + +check_sub_acl(_Packet, #channel{enable_acl = false}) -> + ok. + +do_acl_check(Endpoint, PubSub, Topic) -> + case emqx_access_control:check_acl( + emqx_endpoint:to_map(Endpoint), PubSub, Topic) of + allow -> ok; + deny -> {error, ?RC_NOT_AUTHORIZED} + end. + +%%-------------------------------------------------------------------- +%% Check Subscribe Packet +%%-------------------------------------------------------------------- + +check_subscribe(TopicFilters, _Channel) -> + {ok, TopicFilters}. + +%%-------------------------------------------------------------------- +%% Pipeline +%%-------------------------------------------------------------------- + +pipeline([Fun], Packet, Channel) -> + Fun(Packet, Channel); +pipeline([Fun|More], Packet, Channel) -> + case Fun(Packet, Channel) of + ok -> pipeline(More, Packet, Channel); + {ok, NChannel} -> + pipeline(More, Packet, NChannel); + {ok, NPacket, NChannel} -> + pipeline(More, NPacket, NChannel); + {error, Reason} -> + {error, Reason} + end. + +%%-------------------------------------------------------------------- +%% Preprocess topic filters +%%-------------------------------------------------------------------- + +preprocess_topic_filters(Type, Endpoint, TopicFilters) -> + TopicFilters1 = emqx_hooks:run_fold(case Type of + ?SUBSCRIBE -> 'client.subscribe'; + ?UNSUBSCRIBE -> 'client.unsubscribe' + end, + [emqx_endpoint:credentials(Endpoint)], + TopicFilters), + emqx_mountpoint:mount(emqx_endpoint:mountpoint(Endpoint), TopicFilters1). + +%%-------------------------------------------------------------------- +%% Enrich subopts +%%-------------------------------------------------------------------- + +enrich_subopts(TopicFilters, #channel{proto_ver = ?MQTT_PROTO_V5}) -> + TopicFilters; +enrich_subopts(TopicFilters, #channel{endpoint = Endpoint, is_bridge = IsBridge}) -> + Rap = flag(IsBridge), + Nl = flag(emqx_zone:get_env(emqx_endpoint:zone(Endpoint), ignore_loop_deliver, false)), + [{Topic, SubOpts#{rap => Rap, nl => Nl}} || {Topic, SubOpts} <- TopicFilters]. + +%%-------------------------------------------------------------------- +%% Parse topic filters +%%-------------------------------------------------------------------- + +parse_topic_filters(?SUBSCRIBE, TopicFilters) -> + [emqx_topic:parse(Topic, SubOpts) || {Topic, SubOpts} <- TopicFilters]; + +parse_topic_filters(?UNSUBSCRIBE, TopicFilters) -> + lists:map(fun emqx_topic:parse/1, TopicFilters). + +%%-------------------------------------------------------------------- +%% Helper functions +%%-------------------------------------------------------------------- + +sp(true) -> 1; +sp(false) -> 0. + +flag(true) -> 1; +flag(false) -> 0. + diff --git a/src/emqx_cm.erl b/src/emqx_cm.erl index 39b19d9ba..7f230e841 100644 --- a/src/emqx_cm.erl +++ b/src/emqx_cm.erl @@ -208,7 +208,7 @@ open_session(Attrs = #{clean_start := true, client_id := ClientId}) -> CleanStart = fun(_) -> ok = discard_session(ClientId), - {ok, emqx_session:new(Attrs)} + {ok, emqx_session:new(Attrs), false} end, emqx_cm_locker:trans(ClientId, CleanStart); @@ -219,7 +219,7 @@ open_session(Attrs = #{clean_start := false, {ok, Session} -> {ok, Session, true}; {error, not_found} -> - {ok, emqx_session:new(Attrs)} + {ok, emqx_session:new(Attrs), false} end end, emqx_cm_locker:trans(ClientId, ResumeStart). diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index be192f22e..f58e2fa57 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -14,8 +14,8 @@ %% limitations under the License. %%-------------------------------------------------------------------- -%% MQTT TCP/SSL Channel --module(emqx_channel). +%% MQTT TCP/SSL Connection +-module(emqx_connection). -behaviour(gen_statem). @@ -24,7 +24,7 @@ -include("logger.hrl"). -include("types.hrl"). --logger_header("[Channel]"). +-logger_header("[Conn]"). -export([start_link/3]). @@ -48,21 +48,19 @@ -record(state, { transport :: esockd:transport(), - socket :: esockd:sock(), - peername :: {inet:ip_address(), inet:port_number()}, - sockname :: {inet:ip_address(), inet:port_number()}, + socket :: esockd:socket(), + peername :: emqx_types:peername(), + sockname :: emqx_types:peername(), conn_state :: running | blocked, active_n :: pos_integer(), rate_limit :: maybe(esockd_rate_limit:bucket()), pub_limit :: maybe(esockd_rate_limit:bucket()), limit_timer :: maybe(reference()), - serializer :: emqx_frame:serializer(), %% TODO: remove it later. parse_state :: emqx_frame:parse_state(), - proto_state :: emqx_protocol:protocol(), + chan_state :: emqx_channel:channel(), gc_state :: emqx_gc:gc_state(), - keepalive :: maybe(reference()), - enable_stats :: boolean(), - stats_timer :: maybe(reference()), + keepalive :: maybe(emqx_keepalive:keepalive()), + stats_timer :: disabled | maybe(reference()), idle_timeout :: timeout() }). @@ -71,7 +69,7 @@ -define(CHAN_STATS, [recv_pkt, recv_msg, send_pkt, send_msg]). -define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt, send_pend]). --spec(start_link(esockd:transport(), esockd:sock(), proplists:proplist()) +-spec(start_link(esockd:transport(), esockd:socket(), proplists:proplist()) -> {ok, pid()}). start_link(Transport, Socket, Options) -> {ok, proc_lib:spawn_link(?MODULE, init, [{Transport, Socket, Options}])}. @@ -93,7 +91,7 @@ info(#state{transport = Transport, active_n = ActiveN, rate_limit = RateLimit, pub_limit = PubLimit, - proto_state = ProtoState}) -> + chan_state = ChanState}) -> ConnInfo = #{socktype => Transport:type(Socket), peername => Peername, sockname => Sockname, @@ -102,11 +100,11 @@ info(#state{transport = Transport, rate_limit => rate_limit_info(RateLimit), pub_limit => rate_limit_info(PubLimit) }, - ProtoInfo = emqx_protocol:info(ProtoState), - maps:merge(ConnInfo, ProtoInfo). + ChanInfo = emqx_channel:info(ChanState), + maps:merge(ConnInfo, ChanInfo). rate_limit_info(undefined) -> - #{}; + undefined; rate_limit_info(Limit) -> esockd_rate_limit:info(Limit). @@ -116,13 +114,16 @@ attrs(CPid) when is_pid(CPid) -> attrs(#state{peername = Peername, sockname = Sockname, - proto_state = ProtoState}) -> + conn_state = ConnState, + chan_state = ChanState}) -> SockAttrs = #{peername => Peername, - sockname => Sockname}, - ProtoAttrs = emqx_protocol:attrs(ProtoState), - maps:merge(SockAttrs, ProtoAttrs). + sockname => Sockname, + conn_state => ConnState + }, + ChanAttrs = emqx_channel:attrs(ChanState), + maps:merge(SockAttrs, ChanAttrs). -%% Conn stats +%% @doc Get connection stats stats(CPid) when is_pid(CPid) -> call(CPid, stats); @@ -153,15 +154,16 @@ init({Transport, RawSocket, Options}) -> ActiveN = proplists:get_value(active_n, Options, ?ACTIVE_N), MaxSize = emqx_zone:get_env(Zone, max_packet_size, ?MAX_PACKET_SIZE), ParseState = emqx_frame:initial_parse_state(#{max_size => MaxSize}), - ProtoState = emqx_protocol:init(#{peername => Peername, - sockname => Sockname, - peercert => Peercert, - conn_mod => ?MODULE}, Options), + ChanState = emqx_channel:init(#{peername => Peername, + sockname => Sockname, + peercert => Peercert, + conn_mod => ?MODULE}, Options), GcPolicy = emqx_zone:get_env(Zone, force_gc_policy, false), GcState = emqx_gc:init(GcPolicy), - ok = emqx_misc:init_proc_mng_policy(Zone), EnableStats = emqx_zone:get_env(Zone, enable_stats, true), + StatsTimer = if EnableStats -> undefined; ?Otherwise-> disabled end, IdleTimout = emqx_zone:get_env(Zone, idle_timeout, 30000), + ok = emqx_misc:init_proc_mng_policy(Zone), State = #state{transport = Transport, socket = Socket, peername = Peername, @@ -170,9 +172,9 @@ init({Transport, RawSocket, Options}) -> rate_limit = RateLimit, pub_limit = PubLimit, parse_state = ParseState, - proto_state = ProtoState, + chan_state = ChanState, gc_state = GcState, - enable_stats = EnableStats, + stats_timer = StatsTimer, idle_timeout = IdleTimout }, gen_statem:enter_loop(?MODULE, [{hibernate_after, 2 * IdleTimout}], @@ -190,17 +192,23 @@ callback_mode() -> %% Idle State idle(enter, _, State) -> - ok = activate_socket(State), - keep_state_and_data; + case activate_socket(State) of + ok -> keep_state_and_data; + {error, Reason} -> + shutdown(Reason, State) + end; idle(timeout, _Timeout, State) -> stop(idle_timeout, State); idle(cast, {incoming, Packet = ?CONNECT_PACKET(ConnVar)}, State) -> - #mqtt_packet_connect{proto_ver = ProtoVer} = ConnVar, - Serializer = emqx_frame:init_serializer(#{version => ProtoVer}), - NState = State#state{serializer = Serializer}, - handle_incoming(Packet, fun(St) -> {next_state, connected, St} end, NState); + handle_incoming(Packet, + fun(St = #state{chan_state = ChanState}) -> + %% Ensure keepalive after connected successfully. + Interval = emqx_channel:keepalive(ChanState), + NextEvent = {next_event, info, {keepalive, start, Interval}}, + {next_state, connected, St, NextEvent} + end, State); idle(cast, {incoming, Packet}, State) -> ?LOG(warning, "Unexpected incoming: ~p", [Packet]), @@ -221,47 +229,45 @@ connected(cast, {incoming, Packet = ?PACKET(?CONNECT)}, State) -> shutdown(unexpected_incoming_connect, State); connected(cast, {incoming, Packet = ?PACKET(Type)}, State) -> - ok = emqx_metrics:inc_recv(Packet), - (Type == ?PUBLISH) andalso emqx_pd:update_counter(incoming_pubs, 1), - handle_incoming(Packet, fun(St) -> {keep_state, St} end, State); + handle_incoming(Packet, fun keep_state/1, State); -%% Handle delivery -connected(info, Devliery = {deliver, _Topic, Msg}, State = #state{proto_state = ProtoState}) -> - case emqx_protocol:handle_out(Devliery, ProtoState) of - {ok, NProtoState} -> - {keep_state, State#state{proto_state = NProtoState}}; - {ok, Packet, NProtoState} -> - NState = State#state{proto_state = NProtoState}, - handle_outgoing(Packet, fun(St) -> {keep_state, St} end, NState); +connected(info, Deliver = {deliver, _Topic, _Msg}, + State = #state{chan_state = ChanState}) -> + Delivers = emqx_misc:drain_deliver([Deliver]), + %% TODO: ... + case BatchLen = length(Delivers) of + 1 -> ok; + N -> io:format("Batch Deliver: ~w~n", [N]) + end, + case emqx_channel:handle_out(Delivers, ChanState) of + {ok, NChanState} -> + keep_state(State#state{chan_state = NChanState}); + {ok, Packets, NChanState} -> + NState = State#state{chan_state = NChanState}, + handle_outgoing(Packets, fun keep_state/1, NState); {error, Reason} -> shutdown(Reason, State) end; %% Start Keepalive -connected(info, {keepalive, start, Interval}, - State = #state{transport = Transport, socket = Socket}) -> - StatFun = fun() -> - case Transport:getstat(Socket, [recv_oct]) of - {ok, [{recv_oct, RecvOct}]} -> {ok, RecvOct}; - Error -> Error - end - end, - case emqx_keepalive:start(StatFun, Interval, {keepalive, check}) of +connected(info, {keepalive, start, Interval}, State) -> + case ensure_keepalive(Interval, State) of + ignore -> keep_state(State); {ok, KeepAlive} -> - {keep_state, State#state{keepalive = KeepAlive}}; - {error, Error} -> - shutdown(Error, State) + keep_state(State#state{keepalive = KeepAlive}); + {error, Reason} -> + shutdown(Reason, State) end; %% Keepalive timer connected(info, {keepalive, check}, State = #state{keepalive = KeepAlive}) -> case emqx_keepalive:check(KeepAlive) of {ok, KeepAlive1} -> - {keep_state, State#state{keepalive = KeepAlive1}}; + keep_state(State#state{keepalive = KeepAlive1}); {error, timeout} -> shutdown(keepalive_timeout, State); - {error, Error} -> - shutdown(Error, State) + {error, Reason} -> + shutdown(Reason, State) end; connected(EventType, Content, State) -> @@ -287,13 +293,13 @@ handle({call, From}, attrs, State) -> handle({call, From}, stats, State) -> reply(From, stats(State), State); -handle({call, From}, kick, State) -> - ok = gen_statem:reply(From, ok), - shutdown(kicked, State); +%%handle({call, From}, kick, State) -> +%% ok = gen_statem:reply(From, ok), +%% shutdown(kicked, State); -handle({call, From}, discard, State) -> - ok = gen_statem:reply(From, ok), - shutdown(discard, State); +%%handle({call, From}, discard, State) -> +%% ok = gen_statem:reply(From, ok), +%% shutdown(discard, State); handle({call, From}, Req, State) -> ?LOG(error, "Unexpected call: ~p", [Req]), @@ -302,16 +308,16 @@ handle({call, From}, Req, State) -> %% Handle cast handle(cast, Msg, State) -> ?LOG(error, "Unexpected cast: ~p", [Msg]), - {keep_state, State}; + keep_state(State); %% Handle Incoming handle(info, {Inet, _Sock, Data}, State) when Inet == tcp; Inet == ssl -> - Oct = iolist_size(Data), ?LOG(debug, "RECV ~p", [Data]), + Oct = iolist_size(Data), emqx_pd:update_counter(incoming_bytes, Oct), ok = emqx_metrics:inc('bytes.received', Oct), - NState = ensure_stats_timer(maybe_gc({1, Oct}, State)), + NState = ensure_stats_timer(maybe_gc(1, Oct, State)), process_incoming(Data, [], NState); handle(info, {Error, _Sock, Reason}, State) @@ -326,32 +332,40 @@ handle(info, {Passive, _Sock}, State) when Passive == tcp_passive; Passive == ssl_passive -> %% Rate limit here:) NState = ensure_rate_limit(State), - ok = activate_socket(NState), - {keep_state, NState}; + case activate_socket(NState) of + ok -> keep_state(NState); + {error, Reason} -> + shutdown(Reason, NState) + end; handle(info, activate_socket, State) -> %% Rate limit timer expired. - ok = activate_socket(State#state{conn_state = running}), - {keep_state, State#state{conn_state = running, limit_timer = undefined}}; + NState = State#state{conn_state = running}, + case activate_socket(NState) of + ok -> + keep_state(NState#state{limit_timer = undefined}); + {error, Reason} -> + shutdown(Reason, NState) + end; handle(info, {inet_reply, _Sock, ok}, State) -> %% something sent - {keep_state, ensure_stats_timer(State)}; + keep_state(ensure_stats_timer(State)); handle(info, {inet_reply, _Sock, {error, Reason}}, State) -> shutdown(Reason, State); handle(info, {timeout, Timer, emit_stats}, State = #state{stats_timer = Timer, - proto_state = ProtoState, - gc_state = GcState}) -> - ClientId = emqx_protocol:client_id(ProtoState), - emqx_cm:set_conn_stats(ClientId, stats(State)), + chan_state = ChanState, + gc_state = GcState}) -> + ClientId = emqx_channel:client_id(ChanState), + ok = emqx_cm:set_conn_stats(ClientId, stats(State)), NState = State#state{stats_timer = undefined}, Limits = erlang:get(force_shutdown_policy), case emqx_misc:conn_proc_mng_policy(Limits) of continue -> - {keep_state, NState}; + keep_state(NState); hibernate -> %% going to hibernate, reset gc stats GcState1 = emqx_gc:reset(GcState), @@ -374,7 +388,7 @@ handle(info, {shutdown, Reason}, State) -> handle(info, Info, State) -> ?LOG(error, "Unexpected info: ~p", [Info]), - {keep_state, State}. + keep_state(State). code_change(_Vsn, State, Data, _Extra) -> {ok, State, Data}. @@ -382,11 +396,11 @@ code_change(_Vsn, State, Data, _Extra) -> terminate(Reason, _StateName, #state{transport = Transport, socket = Socket, keepalive = KeepAlive, - proto_state = ProtoState}) -> + chan_state = ChanState}) -> ?LOG(debug, "Terminated for ~p", [Reason]), ok = Transport:fast_close(Socket), ok = emqx_keepalive:cancel(KeepAlive), - emqx_protocol:terminate(Reason, ProtoState). + emqx_channel:terminate(Reason, ChanState). %%-------------------------------------------------------------------- %% Process incoming data @@ -420,39 +434,74 @@ next_events(Packet) -> %% Handle incoming packet handle_incoming(Packet = ?PACKET(Type), SuccFun, - State = #state{proto_state = ProtoState}) -> + State = #state{chan_state = ChanState}) -> _ = inc_incoming_stats(Type), + ok = emqx_metrics:inc_recv(Packet), ?LOG(debug, "RECV ~s", [emqx_packet:format(Packet)]), - case emqx_protocol:handle_in(Packet, ProtoState) of - {ok, NProtoState} -> - SuccFun(State#state{proto_state = NProtoState}); - {ok, OutPacket, NProtoState} -> + case emqx_channel:handle_in(Packet, ChanState) of + {ok, NChanState} -> + SuccFun(State#state{chan_state = NChanState}); + {ok, OutPacket, NChanState} -> handle_outgoing(OutPacket, SuccFun, - State#state{proto_state = NProtoState}); - {error, Reason} -> - shutdown(Reason, State); - {error, Reason, NProtoState} -> - shutdown(Reason, State#state{proto_state = NProtoState}); - {stop, Error, NProtoState} -> - stop(Error, State#state{proto_state = NProtoState}) + State#state{chan_state = NChanState}); + {error, Reason, NChanState} -> + shutdown(Reason, State#state{chan_state = NChanState}); + {stop, Error, NChanState} -> + stop(Error, State#state{chan_state = NChanState}) end. %%-------------------------------------------------------------------- -%% Handle outgoing packet +%% Handle outgoing packets -handle_outgoing(Packet = ?PACKET(Type), SuccFun, - State = #state{transport = Transport, - socket = Socket, - serializer = Serializer}) -> - _ = inc_outgoing_stats(Type), +handle_outgoing(Packets, SuccFun, State = #state{chan_state = ChanState}) + when is_list(Packets) -> + ProtoVer = emqx_channel:proto_ver(ChanState), + IoData = lists:foldl( + fun(Packet = ?PACKET(Type), Acc) -> + ?LOG(debug, "SEND ~s", [emqx_packet:format(Packet)]), + _ = inc_outgoing_stats(Type), + [emqx_frame:serialize(Packet, ProtoVer)|Acc] + end, [], Packets), + send(lists:reverse(IoData), SuccFun, State); + +handle_outgoing(Packet = ?PACKET(Type), SuccFun, State = #state{chan_state = ChanState}) -> ?LOG(debug, "SEND ~s", [emqx_packet:format(Packet)]), - Data = Serializer(Packet), - case Transport:async_send(Socket, Data) of - ok -> SuccFun(State); + _ = inc_outgoing_stats(Type), + ProtoVer = emqx_channel:proto_ver(ChanState), + IoData = emqx_frame:serialize(Packet, ProtoVer), + send(IoData, SuccFun, State). + +%%-------------------------------------------------------------------- +%% Send data + +send(IoData, SuccFun, State = #state{transport = Transport, socket = Socket}) -> + Oct = iolist_size(IoData), + ok = emqx_metrics:inc('bytes.sent', Oct), + case Transport:async_send(Socket, IoData) of + ok -> SuccFun(maybe_gc(1, Oct, State)); {error, Reason} -> shutdown(Reason, State) end. +%%-------------------------------------------------------------------- +%% Ensure keepalive + +ensure_keepalive(0, State) -> + ignore; +ensure_keepalive(Interval, State = #state{transport = Transport, + socket = Socket, + chan_state = ChanState}) -> + StatFun = fun() -> + case Transport:getstat(Socket, [recv_oct]) of + {ok, [{recv_oct, RecvOct}]} -> + {ok, RecvOct}; + Error -> Error + end + end, + Backoff = emqx_zone:get_env(emqx_channel:zone(ChanState), + keepalive_backoff, 0.75), + emqx_keepalive:start(StatFun, round(Interval * Backoff), {keepalive, check}). + %%-------------------------------------------------------------------- %% Ensure rate limit @@ -466,82 +515,77 @@ ensure_rate_limit([], State) -> ensure_rate_limit([{undefined, _Pos, _Cnt}|Limiters], State) -> ensure_rate_limit(Limiters, State); ensure_rate_limit([{Rl, Pos, Cnt}|Limiters], State) -> - case esockd_rate_limit:check(Cnt, Rl) of - {0, Rl1} -> - ensure_rate_limit(Limiters, setelement(Pos, State, Rl1)); - {Pause, Rl1} -> - ?LOG(debug, "Rate limit pause connection ~pms", [Pause]), - TRef = erlang:send_after(Pause, self(), activate_socket), - setelement(Pos, State#state{conn_state = blocked, limit_timer = TRef}, Rl1) - end. - -%% start_keepalive(0, _PState) -> -%% ignore; -%% start_keepalive(Secs, #pstate{zone = Zone}) when Secs > 0 -> -%% Backoff = emqx_zone:get_env(Zone, keepalive_backoff, 0.75), -%% self() ! {keepalive, start, round(Secs * Backoff)}. + case esockd_rate_limit:check(Cnt, Rl) of + {0, Rl1} -> + ensure_rate_limit(Limiters, setelement(Pos, State, Rl1)); + {Pause, Rl1} -> + ?LOG(debug, "Rate limit pause connection ~pms", [Pause]), + TRef = erlang:send_after(Pause, self(), activate_socket), + setelement(Pos, State#state{conn_state = blocked, limit_timer = TRef}, Rl1) + end. %%-------------------------------------------------------------------- -%% Activate socket +%% Activate Socket activate_socket(#state{conn_state = blocked}) -> ok; - -activate_socket(#state{transport = Transport, socket = Socket, active_n = N}) -> - case Transport:setopts(Socket, [{active, N}]) of - ok -> ok; - {error, Reason} -> - self() ! {shutdown, Reason}, - ok - end. +activate_socket(#state{transport = Transport, + socket = Socket, + active_n = N}) -> + Transport:setopts(Socket, [{active, N}]). %%-------------------------------------------------------------------- %% Inc incoming/outgoing stats inc_incoming_stats(Type) -> emqx_pd:update_counter(recv_pkt, 1), - Type =:= ?PUBLISH andalso emqx_pd:update_counter(recv_msg, 1). + case Type == ?PUBLISH of + true -> + emqx_pd:update_counter(recv_msg, 1), + emqx_pd:update_counter(incoming_pubs, 1); + false -> ok + end. inc_outgoing_stats(Type) -> emqx_pd:update_counter(send_pkt, 1), - Type =:= ?PUBLISH andalso emqx_pd:update_counter(send_msg, 1). + (Type == ?PUBLISH) + andalso emqx_pd:update_counter(send_msg, 1). %%-------------------------------------------------------------------- %% Ensure stats timer -ensure_stats_timer(State = #state{enable_stats = true, - stats_timer = undefined, +ensure_stats_timer(State = #state{stats_timer = undefined, idle_timeout = IdleTimeout}) -> State#state{stats_timer = emqx_misc:start_timer(IdleTimeout, emit_stats)}; +%% disabled or timer existed ensure_stats_timer(State) -> State. %%-------------------------------------------------------------------- %% Maybe GC -maybe_gc(_, State = #state{gc_state = undefined}) -> +maybe_gc(_Cnt, _Oct, State = #state{gc_state = undefined}) -> State; -maybe_gc({publish, _, #message{payload = Payload}}, State) -> - Oct = iolist_size(Payload), - maybe_gc({1, Oct}, State); -maybe_gc(Packets, State) when is_list(Packets) -> - {Cnt, Oct} = - lists:unzip([{1, iolist_size(Payload)} - || {publish, _, #message{payload = Payload}} <- Packets]), - maybe_gc({lists:sum(Cnt), lists:sum(Oct)}, State); -maybe_gc({Cnt, Oct}, State = #state{gc_state = GCSt}) -> +maybe_gc(Cnt, Oct, State = #state{gc_state = GCSt}) -> {_, GCSt1} = emqx_gc:run(Cnt, Oct, GCSt), - State#state{gc_state = GCSt1}; -maybe_gc(_, State) -> State. + %% TODO: gc metric? + State#state{gc_state = GCSt1}. %%-------------------------------------------------------------------- %% Helper functions +-compile({inline, [reply/3]}). reply(From, Reply, State) -> {keep_state, State, [{reply, From, Reply}]}. +-compile({inline, [keep_state/1]}). +keep_state(State) -> + {keep_state, State}. + +-compile({inline, [shutdown/2]}). shutdown(Reason, State) -> stop({shutdown, Reason}, State). +-compile({inline, [stop/2]}). stop(Reason, State) -> {stop, Reason, State}. diff --git a/src/emqx_endpoint.erl b/src/emqx_endpoint.erl new file mode 100644 index 000000000..1529698aa --- /dev/null +++ b/src/emqx_endpoint.erl @@ -0,0 +1,95 @@ +%%-------------------------------------------------------------------- +%% 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_endpoint). + +-include("types.hrl"). + +%% APIs +-export([ new/0 + , new/1 + ]). + +-export([ zone/1 + , client_id/1 + , mountpoint/1 + , is_superuser/1 + , credentials/1 + ]). + +-export([update/2]). + +-export([to_map/1]). + +-export_type([endpoint/0]). + +-opaque(endpoint() :: + {endpoint, + #{zone := emqx_types:zone(), + peername := emqx_types:peername(), + sockname => emqx_types:peername(), + client_id := emqx_types:client_id(), + username := emqx_types:username(), + peercert := esockd_peercert:peercert(), + is_superuser := boolean(), + mountpoint := maybe(binary()), + ws_cookie := maybe(list()), + password => binary(), + auth_result => emqx_types:auth_result(), + anonymous => boolean(), + atom() => term() + } + }). + +-define(Endpoint(M), {endpoint, M}). + +-define(Default, #{is_superuser => false, + anonymous => false + }). + +-spec(new() -> endpoint()). +new() -> + ?Endpoint(?Default). + +-spec(new(map()) -> endpoint()). +new(M) when is_map(M) -> + ?Endpoint(maps:merge(?Default, M)). + +-spec(zone(endpoint()) -> emqx_zone:zone()). +zone(?Endpoint(#{zone := Zone})) -> + Zone. + +client_id(?Endpoint(#{client_id := ClientId})) -> + ClientId. + +-spec(mountpoint(endpoint()) -> maybe(binary())). +mountpoint(?Endpoint(#{mountpoint := Mountpoint})) -> + Mountpoint; +mountpoint(_) -> undefined. + +is_superuser(?Endpoint(#{is_superuser := B})) -> + B. + +update(Attrs, ?Endpoint(M)) -> + ?Endpoint(maps:merge(M, Attrs)). + +credentials(?Endpoint(M)) -> + M. %% TODO: ... + +-spec(to_map(endpoint()) -> map()). +to_map(?Endpoint(M)) -> + M. + diff --git a/src/emqx_frame.erl b/src/emqx_frame.erl index 3c46e50db..3a18ab297 100644 --- a/src/emqx_frame.erl +++ b/src/emqx_frame.erl @@ -21,7 +21,6 @@ -export([ initial_parse_state/0 , initial_parse_state/1 - , init_serializer/1 ]). -export([ parse/1 @@ -386,18 +385,15 @@ parse_binary_data(<>) -> %% Serialize MQTT Packet %%-------------------------------------------------------------------- -init_serializer(Options) -> - fun(Packet) -> serialize(Packet, Options) end. - -spec(serialize(emqx_mqtt:packet()) -> iodata()). serialize(Packet) -> - serialize(Packet, ?DEFAULT_OPTIONS). + serialize(Packet, ?MQTT_PROTO_V4). --spec(serialize(emqx_mqtt:packet(), options()) -> iodata()). +-spec(serialize(emqx_mqtt:packet(), emqx_mqtt:version()) -> iodata()). serialize(#mqtt_packet{header = Header, variable = Variable, - payload = Payload}, Options) when is_map(Options) -> - serialize(Header, serialize_variable(Variable, merge_opts(Options)), serialize_payload(Payload)). + payload = Payload}, Ver) -> + serialize(Header, serialize_variable(Variable, Ver), serialize_payload(Payload)). serialize(#mqtt_packet_header{type = Type, dup = Dup, @@ -424,7 +420,7 @@ serialize_variable(#mqtt_packet_connect{ will_topic = WillTopic, will_payload = WillPayload, username = Username, - password = Password}, _Options) -> + password = Password}, _Ver) -> [serialize_binary_data(ProtoName), <<(case IsBridge of true -> 16#80 + ProtoVer; @@ -451,14 +447,12 @@ serialize_variable(#mqtt_packet_connect{ serialize_variable(#mqtt_packet_connack{ack_flags = AckFlags, reason_code = ReasonCode, - properties = Properties}, - #{version := Ver}) -> + properties = Properties}, Ver) -> [AckFlags, ReasonCode, serialize_properties(Properties, Ver)]; serialize_variable(#mqtt_packet_publish{topic_name = TopicName, packet_id = PacketId, - properties = Properties}, - #{version := Ver}) -> + properties = Properties}, Ver) -> [serialize_utf8_string(TopicName), if PacketId =:= undefined -> <<>>; @@ -466,59 +460,54 @@ serialize_variable(#mqtt_packet_publish{topic_name = TopicName, end, serialize_properties(Properties, Ver)]; -serialize_variable(#mqtt_packet_puback{packet_id = PacketId}, - #{version := Ver}) +serialize_variable(#mqtt_packet_puback{packet_id = PacketId}, Ver) when Ver == ?MQTT_PROTO_V3; Ver == ?MQTT_PROTO_V4 -> <>; serialize_variable(#mqtt_packet_puback{packet_id = PacketId, reason_code = ReasonCode, properties = Properties}, - #{version := ?MQTT_PROTO_V5}) -> + ?MQTT_PROTO_V5) -> [<>, ReasonCode, serialize_properties(Properties, ?MQTT_PROTO_V5)]; serialize_variable(#mqtt_packet_subscribe{packet_id = PacketId, properties = Properties, - topic_filters = TopicFilters}, - #{version := Ver}) -> + topic_filters = TopicFilters}, Ver) -> [<>, serialize_properties(Properties, Ver), serialize_topic_filters(subscribe, TopicFilters, Ver)]; serialize_variable(#mqtt_packet_suback{packet_id = PacketId, properties = Properties, - reason_codes = ReasonCodes}, - #{version := Ver}) -> + reason_codes = ReasonCodes}, Ver) -> [<>, serialize_properties(Properties, Ver), serialize_reason_codes(ReasonCodes)]; serialize_variable(#mqtt_packet_unsubscribe{packet_id = PacketId, properties = Properties, - topic_filters = TopicFilters}, - #{version := Ver}) -> + topic_filters = TopicFilters}, Ver) -> [<>, serialize_properties(Properties, Ver), serialize_topic_filters(unsubscribe, TopicFilters, Ver)]; serialize_variable(#mqtt_packet_unsuback{packet_id = PacketId, properties = Properties, - reason_codes = ReasonCodes}, - #{version := Ver}) -> + reason_codes = ReasonCodes}, Ver) -> [<>, serialize_properties(Properties, Ver), serialize_reason_codes(ReasonCodes)]; -serialize_variable(#mqtt_packet_disconnect{}, #{version := Ver}) +serialize_variable(#mqtt_packet_disconnect{}, Ver) when Ver == ?MQTT_PROTO_V3; Ver == ?MQTT_PROTO_V4 -> <<>>; serialize_variable(#mqtt_packet_disconnect{reason_code = ReasonCode, properties = Properties}, - #{version := Ver = ?MQTT_PROTO_V5}) -> + Ver = ?MQTT_PROTO_V5) -> [ReasonCode, serialize_properties(Properties, Ver)]; serialize_variable(#mqtt_packet_disconnect{}, _Ver) -> <<>>; serialize_variable(#mqtt_packet_auth{reason_code = ReasonCode, properties = Properties}, - #{version := Ver = ?MQTT_PROTO_V5}) -> + Ver = ?MQTT_PROTO_V5) -> [ReasonCode, serialize_properties(Properties, Ver)]; serialize_variable(PacketId, ?MQTT_PROTO_V3) when is_integer(PacketId) -> diff --git a/src/emqx_listeners.erl b/src/emqx_listeners.erl index 043b55fcc..94babe7fd 100644 --- a/src/emqx_listeners.erl +++ b/src/emqx_listeners.erl @@ -75,7 +75,7 @@ start_listener(Proto, ListenOn, Options) when Proto == https; Proto == wss -> start_mqtt_listener(Name, ListenOn, Options) -> SockOpts = esockd:parse_opt(Options), esockd:open(Name, ListenOn, merge_default(SockOpts), - {emqx_channel, start_link, [Options -- SockOpts]}). + {emqx_connection, start_link, [Options -- SockOpts]}). start_http_listener(Start, Name, ListenOn, RanchOpts, ProtoOpts) -> Start(Name, with_port(ListenOn, RanchOpts), ProtoOpts). @@ -84,7 +84,7 @@ mqtt_path(Options) -> proplists:get_value(mqtt_path, Options, "/mqtt"). ws_opts(Options) -> - Dispatch = cowboy_router:compile([{'_', [{mqtt_path(Options), emqx_ws_channel, Options}]}]), + Dispatch = cowboy_router:compile([{'_', [{mqtt_path(Options), emqx_ws_connection, Options}]}]), #{env => #{dispatch => Dispatch}, proxy_header => proplists:get_value(proxy_protocol, Options, false)}. ranch_opts(Options) -> diff --git a/src/emqx_misc.erl b/src/emqx_misc.erl index 26c97c92f..e04fd606d 100644 --- a/src/emqx_misc.erl +++ b/src/emqx_misc.erl @@ -29,7 +29,14 @@ , conn_proc_mng_policy/1 ]). --export([drain_down/1]). +-export([ drain_deliver/1 + , drain_down/1 + ]). + +-compile({inline, + [ start_timer/2 + , start_timer/3 + ]}). %% @doc Merge options -spec(merge_opts(list(), list()) -> list()). @@ -121,6 +128,16 @@ proc_info(Key) -> {Key, Value} = erlang:process_info(self(), Key), Value. +%% @doc Drain delivers from channel's mailbox. +drain_deliver(Acc) -> + receive + Deliver = {deliver, _Topic, _Msg} -> + drain_deliver([Deliver|Acc]) + after 0 -> + lists:reverse(Acc) + end. + +%% @doc Drain process down events. -spec(drain_down(pos_integer()) -> list(pid())). drain_down(Cnt) when Cnt > 0 -> drain_down(Cnt, []). diff --git a/src/emqx_mountpoint.erl b/src/emqx_mountpoint.erl index 1e52d9d25..80a65b743 100644 --- a/src/emqx_mountpoint.erl +++ b/src/emqx_mountpoint.erl @@ -19,8 +19,6 @@ -include("emqx.hrl"). -include("logger.hrl"). --logger_header("[Mountpoint]"). - -export([ mount/2 , unmount/2 ]). @@ -41,7 +39,8 @@ mount(MountPoint, Msg = #message{topic = Topic}) -> Msg#message{topic = <>}; mount(MountPoint, TopicFilters) when is_list(TopicFilters) -> - [{<>, SubOpts} || {Topic, SubOpts} <- TopicFilters]. + [{<>, SubOpts} + || {Topic, SubOpts} <- TopicFilters]. unmount(undefined, Msg) -> Msg; @@ -49,8 +48,7 @@ unmount(MountPoint, Msg = #message{topic = Topic}) -> try split_binary(Topic, byte_size(MountPoint)) of {MountPoint, Topic1} -> Msg#message{topic = Topic1} catch - _Error:Reason -> - ?LOG(error, "Unmount error : ~p", [Reason]), + error:badarg-> Msg end. diff --git a/src/emqx_pd.erl b/src/emqx_pd.erl index 5d1277833..9800d9d57 100644 --- a/src/emqx_pd.erl +++ b/src/emqx_pd.erl @@ -24,6 +24,12 @@ , reset_counter/1 ]). +-compile({inline, + [ update_counter/2 + , get_counter/1 + , reset_counter/1 + ]}). + -type(key() :: term()). -spec(update_counter(key(), number()) -> maybe(number())). diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl deleted file mode 100644 index b1396db3c..000000000 --- a/src/emqx_protocol.erl +++ /dev/null @@ -1,1010 +0,0 @@ -%%-------------------------------------------------------------------- -%% 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. -%%-------------------------------------------------------------------- - -%% MQTT Protocol --module(emqx_protocol). - --include("emqx.hrl"). --include("emqx_mqtt.hrl"). --include("logger.hrl"). --include("types.hrl"). - --logger_header("[Protocol]"). - --export([ info/1 - , attrs/1 - , attr/2 - , caps/1 - , caps/2 - , client_id/1 - , credentials/1 - , session/1 - ]). - --export([ init/2 - , handle_in/2 - , handle_out/2 - , handle_timeout/3 - , terminate/2 - ]). - --export_type([protocol/0]). - --record(protocol, { - zone :: emqx_zone:zone(), - conn_mod :: module(), - sendfun, - sockname, - peername, - peercert, - proto_ver :: emqx_mqtt:version(), - proto_name, - client_id :: maybe(emqx_types:client_id()), - is_assigned, - username :: maybe(emqx_types:username()), - conn_props, - ack_props, - credentials :: map(), - session :: maybe(emqx_session:session()), - clean_start, - topic_aliases, - will_topic, - will_msg, - keepalive, - is_bridge :: boolean(), - connected :: boolean(), - connected_at :: erlang:timestamp(), - topic_alias_maximum, - ws_cookie - }). - --opaque(protocol() :: #protocol{}). - --ifdef(TEST). --compile(export_all). --compile(nowarn_export_all). --endif. - --define(NO_PROPS, undefined). - -%%-------------------------------------------------------------------- -%% Init -%%-------------------------------------------------------------------- - --spec(init(map(), list()) -> protocol()). -init(SocketOpts = #{sockname := Sockname, - peername := Peername, - peercert := Peercert}, Options) -> - Zone = proplists:get_value(zone, Options), - #protocol{zone = Zone, - %%sendfun = SendFun, - sockname = Sockname, - peername = Peername, - peercert = Peercert, - proto_ver = ?MQTT_PROTO_V4, - proto_name = <<"MQTT">>, - client_id = <<>>, - is_assigned = false, - %%conn_pid = self(), - username = init_username(Peercert, Options), - clean_start = false, - topic_aliases = #{}, - is_bridge = false, - connected = false, - topic_alias_maximum = #{to_client => 0, from_client => 0}, - conn_mod = maps:get(conn_mod, SocketOpts, undefined), - credentials = #{}, - ws_cookie = maps:get(ws_cookie, SocketOpts, undefined) - }. - -init_username(Peercert, Options) -> - case proplists:get_value(peer_cert_as_username, Options) of - cn -> esockd_peercert:common_name(Peercert); - dn -> esockd_peercert:subject(Peercert); - crt -> Peercert; - _ -> undefined - end. - -set_username(Username, PState = #protocol{username = undefined}) -> - PState#protocol{username = Username}; -set_username(_Username, PState) -> - PState. - -%%-------------------------------------------------------------------- -%% API -%%-------------------------------------------------------------------- - -info(PState = #protocol{zone = Zone, - conn_props = ConnProps, - ack_props = AckProps, - session = Session, - topic_aliases = Aliases, - will_msg = WillMsg}) -> - maps:merge(attrs(PState), #{conn_props => ConnProps, - ack_props => AckProps, - session => Session, - topic_aliases => Aliases, - will_msg => WillMsg, - enable_acl => emqx_zone:get_env(Zone, enable_acl, false) - }). - -attrs(#protocol{zone = Zone, - client_id = ClientId, - username = Username, - peername = Peername, - peercert = Peercert, - clean_start = CleanStart, - proto_ver = ProtoVer, - proto_name = ProtoName, - keepalive = Keepalive, - is_bridge = IsBridge, - connected_at = ConnectedAt, - conn_mod = ConnMod, - credentials = Credentials}) -> - #{zone => Zone, - client_id => ClientId, - username => Username, - peername => Peername, - peercert => Peercert, - proto_ver => ProtoVer, - proto_name => ProtoName, - clean_start => CleanStart, - keepalive => Keepalive, - is_bridge => IsBridge, - connected_at => ConnectedAt, - conn_mod => ConnMod, - credentials => Credentials - }. - -attr(proto_ver, #protocol{proto_ver = ProtoVer}) -> - ProtoVer; -attr(max_inflight, #protocol{proto_ver = ?MQTT_PROTO_V5, conn_props = ConnProps}) -> - get_property('Receive-Maximum', ConnProps, 65535); -attr(max_inflight, #protocol{zone = Zone}) -> - emqx_zone:get_env(Zone, max_inflight, 65535); -attr(expiry_interval, #protocol{proto_ver = ?MQTT_PROTO_V5, conn_props = ConnProps}) -> - get_property('Session-Expiry-Interval', ConnProps, 0); -attr(expiry_interval, #protocol{zone = Zone, clean_start = CleanStart}) -> - case CleanStart of - true -> 0; - false -> emqx_zone:get_env(Zone, session_expiry_interval, 16#ffffffff) - end; -attr(topic_alias_maximum, #protocol{proto_ver = ?MQTT_PROTO_V5, conn_props = ConnProps}) -> - get_property('Topic-Alias-Maximum', ConnProps, 0); -attr(topic_alias_maximum, #protocol{zone = Zone}) -> - emqx_zone:get_env(Zone, max_topic_alias, 0); -attr(Name, PState) -> - Attrs = lists:zip(record_info(fields, protocol), tl(tuple_to_list(PState))), - case lists:keyfind(Name, 1, Attrs) of - {_, Value} -> Value; - false -> undefined - end. - -caps(Name, PState) -> - maps:get(Name, caps(PState)). - -caps(#protocol{zone = Zone}) -> - emqx_mqtt_caps:get_caps(Zone). - -client_id(#protocol{client_id = ClientId}) -> - ClientId. - -credentials(#protocol{zone = Zone, - client_id = ClientId, - username = Username, - sockname = Sockname, - peername = Peername, - peercert = Peercert, - ws_cookie = WsCookie}) -> - with_cert(#{zone => Zone, - client_id => ClientId, - sockname => Sockname, - username => Username, - peername => Peername, - ws_cookie => WsCookie, - mountpoint => emqx_zone:get_env(Zone, mountpoint)}, Peercert). - -with_cert(Credentials, undefined) -> Credentials; -with_cert(Credentials, Peercert) -> - Credentials#{dn => esockd_peercert:subject(Peercert), - cn => esockd_peercert:common_name(Peercert)}. - -keepsafety(Credentials) -> - maps:filter(fun(password, _) -> false; - (dn, _) -> false; - (cn, _) -> false; - (_, _) -> true end, Credentials). - -session(#protocol{session = Session}) -> - Session. - -%%-------------------------------------------------------------------- -%% Packet Received -%%-------------------------------------------------------------------- - -set_protover(?CONNECT_PACKET(#mqtt_packet_connect{proto_ver = ProtoVer}), PState) -> - PState#protocol{proto_ver = ProtoVer}; -set_protover(_Packet, PState) -> - PState. - -handle_in(?PACKET(Type), PState = #protocol{connected = false}) when Type =/= ?CONNECT -> - {error, proto_not_connected, PState}; - -handle_in(?PACKET(?CONNECT), PState = #protocol{connected = true}) -> - {error, proto_unexpected_connect, PState}; - -handle_in(Packet = ?PACKET(_Type), PState) -> - PState1 = set_protover(Packet, PState), - try emqx_packet:validate(Packet) of - true -> - case preprocess_properties(Packet, PState1) of - {ok, Packet1, PState2} -> - process(Packet1, PState2); - {error, ReasonCode} -> - handle_out({disconnect, ReasonCode}, PState1) - end - catch - error:protocol_error -> - handle_out({disconnect, ?RC_PROTOCOL_ERROR}, PState1); - error:subscription_identifier_invalid -> - handle_out({disconnect, ?RC_SUBSCRIPTION_IDENTIFIERS_NOT_SUPPORTED}, PState1); - error:topic_alias_invalid -> - handle_out({disconnect, ?RC_TOPIC_ALIAS_INVALID}, PState1); - error:topic_filters_invalid -> - handle_out({disconnect, ?RC_TOPIC_FILTER_INVALID}, PState1); - error:topic_name_invalid -> - handle_out({disconnect, ?RC_TOPIC_FILTER_INVALID}, PState1); - error:_Reason -> - %% TODO: {error, Reason, PState1} - handle_out({disconnect, ?RC_MALFORMED_PACKET}, PState1) - end. - -%%-------------------------------------------------------------------- -%% Preprocess MQTT Properties -%%-------------------------------------------------------------------- - -preprocess_properties(Packet = #mqtt_packet{ - variable = #mqtt_packet_connect{ - properties = #{'Topic-Alias-Maximum' := ToClient} - } - }, - PState = #protocol{topic_alias_maximum = TopicAliasMaximum}) -> - {ok, Packet, PState#protocol{topic_alias_maximum = TopicAliasMaximum#{to_client => ToClient}}}; - -%% Subscription Identifier -preprocess_properties(Packet = #mqtt_packet{ - variable = Subscribe = #mqtt_packet_subscribe{ - properties = #{'Subscription-Identifier' := SubId}, - topic_filters = TopicFilters - } - }, - PState = #protocol{proto_ver = ?MQTT_PROTO_V5}) -> - TopicFilters1 = [{Topic, SubOpts#{subid => SubId}} || {Topic, SubOpts} <- TopicFilters], - {ok, Packet#mqtt_packet{variable = Subscribe#mqtt_packet_subscribe{topic_filters = TopicFilters1}}, PState}; - -%% Topic Alias Mapping -preprocess_properties(#mqtt_packet{ - variable = #mqtt_packet_publish{ - properties = #{'Topic-Alias' := 0}} - }, - PState) -> - {error, ?RC_TOPIC_ALIAS_INVALID}; - -preprocess_properties(Packet = #mqtt_packet{ - variable = Publish = #mqtt_packet_publish{ - topic_name = <<>>, - properties = #{'Topic-Alias' := AliasId}} - }, - PState = #protocol{proto_ver = ?MQTT_PROTO_V5, - topic_aliases = Aliases, - topic_alias_maximum = #{from_client := TopicAliasMaximum}}) -> - case AliasId =< TopicAliasMaximum of - true -> - {ok, Packet#mqtt_packet{variable = Publish#mqtt_packet_publish{ - topic_name = maps:get(AliasId, Aliases, <<>>)}}, PState}; - false -> - {error, ?RC_TOPIC_ALIAS_INVALID} - end; - -preprocess_properties(Packet = #mqtt_packet{ - variable = #mqtt_packet_publish{ - topic_name = Topic, - properties = #{'Topic-Alias' := AliasId}} - }, - PState = #protocol{proto_ver = ?MQTT_PROTO_V5, - topic_aliases = Aliases, - topic_alias_maximum = #{from_client := TopicAliasMaximum}}) -> - case AliasId =< TopicAliasMaximum of - true -> - {ok, Packet, PState#protocol{topic_aliases = maps:put(AliasId, Topic, Aliases)}}; - false -> - {error, ?RC_TOPIC_ALIAS_INVALID} - end; - -preprocess_properties(Packet, PState) -> - {ok, Packet, PState}. - -%%-------------------------------------------------------------------- -%% Process MQTT Packet -%%-------------------------------------------------------------------- - -process(?CONNECT_PACKET( - #mqtt_packet_connect{proto_name = ProtoName, - proto_ver = ProtoVer, - is_bridge = IsBridge, - clean_start = CleanStart, - keepalive = Keepalive, - properties = ConnProps, - client_id = ClientId, - username = Username, - password = Password} = ConnPkt), PState) -> - - %% TODO: Mountpoint... - %% Msg -> emqx_mountpoint:mount(MountPoint, Msg) - PState0 = maybe_use_username_as_clientid(ClientId, - set_username(Username, - PState#protocol{proto_ver = ProtoVer, - proto_name = ProtoName, - clean_start = CleanStart, - keepalive = Keepalive, - conn_props = ConnProps, - is_bridge = IsBridge, - connected_at = os:timestamp()})), - - NewClientId = PState0#protocol.client_id, - - emqx_logger:set_metadata_client_id(NewClientId), - - Credentials = credentials(PState0), - PState1 = PState0#protocol{credentials = Credentials}, - connack( - case check_connect(ConnPkt, PState1) of - ok -> - case emqx_access_control:authenticate(Credentials#{password => Password}) of - {ok, Credentials0} -> - PState3 = maybe_assign_client_id(PState1), - emqx_logger:set_metadata_client_id(PState3#protocol.client_id), - %% Open session - SessAttrs = #{will_msg => make_will_msg(ConnPkt)}, - case try_open_session(SessAttrs, PState3) of - {ok, Session, SP} -> - PState4 = PState3#protocol{session = Session, connected = true, - credentials = keepsafety(Credentials0)}, - ok = emqx_cm:register_channel(client_id(PState4)), - ok = emqx_cm:set_conn_attrs(client_id(PState4), attrs(PState4)), - %% Start keepalive - start_keepalive(Keepalive, PState4), - %% Success - {?RC_SUCCESS, SP, PState4}; - {error, Error} -> - ?LOG(error, "Failed to open session: ~p", [Error]), - {?RC_UNSPECIFIED_ERROR, PState1#protocol{credentials = Credentials0}} - end; - {error, Reason} -> - ?LOG(warning, "Client ~s (Username: '~s') login failed for ~p", [NewClientId, Username, Reason]), - {emqx_reason_codes:connack_error(Reason), PState1#protocol{credentials = Credentials}} - end; - {error, ReasonCode} -> - {ReasonCode, PState1} - end); - -process(Packet = ?PUBLISH_PACKET(?QOS_0, Topic, _PacketId, _Payload), PState = #protocol{zone = Zone}) -> - case check_publish(Packet, PState) of - ok -> - do_publish(Packet, PState); - {error, ReasonCode} -> - ?LOG(warning, "Cannot publish qos0 message to ~s for ~s", - [Topic, emqx_reason_codes:text(ReasonCode)]), - %% TODO: ... - AclDenyAction = emqx_zone:get_env(Zone, acl_deny_action, ignore), - do_acl_deny_action(AclDenyAction, Packet, ReasonCode, PState) - end; - -process(Packet = ?PUBLISH_PACKET(?QOS_1, Topic, PacketId, _Payload), PState = #protocol{zone = Zone}) -> - case check_publish(Packet, PState) of - ok -> - do_publish(Packet, PState); - {error, ReasonCode} -> - ?LOG(warning, "Cannot publish qos1 message to ~s for ~s", - [Topic, emqx_reason_codes:text(ReasonCode)]), - handle_out({puback, PacketId, ReasonCode}, PState) - end; - -process(Packet = ?PUBLISH_PACKET(?QOS_2, Topic, PacketId, _Payload), PState = #protocol{zone = Zone}) -> - case check_publish(Packet, PState) of - ok -> - do_publish(Packet, PState); - {error, ReasonCode} -> - ?LOG(warning, "Cannot publish qos2 message to ~s for ~s", - [Topic, emqx_reason_codes:text(ReasonCode)]), - handle_out({pubrec, PacketId, ReasonCode}, PState) - end; - -process(?PUBACK_PACKET(PacketId, ReasonCode), PState = #protocol{session = Session}) -> - case emqx_session:puback(PacketId, ReasonCode, Session) of - {ok, NSession} -> - {ok, PState#protocol{session = NSession}}; - {error, _NotFound} -> - {ok, PState} %% TODO: Fixme later - end; - -process(?PUBREC_PACKET(PacketId, ReasonCode), PState = #protocol{session = Session}) -> - case emqx_session:pubrec(PacketId, ReasonCode, Session) of - {ok, NSession} -> - {ok, ?PUBREL_PACKET(PacketId), PState#protocol{session = NSession}}; - {error, NotFound} -> - {ok, ?PUBREL_PACKET(PacketId, NotFound), PState} - end; - -process(?PUBREL_PACKET(PacketId, ReasonCode), PState = #protocol{session = Session}) -> - case emqx_session:pubrel(PacketId, ReasonCode, Session) of - {ok, NSession} -> - {ok, ?PUBCOMP_PACKET(PacketId), PState#protocol{session = NSession}}; - {error, NotFound} -> - {ok, ?PUBCOMP_PACKET(PacketId, NotFound), PState} - end; - -process(?PUBCOMP_PACKET(PacketId, ReasonCode), PState = #protocol{session = Session}) -> - case emqx_session:pubcomp(PacketId, ReasonCode, Session) of - {ok, NSession} -> - {ok, PState#protocol{session = NSession}}; - {error, _NotFound} -> ok - %% TODO: How to handle NotFound? - end; - -process(Packet = ?SUBSCRIBE_PACKET(PacketId, Properties, RawTopicFilters), - PState = #protocol{zone = Zone, session = Session, credentials = Credentials}) -> - case check_subscribe(parse_topic_filters(?SUBSCRIBE, raw_topic_filters(PState, RawTopicFilters)), PState) of - {ok, TopicFilters} -> - TopicFilters0 = emqx_hooks:run_fold('client.subscribe', [Credentials], TopicFilters), - TopicFilters1 = emqx_mountpoint:mount(mountpoint(Credentials), TopicFilters0), - {ok, ReasonCodes, NSession} = emqx_session:subscribe(Credentials, TopicFilters1, Session), - handle_out({suback, PacketId, ReasonCodes}, PState#protocol{session = NSession}); - {error, TopicFilters} -> - {SubTopics, ReasonCodes} = - lists:foldr(fun({Topic, #{rc := ?RC_SUCCESS}}, {Topics, Codes}) -> - {[Topic|Topics], [?RC_IMPLEMENTATION_SPECIFIC_ERROR | Codes]}; - ({Topic, #{rc := Code}}, {Topics, Codes}) -> - {[Topic|Topics], [Code|Codes]} - end, {[], []}, TopicFilters), - ?LOG(warning, "Cannot subscribe ~p for ~p", - [SubTopics, [emqx_reason_codes:text(R) || R <- ReasonCodes]]), - handle_out({suback, PacketId, ReasonCodes}, PState) - end; - -process(?UNSUBSCRIBE_PACKET(PacketId, Properties, RawTopicFilters), - PState = #protocol{session = Session, credentials = Credentials}) -> - TopicFilters = emqx_hooks:run_fold('client.unsubscribe', [Credentials], - parse_topic_filters(?UNSUBSCRIBE, RawTopicFilters)), - TopicFilters1 = emqx_mountpoint:mount(mountpoint(Credentials), TopicFilters), - {ok, ReasonCodes, NSession} = emqx_session:unsubscribe(Credentials, TopicFilters1, Session), - handle_out({unsuback, PacketId, ReasonCodes}, PState#protocol{session = NSession}); - -process(?PACKET(?PINGREQ), PState) -> - {ok, ?PACKET(?PINGRESP), PState}; - -process(?DISCONNECT_PACKET(?RC_SUCCESS, #{'Session-Expiry-Interval' := Interval}), - PState = #protocol{session = Session, conn_props = #{'Session-Expiry-Interval' := OldInterval}}) -> - case Interval =/= 0 andalso OldInterval =:= 0 of - true -> - handle_out({disconnect, ?RC_PROTOCOL_ERROR}, PState#protocol{will_msg = undefined}); - false -> - %% TODO: - %% emqx_session:update_expiry_interval(SPid, Interval), - %% Clean willmsg - {stop, normal, PState#protocol{will_msg = undefined}} - end; - -process(?DISCONNECT_PACKET(?RC_SUCCESS), PState) -> - {stop, normal, PState#protocol{will_msg = undefined}}; - -process(?DISCONNECT_PACKET(_), PState) -> - {stop, {shutdown, abnormal_disconnet}, PState}; - -process(?AUTH_PACKET(), State) -> - %%TODO: implement later. - {ok, State}. - -%%-------------------------------------------------------------------- -%% ConnAck --> Client -%%-------------------------------------------------------------------- - -connack({?RC_SUCCESS, SP, PState = #protocol{credentials = Credentials}}) -> - ok = emqx_hooks:run('client.connected', [Credentials, ?RC_SUCCESS, attrs(PState)]), - handle_out({connack, ?RC_SUCCESS, sp(SP)}, PState); - -connack({ReasonCode, PState = #protocol{proto_ver = ProtoVer, credentials = Credentials}}) -> - ok = emqx_hooks:run('client.connected', [Credentials, ReasonCode, attrs(PState)]), - [ReasonCode1] = reason_codes_compat(connack, [ReasonCode], ProtoVer), - handle_out({connack, ReasonCode1}, PState). - -%%------------------------------------------------------------------------------ -%% Publish Message -> Broker -%%------------------------------------------------------------------------------ - -do_publish(Packet = ?PUBLISH_PACKET(QoS, PacketId), - PState = #protocol{session = Session, credentials = Credentials}) -> - Msg = emqx_mountpoint:mount(mountpoint(Credentials), - emqx_packet:to_message(Credentials, Packet)), - Msg1 = emqx_message:set_flag(dup, false, Msg), - case emqx_session:publish(PacketId, Msg1, Session) of - {ok, Results} -> - puback(QoS, PacketId, Results, PState); - {ok, Results, NSession} -> - puback(QoS, PacketId, Results, PState#protocol{session = NSession}); - {error, Reason} -> - puback(QoS, PacketId, {error, Reason}, PState) - end. - -%%------------------------------------------------------------------------------ -%% Puback -> Client -%%------------------------------------------------------------------------------ - -puback(?QOS_0, _PacketId, _Result, PState) -> - {ok, PState}; -puback(?QOS_1, PacketId, [], PState) -> - handle_out({puback, PacketId, ?RC_NO_MATCHING_SUBSCRIBERS}, PState); -%%TODO: calc the deliver count? -puback(?QOS_1, PacketId, Result, PState) when is_list(Result) -> - handle_out({puback, PacketId, ?RC_SUCCESS}, PState); -puback(?QOS_1, PacketId, {error, ReasonCode}, PState) -> - handle_out({puback, PacketId, ReasonCode}, PState); -puback(?QOS_2, PacketId, [], PState) -> - handle_out({pubrec, PacketId, ?RC_NO_MATCHING_SUBSCRIBERS}, PState); -puback(?QOS_2, PacketId, Result, PState) when is_list(Result) -> - handle_out({pubrec, PacketId, ?RC_SUCCESS}, PState); -puback(?QOS_2, PacketId, {error, ReasonCode}, PState) -> - handle_out({pubrec, PacketId, ReasonCode}, PState). - -%%-------------------------------------------------------------------- -%% Handle outgoing -%%-------------------------------------------------------------------- - -handle_out({connack, ?RC_SUCCESS, SP}, PState = #protocol{zone = Zone, - proto_ver = ?MQTT_PROTO_V5, - client_id = ClientId, - is_assigned = IsAssigned, - topic_alias_maximum = TopicAliasMaximum}) -> - #{max_packet_size := MaxPktSize, - max_qos_allowed := MaxQoS, - mqtt_retain_available := Retain, - max_topic_alias := MaxAlias, - mqtt_shared_subscription := Shared, - mqtt_wildcard_subscription := Wildcard} = caps(PState), - %% Response-Information is so far not set by broker. - %% i.e. It's a Client-to-Client contract for the request-response topic naming scheme. - %% According to MQTT 5.0 spec: - %% A common use of this is to pass a globally unique portion of the topic tree which - %% is reserved for this Client for at least the lifetime of its Session. - %% This often cannot just be a random name as both the requesting Client and the - %% responding Client need to be authorized to use it. - %% If we are to support it in the feature, the implementation should be flexible - %% to allow prefixing the response topic based on different ACL config. - %% e.g. prefix by username or client-id, so that unauthorized clients can not - %% subscribe requests or responses that are not intended for them. - Props = #{'Retain-Available' => flag(Retain), - 'Maximum-Packet-Size' => MaxPktSize, - 'Topic-Alias-Maximum' => MaxAlias, - 'Wildcard-Subscription-Available' => flag(Wildcard), - 'Subscription-Identifier-Available' => 1, - %'Response-Information' => - 'Shared-Subscription-Available' => flag(Shared)}, - - Props1 = if - MaxQoS =:= ?QOS_2 -> - Props; - true -> - maps:put('Maximum-QoS', MaxQoS, Props) - end, - - Props2 = if IsAssigned -> - Props1#{'Assigned-Client-Identifier' => ClientId}; - true -> Props1 - - end, - - Props3 = case emqx_zone:get_env(Zone, server_keepalive) of - undefined -> Props2; - Keepalive -> Props2#{'Server-Keep-Alive' => Keepalive} - end, - - PState1 = PState#protocol{topic_alias_maximum = TopicAliasMaximum#{from_client => MaxAlias}}, - - {ok, ?CONNACK_PACKET(?RC_SUCCESS, SP, Props3), PState1}; - -handle_out({connack, ?RC_SUCCESS, SP}, PState) -> - {ok, ?CONNACK_PACKET(?RC_SUCCESS, SP), PState}; - -handle_out({connack, ReasonCode}, PState = #protocol{proto_ver = ProtoVer}) -> - Reason = emqx_reason_codes:name(ReasonCode, ProtoVer), - {error, Reason, ?CONNACK_PACKET(ReasonCode), PState}; - -handle_out({deliver, Topic, Msg}, PState = #protocol{session = Session}) -> - case emqx_session:deliver(Topic, Msg, Session) of - {ok, Publish, NSession} -> - handle_out(Publish, PState#protocol{session = NSession}); - {ok, NSession} -> - {ok, PState#protocol{session = NSession}} - end; - -handle_out({publish, PacketId, Msg}, PState = #protocol{credentials = Credentials}) -> - Msg0 = emqx_hooks:run_fold('message.deliver', [Credentials], Msg), - Msg1 = emqx_message:update_expiry(Msg0), - Msg2 = emqx_mountpoint:unmount(mountpoint(Credentials), Msg1), - {ok, emqx_packet:from_message(PacketId, Msg2), PState}; - -handle_out({puback, PacketId, ReasonCode}, PState) -> - {ok, ?PUBACK_PACKET(PacketId, ReasonCode), PState}; - %% TODO: - %% AclDenyAction = emqx_zone:get_env(Zone, acl_deny_action, ignore), - %% do_acl_deny_action(AclDenyAction, Packet, ReasonCode, PState1); - -handle_out({pubrel, PacketId}, PState) -> - {ok, ?PUBREL_PACKET(PacketId), PState}; - -handle_out({pubrec, PacketId, ReasonCode}, PState) -> - %% TODO: - %% AclDenyAction = emqx_zone:get_env(Zone, acl_deny_action, ignore), - %% do_acl_deny_action(AclDenyAction, Packet, ReasonCode, PState1); - {ok, ?PUBREC_PACKET(PacketId, ReasonCode), PState}; - -%%handle_out({pubrec, PacketId, ReasonCode}, PState) -> -%% {ok, ?PUBREC_PACKET(PacketId, ReasonCode), PState}; - -handle_out({suback, PacketId, ReasonCodes}, PState = #protocol{proto_ver = ProtoVer}) -> - %% TODO: ACL Deny - {ok, ?SUBACK_PACKET(PacketId, reason_codes_compat(suback, ReasonCodes, ProtoVer)), PState}; - -handle_out({unsuback, PacketId, ReasonCodes}, PState = #protocol{proto_ver = ProtoVer}) -> - {ok, ?UNSUBACK_PACKET(PacketId, reason_codes_compat(unsuback, ReasonCodes, ProtoVer)), PState}; - -%% Deliver a disconnect for mqtt 5.0 -handle_out({disconnect, RC}, PState = #protocol{proto_ver = ?MQTT_PROTO_V5}) -> - {error, emqx_reason_codes:name(RC), ?DISCONNECT_PACKET(RC), PState}; - -handle_out({disconnect, RC}, PState) -> - {error, emqx_reason_codes:name(RC), PState}. - -handle_timeout(Timer, Name, PState) -> - {ok, PState}. - -%%------------------------------------------------------------------------------ -%% Maybe use username replace client id - -maybe_use_username_as_clientid(ClientId, PState = #protocol{username = undefined}) -> - PState#protocol{client_id = ClientId}; -maybe_use_username_as_clientid(ClientId, PState = #protocol{username = Username, zone = Zone}) -> - case emqx_zone:get_env(Zone, use_username_as_clientid, false) of - true -> - PState#protocol{client_id = Username}; - false -> - PState#protocol{client_id = ClientId} - end. - -%%------------------------------------------------------------------------------ -%% Assign a clientId - -maybe_assign_client_id(PState = #protocol{client_id = <<>>, ack_props = AckProps}) -> - ClientId = emqx_guid:to_base62(emqx_guid:gen()), - AckProps1 = set_property('Assigned-Client-Identifier', ClientId, AckProps), - PState#protocol{client_id = ClientId, is_assigned = true, ack_props = AckProps1}; -maybe_assign_client_id(PState) -> - PState. - -try_open_session(SessAttrs, PState = #protocol{zone = Zone, - client_id = ClientId, - username = Username, - clean_start = CleanStart}) -> - case emqx_cm:open_session( - maps:merge(#{zone => Zone, - client_id => ClientId, - username => Username, - clean_start => CleanStart, - max_inflight => attr(max_inflight, PState), - expiry_interval => attr(expiry_interval, PState), - topic_alias_maximum => attr(topic_alias_maximum, PState)}, - SessAttrs)) of - {ok, Session} -> - {ok, Session, false}; - Other -> Other - end. - -set_property(Name, Value, ?NO_PROPS) -> - #{Name => Value}; -set_property(Name, Value, Props) -> - Props#{Name => Value}. - -get_property(_Name, undefined, Default) -> - Default; -get_property(Name, Props, Default) -> - maps:get(Name, Props, Default). - -make_will_msg(#mqtt_packet_connect{proto_ver = ProtoVer, - will_props = WillProps} = ConnPkt) -> - emqx_packet:will_msg( - case ProtoVer of - ?MQTT_PROTO_V5 -> - WillDelayInterval = get_property('Will-Delay-Interval', WillProps, 0), - ConnPkt#mqtt_packet_connect{ - will_props = set_property('Will-Delay-Interval', WillDelayInterval, WillProps)}; - _ -> - ConnPkt - end). - -%%-------------------------------------------------------------------- -%% Check Packet -%%-------------------------------------------------------------------- - -check_connect(Packet, PState) -> - run_check_steps([fun check_proto_ver/2, - fun check_client_id/2, - fun check_flapping/2, - fun check_banned/2, - fun check_will_topic/2, - fun check_will_retain/2], Packet, PState). - -check_proto_ver(#mqtt_packet_connect{proto_ver = Ver, - proto_name = Name}, _PState) -> - case lists:member({Ver, Name}, ?PROTOCOL_NAMES) of - true -> ok; - false -> {error, ?RC_PROTOCOL_ERROR} - end. - -%% MQTT3.1 does not allow null clientId -check_client_id(#mqtt_packet_connect{proto_ver = ?MQTT_PROTO_V3, - client_id = <<>>}, _PState) -> - {error, ?RC_CLIENT_IDENTIFIER_NOT_VALID}; - -%% Issue#599: Null clientId and clean_start = false -check_client_id(#mqtt_packet_connect{client_id = <<>>, - clean_start = false}, _PState) -> - {error, ?RC_CLIENT_IDENTIFIER_NOT_VALID}; - -check_client_id(#mqtt_packet_connect{client_id = <<>>, - clean_start = true}, _PState) -> - ok; - -check_client_id(#mqtt_packet_connect{client_id = ClientId}, #protocol{zone = Zone}) -> - Len = byte_size(ClientId), - MaxLen = emqx_zone:get_env(Zone, max_clientid_len), - case (1 =< Len) andalso (Len =< MaxLen) of - true -> ok; - false -> {error, ?RC_CLIENT_IDENTIFIER_NOT_VALID} - end. - -check_flapping(#mqtt_packet_connect{}, PState) -> - do_flapping_detect(connect, PState). - -check_banned(#mqtt_packet_connect{client_id = ClientId, username = Username}, - #protocol{zone = Zone, peername = Peername}) -> - Credentials = #{client_id => ClientId, - username => Username, - peername => Peername}, - EnableBan = emqx_zone:get_env(Zone, enable_ban, false), - do_check_banned(EnableBan, Credentials). - -check_will_topic(#mqtt_packet_connect{will_flag = false}, _PState) -> - ok; -check_will_topic(#mqtt_packet_connect{will_topic = WillTopic} = ConnPkt, PState) -> - try emqx_topic:validate(WillTopic) of - true -> check_will_acl(ConnPkt, PState) - catch error : _Error -> - {error, ?RC_TOPIC_NAME_INVALID} - end. - -check_will_retain(#mqtt_packet_connect{will_retain = false, proto_ver = ?MQTT_PROTO_V5}, _PState) -> - ok; -check_will_retain(#mqtt_packet_connect{will_retain = true, proto_ver = ?MQTT_PROTO_V5}, #protocol{zone = Zone}) -> - case emqx_zone:get_env(Zone, mqtt_retain_available, true) of - true -> {error, ?RC_RETAIN_NOT_SUPPORTED}; - false -> ok - end; -check_will_retain(_Packet, _PState) -> - ok. - -check_will_acl(#mqtt_packet_connect{will_topic = WillTopic}, - #protocol{zone = Zone, credentials = Credentials}) -> - EnableAcl = emqx_zone:get_env(Zone, enable_acl, false), - case do_acl_check(EnableAcl, publish, Credentials, WillTopic) of - ok -> ok; - Other -> - ?LOG(warning, "Cannot publish will message to ~p for acl denied", [WillTopic]), - Other - end. - -check_publish(Packet, PState) -> - run_check_steps([fun check_pub_caps/2, - fun check_pub_acl/2], Packet, PState). - -check_pub_caps(#mqtt_packet{header = #mqtt_packet_header{qos = QoS, retain = Retain}, - variable = #mqtt_packet_publish{properties = _Properties}}, - #protocol{zone = Zone}) -> - emqx_mqtt_caps:check_pub(Zone, #{qos => QoS, retain => Retain}). - -check_pub_acl(_Packet, #protocol{credentials = #{is_superuser := IsSuper}}) - when IsSuper -> - ok; -check_pub_acl(#mqtt_packet{variable = #mqtt_packet_publish{topic_name = Topic}}, - #protocol{zone = Zone, credentials = Credentials}) -> - EnableAcl = emqx_zone:get_env(Zone, enable_acl, false), - do_acl_check(EnableAcl, publish, Credentials, Topic). - -run_check_steps([], _Packet, _PState) -> - ok; -run_check_steps([Check|Steps], Packet, PState) -> - case Check(Packet, PState) of - ok -> - run_check_steps(Steps, Packet, PState); - Error = {error, _RC} -> - Error - end. - -check_subscribe(TopicFilters, PState = #protocol{zone = Zone}) -> - case emqx_mqtt_caps:check_sub(Zone, TopicFilters) of - {ok, TopicFilter1} -> - check_sub_acl(TopicFilter1, PState); - {error, TopicFilter1} -> - {error, TopicFilter1} - end. - -check_sub_acl(TopicFilters, #protocol{credentials = #{is_superuser := IsSuper}}) - when IsSuper -> - {ok, TopicFilters}; -check_sub_acl(TopicFilters, #protocol{zone = Zone, credentials = Credentials}) -> - EnableAcl = emqx_zone:get_env(Zone, enable_acl, false), - lists:foldr( - fun({Topic, SubOpts}, {Ok, Acc}) when EnableAcl -> - AllowTerm = {Ok, [{Topic, SubOpts}|Acc]}, - DenyTerm = {error, [{Topic, SubOpts#{rc := ?RC_NOT_AUTHORIZED}}|Acc]}, - do_acl_check(subscribe, Credentials, Topic, AllowTerm, DenyTerm); - (TopicFilter, Acc) -> - {ok, [TopicFilter | Acc]} - end, {ok, []}, TopicFilters). - -terminate(_Reason, #protocol{client_id = undefined}) -> - ok; -terminate(_Reason, PState = #protocol{connected = false}) -> - do_flapping_detect(disconnect, PState), - ok; -terminate(Reason, PState) when Reason =:= conflict; - Reason =:= discard -> - do_flapping_detect(disconnect, PState), - ok; - -terminate(Reason, PState = #protocol{credentials = Credentials}) -> - do_flapping_detect(disconnect, PState), - ?LOG(info, "Shutdown for ~p", [Reason]), - ok = emqx_hooks:run('client.disconnected', [Credentials, Reason]). - -start_keepalive(0, _PState) -> - ignore; -start_keepalive(Secs, #protocol{zone = Zone}) when Secs > 0 -> - Backoff = emqx_zone:get_env(Zone, keepalive_backoff, 0.75), - self() ! {keepalive, start, round(Secs * Backoff)}. - -%%-------------------------------------------------------------------- -%% Parse topic filters -%%-------------------------------------------------------------------- - -parse_topic_filters(?SUBSCRIBE, RawTopicFilters) -> - [emqx_topic:parse(RawTopic, SubOpts) || {RawTopic, SubOpts} <- RawTopicFilters]; - -parse_topic_filters(?UNSUBSCRIBE, RawTopicFilters) -> - lists:map(fun emqx_topic:parse/1, RawTopicFilters). - -sp(true) -> 1; -sp(false) -> 0. - -flag(false) -> 0; -flag(true) -> 1. - -%%-------------------------------------------------------------------- -%% Execute actions in case acl deny - -do_flapping_detect(Action, #protocol{zone = Zone, - client_id = ClientId}) -> - ok = case emqx_zone:get_env(Zone, enable_flapping_detect, false) of - true -> - Threshold = emqx_zone:get_env(Zone, flapping_threshold, {10, 60}), - case emqx_flapping:check(Action, ClientId, Threshold) of - flapping -> - BanExpiryInterval = emqx_zone:get_env(Zone, flapping_banned_expiry_interval, 3600000), - Until = erlang:system_time(second) + BanExpiryInterval, - emqx_banned:add(#banned{who = {client_id, ClientId}, - reason = <<"flapping">>, - by = <<"flapping_checker">>, - until = Until}), - ok; - _Other -> - ok - end; - _EnableFlappingDetect -> ok - end. - -do_acl_deny_action(disconnect, ?PUBLISH_PACKET(?QOS_0, _Topic, _PacketId, _Payload), - ?RC_NOT_AUTHORIZED, PState = #protocol{proto_ver = ProtoVer}) -> - {error, emqx_reason_codes:name(?RC_NOT_AUTHORIZED, ProtoVer), PState}; - -do_acl_deny_action(disconnect, ?PUBLISH_PACKET(QoS, _Topic, _PacketId, _Payload), - ?RC_NOT_AUTHORIZED, PState = #protocol{proto_ver = ProtoVer}) - when QoS =:= ?QOS_1; QoS =:= ?QOS_2 -> - %% TODO:... - %% deliver({disconnect, ?RC_NOT_AUTHORIZED}, PState), - {error, emqx_reason_codes:name(?RC_NOT_AUTHORIZED, ProtoVer), PState}; - -do_acl_deny_action(Action, ?SUBSCRIBE_PACKET(_PacketId, _Properties, _RawTopicFilters), ReasonCodes, PState) - when is_list(ReasonCodes) -> - traverse_reason_codes(ReasonCodes, Action, PState); -do_acl_deny_action(_OtherAction, _PubSubPacket, ?RC_NOT_AUTHORIZED, PState) -> - {ok, PState}; -do_acl_deny_action(_OtherAction, _PubSubPacket, ReasonCode, PState = #protocol{proto_ver = ProtoVer}) -> - {error, emqx_reason_codes:name(ReasonCode, ProtoVer), PState}. - -traverse_reason_codes([], _Action, PState) -> - {ok, PState}; -traverse_reason_codes([?RC_SUCCESS | LeftReasonCodes], Action, PState) -> - traverse_reason_codes(LeftReasonCodes, Action, PState); -traverse_reason_codes([?RC_NOT_AUTHORIZED | _LeftReasonCodes], disconnect, PState = #protocol{proto_ver = ProtoVer}) -> - {error, emqx_reason_codes:name(?RC_NOT_AUTHORIZED, ProtoVer), PState}; -traverse_reason_codes([?RC_NOT_AUTHORIZED | LeftReasonCodes], Action, PState) -> - traverse_reason_codes(LeftReasonCodes, Action, PState); -traverse_reason_codes([OtherCode | _LeftReasonCodes], _Action, PState = #protocol{proto_ver = ProtoVer}) -> - {error, emqx_reason_codes:name(OtherCode, ProtoVer), PState}. - -%% Reason code compat -reason_codes_compat(_PktType, ReasonCodes, ?MQTT_PROTO_V5) -> - ReasonCodes; -reason_codes_compat(unsuback, _ReasonCodes, _ProtoVer) -> - undefined; -reason_codes_compat(PktType, ReasonCodes, _ProtoVer) -> - [emqx_reason_codes:compat(PktType, RC) || RC <- ReasonCodes]. - -raw_topic_filters(#protocol{zone = Zone, proto_ver = ProtoVer, is_bridge = IsBridge}, RawTopicFilters) -> - IgnoreLoop = emqx_zone:get_env(Zone, ignore_loop_deliver, false), - case ProtoVer < ?MQTT_PROTO_V5 of - true -> - IfIgnoreLoop = case IgnoreLoop of true -> 1; false -> 0 end, - case IsBridge of - true -> [{RawTopic, SubOpts#{rap => 1, nl => IfIgnoreLoop}} || {RawTopic, SubOpts} <- RawTopicFilters]; - false -> [{RawTopic, SubOpts#{rap => 0, nl => IfIgnoreLoop}} || {RawTopic, SubOpts} <- RawTopicFilters] - end; - false -> - RawTopicFilters - end. - -mountpoint(Credentials) -> - maps:get(mountpoint, Credentials, undefined). - -do_check_banned(_EnableBan = true, Credentials) -> - case emqx_banned:check(Credentials) of - true -> {error, ?RC_BANNED}; - false -> ok - end; -do_check_banned(_EnableBan, _Credentials) -> ok. - -do_acl_check(_EnableAcl = true, Action, Credentials, Topic) -> - AllowTerm = ok, - DenyTerm = {error, ?RC_NOT_AUTHORIZED}, - do_acl_check(Action, Credentials, Topic, AllowTerm, DenyTerm); -do_acl_check(_EnableAcl, _Action, _Credentials, _Topic) -> - ok. - -do_acl_check(Action, Credentials, Topic, AllowTerm, DenyTerm) -> - case emqx_access_control:check_acl(Credentials, Action, Topic) of - allow -> AllowTerm; - deny -> DenyTerm - end. - diff --git a/src/emqx_reason_codes.erl b/src/emqx_reason_codes.erl index a99406f53..327b96018 100644 --- a/src/emqx_reason_codes.erl +++ b/src/emqx_reason_codes.erl @@ -22,6 +22,7 @@ -export([ name/2 , text/1 , connack_error/1 + , puback/1 ]). -export([compat/2]). @@ -161,3 +162,6 @@ connack_error(server_busy) -> ?RC_SERVER_BUSY; connack_error(banned) -> ?RC_BANNED; connack_error(bad_authentication_method) -> ?RC_BAD_AUTHENTICATION_METHOD; connack_error(_) -> ?RC_NOT_AUTHORIZED. + +puback([]) -> ?RC_NO_MATCHING_SUBSCRIBERS; +puback(L) when is_list(L) -> ?RC_SUCCESS. diff --git a/src/emqx_session.erl b/src/emqx_session.erl index f5e7414d5..e699a7252 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -69,7 +69,7 @@ , pubcomp/3 ]). --export([ deliver/3 +-export([ deliver/2 , await/3 , enqueue/2 ]). @@ -397,31 +397,29 @@ pubcomp(PacketId, ReasonCode, Session = #session{inflight = Inflight, mqueue = Q %% Handle delivery %%-------------------------------------------------------------------- -deliver(Topic, Msg, Session = #session{subscriptions = SubMap}) -> - SubOpts = get_subopts(Topic, SubMap), - case enrich(SubOpts, Msg, Session) of - {ok, Msg1} -> - deliver(Msg1, Session); - ignore -> ignore - end. +deliver(Delivers, Session = #session{subscriptions = SubMap}) + when is_list(Delivers) -> + Msgs = [enrich(get_subopts(Topic, SubMap), Msg, Session) + || {Topic, Msg} <- Delivers], + deliver(Msgs, [], Session). -%% Enqueue message if the client has been disconnected -%% process_msg(Msg, Session = #session{conn_pid = undefined}) -> -%% {ignore, enqueue_msg(Msg, Session)}; -deliver(Msg = #message{qos = ?QOS_0}, Session) -> - {ok, {publish, undefined, Msg}, Session}; +deliver([], Publishes, Session) -> + {ok, lists:reverse(Publishes), Session}; -deliver(Msg = #message{qos = QoS}, +deliver([Msg = #message{qos = ?QOS_0}|More], Acc, Session) -> + deliver(More, [{publish, undefined, Msg}|Acc], Session); + +deliver([Msg = #message{qos = QoS}|More], Acc, Session = #session{next_pkt_id = PacketId, inflight = Inflight}) when QoS =:= ?QOS_1 orelse QoS =:= ?QOS_2 -> case emqx_inflight:is_full(Inflight) of true -> - {ignore, enqueue(Msg, Session)}; + deliver(More, Acc, enqueue(Msg, Session)); false -> Publish = {publish, PacketId, Msg}, NSession = await(PacketId, Msg, Session), - {ok, Publish, next_pkt_id(NSession)} + deliver(More, [Publish|Acc], next_pkt_id(NSession)) end. enqueue(Msg, Session = #session{mqueue = Q}) -> @@ -454,7 +452,7 @@ get_subopts(Topic, SubMap) -> end. enrich([], Msg, _Session) -> - {ok, Msg}; + Msg; %%enrich([{nl, 1}|_Opts], #message{from = ClientId}, #session{client_id = ClientId}) -> %% ignore; enrich([{nl, _}|Opts], Msg, Session) -> diff --git a/src/emqx_ws_connection.erl b/src/emqx_ws_connection.erl index 73c37fc04..b21897b5e 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_connection.erl @@ -14,20 +14,19 @@ %% limitations under the License. %%-------------------------------------------------------------------- -%% MQTT WebSocket Channel --module(emqx_ws_channel). +%% MQTT WebSocket Connection +-module(emqx_ws_connection). -include("emqx.hrl"). -include("emqx_mqtt.hrl"). -include("logger.hrl"). +-include("types.hrl"). --logger_header("[WS Channel]"). +-logger_header("[WS Conn]"). -export([ info/1 , attrs/1 , stats/1 - , kick/1 - , session/1 ]). %% websocket callbacks @@ -41,18 +40,19 @@ -record(state, { request, options, - peername, - sockname, - proto_state, - parse_state, - keepalive, - enable_stats, - stats_timer, - idle_timeout, + peername :: {inet:ip_address(), inet:port_number()}, + sockname :: {inet:ip_address(), inet:port_number()}, + parse_state :: emqx_frame:parse_state(), + packets :: list(emqx_mqtt:packet()), + chan_state :: emqx_channel:channel(), + keepalive :: maybe(emqx_keepalive:keepalive()), + stats_timer :: disabled | maybe(reference()), + idle_timeout :: timeout(), shutdown }). -define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt]). +-define(CHAN_STATS, [recv_pkt, recv_msg, send_pkt, send_msg]). %%-------------------------------------------------------------------- %% API @@ -62,42 +62,40 @@ info(WSPid) when is_pid(WSPid) -> call(WSPid, info); -info(#state{peername = Peername, - sockname = Sockname, - proto_state = ProtoState}) -> - ProtoInfo = emqx_protocol:info(ProtoState), +info(#state{peername = Peername, + sockname = Sockname, + chan_state = ChanState}) -> ConnInfo = #{socktype => websocket, conn_state => running, peername => Peername, - sockname => Sockname}, - maps:merge(ProtoInfo, ConnInfo). + sockname => Sockname + }, + ChanInfo = emqx_channel:info(ChanState), + maps:merge(ConnInfo, ChanInfo). %% for dashboard attrs(WSPid) when is_pid(WSPid) -> call(WSPid, attrs); -attrs(#state{peername = Peername, - sockname = Sockname, - proto_state = ProtoState}) -> +attrs(#state{peername = Peername, + sockname = Sockname, + chan_state = ChanState}) -> SockAttrs = #{peername => Peername, sockname => Sockname}, - ProtoAttrs = emqx_protocol:attrs(ProtoState), - maps:merge(SockAttrs, ProtoAttrs). + ChanAttrs = emqx_channel:attrs(ChanState), + maps:merge(SockAttrs, ChanAttrs). stats(WSPid) when is_pid(WSPid) -> call(WSPid, stats); -stats(#state{proto_state = ProtoState}) -> - lists:append([wsock_stats(), - emqx_misc:proc_stats(), - emqx_protocol:stats(ProtoState) - ]). +stats(#state{}) -> + lists:append([chan_stats(), wsock_stats(), emqx_misc:proc_stats()]). -kick(WSPid) when is_pid(WSPid) -> - call(WSPid, kick). +%%kick(WSPid) when is_pid(WSPid) -> +%% call(WSPid, kick). -session(WSPid) when is_pid(WSPid) -> - call(WSPid, session). +%%session(WSPid) when is_pid(WSPid) -> +%% call(WSPid, session). call(WSPid, Req) when is_pid(WSPid) -> Mref = erlang:monitor(process, WSPid), @@ -153,24 +151,24 @@ websocket_init(#state{request = Req, options = Options}) -> [Error, Reason]), undefined end, - ProtoState = emqx_protocol:init(#{peername => Peername, - sockname => Sockname, - peercert => Peercert, - sendfun => send_fun(self()), - ws_cookie => WsCookie, - conn_mod => ?MODULE}, Options), + ChanState = emqx_channel:init(#{peername => Peername, + sockname => Sockname, + peercert => Peercert, + ws_cookie => WsCookie, + conn_mod => ?MODULE}, Options), Zone = proplists:get_value(zone, Options), MaxSize = emqx_zone:get_env(Zone, max_packet_size, ?MAX_PACKET_SIZE), ParseState = emqx_frame:initial_parse_state(#{max_size => MaxSize}), EnableStats = emqx_zone:get_env(Zone, enable_stats, true), + StatsTimer = if EnableStats -> undefined; ?Otherwise-> disabled end, IdleTimout = emqx_zone:get_env(Zone, idle_timeout, 30000), emqx_logger:set_metadata_peername(esockd_net:format(Peername)), ok = emqx_misc:init_proc_mng_policy(Zone), {ok, #state{peername = Peername, sockname = Sockname, parse_state = ParseState, - proto_state = ProtoState, - enable_stats = EnableStats, + chan_state = ChanState, + stats_timer = StatsTimer, idle_timeout = IdleTimout }}. @@ -244,24 +242,21 @@ websocket_info({call, From, kick}, State) -> gen_server:reply(From, ok), shutdown(kick, State); -websocket_info({call, From, session}, State = #state{proto_state = ProtoState}) -> - gen_server:reply(From, emqx_protocol:session(ProtoState)), - {ok, State}; - -websocket_info(Delivery, State = #state{proto_state = ProtoState}) +websocket_info(Delivery, State = #state{chan_state = ChanState}) when element(1, Delivery) =:= deliver -> - case emqx_protocol:handle_out(Delivery, ProtoState) of - {ok, NProtoState} -> - {ok, State#state{proto_state = NProtoState}}; - {ok, Packet, NProtoState} -> - handle_outgoing(Packet, State#state{proto_state = NProtoState}); + case emqx_channel:handle_out(Delivery, ChanState) of + {ok, NChanState} -> + {ok, State#state{chan_state = NChanState}}; + {ok, Packet, NChanState} -> + handle_outgoing(Packet, State#state{chan_state = NChanState}); {error, Reason} -> shutdown(Reason, State) end; websocket_info({timeout, Timer, emit_stats}, - State = #state{stats_timer = Timer, proto_state = ProtoState}) -> - emqx_cm:set_conn_stats(emqx_protocol:client_id(ProtoState), stats(State)), + State = #state{stats_timer = Timer, chan_state = ChanState}) -> + ClientId = emqx_channel:client_id(ChanState), + ok = emqx_cm:set_conn_stats(ClientId, stats(State)), {ok, State#state{stats_timer = undefined}, hibernate}; websocket_info({keepalive, start, Interval}, State) -> @@ -307,59 +302,74 @@ websocket_info(Info, State) -> ?LOG(error, "Unexpected info: ~p", [Info]), {ok, State}. -terminate(SockError, _Req, #state{keepalive = Keepalive, - proto_state = ProtoState, - shutdown = Shutdown}) -> +terminate(SockError, _Req, #state{keepalive = Keepalive, + chan_state = ChanState, + shutdown = Shutdown}) -> ?LOG(debug, "Terminated for ~p, sockerror: ~p", [Shutdown, SockError]), emqx_keepalive:cancel(Keepalive), - case {ProtoState, Shutdown} of + case {ChanState, Shutdown} of {undefined, _} -> ok; {_, {shutdown, Reason}} -> - emqx_protocol:terminate(Reason, ProtoState); + emqx_channel:terminate(Reason, ChanState); {_, Error} -> - emqx_protocol:terminate(Error, ProtoState) + emqx_channel:terminate(Error, ChanState) end. %%-------------------------------------------------------------------- %% Internal functions %%-------------------------------------------------------------------- -handle_incoming(Packet, SuccFun, State = #state{proto_state = ProtoState}) -> - case emqx_protocol:handle_in(Packet, ProtoState) of - {ok, NProtoState} -> - SuccFun(State#state{proto_state = NProtoState}); - {ok, OutPacket, NProtoState} -> - %% TODO: How to call SuccFun??? - handle_outgoing(OutPacket, State#state{proto_state = NProtoState}); - {error, Reason} -> - ?LOG(error, "Protocol error: ~p", [Reason]), - shutdown(Reason, State); - {error, Reason, NProtoState} -> - shutdown(Reason, State#state{proto_state = NProtoState}); - {stop, Error, NProtoState} -> - shutdown(Error, State#state{proto_state = NProtoState}) +handle_incoming(Packet = ?PACKET(Type), SuccFun, + State = #state{chan_state = ChanState}) -> + _ = inc_incoming_stats(Type), + case emqx_channel:handle_in(Packet, ChanState) of + {ok, NChanState} -> + SuccFun(State#state{chan_state = NChanState}); + {ok, OutPacket, NChanState} -> + %% TODO: SuccFun, + handle_outgoing(OutPacket, State#state{chan_state = NChanState}); + {error, Reason, NChanState} -> + shutdown(Reason, State#state{chan_state = NChanState}); + {stop, Error, NChanState} -> + shutdown(Error, State#state{chan_state = NChanState}) end. -handle_outgoing(Packet, State = #state{proto_state = _NProtoState}) -> - Data = emqx_frame:serialize(Packet), %% TODO:, Options), +handle_outgoing(Packet = ?PACKET(Type), State = #state{chan_state = ChanState}) -> + ProtoVer = emqx_channel:info(proto_ver, ChanState), + Data = emqx_frame:serialize(Packet, ProtoVer), BinSize = iolist_size(Data), - emqx_pd:update_counter(send_cnt, 1), - emqx_pd:update_counter(send_oct, BinSize), + _ = inc_outgoing_stats(Type, BinSize), {reply, {binary, Data}, ensure_stats_timer(State)}. -ensure_stats_timer(State = #state{enable_stats = true, - stats_timer = undefined, - idle_timeout = IdleTimeout}) -> - State#state{stats_timer = emqx_misc:start_timer(IdleTimeout, emit_stats)}; -ensure_stats_timer(State) -> - State. +inc_incoming_stats(Type) -> + emqx_pd:update_counter(recv_pkt, 1), + (Type == ?PUBLISH) + andalso emqx_pd:update_counter(recv_msg, 1). +inc_outgoing_stats(Type, BinSize) -> + emqx_pd:update_counter(send_cnt, 1), + emqx_pd:update_counter(send_oct, BinSize), + emqx_pd:update_counter(send_pkt, 1), + (Type == ?PUBLISH) + andalso emqx_pd:update_counter(send_msg, 1). + +ensure_stats_timer(State = #state{stats_timer = undefined, + idle_timeout = IdleTimeout}) -> + TRef = emqx_misc:start_timer(IdleTimeout, emit_stats), + State#state{stats_timer = TRef}; +%% disabled or timer existed +ensure_stats_timer(State) -> State. + +-compile({inline, [shutdown/2]}). shutdown(Reason, State) -> %% Fix the issue#2591(https://github.com/emqx/emqx/issues/2591#issuecomment-500278696) - self() ! {stop, Reason}, - {ok, State}. + %% self() ! {stop, Reason}, + {stop, State#state{shutdown = Reason}}. wsock_stats() -> [{Key, emqx_pd:get_counter(Key)} || Key <- ?SOCK_STATS]. +chan_stats() -> + [{Name, emqx_pd:get_counter(Name)} || Name <- ?CHAN_STATS]. + From f4a753f683fe603a4320ce2f9f562f2e492d9afd Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 2 Jul 2019 14:50:13 +0800 Subject: [PATCH 16/33] Remove the debug print of batch deliver --- src/emqx_alarm_handler.erl | 2 +- src/emqx_connection.erl | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/emqx_alarm_handler.erl b/src/emqx_alarm_handler.erl index e40c85bb9..ca4f4f9bb 100644 --- a/src/emqx_alarm_handler.erl +++ b/src/emqx_alarm_handler.erl @@ -195,4 +195,4 @@ set_alarm_history(Id, Desc) -> His = #alarm_history{id = Id, desc = Desc, clear_at = os:timestamp()}, - mnesia:dirty_write(?ALARM_HISTORY_TAB, His}). + mnesia:dirty_write(?ALARM_HISTORY_TAB, His). diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index f58e2fa57..b7478e445 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -234,11 +234,6 @@ connected(cast, {incoming, Packet = ?PACKET(Type)}, State) -> connected(info, Deliver = {deliver, _Topic, _Msg}, State = #state{chan_state = ChanState}) -> Delivers = emqx_misc:drain_deliver([Deliver]), - %% TODO: ... - case BatchLen = length(Delivers) of - 1 -> ok; - N -> io:format("Batch Deliver: ~w~n", [N]) - end, case emqx_channel:handle_out(Delivers, ChanState) of {ok, NChanState} -> keep_state(State#state{chan_state = NChanState}); From dfa2c2370ec98ffecac0b12a4ca9e56494a73a46 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sat, 13 Jul 2019 12:25:25 +0800 Subject: [PATCH 17/33] Define 'Otherwise' macro --- include/emqx.hrl | 6 ++++++ include/logger.hrl | 5 ++++- include/types.hrl | 3 +-- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/include/emqx.hrl b/include/emqx.hrl index 987e4672c..745b492ef 100644 --- a/include/emqx.hrl +++ b/include/emqx.hrl @@ -17,6 +17,12 @@ -ifndef(EMQ_X_HRL). -define(EMQ_X_HRL, true). +%%-------------------------------------------------------------------- +%% Common +%%-------------------------------------------------------------------- + +-define(Otherwise, true). + %%-------------------------------------------------------------------- %% Banner %%-------------------------------------------------------------------- diff --git a/include/logger.hrl b/include/logger.hrl index 1cf5facc6..6f046e3c2 100644 --- a/include/logger.hrl +++ b/include/logger.hrl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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. +%%-------------------------------------------------------------------- %% debug | info | notice | warning | error | critical | alert | emergency @@ -43,3 +45,4 @@ begin (logger:log(Level,#{},#{report_cb => fun(_) -> {'$logger_header'()++(Format), (Args)} end})) end). + diff --git a/include/types.hrl b/include/types.hrl index f37d11a66..41b7ad50e 100644 --- a/include/types.hrl +++ b/include/types.hrl @@ -14,8 +14,6 @@ %% limitations under the License. %%-------------------------------------------------------------------- --define(Otherwise, true). - -type(maybe(T) :: undefined | T). -type(startlink_ret() :: {ok, pid()} | ignore | {error, term()}). @@ -23,3 +21,4 @@ -type(ok_or_error(Reason) :: ok | {error, Reason}). -type(ok_or_error(Value, Reason) :: {ok, Value} | {error, Reason}). + From 7b27d49a40cd55215d193c440a15b131a93b99f0 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sun, 14 Jul 2019 08:59:38 +0800 Subject: [PATCH 18/33] Add new metric 'channel.gc.cnt' --- src/emqx_metrics.erl | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/emqx_metrics.erl b/src/emqx_metrics.erl index 5df7c02e9..1f3d389fa 100644 --- a/src/emqx_metrics.erl +++ b/src/emqx_metrics.erl @@ -134,10 +134,15 @@ {counter, 'messages.forward'} % Messages forward ]). +-define(CHAN_METRICS, [ + {counter, 'channel.gc.cnt'} +]). + -define(MQTT_METRICS, [ {counter, 'auth.mqtt.anonymous'} ]). + -record(state, {next_idx = 1}). -record(metric, {name, type, idx}). @@ -262,7 +267,7 @@ update_counter(Name, Value) -> %%-------------------------------------------------------------------- %% @doc Inc packets received. --spec(inc_recv(emqx_mqtt_types:packet()) -> ok). +-spec(inc_recv(emqx_types:packet()) -> ok). inc_recv(Packet) -> inc('packets.received'), do_inc_recv(Packet). @@ -299,7 +304,7 @@ do_inc_recv(_Packet) -> ignore. %% @doc Inc packets sent. Will not count $SYS PUBLISH. --spec(inc_sent(emqx_mqtt_types:packet()) -> ok | ignore). +-spec(inc_sent(emqx_types:packet()) -> ok | ignore). inc_sent(?PUBLISH_PACKET(_QoS, <<"$SYS/", _/binary>>, _, _)) -> ignore; inc_sent(Packet) -> @@ -453,4 +458,5 @@ 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(_) -> undefined. From a6262ffb303b22f332d0fc45f89e465b40a22b02 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 22 Jul 2019 17:07:58 +0800 Subject: [PATCH 19/33] Remove the 'rc' field from DEFAULT_SUBOPTS --- include/emqx_client.hrl | 4 +++- include/emqx_mqtt.hrl | 3 +-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/include/emqx_client.hrl b/include/emqx_client.hrl index b23a02f65..622a51249 100644 --- a/include/emqx_client.hrl +++ b/include/emqx_client.hrl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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. +%%-------------------------------------------------------------------- -ifndef(EMQX_CLIENT_HRL). -define(EMQX_CLIENT_HRL, true). diff --git a/include/emqx_mqtt.hrl b/include/emqx_mqtt.hrl index 52b5d4313..854399999 100644 --- a/include/emqx_mqtt.hrl +++ b/include/emqx_mqtt.hrl @@ -204,8 +204,7 @@ -define(DEFAULT_SUBOPTS, #{rh => 0, %% Retain Handling rap => 0, %% Retain as Publish nl => 0, %% No Local - qos => 0, %% QoS - rc => 0 %% Reason Code + qos => 0 %% QoS }). -record(mqtt_packet_connect, { From 6b84eb0595fd7b45d1380e46b93b80546bb880ed Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 22 Jul 2019 17:08:53 +0800 Subject: [PATCH 20/33] Rewrite the channel, protocol and session modules. --- src/emqx_broker.erl | 4 +- src/emqx_channel.erl | 1030 +++++++++-------- src/emqx_client.erl | 8 +- src/emqx_cm.erl | 22 +- src/emqx_connection.erl | 586 ---------- src/emqx_endpoint.erl | 40 +- src/emqx_frame.erl | 8 +- src/emqx_gc.erl | 9 +- src/emqx_inflight.erl | 12 +- src/emqx_listeners.erl | 34 +- src/emqx_message.erl | 4 +- src/emqx_misc.erl | 9 +- src/emqx_mod_presence.erl | 6 +- src/emqx_mountpoint.erl | 10 +- src/emqx_mqtt.erl | 60 - src/emqx_mqtt_caps.erl | 9 +- src/emqx_packet.erl | 14 +- src/emqx_protocol.erl | 594 ++++++++++ src/emqx_session.erl | 461 ++++---- src/emqx_topic.erl | 41 +- src/emqx_types.erl | 78 +- ..._ws_connection.erl => emqx_ws_channel.erl} | 363 ++++-- test/emqx_inflight_SUITE.erl | 5 +- test/emqx_topic_SUITE.erl | 23 +- 24 files changed, 1824 insertions(+), 1606 deletions(-) delete mode 100644 src/emqx_connection.erl delete mode 100644 src/emqx_mqtt.erl create mode 100644 src/emqx_protocol.erl rename src/{emqx_ws_connection.erl => emqx_ws_channel.erl} (51%) diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index 6e74bcfa4..674c50194 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -376,9 +376,9 @@ set_subopts(Topic, NewOpts) when is_binary(Topic), is_map(NewOpts) -> topics() -> emqx_router:topics(). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Stats fun -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- stats_fun() -> safe_update_stats(?SUBSCRIBER, 'subscribers.count', 'subscribers.max'), diff --git a/src/emqx_channel.erl b/src/emqx_channel.erl index 9799719b6..a3804298b 100644 --- a/src/emqx_channel.erl +++ b/src/emqx_channel.erl @@ -14,568 +14,598 @@ %% limitations under the License. %%-------------------------------------------------------------------- -%% MQTT Channel +%% MQTT TCP/SSL Channel -module(emqx_channel). +-behaviour(gen_statem). + -include("emqx.hrl"). -include("emqx_mqtt.hrl"). -include("logger.hrl"). -include("types.hrl"). --logger_header("[Channel]"). +-export([start_link/3]). --export([ attrs/1 ]). - --export([ zone/1 - , client_id/1 - , conn_mod/1 - , endpoint/1 - , proto_ver/1 - , keepalive/1 - , session/1 +%% APIs +-export([ info/1 + , stats/1 ]). --export([ init/2 - , handle_in/2 - , handle_out/2 - , handle_timeout/3 - , terminate/2 +%% state callbacks +-export([ idle/3 + , connected/3 + , disconnected/3 ]). --export_type([channel/0]). +%% gen_statem callbacks +-export([ init/1 + , callback_mode/0 + , code_change/4 + , terminate/3 + ]). --record(channel, { - conn_mod :: maybe(module()), - endpoint :: emqx_endpoint:endpoint(), - proto_name :: binary(), - proto_ver :: emqx_mqtt:version(), - keepalive :: non_neg_integer(), - session :: emqx_session:session(), - will_msg :: emqx_types:message(), - enable_acl :: boolean(), - is_bridge :: boolean(), - connected :: boolean(), - topic_aliases :: map(), - alias_maximum :: map(), - connected_at :: erlang:timestamp() +-record(state, { + transport :: esockd:transport(), + socket :: esockd:socket(), + peername :: emqx_types:peername(), + sockname :: emqx_types:peername(), + conn_state :: running | blocked, + active_n :: pos_integer(), + rate_limit :: maybe(esockd_rate_limit:bucket()), + pub_limit :: maybe(esockd_rate_limit:bucket()), + limit_timer :: maybe(reference()), + serialize :: fun((emqx_types:packet()) -> iodata()), + parse_state :: emqx_frame:parse_state(), + proto_state :: emqx_protocol:proto_state(), + gc_state :: emqx_gc:gc_state(), + keepalive :: maybe(emqx_keepalive:keepalive()), + stats_timer :: disabled | maybe(reference()), + idle_timeout :: timeout() }). --opaque(channel() :: #channel{}). +-logger_header("[Channel]"). -attrs(#channel{endpoint = Endpoint, session = Session}) -> - maps:merge(emqx_endpoint:to_map(Endpoint), - emqx_session:attrs(Session)). +-define(ACTIVE_N, 100). +-define(HANDLE(T, C, D), handle((T), (C), (D))). +-define(CHAN_STATS, [recv_pkt, recv_msg, send_pkt, send_msg]). +-define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt, send_pend]). -zone(#channel{endpoint = Endpoint}) -> - emqx_endpoint:zone(Endpoint). +-spec(start_link(esockd:transport(), esockd:socket(), proplists:proplist()) + -> {ok, pid()}). +start_link(Transport, Socket, Options) -> + {ok, proc_lib:spawn_link(?MODULE, init, [{Transport, Socket, Options}])}. --spec(client_id(channel()) -> emqx_types:client_id()). -client_id(#channel{endpoint = Endpoint}) -> - emqx_endpoint:client_id(Endpoint). +%%-------------------------------------------------------------------- +%% API +%%-------------------------------------------------------------------- --spec(conn_mod(channel()) -> module()). -conn_mod(#channel{conn_mod = ConnMod}) -> - ConnMod. +%% @doc Get channel's info. +-spec(info(pid() | #state{}) -> proplists:proplist()). +info(CPid) when is_pid(CPid) -> + call(CPid, info); --spec(endpoint(channel()) -> emqx_endpoint:endpoint()). -endpoint(#channel{endpoint = Endpoint}) -> - Endpoint. +info(#state{transport = Transport, + socket = Socket, + peername = Peername, + sockname = Sockname, + conn_state = ConnState, + active_n = ActiveN, + rate_limit = RateLimit, + pub_limit = PubLimit, + proto_state = ProtoState, + gc_state = GCState, + stats_timer = StatsTimer, + idle_timeout = IdleTimeout}) -> + [{socktype, Transport:type(Socket)}, + {peername, Peername}, + {sockname, Sockname}, + {conn_state, ConnState}, + {active_n, ActiveN}, + {rate_limit, rate_limit_info(RateLimit)}, + {pub_limit, rate_limit_info(PubLimit)}, + {gc_state, emqx_gc:info(GCState)}, + {enable_stats, case StatsTimer of + disabled -> false; + _Otherwise -> true + end}, + {idle_timeout, IdleTimeout} | + emqx_protocol:info(ProtoState)]. --spec(proto_ver(channel()) -> emqx_mqtt:version()). -proto_ver(#channel{proto_ver = ProtoVer}) -> - ProtoVer. +rate_limit_info(undefined) -> + undefined; +rate_limit_info(Limit) -> + esockd_rate_limit:info(Limit). -keepalive(#channel{keepalive = Keepalive}) -> - Keepalive. +%% @doc Get channel's stats. +-spec(stats(pid() | #state{}) -> proplists:proplist()). +stats(CPid) when is_pid(CPid) -> + call(CPid, stats); --spec(session(channel()) -> emqx_session:session()). -session(#channel{session = Session}) -> - Session. +stats(#state{transport = Transport, socket = Socket}) -> + SockStats = case Transport:getstat(Socket, ?SOCK_STATS) of + {ok, Ss} -> Ss; + {error, _} -> [] + end, + ChanStats = [{Name, emqx_pd:get_counter(Name)} || Name <- ?CHAN_STATS], + lists:append([SockStats, ChanStats, emqx_misc:proc_stats()]). --spec(init(map(), proplists:proplist()) -> channel()). -init(ConnInfo = #{peername := Peername, - sockname := Sockname, - conn_mod := ConnMod}, Options) -> +%% @private +call(CPid, Req) -> + gen_statem:call(CPid, Req, infinity). + +%%-------------------------------------------------------------------- +%% gen_statem callbacks +%%-------------------------------------------------------------------- + +init({Transport, RawSocket, Options}) -> + {ok, Socket} = Transport:wait(RawSocket), + {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]), + emqx_logger:set_metadata_peername(esockd_net:format(Peername)), Zone = proplists:get_value(zone, Options), - Peercert = maps:get(peercert, ConnInfo, nossl), - Username = peer_cert_as_username(Peercert, Options), - Mountpoint = emqx_zone:get_env(Zone, mountpoint), - WsCookie = maps:get(ws_cookie, ConnInfo, undefined), - Endpoint = emqx_endpoint:new(#{zone => Zone, - peername => Peername, - sockname => Sockname, - username => Username, - peercert => Peercert, - mountpoint => Mountpoint, - ws_cookie => WsCookie - }), - EnableAcl = emqx_zone:get_env(Zone, enable_acl, false), - #channel{conn_mod = ConnMod, - endpoint = Endpoint, - enable_acl = EnableAcl, - is_bridge = false, - connected = false - }. + RateLimit = init_limiter(proplists:get_value(rate_limit, Options)), + PubLimit = init_limiter(emqx_zone:get_env(Zone, publish_limit)), + ActiveN = proplists:get_value(active_n, Options, ?ACTIVE_N), + MaxSize = emqx_zone:get_env(Zone, max_packet_size, ?MAX_PACKET_SIZE), + ParseState = emqx_frame:initial_parse_state(#{max_size => MaxSize}), + ProtoState = emqx_protocol:init(#{peername => Peername, + sockname => Sockname, + peercert => Peercert, + conn_mod => ?MODULE}, Options), + GcPolicy = emqx_zone:get_env(Zone, force_gc_policy, false), + GcState = emqx_gc:init(GcPolicy), + EnableStats = emqx_zone:get_env(Zone, enable_stats, true), + StatsTimer = if EnableStats -> undefined; ?Otherwise -> disabled end, + IdleTimout = emqx_zone:get_env(Zone, idle_timeout, 30000), + ok = emqx_misc:init_proc_mng_policy(Zone), + State = #state{transport = Transport, + socket = Socket, + peername = Peername, + conn_state = running, + active_n = ActiveN, + rate_limit = RateLimit, + pub_limit = PubLimit, + parse_state = ParseState, + proto_state = ProtoState, + gc_state = GcState, + stats_timer = StatsTimer, + idle_timeout = IdleTimout + }, + gen_statem:enter_loop(?MODULE, [{hibernate_after, 2 * IdleTimout}], + idle, State, self(), [IdleTimout]). -peer_cert_as_username(Peercert, Options) -> - case proplists:get_value(peer_cert_as_username, Options) of - cn -> esockd_peercert:common_name(Peercert); - dn -> esockd_peercert:subject(Peercert); - crt -> Peercert; - _ -> undefined +init_limiter(undefined) -> + undefined; +init_limiter({Rate, Burst}) -> + esockd_rate_limit:new(Rate, Burst). + +callback_mode() -> + [state_functions, state_enter]. + +%%-------------------------------------------------------------------- +%% Idle State + +idle(enter, _, State) -> + case activate_socket(State) of + ok -> keep_state_and_data; + {error, Reason} -> + shutdown(Reason, State) + end; + +idle(timeout, _Timeout, State) -> + stop(idle_timeout, State); + +idle(cast, {incoming, Packet = ?CONNECT_PACKET( + #mqtt_packet_connect{ + proto_ver = ProtoVer} + )}, State) -> + State1 = State#state{serialize = serialize_fun(ProtoVer)}, + handle_incoming(Packet, fun(NewSt) -> + {next_state, connected, NewSt} + end, State1); + +idle(cast, {incoming, Packet}, State) -> + ?LOG(warning, "Unexpected incoming: ~p", [Packet]), + shutdown(unexpected_incoming_packet, State); + +idle(EventType, Content, State) -> + ?HANDLE(EventType, Content, State). + +%%-------------------------------------------------------------------- +%% Connected State + +connected(enter, _PrevSt, State = #state{proto_state = ProtoState}) -> + ClientId = emqx_protocol:client_id(ProtoState), + ok = emqx_cm:set_chan_attrs(ClientId, info(State)), + %% Ensure keepalive after connected successfully. + Interval = emqx_protocol:info(keepalive, ProtoState), + case ensure_keepalive(Interval, State) of + ignore -> keep_state_and_data; + {ok, KeepAlive} -> + keep_state(State#state{keepalive = KeepAlive}); + {error, Reason} -> + shutdown(Reason, State) + end; + +connected(cast, {incoming, Packet = ?PACKET(?CONNECT)}, State) -> + ?LOG(warning, "Unexpected connect: ~p", [Packet]), + shutdown(unexpected_incoming_connect, State); + +connected(cast, {incoming, Packet}, State) when is_record(Packet, mqtt_packet) -> + handle_incoming(Packet, fun keep_state/1, State); + +connected(info, Deliver = {deliver, _Topic, _Msg}, + State = #state{proto_state = ProtoState}) -> + Delivers = emqx_misc:drain_deliver([Deliver]), + case emqx_protocol:handle_deliver(Delivers, ProtoState) of + {ok, NProtoState} -> + keep_state(State#state{proto_state = NProtoState}); + {ok, Packets, NProtoState} -> + NState = State#state{proto_state = NProtoState}, + handle_outgoing(Packets, fun keep_state/1, NState); + {error, Reason} -> + shutdown(Reason, State); + {error, Reason, NProtoState} -> + shutdown(Reason, State#state{proto_state = NProtoState}) + end; + +%% Keepalive timer +connected(info, {keepalive, check}, State = #state{keepalive = KeepAlive}) -> + case emqx_keepalive:check(KeepAlive) of + {ok, KeepAlive1} -> + keep_state(State#state{keepalive = KeepAlive1}); + {error, timeout} -> + shutdown(keepalive_timeout, State); + {error, Reason} -> + shutdown(Reason, State) + end; + +connected(EventType, Content, State) -> + ?HANDLE(EventType, Content, State). + +%%-------------------------------------------------------------------- +%% Disconnected State + +disconnected(enter, _, _State) -> + %% TODO: What to do? + %% CleanStart is true + keep_state_and_data; + +disconnected(EventType, Content, State) -> + ?HANDLE(EventType, Content, State). + +%% Handle call +handle({call, From}, info, State) -> + reply(From, info(State), State); + +handle({call, From}, stats, State) -> + reply(From, stats(State), State); + +%%handle({call, From}, kick, State) -> +%% ok = gen_statem:reply(From, ok), +%% shutdown(kicked, State); + +%%handle({call, From}, discard, State) -> +%% ok = gen_statem:reply(From, ok), +%% shutdown(discard, State); + +handle({call, From}, Req, State) -> + ?LOG(error, "Unexpected call: ~p", [Req]), + reply(From, ignored, State); + +%% Handle cast +handle(cast, Msg, State) -> + ?LOG(error, "Unexpected cast: ~p", [Msg]), + keep_state(State); + +%% Handle incoming data +handle(info, {Inet, _Sock, Data}, State) when Inet == tcp; + Inet == ssl -> + Oct = iolist_size(Data), + ?LOG(debug, "RECV ~p", [Data]), + emqx_pd:update_counter(incoming_bytes, Oct), + ok = emqx_metrics:inc('bytes.received', Oct), + NState = maybe_gc(1, Oct, State), + process_incoming(Data, ensure_stats_timer(NState)); + +handle(info, {Error, _Sock, Reason}, State) + when Error == tcp_error; Error == ssl_error -> + shutdown(Reason, State); + +handle(info, {Closed, _Sock}, State) + when Closed == tcp_closed; Closed == ssl_closed -> + shutdown(closed, State); + +handle(info, {Passive, _Sock}, State) when Passive == tcp_passive; + Passive == ssl_passive -> + %% Rate limit here:) + NState = ensure_rate_limit(State), + case activate_socket(NState) of + ok -> keep_state(NState); + {error, Reason} -> + shutdown(Reason, NState) + end; + +handle(info, activate_socket, State) -> + %% Rate limit timer expired. + NState = State#state{conn_state = running}, + case activate_socket(NState) of + ok -> + keep_state(NState#state{limit_timer = undefined}); + {error, Reason} -> + shutdown(Reason, NState) + end; + +handle(info, {inet_reply, _Sock, ok}, State) -> + %% something sent + keep_state(ensure_stats_timer(State)); + +handle(info, {inet_reply, _Sock, {error, Reason}}, State) -> + shutdown(Reason, State); + +handle(info, {timeout, Timer, emit_stats}, + State = #state{stats_timer = Timer, + proto_state = ProtoState, + gc_state = GcState}) -> + ClientId = emqx_protocol:client_id(ProtoState), + ok = emqx_cm:set_chan_stats(ClientId, stats(State)), + NState = State#state{stats_timer = undefined}, + Limits = erlang:get(force_shutdown_policy), + case emqx_misc:conn_proc_mng_policy(Limits) of + continue -> + keep_state(NState); + hibernate -> + %% going to hibernate, reset gc stats + GcState1 = emqx_gc:reset(GcState), + {keep_state, NState#state{gc_state = GcState1}, hibernate}; + {shutdown, Reason} -> + ?LOG(error, "Shutdown exceptionally due to ~p", [Reason]), + shutdown(Reason, NState) + end; + +handle(info, {timeout, Timer, Msg}, + State = #state{proto_state = ProtoState}) -> + case emqx_protocol:handle_timeout(Timer, Msg, ProtoState) of + {ok, NProtoState} -> + keep_state(State#state{proto_state = NProtoState}); + {ok, Packets, NProtoState} -> + handle_outgoing(Packets, fun keep_state/1, + State#state{proto_state = NProtoState}); + {error, Reason} -> + shutdown(Reason, State); + {error, Reason, NProtoState} -> + shutdown(Reason, State#state{proto_state = NProtoState}) + end; + +handle(info, {shutdown, discard, {ClientId, ByPid}}, State) -> + ?LOG(error, "Discarded by ~s:~p", [ClientId, ByPid]), + shutdown(discard, State); + +handle(info, {shutdown, conflict, {ClientId, NewPid}}, State) -> + ?LOG(warning, "Clientid '~s' conflict with ~p", [ClientId, NewPid]), + shutdown(conflict, State); + +handle(info, {shutdown, Reason}, State) -> + shutdown(Reason, State); + +handle(info, Info, State) -> + ?LOG(error, "Unexpected info: ~p", [Info]), + keep_state(State). + +code_change(_Vsn, State, Data, _Extra) -> + {ok, State, Data}. + +terminate(Reason, _StateName, #state{transport = Transport, + socket = Socket, + keepalive = KeepAlive, + proto_state = ProtoState}) -> + ?LOG(debug, "Terminated for ~p", [Reason]), + ok = Transport:fast_close(Socket), + ok = emqx_keepalive:cancel(KeepAlive), + emqx_protocol:terminate(Reason, ProtoState). + +%%-------------------------------------------------------------------- +%% Process incoming data + +-compile({inline, [process_incoming/2]}). +process_incoming(Data, State) -> + process_incoming(Data, [], State). + +process_incoming(<<>>, Packets, State) -> + {keep_state, State, next_incoming_events(Packets)}; + +process_incoming(Data, Packets, State = #state{parse_state = ParseState}) -> + try emqx_frame:parse(Data, ParseState) of + {ok, NParseState} -> + NState = State#state{parse_state = NParseState}, + {keep_state, NState, next_incoming_events(Packets)}; + {ok, Packet, Rest, NParseState} -> + NState = State#state{parse_state = NParseState}, + process_incoming(Rest, [Packet|Packets], NState); + {error, Reason} -> + shutdown(Reason, State) + catch + error:Reason:Stk -> + ?LOG(error, "Parse failed for ~p~n\ + Stacktrace:~p~nError data:~p", [Reason, Stk, Data]), + shutdown(parse_error, State) end. +next_incoming_events(Packets) when is_list(Packets) -> + [next_event(cast, {incoming, Packet}) + || Packet <- lists:reverse(Packets)]. + %%-------------------------------------------------------------------- %% Handle incoming packet -%%-------------------------------------------------------------------- --spec(handle_in(emqx_mqtt:packet(), channel()) - -> {ok, channel()} - | {ok, emqx_mqtt:packet(), channel()} - | {error, Reason :: term(), channel()} - | {stop, Error :: atom(), channel()}). -handle_in(?CONNECT_PACKET( - #mqtt_packet_connect{proto_name = ProtoName, - proto_ver = ProtoVer, - is_bridge = IsBridge, - client_id = ClientId, - username = Username, - password = Password, - keepalive = Keepalive} = ConnPkt), - Channel = #channel{endpoint = Endpoint}) -> - Endpoint1 = emqx_endpoint:update(#{client_id => ClientId, - username => Username, - password => Password - }, Endpoint), - emqx_logger:set_metadata_client_id(ClientId), - WillMsg = emqx_packet:will_msg(ConnPkt), - Channel1 = Channel#channel{endpoint = Endpoint1, - proto_name = ProtoName, - proto_ver = ProtoVer, - is_bridge = IsBridge, - keepalive = Keepalive, - will_msg = WillMsg - }, - %% fun validate_packet/2, - case pipeline([fun check_connect/2, - fun handle_connect/2], ConnPkt, Channel1) of - {ok, SP, Channel2} -> - handle_out({connack, ?RC_SUCCESS, sp(SP)}, Channel2); - {error, ReasonCode} -> - handle_out({connack, ReasonCode}, Channel1); - {error, ReasonCode, Channel2} -> - handle_out({connack, ReasonCode}, Channel2) - end; - -handle_in(Packet = ?PUBLISH_PACKET(QoS, Topic, PacketId), Channel) -> - case pipeline([fun validate_packet/2, - fun check_pub_caps/2, - fun check_pub_acl/2, - fun handle_publish/2], Packet, Channel) of - {error, ReasonCode} -> - ?LOG(warning, "Cannot publish qos~w message to ~s due to ~s", - [QoS, Topic, emqx_reason_codes:text(ReasonCode)]), - handle_out(case QoS of - ?QOS_0 -> {puberr, ReasonCode}; - ?QOS_1 -> {puback, PacketId, ReasonCode}; - ?QOS_2 -> {pubrec, PacketId, ReasonCode} - end, Channel); - Result -> Result - end; - -handle_in(?PUBACK_PACKET(PacketId, ReasonCode), Channel = #channel{session = Session}) -> - case emqx_session:puback(PacketId, ReasonCode, Session) of - {ok, NSession} -> - {ok, Channel#channel{session = NSession}}; - {error, _NotFound} -> - %% TODO: metrics? error msg? - {ok, Channel} - end; - -handle_in(?PUBREC_PACKET(PacketId, ReasonCode), Channel = #channel{session = Session}) -> - case emqx_session:pubrec(PacketId, ReasonCode, Session) of - {ok, NSession} -> - handle_out({pubrel, PacketId}, Channel#channel{session = NSession}); - {error, ReasonCode} -> - handle_out({pubrel, PacketId, ReasonCode}, Channel) - end; - -handle_in(?PUBREL_PACKET(PacketId, ReasonCode), Channel = #channel{session = Session}) -> - case emqx_session:pubrel(PacketId, ReasonCode, Session) of - {ok, NSession} -> - handle_out({pubcomp, PacketId}, Channel#channel{session = NSession}); - {error, ReasonCode} -> - handle_out({pubcomp, PacketId, ReasonCode}, Channel) - end; - -handle_in(?PUBCOMP_PACKET(PacketId, ReasonCode), Channel = #channel{session = Session}) -> - case emqx_session:pubcomp(PacketId, ReasonCode, Session) of - {ok, NSession} -> - {ok, Channel#channel{session = NSession}}; - {error, _ReasonCode} -> - %% TODO: How to handle the reason code? - {ok, Channel} - end; - -handle_in(?SUBSCRIBE_PACKET(PacketId, Properties, RawTopicFilters), - Channel = #channel{endpoint = Endpoint, session = Session}) -> - case check_subscribe(parse_topic_filters(?SUBSCRIBE, RawTopicFilters), Channel) of - {ok, TopicFilters} -> - TopicFilters1 = preprocess_topic_filters(?SUBSCRIBE, Endpoint, - enrich_subopts(TopicFilters, Channel)), - {ok, ReasonCodes, NSession} = emqx_session:subscribe(emqx_endpoint:to_map(Endpoint), - TopicFilters1, Session), - handle_out({suback, PacketId, ReasonCodes}, Channel#channel{session = NSession}); - {error, TopicFilters} -> - {Topics, ReasonCodes} = lists:unzip([{Topic, RC} || {Topic, #{rc := RC}} <- TopicFilters]), - ?LOG(warning, "Cannot subscribe ~p due to ~p", - [Topics, [emqx_reason_codes:text(R) || R <- ReasonCodes]]), - %% Tell the client that all subscriptions has been failed. - ReasonCodes1 = lists:map(fun(?RC_SUCCESS) -> - ?RC_IMPLEMENTATION_SPECIFIC_ERROR; - (RC) -> RC - end, ReasonCodes), - handle_out({suback, PacketId, ReasonCodes1}, Channel) - end; - -handle_in(?UNSUBSCRIBE_PACKET(PacketId, Properties, RawTopicFilters), - Channel = #channel{endpoint = Endpoint, session = Session}) -> - TopicFilters = preprocess_topic_filters( - ?UNSUBSCRIBE, Endpoint, - parse_topic_filters(?UNSUBSCRIBE, RawTopicFilters)), - {ok, ReasonCodes, NSession} = emqx_session:unsubscribe(emqx_endpoint:to_map(Endpoint), TopicFilters, Session), - handle_out({unsuback, PacketId, ReasonCodes}, Channel#channel{session = NSession}); - -handle_in(?PACKET(?PINGREQ), Channel) -> - {ok, ?PACKET(?PINGRESP), Channel}; - -handle_in(?DISCONNECT_PACKET(?RC_SUCCESS), Channel) -> - %% Clear will msg - {stop, normal, Channel#channel{will_msg = undefined}}; - -handle_in(?DISCONNECT_PACKET(RC), Channel = #channel{proto_ver = Ver}) -> - %% TODO: - %% {stop, {shutdown, abnormal_disconnet}, Channel}; - {stop, {shutdown, emqx_reason_codes:name(RC, Ver)}, Channel}; - -handle_in(?AUTH_PACKET(), Channel) -> - %%TODO: implement later. - {ok, Channel}; - -handle_in(Packet, Channel) -> - io:format("In: ~p~n", [Packet]), - {ok, Channel}. +handle_incoming(Packet = ?PACKET(Type), SuccFun, + State = #state{proto_state = ProtoState}) -> + _ = inc_incoming_stats(Type), + ok = emqx_metrics:inc_recv(Packet), + ?LOG(debug, "RECV ~s", [emqx_packet:format(Packet)]), + case emqx_protocol:handle_in(Packet, ProtoState) of + {ok, NProtoState} -> + SuccFun(State#state{proto_state = NProtoState}); + {ok, OutPackets, NProtoState} -> + handle_outgoing(OutPackets, SuccFun, + State#state{proto_state = NProtoState}); + {error, Reason, NProtoState} -> + shutdown(Reason, State#state{proto_state = NProtoState}); + {stop, Error, NProtoState} -> + stop(Error, State#state{proto_state = NProtoState}) + end. %%-------------------------------------------------------------------- -%% Handle outgoing packet -%%-------------------------------------------------------------------- +%% Handle outgoing packets -handle_out({connack, ?RC_SUCCESS, SP}, Channel = #channel{endpoint = Endpoint}) -> - ok = emqx_hooks:run('client.connected', - [emqx_endpoint:to_map(Endpoint), ?RC_SUCCESS, attrs(Channel)]), - Props = #{}, %% TODO: ... - {ok, ?CONNACK_PACKET(?RC_SUCCESS, SP, Props), Channel}; +handle_outgoing(Packets, SuccFun, State = #state{serialize = Serialize}) + when is_list(Packets) -> + send(lists:map(Serialize, Packets), SuccFun, State); -handle_out({connack, ReasonCode}, Channel = #channel{endpoint = Endpoint, - proto_ver = ProtoVer}) -> - ok = emqx_hooks:run('client.connected', - [emqx_endpoint:to_map(Endpoint), ReasonCode, attrs(Channel)]), - ReasonCode1 = if - ProtoVer == ?MQTT_PROTO_V5 -> ReasonCode; - true -> emqx_reason_codes:compat(connack, ReasonCode) - end, - Reason = emqx_reason_codes:name(ReasonCode1, ProtoVer), - {error, Reason, ?CONNACK_PACKET(ReasonCode1), Channel}; - -handle_out(Delivers, Channel = #channel{endpoint = Endpoint, session = Session}) - when is_list(Delivers) -> - case emqx_session:deliver([{Topic, Msg} || {deliver, Topic, Msg} <- Delivers], Session) of - {ok, Publishes, NSession} -> - Credentials = emqx_endpoint:credentials(Endpoint), - Packets = lists:map(fun({publish, PacketId, Msg}) -> - Msg0 = emqx_hooks:run_fold('message.deliver', [Credentials], Msg), - Msg1 = emqx_message:update_expiry(Msg0), - Msg2 = emqx_mountpoint:unmount(emqx_endpoint:mountpoint(Endpoint), Msg1), - emqx_packet:from_message(PacketId, Msg2) - end, Publishes), - {ok, Packets, Channel#channel{session = NSession}}; - {ok, NSession} -> - {ok, Channel#channel{session = NSession}} - end; - -handle_out({publish, PacketId, Msg}, Channel = #channel{endpoint = Endpoint}) -> - Credentials = emqx_endpoint:credentials(Endpoint), - Msg0 = emqx_hooks:run_fold('message.deliver', [Credentials], Msg), - Msg1 = emqx_message:update_expiry(Msg0), - Msg2 = emqx_mountpoint:unmount( - emqx_endpoint:mountpoint(Credentials), Msg1), - {ok, emqx_packet:from_message(PacketId, Msg2), Channel}; - -handle_out({puberr, ReasonCode}, Channel) -> - {ok, Channel}; - -handle_out({puback, PacketId, ReasonCode}, Channel) -> - {ok, ?PUBACK_PACKET(PacketId, ReasonCode), Channel}; - -handle_out({pubrel, PacketId}, Channel) -> - {ok, ?PUBREL_PACKET(PacketId), Channel}; -handle_out({pubrel, PacketId, ReasonCode}, Channel) -> - {ok, ?PUBREL_PACKET(PacketId, ReasonCode), Channel}; - -handle_out({pubrec, PacketId, ReasonCode}, Channel) -> - {ok, ?PUBREC_PACKET(PacketId, ReasonCode), Channel}; - -handle_out({pubcomp, PacketId}, Channel) -> - {ok, ?PUBCOMP_PACKET(PacketId), Channel}; -handle_out({pubcomp, PacketId, ReasonCode}, Channel) -> - {ok, ?PUBCOMP_PACKET(PacketId, ReasonCode), Channel}; - -handle_out({suback, PacketId, ReasonCodes}, Channel = #channel{proto_ver = ?MQTT_PROTO_V5}) -> - %% TODO: ACL Deny - {ok, ?SUBACK_PACKET(PacketId, ReasonCodes), Channel}; -handle_out({suback, PacketId, ReasonCodes}, Channel) -> - %% TODO: ACL Deny - ReasonCodes1 = [emqx_reason_codes:compat(suback, RC) || RC <- ReasonCodes], - {ok, ?SUBACK_PACKET(PacketId, ReasonCodes1), Channel}; - -handle_out({unsuback, PacketId, ReasonCodes}, Channel = #channel{proto_ver = ?MQTT_PROTO_V5}) -> - {ok, ?UNSUBACK_PACKET(PacketId, ReasonCodes), Channel}; -%% Ignore reason codes if not MQTT5 -handle_out({unsuback, PacketId, _ReasonCodes}, Channel) -> - {ok, ?UNSUBACK_PACKET(PacketId), Channel}; - -handle_out(Packet, State) -> - io:format("Out: ~p~n", [Packet]), - {ok, State}. - -handle_deliver(Msg, State) -> - io:format("Msg: ~p~n", [Msg]), - %% Msg -> Pub - {ok, State}. - -handle_timeout(Name, TRef, State) -> - io:format("Timeout: ~s ~p~n", [Name, TRef]), - {ok, State}. - -terminate(Reason, _State) -> - %%io:format("Terminated for ~p~n", [Reason]), - ok. +handle_outgoing(Packet, SuccFun, State = #state{serialize = Serialize}) -> + send(Serialize(Packet), SuccFun, State). %%-------------------------------------------------------------------- -%% Check Connect Packet -%%-------------------------------------------------------------------- +%% Serialize fun -check_connect(_ConnPkt, Channel) -> - {ok, Channel}. +serialize_fun(ProtoVer) -> + fun(Packet = ?PACKET(Type)) -> + ?LOG(debug, "SEND ~s", [emqx_packet:format(Packet)]), + _ = inc_outgoing_stats(Type), + emqx_frame:serialize(Packet, ProtoVer) + end. %%-------------------------------------------------------------------- -%% Handle Connect Packet -%%-------------------------------------------------------------------- +%% Send data -handle_connect(#mqtt_packet_connect{proto_name = ProtoName, - proto_ver = ProtoVer, - is_bridge = IsBridge, - clean_start = CleanStart, - keepalive = Keepalive, - properties = ConnProps, - client_id = ClientId, - username = Username, - password = Password} = ConnPkt, - Channel = #channel{endpoint = Endpoint}) -> - Credentials = emqx_endpoint:credentials(Endpoint), - case emqx_access_control:authenticate( - Credentials#{password => Password}) of - {ok, Credentials1} -> - Endpoint1 = emqx_endpoint:update(Credentials1, Endpoint), - %% Open session - case open_session(ConnPkt, Channel) of - {ok, Session, SP} -> - Channel1 = Channel#channel{endpoint = Endpoint1, - session = Session, - connected = true, - connected_at = os:timestamp() - }, - ok = emqx_cm:register_channel(ClientId), - {ok, SP, Channel1}; - {error, Error} -> - ?LOG(error, "Failed to open session: ~p", [Error]), - {error, ?RC_UNSPECIFIED_ERROR, Channel#channel{endpoint = Endpoint1}} - end; +send(IoData, SuccFun, State = #state{transport = Transport, + socket = Socket}) -> + Oct = iolist_size(IoData), + ok = emqx_metrics:inc('bytes.sent', Oct), + case Transport:async_send(Socket, IoData) of + ok -> SuccFun(maybe_gc(1, Oct, State)); {error, Reason} -> - ?LOG(warning, "Client ~s (Username: '~s') login failed for ~p", - [ClientId, Username, Reason]), - {error, emqx_reason_codes:connack_error(Reason), Channel} - end. - -open_session(#mqtt_packet_connect{clean_start = CleanStart, - %%properties = ConnProps, - client_id = ClientId, - username = Username} = ConnPkt, - Channel = #channel{endpoint = Endpoint}) -> - emqx_cm:open_session(maps:merge(emqx_endpoint:to_map(Endpoint), - #{clean_start => CleanStart, - max_inflight => 0, - expiry_interval => 0})). - -%%-------------------------------------------------------------------- -%% Handle Publish Message: Client -> Broker -%%-------------------------------------------------------------------- - -handle_publish(Packet = ?PUBLISH_PACKET(QoS, Topic, PacketId), - Channel = #channel{endpoint = Endpoint}) -> - Credentials = emqx_endpoint:credentials(Endpoint), - %% TODO: ugly... publish_to_msg(...) - Msg = emqx_packet:to_message(Credentials, Packet), - Msg1 = emqx_mountpoint:mount( - emqx_endpoint:mountpoint(Endpoint), Msg), - Msg2 = emqx_message:set_flag(dup, false, Msg1), - handle_publish(PacketId, Msg2, Channel). - -handle_publish(_PacketId, Msg = #message{qos = ?QOS_0}, Channel) -> - _ = emqx_broker:publish(Msg), - {ok, Channel}; - -handle_publish(PacketId, Msg = #message{qos = ?QOS_1}, Channel) -> - Results = emqx_broker:publish(Msg), - ReasonCode = emqx_reason_codes:puback(Results), - handle_out({puback, PacketId, ReasonCode}, Channel); - -handle_publish(PacketId, Msg = #message{qos = ?QOS_2}, - Channel = #channel{session = Session}) -> - case emqx_session:publish(PacketId, Msg, Session) of - {ok, Results, NSession} -> - ReasonCode = emqx_reason_codes:puback(Results), - handle_out({pubrec, PacketId, ReasonCode}, - Channel#channel{session = NSession}); - {error, ReasonCode} -> - handle_out({pubrec, PacketId, ReasonCode}, Channel) - end. - -%%-------------------------------------------------------------------- -%% Validate Incoming Packet -%%-------------------------------------------------------------------- - --spec(validate_packet(emqx_mqtt:packet(), channel()) -> ok). -validate_packet(Packet, _Channel) -> - try emqx_packet:validate(Packet) of - true -> ok - catch - error:protocol_error -> - {error, ?RC_PROTOCOL_ERROR}; - error:subscription_identifier_invalid -> - {error, ?RC_SUBSCRIPTION_IDENTIFIERS_NOT_SUPPORTED}; - error:topic_alias_invalid -> - {error, ?RC_TOPIC_ALIAS_INVALID}; - error:topic_filters_invalid -> - {error, ?RC_TOPIC_FILTER_INVALID}; - error:topic_name_invalid -> - {error, ?RC_TOPIC_FILTER_INVALID}; - error:_Reason -> - {error, ?RC_MALFORMED_PACKET} + shutdown(Reason, State) end. %%-------------------------------------------------------------------- -%% Preprocess MQTT Properties -%%-------------------------------------------------------------------- +%% Ensure keepalive -%% TODO:... +ensure_keepalive(0, _State) -> + ignore; +ensure_keepalive(Interval, #state{transport = Transport, + socket = Socket, + proto_state = ProtoState}) -> + StatFun = fun() -> + case Transport:getstat(Socket, [recv_oct]) of + {ok, [{recv_oct, RecvOct}]} -> + {ok, RecvOct}; + Error -> Error + end + end, + Backoff = emqx_zone:get_env(emqx_protocol:info(zone, ProtoState), + keepalive_backoff, 0.75), + emqx_keepalive:start(StatFun, round(Interval * Backoff), {keepalive, check}). %%-------------------------------------------------------------------- -%% Check Publish +%% Ensure rate limit + +ensure_rate_limit(State = #state{rate_limit = Rl, pub_limit = Pl}) -> + Limiters = [{Pl, #state.pub_limit, emqx_pd:reset_counter(incoming_pubs)}, + {Rl, #state.rate_limit, emqx_pd:reset_counter(incoming_bytes)}], + ensure_rate_limit(Limiters, State). + +ensure_rate_limit([], State) -> + State; +ensure_rate_limit([{undefined, _Pos, _Cnt}|Limiters], State) -> + ensure_rate_limit(Limiters, State); +ensure_rate_limit([{Rl, Pos, Cnt}|Limiters], State) -> + case esockd_rate_limit:check(Cnt, Rl) of + {0, Rl1} -> + ensure_rate_limit(Limiters, setelement(Pos, State, Rl1)); + {Pause, Rl1} -> + ?LOG(debug, "Rate limit pause connection ~pms", [Pause]), + TRef = erlang:send_after(Pause, self(), activate_socket), + setelement(Pos, State#state{conn_state = blocked, + limit_timer = TRef}, Rl1) + end. + %%-------------------------------------------------------------------- +%% Activate Socket -check_pub_caps(#mqtt_packet{header = #mqtt_packet_header{qos = QoS, - retain = Retain}, - variable = #mqtt_packet_publish{}}, - #channel{endpoint = Endpoint}) -> - emqx_mqtt_caps:check_pub(emqx_endpoint:zone(Endpoint), - #{qos => QoS, retain => Retain}). - -check_pub_acl(_Packet, #channel{enable_acl = false}) -> +activate_socket(#state{conn_state = blocked}) -> ok; -check_pub_acl(#mqtt_packet{variable = #mqtt_packet_publish{topic_name = Topic}}, - #channel{endpoint = Endpoint}) -> - case emqx_endpoint:is_superuser(Endpoint) of - true -> ok; - false -> - do_acl_check(Endpoint, publish, Topic) +activate_socket(#state{transport = Transport, + socket = Socket, + active_n = N}) -> + Transport:setopts(Socket, [{active, N}]). + +%%-------------------------------------------------------------------- +%% Inc incoming/outgoing stats + +-compile({inline, + [ inc_incoming_stats/1 + , inc_outgoing_stats/1 + ]}). + +inc_incoming_stats(Type) -> + emqx_pd:update_counter(recv_pkt, 1), + case Type == ?PUBLISH of + true -> + emqx_pd:update_counter(recv_msg, 1), + emqx_pd:update_counter(incoming_pubs, 1); + false -> ok end. -check_sub_acl(_Packet, #channel{enable_acl = false}) -> - ok. - -do_acl_check(Endpoint, PubSub, Topic) -> - case emqx_access_control:check_acl( - emqx_endpoint:to_map(Endpoint), PubSub, Topic) of - allow -> ok; - deny -> {error, ?RC_NOT_AUTHORIZED} - end. +inc_outgoing_stats(Type) -> + emqx_pd:update_counter(send_pkt, 1), + (Type == ?PUBLISH) + andalso emqx_pd:update_counter(send_msg, 1). %%-------------------------------------------------------------------- -%% Check Subscribe Packet -%%-------------------------------------------------------------------- +%% Ensure stats timer -check_subscribe(TopicFilters, _Channel) -> - {ok, TopicFilters}. +ensure_stats_timer(State = #state{stats_timer = undefined, + idle_timeout = IdleTimeout}) -> + TRef = emqx_misc:start_timer(IdleTimeout, emit_stats), + State#state{stats_timer = TRef}; +%% disabled or timer existed +ensure_stats_timer(State) -> State. %%-------------------------------------------------------------------- -%% Pipeline -%%-------------------------------------------------------------------- +%% Maybe GC -pipeline([Fun], Packet, Channel) -> - Fun(Packet, Channel); -pipeline([Fun|More], Packet, Channel) -> - case Fun(Packet, Channel) of - ok -> pipeline(More, Packet, Channel); - {ok, NChannel} -> - pipeline(More, Packet, NChannel); - {ok, NPacket, NChannel} -> - pipeline(More, NPacket, NChannel); - {error, Reason} -> - {error, Reason} - end. - -%%-------------------------------------------------------------------- -%% Preprocess topic filters -%%-------------------------------------------------------------------- - -preprocess_topic_filters(Type, Endpoint, TopicFilters) -> - TopicFilters1 = emqx_hooks:run_fold(case Type of - ?SUBSCRIBE -> 'client.subscribe'; - ?UNSUBSCRIBE -> 'client.unsubscribe' - end, - [emqx_endpoint:credentials(Endpoint)], - TopicFilters), - emqx_mountpoint:mount(emqx_endpoint:mountpoint(Endpoint), TopicFilters1). - -%%-------------------------------------------------------------------- -%% Enrich subopts -%%-------------------------------------------------------------------- - -enrich_subopts(TopicFilters, #channel{proto_ver = ?MQTT_PROTO_V5}) -> - TopicFilters; -enrich_subopts(TopicFilters, #channel{endpoint = Endpoint, is_bridge = IsBridge}) -> - Rap = flag(IsBridge), - Nl = flag(emqx_zone:get_env(emqx_endpoint:zone(Endpoint), ignore_loop_deliver, false)), - [{Topic, SubOpts#{rap => Rap, nl => Nl}} || {Topic, SubOpts} <- TopicFilters]. - -%%-------------------------------------------------------------------- -%% Parse topic filters -%%-------------------------------------------------------------------- - -parse_topic_filters(?SUBSCRIBE, TopicFilters) -> - [emqx_topic:parse(Topic, SubOpts) || {Topic, SubOpts} <- TopicFilters]; - -parse_topic_filters(?UNSUBSCRIBE, TopicFilters) -> - lists:map(fun emqx_topic:parse/1, TopicFilters). +maybe_gc(_Cnt, _Oct, State = #state{gc_state = undefined}) -> + State; +maybe_gc(Cnt, Oct, State = #state{gc_state = GCSt}) -> + {Ok, GCSt1} = emqx_gc:run(Cnt, Oct, GCSt), + Ok andalso emqx_metrics:inc('channel.gc.cnt'), + State#state{gc_state = GCSt1}. %%-------------------------------------------------------------------- %% Helper functions -%%-------------------------------------------------------------------- -sp(true) -> 1; -sp(false) -> 0. +-compile({inline, + [ reply/3 + , keep_state/1 + , next_event/2 + , shutdown/2 + , stop/2 + ]}). -flag(true) -> 1; -flag(false) -> 0. +reply(From, Reply, State) -> + {keep_state, State, [{reply, From, Reply}]}. + +keep_state(State) -> + {keep_state, State}. + +next_event(Type, Content) -> + {next_event, Type, Content}. + +shutdown(Reason, State) -> + stop({shutdown, Reason}, State). + +stop(Reason, State) -> + {stop, Reason, State}. diff --git a/src/emqx_client.erl b/src/emqx_client.erl index 41ff2f72e..7e4120628 100644 --- a/src/emqx_client.erl +++ b/src/emqx_client.erl @@ -160,7 +160,7 @@ clean_start :: boolean(), username :: maybe(binary()), password :: maybe(binary()), - proto_ver :: emqx_mqtt_types:version(), + proto_ver :: emqx_types:mqtt_ver(), proto_name :: iodata(), keepalive :: non_neg_integer(), keepalive_timer :: maybe(reference()), @@ -192,11 +192,11 @@ -type(payload() :: iodata()). --type(packet_id() :: emqx_mqtt_types:packet_id()). +-type(packet_id() :: emqx_types:packet_id()). --type(properties() :: emqx_mqtt_types:properties()). +-type(properties() :: emqx_types:properties()). --type(qos() :: emqx_mqtt_types:qos_name() | emqx_mqtt_types:qos()). +-type(qos() :: emqx_types:qos_name() | emqx_types:qos()). -type(pubopt() :: {retain, boolean()} | {qos, qos()} | {timeout, timeout()}). diff --git a/src/emqx_cm.erl b/src/emqx_cm.erl index 7f230e841..e756a37e5 100644 --- a/src/emqx_cm.erl +++ b/src/emqx_cm.erl @@ -34,12 +34,12 @@ -export([ get_conn_attrs/1 , get_conn_attrs/2 - , set_conn_attrs/2 + , set_chan_attrs/2 ]). -export([ get_conn_stats/1 , get_conn_stats/2 - , set_conn_stats/2 + , set_chan_stats/2 ]). -export([ open_session/1 @@ -163,8 +163,8 @@ get_conn_attrs(ClientId, ChanPid) -> rpc_call(node(ChanPid), get_conn_attrs, [ClientId, ChanPid]). %% @doc Set conn attrs. --spec(set_conn_attrs(emqx_types:client_id(), attrs()) -> ok). -set_conn_attrs(ClientId, Attrs) when is_binary(ClientId), is_map(Attrs) -> +-spec(set_chan_attrs(emqx_types:client_id(), attrs()) -> ok). +set_chan_attrs(ClientId, Attrs) when is_binary(ClientId) -> Chan = {ClientId, self()}, case ets:update_element(?CONN_TAB, Chan, {2, Attrs}) of true -> ok; @@ -191,12 +191,12 @@ get_conn_stats(ClientId, ChanPid) -> rpc_call(node(ChanPid), get_conn_stats, [ClientId, ChanPid]). %% @doc Set conn stats. --spec(set_conn_stats(emqx_types:client_id(), stats()) -> ok). -set_conn_stats(ClientId, Stats) when is_binary(ClientId) -> - set_conn_stats(ClientId, self(), Stats). +-spec(set_chan_stats(emqx_types:client_id(), stats()) -> ok). +set_chan_stats(ClientId, Stats) when is_binary(ClientId) -> + set_chan_stats(ClientId, self(), Stats). --spec(set_conn_stats(emqx_types:client_id(), chan_pid(), stats()) -> ok). -set_conn_stats(ClientId, ChanPid, Stats) -> +-spec(set_chan_stats(emqx_types:client_id(), chan_pid(), stats()) -> ok). +set_chan_stats(ClientId, ChanPid, Stats) -> Chan = {ClientId, ChanPid}, _ = ets:update_element(?CONN_TAB, Chan, {3, Stats}), ok. @@ -208,7 +208,7 @@ open_session(Attrs = #{clean_start := true, client_id := ClientId}) -> CleanStart = fun(_) -> ok = discard_session(ClientId), - {ok, emqx_session:new(Attrs), false} + {ok, emqx_session:init(Attrs), false} end, emqx_cm_locker:trans(ClientId, CleanStart); @@ -219,7 +219,7 @@ open_session(Attrs = #{clean_start := false, {ok, Session} -> {ok, Session, true}; {error, not_found} -> - {ok, emqx_session:new(Attrs), false} + {ok, emqx_session:init(Attrs), false} end end, emqx_cm_locker:trans(ClientId, ResumeStart). diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl deleted file mode 100644 index b7478e445..000000000 --- a/src/emqx_connection.erl +++ /dev/null @@ -1,586 +0,0 @@ -%%-------------------------------------------------------------------- -%% 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. -%%-------------------------------------------------------------------- - -%% MQTT TCP/SSL Connection --module(emqx_connection). - --behaviour(gen_statem). - --include("emqx.hrl"). --include("emqx_mqtt.hrl"). --include("logger.hrl"). --include("types.hrl"). - --logger_header("[Conn]"). - --export([start_link/3]). - -%% APIs --export([ info/1 - , attrs/1 - , stats/1 - ]). - -%% gen_statem callbacks --export([ idle/3 - , connected/3 - , disconnected/3 - ]). - --export([ init/1 - , callback_mode/0 - , code_change/4 - , terminate/3 - ]). - --record(state, { - transport :: esockd:transport(), - socket :: esockd:socket(), - peername :: emqx_types:peername(), - sockname :: emqx_types:peername(), - conn_state :: running | blocked, - active_n :: pos_integer(), - rate_limit :: maybe(esockd_rate_limit:bucket()), - pub_limit :: maybe(esockd_rate_limit:bucket()), - limit_timer :: maybe(reference()), - parse_state :: emqx_frame:parse_state(), - chan_state :: emqx_channel:channel(), - gc_state :: emqx_gc:gc_state(), - keepalive :: maybe(emqx_keepalive:keepalive()), - stats_timer :: disabled | maybe(reference()), - idle_timeout :: timeout() - }). - --define(ACTIVE_N, 100). --define(HANDLE(T, C, D), handle((T), (C), (D))). --define(CHAN_STATS, [recv_pkt, recv_msg, send_pkt, send_msg]). --define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt, send_pend]). - --spec(start_link(esockd:transport(), esockd:socket(), proplists:proplist()) - -> {ok, pid()}). -start_link(Transport, Socket, Options) -> - {ok, proc_lib:spawn_link(?MODULE, init, [{Transport, Socket, Options}])}. - -%%-------------------------------------------------------------------- -%% API -%%-------------------------------------------------------------------- - -%% For debug --spec(info(pid() | #state{}) -> map()). -info(CPid) when is_pid(CPid) -> - call(CPid, info); - -info(#state{transport = Transport, - socket = Socket, - peername = Peername, - sockname = Sockname, - conn_state = ConnState, - active_n = ActiveN, - rate_limit = RateLimit, - pub_limit = PubLimit, - chan_state = ChanState}) -> - ConnInfo = #{socktype => Transport:type(Socket), - peername => Peername, - sockname => Sockname, - conn_state => ConnState, - active_n => ActiveN, - rate_limit => rate_limit_info(RateLimit), - pub_limit => rate_limit_info(PubLimit) - }, - ChanInfo = emqx_channel:info(ChanState), - maps:merge(ConnInfo, ChanInfo). - -rate_limit_info(undefined) -> - undefined; -rate_limit_info(Limit) -> - esockd_rate_limit:info(Limit). - -%% For dashboard -attrs(CPid) when is_pid(CPid) -> - call(CPid, attrs); - -attrs(#state{peername = Peername, - sockname = Sockname, - conn_state = ConnState, - chan_state = ChanState}) -> - SockAttrs = #{peername => Peername, - sockname => Sockname, - conn_state => ConnState - }, - ChanAttrs = emqx_channel:attrs(ChanState), - maps:merge(SockAttrs, ChanAttrs). - -%% @doc Get connection stats -stats(CPid) when is_pid(CPid) -> - call(CPid, stats); - -stats(#state{transport = Transport, socket = Socket}) -> - SockStats = case Transport:getstat(Socket, ?SOCK_STATS) of - {ok, Ss} -> Ss; - {error, _} -> [] - end, - ChanStats = [{Name, emqx_pd:get_counter(Name)} || Name <- ?CHAN_STATS], - lists:append([SockStats, ChanStats, emqx_misc:proc_stats()]). - -call(CPid, Req) -> - gen_statem:call(CPid, Req, infinity). - -%%-------------------------------------------------------------------- -%% gen_statem callbacks -%%-------------------------------------------------------------------- - -init({Transport, RawSocket, Options}) -> - {ok, Socket} = Transport:wait(RawSocket), - {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]), - emqx_logger:set_metadata_peername(esockd_net:format(Peername)), - Zone = proplists:get_value(zone, Options), - RateLimit = init_limiter(proplists:get_value(rate_limit, Options)), - PubLimit = init_limiter(emqx_zone:get_env(Zone, publish_limit)), - ActiveN = proplists:get_value(active_n, Options, ?ACTIVE_N), - MaxSize = emqx_zone:get_env(Zone, max_packet_size, ?MAX_PACKET_SIZE), - ParseState = emqx_frame:initial_parse_state(#{max_size => MaxSize}), - ChanState = emqx_channel:init(#{peername => Peername, - sockname => Sockname, - peercert => Peercert, - conn_mod => ?MODULE}, Options), - GcPolicy = emqx_zone:get_env(Zone, force_gc_policy, false), - GcState = emqx_gc:init(GcPolicy), - EnableStats = emqx_zone:get_env(Zone, enable_stats, true), - StatsTimer = if EnableStats -> undefined; ?Otherwise-> disabled end, - IdleTimout = emqx_zone:get_env(Zone, idle_timeout, 30000), - ok = emqx_misc:init_proc_mng_policy(Zone), - State = #state{transport = Transport, - socket = Socket, - peername = Peername, - conn_state = running, - active_n = ActiveN, - rate_limit = RateLimit, - pub_limit = PubLimit, - parse_state = ParseState, - chan_state = ChanState, - gc_state = GcState, - stats_timer = StatsTimer, - idle_timeout = IdleTimout - }, - gen_statem:enter_loop(?MODULE, [{hibernate_after, 2 * IdleTimout}], - idle, State, self(), [IdleTimout]). - -init_limiter(undefined) -> - undefined; -init_limiter({Rate, Burst}) -> - esockd_rate_limit:new(Rate, Burst). - -callback_mode() -> - [state_functions, state_enter]. - -%%-------------------------------------------------------------------- -%% Idle State - -idle(enter, _, State) -> - case activate_socket(State) of - ok -> keep_state_and_data; - {error, Reason} -> - shutdown(Reason, State) - end; - -idle(timeout, _Timeout, State) -> - stop(idle_timeout, State); - -idle(cast, {incoming, Packet = ?CONNECT_PACKET(ConnVar)}, State) -> - handle_incoming(Packet, - fun(St = #state{chan_state = ChanState}) -> - %% Ensure keepalive after connected successfully. - Interval = emqx_channel:keepalive(ChanState), - NextEvent = {next_event, info, {keepalive, start, Interval}}, - {next_state, connected, St, NextEvent} - end, State); - -idle(cast, {incoming, Packet}, State) -> - ?LOG(warning, "Unexpected incoming: ~p", [Packet]), - shutdown(unexpected_incoming_packet, State); - -idle(EventType, Content, State) -> - ?HANDLE(EventType, Content, State). - -%%-------------------------------------------------------------------- -%% Connected State - -connected(enter, _, _State) -> - %% What to do? - keep_state_and_data; - -connected(cast, {incoming, Packet = ?PACKET(?CONNECT)}, State) -> - ?LOG(warning, "Unexpected connect: ~p", [Packet]), - shutdown(unexpected_incoming_connect, State); - -connected(cast, {incoming, Packet = ?PACKET(Type)}, State) -> - handle_incoming(Packet, fun keep_state/1, State); - -connected(info, Deliver = {deliver, _Topic, _Msg}, - State = #state{chan_state = ChanState}) -> - Delivers = emqx_misc:drain_deliver([Deliver]), - case emqx_channel:handle_out(Delivers, ChanState) of - {ok, NChanState} -> - keep_state(State#state{chan_state = NChanState}); - {ok, Packets, NChanState} -> - NState = State#state{chan_state = NChanState}, - handle_outgoing(Packets, fun keep_state/1, NState); - {error, Reason} -> - shutdown(Reason, State) - end; - -%% Start Keepalive -connected(info, {keepalive, start, Interval}, State) -> - case ensure_keepalive(Interval, State) of - ignore -> keep_state(State); - {ok, KeepAlive} -> - keep_state(State#state{keepalive = KeepAlive}); - {error, Reason} -> - shutdown(Reason, State) - end; - -%% Keepalive timer -connected(info, {keepalive, check}, State = #state{keepalive = KeepAlive}) -> - case emqx_keepalive:check(KeepAlive) of - {ok, KeepAlive1} -> - keep_state(State#state{keepalive = KeepAlive1}); - {error, timeout} -> - shutdown(keepalive_timeout, State); - {error, Reason} -> - shutdown(Reason, State) - end; - -connected(EventType, Content, State) -> - ?HANDLE(EventType, Content, State). - -%%-------------------------------------------------------------------- -%% Disconnected State - -disconnected(enter, _, _State) -> - %% TODO: What to do? - keep_state_and_data; - -disconnected(EventType, Content, State) -> - ?HANDLE(EventType, Content, State). - -%% Handle call -handle({call, From}, info, State) -> - reply(From, info(State), State); - -handle({call, From}, attrs, State) -> - reply(From, attrs(State), State); - -handle({call, From}, stats, State) -> - reply(From, stats(State), State); - -%%handle({call, From}, kick, State) -> -%% ok = gen_statem:reply(From, ok), -%% shutdown(kicked, State); - -%%handle({call, From}, discard, State) -> -%% ok = gen_statem:reply(From, ok), -%% shutdown(discard, State); - -handle({call, From}, Req, State) -> - ?LOG(error, "Unexpected call: ~p", [Req]), - reply(From, ignored, State); - -%% Handle cast -handle(cast, Msg, State) -> - ?LOG(error, "Unexpected cast: ~p", [Msg]), - keep_state(State); - -%% Handle Incoming -handle(info, {Inet, _Sock, Data}, State) when Inet == tcp; - Inet == ssl -> - ?LOG(debug, "RECV ~p", [Data]), - Oct = iolist_size(Data), - emqx_pd:update_counter(incoming_bytes, Oct), - ok = emqx_metrics:inc('bytes.received', Oct), - NState = ensure_stats_timer(maybe_gc(1, Oct, State)), - process_incoming(Data, [], NState); - -handle(info, {Error, _Sock, Reason}, State) - when Error == tcp_error; Error == ssl_error -> - shutdown(Reason, State); - -handle(info, {Closed, _Sock}, State) - when Closed == tcp_closed; Closed == ssl_closed -> - shutdown(closed, State); - -handle(info, {Passive, _Sock}, State) when Passive == tcp_passive; - Passive == ssl_passive -> - %% Rate limit here:) - NState = ensure_rate_limit(State), - case activate_socket(NState) of - ok -> keep_state(NState); - {error, Reason} -> - shutdown(Reason, NState) - end; - -handle(info, activate_socket, State) -> - %% Rate limit timer expired. - NState = State#state{conn_state = running}, - case activate_socket(NState) of - ok -> - keep_state(NState#state{limit_timer = undefined}); - {error, Reason} -> - shutdown(Reason, NState) - end; - -handle(info, {inet_reply, _Sock, ok}, State) -> - %% something sent - keep_state(ensure_stats_timer(State)); - -handle(info, {inet_reply, _Sock, {error, Reason}}, State) -> - shutdown(Reason, State); - -handle(info, {timeout, Timer, emit_stats}, - State = #state{stats_timer = Timer, - chan_state = ChanState, - gc_state = GcState}) -> - ClientId = emqx_channel:client_id(ChanState), - ok = emqx_cm:set_conn_stats(ClientId, stats(State)), - NState = State#state{stats_timer = undefined}, - Limits = erlang:get(force_shutdown_policy), - case emqx_misc:conn_proc_mng_policy(Limits) of - continue -> - keep_state(NState); - hibernate -> - %% going to hibernate, reset gc stats - GcState1 = emqx_gc:reset(GcState), - {keep_state, NState#state{gc_state = GcState1}, hibernate}; - {shutdown, Reason} -> - ?LOG(error, "Shutdown exceptionally due to ~p", [Reason]), - shutdown(Reason, NState) - end; - -handle(info, {shutdown, discard, {ClientId, ByPid}}, State) -> - ?LOG(error, "Discarded by ~s:~p", [ClientId, ByPid]), - shutdown(discard, State); - -handle(info, {shutdown, conflict, {ClientId, NewPid}}, State) -> - ?LOG(warning, "Clientid '~s' conflict with ~p", [ClientId, NewPid]), - shutdown(conflict, State); - -handle(info, {shutdown, Reason}, State) -> - shutdown(Reason, State); - -handle(info, Info, State) -> - ?LOG(error, "Unexpected info: ~p", [Info]), - keep_state(State). - -code_change(_Vsn, State, Data, _Extra) -> - {ok, State, Data}. - -terminate(Reason, _StateName, #state{transport = Transport, - socket = Socket, - keepalive = KeepAlive, - chan_state = ChanState}) -> - ?LOG(debug, "Terminated for ~p", [Reason]), - ok = Transport:fast_close(Socket), - ok = emqx_keepalive:cancel(KeepAlive), - emqx_channel:terminate(Reason, ChanState). - -%%-------------------------------------------------------------------- -%% Process incoming data - -process_incoming(<<>>, Packets, State) -> - {keep_state, State, next_events(Packets)}; - -process_incoming(Data, Packets, State = #state{parse_state = ParseState}) -> - try emqx_frame:parse(Data, ParseState) of - {ok, NParseState} -> - NState = State#state{parse_state = NParseState}, - {keep_state, NState, next_events(Packets)}; - {ok, Packet, Rest, NParseState} -> - NState = State#state{parse_state = NParseState}, - process_incoming(Rest, [Packet|Packets], NState); - {error, Reason} -> - shutdown(Reason, State) - catch - error:Reason:Stk -> - ?LOG(error, "Parse failed for ~p~n\ - Stacktrace:~p~nError data:~p", [Reason, Stk, Data]), - shutdown(parse_error, State) - end. - -next_events(Packets) when is_list(Packets) -> - [next_events(Packet) || Packet <- lists:reverse(Packets)]; -next_events(Packet) -> - {next_event, cast, {incoming, Packet}}. - -%%-------------------------------------------------------------------- -%% Handle incoming packet - -handle_incoming(Packet = ?PACKET(Type), SuccFun, - State = #state{chan_state = ChanState}) -> - _ = inc_incoming_stats(Type), - ok = emqx_metrics:inc_recv(Packet), - ?LOG(debug, "RECV ~s", [emqx_packet:format(Packet)]), - case emqx_channel:handle_in(Packet, ChanState) of - {ok, NChanState} -> - SuccFun(State#state{chan_state = NChanState}); - {ok, OutPacket, NChanState} -> - handle_outgoing(OutPacket, SuccFun, - State#state{chan_state = NChanState}); - {error, Reason, NChanState} -> - shutdown(Reason, State#state{chan_state = NChanState}); - {stop, Error, NChanState} -> - stop(Error, State#state{chan_state = NChanState}) - end. - -%%-------------------------------------------------------------------- -%% Handle outgoing packets - -handle_outgoing(Packets, SuccFun, State = #state{chan_state = ChanState}) - when is_list(Packets) -> - ProtoVer = emqx_channel:proto_ver(ChanState), - IoData = lists:foldl( - fun(Packet = ?PACKET(Type), Acc) -> - ?LOG(debug, "SEND ~s", [emqx_packet:format(Packet)]), - _ = inc_outgoing_stats(Type), - [emqx_frame:serialize(Packet, ProtoVer)|Acc] - end, [], Packets), - send(lists:reverse(IoData), SuccFun, State); - -handle_outgoing(Packet = ?PACKET(Type), SuccFun, State = #state{chan_state = ChanState}) -> - ?LOG(debug, "SEND ~s", [emqx_packet:format(Packet)]), - _ = inc_outgoing_stats(Type), - ProtoVer = emqx_channel:proto_ver(ChanState), - IoData = emqx_frame:serialize(Packet, ProtoVer), - send(IoData, SuccFun, State). - -%%-------------------------------------------------------------------- -%% Send data - -send(IoData, SuccFun, State = #state{transport = Transport, socket = Socket}) -> - Oct = iolist_size(IoData), - ok = emqx_metrics:inc('bytes.sent', Oct), - case Transport:async_send(Socket, IoData) of - ok -> SuccFun(maybe_gc(1, Oct, State)); - {error, Reason} -> - shutdown(Reason, State) - end. - -%%-------------------------------------------------------------------- -%% Ensure keepalive - -ensure_keepalive(0, State) -> - ignore; -ensure_keepalive(Interval, State = #state{transport = Transport, - socket = Socket, - chan_state = ChanState}) -> - StatFun = fun() -> - case Transport:getstat(Socket, [recv_oct]) of - {ok, [{recv_oct, RecvOct}]} -> - {ok, RecvOct}; - Error -> Error - end - end, - Backoff = emqx_zone:get_env(emqx_channel:zone(ChanState), - keepalive_backoff, 0.75), - emqx_keepalive:start(StatFun, round(Interval * Backoff), {keepalive, check}). - -%%-------------------------------------------------------------------- -%% Ensure rate limit - -ensure_rate_limit(State = #state{rate_limit = Rl, pub_limit = Pl}) -> - Limiters = [{Pl, #state.pub_limit, emqx_pd:reset_counter(incoming_pubs)}, - {Rl, #state.rate_limit, emqx_pd:reset_counter(incoming_bytes)}], - ensure_rate_limit(Limiters, State). - -ensure_rate_limit([], State) -> - State; -ensure_rate_limit([{undefined, _Pos, _Cnt}|Limiters], State) -> - ensure_rate_limit(Limiters, State); -ensure_rate_limit([{Rl, Pos, Cnt}|Limiters], State) -> - case esockd_rate_limit:check(Cnt, Rl) of - {0, Rl1} -> - ensure_rate_limit(Limiters, setelement(Pos, State, Rl1)); - {Pause, Rl1} -> - ?LOG(debug, "Rate limit pause connection ~pms", [Pause]), - TRef = erlang:send_after(Pause, self(), activate_socket), - setelement(Pos, State#state{conn_state = blocked, limit_timer = TRef}, Rl1) - end. - -%%-------------------------------------------------------------------- -%% Activate Socket - -activate_socket(#state{conn_state = blocked}) -> - ok; -activate_socket(#state{transport = Transport, - socket = Socket, - active_n = N}) -> - Transport:setopts(Socket, [{active, N}]). - -%%-------------------------------------------------------------------- -%% Inc incoming/outgoing stats - -inc_incoming_stats(Type) -> - emqx_pd:update_counter(recv_pkt, 1), - case Type == ?PUBLISH of - true -> - emqx_pd:update_counter(recv_msg, 1), - emqx_pd:update_counter(incoming_pubs, 1); - false -> ok - end. - -inc_outgoing_stats(Type) -> - emqx_pd:update_counter(send_pkt, 1), - (Type == ?PUBLISH) - andalso emqx_pd:update_counter(send_msg, 1). - -%%-------------------------------------------------------------------- -%% Ensure stats timer - -ensure_stats_timer(State = #state{stats_timer = undefined, - idle_timeout = IdleTimeout}) -> - State#state{stats_timer = emqx_misc:start_timer(IdleTimeout, emit_stats)}; -%% disabled or timer existed -ensure_stats_timer(State) -> State. - -%%-------------------------------------------------------------------- -%% Maybe GC - -maybe_gc(_Cnt, _Oct, State = #state{gc_state = undefined}) -> - State; -maybe_gc(Cnt, Oct, State = #state{gc_state = GCSt}) -> - {_, GCSt1} = emqx_gc:run(Cnt, Oct, GCSt), - %% TODO: gc metric? - State#state{gc_state = GCSt1}. - -%%-------------------------------------------------------------------- -%% Helper functions - --compile({inline, [reply/3]}). -reply(From, Reply, State) -> - {keep_state, State, [{reply, From, Reply}]}. - --compile({inline, [keep_state/1]}). -keep_state(State) -> - {keep_state, State}. - --compile({inline, [shutdown/2]}). -shutdown(Reason, State) -> - stop({shutdown, Reason}, State). - --compile({inline, [stop/2]}). -stop(Reason, State) -> - {stop, Reason, State}. - diff --git a/src/emqx_endpoint.erl b/src/emqx_endpoint.erl index 1529698aa..dee80a099 100644 --- a/src/emqx_endpoint.erl +++ b/src/emqx_endpoint.erl @@ -21,6 +21,7 @@ %% APIs -export([ new/0 , new/1 + , info/1 ]). -export([ zone/1 @@ -36,25 +37,25 @@ -export_type([endpoint/0]). --opaque(endpoint() :: - {endpoint, - #{zone := emqx_types:zone(), - peername := emqx_types:peername(), - sockname => emqx_types:peername(), - client_id := emqx_types:client_id(), - username := emqx_types:username(), - peercert := esockd_peercert:peercert(), - is_superuser := boolean(), - mountpoint := maybe(binary()), - ws_cookie := maybe(list()), - password => binary(), - auth_result => emqx_types:auth_result(), - anonymous => boolean(), - atom() => term() - } - }). +-type(st() :: #{zone := emqx_types:zone(), + conn_mod := maybe(module()), + peername := emqx_types:peername(), + sockname := emqx_types:peername(), + client_id := emqx_types:client_id(), + username := emqx_types:username(), + peercert := esockd_peercert:peercert(), + is_superuser := boolean(), + mountpoint := maybe(binary()), + ws_cookie := maybe(list()), + password => binary(), + auth_result => emqx_types:auth_result(), + anonymous => boolean(), + atom() => term() + }). --define(Endpoint(M), {endpoint, M}). +-opaque(endpoint() :: {endpoint, st()}). + +-define(Endpoint(St), {endpoint, St}). -define(Default, #{is_superuser => false, anonymous => false @@ -68,6 +69,9 @@ new() -> new(M) when is_map(M) -> ?Endpoint(maps:merge(?Default, M)). +info(?Endpoint(M)) -> + maps:to_list(M). + -spec(zone(endpoint()) -> emqx_zone:zone()). zone(?Endpoint(#{zone := Zone})) -> Zone. diff --git a/src/emqx_frame.erl b/src/emqx_frame.erl index 3a18ab297..c0115cea8 100644 --- a/src/emqx_frame.erl +++ b/src/emqx_frame.erl @@ -35,13 +35,13 @@ ]). -type(options() :: #{max_size => 1..?MAX_PACKET_SIZE, - version => emqx_mqtt:version() + version => emqx_types:version() }). -opaque(parse_state() :: {none, options()} | {more, cont_fun()}). -opaque(parse_result() :: {ok, parse_state()} - | {ok, emqx_mqtt:packet(), binary(), parse_state()}). + | {ok, emqx_types:packet(), binary(), parse_state()}). -type(cont_fun() :: fun((binary()) -> parse_result())). @@ -385,11 +385,11 @@ parse_binary_data(<>) -> %% Serialize MQTT Packet %%-------------------------------------------------------------------- --spec(serialize(emqx_mqtt:packet()) -> iodata()). +-spec(serialize(emqx_types:packet()) -> iodata()). serialize(Packet) -> serialize(Packet, ?MQTT_PROTO_V4). --spec(serialize(emqx_mqtt:packet(), emqx_mqtt:version()) -> iodata()). +-spec(serialize(emqx_types:packet(), emqx_types:version()) -> iodata()). serialize(#mqtt_packet{header = Header, variable = Variable, payload = Payload}, Ver) -> diff --git a/src/emqx_gc.erl b/src/emqx_gc.erl index 83264ecee..974d5b48e 100644 --- a/src/emqx_gc.erl +++ b/src/emqx_gc.erl @@ -15,8 +15,9 @@ %%-------------------------------------------------------------------- %%-------------------------------------------------------------------- -%% @doc This module manages an opaque collection of statistics data used -%% to force garbage collection on `self()' process when hitting thresholds. +%% @doc +%% This module manages an opaque collection of statistics data used to +%% force garbage collection on `self()' process when hitting thresholds. %% Namely: %% (1) Total number of messages passed through %% (2) Total data volume passed through @@ -41,9 +42,9 @@ -type(st() :: #{cnt => {integer(), integer()}, oct => {integer(), integer()}}). --opaque(gc_state() :: {?MODULE, st()}). +-opaque(gc_state() :: {gc_state, st()}). --define(GCS(St), {?MODULE, St}). +-define(GCS(St), {gc_state, St}). -define(disabled, disabled). -define(ENABLED(X), (is_integer(X) andalso X > 0)). diff --git a/src/emqx_inflight.erl b/src/emqx_inflight.erl index 88c4796c4..1bf70f4c8 100644 --- a/src/emqx_inflight.erl +++ b/src/emqx_inflight.erl @@ -22,7 +22,7 @@ , lookup/2 , insert/3 , update/3 - , update_size/2 + , resize/2 , delete/2 , values/1 , to_list/1 @@ -39,11 +39,11 @@ -type(max_size() :: pos_integer()). --opaque(inflight() :: {?MODULE, max_size(), gb_trees:tree()}). +-opaque(inflight() :: {inflight, max_size(), gb_trees:tree()}). --define(Inflight(Tree), {?MODULE, _MaxSize, Tree}). +-define(Inflight(Tree), {inflight, _MaxSize, Tree}). --define(Inflight(MaxSize, Tree), {?MODULE, MaxSize, (Tree)}). +-define(Inflight(MaxSize, Tree), {inflight, MaxSize, (Tree)}). %%-------------------------------------------------------------------- %% APIs @@ -73,8 +73,8 @@ delete(Key, ?Inflight(MaxSize, Tree)) -> update(Key, Val, ?Inflight(MaxSize, Tree)) -> ?Inflight(MaxSize, gb_trees:update(Key, Val, Tree)). --spec(update_size(integer(), inflight()) -> inflight()). -update_size(MaxSize, ?Inflight(Tree)) -> +-spec(resize(integer(), inflight()) -> inflight()). +resize(MaxSize, ?Inflight(Tree)) -> ?Inflight(MaxSize, Tree). -spec(is_full(inflight()) -> boolean()). diff --git a/src/emqx_listeners.erl b/src/emqx_listeners.erl index 94babe7fd..b39873879 100644 --- a/src/emqx_listeners.erl +++ b/src/emqx_listeners.erl @@ -46,13 +46,15 @@ start() -> -spec(start_listener(listener()) -> {ok, pid()} | {error, term()}). start_listener({Proto, ListenOn, Options}) -> - case start_listener(Proto, ListenOn, Options) of - {ok, _} -> - io:format("Start mqtt:~s listener on ~s successfully.~n", [Proto, format(ListenOn)]); + StartRet = start_listener(Proto, ListenOn, Options), + case StartRet of + {ok, _} -> io:format("Start mqtt:~s listener on ~s successfully.~n", + [Proto, format(ListenOn)]); {error, Reason} -> io:format(standard_error, "Failed to start mqtt:~s listener on ~s - ~p~n!", [Proto, format(ListenOn), Reason]) - end. + end, + StartRet. %% Start MQTT/TCP listener -spec(start_listener(esockd:proto(), esockd:listen_on(), [esockd:option()]) @@ -66,16 +68,18 @@ start_listener(Proto, ListenOn, Options) when Proto == ssl; Proto == tls -> %% Start MQTT/WS listener start_listener(Proto, ListenOn, Options) when Proto == http; Proto == ws -> - start_http_listener(fun cowboy:start_clear/3, 'mqtt:ws', ListenOn, ranch_opts(Options), ws_opts(Options)); + start_http_listener(fun cowboy:start_clear/3, 'mqtt:ws', ListenOn, + ranch_opts(Options), ws_opts(Options)); %% Start MQTT/WSS listener start_listener(Proto, ListenOn, Options) when Proto == https; Proto == wss -> - start_http_listener(fun cowboy:start_tls/3, 'mqtt:wss', ListenOn, ranch_opts(Options), ws_opts(Options)). + start_http_listener(fun cowboy:start_tls/3, 'mqtt:wss', ListenOn, + ranch_opts(Options), ws_opts(Options)). start_mqtt_listener(Name, ListenOn, Options) -> SockOpts = esockd:parse_opt(Options), esockd:open(Name, ListenOn, merge_default(SockOpts), - {emqx_connection, start_link, [Options -- SockOpts]}). + {emqx_channel, start_link, [Options -- SockOpts]}). start_http_listener(Start, Name, ListenOn, RanchOpts, ProtoOpts) -> Start(Name, with_port(ListenOn, RanchOpts), ProtoOpts). @@ -84,8 +88,10 @@ mqtt_path(Options) -> proplists:get_value(mqtt_path, Options, "/mqtt"). ws_opts(Options) -> - Dispatch = cowboy_router:compile([{'_', [{mqtt_path(Options), emqx_ws_connection, Options}]}]), - #{env => #{dispatch => Dispatch}, proxy_header => proplists:get_value(proxy_protocol, Options, false)}. + WsPaths = [{mqtt_path(Options), emqx_ws_channel, Options}], + Dispatch = cowboy_router:compile([{'_', WsPaths}]), + ProxyProto = proplists:get_value(proxy_protocol, Options, false), + #{env => #{dispatch => Dispatch}, proxy_header => ProxyProto}. ranch_opts(Options) -> NumAcceptors = proplists:get_value(acceptors, Options, 4), @@ -134,13 +140,15 @@ stop() -> -spec(stop_listener(listener()) -> ok | {error, term()}). stop_listener({Proto, ListenOn, Opts}) -> - case stop_listener(Proto, ListenOn, Opts) of - ok -> - io:format("Stop mqtt:~s listener on ~s successfully.~n", [Proto, format(ListenOn)]); + StopRet = stop_listener(Proto, ListenOn, Opts), + case StopRet of + ok -> io:format("Stop mqtt:~s listener on ~s successfully.~n", + [Proto, format(ListenOn)]); {error, Reason} -> io:format(standard_error, "Failed to stop mqtt:~s listener on ~s - ~p~n.", [Proto, format(ListenOn), Reason]) - end. + end, + StopRet. -spec(stop_listener(esockd:proto(), esockd:listen_on(), [esockd:option()]) -> ok | {error, term()}). diff --git a/src/emqx_message.erl b/src/emqx_message.erl index a00928af8..8617c8608 100644 --- a/src/emqx_message.erl +++ b/src/emqx_message.erl @@ -76,7 +76,7 @@ make(From, Topic, Payload) -> make(From, ?QOS_0, Topic, Payload). -spec(make(atom() | emqx_types:client_id(), - emqx_mqtt_types:qos(), + emqx_types:qos(), emqx_topic:topic(), emqx_types:payload()) -> emqx_types:message()). make(From, QoS, Topic, Payload) when ?QOS_0 =< QoS, QoS =< ?QOS_2 -> @@ -91,7 +91,7 @@ make(From, QoS, Topic, Payload) when ?QOS_0 =< QoS, QoS =< ?QOS_2 -> -spec(id(emqx_types:message()) -> maybe(binary())). id(#message{id = Id}) -> Id. --spec(qos(emqx_types:message()) -> emqx_mqtt_types:qos()). +-spec(qos(emqx_types:message()) -> emqx_types:qos()). qos(#message{qos = QoS}) -> QoS. -spec(from(emqx_types:message()) -> atom() | binary()). diff --git a/src/emqx_misc.erl b/src/emqx_misc.erl index e04fd606d..fdc7c5dc3 100644 --- a/src/emqx_misc.erl +++ b/src/emqx_misc.erl @@ -122,19 +122,20 @@ check([{Pred, Result} | Rest]) -> is_message_queue_too_long(Qlength, Max) -> is_enabled(Max) andalso Qlength > Max. -is_enabled(Max) -> is_integer(Max) andalso Max > ?DISABLED. +is_enabled(Max) -> + is_integer(Max) andalso Max > ?DISABLED. proc_info(Key) -> {Key, Value} = erlang:process_info(self(), Key), Value. -%% @doc Drain delivers from channel's mailbox. +%% @doc Drain delivers from the channel's mailbox. drain_deliver(Acc) -> receive Deliver = {deliver, _Topic, _Msg} -> drain_deliver([Deliver|Acc]) after 0 -> - lists:reverse(Acc) + lists:reverse(Acc) end. %% @doc Drain process down events. @@ -150,6 +151,6 @@ drain_down(Cnt, Acc) -> {'DOWN', _MRef, process, Pid, _Reason} -> drain_down(Cnt - 1, [Pid|Acc]) after 0 -> - lists:reverse(Acc) + drain_down(0, Acc) end. diff --git a/src/emqx_mod_presence.erl b/src/emqx_mod_presence.erl index 151ffbb7f..9c96bd774 100644 --- a/src/emqx_mod_presence.erl +++ b/src/emqx_mod_presence.erl @@ -46,9 +46,9 @@ load(Env) -> on_client_connected(#{client_id := ClientId, username := Username, peername := {IpAddr, _}}, ConnAck, ConnAttrs, Env) -> - Attrs = maps:filter(fun(K, _) -> - lists:member(K, ?ATTR_KEYS) - end, ConnAttrs), + Attrs = #{},%maps:filter(fun(K, _) -> + % lists:member(K, ?ATTR_KEYS) + % end, ConnAttrs), case emqx_json:safe_encode(Attrs#{clientid => ClientId, username => Username, ipaddress => iolist_to_binary(esockd_net:ntoa(IpAddr)), diff --git a/src/emqx_mountpoint.erl b/src/emqx_mountpoint.erl index 80a65b743..4b820fdc7 100644 --- a/src/emqx_mountpoint.erl +++ b/src/emqx_mountpoint.erl @@ -35,15 +35,23 @@ mount(undefined, Any) -> Any; +mount(MountPoint, Topic) when is_binary(Topic) -> + <>; mount(MountPoint, Msg = #message{topic = Topic}) -> Msg#message{topic = <>}; - mount(MountPoint, TopicFilters) when is_list(TopicFilters) -> [{<>, SubOpts} || {Topic, SubOpts} <- TopicFilters]. unmount(undefined, Msg) -> Msg; +%% TODO: Fixme later +unmount(MountPoint, Topic) when is_binary(Topic) -> + try split_binary(Topic, byte_size(MountPoint)) of + {MountPoint, Topic1} -> Topic1 + catch + error:badarg-> Topic + end; unmount(MountPoint, Msg = #message{topic = Topic}) -> try split_binary(Topic, byte_size(MountPoint)) of {MountPoint, Topic1} -> Msg#message{topic = Topic1} diff --git a/src/emqx_mqtt.erl b/src/emqx_mqtt.erl deleted file mode 100644 index bd10bce40..000000000 --- a/src/emqx_mqtt.erl +++ /dev/null @@ -1,60 +0,0 @@ -%%-------------------------------------------------------------------- -%% 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. -%%-------------------------------------------------------------------- - -%% MQTT Types --module(emqx_mqtt). - --include("emqx_mqtt.hrl"). - --export_type([ version/0 - , qos/0 - , qos_name/0 - ]). - --export_type([ connack/0 - , reason_code/0 - ]). - --export_type([ properties/0 - , subopts/0 - ]). - --export_type([topic_filters/0]). - --export_type([ packet_id/0 - , packet_type/0 - , packet/0 - ]). - --type(qos() :: ?QOS_0 | ?QOS_1 | ?QOS_2). --type(version() :: ?MQTT_PROTO_V3 | ?MQTT_PROTO_V4 | ?MQTT_PROTO_V5). --type(qos_name() :: qos0 | at_most_once | - qos1 | at_least_once | - qos2 | exactly_once). --type(packet_type() :: ?RESERVED..?AUTH). --type(connack() :: ?CONNACK_ACCEPT..?CONNACK_AUTH). --type(reason_code() :: 0..16#FF). --type(packet_id() :: 1..16#FFFF). --type(properties() :: #{atom() => term()}). --type(subopts() :: #{rh := 0 | 1 | 2, - rap := 0 | 1, - nl := 0 | 1, - qos := qos(), - rc => reason_code() - }). --type(topic_filters() :: list({emqx_topic:topic(), subopts()})). --type(packet() :: #mqtt_packet{}). - diff --git a/src/emqx_mqtt_caps.erl b/src/emqx_mqtt_caps.erl index f941b1e24..a95641723 100644 --- a/src/emqx_mqtt_caps.erl +++ b/src/emqx_mqtt_caps.erl @@ -32,7 +32,7 @@ max_clientid_len => integer(), max_topic_alias => integer(), max_topic_levels => integer(), - max_qos_allowed => emqx_mqtt_types:qos(), + max_qos_allowed => emqx_types:qos(), mqtt_retain_available => boolean(), mqtt_shared_subscription => boolean(), mqtt_wildcard_subscription => boolean()}). @@ -57,7 +57,7 @@ mqtt_shared_subscription, mqtt_wildcard_subscription]). --spec(check_pub(emqx_types:zone(), map()) -> ok | {error, emqx_mqtt_types:reason_code()}). +-spec(check_pub(emqx_types:zone(), map()) -> ok | {error, emqx_types:reason_code()}). check_pub(Zone, Props) when is_map(Props) -> do_check_pub(Props, maps:to_list(get_caps(Zone, publish))). @@ -80,8 +80,8 @@ do_check_pub(Props, [{max_topic_alias, _} | Caps]) -> do_check_pub(Props, [{mqtt_retain_available, _}|Caps]) -> do_check_pub(Props, Caps). --spec(check_sub(emqx_types:zone(), emqx_mqtt_types:topic_filters()) - -> {ok | error, emqx_mqtt_types:topic_filters()}). +-spec(check_sub(emqx_types:zone(), emqx_types:topic_filters()) + -> {ok | error, emqx_types:topic_filters()}). check_sub(Zone, TopicFilters) -> Caps = maps:to_list(get_caps(Zone, subscribe)), lists:foldr(fun({Topic, Opts}, {Ok, Result}) -> @@ -154,3 +154,4 @@ with_env(Zone, Key, InitFun) -> Caps; ZoneCaps -> ZoneCaps end. + diff --git a/src/emqx_packet.erl b/src/emqx_packet.erl index ea917c8bf..71d1fb116 100644 --- a/src/emqx_packet.erl +++ b/src/emqx_packet.erl @@ -29,7 +29,7 @@ ]). %% @doc Protocol name of version --spec(protocol_name(emqx_mqtt_types:version()) -> binary()). +-spec(protocol_name(emqx_types:version()) -> binary()). protocol_name(?MQTT_PROTO_V3) -> <<"MQIsdp">>; protocol_name(?MQTT_PROTO_V4) -> @@ -38,7 +38,7 @@ protocol_name(?MQTT_PROTO_V5) -> <<"MQTT">>. %% @doc Name of MQTT packet type --spec(type_name(emqx_mqtt_types:packet_type()) -> atom()). +-spec(type_name(emqx_types:packet_type()) -> atom()). type_name(Type) when Type > ?RESERVED andalso Type =< ?AUTH -> lists:nth(Type, ?TYPE_NAMES). @@ -46,7 +46,7 @@ type_name(Type) when Type > ?RESERVED andalso Type =< ?AUTH -> %% Validate MQTT Packet %%-------------------------------------------------------------------- --spec(validate(emqx_mqtt_types:packet()) -> true). +-spec(validate(emqx_types:packet()) -> true). validate(?SUBSCRIBE_PACKET(_PacketId, _Properties, [])) -> error(topic_filters_invalid); validate(?SUBSCRIBE_PACKET(PacketId, Properties, TopicFilters)) -> @@ -113,8 +113,8 @@ validate_qos(QoS) when ?QOS_0 =< QoS, QoS =< ?QOS_2 -> validate_qos(_) -> error(bad_qos). %% @doc From message to packet --spec(from_message(emqx_mqtt_types:packet_id(), emqx_types:message()) - -> emqx_mqtt_types:packet()). +-spec(from_message(emqx_types:packet_id(), emqx_types:message()) + -> emqx_types:packet()). from_message(PacketId, #message{qos = QoS, flags = Flags, headers = Headers, topic = Topic, payload = Payload}) -> Flags1 = if Flags =:= undefined -> @@ -142,7 +142,7 @@ publish_props(Headers) -> 'Message-Expiry-Interval'], Headers). %% @doc Message from Packet --spec(to_message(emqx_types:credentials(), emqx_mqtt_types:packet()) +-spec(to_message(emqx_types:credentials(), emqx_ypes:packet()) -> emqx_types:message()). to_message(#{client_id := ClientId, username := Username, peername := Peername}, #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, @@ -177,7 +177,7 @@ merge_props(Headers, Props) -> maps:merge(Headers, Props). %% @doc Format packet --spec(format(emqx_mqtt_types:packet()) -> iolist()). +-spec(format(emqx_types:packet()) -> iolist()). format(#mqtt_packet{header = Header, variable = Variable, payload = Payload}) -> format_header(Header, format_variable(Variable, Payload)). diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl new file mode 100644 index 000000000..9bbb63bf4 --- /dev/null +++ b/src/emqx_protocol.erl @@ -0,0 +1,594 @@ +%%-------------------------------------------------------------------- +%% 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. +%%-------------------------------------------------------------------- + +%% MQTT Protocol +-module(emqx_protocol). + +-include("emqx.hrl"). +-include("emqx_mqtt.hrl"). +-include("logger.hrl"). +-include("types.hrl"). + +-logger_header("[Protocol]"). + +-export([ info/1 + , info/2 + , attrs/1 + , stats/1 + ]). + +-export([ client_id/1 + , session/1 + ]). + +-export([ init/2 + , handle_in/2 + , handle_deliver/2 + , handle_out/2 + , handle_timeout/3 + , terminate/2 + ]). + +-export_type([proto_state/0]). + +-record(protocol, { + proto_name :: binary(), + proto_ver :: emqx_types:version(), + client :: emqx_types:client(), + session :: emqx_session:session(), + mountfun :: fun((emqx_topic:topic()) -> emqx_topic:topic()), + keepalive :: non_neg_integer(), + will_msg :: emqx_types:message(), + enable_acl :: boolean(), + is_bridge :: boolean(), + connected :: boolean(), + connected_at :: erlang:timestamp(), + topic_aliases :: map(), + alias_maximum :: map() + }). + +-opaque(proto_state() :: #protocol{}). + +info(#protocol{client = Client, session = Session}) -> + lists:append([maps:to_list(Client), emqx_session:info(Session)]). + +info(zone, #protocol{client = #{zone := Zone}}) -> + Zone; +info(proto_name, #protocol{proto_name = ProtoName}) -> + ProtoName; +info(proto_ver, #protocol{proto_ver = ProtoVer}) -> + ProtoVer; +info(keepalive, #protocol{keepalive = Keepalive}) -> + Keepalive. + +attrs(#protocol{}) -> + #{}. + +stats(#protocol{}) -> + []. + +-spec(client_id(proto_state()) -> emqx_types:client_id()). +client_id(#protocol{client = #{client_id := ClientId}}) -> + ClientId. + +-spec(session(proto_state()) -> emqx_session:session()). +session(#protocol{session = Session}) -> + Session. + +-spec(init(map(), proplists:proplist()) -> proto_state()). +init(ConnInfo, Options) -> + Zone = proplists:get_value(zone, Options), + Peercert = maps:get(peercert, ConnInfo, nossl), + Username = peer_cert_as_username(Peercert, Options), + Mountpoint = emqx_zone:get_env(Zone, mountpoint), + Client = maps:merge(#{zone => Zone, + username => Username, + mountpoint => Mountpoint + }, ConnInfo), + EnableAcl = emqx_zone:get_env(Zone, enable_acl, false), + MountFun = fun(Topic) -> + emqx_mountpoint:mount(Mountpoint, Topic) + end, + #protocol{client = Client, + mountfun = MountFun, + enable_acl = EnableAcl, + is_bridge = false, + connected = false + }. + +peer_cert_as_username(Peercert, Options) -> + case proplists:get_value(peer_cert_as_username, Options) of + cn -> esockd_peercert:common_name(Peercert); + dn -> esockd_peercert:subject(Peercert); + crt -> Peercert; + _ -> undefined + end. + +%%-------------------------------------------------------------------- +%% Handle incoming packet +%%-------------------------------------------------------------------- + +-spec(handle_in(emqx_types:packet(), proto_state()) + -> {ok, proto_state()} + | {ok, emqx_types:packet(), proto_state()} + | {error, Reason :: term(), proto_state()} + | {stop, Error :: atom(), proto_state()}). +handle_in(?CONNECT_PACKET( + #mqtt_packet_connect{proto_name = ProtoName, + proto_ver = ProtoVer, + is_bridge = IsBridge, + client_id = ClientId, + username = Username, + password = Password, + keepalive = Keepalive} = ConnPkt), + PState = #protocol{client = Client}) -> + Client1 = maps:merge(Client, #{client_id => ClientId, + username => Username, + password => Password + }), + emqx_logger:set_metadata_client_id(ClientId), + WillMsg = emqx_packet:will_msg(ConnPkt), + PState1 = PState#protocol{client = Client1, + proto_name = ProtoName, + proto_ver = ProtoVer, + is_bridge = IsBridge, + keepalive = Keepalive, + will_msg = WillMsg + }, + %% fun validate_packet/2, + case pipeline([fun check_connect/2, + fun handle_connect/2], ConnPkt, PState1) of + {ok, SP, PState2} -> + handle_out({connack, ?RC_SUCCESS, sp(SP)}, PState2); + {error, ReasonCode} -> + handle_out({connack, ReasonCode}, PState1); + {error, ReasonCode, PState2} -> + handle_out({connack, ReasonCode}, PState2) + end; + +handle_in(Packet = ?PUBLISH_PACKET(QoS, Topic, PacketId), PState) -> + case pipeline([fun validate_packet/2, + fun check_pub_caps/2, + fun check_pub_acl/2, + fun handle_publish/2], Packet, PState) of + {error, ReasonCode} -> + ?LOG(warning, "Cannot publish qos~w message to ~s due to ~s", + [QoS, Topic, emqx_reason_codes:text(ReasonCode)]), + handle_out(case QoS of + ?QOS_0 -> {puberr, ReasonCode}; + ?QOS_1 -> {puback, PacketId, ReasonCode}; + ?QOS_2 -> {pubrec, PacketId, ReasonCode} + end, PState); + Result -> Result + end; + +handle_in(?PUBACK_PACKET(PacketId, ReasonCode), PState = #protocol{session = Session}) -> + case emqx_session:puback(PacketId, ReasonCode, Session) of + {ok, NSession} -> + {ok, PState#protocol{session = NSession}}; + {error, _NotFound} -> + %% TODO: metrics? error msg? + {ok, PState} + end; + +handle_in(?PUBREC_PACKET(PacketId, ReasonCode), PState = #protocol{session = Session}) -> + case emqx_session:pubrec(PacketId, ReasonCode, Session) of + {ok, NSession} -> + handle_out({pubrel, PacketId}, PState#protocol{session = NSession}); + {error, ReasonCode} -> + handle_out({pubrel, PacketId, ReasonCode}, PState) + end; + +handle_in(?PUBREL_PACKET(PacketId, ReasonCode), PState = #protocol{session = Session}) -> + case emqx_session:pubrel(PacketId, ReasonCode, Session) of + {ok, NSession} -> + handle_out({pubcomp, PacketId}, PState#protocol{session = NSession}); + {error, ReasonCode} -> + handle_out({pubcomp, PacketId, ReasonCode}, PState) + end; + +handle_in(?PUBCOMP_PACKET(PacketId, ReasonCode), + PState = #protocol{session = Session}) -> + case emqx_session:pubcomp(PacketId, ReasonCode, Session) of + {ok, NSession} -> + {ok, PState#protocol{session = NSession}}; + {error, _ReasonCode} -> + %% TODO: How to handle the reason code? + {ok, PState} + end; + +handle_in(Packet = ?SUBSCRIBE_PACKET(PacketId, Properties, TopicFilters), + PState = #protocol{client = Client}) -> + case validate(Packet) of + ok -> ok = emqx_hooks:run('client.subscribe', + [Client, Properties, TopicFilters]), + TopicFilters1 = enrich_subid(Properties, TopicFilters), + {ReasonCodes, PState1} = handle_subscribe(TopicFilters1, PState), + handle_out({suback, PacketId, ReasonCodes}, PState1); + {error, ReasonCode} -> + handle_out({disconnect, ReasonCode}, PState) + end; + +handle_in(Packet = ?UNSUBSCRIBE_PACKET(PacketId, Properties, TopicFilters), + PState = #protocol{client = Client}) -> + case validate(Packet) of + ok -> ok = emqx_hooks:run('client.unsubscribe', + [Client, Properties, TopicFilters]), + {ReasonCodes, PState1} = handle_unsubscribe(TopicFilters, PState), + handle_out({unsuback, PacketId, ReasonCodes}, PState1); + {error, ReasonCode} -> + handle_out({disconnect, ReasonCode}, PState) + end; + +handle_in(?PACKET(?PINGREQ), PState) -> + {ok, ?PACKET(?PINGRESP), PState}; + +handle_in(?DISCONNECT_PACKET(?RC_SUCCESS), PState) -> + %% Clear will msg + {stop, normal, PState#protocol{will_msg = undefined}}; + +handle_in(?DISCONNECT_PACKET(RC), PState = #protocol{proto_ver = Ver}) -> + %% TODO: + %% {stop, {shutdown, abnormal_disconnet}, PState}; + {sto, {shutdown, emqx_reason_codes:name(RC, Ver)}, PState}; + +handle_in(?AUTH_PACKET(), PState) -> + %%TODO: implement later. + {ok, PState}; + +handle_in(Packet, PState) -> + io:format("In: ~p~n", [Packet]), + {ok, PState}. + +%%-------------------------------------------------------------------- +%% Handle delivers +%%-------------------------------------------------------------------- + +handle_deliver(Delivers, PState = #protocol{client = Client, session = Session}) + when is_list(Delivers) -> + case emqx_session:handle(Delivers, Session) of + {ok, Publishes, NSession} -> + Packets = lists:map(fun({publish, PacketId, Msg}) -> + Msg0 = emqx_hooks:run_fold('message.deliver', [Client], Msg), + Msg1 = emqx_message:update_expiry(Msg0), + Msg2 = emqx_mountpoint:unmount(maps:get(mountpoint, Client), Msg1), + emqx_packet:from_message(PacketId, Msg2) + end, Publishes), + {ok, Packets, PState#protocol{session = NSession}}; + {ok, NSession} -> + {ok, PState#protocol{session = NSession}} + end. + +%%-------------------------------------------------------------------- +%% Handle outgoing packet +%%-------------------------------------------------------------------- + +handle_out({connack, ?RC_SUCCESS, SP}, PState = #protocol{client = Client}) -> + ok = emqx_hooks:run('client.connected', [Client, ?RC_SUCCESS, info(PState)]), + Props = #{}, %% TODO: ... + {ok, ?CONNACK_PACKET(?RC_SUCCESS, SP, Props), PState}; + +handle_out({connack, ReasonCode}, PState = #protocol{client = Client, + proto_ver = ProtoVer}) -> + ok = emqx_hooks:run('client.connected', [Client, ReasonCode, info(PState)]), + ReasonCode1 = if + ProtoVer == ?MQTT_PROTO_V5 -> ReasonCode; + true -> emqx_reason_codes:compat(connack, ReasonCode) + end, + Reason = emqx_reason_codes:name(ReasonCode1, ProtoVer), + {error, Reason, ?CONNACK_PACKET(ReasonCode1), PState}; + +handle_out({publish, PacketId, Msg}, PState = #protocol{client = Client}) -> + Msg0 = emqx_hooks:run_fold('message.deliver', [Client], Msg), + Msg1 = emqx_message:update_expiry(Msg0), + Msg2 = emqx_mountpoint:unmount(maps:get(mountpoint, Client), Msg1), + {ok, emqx_packet:from_message(PacketId, Msg2), PState}; + +handle_out({puberr, ReasonCode}, PState) -> + {ok, PState}; + +handle_out({puback, PacketId, ReasonCode}, PState) -> + {ok, ?PUBACK_PACKET(PacketId, ReasonCode), PState}; + +handle_out({pubrel, PacketId}, PState) -> + {ok, ?PUBREL_PACKET(PacketId), PState}; +handle_out({pubrel, PacketId, ReasonCode}, PState) -> + {ok, ?PUBREL_PACKET(PacketId, ReasonCode), PState}; + +handle_out({pubrec, PacketId, ReasonCode}, PState) -> + {ok, ?PUBREC_PACKET(PacketId, ReasonCode), PState}; + +handle_out({pubcomp, PacketId}, PState) -> + {ok, ?PUBCOMP_PACKET(PacketId), PState}; +handle_out({pubcomp, PacketId, ReasonCode}, PState) -> + {ok, ?PUBCOMP_PACKET(PacketId, ReasonCode), PState}; + +handle_out({suback, PacketId, ReasonCodes}, PState = #protocol{proto_ver = ?MQTT_PROTO_V5}) -> + %% TODO: ACL Deny + {ok, ?SUBACK_PACKET(PacketId, ReasonCodes), PState}; +handle_out({suback, PacketId, ReasonCodes}, PState) -> + %% TODO: ACL Deny + ReasonCodes1 = [emqx_reason_codes:compat(suback, RC) || RC <- ReasonCodes], + {ok, ?SUBACK_PACKET(PacketId, ReasonCodes1), PState}; + +handle_out({unsuback, PacketId, ReasonCodes}, PState = #protocol{proto_ver = ?MQTT_PROTO_V5}) -> + {ok, ?UNSUBACK_PACKET(PacketId, ReasonCodes), PState}; +%% Ignore reason codes if not MQTT5 +handle_out({unsuback, PacketId, _ReasonCodes}, PState) -> + {ok, ?UNSUBACK_PACKET(PacketId), PState}; + +handle_out(Packet, State) -> + io:format("Out: ~p~n", [Packet]), + {ok, State}. + +%%-------------------------------------------------------------------- +%% Handle timeout +%%-------------------------------------------------------------------- + +handle_timeout(TRef, Msg, PState = #protocol{session = Session}) -> + case emqx_session:timeout(TRef, Msg, Session) of + {ok, NSession} -> + {ok, PState#protocol{session = NSession}}; + {ok, Publishes, NSession} -> + %% TODO: handle out... + io:format("Timeout publishes: ~p~n", [Publishes]), + {ok, PState#protocol{session = NSession}} + end. + +terminate(Reason, _State) -> + io:format("Terminated for ~p~n", [Reason]), + ok. + +%%-------------------------------------------------------------------- +%% Check Connect Packet +%%-------------------------------------------------------------------- + +check_connect(_ConnPkt, PState) -> + {ok, PState}. + +%%-------------------------------------------------------------------- +%% Handle Connect Packet +%%-------------------------------------------------------------------- + +handle_connect(#mqtt_packet_connect{proto_name = ProtoName, + proto_ver = ProtoVer, + is_bridge = IsBridge, + clean_start = CleanStart, + keepalive = Keepalive, + properties = ConnProps, + client_id = ClientId, + username = Username, + password = Password + } = ConnPkt, + PState = #protocol{client = Client}) -> + case emqx_access_control:authenticate( + Client#{password => Password}) of + {ok, AuthResult} -> + Client1 = maps:merge(Client, AuthResult), + %% Open session + case open_session(ConnPkt, PState) of + {ok, Session, SP} -> + PState1 = PState#protocol{client = Client1, + session = Session, + connected = true, + connected_at = os:timestamp() + }, + ok = emqx_cm:register_channel(ClientId), + {ok, SP, PState1}; + {error, Error} -> + ?LOG(error, "Failed to open session: ~p", [Error]), + {error, ?RC_UNSPECIFIED_ERROR, PState#protocol{client = Client1}} + end; + {error, Reason} -> + ?LOG(warning, "Client ~s (Username: '~s') login failed for ~p", + [ClientId, Username, Reason]), + {error, emqx_reason_codes:connack_error(Reason), PState} + end. + +open_session(#mqtt_packet_connect{clean_start = CleanStart, + %%properties = ConnProps, + client_id = ClientId, + username = Username} = ConnPkt, + PState = #protocol{client = Client}) -> + emqx_cm:open_session(maps:merge(Client, #{clean_start => CleanStart, + max_inflight => 0, + expiry_interval => 0})). + +%%-------------------------------------------------------------------- +%% Handle Publish Message: Client -> Broker +%%-------------------------------------------------------------------- + +handle_publish(Packet = ?PUBLISH_PACKET(_QoS, Topic, PacketId), + PState = #protocol{client = Client = #{mountpoint := Mountpoint}}) -> + %% TODO: ugly... publish_to_msg(...) + Msg = emqx_packet:to_message(Client, Packet), + Msg1 = emqx_mountpoint:mount(Mountpoint, Msg), + Msg2 = emqx_message:set_flag(dup, false, Msg1), + handle_publish(PacketId, Msg2, PState). + +handle_publish(_PacketId, Msg = #message{qos = ?QOS_0}, PState) -> + _ = emqx_broker:publish(Msg), + {ok, PState}; + +handle_publish(PacketId, Msg = #message{qos = ?QOS_1}, PState) -> + Results = emqx_broker:publish(Msg), + ReasonCode = emqx_reason_codes:puback(Results), + handle_out({puback, PacketId, ReasonCode}, PState); + +handle_publish(PacketId, Msg = #message{qos = ?QOS_2}, + PState = #protocol{session = Session}) -> + case emqx_session:publish(PacketId, Msg, Session) of + {ok, Results, NSession} -> + ReasonCode = emqx_reason_codes:puback(Results), + handle_out({pubrec, PacketId, ReasonCode}, + PState#protocol{session = NSession}); + {error, ReasonCode} -> + handle_out({pubrec, PacketId, ReasonCode}, PState) + end. + +%%-------------------------------------------------------------------- +%% Handle Subscribe Request +%%-------------------------------------------------------------------- + +handle_subscribe(TopicFilters, PState) -> + handle_subscribe(TopicFilters, [], PState). + +handle_subscribe([], Acc, PState) -> + {lists:reverse(Acc), PState}; + +handle_subscribe([{TopicFilter, SubOpts}|More], Acc, PState) -> + {RC, PState1} = do_subscribe(TopicFilter, SubOpts, PState), + handle_subscribe(More, [RC|Acc], PState1). + +do_subscribe(TopicFilter, SubOpts = #{qos := QoS}, + PState = #protocol{client = Client, + session = Session, + mountfun = Mount}) -> + %% 1. Parse 2. Check 3. Enrich 5. MountPoint 6. Session + SubOpts1 = maps:merge(?DEFAULT_SUBOPTS, SubOpts), + {TopicFilter1, SubOpts2} = emqx_topic:parse(TopicFilter, SubOpts1), + SubOpts3 = enrich_subopts(SubOpts2, PState), + case check_subscribe(TopicFilter1, PState) of + ok -> + TopicFilter2 = Mount(TopicFilter1), + case emqx_session:subscribe(Client, TopicFilter2, SubOpts3, Session) of + {ok, NSession} -> + {QoS, PState#protocol{session = NSession}}; + {error, RC} -> {RC, PState} + end; + {error, RC} -> {RC, PState} + end. + +enrich_subid(#{'Subscription-Identifier' := SubId}, TopicFilters) -> + [{Topic, SubOpts#{subid => SubId}} || {Topic, SubOpts} <- TopicFilters]; +enrich_subid(_Properties, TopicFilters) -> + TopicFilters. + +enrich_subopts(SubOpts, #protocol{proto_ver = ?MQTT_PROTO_V5}) -> + SubOpts; +enrich_subopts(SubOpts, #protocol{client = #{zone := Zone}, + is_bridge = IsBridge}) -> + Rap = flag(IsBridge), + Nl = flag(emqx_zone:get_env(Zone, ignore_loop_deliver, false)), + SubOpts#{rap => Rap, nl => Nl}. + +check_subscribe(_TopicFilter, _PState) -> + ok. + +%%-------------------------------------------------------------------- +%% Handle Unsubscribe Request +%%-------------------------------------------------------------------- + +handle_unsubscribe(TopicFilters, PState) -> + handle_unsubscribe(TopicFilters, [], PState). + +handle_unsubscribe([], Acc, PState) -> + {lists:reverse(Acc), PState}; + +handle_unsubscribe([TopicFilter|More], Acc, PState) -> + {RC, PState1} = do_unsubscribe(TopicFilter, PState), + handle_unsubscribe(More, [RC|Acc], PState1). + +do_unsubscribe(TopicFilter, PState = #protocol{client = Client, + session = Session, + mountfun = Mount}) -> + TopicFilter1 = Mount(element(1, emqx_topic:parse(TopicFilter))), + case emqx_session:unsubscribe(Client, TopicFilter1, Session) of + {ok, NSession} -> + {?RC_SUCCESS, PState#protocol{session = NSession}}; + {error, RC} -> {RC, PState} + end. + +%%-------------------------------------------------------------------- +%% Validate Incoming Packet +%%-------------------------------------------------------------------- + +validate_packet(Packet, _PState) -> + validate(Packet). + +-spec(validate(emqx_types:packet()) -> ok | {error, emqx_types:reason_code()}). +validate(Packet) -> + try emqx_packet:validate(Packet) of + true -> ok + catch + error:protocol_error -> + {error, ?RC_PROTOCOL_ERROR}; + error:subscription_identifier_invalid -> + {error, ?RC_SUBSCRIPTION_IDENTIFIERS_NOT_SUPPORTED}; + error:topic_alias_invalid -> + {error, ?RC_TOPIC_ALIAS_INVALID}; + error:topic_filters_invalid -> + {error, ?RC_TOPIC_FILTER_INVALID}; + error:topic_name_invalid -> + {error, ?RC_TOPIC_FILTER_INVALID}; + error:_Reason -> + {error, ?RC_MALFORMED_PACKET} + end. + +%%-------------------------------------------------------------------- +%% Check Publish +%%-------------------------------------------------------------------- + +check_pub_caps(#mqtt_packet{header = #mqtt_packet_header{qos = QoS, + retain = Retain}, + variable = #mqtt_packet_publish{}}, + #protocol{client = #{zone := Zone}}) -> + emqx_mqtt_caps:check_pub(Zone, #{qos => QoS, retain => Retain}). + +check_pub_acl(_Packet, #protocol{enable_acl = false}) -> + ok; +check_pub_acl(_Packet, #protocol{client = #{is_superuser := true}}) -> + ok; +check_pub_acl(#mqtt_packet{variable = #mqtt_packet_publish{topic_name = Topic}}, + #protocol{client = Client}) -> + do_acl_check(Client, publish, Topic). + +check_sub_acl(_Packet, #protocol{enable_acl = false}) -> + ok. + +do_acl_check(Client, PubSub, Topic) -> + case emqx_access_control:check_acl(Client, PubSub, Topic) of + allow -> ok; + deny -> {error, ?RC_NOT_AUTHORIZED} + end. + +%%-------------------------------------------------------------------- +%% Pipeline +%%-------------------------------------------------------------------- + +pipeline([Fun], Packet, PState) -> + Fun(Packet, PState); +pipeline([Fun|More], Packet, PState) -> + case Fun(Packet, PState) of + ok -> pipeline(More, Packet, PState); + {ok, NPState} -> + pipeline(More, Packet, NPState); + {ok, NPacket, NPState} -> + pipeline(More, NPacket, NPState); + {error, Reason} -> + {error, Reason} + end. + +%%-------------------------------------------------------------------- +%% Helper functions +%%-------------------------------------------------------------------- + +sp(true) -> 1; +sp(false) -> 0. + +flag(true) -> 1; +flag(false) -> 0. + diff --git a/src/emqx_session.erl b/src/emqx_session.erl index e699a7252..e5978142b 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -48,35 +48,28 @@ -include("logger.hrl"). -include("types.hrl"). --logger_header("[Session]"). - --export([new/1]). +-export([init/1]). -export([ info/1 - , attrs/1 , stats/1 ]). --export([ subscribe/3 +-export([ subscribe/4 , unsubscribe/3 ]). --export([publish/3]). - --export([ puback/3 +-export([ publish/3 + , puback/3 , pubrec/3 , pubrel/3 , pubcomp/3 ]). --export([ deliver/2 - , await/3 - , enqueue/2 - ]). +-export([handle/2]). --export_type([ session/0 - , puback_ret/0 - ]). +-export([timeout/3]). + +-export_type([session/0]). -import(emqx_zone, [ get_env/2 @@ -107,7 +100,7 @@ mqueue :: emqx_mqueue:mqueue(), %% Next packet id of the session - next_pkt_id = 1 :: emqx_mqtt:packet_id(), + next_pkt_id = 1 :: emqx_types:packet_id(), %% Retry interval for redelivering QoS1/2 messages retry_interval :: timeout(), @@ -140,17 +133,20 @@ -opaque(session() :: #session{}). --type(puback_ret() :: {ok, session()} - | {ok, emqx_types:message(), session()} - | {error, emqx_mqtt:reason_code()}). +-logger_header("[Session]"). -%% @doc Create a session. --spec(new(Attrs :: map()) -> session()). -new(#{zone := Zone, - clean_start := CleanStart, - max_inflight := MaxInflight, - expiry_interval := ExpiryInterval}) -> - %% emqx_logger:set_metadata_client_id(ClientId), +-define(DEFAULT_BATCH_N, 1000). + +%%-------------------------------------------------------------------- +%% Init a session +%%-------------------------------------------------------------------- + +%% @doc Init a session. +-spec(init(Attrs :: map()) -> session()). +init(#{zone := Zone, + clean_start := CleanStart, + max_inflight := MaxInflight, + expiry_interval := ExpiryInterval}) -> #session{clean_start = CleanStart, max_subscriptions = get_env(Zone, max_subscriptions, 0), subscriptions = #{}, @@ -173,12 +169,11 @@ init_mqueue(Zone) -> default_priority => get_env(Zone, mqueue_default_priority) }). -%%------------------------------------------------------------------------------ -%% Info, Attrs, Stats -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- +%% Info, Stats of Session +%%-------------------------------------------------------------------- -%% @doc Get session info --spec(info(session()) -> map()). +-spec(info(session()) -> proplists:proplist()). info(#session{clean_start = CleanStart, max_subscriptions = MaxSubscriptions, subscriptions = Subscriptions, @@ -186,174 +181,163 @@ info(#session{clean_start = CleanStart, inflight = Inflight, retry_interval = RetryInterval, mqueue = MQueue, - next_pkt_id = PktId, + next_pkt_id = PacketId, max_awaiting_rel = MaxAwaitingRel, awaiting_rel = AwaitingRel, await_rel_timeout = AwaitRelTimeout, expiry_interval = ExpiryInterval, created_at = CreatedAt}) -> - #{clean_start => CleanStart, - max_subscriptions => MaxSubscriptions, - subscriptions => Subscriptions, - upgrade_qos => UpgradeQoS, - inflight => Inflight, - retry_interval => RetryInterval, - mqueue_len => emqx_mqueue:len(MQueue), - next_pkt_id => PktId, - awaiting_rel => AwaitingRel, - max_awaiting_rel => MaxAwaitingRel, - await_rel_timeout => AwaitRelTimeout, - expiry_interval => ExpiryInterval div 1000, - created_at => CreatedAt - }. - -%% @doc Get session attrs. --spec(attrs(session()) -> map()). -attrs(#session{clean_start = CleanStart, - expiry_interval = ExpiryInterval, - created_at = CreatedAt}) -> - #{clean_start => CleanStart, - expiry_interval => ExpiryInterval div 1000, - created_at => CreatedAt - }. + [{clean_start, CleanStart}, + {max_subscriptions, MaxSubscriptions}, + {subscriptions, Subscriptions}, + {upgrade_qos, UpgradeQoS}, + {inflight, Inflight}, + {retry_interval, RetryInterval}, + {mqueue_len, emqx_mqueue:len(MQueue)}, + {next_pkt_id, PacketId}, + {awaiting_rel, AwaitingRel}, + {max_awaiting_rel, MaxAwaitingRel}, + {await_rel_timeout, AwaitRelTimeout}, + {expiry_interval, ExpiryInterval div 1000}, + {created_at, CreatedAt}]. %% @doc Get session stats. --spec(stats(session()) -> #{atom() => non_neg_integer()}). +-spec(stats(session()) -> list({atom(), non_neg_integer()})). stats(#session{max_subscriptions = MaxSubscriptions, subscriptions = Subscriptions, inflight = Inflight, mqueue = MQueue, max_awaiting_rel = MaxAwaitingRel, awaiting_rel = AwaitingRel}) -> - #{max_subscriptions => MaxSubscriptions, - subscriptions_count => maps:size(Subscriptions), - max_inflight => emqx_inflight:max_size(Inflight), - inflight_len => emqx_inflight:size(Inflight), - max_mqueue => emqx_mqueue:max_len(MQueue), - mqueue_len => emqx_mqueue:len(MQueue), - mqueue_dropped => emqx_mqueue:dropped(MQueue), - max_awaiting_rel => MaxAwaitingRel, - awaiting_rel_len => maps:size(AwaitingRel) - }. + [{max_subscriptions, MaxSubscriptions}, + {subscriptions_count, maps:size(Subscriptions)}, + {max_inflight, emqx_inflight:max_size(Inflight)}, + {inflight_len, emqx_inflight:size(Inflight)}, + {max_mqueue, emqx_mqueue:max_len(MQueue)}, + {mqueue_len, emqx_mqueue:len(MQueue)}, + {mqueue_dropped, emqx_mqueue:dropped(MQueue)}, + {max_awaiting_rel, MaxAwaitingRel}, + {awaiting_rel_len, maps:size(AwaitingRel)}]. %%-------------------------------------------------------------------- -%% PubSub API -%%-------------------------------------------------------------------- - %% Client -> Broker: SUBSCRIBE --spec(subscribe(emqx_types:credentials(), emqx_mqtt:topic_filters(), session()) - -> {ok, list(emqx_mqtt:reason_code()), session()}). -subscribe(Credentials, RawTopicFilters, Session = #session{subscriptions = Subscriptions}) - when is_list(RawTopicFilters) -> - TopicFilters = [emqx_topic:parse(RawTopic, maps:merge(?DEFAULT_SUBOPTS, SubOpts)) - || {RawTopic, SubOpts} <- RawTopicFilters], - {ReasonCodes, Subscriptions1} = - lists:foldr( - fun({Topic, SubOpts = #{qos := QoS, rc := RC}}, {RcAcc, SubMap}) - when RC == ?QOS_0; RC == ?QOS_1; RC == ?QOS_2 -> - {[QoS|RcAcc], do_subscribe(Credentials, Topic, SubOpts, SubMap)}; - ({_Topic, #{rc := RC}}, {RcAcc, SubMap}) -> - {[RC|RcAcc], SubMap} - end, {[], Subscriptions}, TopicFilters), - {ok, ReasonCodes, Session#session{subscriptions = Subscriptions1}}. +%%-------------------------------------------------------------------- -do_subscribe(Credentials = #{client_id := ClientId}, Topic, SubOpts, SubMap) -> - case maps:find(Topic, SubMap) of - {ok, SubOpts} -> - ok = emqx_hooks:run('session.subscribed', [Credentials, Topic, SubOpts#{first => false}]), - SubMap; - {ok, _SubOpts} -> - emqx_broker:set_subopts(Topic, SubOpts), - %% Why??? - ok = emqx_hooks:run('session.subscribed', [Credentials, Topic, SubOpts#{first => false}]), - maps:put(Topic, SubOpts, SubMap); - error -> - ok = emqx_broker:subscribe(Topic, ClientId, SubOpts), - ok = emqx_hooks:run('session.subscribed', [Credentials, Topic, SubOpts#{first => true}]), - maps:put(Topic, SubOpts, SubMap) +-spec(subscribe(emqx_types:client(), emqx_types:topic(), emqx_types:subopts(), + session()) -> {ok, session()} | {error, emqx_types:reason_code()}). +subscribe(Client, TopicFilter, SubOpts, Session = #session{subscriptions = Subs}) -> + case is_subscriptions_full(Session) + andalso (not maps:is_key(TopicFilter, Subs)) of + true -> {error, ?RC_QUOTA_EXCEEDED}; + false -> + do_subscribe(Client, TopicFilter, SubOpts, Session) end. -%% Client -> Broker: UNSUBSCRIBE --spec(unsubscribe(emqx_types:credentials(), emqx_mqtt:topic_filters(), session()) - -> {ok, list(emqx_mqtt:reason_code()), session()}). -unsubscribe(Credentials, RawTopicFilters, Session = #session{subscriptions = Subscriptions}) - when is_list(RawTopicFilters) -> - TopicFilters = lists:map(fun({RawTopic, Opts}) -> - emqx_topic:parse(RawTopic, Opts); - (RawTopic) when is_binary(RawTopic) -> - emqx_topic:parse(RawTopic) - end, RawTopicFilters), - {ReasonCodes, Subscriptions1} = - lists:foldr(fun({Topic, _SubOpts}, {Acc, SubMap}) -> - case maps:find(Topic, SubMap) of - {ok, SubOpts} -> - ok = emqx_broker:unsubscribe(Topic), - ok = emqx_hooks:run('session.unsubscribed', [Credentials, Topic, SubOpts]), - {[?RC_SUCCESS|Acc], maps:remove(Topic, SubMap)}; - error -> - {[?RC_NO_SUBSCRIPTION_EXISTED|Acc], SubMap} - end - end, {[], Subscriptions}, TopicFilters), - {ok, ReasonCodes, Session#session{subscriptions = Subscriptions1}}. +is_subscriptions_full(#session{max_subscriptions = 0}) -> + false; +is_subscriptions_full(#session{max_subscriptions = MaxLimit, + subscriptions = Subs}) -> + maps:size(Subs) >= MaxLimit. -%% Client -> Broker: QoS2 PUBLISH --spec(publish(emqx_mqtt:packet_id(), emqx_types:message(), session()) - -> {ok, emqx_types:deliver_results(), session()} | {error, emqx_mqtt:reason_code()}). -publish(PacketId, Msg = #message{qos = ?QOS_2, timestamp = Ts}, - Session = #session{awaiting_rel = AwaitingRel, - max_awaiting_rel = MaxAwaitingRel}) -> - case is_awaiting_full(MaxAwaitingRel, AwaitingRel) of +do_subscribe(Client = #{client_id := ClientId}, + TopicFilter, SubOpts, Session = #session{subscriptions = Subs}) -> + case IsNew = (not maps:is_key(TopicFilter, Subs)) of + true -> + ok = emqx_broker:subscribe(TopicFilter, ClientId, SubOpts); false -> - case maps:is_key(PacketId, AwaitingRel) of - false -> - DeliverResults = emqx_broker:publish(Msg), - AwaitingRel1 = maps:put(PacketId, Ts, AwaitingRel), - NSession = Session#session{awaiting_rel = AwaitingRel1}, - {ok, DeliverResults, ensure_await_rel_timer(NSession)}; - true -> - {error, ?RC_PACKET_IDENTIFIER_IN_USE} - end; + _ = emqx_broker:set_subopts(TopicFilter, SubOpts) + end, + ok = emqx_hooks:run('session.subscribed', + [Client, TopicFilter, SubOpts#{new => IsNew}]), + Subs1 = maps:put(TopicFilter, SubOpts, Subs), + {ok, Session#session{subscriptions = Subs1}}. + +%%-------------------------------------------------------------------- +%% Client -> Broker: UNSUBSCRIBE +%%-------------------------------------------------------------------- + +-spec(unsubscribe(emqx_types:client(), emqx_types:topic(), session()) + -> {ok, session()} | {error, emqx_types:reason_code()}). +unsubscribe(Client, TopicFilter, Session = #session{subscriptions = Subs}) -> + case maps:find(TopicFilter, Subs) of + {ok, SubOpts} -> + ok = emqx_broker:unsubscribe(TopicFilter), + ok = emqx_hooks:run('session.unsubscribed', [Client, TopicFilter, SubOpts]), + {ok, Session#session{subscriptions = maps:remove(TopicFilter, Subs)}}; + error -> + {error, ?RC_NO_SUBSCRIPTION_EXISTED} + end. + +%%-------------------------------------------------------------------- +%% Client -> Broker: PUBLISH +%%-------------------------------------------------------------------- + +-spec(publish(emqx_types:packet_id(), emqx_types:message(), session()) + -> {ok, emqx_types:deliver_results()} | + {ok, emqx_types:deliver_results(), session()} | + {error, emqx_types:reason_code()}). +publish(PacketId, Msg = #message{qos = ?QOS_2}, Session) -> + case is_awaiting_full(Session) of + false -> + do_publish(PacketId, Msg, Session); true -> ?LOG(warning, "Dropped qos2 packet ~w for too many awaiting_rel", [PacketId]), ok = emqx_metrics:inc('messages.qos2.dropped'), {error, ?RC_RECEIVE_MAXIMUM_EXCEEDED} end; -%% QoS0/1 -publish(_PacketId, Msg, Session) -> +%% Publish QoS0/1 directly +publish(_PacketId, Msg, _Session) -> {ok, emqx_broker:publish(Msg)}. +is_awaiting_full(#session{max_awaiting_rel = 0}) -> + false; +is_awaiting_full(#session{awaiting_rel = AwaitingRel, + max_awaiting_rel = MaxLimit}) -> + maps:size(AwaitingRel) >= MaxLimit. + +-compile({inline, [do_publish/3]}). +do_publish(PacketId, Msg = #message{timestamp = Ts}, + Session = #session{awaiting_rel = AwaitingRel}) -> + case maps:is_key(PacketId, AwaitingRel) of + false -> + DeliverResults = emqx_broker:publish(Msg), + AwaitingRel1 = maps:put(PacketId, Ts, AwaitingRel), + Session1 = Session#session{awaiting_rel = AwaitingRel1}, + {ok, DeliverResults, ensure_await_rel_timer(Session1)}; + true -> + {error, ?RC_PACKET_IDENTIFIER_IN_USE} + end. + +%%-------------------------------------------------------------------- %% Client -> Broker: PUBACK --spec(puback(emqx_mqtt:packet_id(), emqx_mqtt:reason_code(), session()) - -> puback_ret()). -puback(PacketId, _ReasonCode, Session = #session{inflight = Inflight, mqueue = Q}) -> +%%-------------------------------------------------------------------- + +-spec(puback(emqx_types:packet_id(), emqx_types:reason_code(), session()) + -> {ok, session()} | {error, emqx_types:reason_code()}). +puback(PacketId, _ReasonCode, Session = #session{inflight = Inflight}) -> case emqx_inflight:lookup(PacketId, Inflight) of - {value, {publish, {_, Msg}, _Ts}} -> - %% #{client_id => ClientId, username => Username} - %% ok = emqx_hooks:run('message.acked', [], Msg]), + {value, {Msg, _Ts}} when is_record(Msg, message) -> Inflight1 = emqx_inflight:delete(PacketId, Inflight), - Session1 = Session#session{inflight = Inflight1}, - case (emqx_mqueue:is_empty(Q) orelse emqx_mqueue:out(Q)) of - true -> {ok, Session1}; - {{value, Msg}, Q1} -> - {ok, Msg, Session1#session{mqueue = Q1}} - end; + dequeue(Session#session{inflight = Inflight1}); false -> ?LOG(warning, "The PUBACK PacketId ~w is not found", [PacketId]), ok = emqx_metrics:inc('packets.puback.missed'), {error, ?RC_PACKET_IDENTIFIER_NOT_FOUND} end. +%%-------------------------------------------------------------------- %% Client -> Broker: PUBREC --spec(pubrec(emqx_mqtt:packet_id(), emqx_mqtt:reason_code(), session()) - -> {ok, session()} | {error, emqx_mqtt:reason_code()}). +%%-------------------------------------------------------------------- + +-spec(pubrec(emqx_types:packet_id(), emqx_types:reason_code(), session()) + -> {ok, session()} | {error, emqx_types:reason_code()}). pubrec(PacketId, _ReasonCode, Session = #session{inflight = Inflight}) -> case emqx_inflight:lookup(PacketId, Inflight) of - {value, {publish, {_, Msg}, _Ts}} -> - %% ok = emqx_hooks:run('message.acked', [#{client_id => ClientId, username => Username}, Msg]), - Inflight1 = emqx_inflight:update(PacketId, {pubrel, PacketId, os:timestamp()}, Inflight), + {value, {Msg, _Ts}} when is_record(Msg, message) -> + Inflight1 = emqx_inflight:update(PacketId, {pubrel, os:timestamp()}, Inflight), {ok, Session#session{inflight = Inflight1}}; - {value, {pubrel, PacketId, _Ts}} -> + {value, {pubrel, _Ts}} -> ?LOG(warning, "The PUBREC ~w is duplicated", [PacketId]), {error, ?RC_PACKET_IDENTIFIER_IN_USE}; none -> @@ -362,10 +346,13 @@ pubrec(PacketId, _ReasonCode, Session = #session{inflight = Inflight}) -> {error, ?RC_PACKET_IDENTIFIER_NOT_FOUND} end. +%%-------------------------------------------------------------------- %% Client -> Broker: PUBREL --spec(pubrel(emqx_mqtt:packet_id(), emqx_mqtt:reason_code(), session()) - -> {ok, session()} | {error, emqx_mqtt:reason_code()}). -pubrel(PacketId, ReasonCode, Session = #session{awaiting_rel = AwaitingRel}) -> +%%-------------------------------------------------------------------- + +-spec(pubrel(emqx_types:packet_id(), emqx_types:reason_code(), session()) + -> {ok, session()} | {error, emqx_types:reason_code()}). +pubrel(PacketId, _ReasonCode, Session = #session{awaiting_rel = AwaitingRel}) -> case maps:take(PacketId, AwaitingRel) of {_Ts, AwaitingRel1} -> {ok, Session#session{awaiting_rel = AwaitingRel1}}; @@ -375,18 +362,17 @@ pubrel(PacketId, ReasonCode, Session = #session{awaiting_rel = AwaitingRel}) -> {error, ?RC_PACKET_IDENTIFIER_NOT_FOUND} end. +%%-------------------------------------------------------------------- %% Client -> Broker: PUBCOMP --spec(pubcomp(emqx_mqtt:packet_id(), emqx_mqtt:reason_code(), session()) -> puback_ret()). -pubcomp(PacketId, ReasonCode, Session = #session{inflight = Inflight, mqueue = Q}) -> +%%-------------------------------------------------------------------- + +-spec(pubcomp(emqx_types:packet_id(), emqx_types:reason_code(), session()) + -> {ok, session()} | {error, emqx_types:reason_code()}). +pubcomp(PacketId, _ReasonCode, Session = #session{inflight = Inflight}) -> case emqx_inflight:contain(PacketId, Inflight) of true -> Inflight1 = emqx_inflight:delete(PacketId, Inflight), - Session1 = Session#session{inflight = Inflight1}, - case (emqx_mqueue:is_empty(Q) orelse emqx_mqueue:out(Q)) of - true -> {ok, Session1}; - {{value, Msg}, Q1} -> - {ok, Msg, Session1#session{mqueue = Q1}} - end; + dequeue(Session#session{inflight = Inflight1}); false -> ?LOG(warning, "The PUBCOMP PacketId ~w is not found", [PacketId]), ok = emqx_metrics:inc('packets.pubcomp.missed'), @@ -394,32 +380,59 @@ pubcomp(PacketId, ReasonCode, Session = #session{inflight = Inflight, mqueue = Q end. %%-------------------------------------------------------------------- -%% Handle delivery +%% Dequeue Msgs %%-------------------------------------------------------------------- -deliver(Delivers, Session = #session{subscriptions = SubMap}) +dequeue(Session = #session{inflight = Inflight, mqueue = Q}) -> + case emqx_mqueue:is_empty(Q) of + true -> {ok, Session}; + false -> + {Msgs, Q1} = dequeue(batch_n(Inflight), [], Q), + handle(lists:reverse(Msgs), [], Session#session{mqueue = Q1}) + end. + +dequeue(Cnt, Msgs, Q) when Cnt =< 0 -> + {Msgs, Q}; + +dequeue(Cnt, Msgs, Q) -> + case emqx_mqueue:out(Q) of + {empty, _Q} -> {Msgs, Q}; + {{value, Msg}, Q1} -> + dequeue(Cnt-1, [Msg|Msgs], Q1) + end. + +batch_n(Inflight) -> + case emqx_inflight:max_size(Inflight) of + 0 -> ?DEFAULT_BATCH_N; + Sz -> Sz - emqx_inflight:size(Inflight) + end. + +%%-------------------------------------------------------------------- +%% Broker -> Client: Publish | Msg +%%-------------------------------------------------------------------- + +handle(Delivers, Session = #session{subscriptions = Subs}) when is_list(Delivers) -> - Msgs = [enrich(get_subopts(Topic, SubMap), Msg, Session) - || {Topic, Msg} <- Delivers], - deliver(Msgs, [], Session). + Msgs = [enrich(get_subopts(Topic, Subs), Msg, Session) + || {deliver, Topic, Msg} <- Delivers], + handle(Msgs, [], Session). - -deliver([], Publishes, Session) -> +handle([], Publishes, Session) -> {ok, lists:reverse(Publishes), Session}; -deliver([Msg = #message{qos = ?QOS_0}|More], Acc, Session) -> - deliver(More, [{publish, undefined, Msg}|Acc], Session); +handle([Msg = #message{qos = ?QOS_0}|More], Acc, Session) -> + handle(More, [{publish, undefined, Msg}|Acc], Session); -deliver([Msg = #message{qos = QoS}|More], Acc, - Session = #session{next_pkt_id = PacketId, inflight = Inflight}) +handle([Msg = #message{qos = QoS}|More], Acc, + Session = #session{next_pkt_id = PacketId, inflight = Inflight}) when QoS =:= ?QOS_1 orelse QoS =:= ?QOS_2 -> case emqx_inflight:is_full(Inflight) of true -> - deliver(More, Acc, enqueue(Msg, Session)); + handle(More, Acc, enqueue(Msg, Session)); false -> Publish = {publish, PacketId, Msg}, - NSession = await(PacketId, Msg, Session), - deliver(More, [Publish|Acc], next_pkt_id(NSession)) + Session1 = await(PacketId, Msg, Session), + handle(More, [Publish|Acc], next_pkt_id(Session1)) end. enqueue(Msg, Session = #session{mqueue = Q}) -> @@ -427,19 +440,20 @@ enqueue(Msg, Session = #session{mqueue = Q}) -> {Dropped, NewQ} = emqx_mqueue:in(Msg, Q), if Dropped =/= undefined -> + %% TODO:... %% SessProps = #{client_id => ClientId, username => Username}, ok; %% = emqx_hooks:run('message.dropped', [SessProps, Dropped]); true -> ok end, Session#session{mqueue = NewQ}. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Awaiting ACK for QoS1/QoS2 Messages -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- await(PacketId, Msg, Session = #session{inflight = Inflight}) -> - Publish = {publish, {PacketId, Msg}, os:timestamp()}, - Inflight1 = emqx_inflight:insert(PacketId, Publish, Inflight), + Inflight1 = emqx_inflight:insert( + PacketId, {Msg, os:timestamp()}, Inflight), ensure_retry_timer(Session#session{inflight = Inflight1}). get_subopts(Topic, SubMap) -> @@ -470,11 +484,28 @@ enrich([{rap, _}|Opts], Msg, Session) -> enrich([{subid, SubId}|Opts], Msg, Session) -> enrich(Opts, emqx_message:set_header('Subscription-Identifier', SubId, Msg), Session). +%%-------------------------------------------------------------------- +%% Handle timeout +%%-------------------------------------------------------------------- + +-spec(timeout(reference(), atom(), session()) + -> {ok, session()} | {ok, list(), session()}). +timeout(TRef, retry_delivery, Session = #session{retry_timer = TRef}) -> + retry_delivery(Session#session{retry_timer = undefined}); + +timeout(TRef, check_awaiting_rel, Session = #session{await_rel_timer = TRef}) -> + expire_awaiting_rel(Session); + +timeout(TRef, Msg, Session) -> + ?LOG(error, "unexpected timeout - ~p: ~p", [TRef, Msg]), + {ok, Session}. + %%-------------------------------------------------------------------- %% Ensure retry timer %%-------------------------------------------------------------------- -ensure_retry_timer(Session = #session{retry_interval = Interval, retry_timer = undefined}) -> +ensure_retry_timer(Session = #session{retry_interval = Interval, + retry_timer = undefined}) -> ensure_retry_timer(Interval, Session); ensure_retry_timer(Session) -> Session. @@ -486,13 +517,48 @@ ensure_retry_timer(_Interval, Session) -> Session. %%-------------------------------------------------------------------- -%% Check awaiting rel +%% Retry Delivery %%-------------------------------------------------------------------- -is_awaiting_full(_MaxAwaitingRel = 0, _AwaitingRel) -> - false; -is_awaiting_full(MaxAwaitingRel, AwaitingRel) -> - maps:size(AwaitingRel) >= MaxAwaitingRel. +%% Redeliver at once if force is true +retry_delivery(Session = #session{inflight = Inflight}) -> + case emqx_inflight:is_empty(Inflight) of + true -> {ok, Session}; + false -> + SortFun = fun({_, {_, Ts1}}, {_, {_, Ts2}}) -> Ts1 < Ts2 end, + Msgs = lists:sort(SortFun, emqx_inflight:to_list(Inflight)), + retry_delivery(Msgs, os:timestamp(), [], Session) + end. + +retry_delivery([], _Now, Acc, Session) -> + %% Retry again... + {ok, lists:reverse(Acc), ensure_retry_timer(Session)}; + +retry_delivery([{PacketId, {Val, Ts}}|More], Now, Acc, + Session = #session{retry_interval = Interval, inflight = Inflight}) -> + %% Microseconds -> MilliSeconds + Age = timer:now_diff(Now, Ts) div 1000, + if + Age >= Interval -> + {Acc1, Inflight1} = retry_delivery(PacketId, Val, Now, Acc, Inflight), + retry_delivery(More, Now, Acc1, Session#session{inflight = Inflight1}); + true -> + {ok, lists:reverse(Acc), ensure_retry_timer(Interval - max(0, Age), Session)} + end. + +retry_delivery(PacketId, Msg, Now, Acc, Inflight) when is_record(Msg, message) -> + case emqx_message:is_expired(Msg) of + true -> + ok = emqx_metrics:inc('messages.expired'), + {Acc, emqx_inflight:delete(PacketId, Inflight)}; + false -> + {[{publish, PacketId, Msg}|Acc], + emqx_inflight:update(PacketId, {Msg, Now}, Inflight)} + end; + +retry_delivery(PacketId, pubrel, Now, Acc, Inflight) -> + Inflight1 = emqx_inflight:update(PacketId, {pubrel, Now}, Inflight), + {[{pubrel, PacketId}|Acc], Inflight1}. %%-------------------------------------------------------------------- %% Ensure await_rel timer @@ -516,22 +582,21 @@ ensure_await_rel_timer(_Timeout, Session) -> expire_awaiting_rel(Session = #session{awaiting_rel = AwaitingRel}) -> case maps:size(AwaitingRel) of - 0 -> Session; + 0 -> {ok, Session}; _ -> expire_awaiting_rel(lists:keysort(2, maps:to_list(AwaitingRel)), os:timestamp(), Session) end. expire_awaiting_rel([], _Now, Session) -> - Session#session{await_rel_timer = undefined}; + {ok, Session#session{await_rel_timer = undefined}}; expire_awaiting_rel([{PacketId, Ts} | More], Now, - Session = #session{awaiting_rel = AwaitingRel, - await_rel_timeout = Timeout}) -> + Session = #session{awaiting_rel = AwaitingRel, await_rel_timeout = Timeout}) -> case (timer:now_diff(Now, Ts) div 1000) of Age when Age >= Timeout -> ok = emqx_metrics:inc('messages.qos2.expired'), ?LOG(warning, "Dropped qos2 packet ~s for await_rel_timeout", [PacketId]), - NSession = Session#session{awaiting_rel = maps:remove(PacketId, AwaitingRel)}, - expire_awaiting_rel(More, Now, NSession); + Session1 = Session#session{awaiting_rel = maps:remove(PacketId, AwaitingRel)}, + expire_awaiting_rel(More, Now, Session1); Age -> ensure_await_rel_timer(Timeout - max(0, Age), Session) end. diff --git a/src/emqx_topic.erl b/src/emqx_topic.erl index 86cbcf547..dd187ddf6 100644 --- a/src/emqx_topic.erl +++ b/src/emqx_topic.erl @@ -212,28 +212,27 @@ join(Words) -> end, {true, <<>>}, [bin(W) || W <- Words]), Bin. --spec(parse(topic()) -> {topic(), #{}}). -parse(Topic) when is_binary(Topic) -> - parse(Topic, #{}). +-spec(parse(topic() | {topic(), map()}) -> {topic(), #{share => binary()}}). +parse(TopicFilter) when is_binary(TopicFilter) -> + parse(TopicFilter, #{}); +parse({TopicFilter, Options}) when is_binary(TopicFilter) -> + parse(TopicFilter, Options). -parse(Topic = <<"$queue/", _/binary>>, #{share := _Group}) -> - error({invalid_topic, Topic}); -parse(Topic = <>, #{share := _Group}) -> - error({invalid_topic, Topic}); -parse(<<"$queue/", Topic1/binary>>, Options) -> - parse(Topic1, maps:put(share, <<"$queue">>, Options)); -parse(Topic = <>, Options) -> - case binary:split(Topic1, <<"/">>) of - [<<>>] -> error({invalid_topic, Topic}); - [_] -> error({invalid_topic, Topic}); - [Group, Topic2] -> - case binary:match(Group, [<<"/">>, <<"+">>, <<"#">>]) of - nomatch -> {Topic2, maps:put(share, Group, Options)}; - _ -> error({invalid_topic, Topic}) +parse(TopicFilter = <<"$queue/", _/binary>>, #{share := _Group}) -> + error({invalid_topic_filter, TopicFilter}); +parse(TopicFilter = <>, #{share := _Group}) -> + error({invalid_topic_filter, TopicFilter}); +parse(<<"$queue/", TopicFilter/binary>>, Options) -> + parse(TopicFilter, Options#{share => <<"$queue">>}); +parse(TopicFilter = <>, Options) -> + case binary:split(Rest, <<"/">>) of + [_Any] -> error({invalid_topic_filter, TopicFilter}); + [ShareName, Filter] -> + case binary:match(ShareName, [<<"+">>, <<"#">>]) of + nomatch -> parse(Filter, Options#{share => ShareName}); + _ -> error({invalid_topic_filter, TopicFilter}) end end; -parse(Topic, Options = #{qos := QoS}) -> - {Topic, Options#{rc => QoS}}; -parse(Topic, Options) -> - {Topic, Options}. +parse(TopicFilter, Options) -> + {TopicFilter, Options}. diff --git a/src/emqx_types.erl b/src/emqx_types.erl index 57dfe7e8c..e4ec9ec75 100644 --- a/src/emqx_types.erl +++ b/src/emqx_types.erl @@ -18,23 +18,39 @@ -include("emqx.hrl"). -include("types.hrl"). +-include("emqx_mqtt.hrl"). -export_type([zone/0]). +-export_type([ ver/0 + , qos/0 + , qos_name/0 + ]). + -export_type([ pubsub/0 , topic/0 , subid/0 , subopts/0 ]). --export_type([ client_id/0 +-export_type([ client/0 + , client_id/0 , username/0 , password/0 , peername/0 , protocol/0 ]). --export_type([credentials/0]). +-export_type([ connack/0 + , reason_code/0 + , properties/0 + , topic_filters/0 + ]). + +-export_type([ packet_id/0 + , packet_type/0 + , packet/0 + ]). -export_type([ subscription/0 , subscriber/0 @@ -49,22 +65,56 @@ , deliver_results/0 ]). --export_type([route/0]). - --export_type([ alarm/0 +-export_type([ route/0 + , alarm/0 , plugin/0 , banned/0 , command/0 ]). --type(zone() :: atom()). +-type(zone() :: emqx_zone:zone()). +-type(ver() :: ?MQTT_PROTO_V3 | ?MQTT_PROTO_V4 | ?MQTT_PROTO_V5). +-type(qos() :: ?QOS_0 | ?QOS_1 | ?QOS_2). +-type(qos_name() :: qos0 | at_most_once | + qos1 | at_least_once | + qos2 | exactly_once). + -type(pubsub() :: publish | subscribe). --type(topic() :: binary()). +-type(topic() :: emqx_topic:topic()). -type(subid() :: binary() | atom()). --type(subopts() :: #{qos := emqx_mqtt_types:qos(), + +-type(subopts() :: #{rh := 0 | 1 | 2, + rap := 0 | 1, + nl := 0 | 1, + qos := qos(), + rc => reason_code(), share => binary(), atom() => term() }). + +-type(packet_type() :: ?RESERVED..?AUTH). +-type(connack() :: ?CONNACK_ACCEPT..?CONNACK_AUTH). +-type(reason_code() :: 0..16#FF). +-type(packet_id() :: 1..16#FFFF). +-type(properties() :: #{atom() => term()}). +-type(topic_filters() :: list({emqx_topic:topic(), subopts()})). +-type(packet() :: #mqtt_packet{}). + +-type(client() :: #{zone := zone(), + conn_mod := maybe(module()), + peername := peername(), + sockname := peername(), + client_id := client_id(), + username := username(), + peercert := esockd_peercert:peercert(), + is_superuser := boolean(), + mountpoint := maybe(binary()), + ws_cookie := maybe(list()), + password => maybe(binary()), + auth_result => auth_result(), + anonymous => boolean(), + atom() => term() + }). -type(client_id() :: binary() | atom()). -type(username() :: maybe(binary())). -type(password() :: maybe(binary())). @@ -79,18 +129,6 @@ | banned | bad_authentication_method). -type(protocol() :: mqtt | 'mqtt-sn' | coap | stomp | none | atom()). --type(credentials() :: #{zone := zone(), - client_id := client_id(), - username := username(), - sockname := peername(), - peername := peername(), - ws_cookie := undefined | list(), - mountpoint := binary(), - password => binary(), - auth_result => auth_result(), - anonymous => boolean(), - atom() => term() - }). -type(subscription() :: #subscription{}). -type(subscriber() :: {pid(), subid()}). -type(topic_table() :: [{topic(), subopts()}]). diff --git a/src/emqx_ws_connection.erl b/src/emqx_ws_channel.erl similarity index 51% rename from src/emqx_ws_connection.erl rename to src/emqx_ws_channel.erl index b21897b5e..4da81a2d7 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_channel.erl @@ -14,22 +14,22 @@ %% limitations under the License. %%-------------------------------------------------------------------- -%% MQTT WebSocket Connection --module(emqx_ws_connection). +%% MQTT WebSocket Channel +-module(emqx_ws_channel). -include("emqx.hrl"). -include("emqx_mqtt.hrl"). -include("logger.hrl"). -include("types.hrl"). --logger_header("[WS Conn]"). +-logger_header("[WsChannel]"). -export([ info/1 , attrs/1 , stats/1 ]). -%% websocket callbacks +%% WebSocket callbacks -export([ init/2 , websocket_init/1 , websocket_handle/2 @@ -38,13 +38,15 @@ ]). -record(state, { - request, - options, - peername :: {inet:ip_address(), inet:port_number()}, - sockname :: {inet:ip_address(), inet:port_number()}, + request :: cowboy_req:req(), + options :: proplists:proplist(), + peername :: emqx_types:peername(), + sockname :: emqx_types:peername(), + fsm_state :: idle | connected | disconnected, + serialize :: fun((emqx_types:packet()) -> iodata()), parse_state :: emqx_frame:parse_state(), - packets :: list(emqx_mqtt:packet()), - chan_state :: emqx_channel:channel(), + proto_state :: emqx_protocol:proto_state(), + gc_state :: emqx_gc:gc_state(), keepalive :: maybe(emqx_keepalive:keepalive()), stats_timer :: disabled | maybe(reference()), idle_timeout :: timeout(), @@ -64,14 +66,12 @@ info(WSPid) when is_pid(WSPid) -> info(#state{peername = Peername, sockname = Sockname, - chan_state = ChanState}) -> - ConnInfo = #{socktype => websocket, - conn_state => running, - peername => Peername, - sockname => Sockname - }, - ChanInfo = emqx_channel:info(ChanState), - maps:merge(ConnInfo, ChanInfo). + proto_state = ProtoState}) -> + [{socktype, websocket}, + {conn_state, running}, + {peername, Peername}, + {sockname, Sockname} | + emqx_protocol:info(ProtoState)]. %% for dashboard attrs(WSPid) when is_pid(WSPid) -> @@ -79,11 +79,10 @@ attrs(WSPid) when is_pid(WSPid) -> attrs(#state{peername = Peername, sockname = Sockname, - chan_state = ChanState}) -> - SockAttrs = #{peername => Peername, - sockname => Sockname}, - ChanAttrs = emqx_channel:attrs(ChanState), - maps:merge(SockAttrs, ChanAttrs). + proto_state = ProtoState}) -> + [{peername, Peername}, + {sockname, Sockname} | + emqx_protocol:attrs(ProtoState)]. stats(WSPid) when is_pid(WSPid) -> call(WSPid, stats); @@ -91,12 +90,6 @@ stats(WSPid) when is_pid(WSPid) -> stats(#state{}) -> lists:append([chan_stats(), wsock_stats(), emqx_misc:proc_stats()]). -%%kick(WSPid) when is_pid(WSPid) -> -%% call(WSPid, kick). - -%%session(WSPid) when is_pid(WSPid) -> -%% call(WSPid, session). - call(WSPid, Req) when is_pid(WSPid) -> Mref = erlang:monitor(process, WSPid), WSPid ! {call, {self(), Mref}, Req}, @@ -120,7 +113,7 @@ init(Req, Opts) -> DeflateOptions = maps:from_list(proplists:get_value(deflate_options, Opts, [])), MaxFrameSize = case proplists:get_value(max_frame_size, Opts, 0) of 0 -> infinity; - MFS -> MFS + I -> I end, Compress = proplists:get_value(compress, Opts, false), Options = #{compress => Compress, @@ -151,80 +144,59 @@ websocket_init(#state{request = Req, options = Options}) -> [Error, Reason]), undefined end, - ChanState = emqx_channel:init(#{peername => Peername, - sockname => Sockname, - peercert => Peercert, - ws_cookie => WsCookie, - conn_mod => ?MODULE}, Options), + ProtoState = emqx_protocol:init(#{peername => Peername, + sockname => Sockname, + peercert => Peercert, + ws_cookie => WsCookie, + conn_mod => ?MODULE}, Options), Zone = proplists:get_value(zone, Options), MaxSize = emqx_zone:get_env(Zone, max_packet_size, ?MAX_PACKET_SIZE), ParseState = emqx_frame:initial_parse_state(#{max_size => MaxSize}), + GcPolicy = emqx_zone:get_env(Zone, force_gc_policy, false), + GcState = emqx_gc:init(GcPolicy), EnableStats = emqx_zone:get_env(Zone, enable_stats, true), StatsTimer = if EnableStats -> undefined; ?Otherwise-> disabled end, IdleTimout = emqx_zone:get_env(Zone, idle_timeout, 30000), emqx_logger:set_metadata_peername(esockd_net:format(Peername)), ok = emqx_misc:init_proc_mng_policy(Zone), + %% TODO: Idle timeout? {ok, #state{peername = Peername, sockname = Sockname, + fsm_state = idle, parse_state = ParseState, - chan_state = ChanState, + proto_state = ProtoState, + gc_state = GcState, stats_timer = StatsTimer, idle_timeout = IdleTimout }}. -send_fun(WsPid) -> - fun(Packet, Options) -> - Data = emqx_frame:serialize(Packet, Options), - BinSize = iolist_size(Data), - emqx_pd:update_counter(send_cnt, 1), - emqx_pd:update_counter(send_oct, BinSize), - WsPid ! {binary, iolist_to_binary(Data)}, - {ok, Data} - end. - stat_fun() -> fun() -> {ok, emqx_pd:get_counter(recv_oct)} end. -websocket_handle({binary, <<>>}, State) -> - {ok, ensure_stats_timer(State)}; -websocket_handle({binary, [<<>>]}, State) -> - {ok, ensure_stats_timer(State)}; -websocket_handle({binary, Data}, State = #state{parse_state = ParseState}) -> +websocket_handle({binary, Data}, State) when is_list(Data) -> + websocket_handle({binary, iolist_to_binary(Data)}, State); + +websocket_handle({binary, Data}, State) when is_binary(Data) -> ?LOG(debug, "RECV ~p", [Data]), - BinSize = iolist_size(Data), - emqx_pd:update_counter(recv_oct, BinSize), - ok = emqx_metrics:inc('bytes.received', BinSize), - try emqx_frame:parse(iolist_to_binary(Data), ParseState) of - {ok, NParseState} -> - {ok, State#state{parse_state = NParseState}}; - {ok, Packet, Rest, NParseState} -> - ok = emqx_metrics:inc_recv(Packet), - emqx_pd:update_counter(recv_cnt, 1), - handle_incoming(Packet, fun(NState) -> - websocket_handle({binary, Rest}, NState) - end, - State#state{parse_state = NParseState}); - {error, Reason} -> - ?LOG(error, "Frame error: ~p", [Reason]), - shutdown(Reason, State) - catch - error:Reason:Stk -> - ?LOG(error, "Parse failed for ~p~n\ - Stacktrace:~p~nFrame data: ~p", [Reason, Stk, Data]), - shutdown(parse_error, State) - end; + Oct = iolist_size(Data), + emqx_pd:update_counter(recv_cnt, 1), + emqx_pd:update_counter(recv_oct, Oct), + ok = emqx_metrics:inc('bytes.received', Oct), + NState = maybe_gc(1, Oct, State), + process_incoming(Data, ensure_stats_timer(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, ensure_stats_timer(State)}; + {ok, State}; websocket_handle({FrameType, _}, State) when FrameType =:= ping; FrameType =:= pong -> - {ok, ensure_stats_timer(State)}; + {ok, State}; %% According to mqtt spec[https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901285] -websocket_handle({_OtherFrameType, _}, State) -> - ?LOG(error, "Frame error: Other type of data frame"), - shutdown(other_frame_type, State). +websocket_handle({FrameType, _}, State) -> + ?LOG(error, "Frame error: unexpected frame - ~p", [FrameType]), + shutdown(unexpected_ws_frame, State). websocket_info({call, From, info}, State) -> gen_server:reply(From, info(State)), @@ -242,31 +214,60 @@ websocket_info({call, From, kick}, State) -> gen_server:reply(From, ok), shutdown(kick, State); -websocket_info(Delivery, State = #state{chan_state = ChanState}) - when element(1, Delivery) =:= deliver -> - case emqx_channel:handle_out(Delivery, ChanState) of - {ok, NChanState} -> - {ok, State#state{chan_state = NChanState}}; - {ok, Packet, NChanState} -> - handle_outgoing(Packet, State#state{chan_state = NChanState}); - {error, Reason} -> - shutdown(Reason, State) +websocket_info({incoming, Packet = ?CONNECT_PACKET( + #mqtt_packet_connect{ + proto_ver = ProtoVer} + )}, + State = #state{fsm_state = idle}) -> + State1 = State#state{serialize = serialize_fun(ProtoVer)}, + %% TODO: Fixme later + case handle_incoming(Packet, State1) of + Rep = {reply, _Data, _State} -> + self() ! {enter, connected}, + Rep; + Other -> Other end; -websocket_info({timeout, Timer, emit_stats}, - State = #state{stats_timer = Timer, chan_state = ChanState}) -> - ClientId = emqx_channel:client_id(ChanState), - ok = emqx_cm:set_conn_stats(ClientId, stats(State)), - {ok, State#state{stats_timer = undefined}, hibernate}; +websocket_info({incoming, Packet}, State = #state{fsm_state = idle}) -> + ?LOG(warning, "Unexpected incoming: ~p", [Packet]), + shutdown(unexpected_incoming_packet, State); -websocket_info({keepalive, start, Interval}, State) -> - ?LOG(debug, "Keepalive at the interval of ~p", [Interval]), - case emqx_keepalive:start(stat_fun(), Interval, {keepalive, check}) of +websocket_info({enter, connected}, State = #state{proto_state = ProtoState}) -> + ClientId = emqx_protocol:client_id(ProtoState), + ok = emqx_cm:set_chan_attrs(ClientId, info(State)), + %% Ensure keepalive after connected successfully. + Interval = emqx_protocol:info(keepalive, ProtoState), + State1 = State#state{fsm_state = connected}, + case ensure_keepalive(Interval, State1) of + ignore -> {ok, State1}; {ok, KeepAlive} -> - {ok, State#state{keepalive = KeepAlive}}; - {error, Error} -> - ?LOG(warning, "Keepalive error: ~p", [Error]), - shutdown(Error, State) + {ok, State1#state{keepalive = KeepAlive}}; + {error, Reason} -> + shutdown(Reason, State1) + end; + +websocket_info({incoming, Packet = ?PACKET(?CONNECT)}, + State = #state{fsm_state = connected}) -> + ?LOG(warning, "Unexpected connect: ~p", [Packet]), + shutdown(unexpected_incoming_connect, State); + +websocket_info({incoming, Packet}, State = #state{fsm_state = connected}) + when is_record(Packet, mqtt_packet) -> + handle_incoming(Packet, State); + +websocket_info(Deliver = {deliver, _Topic, _Msg}, + State = #state{proto_state = ProtoState}) -> + Delivers = emqx_misc:drain_deliver([Deliver]), + case emqx_protocol:handle_deliver(Delivers, ProtoState) of + {ok, NProtoState} -> + {ok, State#state{proto_state = NProtoState}}; + {ok, Packets, NProtoState} -> + NState = State#state{proto_state = NProtoState}, + handle_outgoing(Packets, NState); + {error, Reason} -> + shutdown(Reason, State); + {error, Reason, NProtoState} -> + shutdown(Reason, State#state{proto_state = NProtoState}) end; websocket_info({keepalive, check}, State = #state{keepalive = KeepAlive}) -> @@ -281,6 +282,39 @@ websocket_info({keepalive, check}, State = #state{keepalive = KeepAlive}) -> shutdown(keepalive_error, State) end; +websocket_info({timeout, Timer, emit_stats}, + State = #state{stats_timer = Timer, + proto_state = ProtoState, + gc_state = GcState}) -> + ClientId = emqx_protocol:client_id(ProtoState), + ok = emqx_cm:set_chan_stats(ClientId, stats(State)), + NState = State#state{stats_timer = undefined}, + Limits = erlang:get(force_shutdown_policy), + case emqx_misc:conn_proc_mng_policy(Limits) of + continue -> + {ok, NState}; + hibernate -> + %% going to hibernate, reset gc stats + GcState1 = emqx_gc:reset(GcState), + {ok, NState#state{gc_state = GcState1}, hibernate}; + {shutdown, Reason} -> + ?LOG(error, "Shutdown exceptionally due to ~p", [Reason]), + shutdown(Reason, NState) + end; + +websocket_info({timeout, Timer, Msg}, + State = #state{proto_state = ProtoState}) -> + case emqx_protocol:handle_timeout(Timer, Msg, ProtoState) of + {ok, NProtoState} -> + {ok, State#state{proto_state = NProtoState}}; + {ok, Packets, NProtoState} -> + handle_outgoing(Packets, State#state{proto_state = NProtoState}); + {error, Reason} -> + shutdown(Reason, State); + {error, Reason, NProtoState} -> + shutdown(Reason, State#state{proto_state = NProtoState}) + end; + websocket_info({shutdown, discard, {ClientId, ByPid}}, State) -> ?LOG(warning, "Discarded by ~s:~p", [ClientId, ByPid]), shutdown(discard, State); @@ -302,58 +336,123 @@ websocket_info(Info, State) -> ?LOG(error, "Unexpected info: ~p", [Info]), {ok, State}. -terminate(SockError, _Req, #state{keepalive = Keepalive, - chan_state = ChanState, - shutdown = Shutdown}) -> +terminate(SockError, _Req, #state{keepalive = Keepalive, + proto_state = ProtoState, + shutdown = Shutdown}) -> ?LOG(debug, "Terminated for ~p, sockerror: ~p", [Shutdown, SockError]), emqx_keepalive:cancel(Keepalive), - case {ChanState, Shutdown} of + case {ProtoState, Shutdown} of {undefined, _} -> ok; {_, {shutdown, Reason}} -> - emqx_channel:terminate(Reason, ChanState); + emqx_protocol:terminate(Reason, ProtoState); {_, Error} -> - emqx_channel:terminate(Error, ChanState) + emqx_protocol:terminate(Error, ProtoState) end. %%-------------------------------------------------------------------- -%% Internal functions -%%-------------------------------------------------------------------- +%% Ensure keepalive -handle_incoming(Packet = ?PACKET(Type), SuccFun, - State = #state{chan_state = ChanState}) -> +ensure_keepalive(0, _State) -> + ignore; +ensure_keepalive(Interval, State = #state{proto_state = ProtoState}) -> + Backoff = emqx_zone:get_env(emqx_protocol:info(zone, ProtoState), + keepalive_backoff, 0.75), + case emqx_keepalive:start(stat_fun(), round(Interval * Backoff), {keepalive, check}) of + {ok, KeepAlive} -> + {ok, State#state{keepalive = KeepAlive}}; + {error, Error} -> + ?LOG(warning, "Keepalive error: ~p", [Error]), + shutdown(Error, State) + end. + +%%-------------------------------------------------------------------- +%% Process incoming data + +process_incoming(<<>>, State) -> + {ok, State}; + +process_incoming(Data, State = #state{parse_state = ParseState}) -> + try emqx_frame:parse(Data, ParseState) of + {ok, NParseState} -> + {ok, State#state{parse_state = NParseState}}; + {ok, Packet, Rest, NParseState} -> + self() ! {incoming, Packet}, + process_incoming(Rest, State#state{parse_state = NParseState}); + {error, Reason} -> + ?LOG(error, "Frame error: ~p", [Reason]), + shutdown(Reason, State) + catch + error:Reason:Stk -> + ?LOG(error, "Parse failed for ~p~n\ + Stacktrace:~p~nFrame data: ~p", [Reason, Stk, Data]), + shutdown(parse_error, State) + end. + +%%-------------------------------------------------------------------- +%% Handle incoming packets + +handle_incoming(Packet = ?PACKET(Type), State = #state{proto_state = ProtoState}) -> _ = inc_incoming_stats(Type), - case emqx_channel:handle_in(Packet, ChanState) of - {ok, NChanState} -> - SuccFun(State#state{chan_state = NChanState}); - {ok, OutPacket, NChanState} -> - %% TODO: SuccFun, - handle_outgoing(OutPacket, State#state{chan_state = NChanState}); - {error, Reason, NChanState} -> - shutdown(Reason, State#state{chan_state = NChanState}); - {stop, Error, NChanState} -> - shutdown(Error, State#state{chan_state = NChanState}) + ok = emqx_metrics:inc_recv(Packet), + ?LOG(debug, "RECV ~s", [emqx_packet:format(Packet)]), + case emqx_protocol:handle_in(Packet, ProtoState) of + {ok, NProtoState} -> + {ok, State#state{proto_state = NProtoState}}; + {ok, OutPackets, NProtoState} -> + handle_outgoing(OutPackets, State#state{proto_state = NProtoState}); + {error, Reason, NProtoState} -> + shutdown(Reason, State#state{proto_state = NProtoState}); + {stop, Error, NProtoState} -> + shutdown(Error, State#state{proto_state = NProtoState}) end. -handle_outgoing(Packet = ?PACKET(Type), State = #state{chan_state = ChanState}) -> - ProtoVer = emqx_channel:info(proto_ver, ChanState), - Data = emqx_frame:serialize(Packet, ProtoVer), - BinSize = iolist_size(Data), - _ = inc_outgoing_stats(Type, BinSize), - {reply, {binary, Data}, ensure_stats_timer(State)}. +%%-------------------------------------------------------------------- +%% Handle outgoing packets + +handle_outgoing(Packets, State = #state{serialize = Serialize}) + when is_list(Packets) -> + reply(lists:map(Serialize, Packets), State); + +handle_outgoing(Packet, State = #state{serialize = Serialize}) -> + reply(Serialize(Packet), State). + +%%-------------------------------------------------------------------- +%% Serialize fun + +serialize_fun(ProtoVer) -> + fun(Packet = ?PACKET(Type)) -> + ?LOG(debug, "SEND ~s", [emqx_packet:format(Packet)]), + _ = inc_outgoing_stats(Type), + emqx_frame:serialize(Packet, ProtoVer) + end. + +%%-------------------------------------------------------------------- +%% Inc incoming/outgoing stats inc_incoming_stats(Type) -> emqx_pd:update_counter(recv_pkt, 1), (Type == ?PUBLISH) andalso emqx_pd:update_counter(recv_msg, 1). -inc_outgoing_stats(Type, BinSize) -> +inc_outgoing_stats(Type) -> emqx_pd:update_counter(send_cnt, 1), - emqx_pd:update_counter(send_oct, BinSize), emqx_pd:update_counter(send_pkt, 1), (Type == ?PUBLISH) andalso emqx_pd:update_counter(send_msg, 1). +%%-------------------------------------------------------------------- +%% Reply data + +-compile({inline, [reply/2]}). +reply(Data, State) -> + BinSize = iolist_size(Data), + emqx_pd:update_counter(send_oct, BinSize), + {reply, {binary, Data}, State}. + +%%-------------------------------------------------------------------- +%% Ensure stats timer + ensure_stats_timer(State = #state{stats_timer = undefined, idle_timeout = IdleTimeout}) -> TRef = emqx_misc:start_timer(IdleTimeout, emit_stats), @@ -361,6 +460,16 @@ ensure_stats_timer(State = #state{stats_timer = undefined, %% disabled or timer existed ensure_stats_timer(State) -> State. +%%-------------------------------------------------------------------- +%% Maybe GC + +maybe_gc(_Cnt, _Oct, State = #state{gc_state = undefined}) -> + State; +maybe_gc(Cnt, Oct, State = #state{gc_state = GCSt}) -> + {Ok, GCSt1} = emqx_gc:run(Cnt, Oct, GCSt), + Ok andalso emqx_metrics:inc('channel.gc.cnt'), + State#state{gc_state = GCSt1}. + -compile({inline, [shutdown/2]}). shutdown(Reason, State) -> %% Fix the issue#2591(https://github.com/emqx/emqx/issues/2591#issuecomment-500278696) diff --git a/test/emqx_inflight_SUITE.erl b/test/emqx_inflight_SUITE.erl index d0373e11b..533194f1e 100644 --- a/test/emqx_inflight_SUITE.erl +++ b/test/emqx_inflight_SUITE.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_inflight_SUITE). @@ -39,3 +41,4 @@ t_inflight_all(_) -> [1, 2] = emqx_inflight:values(Inflight2), [{a, 1}, {b ,2}] = emqx_inflight:to_list(Inflight2), [a, b] = emqx_inflight:window(Inflight2). + diff --git a/test/emqx_topic_SUITE.erl b/test/emqx_topic_SUITE.erl index 0a51598ee..202f8beb8 100644 --- a/test/emqx_topic_SUITE.erl +++ b/test/emqx_topic_SUITE.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_topic_SUITE). @@ -21,15 +23,15 @@ -compile(nowarn_export_all). -import(emqx_topic, - [wildcard/1, - match/2, - validate/1, - triples/1, - join/1, - words/1, - systop/1, - feed_var/3, - parse/1 + [ wildcard/1 + , match/2 + , validate/1 + , triples/1 + , join/1 + , words/1 + , systop/1 + , feed_var/3 + , parse/1 ]). -define(N, 10000). @@ -218,6 +220,7 @@ long_topic() -> t_parse(_) -> ?assertEqual({<<"a/b/+/#">>, #{}}, parse(<<"a/b/+/#">>)), + ?assertEqual({<<"a/b/+/#">>, #{qos => 1}}, parse({<<"a/b/+/#">>, #{qos => 1}})), ?assertEqual({<<"topic">>, #{ share => <<"$queue">> }}, parse(<<"$queue/topic">>)), ?assertEqual({<<"topic">>, #{ share => <<"group">>}}, parse(<<"$share/group/topic">>)), ?assertEqual({<<"$local/topic">>, #{}}, parse(<<"$local/topic">>)), From 2de4bb0b8ff15e67c7f75809088a1748925b5778 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 25 Jul 2019 09:24:10 +0800 Subject: [PATCH 21/33] Improve the channel modules and add 'attrs/1' API --- src/emqx_channel.erl | 149 ++++++++++++++-------- src/emqx_endpoint.erl | 99 --------------- src/emqx_ws_channel.erl | 268 ++++++++++++++++++++++------------------ 3 files changed, 247 insertions(+), 269 deletions(-) delete mode 100644 src/emqx_endpoint.erl diff --git a/src/emqx_channel.erl b/src/emqx_channel.erl index a3804298b..aa9a9f897 100644 --- a/src/emqx_channel.erl +++ b/src/emqx_channel.erl @@ -24,13 +24,19 @@ -include("logger.hrl"). -include("types.hrl"). +-logger_header("[Channel]"). + -export([start_link/3]). %% APIs -export([ info/1 + , attrs/1 , stats/1 ]). +%% for Debug +-export([state/1]). + %% state callbacks -export([ idle/3 , connected/3 @@ -60,10 +66,12 @@ gc_state :: emqx_gc:gc_state(), keepalive :: maybe(emqx_keepalive:keepalive()), stats_timer :: disabled | maybe(reference()), - idle_timeout :: timeout() - }). + idle_timeout :: timeout(), + connected :: boolean(), + connected_at :: erlang:timestamp() + }). --logger_header("[Channel]"). +-type(state() :: #state{}). -define(ACTIVE_N, 100). -define(HANDLE(T, C, D), handle((T), (C), (D))). @@ -79,55 +87,81 @@ start_link(Transport, Socket, Options) -> %% API %%-------------------------------------------------------------------- -%% @doc Get channel's info. --spec(info(pid() | #state{}) -> proplists:proplist()). +%% @doc Get infos of the channel. +-spec(info(pid() | state()) -> emqx_types:infos()). info(CPid) when is_pid(CPid) -> call(CPid, info); +info(#state{transport = Transport, + socket = Socket, + peername = Peername, + sockname = Sockname, + conn_state = ConnState, + active_n = ActiveN, + rate_limit = RateLimit, + pub_limit = PubLimit, + proto_state = ProtoState, + gc_state = GCState, + stats_timer = StatsTimer, + idle_timeout = IdleTimeout, + connected = Connected, + connected_at = ConnectedAt}) -> + ChanInfo = #{socktype => Transport:type(Socket), + peername => Peername, + sockname => Sockname, + conn_state => ConnState, + active_n => ActiveN, + rate_limit => limit_info(RateLimit), + pub_limit => limit_info(PubLimit), + gc_state => emqx_gc:info(GCState), + enable_stats => case StatsTimer of + disabled -> false; + _Otherwise -> true + end, + idle_timeout => IdleTimeout, + connected => Connected, + connected_at => ConnectedAt + }, + maps:merge(ChanInfo, emqx_protocol:info(ProtoState)). -info(#state{transport = Transport, - socket = Socket, - peername = Peername, - sockname = Sockname, - conn_state = ConnState, - active_n = ActiveN, - rate_limit = RateLimit, - pub_limit = PubLimit, - proto_state = ProtoState, - gc_state = GCState, - stats_timer = StatsTimer, - idle_timeout = IdleTimeout}) -> - [{socktype, Transport:type(Socket)}, - {peername, Peername}, - {sockname, Sockname}, - {conn_state, ConnState}, - {active_n, ActiveN}, - {rate_limit, rate_limit_info(RateLimit)}, - {pub_limit, rate_limit_info(PubLimit)}, - {gc_state, emqx_gc:info(GCState)}, - {enable_stats, case StatsTimer of - disabled -> false; - _Otherwise -> true - end}, - {idle_timeout, IdleTimeout} | - emqx_protocol:info(ProtoState)]. - -rate_limit_info(undefined) -> +limit_info(undefined) -> undefined; -rate_limit_info(Limit) -> +limit_info(Limit) -> esockd_rate_limit:info(Limit). -%% @doc Get channel's stats. --spec(stats(pid() | #state{}) -> proplists:proplist()). +%% @doc Get attrs of the channel. +-spec(attrs(pid() | state()) -> emqx_types:attrs()). +attrs(CPid) when is_pid(CPid) -> + call(CPid, attrs); +attrs(#state{transport = Transport, + socket = Socket, + peername = Peername, + sockname = Sockname, + proto_state = ProtoState, + connected = Connected, + connected_at = ConnectedAt}) -> + ConnAttrs = #{socktype => Transport:type(Socket), + peername => Peername, + sockname => Sockname, + connected => Connected, + connected_at => ConnectedAt}, + maps:merge(ConnAttrs, emqx_protocol:attrs(ProtoState)). + +%% @doc Get stats of the channel. +-spec(stats(pid() | state()) -> emqx_types:stats()). stats(CPid) when is_pid(CPid) -> call(CPid, stats); - -stats(#state{transport = Transport, socket = Socket}) -> +stats(#state{transport = Transport, + socket = Socket, + proto_state = ProtoState}) -> SockStats = case Transport:getstat(Socket, ?SOCK_STATS) of {ok, Ss} -> Ss; {error, _} -> [] end, ChanStats = [{Name, emqx_pd:get_counter(Name)} || Name <- ?CHAN_STATS], - lists:append([SockStats, ChanStats, emqx_misc:proc_stats()]). + SessStats = emqx_session:stats(emqx_protocol:info(session, ProtoState)), + lists:append([SockStats, ChanStats, SessStats, emqx_misc:proc_stats()]). + +state(CPid) -> call(CPid, get_state). %% @private call(CPid, Req) -> @@ -162,6 +196,7 @@ init({Transport, RawSocket, Options}) -> State = #state{transport = Transport, socket = Socket, peername = Peername, + sockname = Sockname, conn_state = running, active_n = ActiveN, rate_limit = RateLimit, @@ -170,7 +205,8 @@ init({Transport, RawSocket, Options}) -> proto_state = ProtoState, gc_state = GcState, stats_timer = StatsTimer, - idle_timeout = IdleTimout + idle_timeout = IdleTimout, + connected = false }, gen_statem:enter_loop(?MODULE, [{hibernate_after, 2 * IdleTimout}], idle, State, self(), [IdleTimout]). @@ -216,16 +252,19 @@ idle(EventType, Content, State) -> %% Connected State connected(enter, _PrevSt, State = #state{proto_state = ProtoState}) -> - ClientId = emqx_protocol:client_id(ProtoState), - ok = emqx_cm:set_chan_attrs(ClientId, info(State)), + NState = State#state{connected = true, + connected_at = os:timestamp()}, + ClientId = emqx_protocol:info(client_id, ProtoState), + ok = emqx_cm:set_chan_attrs(ClientId, attrs(NState)), %% Ensure keepalive after connected successfully. Interval = emqx_protocol:info(keepalive, ProtoState), - case ensure_keepalive(Interval, State) of - ignore -> keep_state_and_data; + case ensure_keepalive(Interval, NState) of + ignore -> + keep_state(NState); {ok, KeepAlive} -> - keep_state(State#state{keepalive = KeepAlive}); + keep_state(NState#state{keepalive = KeepAlive}); {error, Reason} -> - shutdown(Reason, State) + shutdown(Reason, NState) end; connected(cast, {incoming, Packet = ?PACKET(?CONNECT)}, State) -> @@ -279,9 +318,15 @@ disconnected(EventType, Content, State) -> handle({call, From}, info, State) -> reply(From, info(State), State); +handle({call, From}, attrs, State) -> + reply(From, attrs(State), State); + handle({call, From}, stats, State) -> reply(From, stats(State), State); +handle({call, From}, get_state, State) -> + reply(From, State, State); + %%handle({call, From}, kick, State) -> %% ok = gen_statem:reply(From, ok), %% shutdown(kicked, State); @@ -309,12 +354,12 @@ handle(info, {Inet, _Sock, Data}, State) when Inet == tcp; NState = maybe_gc(1, Oct, State), process_incoming(Data, ensure_stats_timer(NState)); -handle(info, {Error, _Sock, Reason}, State) - when Error == tcp_error; Error == ssl_error -> +handle(info, {Error, _Sock, Reason}, State) when Error == tcp_error; + Error == ssl_error -> shutdown(Reason, State); -handle(info, {Closed, _Sock}, State) - when Closed == tcp_closed; Closed == ssl_closed -> +handle(info, {Closed, _Sock}, State) when Closed == tcp_closed; + Closed == ssl_closed -> shutdown(closed, State); handle(info, {Passive, _Sock}, State) when Passive == tcp_passive; @@ -348,7 +393,7 @@ handle(info, {timeout, Timer, emit_stats}, State = #state{stats_timer = Timer, proto_state = ProtoState, gc_state = GcState}) -> - ClientId = emqx_protocol:client_id(ProtoState), + ClientId = emqx_protocol:info(client_id, ProtoState), ok = emqx_cm:set_chan_stats(ClientId, stats(State)), NState = State#state{stats_timer = undefined}, Limits = erlang:get(force_shutdown_policy), @@ -474,7 +519,7 @@ serialize_fun(ProtoVer) -> ?LOG(debug, "SEND ~s", [emqx_packet:format(Packet)]), _ = inc_outgoing_stats(Type), emqx_frame:serialize(Packet, ProtoVer) - end. + end. %%-------------------------------------------------------------------- %% Send data diff --git a/src/emqx_endpoint.erl b/src/emqx_endpoint.erl deleted file mode 100644 index dee80a099..000000000 --- a/src/emqx_endpoint.erl +++ /dev/null @@ -1,99 +0,0 @@ -%%-------------------------------------------------------------------- -%% 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_endpoint). - --include("types.hrl"). - -%% APIs --export([ new/0 - , new/1 - , info/1 - ]). - --export([ zone/1 - , client_id/1 - , mountpoint/1 - , is_superuser/1 - , credentials/1 - ]). - --export([update/2]). - --export([to_map/1]). - --export_type([endpoint/0]). - --type(st() :: #{zone := emqx_types:zone(), - conn_mod := maybe(module()), - peername := emqx_types:peername(), - sockname := emqx_types:peername(), - client_id := emqx_types:client_id(), - username := emqx_types:username(), - peercert := esockd_peercert:peercert(), - is_superuser := boolean(), - mountpoint := maybe(binary()), - ws_cookie := maybe(list()), - password => binary(), - auth_result => emqx_types:auth_result(), - anonymous => boolean(), - atom() => term() - }). - --opaque(endpoint() :: {endpoint, st()}). - --define(Endpoint(St), {endpoint, St}). - --define(Default, #{is_superuser => false, - anonymous => false - }). - --spec(new() -> endpoint()). -new() -> - ?Endpoint(?Default). - --spec(new(map()) -> endpoint()). -new(M) when is_map(M) -> - ?Endpoint(maps:merge(?Default, M)). - -info(?Endpoint(M)) -> - maps:to_list(M). - --spec(zone(endpoint()) -> emqx_zone:zone()). -zone(?Endpoint(#{zone := Zone})) -> - Zone. - -client_id(?Endpoint(#{client_id := ClientId})) -> - ClientId. - --spec(mountpoint(endpoint()) -> maybe(binary())). -mountpoint(?Endpoint(#{mountpoint := Mountpoint})) -> - Mountpoint; -mountpoint(_) -> undefined. - -is_superuser(?Endpoint(#{is_superuser := B})) -> - B. - -update(Attrs, ?Endpoint(M)) -> - ?Endpoint(maps:merge(M, Attrs)). - -credentials(?Endpoint(M)) -> - M. %% TODO: ... - --spec(to_map(endpoint()) -> map()). -to_map(?Endpoint(M)) -> - M. - diff --git a/src/emqx_ws_channel.erl b/src/emqx_ws_channel.erl index 4da81a2d7..3c04e4cea 100644 --- a/src/emqx_ws_channel.erl +++ b/src/emqx_ws_channel.erl @@ -38,8 +38,6 @@ ]). -record(state, { - request :: cowboy_req:req(), - options :: proplists:proplist(), peername :: emqx_types:peername(), sockname :: emqx_types:peername(), fsm_state :: idle | connected | disconnected, @@ -48,10 +46,15 @@ proto_state :: emqx_protocol:proto_state(), gc_state :: emqx_gc:gc_state(), keepalive :: maybe(emqx_keepalive:keepalive()), + pendings :: list(), stats_timer :: disabled | maybe(reference()), idle_timeout :: timeout(), - shutdown - }). + connected :: boolean(), + connected_at :: erlang:timestamp(), + reason :: term() + }). + +-type(state() :: #state{}). -define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt]). -define(CHAN_STATS, [recv_pkt, recv_msg, send_pkt, send_msg]). @@ -60,36 +63,57 @@ %% API %%-------------------------------------------------------------------- -%% for debug +-spec(info(pid() | state()) -> emqx_types:infos()). info(WSPid) when is_pid(WSPid) -> call(WSPid, info); - info(#state{peername = Peername, sockname = Sockname, - proto_state = ProtoState}) -> - [{socktype, websocket}, - {conn_state, running}, - {peername, Peername}, - {sockname, Sockname} | - emqx_protocol:info(ProtoState)]. + proto_state = ProtoState, + gc_state = GCState, + stats_timer = StatsTimer, + idle_timeout = IdleTimeout, + connected = Connected, + connected_at = ConnectedAt}) -> + ChanInfo = #{socktype => websocket, + peername => Peername, + sockname => Sockname, + conn_state => running, + gc_state => emqx_gc:info(GCState), + enable_stats => enable_stats(StatsTimer), + idle_timeout => IdleTimeout, + connected => Connected, + connected_at => ConnectedAt + }, + maps:merge(ChanInfo, emqx_protocol:info(ProtoState)). -%% for dashboard +enable_stats(disabled) -> false; +enable_stats(_MaybeRef) -> true. + +-spec(attrs(pid() | state()) -> emqx_types:attrs()). attrs(WSPid) when is_pid(WSPid) -> call(WSPid, attrs); - attrs(#state{peername = Peername, sockname = Sockname, - proto_state = ProtoState}) -> - [{peername, Peername}, - {sockname, Sockname} | - emqx_protocol:attrs(ProtoState)]. + proto_state = ProtoState, + connected = Connected, + connected_at = ConnectedAt}) -> + ConnAttrs = #{socktype => websocket, + peername => Peername, + sockname => Sockname, + connected => Connected, + connected_at => ConnectedAt + }, + maps:merge(ConnAttrs, emqx_protocol:attrs(ProtoState)). +-spec(stats(pid() | state()) -> emqx_types:stats()). stats(WSPid) when is_pid(WSPid) -> call(WSPid, stats); +stats(#state{proto_state = ProtoState}) -> + ProcStats = emqx_misc:proc_stats(), + SessStats = emqx_session:stats(emqx_protocol:info(session, ProtoState)), + lists:append([ProcStats, SessStats, chan_stats(), wsock_stats()]). -stats(#state{}) -> - lists:append([chan_stats(), wsock_stats(), emqx_misc:proc_stats()]). - +%% @private call(WSPid, Req) when is_pid(WSPid) -> Mref = erlang:monitor(process, WSPid), WSPid ! {call, {self(), Mref}, Req}, @@ -116,21 +140,24 @@ init(Req, Opts) -> I -> I end, Compress = proplists:get_value(compress, Opts, false), - Options = #{compress => Compress, - deflate_opts => DeflateOptions, - max_frame_size => MaxFrameSize, - idle_timeout => IdleTimeout}, + WsOpts = #{compress => Compress, + deflate_opts => DeflateOptions, + max_frame_size => MaxFrameSize, + idle_timeout => IdleTimeout + }, case cowboy_req:parse_header(<<"sec-websocket-protocol">>, Req) of undefined -> - {cowboy_websocket, Req, #state{}, Options}; + %% TODO: why not reply 500??? + {cowboy_websocket, Req, [Req, Opts], WsOpts}; [<<"mqtt", Vsn/binary>>] -> - Resp = cowboy_req:set_resp_header(<<"sec-websocket-protocol">>, <<"mqtt", Vsn/binary>>, Req), - {cowboy_websocket, Resp, #state{request = Req, options = Opts}, Options}; + Resp = cowboy_req:set_resp_header( + <<"sec-websocket-protocol">>, <<"mqtt", Vsn/binary>>, Req), + {cowboy_websocket, Resp, [Req, Opts], WsOpts}; _ -> {ok, cowboy_req:reply(400, Req), #state{}} end. -websocket_init(#state{request = Req, options = Options}) -> +websocket_init([Req, Opts]) -> Peername = cowboy_req:peer(Req), Sockname = cowboy_req:sock(Req), Peercert = cowboy_req:cert(Req), @@ -148,8 +175,8 @@ websocket_init(#state{request = Req, options = Options}) -> sockname => Sockname, peercert => Peercert, ws_cookie => WsCookie, - conn_mod => ?MODULE}, Options), - Zone = proplists:get_value(zone, Options), + conn_mod => ?MODULE}, Opts), + Zone = proplists:get_value(zone, Opts), MaxSize = emqx_zone:get_env(Zone, max_packet_size, ?MAX_PACKET_SIZE), ParseState = emqx_frame:initial_parse_state(#{max_size => MaxSize}), GcPolicy = emqx_zone:get_env(Zone, force_gc_policy, false), @@ -159,15 +186,16 @@ websocket_init(#state{request = Req, options = Options}) -> IdleTimout = emqx_zone:get_env(Zone, idle_timeout, 30000), emqx_logger:set_metadata_peername(esockd_net:format(Peername)), ok = emqx_misc:init_proc_mng_policy(Zone), - %% TODO: Idle timeout? {ok, #state{peername = Peername, sockname = Sockname, fsm_state = idle, parse_state = ParseState, proto_state = ProtoState, gc_state = GcState, + pendings = [], stats_timer = StatsTimer, - idle_timeout = IdleTimout + idle_timeout = IdleTimout, + connected = false }}. stat_fun() -> @@ -196,7 +224,7 @@ websocket_handle({FrameType, _}, State) %% According to mqtt spec[https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901285] websocket_handle({FrameType, _}, State) -> ?LOG(error, "Frame error: unexpected frame - ~p", [FrameType]), - shutdown(unexpected_ws_frame, State). + stop(unexpected_ws_frame, State). websocket_info({call, From, info}, State) -> gen_server:reply(From, info(State)), @@ -212,62 +240,41 @@ websocket_info({call, From, stats}, State) -> websocket_info({call, From, kick}, State) -> gen_server:reply(From, ok), - shutdown(kick, State); + stop(kick, State); websocket_info({incoming, Packet = ?CONNECT_PACKET( #mqtt_packet_connect{ proto_ver = ProtoVer} )}, State = #state{fsm_state = idle}) -> - State1 = State#state{serialize = serialize_fun(ProtoVer)}, - %% TODO: Fixme later - case handle_incoming(Packet, State1) of - Rep = {reply, _Data, _State} -> - self() ! {enter, connected}, - Rep; - Other -> Other - end; + handle_incoming(Packet, fun connected/1, + State#state{serialize = serialize_fun(ProtoVer)}); websocket_info({incoming, Packet}, State = #state{fsm_state = idle}) -> ?LOG(warning, "Unexpected incoming: ~p", [Packet]), - shutdown(unexpected_incoming_packet, State); - -websocket_info({enter, connected}, State = #state{proto_state = ProtoState}) -> - ClientId = emqx_protocol:client_id(ProtoState), - ok = emqx_cm:set_chan_attrs(ClientId, info(State)), - %% Ensure keepalive after connected successfully. - Interval = emqx_protocol:info(keepalive, ProtoState), - State1 = State#state{fsm_state = connected}, - case ensure_keepalive(Interval, State1) of - ignore -> {ok, State1}; - {ok, KeepAlive} -> - {ok, State1#state{keepalive = KeepAlive}}; - {error, Reason} -> - shutdown(Reason, State1) - end; + stop(unexpected_incoming_packet, State); websocket_info({incoming, Packet = ?PACKET(?CONNECT)}, State = #state{fsm_state = connected}) -> ?LOG(warning, "Unexpected connect: ~p", [Packet]), - shutdown(unexpected_incoming_connect, State); + stop(unexpected_incoming_connect, State); websocket_info({incoming, Packet}, State = #state{fsm_state = connected}) when is_record(Packet, mqtt_packet) -> - handle_incoming(Packet, State); + handle_incoming(Packet, fun reply/1, State); -websocket_info(Deliver = {deliver, _Topic, _Msg}, +websocket_info(Deliver = {deliver, _Topic, _Msg}, State = #state{proto_state = ProtoState}) -> Delivers = emqx_misc:drain_deliver([Deliver]), case emqx_protocol:handle_deliver(Delivers, ProtoState) of {ok, NProtoState} -> - {ok, State#state{proto_state = NProtoState}}; + reply(State#state{proto_state = NProtoState}); {ok, Packets, NProtoState} -> - NState = State#state{proto_state = NProtoState}, - handle_outgoing(Packets, NState); + reply(Packets, State#state{proto_state = NProtoState}); {error, Reason} -> - shutdown(Reason, State); + stop(Reason, State); {error, Reason, NProtoState} -> - shutdown(Reason, State#state{proto_state = NProtoState}) + stop(Reason, State#state{proto_state = NProtoState}) end; websocket_info({keepalive, check}, State = #state{keepalive = KeepAlive}) -> @@ -276,17 +283,17 @@ websocket_info({keepalive, check}, State = #state{keepalive = KeepAlive}) -> {ok, State#state{keepalive = KeepAlive1}}; {error, timeout} -> ?LOG(debug, "Keepalive Timeout!"), - shutdown(keepalive_timeout, State); + stop(keepalive_timeout, State); {error, Error} -> ?LOG(error, "Keepalive error: ~p", [Error]), - shutdown(keepalive_error, State) + stop(keepalive_error, State) end; websocket_info({timeout, Timer, emit_stats}, State = #state{stats_timer = Timer, proto_state = ProtoState, gc_state = GcState}) -> - ClientId = emqx_protocol:client_id(ProtoState), + ClientId = emqx_protocol:info(client_id, ProtoState), ok = emqx_cm:set_chan_stats(ClientId, stats(State)), NState = State#state{stats_timer = undefined}, Limits = erlang:get(force_shutdown_policy), @@ -299,7 +306,7 @@ websocket_info({timeout, Timer, emit_stats}, {ok, NState#state{gc_state = GcState1}, hibernate}; {shutdown, Reason} -> ?LOG(error, "Shutdown exceptionally due to ~p", [Reason]), - shutdown(Reason, NState) + stop(Reason, NState) end; websocket_info({timeout, Timer, Msg}, @@ -308,29 +315,29 @@ websocket_info({timeout, Timer, Msg}, {ok, NProtoState} -> {ok, State#state{proto_state = NProtoState}}; {ok, Packets, NProtoState} -> - handle_outgoing(Packets, State#state{proto_state = NProtoState}); + reply(Packets, State#state{proto_state = NProtoState}); {error, Reason} -> - shutdown(Reason, State); + stop(Reason, State); {error, Reason, NProtoState} -> - shutdown(Reason, State#state{proto_state = NProtoState}) + stop(Reason, State#state{proto_state = NProtoState}) end; websocket_info({shutdown, discard, {ClientId, ByPid}}, State) -> ?LOG(warning, "Discarded by ~s:~p", [ClientId, ByPid]), - shutdown(discard, State); + stop(discard, State); websocket_info({shutdown, conflict, {ClientId, NewPid}}, State) -> ?LOG(warning, "Clientid '~s' conflict with ~p", [ClientId, NewPid]), - shutdown(conflict, State); + stop(conflict, State); %% websocket_info({binary, Data}, State) -> %% {reply, {binary, Data}, State}; websocket_info({shutdown, Reason}, State) -> - shutdown(Reason, State); + stop(Reason, State); websocket_info({stop, Reason}, State) -> - {stop, State#state{shutdown = Reason}}; + stop(Reason, State); websocket_info(Info, State) -> ?LOG(error, "Unexpected info: ~p", [Info]), @@ -338,16 +345,31 @@ websocket_info(Info, State) -> terminate(SockError, _Req, #state{keepalive = Keepalive, proto_state = ProtoState, - shutdown = Shutdown}) -> + reason = Reason}) -> ?LOG(debug, "Terminated for ~p, sockerror: ~p", - [Shutdown, SockError]), + [Reason, SockError]), emqx_keepalive:cancel(Keepalive), - case {ProtoState, Shutdown} of - {undefined, _} -> ok; - {_, {shutdown, Reason}} -> - emqx_protocol:terminate(Reason, ProtoState); - {_, Error} -> - emqx_protocol:terminate(Error, ProtoState) + emqx_protocol:terminate(Reason, ProtoState). + +%%-------------------------------------------------------------------- +%% Connected callback + +connected(State = #state{proto_state = ProtoState}) -> + NState = State#state{fsm_state = connected, + connected = true, + connected_at = os:timestamp() + }, + ClientId = emqx_protocol:info(client_id, ProtoState), + ok = emqx_cm:set_chan_attrs(ClientId, info(NState)), + %% Ensure keepalive after connected successfully. + Interval = emqx_protocol:info(keepalive, ProtoState), + case ensure_keepalive(Interval, NState) of + ignore -> + reply(NState); + {ok, KeepAlive} -> + reply(NState#state{keepalive = KeepAlive}); + {error, Reason} -> + stop(Reason, NState) end. %%-------------------------------------------------------------------- @@ -361,9 +383,9 @@ ensure_keepalive(Interval, State = #state{proto_state = ProtoState}) -> case emqx_keepalive:start(stat_fun(), round(Interval * Backoff), {keepalive, check}) of {ok, KeepAlive} -> {ok, State#state{keepalive = KeepAlive}}; - {error, Error} -> - ?LOG(warning, "Keepalive error: ~p", [Error]), - shutdown(Error, State) + {error, Reason} -> + ?LOG(warning, "Keepalive error: ~p", [Reason]), + stop(Reason, State) end. %%-------------------------------------------------------------------- @@ -381,41 +403,46 @@ process_incoming(Data, State = #state{parse_state = ParseState}) -> process_incoming(Rest, State#state{parse_state = NParseState}); {error, Reason} -> ?LOG(error, "Frame error: ~p", [Reason]), - shutdown(Reason, State) + stop(Reason, State) catch error:Reason:Stk -> ?LOG(error, "Parse failed for ~p~n\ Stacktrace:~p~nFrame data: ~p", [Reason, Stk, Data]), - shutdown(parse_error, State) + stop(parse_error, State) end. %%-------------------------------------------------------------------- %% Handle incoming packets -handle_incoming(Packet = ?PACKET(Type), State = #state{proto_state = ProtoState}) -> +handle_incoming(Packet = ?PACKET(Type), SuccFun, + State = #state{proto_state = ProtoState, + pendings = Pendings}) -> _ = inc_incoming_stats(Type), ok = emqx_metrics:inc_recv(Packet), ?LOG(debug, "RECV ~s", [emqx_packet:format(Packet)]), case emqx_protocol:handle_in(Packet, ProtoState) of {ok, NProtoState} -> - {ok, State#state{proto_state = NProtoState}}; + SuccFun(State#state{proto_state = NProtoState}); {ok, OutPackets, NProtoState} -> - handle_outgoing(OutPackets, State#state{proto_state = NProtoState}); + Pendings1 = lists:append(Pendings, OutPackets), + SuccFun(State#state{proto_state = NProtoState, + pendings = Pendings1}); {error, Reason, NProtoState} -> - shutdown(Reason, State#state{proto_state = NProtoState}); + stop(Reason, State#state{proto_state = NProtoState}); {stop, Error, NProtoState} -> - shutdown(Error, State#state{proto_state = NProtoState}) + stop(Error, State#state{proto_state = NProtoState}) end. %%-------------------------------------------------------------------- %% Handle outgoing packets -handle_outgoing(Packets, State = #state{serialize = Serialize}) - when is_list(Packets) -> - reply(lists:map(Serialize, Packets), State); +handle_outgoing(Packet, State) when is_tuple(Packet) -> + handle_outgoing([Packet], State); -handle_outgoing(Packet, State = #state{serialize = Serialize}) -> - reply(Serialize(Packet), State). +handle_outgoing(Packets, #state{serialize = Serialize}) -> + Data = lists:map(Serialize, Packets), + emqx_pd:update_counter(send_oct, iolist_size(Data)), + {binary, Data}. %%-------------------------------------------------------------------- %% Serialize fun @@ -442,13 +469,24 @@ inc_outgoing_stats(Type) -> andalso emqx_pd:update_counter(send_msg, 1). %%-------------------------------------------------------------------- -%% Reply data +%% Reply or Stop --compile({inline, [reply/2]}). -reply(Data, State) -> - BinSize = iolist_size(Data), - emqx_pd:update_counter(send_oct, BinSize), - {reply, {binary, Data}, State}. +reply(Packets, State = #state{pendings = Pendings}) -> + Pendings1 = lists:append(Pendings, Packets), + reply(State#state{pendings = Pendings1}). + +reply(State = #state{pendings = []}) -> + {ok, State}; +reply(State = #state{pendings = Pendings}) -> + Reply = handle_outgoing(Pendings, State), + {reply, Reply, State#state{pendings = []}}. + +stop(Reason, State = #state{pendings = []}) -> + {stop, State#state{reason = Reason}}; +stop(Reason, State = #state{pendings = Pendings}) -> + Reply = handle_outgoing(Pendings, State), + {reply, [Reply, close], + State#state{pendings = [], reason = Reason}}. %%-------------------------------------------------------------------- %% Ensure stats timer @@ -460,6 +498,12 @@ ensure_stats_timer(State = #state{stats_timer = undefined, %% disabled or timer existed ensure_stats_timer(State) -> State. +wsock_stats() -> + [{Key, emqx_pd:get_counter(Key)} || Key <- ?SOCK_STATS]. + +chan_stats() -> + [{Name, emqx_pd:get_counter(Name)} || Name <- ?CHAN_STATS]. + %%-------------------------------------------------------------------- %% Maybe GC @@ -470,15 +514,3 @@ maybe_gc(Cnt, Oct, State = #state{gc_state = GCSt}) -> Ok andalso emqx_metrics:inc('channel.gc.cnt'), State#state{gc_state = GCSt1}. --compile({inline, [shutdown/2]}). -shutdown(Reason, State) -> - %% Fix the issue#2591(https://github.com/emqx/emqx/issues/2591#issuecomment-500278696) - %% self() ! {stop, Reason}, - {stop, State#state{shutdown = Reason}}. - -wsock_stats() -> - [{Key, emqx_pd:get_counter(Key)} || Key <- ?SOCK_STATS]. - -chan_stats() -> - [{Name, emqx_pd:get_counter(Name)} || Name <- ?CHAN_STATS]. - From a0a2375810f6c265bde6ec5c9fbe837ccdac777e Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 25 Jul 2019 09:25:00 +0800 Subject: [PATCH 22/33] Update the .app file --- src/emqx.app.src | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/emqx.app.src b/src/emqx.app.src index c88be22c7..cf70b5031 100644 --- a/src/emqx.app.src +++ b/src/emqx.app.src @@ -1,12 +1,14 @@ -{application,emqx, - [{description,"EMQ X Broker"}, - {vsn,"git"}, - {modules,[]}, - {registered,[emqx_sup]}, - {applications,[kernel,stdlib,jsx,gproc,gen_rpc,esockd,cowboy, - replayq,sasl,os_mon]}, - {env,[]}, - {mod,{emqx_app,[]}}, - {maintainers,["Feng Lee "]}, - {licenses,["Apache-2.0"]}, - {links,[{"Github","https://github.com/emqx/emqx"}]}]}. +{application, emqx, [ + {id, "emqx"}, + {vsn, "git"}, + {description, "EMQ X Broker"}, + {modules, []}, + {registered, []}, + {applications, [kernel,stdlib,jsx,gproc,gen_rpc,esockd,cowboy, + replayq,sasl,os_mon]}, + {env, []}, + {mod, {emqx_app,[]}}, + {maintainers, ["Feng Lee "]}, + {licenses, ["Apache-2.0"]}, + {links, [{"Github", "https://github.com/emqx/emqx"}]} +]}. From 5b3a61b7999fcd6da75c02a1d4068e1f7da6520b Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 25 Jul 2019 09:25:45 +0800 Subject: [PATCH 23/33] Merge the connection and session tabs into channel tab --- src/emqx_cm.erl | 179 ++++++++++++------------------------------------ 1 file changed, 43 insertions(+), 136 deletions(-) diff --git a/src/emqx_cm.erl b/src/emqx_cm.erl index e756a37e5..b1e3982fe 100644 --- a/src/emqx_cm.erl +++ b/src/emqx_cm.erl @@ -32,13 +32,13 @@ , unregister_channel/2 ]). --export([ get_conn_attrs/1 - , get_conn_attrs/2 +-export([ get_chan_attrs/1 + , get_chan_attrs/2 , set_chan_attrs/2 ]). --export([ get_conn_stats/1 - , get_conn_stats/2 +-export([ get_chan_stats/1 + , get_chan_stats/2 , set_chan_stats/2 ]). @@ -47,16 +47,6 @@ , resume_session/1 ]). --export([ get_session_attrs/1 - , get_session_attrs/2 - , set_session_attrs/2 - ]). - --export([ get_session_stats/1 - , get_session_stats/2 - , set_session_stats/2 - ]). - -export([ lookup_channels/1 , lookup_channels/2 ]). @@ -73,29 +63,19 @@ %% Internal export -export([stats_fun/0]). --export_type([attrs/0, stats/0]). - -type(chan_pid() :: pid()). --opaque(attrs() :: #{atom() => term()}). - --opaque(stats() :: #{atom() => integer()}). - %% Tables for channel management. -define(CHAN_TAB, emqx_channel). +-define(CHAN_P_TAB, emqx_channel_p). +-define(CHAN_ATTRS_TAB, emqx_channel_attrs). +-define(CHAN_STATS_TAB, emqx_channel_stats). --define(CONN_TAB, emqx_connection). - --define(SESSION_TAB, emqx_session). - --define(SESSION_P_TAB, emqx_session_p). - -%% Chan stats -define(CHAN_STATS, [{?CHAN_TAB, 'channels.count', 'channels.max'}, - {?CONN_TAB, 'connections.count', 'connections.max'}, - {?SESSION_TAB, 'sessions.count', 'sessions.max'}, - {?SESSION_P_TAB, 'sessions.persistent.count', 'sessions.persistent.max'} + {?CHAN_TAB, 'connections.count', 'connections.max'}, + {?CHAN_TAB, 'sessions.count', 'sessions.max'}, + {?CHAN_P_TAB, 'sessions.persistent.count', 'sessions.persistent.max'} ]). %% Batch drain @@ -118,6 +98,7 @@ start_link() -> register_channel(ClientId) when is_binary(ClientId) -> register_channel(ClientId, self()). +%% @doc Register a channel with pid. -spec(register_channel(emqx_types:client_id(), chan_pid()) -> ok). register_channel(ClientId, ChanPid) -> Chan = {ClientId, ChanPid}, @@ -139,66 +120,51 @@ unregister_channel(ClientId, ChanPid) -> %% @private do_unregister_channel(Chan) -> ok = emqx_cm_registry:unregister_channel(Chan), - true = ets:delete_object(?SESSION_P_TAB, Chan), - true = ets:delete(?SESSION_TAB, Chan), - true = ets:delete(?CONN_TAB, Chan), + true = ets:delete_object(?CHAN_P_TAB, Chan), + true = ets:delete(?CHAN_ATTRS_TAB, Chan), + true = ets:delete(?CHAN_STATS_TAB, Chan), ets:delete_object(?CHAN_TAB, Chan). -%% @doc Get conn attrs. --spec(get_conn_attrs(emqx_types:client_id()) -> maybe(attrs())). -get_conn_attrs(ClientId) -> - with_channel(ClientId, fun(ChanPid) -> - get_conn_attrs(ClientId, ChanPid) - end). +%% @doc Get attrs of a channel. +-spec(get_chan_attrs(emqx_types:client_id()) -> maybe(emqx_types:attrs())). +get_chan_attrs(ClientId) -> + with_channel(ClientId, fun(ChanPid) -> get_chan_attrs(ClientId, ChanPid) end). --spec(get_conn_attrs(emqx_types:client_id(), chan_pid()) -> maybe(attrs())). -get_conn_attrs(ClientId, ChanPid) when node(ChanPid) == node() -> +-spec(get_chan_attrs(emqx_types:client_id(), chan_pid()) -> maybe(emqx_types:attrs())). +get_chan_attrs(ClientId, ChanPid) when node(ChanPid) == node() -> Chan = {ClientId, ChanPid}, - try ets:lookup_element(?CONN_TAB, Chan, 2) of - Attrs -> Attrs - catch - error:badarg -> undefined - end; -get_conn_attrs(ClientId, ChanPid) -> - rpc_call(node(ChanPid), get_conn_attrs, [ClientId, ChanPid]). + emqx_tables:lookup_value(?CHAN_ATTRS_TAB, Chan); +get_chan_attrs(ClientId, ChanPid) -> + rpc_call(node(ChanPid), get_chan_attrs, [ClientId, ChanPid]). -%% @doc Set conn attrs. --spec(set_chan_attrs(emqx_types:client_id(), attrs()) -> ok). +%% @doc Set attrs of a channel. +-spec(set_chan_attrs(emqx_types:client_id(), emqx_types:attrs()) -> ok). set_chan_attrs(ClientId, Attrs) when is_binary(ClientId) -> Chan = {ClientId, self()}, - case ets:update_element(?CONN_TAB, Chan, {2, Attrs}) of - true -> ok; - false -> true = ets:insert(?CONN_TAB, {Chan, Attrs, #{}}), - ok - end. + true = ets:insert(?CHAN_ATTRS_TAB, {Chan, Attrs}), + ok. -%% @doc Get conn stats. --spec(get_conn_stats(emqx_types:client_id()) -> maybe(stats())). -get_conn_stats(ClientId) -> - with_channel(ClientId, fun(ChanPid) -> - get_conn_stats(ClientId, ChanPid) - end). +%% @doc Get channel's stats. +-spec(get_chan_stats(emqx_types:client_id()) -> maybe(emqx_types:stats())). +get_chan_stats(ClientId) -> + with_channel(ClientId, fun(ChanPid) -> get_chan_stats(ClientId, ChanPid) end). --spec(get_conn_stats(emqx_types:client_id(), chan_pid()) -> maybe(stats())). -get_conn_stats(ClientId, ChanPid) when node(ChanPid) == node() -> +-spec(get_chan_stats(emqx_types:client_id(), chan_pid()) -> maybe(emqx_types:stats())). +get_chan_stats(ClientId, ChanPid) when node(ChanPid) == node() -> Chan = {ClientId, ChanPid}, - try ets:lookup_element(?CONN_TAB, Chan, 3) of - Stats -> Stats - catch - error:badarg -> undefined - end; -get_conn_stats(ClientId, ChanPid) -> - rpc_call(node(ChanPid), get_conn_stats, [ClientId, ChanPid]). + emqx_tables:lookup_value(?CHAN_STATS_TAB, Chan); +get_chan_stats(ClientId, ChanPid) -> + rpc_call(node(ChanPid), get_chan_stats, [ClientId, ChanPid]). -%% @doc Set conn stats. --spec(set_chan_stats(emqx_types:client_id(), stats()) -> ok). +%% @doc Set channel's stats. +-spec(set_chan_stats(emqx_types:client_id(), emqx_types:stats()) -> ok). set_chan_stats(ClientId, Stats) when is_binary(ClientId) -> set_chan_stats(ClientId, self(), Stats). --spec(set_chan_stats(emqx_types:client_id(), chan_pid(), stats()) -> ok). +-spec(set_chan_stats(emqx_types:client_id(), chan_pid(), emqx_types:stats()) -> ok). set_chan_stats(ClientId, ChanPid, Stats) -> Chan = {ClientId, ChanPid}, - _ = ets:update_element(?CONN_TAB, Chan, {3, Stats}), + true = ets:insert(?CHAN_STATS_TAB, {Chan, Stats}), ok. %% @doc Open a session. @@ -257,69 +223,10 @@ discard_session(ClientId) when is_binary(ClientId) -> end, ChanPids) end. -%% @doc Get session attrs. --spec(get_session_attrs(emqx_types:client_id()) -> attrs()). -get_session_attrs(ClientId) -> - with_channel(ClientId, fun(ChanPid) -> - get_session_attrs(ClientId, ChanPid) - end). - --spec(get_session_attrs(emqx_types:client_id(), chan_pid()) -> maybe(attrs())). -get_session_attrs(ClientId, ChanPid) when node(ChanPid) == node() -> - Chan = {ClientId, ChanPid}, - try ets:lookup_element(?SESSION_TAB, Chan, 2) of - Attrs -> Attrs - catch - error:badarg -> undefined - end; -get_session_attrs(ClientId, ChanPid) -> - rpc_call(node(ChanPid), get_session_attrs, [ClientId, ChanPid]). - -%% @doc Set session attrs. --spec(set_session_attrs(emqx_types:client_id(), attrs()) -> ok). -set_session_attrs(ClientId, Attrs) when is_binary(ClientId) -> - Chan = {ClientId, self()}, - case ets:update_element(?SESSION_TAB, Chan, {2, Attrs}) of - true -> ok; - false -> - true = ets:insert(?SESSION_TAB, {Chan, Attrs, #{}}), - is_clean_start(Attrs) orelse ets:insert(?SESSION_P_TAB, Chan), - ok - end. - %% @doc Is clean start? is_clean_start(#{clean_start := false}) -> false; is_clean_start(_Attrs) -> true. -%% @doc Get session stats. --spec(get_session_stats(emqx_types:client_id()) -> stats()). -get_session_stats(ClientId) -> - with_channel(ClientId, fun(ChanPid) -> - get_session_stats(ClientId, ChanPid) - end). - --spec(get_session_stats(emqx_types:client_id(), chan_pid()) -> maybe(stats())). -get_session_stats(ClientId, ChanPid) when node(ChanPid) == node() -> - Chan = {ClientId, ChanPid}, - try ets:lookup_element(?SESSION_TAB, Chan, 3) of - Stats -> Stats - catch - error:badarg -> undefined - end; -get_session_stats(ClientId, ChanPid) -> - rpc_call(node(ChanPid), get_session_stats, [ClientId, ChanPid]). - -%% @doc Set session stats. --spec(set_session_stats(emqx_types:client_id(), stats()) -> ok). -set_session_stats(ClientId, Stats) when is_binary(ClientId) -> - set_session_stats(ClientId, self(), Stats). - --spec(set_session_stats(emqx_types:client_id(), chan_pid(), stats()) -> ok). -set_session_stats(ClientId, ChanPid, Stats) -> - Chan = {ClientId, ChanPid}, - _ = ets:update_element(?SESSION_TAB, Chan, {3, Stats}), - ok. - with_channel(ClientId, Fun) -> case lookup_channels(ClientId) of [] -> undefined; @@ -362,9 +269,9 @@ cast(Msg) -> gen_server:cast(?CM, Msg). init([]) -> TabOpts = [public, {write_concurrency, true}], ok = emqx_tables:new(?CHAN_TAB, [bag, {read_concurrency, true} | TabOpts]), - ok = emqx_tables:new(?CONN_TAB, [set, compressed | TabOpts]), - ok = emqx_tables:new(?SESSION_TAB, [set, compressed | TabOpts]), - ok = emqx_tables:new(?SESSION_P_TAB, [bag | TabOpts]), + ok = emqx_tables:new(?CHAN_P_TAB, [bag | TabOpts]), + ok = emqx_tables:new(?CHAN_ATTRS_TAB, [set, compressed | TabOpts]), + ok = emqx_tables:new(?CHAN_STATS_TAB, [set | TabOpts]), ok = emqx_stats:update_interval(chan_stats, fun ?MODULE:stats_fun/0), {ok, #{chan_pmon => emqx_pmon:new()}}. From b4a0a1c228896a7da1696bbb41c1e15889b1e0f7 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 25 Jul 2019 09:26:02 +0800 Subject: [PATCH 24/33] Fix whitespace --- src/emqx_misc.erl | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/emqx_misc.erl b/src/emqx_misc.erl index fdc7c5dc3..42e88d850 100644 --- a/src/emqx_misc.erl +++ b/src/emqx_misc.erl @@ -75,9 +75,12 @@ proc_stats() -> -spec(proc_stats(pid()) -> list()). proc_stats(Pid) -> - Stats = process_info(Pid, [message_queue_len, heap_size, reductions]), - {value, {_, V}, Stats1} = lists:keytake(message_queue_len, 1, Stats), - [{mailbox_len, V} | Stats1]. + case process_info(Pid, [message_queue_len, heap_size, + total_heap_size, reductions, memory]) of + undefined -> []; + [{message_queue_len, Len}|Stats] -> + [{mailbox_len, Len}|Stats] + end. -define(DISABLED, 0). @@ -145,12 +148,11 @@ drain_down(Cnt) when Cnt > 0 -> drain_down(0, Acc) -> lists:reverse(Acc); - drain_down(Cnt, Acc) -> receive {'DOWN', _MRef, process, Pid, _Reason} -> drain_down(Cnt - 1, [Pid|Acc]) after 0 -> - drain_down(0, Acc) + drain_down(0, Acc) end. From 64148ac0e8faf790572241fb628d52c1df32f14a Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 25 Jul 2019 09:26:36 +0800 Subject: [PATCH 25/33] Improve the 'info/1', 'attrs/1' and 'stats/1' APIs --- src/emqx_protocol.erl | 100 +++++++++++++++++++++++++----------------- src/emqx_session.erl | 98 +++++++++++++++++++++++++---------------- src/emqx_types.erl | 10 ++++- 3 files changed, 128 insertions(+), 80 deletions(-) diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 9bbb63bf4..ea052f05f 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -27,11 +27,6 @@ -export([ info/1 , info/2 , attrs/1 - , stats/1 - ]). - --export([ client_id/1 - , session/1 ]). -export([ init/2 @@ -45,48 +40,75 @@ -export_type([proto_state/0]). -record(protocol, { - proto_name :: binary(), - proto_ver :: emqx_types:version(), - client :: emqx_types:client(), - session :: emqx_session:session(), - mountfun :: fun((emqx_topic:topic()) -> emqx_topic:topic()), - keepalive :: non_neg_integer(), - will_msg :: emqx_types:message(), - enable_acl :: boolean(), - is_bridge :: boolean(), - connected :: boolean(), - connected_at :: erlang:timestamp(), + proto_name :: binary(), + proto_ver :: emqx_types:version(), + client :: emqx_types:client(), + session :: emqx_session:session(), + mountfun :: fun((emqx_topic:topic()) -> emqx_topic:topic()), + keepalive :: non_neg_integer(), + will_msg :: emqx_types:message(), + enable_acl :: boolean(), + is_bridge :: boolean(), topic_aliases :: map(), alias_maximum :: map() - }). + }). -opaque(proto_state() :: #protocol{}). -info(#protocol{client = Client, session = Session}) -> - lists:append([maps:to_list(Client), emqx_session:info(Session)]). +-spec(info(proto_state()) -> emqx_types:infos()). +info(#protocol{proto_name = ProtoName, + proto_ver = ProtoVer, + client = Client, + session = Session, + keepalive = Keepalive, + will_msg = WillMsg, + enable_acl = EnableAcl, + is_bridge = IsBridge, + topic_aliases = Aliases}) -> + #{proto_name => ProtoName, + proto_ver => ProtoVer, + client => Client, + session => emqx_session:info(Session), + keepalive => Keepalive, + will_msg => WillMsg, + enable_acl => EnableAcl, + is_bridge => IsBridge, + topic_aliases => Aliases + }. -info(zone, #protocol{client = #{zone := Zone}}) -> - Zone; +-spec(info(atom(), proto_state()) -> term()). info(proto_name, #protocol{proto_name = ProtoName}) -> ProtoName; info(proto_ver, #protocol{proto_ver = ProtoVer}) -> ProtoVer; +info(client, #protocol{client = Client}) -> + Client; +info(zone, #protocol{client = #{zone := Zone}}) -> + Zone; +info(client_id, #protocol{client = #{client_id := ClientId}}) -> + ClientId; +info(session, #protocol{session = Session}) -> + Session; info(keepalive, #protocol{keepalive = Keepalive}) -> - Keepalive. + Keepalive; +info(is_bridge, #protocol{is_bridge = IsBridge}) -> + IsBridge; +info(topic_aliases, #protocol{topic_aliases = Aliases}) -> + Aliases. -attrs(#protocol{}) -> - #{}. - -stats(#protocol{}) -> - []. - --spec(client_id(proto_state()) -> emqx_types:client_id()). -client_id(#protocol{client = #{client_id := ClientId}}) -> - ClientId. - --spec(session(proto_state()) -> emqx_session:session()). -session(#protocol{session = Session}) -> - Session. +attrs(#protocol{proto_name = ProtoName, + proto_ver = ProtoVer, + client = Client, + session = Session, + keepalive = Keepalive, + is_bridge = IsBridge}) -> + #{proto_name => ProtoName, + proto_ver => ProtoVer, + client => Client, + session => emqx_session:attrs(Session), + keepalive => Keepalive, + is_bridge => IsBridge + }. -spec(init(map(), proplists:proplist()) -> proto_state()). init(ConnInfo, Options) -> @@ -105,8 +127,7 @@ init(ConnInfo, Options) -> #protocol{client = Client, mountfun = MountFun, enable_acl = EnableAcl, - is_bridge = false, - connected = false + is_bridge = false }. peer_cert_as_username(Peercert, Options) -> @@ -382,10 +403,7 @@ handle_connect(#mqtt_packet_connect{proto_name = ProtoName, case open_session(ConnPkt, PState) of {ok, Session, SP} -> PState1 = PState#protocol{client = Client1, - session = Session, - connected = true, - connected_at = os:timestamp() - }, + session = Session}, ok = emqx_cm:register_channel(ClientId), {ok, SP, PState1}; {error, Error} -> diff --git a/src/emqx_session.erl b/src/emqx_session.erl index e5978142b..95f8b3727 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -48,9 +48,12 @@ -include("logger.hrl"). -include("types.hrl"). +-logger_header("[Session]"). + -export([init/1]). -export([ info/1 + , attrs/1 , stats/1 ]). @@ -80,12 +83,12 @@ %% Clean Start Flag clean_start :: boolean(), - %% Max subscriptions allowed - max_subscriptions :: non_neg_integer(), - %% Client’s Subscriptions. subscriptions :: map(), + %% Max subscriptions allowed + max_subscriptions :: non_neg_integer(), + %% Upgrade QoS? upgrade_qos :: boolean(), @@ -112,12 +115,12 @@ %% Inflight QoS2 messages received from client and waiting for pubrel. awaiting_rel :: map(), - %% Awaiting PUBREL Timer - await_rel_timer :: maybe(reference()), - %% Max Packets Awaiting PUBREL max_awaiting_rel :: non_neg_integer(), + %% Awaiting PUBREL Timer + await_rel_timer :: maybe(reference()), + %% Awaiting PUBREL Timeout await_rel_timeout :: timeout(), @@ -133,8 +136,6 @@ -opaque(session() :: #session{}). --logger_header("[Session]"). - -define(DEFAULT_BATCH_N, 1000). %%-------------------------------------------------------------------- @@ -170,10 +171,10 @@ init_mqueue(Zone) -> }). %%-------------------------------------------------------------------- -%% Info, Stats of Session +%% Infos of the session %%-------------------------------------------------------------------- --spec(info(session()) -> proplists:proplist()). +-spec(info(session()) -> emqx_types:infos()). info(#session{clean_start = CleanStart, max_subscriptions = MaxSubscriptions, subscriptions = Subscriptions, @@ -187,37 +188,58 @@ info(#session{clean_start = CleanStart, await_rel_timeout = AwaitRelTimeout, expiry_interval = ExpiryInterval, created_at = CreatedAt}) -> - [{clean_start, CleanStart}, - {max_subscriptions, MaxSubscriptions}, - {subscriptions, Subscriptions}, - {upgrade_qos, UpgradeQoS}, - {inflight, Inflight}, - {retry_interval, RetryInterval}, - {mqueue_len, emqx_mqueue:len(MQueue)}, - {next_pkt_id, PacketId}, - {awaiting_rel, AwaitingRel}, - {max_awaiting_rel, MaxAwaitingRel}, - {await_rel_timeout, AwaitRelTimeout}, - {expiry_interval, ExpiryInterval div 1000}, - {created_at, CreatedAt}]. + #{clean_start => CleanStart, + subscriptions => Subscriptions, + max_subscriptions => MaxSubscriptions, + upgrade_qos => UpgradeQoS, + inflight => emqx_inflight:size(Inflight), + max_inflight => emqx_inflight:max_size(Inflight), + retry_interval => RetryInterval, + mqueue_len => emqx_mqueue:len(MQueue), + max_mqueue => emqx_mqueue:max_len(MQueue), + mqueue_dropped => emqx_mqueue:dropped(MQueue), + next_pkt_id => PacketId, + awaiting_rel => maps:size(AwaitingRel), + max_awaiting_rel => MaxAwaitingRel, + await_rel_timeout => AwaitRelTimeout, + expiry_interval => ExpiryInterval div 1000, + created_at => CreatedAt + }. -%% @doc Get session stats. --spec(stats(session()) -> list({atom(), non_neg_integer()})). -stats(#session{max_subscriptions = MaxSubscriptions, - subscriptions = Subscriptions, +%%-------------------------------------------------------------------- +%% Attrs of the session +%%-------------------------------------------------------------------- + +-spec(attrs(session()) -> emqx_types:attrs()). +attrs(#session{clean_start = CleanStart, + expiry_interval = ExpiryInterval, + created_at = CreatedAt}) -> + #{clean_start => CleanStart, + expiry_interval => ExpiryInterval, + created_at => CreatedAt + }. + +%%-------------------------------------------------------------------- +%% Stats of the session +%%-------------------------------------------------------------------- + +%% @doc Get stats of the session. +-spec(stats(session()) -> emqx_types:stats()). +stats(#session{subscriptions = Subscriptions, + max_subscriptions = MaxSubscriptions, inflight = Inflight, mqueue = MQueue, - max_awaiting_rel = MaxAwaitingRel, - awaiting_rel = AwaitingRel}) -> - [{max_subscriptions, MaxSubscriptions}, - {subscriptions_count, maps:size(Subscriptions)}, - {max_inflight, emqx_inflight:max_size(Inflight)}, - {inflight_len, emqx_inflight:size(Inflight)}, - {max_mqueue, emqx_mqueue:max_len(MQueue)}, - {mqueue_len, emqx_mqueue:len(MQueue)}, - {mqueue_dropped, emqx_mqueue:dropped(MQueue)}, - {max_awaiting_rel, MaxAwaitingRel}, - {awaiting_rel_len, maps:size(AwaitingRel)}]. + awaiting_rel = AwaitingRel, + max_awaiting_rel = MaxAwaitingRel}) -> + [{subscriptions, maps:size(Subscriptions)}, + {max_subscriptions, MaxSubscriptions}, + {inflight, emqx_inflight:size(Inflight)}, + {max_inflight, emqx_inflight:max_size(Inflight)}, + {mqueue_len, emqx_mqueue:len(MQueue)}, + {max_mqueue, emqx_mqueue:max_len(MQueue)}, + {mqueue_dropped, emqx_mqueue:dropped(MQueue)}, + {awaiting_rel, maps:size(AwaitingRel)}, + {max_awaiting_rel, MaxAwaitingRel}]. %%-------------------------------------------------------------------- %% Client -> Broker: SUBSCRIBE diff --git a/src/emqx_types.erl b/src/emqx_types.erl index e4ec9ec75..490284039 100644 --- a/src/emqx_types.erl +++ b/src/emqx_types.erl @@ -72,6 +72,11 @@ , command/0 ]). +-export_type([ infos/0 + , attrs/0 + , stats/0 + ]). + -type(zone() :: emqx_zone:zone()). -type(ver() :: ?MQTT_PROTO_V3 | ?MQTT_PROTO_V4 | ?MQTT_PROTO_V5). -type(qos() :: ?QOS_0 | ?QOS_1 | ?QOS_2). @@ -87,7 +92,6 @@ rap := 0 | 1, nl := 0 | 1, qos := qos(), - rc => reason_code(), share => binary(), atom() => term() }). @@ -143,3 +147,7 @@ -type(plugin() :: #plugin{}). -type(command() :: #command{}). +-type(infos() :: #{atom() => term()}). +-type(attrs() :: #{atom() => term()}). +-type(stats() :: list({atom(), non_neg_integer()})). + From 32b2a01d685f1e7a207f8dcb585a1965498bb44a Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 26 Jul 2019 00:18:25 +0800 Subject: [PATCH 26/33] Fix function_clause error and improve ws channel --- src/emqx_channel.erl | 3 +-- src/emqx_ws_channel.erl | 37 ++++++++++++------------------------- 2 files changed, 13 insertions(+), 27 deletions(-) diff --git a/src/emqx_channel.erl b/src/emqx_channel.erl index aa9a9f897..9c908c9bf 100644 --- a/src/emqx_channel.erl +++ b/src/emqx_channel.erl @@ -259,8 +259,7 @@ connected(enter, _PrevSt, State = #state{proto_state = ProtoState}) -> %% Ensure keepalive after connected successfully. Interval = emqx_protocol:info(keepalive, ProtoState), case ensure_keepalive(Interval, NState) of - ignore -> - keep_state(NState); + ignore -> keep_state(NState); {ok, KeepAlive} -> keep_state(NState#state{keepalive = KeepAlive}); {error, Reason} -> diff --git a/src/emqx_ws_channel.erl b/src/emqx_ws_channel.erl index 3c04e4cea..1c21c29d8 100644 --- a/src/emqx_ws_channel.erl +++ b/src/emqx_ws_channel.erl @@ -270,7 +270,7 @@ websocket_info(Deliver = {deliver, _Topic, _Msg}, {ok, NProtoState} -> reply(State#state{proto_state = NProtoState}); {ok, Packets, NProtoState} -> - reply(Packets, State#state{proto_state = NProtoState}); + reply(enqueue(Packets, State#state{proto_state = NProtoState})); {error, Reason} -> stop(Reason, State); {error, Reason, NProtoState} -> @@ -282,7 +282,6 @@ websocket_info({keepalive, check}, State = #state{keepalive = KeepAlive}) -> {ok, KeepAlive1} -> {ok, State#state{keepalive = KeepAlive1}}; {error, timeout} -> - ?LOG(debug, "Keepalive Timeout!"), stop(keepalive_timeout, State); {error, Error} -> ?LOG(error, "Keepalive error: ~p", [Error]), @@ -315,7 +314,7 @@ websocket_info({timeout, Timer, Msg}, {ok, NProtoState} -> {ok, State#state{proto_state = NProtoState}}; {ok, Packets, NProtoState} -> - reply(Packets, State#state{proto_state = NProtoState}); + reply(enqueue(Packets, State#state{proto_state = NProtoState})); {error, Reason} -> stop(Reason, State); {error, Reason, NProtoState} -> @@ -364,8 +363,7 @@ connected(State = #state{proto_state = ProtoState}) -> %% Ensure keepalive after connected successfully. Interval = emqx_protocol:info(keepalive, ProtoState), case ensure_keepalive(Interval, NState) of - ignore -> - reply(NState); + ignore -> reply(NState); {ok, KeepAlive} -> reply(NState#state{keepalive = KeepAlive}); {error, Reason} -> @@ -377,16 +375,10 @@ connected(State = #state{proto_state = ProtoState}) -> ensure_keepalive(0, _State) -> ignore; -ensure_keepalive(Interval, State = #state{proto_state = ProtoState}) -> +ensure_keepalive(Interval, #state{proto_state = ProtoState}) -> Backoff = emqx_zone:get_env(emqx_protocol:info(zone, ProtoState), keepalive_backoff, 0.75), - case emqx_keepalive:start(stat_fun(), round(Interval * Backoff), {keepalive, check}) of - {ok, KeepAlive} -> - {ok, State#state{keepalive = KeepAlive}}; - {error, Reason} -> - ?LOG(warning, "Keepalive error: ~p", [Reason]), - stop(Reason, State) - end. + emqx_keepalive:start(stat_fun(), round(Interval * Backoff), {keepalive, check}). %%-------------------------------------------------------------------- %% Process incoming data @@ -415,8 +407,7 @@ process_incoming(Data, State = #state{parse_state = ParseState}) -> %% Handle incoming packets handle_incoming(Packet = ?PACKET(Type), SuccFun, - State = #state{proto_state = ProtoState, - pendings = Pendings}) -> + State = #state{proto_state = ProtoState}) -> _ = inc_incoming_stats(Type), ok = emqx_metrics:inc_recv(Packet), ?LOG(debug, "RECV ~s", [emqx_packet:format(Packet)]), @@ -424,9 +415,7 @@ handle_incoming(Packet = ?PACKET(Type), SuccFun, {ok, NProtoState} -> SuccFun(State#state{proto_state = NProtoState}); {ok, OutPackets, NProtoState} -> - Pendings1 = lists:append(Pendings, OutPackets), - SuccFun(State#state{proto_state = NProtoState, - pendings = Pendings1}); + SuccFun(enqueue(OutPackets, State#state{proto_state = NProtoState})); {error, Reason, NProtoState} -> stop(Reason, State#state{proto_state = NProtoState}); {stop, Error, NProtoState} -> @@ -436,9 +425,6 @@ handle_incoming(Packet = ?PACKET(Type), SuccFun, %%-------------------------------------------------------------------- %% Handle outgoing packets -handle_outgoing(Packet, State) when is_tuple(Packet) -> - handle_outgoing([Packet], State); - handle_outgoing(Packets, #state{serialize = Serialize}) -> Data = lists:map(Serialize, Packets), emqx_pd:update_counter(send_oct, iolist_size(Data)), @@ -471,10 +457,6 @@ inc_outgoing_stats(Type) -> %%-------------------------------------------------------------------- %% Reply or Stop -reply(Packets, State = #state{pendings = Pendings}) -> - Pendings1 = lists:append(Pendings, Packets), - reply(State#state{pendings = Pendings1}). - reply(State = #state{pendings = []}) -> {ok, State}; reply(State = #state{pendings = Pendings}) -> @@ -488,6 +470,11 @@ stop(Reason, State = #state{pendings = Pendings}) -> {reply, [Reply, close], State#state{pendings = [], reason = Reason}}. +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)}. + %%-------------------------------------------------------------------- %% Ensure stats timer From d99c9daf763746711ecbf772421ecfaa75ba1c21 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sat, 27 Jul 2019 23:22:02 +0800 Subject: [PATCH 27/33] Replace credentials with client --- src/emqx_access_control.erl | 81 ++++++++++++++++++++----------------- src/emqx_access_rule.erl | 66 ++++++++++++++---------------- 2 files changed, 74 insertions(+), 73 deletions(-) diff --git a/src/emqx_access_control.erl b/src/emqx_access_control.erl index 7127c8d33..847f801ec 100644 --- a/src/emqx_access_control.erl +++ b/src/emqx_access_control.erl @@ -28,52 +28,57 @@ %% APIs %%-------------------------------------------------------------------- --spec(authenticate(emqx_types:credentials()) - -> {ok, emqx_types:credentials()} | {error, term()}). -authenticate(Credentials) -> - case emqx_hooks:run_fold('client.authenticate', [], init_auth_result(Credentials)) of - #{auth_result := success, anonymous := true} = NewCredentials -> +-spec(authenticate(emqx_types:client()) + -> {ok, #{auth_result := emqx_types:auth_result(), + anonymous := boolean}} | {error, term()}). +authenticate(Client = #{zone := Zone}) -> + case emqx_hooks:run_fold('client.authenticate', + [Client], default_auth_result(Zone)) of + Result = #{auth_result := success, anonymous := true} -> emqx_metrics:inc('auth.mqtt.anonymous'), - {ok, NewCredentials}; - #{auth_result := success} = NewCredentials -> - {ok, NewCredentials}; - NewCredentials -> - {error, maps:get(auth_result, NewCredentials, unknown_error)} - end. - -%% @doc Check ACL --spec(check_acl(emqx_types:credentials(), emqx_types:pubsub(), emqx_types:topic()) - -> allow | deny). -check_acl(Credentials, PubSub, Topic) -> - case emqx_acl_cache:is_enabled() of - false -> - do_check_acl(Credentials, PubSub, Topic); - true -> - case emqx_acl_cache:get_acl_cache(PubSub, Topic) of - not_found -> - AclResult = do_check_acl(Credentials, PubSub, Topic), - emqx_acl_cache:put_acl_cache(PubSub, Topic, AclResult), - AclResult; - AclResult -> AclResult - end + {ok, Result}; + Result = #{auth_result := success} -> + {ok, Result}; + Result -> + {error, maps:get(auth_result, Result, unknown_error)} end. -do_check_acl(#{zone := Zone} = Credentials, PubSub, Topic) -> - case emqx_hooks:run_fold('client.check_acl', [Credentials, PubSub, Topic], - emqx_zone:get_env(Zone, acl_nomatch, deny)) of - allow -> allow; - _ -> deny +%% @doc Check ACL +-spec(check_acl(emqx_types:cient(), emqx_types:pubsub(), emqx_types:topic()) + -> allow | deny). +check_acl(Client, PubSub, Topic) -> + case emqx_acl_cache:is_enabled() of + true -> + check_acl_cache(Client, PubSub, Topic); + false -> + do_check_acl(Client, PubSub, Topic) + end. + +check_acl_cache(Client, PubSub, Topic) -> + case emqx_acl_cache:get_acl_cache(PubSub, Topic) of + not_found -> + AclResult = do_check_acl(Client, PubSub, Topic), + emqx_acl_cache:put_acl_cache(PubSub, Topic, AclResult), + AclResult; + AclResult -> AclResult + end. + +do_check_acl(#{zone := Zone} = Client, PubSub, Topic) -> + Default = emqx_zone:get_env(Zone, acl_nomatch, deny), + case emqx_hooks:run_fold('client.check_acl', [Client, PubSub, Topic], Default) of + allow -> allow; + _Other -> deny end. -spec(reload_acl() -> ok | {error, term()}). reload_acl() -> - emqx_acl_cache:is_enabled() andalso - emqx_acl_cache:empty_acl_cache(), + emqx_acl_cache:is_enabled() + andalso emqx_acl_cache:empty_acl_cache(), emqx_mod_acl_internal:reload_acl(). -init_auth_result(Credentials) -> - case emqx_zone:get_env(maps:get(zone, Credentials, undefined), allow_anonymous, false) of - true -> Credentials#{auth_result => success, anonymous => true}; - false -> Credentials#{auth_result => not_authorized, anonymous => false} +default_auth_result(Zone) -> + case emqx_zone:get_env(Zone, allow_anonymous, false) of + true -> #{auth_result => success, anonymous => true}; + false -> #{auth_result => not_authorized, anonymous => false} end. diff --git a/src/emqx_access_rule.erl b/src/emqx_access_rule.erl index d56710a21..b9d166030 100644 --- a/src/emqx_access_rule.erl +++ b/src/emqx_access_rule.erl @@ -40,10 +40,6 @@ -define(ALLOW_DENY(A), ((A =:= allow) orelse (A =:= deny))). -define(PUBSUB(A), ((A =:= subscribe) orelse (A =:= publish) orelse (A =:= pubsub))). -%%-------------------------------------------------------------------- -%% APIs -%%-------------------------------------------------------------------- - %% @doc Compile Access Rule. compile({A, all}) when ?ALLOW_DENY(A) -> {A, all}; @@ -92,21 +88,21 @@ bin(B) when is_binary(B) -> %% @doc Match access rule -spec(match(emqx_types:credentials(), emqx_types:topic(), rule()) -> {matched, allow} | {matched, deny} | nomatch). -match(_Credentials, _Topic, {AllowDeny, all}) when ?ALLOW_DENY(AllowDeny) -> +match(_Client, _Topic, {AllowDeny, all}) when ?ALLOW_DENY(AllowDeny) -> {matched, AllowDeny}; -match(Credentials, Topic, {AllowDeny, Who, _PubSub, TopicFilters}) +match(Client, Topic, {AllowDeny, Who, _PubSub, TopicFilters}) when ?ALLOW_DENY(AllowDeny) -> - case match_who(Credentials, Who) - andalso match_topics(Credentials, Topic, TopicFilters) of + case match_who(Client, Who) + andalso match_topics(Client, Topic, TopicFilters) of true -> {matched, AllowDeny}; false -> nomatch end. -match_who(_Credentials, all) -> +match_who(_Client, all) -> true; -match_who(_Credentials, {user, all}) -> +match_who(_Client, {user, all}) -> true; -match_who(_Credentials, {client, all}) -> +match_who(_Client, {client, all}) -> true; match_who(#{client_id := ClientId}, {client, ClientId}) -> true; @@ -116,44 +112,44 @@ match_who(#{peername := undefined}, {ipaddr, _Tup}) -> false; match_who(#{peername := {IP, _}}, {ipaddr, CIDR}) -> esockd_cidr:match(IP, CIDR); -match_who(Credentials, {'and', Conds}) when is_list(Conds) -> +match_who(Client, {'and', Conds}) when is_list(Conds) -> lists:foldl(fun(Who, Allow) -> - match_who(Credentials, Who) andalso Allow + match_who(Client, Who) andalso Allow end, true, Conds); -match_who(Credentials, {'or', Conds}) when is_list(Conds) -> +match_who(Client, {'or', Conds}) when is_list(Conds) -> lists:foldl(fun(Who, Allow) -> - match_who(Credentials, Who) orelse Allow + match_who(Client, Who) orelse Allow end, false, Conds); -match_who(_Credentials, _Who) -> +match_who(_Client, _Who) -> false. -match_topics(_Credentials, _Topic, []) -> +match_topics(_Client, _Topic, []) -> false; -match_topics(Credentials, Topic, [{pattern, PatternFilter}|Filters]) -> - TopicFilter = feed_var(Credentials, PatternFilter), +match_topics(Client, Topic, [{pattern, PatternFilter}|Filters]) -> + TopicFilter = feed_var(Client, PatternFilter), match_topic(emqx_topic:words(Topic), TopicFilter) - orelse match_topics(Credentials, Topic, Filters); -match_topics(Credentials, Topic, [TopicFilter|Filters]) -> + orelse match_topics(Client, Topic, Filters); +match_topics(Client, Topic, [TopicFilter|Filters]) -> match_topic(emqx_topic:words(Topic), TopicFilter) - orelse match_topics(Credentials, Topic, Filters). + orelse match_topics(Client, Topic, Filters). match_topic(Topic, {eq, TopicFilter}) -> Topic == TopicFilter; match_topic(Topic, TopicFilter) -> emqx_topic:match(Topic, TopicFilter). -feed_var(Credentials, Pattern) -> - feed_var(Credentials, Pattern, []). -feed_var(_Credentials, [], Acc) -> +feed_var(Client, Pattern) -> + feed_var(Client, Pattern, []). +feed_var(_Client, [], Acc) -> lists:reverse(Acc); -feed_var(Credentials = #{client_id := undefined}, [<<"%c">>|Words], Acc) -> - feed_var(Credentials, Words, [<<"%c">>|Acc]); -feed_var(Credentials = #{client_id := ClientId}, [<<"%c">>|Words], Acc) -> - feed_var(Credentials, Words, [ClientId |Acc]); -feed_var(Credentials = #{username := undefined}, [<<"%u">>|Words], Acc) -> - feed_var(Credentials, Words, [<<"%u">>|Acc]); -feed_var(Credentials = #{username := Username}, [<<"%u">>|Words], Acc) -> - feed_var(Credentials, Words, [Username|Acc]); -feed_var(Credentials, [W|Words], Acc) -> - feed_var(Credentials, Words, [W|Acc]). +feed_var(Client = #{client_id := undefined}, [<<"%c">>|Words], Acc) -> + feed_var(Client, Words, [<<"%c">>|Acc]); +feed_var(Client = #{client_id := ClientId}, [<<"%c">>|Words], Acc) -> + feed_var(Client, Words, [ClientId |Acc]); +feed_var(Client = #{username := undefined}, [<<"%u">>|Words], Acc) -> + feed_var(Client, Words, [<<"%u">>|Acc]); +feed_var(Client = #{username := Username}, [<<"%u">>|Words], Acc) -> + feed_var(Client, Words, [Username|Acc]); +feed_var(Client, [W|Words], Acc) -> + feed_var(Client, Words, [W|Acc]). From c55ada352620bfd5402ed8e2c9c901cfb3d5e4fe Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sat, 27 Jul 2019 23:24:38 +0800 Subject: [PATCH 28/33] Improve the types --- src/emqx_types.erl | 61 ++++++++++++++++++++++++---------------------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/src/emqx_types.erl b/src/emqx_types.erl index 490284039..be1cdd42c 100644 --- a/src/emqx_types.erl +++ b/src/emqx_types.erl @@ -17,20 +17,18 @@ -module(emqx_types). -include("emqx.hrl"). --include("types.hrl"). -include("emqx_mqtt.hrl"). - --export_type([zone/0]). +-include("types.hrl"). -export_type([ ver/0 , qos/0 , qos_name/0 ]). --export_type([ pubsub/0 +-export_type([ zone/0 + , pubsub/0 , topic/0 , subid/0 - , subopts/0 ]). -export_type([ client/0 @@ -42,9 +40,9 @@ ]). -export_type([ connack/0 + , subopts/0 , reason_code/0 , properties/0 - , topic_filters/0 ]). -export_type([ packet_id/0 @@ -54,7 +52,7 @@ -export_type([ subscription/0 , subscriber/0 - , topic_table/0 + , topic_filters/0 ]). -export_type([ payload/0 @@ -72,38 +70,25 @@ , command/0 ]). --export_type([ infos/0 +-export_type([ caps/0 + , infos/0 , attrs/0 , stats/0 ]). --type(zone() :: emqx_zone:zone()). --type(ver() :: ?MQTT_PROTO_V3 | ?MQTT_PROTO_V4 | ?MQTT_PROTO_V5). +-type(ver() :: ?MQTT_PROTO_V3 + | ?MQTT_PROTO_V4 + | ?MQTT_PROTO_V5). -type(qos() :: ?QOS_0 | ?QOS_1 | ?QOS_2). -type(qos_name() :: qos0 | at_most_once | qos1 | at_least_once | qos2 | exactly_once). +-type(zone() :: emqx_zone:zone()). -type(pubsub() :: publish | subscribe). -type(topic() :: emqx_topic:topic()). -type(subid() :: binary() | atom()). --type(subopts() :: #{rh := 0 | 1 | 2, - rap := 0 | 1, - nl := 0 | 1, - qos := qos(), - share => binary(), - atom() => term() - }). - --type(packet_type() :: ?RESERVED..?AUTH). --type(connack() :: ?CONNACK_ACCEPT..?CONNACK_AUTH). --type(reason_code() :: 0..16#FF). --type(packet_id() :: 1..16#FFFF). --type(properties() :: #{atom() => term()}). --type(topic_filters() :: list({emqx_topic:topic(), subopts()})). --type(packet() :: #mqtt_packet{}). - -type(client() :: #{zone := zone(), conn_mod := maybe(module()), peername := peername(), @@ -111,6 +96,7 @@ client_id := client_id(), username := username(), peercert := esockd_peercert:peercert(), + is_bridge := boolean(), is_superuser := boolean(), mountpoint := maybe(binary()), ws_cookie := maybe(list()), @@ -123,6 +109,7 @@ -type(username() :: maybe(binary())). -type(password() :: maybe(binary())). -type(peername() :: {inet:ip_address(), inet:port_number()}). +-type(protocol() :: mqtt | 'mqtt-sn' | coap | stomp | none | atom()). -type(auth_result() :: success | client_identifier_not_valid | bad_username_or_password @@ -132,21 +119,37 @@ | server_busy | banned | bad_authentication_method). --type(protocol() :: mqtt | 'mqtt-sn' | coap | stomp | none | atom()). + +-type(packet_type() :: ?RESERVED..?AUTH). +-type(connack() :: ?CONNACK_ACCEPT..?CONNACK_AUTH). +-type(subopts() :: #{rh := 0 | 1 | 2, + rap := 0 | 1, + nl := 0 | 1, + qos := qos(), + share => binary(), + atom() => term() + }). +-type(reason_code() :: 0..16#FF). +-type(packet_id() :: 1..16#FFFF). +-type(properties() :: #{atom() => term()}). +-type(topic_filters() :: list({topic(), subopts()})). +-type(packet() :: #mqtt_packet{}). + -type(subscription() :: #subscription{}). -type(subscriber() :: {pid(), subid()}). --type(topic_table() :: [{topic(), subopts()}]). -type(payload() :: binary() | iodata()). -type(message() :: #message{}). -type(banned() :: #banned{}). -type(delivery() :: #delivery{}). -type(deliver_results() :: [{route, node(), topic()} | - {dispatch, topic(), pos_integer()}]). + {deliver, topic(), non_neg_integer()} + ]). -type(route() :: #route{}). -type(alarm() :: #alarm{}). -type(plugin() :: #plugin{}). -type(command() :: #command{}). +-type(caps() :: emqx_mqtt_caps:caps()). -type(infos() :: #{atom() => term()}). -type(attrs() :: #{atom() => term()}). -type(stats() :: list({atom(), non_neg_integer()})). From 51fb4e33d626eee37c1482ed9cd20c79da198192 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 30 Jul 2019 19:10:38 +0800 Subject: [PATCH 29/33] Rename 'Credentials' to 'Client' --- src/emqx_mod_acl_internal.erl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/emqx_mod_acl_internal.erl b/src/emqx_mod_acl_internal.erl index 8eaa6e138..f1288e323 100644 --- a/src/emqx_mod_acl_internal.erl +++ b/src/emqx_mod_acl_internal.erl @@ -64,8 +64,8 @@ all_rules() -> -spec(check_acl(emqx_types:credentials(), emqx_types:pubsub(), emqx_topic:topic(), emqx_access_rule:acl_result(), acl_rules()) -> {ok, allow} | {ok, deny} | ok). -check_acl(Credentials, PubSub, Topic, _AclResult, Rules) -> - case match(Credentials, Topic, lookup(PubSub, Rules)) of +check_acl(Client, PubSub, Topic, _AclResult, Rules) -> + case match(Client, Topic, lookup(PubSub, Rules)) of {matched, allow} -> {ok, allow}; {matched, deny} -> {ok, deny}; nomatch -> ok @@ -85,12 +85,12 @@ acl_file() -> lookup(PubSub, Rules) -> maps:get(PubSub, Rules, []). -match(_Credentials, _Topic, []) -> +match(_Client, _Topic, []) -> nomatch; -match(Credentials, Topic, [Rule|Rules]) -> - case emqx_access_rule:match(Credentials, Topic, Rule) of +match(Client, Topic, [Rule|Rules]) -> + case emqx_access_rule:match(Client, Topic, Rule) of nomatch -> - match(Credentials, Topic, Rules); + match(Client, Topic, Rules); {matched, AllowDeny} -> {matched, AllowDeny} end. From c880668419867ff52d3e677aa7accb192c894da7 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 30 Jul 2019 19:11:21 +0800 Subject: [PATCH 30/33] Register channel in this module --- src/emqx_channel.erl | 1 + src/emqx_ws_channel.erl | 1 + 2 files changed, 2 insertions(+) diff --git a/src/emqx_channel.erl b/src/emqx_channel.erl index 9c908c9bf..4cb5b3e17 100644 --- a/src/emqx_channel.erl +++ b/src/emqx_channel.erl @@ -255,6 +255,7 @@ connected(enter, _PrevSt, State = #state{proto_state = ProtoState}) -> NState = State#state{connected = true, connected_at = os:timestamp()}, ClientId = emqx_protocol:info(client_id, ProtoState), + ok = emqx_cm:register_channel(ClientId), ok = emqx_cm:set_chan_attrs(ClientId, attrs(NState)), %% Ensure keepalive after connected successfully. Interval = emqx_protocol:info(keepalive, ProtoState), diff --git a/src/emqx_ws_channel.erl b/src/emqx_ws_channel.erl index 1c21c29d8..09a6fb334 100644 --- a/src/emqx_ws_channel.erl +++ b/src/emqx_ws_channel.erl @@ -293,6 +293,7 @@ websocket_info({timeout, Timer, emit_stats}, proto_state = ProtoState, gc_state = GcState}) -> ClientId = emqx_protocol:info(client_id, ProtoState), + ok = emqx_cm:register_channel(ClientId), ok = emqx_cm:set_chan_stats(ClientId, stats(State)), NState = State#state{stats_timer = undefined}, Limits = erlang:get(force_shutdown_policy), From 41dfbd41650bec7d526aa0daa66aeca25f75fdb3 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 30 Jul 2019 19:12:42 +0800 Subject: [PATCH 31/33] Add 'packets.pubrec.inuse' counter --- src/emqx_metrics.erl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/emqx_metrics.erl b/src/emqx_metrics.erl index 1f3d389fa..c7f347dc5 100644 --- a/src/emqx_metrics.erl +++ b/src/emqx_metrics.erl @@ -94,6 +94,7 @@ {counter, 'packets.puback.missed'}, % PUBACK packets missed {counter, 'packets.pubrec.received'}, % PUBREC packets received {counter, 'packets.pubrec.sent'}, % PUBREC packets sent + {counter, 'packets.pubrec.inuse'}, % PUBREC packet_id inuse {counter, 'packets.pubrec.missed'}, % PUBREC packets missed {counter, 'packets.pubrel.received'}, % PUBREL packets received {counter, 'packets.pubrel.sent'}, % PUBREL packets sent @@ -459,4 +460,6 @@ reserved_idx('messages.expired') -> 50; reserved_idx('messages.forward') -> 51; reserved_idx('auth.mqtt.anonymous') -> 52; reserved_idx('channel.gc.cnt') -> 53; +reserved_idx('packets.pubrec.inuse') -> 54; reserved_idx(_) -> undefined. + From b32156bd68283394e381271ddf566aea445779d2 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 31 Jul 2019 08:08:11 +0800 Subject: [PATCH 32/33] Rename the type 'credentials' to 'client' --- src/emqx_access_rule.erl | 2 +- src/emqx_banned.erl | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/emqx_access_rule.erl b/src/emqx_access_rule.erl index b9d166030..dbde32984 100644 --- a/src/emqx_access_rule.erl +++ b/src/emqx_access_rule.erl @@ -86,7 +86,7 @@ bin(B) when is_binary(B) -> B. %% @doc Match access rule --spec(match(emqx_types:credentials(), emqx_types:topic(), rule()) +-spec(match(emqx_types:client(), emqx_types:topic(), rule()) -> {matched, allow} | {matched, deny} | nomatch). match(_Client, _Topic, {AllowDeny, all}) when ?ALLOW_DENY(AllowDeny) -> {matched, AllowDeny}; diff --git a/src/emqx_banned.erl b/src/emqx_banned.erl index 1cee38098..c38552bd7 100644 --- a/src/emqx_banned.erl +++ b/src/emqx_banned.erl @@ -68,7 +68,7 @@ mnesia(copy) -> start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). --spec(check(emqx_types:credentials()) -> boolean()). +-spec(check(emqx_types:client()) -> boolean()). check(#{client_id := ClientId, username := Username, peername := {IPAddr, _}}) -> ets:member(?BANNED_TAB, {client_id, ClientId}) orelse ets:member(?BANNED_TAB, {username, Username}) @@ -78,11 +78,10 @@ check(#{client_id := ClientId, username := Username, peername := {IPAddr, _}}) - add(Banned) when is_record(Banned, banned) -> mnesia:dirty_write(?BANNED_TAB, Banned). --spec(delete({client_id, emqx_types:client_id()} - | {username, emqx_types:username()} - | {peername, emqx_types:peername()}) -> ok). -delete(Key) -> - mnesia:dirty_delete(?BANNED_TAB, Key). +-spec(delete({client_id, emqx_types:client_id()} | + {username, emqx_types:username()} | + {peername, emqx_types:peername()}) -> ok). +delete(Key) -> mnesia:dirty_delete(?BANNED_TAB, Key). %%-------------------------------------------------------------------- %% gen_server callbacks From 0a6468cf484a4c55ec164bd0187effa2174827dc Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 31 Jul 2019 08:09:47 +0800 Subject: [PATCH 33/33] Introduce the 'pipeline' design pattern - Introduce the 'pipeline' design pattern in emqx_protocol module - Reame the '{dispatch, ...' to '{deliver, ...' in emqx_broker module - Rename type 'credentials' to 'client' --- src/emqx_broker.erl | 4 +- src/emqx_client.erl | 2 +- src/emqx_cm.erl | 16 +- src/emqx_mod_acl_internal.erl | 2 +- src/emqx_mod_rewrite.erl | 13 +- src/emqx_mqtt_caps.erl | 22 +- src/emqx_packet.erl | 2 +- src/emqx_protocol.erl | 565 +++++++++++++++++++++------------- src/emqx_session.erl | 33 +- src/emqx_shared_sub.erl | 2 +- 10 files changed, 408 insertions(+), 253 deletions(-) diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index 674c50194..d6acc8cdb 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -274,13 +274,13 @@ dispatch(Topic, Delivery = #delivery{message = Msg, results = Results}) -> Delivery; [Sub] -> %% optimize? Cnt = dispatch(Sub, Topic, Msg), - Delivery#delivery{results = [{dispatch, Topic, Cnt}|Results]}; + Delivery#delivery{results = [{deliver, Topic, Cnt}|Results]}; Subs -> Cnt = lists:foldl( fun(Sub, Acc) -> dispatch(Sub, Topic, Msg) + Acc end, 0, Subs), - Delivery#delivery{results = [{dispatch, Topic, Cnt}|Results]} + Delivery#delivery{results = [{deliver, Topic, Cnt}|Results]} end. dispatch(SubPid, Topic, Msg) when is_pid(SubPid) -> diff --git a/src/emqx_client.erl b/src/emqx_client.erl index 7e4120628..ab7e9386a 100644 --- a/src/emqx_client.erl +++ b/src/emqx_client.erl @@ -205,7 +205,7 @@ | {nl, boolean()} | {qos, qos()}). --type(reason_code() :: emqx_mqtt_types:reason_code()). +-type(reason_code() :: emqx_types:reason_code()). -type(subscribe_ret() :: {ok, properties(), [reason_code()]} | {error, term()}). diff --git a/src/emqx_cm.erl b/src/emqx_cm.erl index b1e3982fe..26e5a4a2f 100644 --- a/src/emqx_cm.erl +++ b/src/emqx_cm.erl @@ -42,7 +42,7 @@ , set_chan_stats/2 ]). --export([ open_session/1 +-export([ open_session/3 , discard_session/1 , resume_session/1 ]). @@ -168,24 +168,22 @@ set_chan_stats(ClientId, ChanPid, Stats) -> ok. %% @doc Open a session. --spec(open_session(map()) -> {ok, emqx_session:session()} - | {error, Reason :: term()}). -open_session(Attrs = #{clean_start := true, - client_id := ClientId}) -> +-spec(open_session(boolean(), emqx_types:client(), map()) + -> {ok, emqx_session:session()} | {error, Reason :: term()}). +open_session(true, Client = #{client_id := ClientId}, Options) -> CleanStart = fun(_) -> ok = discard_session(ClientId), - {ok, emqx_session:init(Attrs), false} + {ok, emqx_session:init(true, Client, Options), false} end, emqx_cm_locker:trans(ClientId, CleanStart); -open_session(Attrs = #{clean_start := false, - client_id := ClientId}) -> +open_session(false, Client = #{client_id := ClientId}, Options) -> ResumeStart = fun(_) -> case resume_session(ClientId) of {ok, Session} -> {ok, Session, true}; {error, not_found} -> - {ok, emqx_session:init(Attrs), false} + {ok, emqx_session:init(false, Client, Options), false} end end, emqx_cm_locker:trans(ClientId, ResumeStart). diff --git a/src/emqx_mod_acl_internal.erl b/src/emqx_mod_acl_internal.erl index f1288e323..2629ed60a 100644 --- a/src/emqx_mod_acl_internal.erl +++ b/src/emqx_mod_acl_internal.erl @@ -61,7 +61,7 @@ all_rules() -> %%-------------------------------------------------------------------- %% @doc Check ACL --spec(check_acl(emqx_types:credentials(), emqx_types:pubsub(), emqx_topic:topic(), +-spec(check_acl(emqx_types:client(), emqx_types:pubsub(), emqx_topic:topic(), emqx_access_rule:acl_result(), acl_rules()) -> {ok, allow} | {ok, deny} | ok). check_acl(Client, PubSub, Topic, _AclResult, Rules) -> diff --git a/src/emqx_mod_rewrite.erl b/src/emqx_mod_rewrite.erl index 0b0285ddf..4a278dd53 100644 --- a/src/emqx_mod_rewrite.erl +++ b/src/emqx_mod_rewrite.erl @@ -32,9 +32,9 @@ , unload/1 ]). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Load/Unload -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- load(RawRules) -> Rules = compile(RawRules), @@ -42,10 +42,10 @@ load(RawRules) -> emqx_hooks:add('client.unsubscribe', fun ?MODULE:rewrite_unsubscribe/3, [Rules]), emqx_hooks:add('message.publish', fun ?MODULE:rewrite_publish/2, [Rules]). -rewrite_subscribe(_Credentials, TopicTable, Rules) -> +rewrite_subscribe(_Client, TopicTable, Rules) -> {ok, [{match_rule(Topic, Rules), Opts} || {Topic, Opts} <- TopicTable]}. -rewrite_unsubscribe(_Credentials, TopicTable, Rules) -> +rewrite_unsubscribe(_Client, TopicTable, Rules) -> {ok, [{match_rule(Topic, Rules), Opts} || {Topic, Opts} <- TopicTable]}. rewrite_publish(Message = #message{topic = Topic}, Rules) -> @@ -56,9 +56,9 @@ unload(_) -> emqx_hooks:del('client.unsubscribe', fun ?MODULE:rewrite_unsubscribe/3), emqx_hooks:del('message.publish', fun ?MODULE:rewrite_publish/2). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Internal functions -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- match_rule(Topic, []) -> Topic; @@ -86,3 +86,4 @@ compile(Rules) -> {ok, MP} = re:compile(Re), {rewrite, Topic, MP, Dest} end, Rules). + diff --git a/src/emqx_mqtt_caps.erl b/src/emqx_mqtt_caps.erl index a95641723..5e0d2a3fc 100644 --- a/src/emqx_mqtt_caps.erl +++ b/src/emqx_mqtt_caps.erl @@ -14,7 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- -%% @doc MQTTv5 capabilities +%% @doc MQTTv5 Capabilities -module(emqx_mqtt_caps). -include("emqx.hrl"). @@ -28,14 +28,17 @@ -export([default_caps/0]). +-export_type([caps/0]). + -type(caps() :: #{max_packet_size => integer(), max_clientid_len => integer(), max_topic_alias => integer(), max_topic_levels => integer(), max_qos_allowed => emqx_types:qos(), - mqtt_retain_available => boolean(), - mqtt_shared_subscription => boolean(), - mqtt_wildcard_subscription => boolean()}). + mqtt_retain_available => boolean(), + mqtt_shared_subscription => boolean(), + mqtt_wildcard_subscription => boolean() + }). -define(UNLIMITED, 0). @@ -44,18 +47,21 @@ {max_topic_alias, ?UNLIMITED}, {max_topic_levels, ?UNLIMITED}, {max_qos_allowed, ?QOS_2}, - {mqtt_retain_available, true}, - {mqtt_shared_subscription, true}, - {mqtt_wildcard_subscription, true}]). + {mqtt_retain_available, true}, + {mqtt_shared_subscription, true}, + {mqtt_wildcard_subscription, true} + ]). -define(PUBCAP_KEYS, [max_qos_allowed, mqtt_retain_available, max_topic_alias ]). + -define(SUBCAP_KEYS, [max_qos_allowed, max_topic_levels, mqtt_shared_subscription, - mqtt_wildcard_subscription]). + mqtt_wildcard_subscription + ]). -spec(check_pub(emqx_types:zone(), map()) -> ok | {error, emqx_types:reason_code()}). check_pub(Zone, Props) when is_map(Props) -> diff --git a/src/emqx_packet.erl b/src/emqx_packet.erl index 71d1fb116..be21f41d1 100644 --- a/src/emqx_packet.erl +++ b/src/emqx_packet.erl @@ -142,7 +142,7 @@ publish_props(Headers) -> 'Message-Expiry-Interval'], Headers). %% @doc Message from Packet --spec(to_message(emqx_types:credentials(), emqx_ypes:packet()) +-spec(to_message(emqx_types:client(), emqx_ypes:packet()) -> emqx_types:message()). to_message(#{client_id := ClientId, username := Username, peername := Peername}, #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index ea052f05f..12bb008ac 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -27,6 +27,7 @@ -export([ info/1 , info/2 , attrs/1 + , caps/1 ]). -export([ init/2 @@ -44,14 +45,12 @@ proto_ver :: emqx_types:version(), client :: emqx_types:client(), session :: emqx_session:session(), - mountfun :: fun((emqx_topic:topic()) -> emqx_topic:topic()), keepalive :: non_neg_integer(), will_msg :: emqx_types:message(), enable_acl :: boolean(), - is_bridge :: boolean(), - topic_aliases :: map(), - alias_maximum :: map() - }). + topic_aliases :: maybe(map()), + alias_maximum :: maybe(map()) + }). -opaque(proto_state() :: #protocol{}). @@ -62,8 +61,6 @@ info(#protocol{proto_name = ProtoName, session = Session, keepalive = Keepalive, will_msg = WillMsg, - enable_acl = EnableAcl, - is_bridge = IsBridge, topic_aliases = Aliases}) -> #{proto_name => ProtoName, proto_ver => ProtoVer, @@ -71,8 +68,6 @@ info(#protocol{proto_name = ProtoName, session => emqx_session:info(Session), keepalive => Keepalive, will_msg => WillMsg, - enable_acl => EnableAcl, - is_bridge => IsBridge, topic_aliases => Aliases }. @@ -91,8 +86,6 @@ info(session, #protocol{session = Session}) -> Session; info(keepalive, #protocol{keepalive = Keepalive}) -> Keepalive; -info(is_bridge, #protocol{is_bridge = IsBridge}) -> - IsBridge; info(topic_aliases, #protocol{topic_aliases = Aliases}) -> Aliases. @@ -100,34 +93,35 @@ attrs(#protocol{proto_name = ProtoName, proto_ver = ProtoVer, client = Client, session = Session, - keepalive = Keepalive, - is_bridge = IsBridge}) -> + keepalive = Keepalive}) -> #{proto_name => ProtoName, - proto_ver => ProtoVer, - client => Client, - session => emqx_session:attrs(Session), - keepalive => Keepalive, - is_bridge => IsBridge + proto_ver => ProtoVer, + client => Client, + session => emqx_session:attrs(Session), + keepalive => Keepalive }. +caps(#protocol{client = #{zone := Zone}}) -> + emqx_mqtt_caps:get_caps(Zone). + -spec(init(map(), proplists:proplist()) -> proto_state()). init(ConnInfo, Options) -> Zone = proplists:get_value(zone, Options), - Peercert = maps:get(peercert, ConnInfo, nossl), + Peercert = maps:get(peercert, ConnInfo, undefined), Username = peer_cert_as_username(Peercert, Options), Mountpoint = emqx_zone:get_env(Zone, mountpoint), - Client = maps:merge(#{zone => Zone, - username => Username, - mountpoint => Mountpoint + EnableAcl = emqx_zone:get_env(Zone, enable_acl, true), + Client = maps:merge(#{zone => Zone, + username => Username, + mountpoint => Mountpoint, + is_bridge => false, + is_superuser => false }, ConnInfo), - EnableAcl = emqx_zone:get_env(Zone, enable_acl, false), - MountFun = fun(Topic) -> - emqx_mountpoint:mount(Mountpoint, Topic) - end, - #protocol{client = Client, - mountfun = MountFun, - enable_acl = EnableAcl, - is_bridge = false + #protocol{proto_ver = ?MQTT_PROTO_V4, + proto_name = <<"MQTT">>, + client = Client, + %%mountfun = MountFun, + enable_acl = EnableAcl }. peer_cert_as_username(Peercert, Options) -> @@ -147,61 +141,39 @@ peer_cert_as_username(Peercert, Options) -> | {ok, emqx_types:packet(), proto_state()} | {error, Reason :: term(), proto_state()} | {stop, Error :: atom(), proto_state()}). -handle_in(?CONNECT_PACKET( - #mqtt_packet_connect{proto_name = ProtoName, - proto_ver = ProtoVer, - is_bridge = IsBridge, - client_id = ClientId, - username = Username, - password = Password, - keepalive = Keepalive} = ConnPkt), - PState = #protocol{client = Client}) -> - Client1 = maps:merge(Client, #{client_id => ClientId, - username => Username, - password => Password - }), - emqx_logger:set_metadata_client_id(ClientId), - WillMsg = emqx_packet:will_msg(ConnPkt), - PState1 = PState#protocol{client = Client1, - proto_name = ProtoName, - proto_ver = ProtoVer, - is_bridge = IsBridge, - keepalive = Keepalive, - will_msg = WillMsg - }, - %% fun validate_packet/2, - case pipeline([fun check_connect/2, - fun handle_connect/2], ConnPkt, PState1) of - {ok, SP, PState2} -> - handle_out({connack, ?RC_SUCCESS, sp(SP)}, PState2); - {error, ReasonCode} -> - handle_out({connack, ReasonCode}, PState1); - {error, ReasonCode, PState2} -> - handle_out({connack, ReasonCode}, PState2) +handle_in(?CONNECT_PACKET(#mqtt_packet_connect{client_id = ClientId} = ConnPkt), + PState) -> + ok = emqx_logger:set_metadata_client_id(ClientId), + case pipeline([fun validate_in/2, + fun preprocess_props/2, + fun check_connect/2, + fun enrich_pstate/2, + fun auth_connect/2], ConnPkt, PState) of + {ok, NConnPkt, NPState} -> + handle_connect(NConnPkt, NPState); + {error, ReasonCode, NPState} -> + handle_out({disconnect, ReasonCode}, NPState) end; handle_in(Packet = ?PUBLISH_PACKET(QoS, Topic, PacketId), PState) -> - case pipeline([fun validate_packet/2, - fun check_pub_caps/2, - fun check_pub_acl/2, - fun handle_publish/2], Packet, PState) of - {error, ReasonCode} -> - ?LOG(warning, "Cannot publish qos~w message to ~s due to ~s", - [QoS, Topic, emqx_reason_codes:text(ReasonCode)]), - handle_out(case QoS of - ?QOS_0 -> {puberr, ReasonCode}; - ?QOS_1 -> {puback, PacketId, ReasonCode}; - ?QOS_2 -> {pubrec, PacketId, ReasonCode} - end, PState); - Result -> Result + case pipeline([fun validate_in/2, + fun preprocess_props/2, + fun check_publish/2], Packet, PState) of + {ok, NPacket, NPState} -> + handle_publish(NPacket, NPState); + {error, ReasonCode, PState1} -> + ?LOG(warning, "Cannot publish message to ~s due to ~s", + [Topic, emqx_reason_codes:text(ReasonCode)]), + handle_puback(QoS, PacketId, ReasonCode, PState1) end; handle_in(?PUBACK_PACKET(PacketId, ReasonCode), PState = #protocol{session = Session}) -> case emqx_session:puback(PacketId, ReasonCode, Session) of {ok, NSession} -> {ok, PState#protocol{session = NSession}}; + {ok, Publishes, NSession} -> + handle_out({publish, Publishes}, PState#protocol{session = NSession}); {error, _NotFound} -> - %% TODO: metrics? error msg? {ok, PState} end; @@ -221,35 +193,37 @@ handle_in(?PUBREL_PACKET(PacketId, ReasonCode), PState = #protocol{session = Ses handle_out({pubcomp, PacketId, ReasonCode}, PState) end; -handle_in(?PUBCOMP_PACKET(PacketId, ReasonCode), - PState = #protocol{session = Session}) -> +handle_in(?PUBCOMP_PACKET(PacketId, ReasonCode), PState = #protocol{session = Session}) -> case emqx_session:pubcomp(PacketId, ReasonCode, Session) of {ok, NSession} -> {ok, PState#protocol{session = NSession}}; - {error, _ReasonCode} -> - %% TODO: How to handle the reason code? + {ok, Publishes, NSession} -> + handle_out({publish, Publishes}, PState#protocol{session = NSession}); + {error, _NotFound} -> {ok, PState} end; handle_in(Packet = ?SUBSCRIBE_PACKET(PacketId, Properties, TopicFilters), PState = #protocol{client = Client}) -> - case validate(Packet) of - ok -> ok = emqx_hooks:run('client.subscribe', - [Client, Properties, TopicFilters]), - TopicFilters1 = enrich_subid(Properties, TopicFilters), - {ReasonCodes, PState1} = handle_subscribe(TopicFilters1, PState), - handle_out({suback, PacketId, ReasonCodes}, PState1); + case validate_in(Packet, PState) of + ok -> + ok = emqx_hooks:run('client.subscribe', + [Client, Properties, TopicFilters]), + TopicFilters1 = enrich_subid(Properties, TopicFilters), + {ReasonCodes, PState1} = handle_subscribe(TopicFilters1, PState), + handle_out({suback, PacketId, ReasonCodes}, PState1); {error, ReasonCode} -> handle_out({disconnect, ReasonCode}, PState) end; handle_in(Packet = ?UNSUBSCRIBE_PACKET(PacketId, Properties, TopicFilters), PState = #protocol{client = Client}) -> - case validate(Packet) of - ok -> ok = emqx_hooks:run('client.unsubscribe', - [Client, Properties, TopicFilters]), - {ReasonCodes, PState1} = handle_unsubscribe(TopicFilters, PState), - handle_out({unsuback, PacketId, ReasonCodes}, PState1); + case validate_in(Packet, PState) of + ok -> + ok = emqx_hooks:run('client.unsubscribe', + [Client, Properties, TopicFilters]), + {ReasonCodes, PState1} = handle_unsubscribe(TopicFilters, PState), + handle_out({unsuback, PacketId, ReasonCodes}, PState1); {error, ReasonCode} -> handle_out({disconnect, ReasonCode}, PState) end; @@ -280,7 +254,7 @@ handle_in(Packet, PState) -> handle_deliver(Delivers, PState = #protocol{client = Client, session = Session}) when is_list(Delivers) -> - case emqx_session:handle(Delivers, Session) of + case emqx_session:deliver(Delivers, Session) of {ok, Publishes, NSession} -> Packets = lists:map(fun({publish, PacketId, Msg}) -> Msg0 = emqx_hooks:run_fold('message.deliver', [Client], Msg), @@ -293,12 +267,24 @@ handle_deliver(Delivers, PState = #protocol{client = Client, session = Session}) {ok, PState#protocol{session = NSession}} end. +%%-------------------------------------------------------------------- +%% Handle puback +%%-------------------------------------------------------------------- + +handle_puback(?QOS_0, _PacketId, ReasonCode, PState) -> + handle_out({puberr, ReasonCode}, PState); +handle_puback(?QOS_1, PacketId, ReasonCode, PState) -> + handle_out({puback, PacketId, ReasonCode}, PState); +handle_puback(?QOS_2, PacketId, ReasonCode, PState) -> + handle_out({pubrec, PacketId, ReasonCode}, PState). + %%-------------------------------------------------------------------- %% Handle outgoing packet %%-------------------------------------------------------------------- handle_out({connack, ?RC_SUCCESS, SP}, PState = #protocol{client = Client}) -> - ok = emqx_hooks:run('client.connected', [Client, ?RC_SUCCESS, info(PState)]), + ok = emqx_hooks:run('client.connected', + [Client, ?RC_SUCCESS, info(PState)]), Props = #{}, %% TODO: ... {ok, ?CONNACK_PACKET(?RC_SUCCESS, SP, Props), PState}; @@ -312,13 +298,24 @@ handle_out({connack, ReasonCode}, PState = #protocol{client = Client, Reason = emqx_reason_codes:name(ReasonCode1, ProtoVer), {error, Reason, ?CONNACK_PACKET(ReasonCode1), PState}; +handle_out({publish, Publishes}, PState = #protocol{client = Client = #{mountpoint := Mountpoint}}) -> + Mount = fun(Msg) -> emqx_mountpoint:mount(Mountpoint, Msg) end, + Packets = lists:map( + fun({publish, PacketId, Msg}) -> + Msg1 = emqx_hooks:run_fold('message.deliver', [Client], Msg), + Msg2 = Mount(emqx_message:update_expiry(Msg1)), + emqx_packet:from_message(PacketId, Msg2) + end, Publishes), + {ok, Packets, PState}; + handle_out({publish, PacketId, Msg}, PState = #protocol{client = Client}) -> Msg0 = emqx_hooks:run_fold('message.deliver', [Client], Msg), Msg1 = emqx_message:update_expiry(Msg0), Msg2 = emqx_mountpoint:unmount(maps:get(mountpoint, Client), Msg1), {ok, emqx_packet:from_message(PacketId, Msg2), PState}; -handle_out({puberr, ReasonCode}, PState) -> +%% TODO: How to handle the err? +handle_out({puberr, _ReasonCode}, PState) -> {ok, PState}; handle_out({puback, PacketId, ReasonCode}, PState) -> @@ -364,77 +361,265 @@ handle_timeout(TRef, Msg, PState = #protocol{session = Session}) -> {ok, NSession} -> {ok, PState#protocol{session = NSession}}; {ok, Publishes, NSession} -> - %% TODO: handle out... - io:format("Timeout publishes: ~p~n", [Publishes]), - {ok, PState#protocol{session = NSession}} + handle_out({publish, Publishes}, PState#protocol{session = NSession}) end. -terminate(Reason, _State) -> +terminate(Reason, _PState) -> io:format("Terminated for ~p~n", [Reason]), ok. +%%-------------------------------------------------------------------- +%% Validate incoming packet +%%-------------------------------------------------------------------- + +-spec(validate_in(emqx_types:packet(), proto_state()) + -> ok | {error, emqx_types:reason_code()}). +validate_in(Packet, _PState) -> + try emqx_packet:validate(Packet) of + true -> ok + catch + error:protocol_error -> + {error, ?RC_PROTOCOL_ERROR}; + error:subscription_identifier_invalid -> + {error, ?RC_SUBSCRIPTION_IDENTIFIERS_NOT_SUPPORTED}; + error:topic_alias_invalid -> + {error, ?RC_TOPIC_ALIAS_INVALID}; + error:topic_filters_invalid -> + {error, ?RC_TOPIC_FILTER_INVALID}; + error:topic_name_invalid -> + {error, ?RC_TOPIC_FILTER_INVALID}; + error:_Reason -> + {error, ?RC_MALFORMED_PACKET} + end. + +%%-------------------------------------------------------------------- +%% PreProcess Properties +%%-------------------------------------------------------------------- + +preprocess_props(#mqtt_packet_connect{ + properties = #{'Topic-Alias-Maximum' := Max} + }, + PState = #protocol{alias_maximum = AliasMaximum}) -> + {ok, PState#protocol{alias_maximum = AliasMaximum#{outbound => Max}}}; + +preprocess_props(Packet = #mqtt_packet{variable = Publish}, PState) -> + case preprocess_props(Publish, PState) of + {ok, Publish1, PState1} -> + {ok, Packet#mqtt_packet{variable = Publish1}, PState1}; + Error -> Error + end; + +preprocess_props(Publish = #mqtt_packet_publish{topic_name = <<>>, + properties = #{'Topic-Alias' := AliasId} + }, + PState = #protocol{topic_aliases = TopicAliases}) -> + case maps:find(AliasId, TopicAliases) of + {ok, Topic} -> + {ok, Publish#mqtt_packet_publish{topic_name = Topic}, PState}; + false -> {error, ?RC_TOPIC_ALIAS_INVALID} + end; + +preprocess_props(Publish = #mqtt_packet_publish{topic_name = Topic, + properties = #{'Topic-Alias' := AliasId} + }, + PState = #protocol{topic_aliases = Aliases}) -> + Aliases1 = maps:put(AliasId, Topic, Aliases), + {ok, Publish, PState#protocol{topic_aliases = Aliases1}}; + +preprocess_props(Packet, PState) -> + {ok, Packet, PState}. + %%-------------------------------------------------------------------- %% Check Connect Packet %%-------------------------------------------------------------------- -check_connect(_ConnPkt, PState) -> - {ok, PState}. +check_connect(ConnPkt, PState) -> + pipeline([fun check_proto_ver/2, + fun check_client_id/2, + %%fun check_flapping/2, + fun check_banned/2, + fun check_will_topic/2, + fun check_will_retain/2], ConnPkt, PState). -%%-------------------------------------------------------------------- -%% Handle Connect Packet -%%-------------------------------------------------------------------- +check_proto_ver(#mqtt_packet_connect{proto_ver = Ver, + proto_name = Name}, _PState) -> + case lists:member({Ver, Name}, ?PROTOCOL_NAMES) of + true -> ok; + false -> {error, ?RC_PROTOCOL_ERROR} + end. -handle_connect(#mqtt_packet_connect{proto_name = ProtoName, - proto_ver = ProtoVer, - is_bridge = IsBridge, - clean_start = CleanStart, - keepalive = Keepalive, - properties = ConnProps, - client_id = ClientId, - username = Username, - password = Password - } = ConnPkt, - PState = #protocol{client = Client}) -> - case emqx_access_control:authenticate( - Client#{password => Password}) of - {ok, AuthResult} -> - Client1 = maps:merge(Client, AuthResult), - %% Open session - case open_session(ConnPkt, PState) of - {ok, Session, SP} -> - PState1 = PState#protocol{client = Client1, - session = Session}, - ok = emqx_cm:register_channel(ClientId), - {ok, SP, PState1}; - {error, Error} -> - ?LOG(error, "Failed to open session: ~p", [Error]), - {error, ?RC_UNSPECIFIED_ERROR, PState#protocol{client = Client1}} +%% MQTT3.1 does not allow null clientId +check_client_id(#mqtt_packet_connect{proto_ver = ?MQTT_PROTO_V3, + client_id = <<>>}, _PState) -> + {error, ?RC_CLIENT_IDENTIFIER_NOT_VALID}; + +%% Issue#599: Null clientId and clean_start = false +check_client_id(#mqtt_packet_connect{client_id = <<>>, + clean_start = false}, _PState) -> + {error, ?RC_CLIENT_IDENTIFIER_NOT_VALID}; +check_client_id(#mqtt_packet_connect{client_id = <<>>, + clean_start = true}, _PState) -> + ok; +check_client_id(#mqtt_packet_connect{client_id = ClientId}, + #protocol{client = #{zone := Zone}}) -> + Len = byte_size(ClientId), + MaxLen = emqx_zone:get_env(Zone, max_clientid_len), + case (1 =< Len) andalso (Len =< MaxLen) of + true -> ok; + false -> {error, ?RC_CLIENT_IDENTIFIER_NOT_VALID} + end. + +check_banned(#mqtt_packet_connect{client_id = ClientId, + username = Username}, + #protocol{client = Client = #{zone := Zone}}) -> + case emqx_zone:get_env(Zone, enable_ban, false) of + true -> + case emqx_banned:check(Client#{client_id => ClientId, + username => Username}) of + true -> {error, ?RC_BANNED}; + false -> ok end; + false -> ok + end. + +check_will_topic(#mqtt_packet_connect{will_flag = false}, _PState) -> + ok; +check_will_topic(#mqtt_packet_connect{will_topic = WillTopic}, _PState) -> + try emqx_topic:validate(WillTopic) of + true -> ok + catch error:_Error -> + {error, ?RC_TOPIC_NAME_INVALID} + end. + +check_will_retain(#mqtt_packet_connect{will_retain = false}, _PState) -> + ok; +check_will_retain(#mqtt_packet_connect{will_retain = true}, + #protocol{client = #{zone := Zone}}) -> + case emqx_zone:get_env(Zone, mqtt_retain_available, true) of + true -> ok; + false -> {error, ?RC_RETAIN_NOT_SUPPORTED} + end. + +%%-------------------------------------------------------------------- +%% Enrich state +%%-------------------------------------------------------------------- + +enrich_pstate(#mqtt_packet_connect{proto_name = ProtoName, + proto_ver = ProtoVer, + keepalive = Keepalive, + client_id = ClientId, + username = Username, + is_bridge = IsBridge + }, + PState = #protocol{client = Client}) -> + Client1 = maps:merge(Client, #{client_id => ClientId, + username => Username, + is_bridge => IsBridge + }), + {ok, PState#protocol{proto_name = ProtoName, + proto_ver = ProtoVer, + client = Client1, + keepalive = Keepalive}}. + +%%-------------------------------------------------------------------- +%% Auth Connect +%%-------------------------------------------------------------------- + +auth_connect(#mqtt_packet_connect{client_id = ClientId, + username = Username, + password = Password}, + PState = #protocol{client = Client}) -> + case emqx_access_control:authenticate(Client#{password => Password}) of + {ok, AuthResult} -> + {ok, PState#protocol{client = maps:merge(Client, AuthResult)}}; {error, Reason} -> ?LOG(warning, "Client ~s (Username: '~s') login failed for ~p", [ClientId, Username, Reason]), - {error, emqx_reason_codes:connack_error(Reason), PState} + {error, Reason} end. +%%-------------------------------------------------------------------- +%% Handle Connect +%%-------------------------------------------------------------------- + +handle_connect(ConnPkt, PState) -> + case open_session(ConnPkt, PState) of + {ok, Session, SP} -> + WillMsg = emqx_packet:will_msg(ConnPkt), + handle_out({connack, ?RC_SUCCESS, sp(SP)}, + PState#protocol{session = Session, will_msg = WillMsg}); + {error, Reason} -> + %% TODO: Unknown error? + ?LOG(error, "Failed to open session: ~p", [Reason]), + handle_out({connack, ?RC_UNSPECIFIED_ERROR}, PState) + end. + +%%-------------------------------------------------------------------- +%% Open session +%%-------------------------------------------------------------------- + open_session(#mqtt_packet_connect{clean_start = CleanStart, - %%properties = ConnProps, - client_id = ClientId, - username = Username} = ConnPkt, - PState = #protocol{client = Client}) -> - emqx_cm:open_session(maps:merge(Client, #{clean_start => CleanStart, - max_inflight => 0, - expiry_interval => 0})). + properties = ConnProps}, + #protocol{client = Client = #{zone := Zone}}) -> + MaxInflight = maps:get('Receive-Maximum', ConnProps, + emqx_zone:get_env(Zone, max_inflight, 65535)), + Interval = maps:get('Session-Expiry-Interval', ConnProps, + emqx_zone:get_env(Zone, session_expiry_interval, 0)), + emqx_cm:open_session(CleanStart, Client, #{max_inflight => MaxInflight, + expiry_interval => Interval + }). %%-------------------------------------------------------------------- %% Handle Publish Message: Client -> Broker %%-------------------------------------------------------------------- -handle_publish(Packet = ?PUBLISH_PACKET(_QoS, Topic, PacketId), - PState = #protocol{client = Client = #{mountpoint := Mountpoint}}) -> + +%% Check Publish +check_publish(Packet, PState) -> + pipeline([fun check_pub_alias/2, + fun check_pub_caps/2, + fun check_pub_acl/2], Packet, PState). + +%% Check Pub Alias +check_pub_alias(#mqtt_packet{ + variable = #mqtt_packet_publish{ + properties = #{'Topic-Alias' := AliasId} + } + }, + #protocol{alias_maximum = Limits}) -> + case (Limits == undefined) + orelse (Max = maps:get(inbound, Limits, 0)) == 0 + orelse (AliasId > Max) of + true -> {error, ?RC_TOPIC_ALIAS_INVALID}; + false -> ok + end; +check_pub_alias(_Packet, _PState) -> ok. + +%% Check Pub Caps +check_pub_caps(#mqtt_packet{header = #mqtt_packet_header{qos = QoS, + retain = Retain + }}, + #protocol{client = #{zone := Zone}}) -> + emqx_mqtt_caps:check_pub(Zone, #{qos => QoS, retain => Retain}). + +%% Check Pub ACL +check_pub_acl(_Packet, #protocol{enable_acl = false}) -> + ok; +check_pub_acl(_Packet, #protocol{client = #{is_superuser := true}}) -> + ok; +check_pub_acl(#mqtt_packet{variable = #mqtt_packet_publish{topic_name = Topic}}, + #protocol{client = Client}) -> + case emqx_access_control:check_acl(Client, publish, Topic) of + allow -> ok; + deny -> {error, ?RC_NOT_AUTHORIZED} + end. + +handle_publish(Packet = ?PUBLISH_PACKET(_QoS, _Topic, PacketId), + PState = #protocol{client = Client = #{mountpoint := MountPoint}}) -> %% TODO: ugly... publish_to_msg(...) - Msg = emqx_packet:to_message(Client, Packet), - Msg1 = emqx_mountpoint:mount(Mountpoint, Msg), - Msg2 = emqx_message:set_flag(dup, false, Msg1), + Mount = fun(Msg) -> emqx_mountpoint:mount(MountPoint, Msg) end, + Msg1 = emqx_packet:to_message(Client, Packet), + Msg2 = Mount(emqx_message:set_flag(dup, false, Msg1)), handle_publish(PacketId, Msg2, PState). handle_publish(_PacketId, Msg = #message{qos = ?QOS_0}, PState) -> @@ -472,15 +657,15 @@ handle_subscribe([{TopicFilter, SubOpts}|More], Acc, PState) -> handle_subscribe(More, [RC|Acc], PState1). do_subscribe(TopicFilter, SubOpts = #{qos := QoS}, - PState = #protocol{client = Client, - session = Session, - mountfun = Mount}) -> + PState = #protocol{client = Client = #{mountpoint := Mountpoint}, + session = Session}) -> + Mount = fun(Msg) -> emqx_mountpoint:mount(Mountpoint, Msg) end, %% 1. Parse 2. Check 3. Enrich 5. MountPoint 6. Session SubOpts1 = maps:merge(?DEFAULT_SUBOPTS, SubOpts), {TopicFilter1, SubOpts2} = emqx_topic:parse(TopicFilter, SubOpts1), SubOpts3 = enrich_subopts(SubOpts2, PState), case check_subscribe(TopicFilter1, PState) of - ok -> + {ok, _, _} -> %% TODO:... TopicFilter2 = Mount(TopicFilter1), case emqx_session:subscribe(Client, TopicFilter2, SubOpts3, Session) of {ok, NSession} -> @@ -497,14 +682,30 @@ enrich_subid(_Properties, TopicFilters) -> enrich_subopts(SubOpts, #protocol{proto_ver = ?MQTT_PROTO_V5}) -> SubOpts; -enrich_subopts(SubOpts, #protocol{client = #{zone := Zone}, - is_bridge = IsBridge}) -> +enrich_subopts(SubOpts, #protocol{client = #{zone := Zone, + is_bridge := IsBridge}}) -> Rap = flag(IsBridge), Nl = flag(emqx_zone:get_env(Zone, ignore_loop_deliver, false)), SubOpts#{rap => Rap, nl => Nl}. -check_subscribe(_TopicFilter, _PState) -> - ok. +check_subscribe(TopicFilter, PState) -> + pipeline([%%TODO: fun check_sub_caps/2, + fun check_sub_acl/2], TopicFilter, PState). + +%% Check Sub Caps +check_sub_caps(TopicFilter, #protocol{client = #{zone := Zone}}) -> + emqx_mqtt_caps:check_sub(Zone, TopicFilter). + +%% Check Sub ACL +check_sub_acl(_TopicFilter, #protocol{enable_acl = false}) -> + ok; +check_sub_acl(_TopicFilter, #protocol{client = #{is_superuser := true}}) -> + ok; +check_sub_acl(TopicFilter, #protocol{client = Client}) -> + case emqx_access_control:check_acl(Client, subscribe, TopicFilter) of + allow -> ok; + deny -> {error, ?RC_NOT_AUTHORIZED} + end. %%-------------------------------------------------------------------- %% Handle Unsubscribe Request @@ -520,9 +721,9 @@ handle_unsubscribe([TopicFilter|More], Acc, PState) -> {RC, PState1} = do_unsubscribe(TopicFilter, PState), handle_unsubscribe(More, [RC|Acc], PState1). -do_unsubscribe(TopicFilter, PState = #protocol{client = Client, - session = Session, - mountfun = Mount}) -> +do_unsubscribe(TopicFilter, PState = #protocol{client = Client = #{mountpoint := Mountpoint}, + session = Session}) -> + Mount = fun(Topic) -> emqx_mountpoint:mount(Mountpoint, Topic) end, TopicFilter1 = Mount(element(1, emqx_topic:parse(TopicFilter))), case emqx_session:unsubscribe(Client, TopicFilter1, Session) of {ok, NSession} -> @@ -530,65 +731,13 @@ do_unsubscribe(TopicFilter, PState = #protocol{client = Client, {error, RC} -> {RC, PState} end. -%%-------------------------------------------------------------------- -%% Validate Incoming Packet -%%-------------------------------------------------------------------- - -validate_packet(Packet, _PState) -> - validate(Packet). - --spec(validate(emqx_types:packet()) -> ok | {error, emqx_types:reason_code()}). -validate(Packet) -> - try emqx_packet:validate(Packet) of - true -> ok - catch - error:protocol_error -> - {error, ?RC_PROTOCOL_ERROR}; - error:subscription_identifier_invalid -> - {error, ?RC_SUBSCRIPTION_IDENTIFIERS_NOT_SUPPORTED}; - error:topic_alias_invalid -> - {error, ?RC_TOPIC_ALIAS_INVALID}; - error:topic_filters_invalid -> - {error, ?RC_TOPIC_FILTER_INVALID}; - error:topic_name_invalid -> - {error, ?RC_TOPIC_FILTER_INVALID}; - error:_Reason -> - {error, ?RC_MALFORMED_PACKET} - end. - -%%-------------------------------------------------------------------- -%% Check Publish -%%-------------------------------------------------------------------- - -check_pub_caps(#mqtt_packet{header = #mqtt_packet_header{qos = QoS, - retain = Retain}, - variable = #mqtt_packet_publish{}}, - #protocol{client = #{zone := Zone}}) -> - emqx_mqtt_caps:check_pub(Zone, #{qos => QoS, retain => Retain}). - -check_pub_acl(_Packet, #protocol{enable_acl = false}) -> - ok; -check_pub_acl(_Packet, #protocol{client = #{is_superuser := true}}) -> - ok; -check_pub_acl(#mqtt_packet{variable = #mqtt_packet_publish{topic_name = Topic}}, - #protocol{client = Client}) -> - do_acl_check(Client, publish, Topic). - -check_sub_acl(_Packet, #protocol{enable_acl = false}) -> - ok. - -do_acl_check(Client, PubSub, Topic) -> - case emqx_access_control:check_acl(Client, PubSub, Topic) of - allow -> ok; - deny -> {error, ?RC_NOT_AUTHORIZED} - end. - %%-------------------------------------------------------------------- %% Pipeline %%-------------------------------------------------------------------- -pipeline([Fun], Packet, PState) -> - Fun(Packet, PState); +pipeline([], Packet, PState) -> + {ok, Packet, PState}; + pipeline([Fun|More], Packet, PState) -> case Fun(Packet, PState) of ok -> pipeline(More, Packet, PState); @@ -597,7 +746,9 @@ pipeline([Fun|More], Packet, PState) -> {ok, NPacket, NPState} -> pipeline(More, NPacket, NPState); {error, Reason} -> - {error, Reason} + {error, Reason, PState}; + {error, Reason, NPState} -> + {error, Reason, NPState} end. %%-------------------------------------------------------------------- diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 95f8b3727..8d529a921 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -50,7 +50,7 @@ -logger_header("[Session]"). --export([init/1]). +-export([init/3]). -export([ info/1 , attrs/1 @@ -68,7 +68,7 @@ , pubcomp/3 ]). --export([handle/2]). +-export([deliver/2]). -export([timeout/3]). @@ -143,11 +143,9 @@ %%-------------------------------------------------------------------- %% @doc Init a session. --spec(init(Attrs :: map()) -> session()). -init(#{zone := Zone, - clean_start := CleanStart, - max_inflight := MaxInflight, - expiry_interval := ExpiryInterval}) -> +-spec(init(boolean(), emqx_types:client(), Options :: map()) -> session()). +init(CleanStart, #{zone := Zone}, #{max_inflight := MaxInflight, + expiry_interval := ExpiryInterval}) -> #session{clean_start = CleanStart, max_subscriptions = get_env(Zone, max_subscriptions, 0), subscriptions = #{}, @@ -361,6 +359,7 @@ pubrec(PacketId, _ReasonCode, Session = #session{inflight = Inflight}) -> {ok, Session#session{inflight = Inflight1}}; {value, {pubrel, _Ts}} -> ?LOG(warning, "The PUBREC ~w is duplicated", [PacketId]), + ok = emqx_metrics:inc('packets.pubrec.inuse'), {error, ?RC_PACKET_IDENTIFIER_IN_USE}; none -> ?LOG(warning, "The PUBREC ~w is not found.", [PacketId]), @@ -410,7 +409,7 @@ dequeue(Session = #session{inflight = Inflight, mqueue = Q}) -> true -> {ok, Session}; false -> {Msgs, Q1} = dequeue(batch_n(Inflight), [], Q), - handle(lists:reverse(Msgs), [], Session#session{mqueue = Q1}) + deliver(lists:reverse(Msgs), [], Session#session{mqueue = Q1}) end. dequeue(Cnt, Msgs, Q) when Cnt =< 0 -> @@ -433,28 +432,28 @@ batch_n(Inflight) -> %% Broker -> Client: Publish | Msg %%-------------------------------------------------------------------- -handle(Delivers, Session = #session{subscriptions = Subs}) +deliver(Delivers, Session = #session{subscriptions = Subs}) when is_list(Delivers) -> Msgs = [enrich(get_subopts(Topic, Subs), Msg, Session) || {deliver, Topic, Msg} <- Delivers], - handle(Msgs, [], Session). + deliver(Msgs, [], Session). -handle([], Publishes, Session) -> +deliver([], Publishes, Session) -> {ok, lists:reverse(Publishes), Session}; -handle([Msg = #message{qos = ?QOS_0}|More], Acc, Session) -> - handle(More, [{publish, undefined, Msg}|Acc], Session); +deliver([Msg = #message{qos = ?QOS_0}|More], Acc, Session) -> + deliver(More, [{publish, undefined, Msg}|Acc], Session); -handle([Msg = #message{qos = QoS}|More], Acc, - Session = #session{next_pkt_id = PacketId, inflight = Inflight}) +deliver([Msg = #message{qos = QoS}|More], Acc, + Session = #session{next_pkt_id = PacketId, inflight = Inflight}) when QoS =:= ?QOS_1 orelse QoS =:= ?QOS_2 -> case emqx_inflight:is_full(Inflight) of true -> - handle(More, Acc, enqueue(Msg, Session)); + deliver(More, Acc, enqueue(Msg, Session)); false -> Publish = {publish, PacketId, Msg}, Session1 = await(PacketId, Msg, Session), - handle(More, [Publish|Acc], next_pkt_id(Session1)) + deliver(More, [Publish|Acc], next_pkt_id(Session1)) end. enqueue(Msg, Session = #session{mqueue = Q}) -> diff --git a/src/emqx_shared_sub.erl b/src/emqx_shared_sub.erl index e6c8deefc..ac64e1d7a 100644 --- a/src/emqx_shared_sub.erl +++ b/src/emqx_shared_sub.erl @@ -118,7 +118,7 @@ dispatch(Group, Topic, Delivery = #delivery{message = Msg, results = Results}, F {Type, SubPid} -> case do_dispatch(SubPid, Topic, Msg, Type) of ok -> - Delivery#delivery{results = [{dispatch, {Group, Topic}, 1} | Results]}; + Delivery#delivery{results = [{deliver, {Group, Topic}, 1} | Results]}; {error, _Reason} -> %% Failed to dispatch to this sub, try next. dispatch(Group, Topic, Delivery, [SubPid | FailedSubs])