From f615f747913ac61c2fb5deedc7875f8647ce9fed Mon Sep 17 00:00:00 2001 From: CrazyWisdom Date: Sat, 10 Aug 2019 18:01:33 +0800 Subject: [PATCH 01/36] fix(README): update docs url --- README-CN.md | 6 +++--- README.md | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README-CN.md b/README-CN.md index d0ad2f556..bdba664b4 100644 --- a/README-CN.md +++ b/README-CN.md @@ -22,8 +22,8 @@ 获取适合你的二进制软件包,[点此下载](https://emqx.io/downloads)。 -- [单节点安装](https://developer.emqx.io/docs/emq/v3/en/install.html) -- [集群安装](https://developer.emqx.io/docs/emq/v3/en/cluster.html) +- [单节点安装](https://docs.emqx.io/broker/v3/cn/install.html) +- [集群安装](https://docs.emqx.io/broker/v3/cn/cluster.html) ## 从源码构建 @@ -54,7 +54,7 @@ cd _rel/emqx && ./bin/emqx console ## FAQ -访问 [FAQ](https://developer.emqx.io/docs/tutorial/zh/faq/faq.html) 以获取常见问题的帮助。 +访问 [FAQ](https://docs.emqx.io/tutorial/zh/faq/faq.html) 以获取常见问题的帮助。 ## 产品路线 diff --git a/README.md b/README.md index a2692c71d..8371297de 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,8 @@ The *EMQ X* broker is cross-platform, which supports Linux, Unix, Mac OS and Win Download the binary package for your platform from [here](https://emqx.io/downloads). -- [Single Node Install](https://developer.emqx.io/docs/emq/v3/en/install.html) -- [Multi Node Install](https://developer.emqx.io/docs/emq/v3/en/cluster.html) +- [Single Node Install](https://docs.emqx.io/broker/v3/en/install.html) +- [Multi Node Install](https://docs.emqx.io/broker/v3/en/cluster.html) ## Build From Source @@ -56,7 +56,7 @@ To view the dashboard after running, use your browser to open: http://localhost: ## FAQ -Visiting [FAQ](https://developer.emqx.io/docs/tutorial/en/faq/faq.html) to get help of common problems. +Visiting [FAQ](https://docs.emqx.io/tutorial/en/faq/faq.html) to get help of common problems. ## Roadmap From 444972968f3f1f7e5980dc3dcb3b817690cc3013 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 12 Aug 2019 11:57:07 +0800 Subject: [PATCH 02/36] Improve emqx_mqtt_props module and add test cases --- src/emqx_mqtt_props.erl | 103 ++++++++++++++++++++------------- test/emqx_mqtt_props_SUITE.erl | 54 ++++++++++++++--- 2 files changed, 110 insertions(+), 47 deletions(-) diff --git a/src/emqx_mqtt_props.erl b/src/emqx_mqtt_props.erl index 47a368714..1b75425a7 100644 --- a/src/emqx_mqtt_props.erl +++ b/src/emqx_mqtt_props.erl @@ -25,6 +25,12 @@ , validate/1 ]). +%% For tests +-export([all/0]). + +-type(prop_name() :: atom()). +-type(prop_id() :: pos_integer()). + -define(PROPS_TABLE, #{16#01 => {'Payload-Format-Indicator', 'Byte', [?PUBLISH]}, 16#02 => {'Message-Expiry-Interval', 'Four-Byte-Integer', [?PUBLISH]}, @@ -52,36 +58,10 @@ 16#27 => {'Maximum-Packet-Size', 'Four-Byte-Integer', [?CONNECT, ?CONNACK]}, 16#28 => {'Wildcard-Subscription-Available', 'Byte', [?CONNACK]}, 16#29 => {'Subscription-Identifier-Available', 'Byte', [?CONNACK]}, - 16#2A => {'Shared-Subscription-Available', 'Byte', [?CONNACK]}}). - -name(16#01) -> 'Payload-Format-Indicator'; -name(16#02) -> 'Message-Expiry-Interval'; -name(16#03) -> 'Content-Type'; -name(16#08) -> 'Response-Topic'; -name(16#09) -> 'Correlation-Data'; -name(16#0B) -> 'Subscription-Identifier'; -name(16#11) -> 'Session-Expiry-Interval'; -name(16#12) -> 'Assigned-Client-Identifier'; -name(16#13) -> 'Server-Keep-Alive'; -name(16#15) -> 'Authentication-Method'; -name(16#16) -> 'Authentication-Data'; -name(16#17) -> 'Request-Problem-Information'; -name(16#18) -> 'Will-Delay-Interval'; -name(16#19) -> 'Request-Response-Information'; -name(16#1A) -> 'Response-Information'; -name(16#1C) -> 'Server-Reference'; -name(16#1F) -> 'Reason-String'; -name(16#21) -> 'Receive-Maximum'; -name(16#22) -> 'Topic-Alias-Maximum'; -name(16#23) -> 'Topic-Alias'; -name(16#24) -> 'Maximum-QoS'; -name(16#25) -> 'Retain-Available'; -name(16#26) -> 'User-Property'; -name(16#27) -> 'Maximum-Packet-Size'; -name(16#28) -> 'Wildcard-Subscription-Available'; -name(16#29) -> 'Subscription-Identifier-Available'; -name(16#2A) -> 'Shared-Subscription-Available'. + 16#2A => {'Shared-Subscription-Available', 'Byte', [?CONNACK]} + }). +-spec(id(prop_name()) -> prop_id()). id('Payload-Format-Indicator') -> 16#01; id('Message-Expiry-Interval') -> 16#02; id('Content-Type') -> 16#03; @@ -108,12 +88,47 @@ id('User-Property') -> 16#26; id('Maximum-Packet-Size') -> 16#27; id('Wildcard-Subscription-Available') -> 16#28; id('Subscription-Identifier-Available') -> 16#29; -id('Shared-Subscription-Available') -> 16#2A. +id('Shared-Subscription-Available') -> 16#2A; +id(Name) -> error({bad_property, Name}). +-spec(name(prop_id()) -> prop_name()). +name(16#01) -> 'Payload-Format-Indicator'; +name(16#02) -> 'Message-Expiry-Interval'; +name(16#03) -> 'Content-Type'; +name(16#08) -> 'Response-Topic'; +name(16#09) -> 'Correlation-Data'; +name(16#0B) -> 'Subscription-Identifier'; +name(16#11) -> 'Session-Expiry-Interval'; +name(16#12) -> 'Assigned-Client-Identifier'; +name(16#13) -> 'Server-Keep-Alive'; +name(16#15) -> 'Authentication-Method'; +name(16#16) -> 'Authentication-Data'; +name(16#17) -> 'Request-Problem-Information'; +name(16#18) -> 'Will-Delay-Interval'; +name(16#19) -> 'Request-Response-Information'; +name(16#1A) -> 'Response-Information'; +name(16#1C) -> 'Server-Reference'; +name(16#1F) -> 'Reason-String'; +name(16#21) -> 'Receive-Maximum'; +name(16#22) -> 'Topic-Alias-Maximum'; +name(16#23) -> 'Topic-Alias'; +name(16#24) -> 'Maximum-QoS'; +name(16#25) -> 'Retain-Available'; +name(16#26) -> 'User-Property'; +name(16#27) -> 'Maximum-Packet-Size'; +name(16#28) -> 'Wildcard-Subscription-Available'; +name(16#29) -> 'Subscription-Identifier-Available'; +name(16#2A) -> 'Shared-Subscription-Available'; +name(Id) -> error({unsupported_property, Id}). + +-spec(filter(emqx_types:packet_type(), emqx_types:properties()|list()) + -> emqx_types:properties()). filter(PacketType, Props) when is_map(Props) -> maps:from_list(filter(PacketType, maps:to_list(Props))); -filter(PacketType, Props) when ?CONNECT =< PacketType, PacketType =< ?AUTH, is_list(Props) -> +filter(PacketType, Props) when ?CONNECT =< PacketType, + PacketType =< ?AUTH, + is_list(Props) -> Filter = fun(Name) -> case maps:find(id(Name), ?PROPS_TABLE) of {ok, {Name, _Type, 'ALL'}} -> @@ -125,6 +140,7 @@ filter(PacketType, Props) when ?CONNECT =< PacketType, PacketType =< ?AUTH, is_l end, [Prop || Prop = {Name, _} <- Props, Filter(Name)]. +-spec(validate(emqx_types:properties()) -> ok). validate(Props) when is_map(Props) -> lists:foreach(fun validate_prop/1, maps:to_list(Props)). @@ -132,23 +148,32 @@ validate_prop(Prop = {Name, Val}) -> case maps:find(id(Name), ?PROPS_TABLE) of {ok, {Name, Type, _}} -> validate_value(Type, Val) - orelse error(bad_property, Prop); + orelse error({bad_property_value, Prop}); error -> - error({bad_property, Prop}) + error({bad_property, Name}) end. validate_value('Byte', Val) -> - is_integer(Val); + is_integer(Val) andalso Val =< 16#FF; validate_value('Two-Byte-Integer', Val) -> - is_integer(Val); + is_integer(Val) andalso 0 =< Val andalso Val =< 16#FFFF; validate_value('Four-Byte-Integer', Val) -> - is_integer(Val); + is_integer(Val) andalso 0 =< Val andalso Val =< 16#FFFFFFFF; validate_value('Variable-Byte-Integer', Val) -> - is_integer(Val); + is_integer(Val) andalso 0 =< Val andalso Val =< 16#7FFFFFFF; +validate_value('UTF8-String-Pair', {Name, Val}) -> + validate_value('UTF8-Encoded-String', Name) + andalso validate_value('UTF8-Encoded-String', Val); +validate_value('UTF8-String-Pair', Pairs) when is_list(Pairs) -> + lists:foldl(fun(Pair, OK) -> + OK andalso validate_value('UTF8-String-Pair', Pair) + end, true, Pairs); validate_value('UTF8-Encoded-String', Val) -> is_binary(Val); validate_value('Binary-Data', Val) -> is_binary(Val); -validate_value('UTF8-String-Pair', Val) -> - is_tuple(Val) orelse is_list(Val). +validate_value(_Type, _Val) -> false. + +-spec(all() -> map()). +all() -> ?PROPS_TABLE. diff --git a/test/emqx_mqtt_props_SUITE.erl b/test/emqx_mqtt_props_SUITE.erl index b1a1b04b4..17ad975b6 100644 --- a/test/emqx_mqtt_props_SUITE.erl +++ b/test/emqx_mqtt_props_SUITE.erl @@ -20,23 +20,61 @@ -compile(nowarn_export_all). -include("emqx_mqtt.hrl"). +-include_lib("eunit/include/eunit.hrl"). +-include_lib("emqx_ct_helpers/include/emqx_ct.hrl"). all() -> emqx_ct:all(?MODULE). t_id(_) -> - 'TODO'. + foreach_prop( + fun({Id, Prop}) -> + ?assertEqual(Id, emqx_mqtt_props:id(element(1, Prop))) + end), + ?catch_error({bad_property, 'Bad-Property'}, emqx_mqtt_props:id('Bad-Property')). t_name(_) -> - 'TODO'. + foreach_prop( + fun({Id, Prop}) -> + ?assertEqual(emqx_mqtt_props:name(Id), element(1, Prop)) + end), + ?catch_error({unsupported_property, 16#FF}, emqx_mqtt_props:name(16#FF)). t_filter(_) -> - 'TODO'. + ConnProps = #{'Session-Expiry-Interval' => 1, + 'Maximum-Packet-Size' => 255 + }, + ?assertEqual(ConnProps, + emqx_mqtt_props:filter(?CONNECT, ConnProps)), + PubProps = #{'Payload-Format-Indicator' => 6, + 'Message-Expiry-Interval' => 300, + 'Session-Expiry-Interval' => 300 + }, + ?assertEqual(#{'Payload-Format-Indicator' => 6, + 'Message-Expiry-Interval' => 300 + }, + emqx_mqtt_props:filter(?PUBLISH, PubProps)). t_validate(_) -> - 'TODO'. + ConnProps = #{'Session-Expiry-Interval' => 1, + 'Maximum-Packet-Size' => 255 + }, + ok = emqx_mqtt_props:validate(ConnProps), + BadProps = #{'Unknown-Property' => 10}, + ?catch_error({bad_property,'Unknown-Property'}, + emqx_mqtt_props:validate(BadProps)). -deprecated_mqtt_properties_all(_) -> - Props = emqx_mqtt_props:filter(?CONNECT, #{'Session-Expiry-Interval' => 1, 'Maximum-Packet-Size' => 255}), - ok = emqx_mqtt_props:validate(Props), - #{} = emqx_mqtt_props:filter(?CONNECT, #{'Maximum-QoS' => ?QOS_2}). +t_validate_value(_) -> + ok = emqx_mqtt_props:validate(#{'Correlation-Data' => <<"correlation-id">>}), + ok = emqx_mqtt_props:validate(#{'Reason-String' => <<"Unknown Reason">>}), + ok = emqx_mqtt_props:validate(#{'User-Property' => {<<"Prop">>, <<"Val">>}}), + ok = emqx_mqtt_props:validate(#{'User-Property' => [{<<"Prop">>, <<"Val">>}]}), + ?catch_error({bad_property_value, {'Payload-Format-Indicator', 16#FFFF}}, + emqx_mqtt_props:validate(#{'Payload-Format-Indicator' => 16#FFFF})), + ?catch_error({bad_property_value, {'Server-Keep-Alive', 16#FFFFFF}}, + emqx_mqtt_props:validate(#{'Server-Keep-Alive' => 16#FFFFFF})), + ?catch_error({bad_property_value, {'Will-Delay-Interval', -16#FF}}, + emqx_mqtt_props:validate(#{'Will-Delay-Interval' => -16#FF})). + +foreach_prop(Fun) -> + lists:foreach(Fun, maps:to_list(emqx_mqtt_props:all())). From b2ddcb26e22dd5245cb94d958c71651376a96f29 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Fri, 16 Aug 2019 15:11:38 +0800 Subject: [PATCH 03/36] Testcase logf (#2800) * Add test cases for logger formatter * Remove the unregister API --- src/emqx_cm.erl | 19 +------- test/emqx_logger_formatter_SUITE.erl | 73 ++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 18 deletions(-) create mode 100644 test/emqx_logger_formatter_SUITE.erl diff --git a/src/emqx_cm.erl b/src/emqx_cm.erl index a4800e839..d23507d7c 100644 --- a/src/emqx_cm.erl +++ b/src/emqx_cm.erl @@ -28,8 +28,6 @@ -export([start_link/0]). -export([ register_channel/1 - , unregister_channel/1 - , unregister_channel/2 ]). -export([ get_chan_attrs/1 @@ -94,6 +92,7 @@ start_link() -> %%-------------------------------------------------------------------- %% @doc Register a channel. +%% Channel will be unregistered automatically when the channel process dies -spec(register_channel(emqx_types:client_id()) -> ok). register_channel(ClientId) when is_binary(ClientId) -> register_channel(ClientId, self()). @@ -106,17 +105,6 @@ register_channel(ClientId, ChanPid) -> ok = emqx_cm_registry:register_channel(Chan), cast({registered, Chan}). -%% @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_channel(emqx_types:client_id(), chan_pid()) -> ok). -unregister_channel(ClientId, ChanPid) -> - Chan = {ClientId, ChanPid}, - true = do_unregister_channel(Chan), - cast({unregistered, Chan}). - %% @private do_unregister_channel(Chan) -> ok = emqx_cm_registry:unregister_channel(Chan), @@ -285,10 +273,6 @@ handle_cast({registered, {ClientId, ChanPid}}, State = #{chan_pmon := PMon}) -> PMon1 = emqx_pmon:monitor(ChanPid, ClientId, PMon), {noreply, State#{chan_pmon := PMon1}}; -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, "Unexpected cast: ~p", [Msg]), {noreply, State}. @@ -325,4 +309,3 @@ update_stats({Tab, Stat, MaxStat}) -> undefined -> ok; Size -> emqx_stats:setstat(Stat, MaxStat, Size) end. - diff --git a/test/emqx_logger_formatter_SUITE.erl b/test/emqx_logger_formatter_SUITE.erl new file mode 100644 index 000000000..ea8180e9e --- /dev/null +++ b/test/emqx_logger_formatter_SUITE.erl @@ -0,0 +1,73 @@ +%%-------------------------------------------------------------------- +%% 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_logger_formatter_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("eunit/include/eunit.hrl"). + +all() -> emqx_ct:all(?MODULE). + +init_per_suite(Config) -> + Config. + +end_per_suite(_Config) -> + ok. + +t_chars_limit(_Config) -> + CharsLimit = 50, + LogFilename = "./t_chars_limit.log", + Formatter = {emqx_logger_formatter, + #{chars_limit => CharsLimit}}, + #{level := OrgLevel} = logger:get_primary_config(), + Config = + #{level => info, + config => #{ + type => halt, + file => LogFilename}, + formatter => Formatter}, + logger:add_handler(t_handler, logger_disk_log_h, Config), + logger:set_primary_config(level, info), + + logger:info("hello"), + logger:info(lists:duplicate(10, "hello")), + logger_disk_log_h:filesync(t_handler), + + ct:pal("content : ~p", [file:read_file(LogFilename)]), + [FirstLine, SecondLine] = readlines(LogFilename), + + ?assertMatch([_Date, _Time, _Level, "hello\n"], string:split(FirstLine, " ", all)), + ?assert(length(SecondLine) =< 50), + + logger:set_primary_config(level, OrgLevel). + + +readlines(FileName) -> + {ok, Device} = file:open(FileName, [read]), + try get_all_lines(Device) + after file:close(Device) + end. + +get_all_lines(Device) -> + get_all_lines(Device, []). +get_all_lines(Device, All) -> + case io:get_line(Device, "") of + eof -> + lists:reverse(All); + Line -> get_all_lines(Device, [Line | All]) + end. \ No newline at end of file From 8e401968f20300fa327fd1b16e78ddedfbec65e3 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Fri, 16 Aug 2019 16:30:32 +0800 Subject: [PATCH 04/36] Add testcase for emqx_mod_sup --- src/emqx_mod_sup.erl | 2 +- test/emqx_mod_sup_SUITE.erl | 51 +++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 test/emqx_mod_sup_SUITE.erl diff --git a/src/emqx_mod_sup.erl b/src/emqx_mod_sup.erl index cb6e86130..9685bc760 100644 --- a/src/emqx_mod_sup.erl +++ b/src/emqx_mod_sup.erl @@ -40,7 +40,7 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). -start_child(ChildSpec) when is_tuple(ChildSpec) -> +start_child(ChildSpec) when is_map(ChildSpec) -> supervisor:start_child(?MODULE, ChildSpec). start_child(Mod, Type) when is_atom(Mod) andalso is_atom(Type) -> diff --git a/test/emqx_mod_sup_SUITE.erl b/test/emqx_mod_sup_SUITE.erl new file mode 100644 index 000000000..fac49363e --- /dev/null +++ b/test/emqx_mod_sup_SUITE.erl @@ -0,0 +1,51 @@ +%%-------------------------------------------------------------------- +%% 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_mod_sup_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("eunit/include/eunit.hrl"). + +all() -> emqx_ct:all(?MODULE). + +%%-------------------------------------------------------------------- +%% Test cases +%%-------------------------------------------------------------------- + +t_start(_) -> + {ok, _} = emqx_mod_sup:start_link(), + ?assertEqual([], supervisor:which_children(emqx_mod_sup)). + +t_start_child(_) -> + %% Set the emqx_mod_sup child with emqx_hooks for test + Mod = emqx_hooks, + Spec = #{id => Mod, + start => {Mod, start_link, []}, + restart => permanent, + shutdown => 5000, + type => worker, + modules => [Mod]}, + + {ok, _} = emqx_mod_sup:start_link(), + {ok, _} = emqx_mod_sup:start_child(Mod, worker), + {error, {already_started, _}} = emqx_mod_sup:start_child(Spec), + + ok = emqx_mod_sup:stop_child(Mod), + {error, not_found} = emqx_mod_sup:stop_child(Mod), + ok. + From 79744af68128e189d5ac91a06b3d10ecc95cf22e Mon Sep 17 00:00:00 2001 From: Gilbert Date: Fri, 16 Aug 2019 18:07:12 +0800 Subject: [PATCH 05/36] Wrapper proper test cases into common test cases (#2785) * Wrapper proper test cases into common test cases * Improve test cases for reason code module (#2789) * Split 3 proper tests into 3 ct cases * Improve test cases for client, rpc and request-response * Add psk suites to increase coverage * Add sys test cases --- .travis.yml | 1 - Makefile | 8 +- include/emqx_mqtt.hrl | 14 + rebar.config | 3 +- src/emqx_broker.erl | 2 +- src/emqx_channel.erl | 1 - src/emqx_client.erl | 10 +- src/emqx_plugins.erl | 16 +- src/emqx_protocol.erl | 4 +- src/emqx_reason_codes.erl | 17 +- src/emqx_rpc.erl | 5 +- src/emqx_sys.erl | 44 ++- ...{prop_base62.erl => emqx_base62_SUITE.erl} | 30 +- test/emqx_ctl_SUTIES.erl | 17 + test/emqx_plugins_SUITE.erl | 26 ++ test/emqx_psk_SUITE.erl | 67 ++++ test/emqx_reason_codes_SUITE.erl | 225 +++++------ test/emqx_request_handler.erl | 94 +++++ test/emqx_request_responser_SUITE.erl | 69 ++++ test/emqx_request_sender.erl | 77 ++++ test/emqx_rpc_SUITE.erl | 124 ++++++ test/emqx_session_SUITE.erl | 374 +++++++++++++++--- test/emqx_sys_SUITE.erl | 136 +++++++ test/prop_emqx_session.erl | 327 --------------- 24 files changed, 1137 insertions(+), 554 deletions(-) rename test/{prop_base62.erl => emqx_base62_SUITE.erl} (58%) create mode 100644 test/emqx_ctl_SUTIES.erl create mode 100644 test/emqx_plugins_SUITE.erl create mode 100644 test/emqx_psk_SUITE.erl create mode 100644 test/emqx_request_handler.erl create mode 100644 test/emqx_request_responser_SUITE.erl create mode 100644 test/emqx_request_sender.erl create mode 100644 test/emqx_rpc_SUITE.erl create mode 100644 test/emqx_sys_SUITE.erl delete mode 100644 test/prop_emqx_session.erl diff --git a/.travis.yml b/.travis.yml index c0b7bf5b9..abe6f7a6a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,6 @@ script: - make xref - make eunit - make ct - - make proper - make cover after_success: diff --git a/Makefile b/Makefile index b0a1dce16..707380095 100644 --- a/Makefile +++ b/Makefile @@ -15,11 +15,7 @@ RUN_NODE_NAME = emqxdebug@127.0.0.1 all: compile .PHONY: tests -tests: eunit ct proper - -.PHONY: proper -proper: - @rebar3 proper +tests: eunit ct .PHONY: run run: run_setup unlock @@ -99,7 +95,7 @@ ct: ct_setup ## e.g. make ct-one-suite suite=emqx_bridge .PHONY: $(SUITES:%=ct-%) $(CT_SUITES:%=ct-%): ct_setup - @rebar3 ct -v --readable=false --name $(CT_NODE_NAME) --suite=$(@:ct-%=%)_SUITE + @rebar3 ct -v --readable=false --name $(CT_NODE_NAME) --suite=$(@:ct-%=%)_SUITE --cover .PHONY: app.config app.config: $(CUTTLEFISH_SCRIPT) etc/gen.emqx.conf diff --git a/include/emqx_mqtt.hrl b/include/emqx_mqtt.hrl index 854399999..cbffe27ee 100644 --- a/include/emqx_mqtt.hrl +++ b/include/emqx_mqtt.hrl @@ -298,6 +298,20 @@ payload :: binary() | undefined }). +%%-------------------------------------------------------------------- +%% MQTT Message Internal +%%-------------------------------------------------------------------- + +-record(mqtt_msg, { + qos = ?QOS_0, + retain = false, + dup = false, + packet_id, + topic, + props, + payload + }). + %%-------------------------------------------------------------------- %% MQTT Packet Match %%-------------------------------------------------------------------- diff --git a/rebar.config b/rebar.config index 0f49084bc..7ae5f1d87 100644 --- a/rebar.config +++ b/rebar.config @@ -21,8 +21,7 @@ {cover_opts, [verbose]}. {cover_export_enabled, true}. -{plugins, [coveralls, - rebar3_proper]}. +{plugins, [coveralls]}. {erl_first_files, ["src/emqx_logger.erl"]}. diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index 3992a0b35..359e65ab8 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -206,7 +206,7 @@ publish(Msg) when is_record(Msg, message) -> end. %% Called internally --spec(safe_publish(emqx_types:message()) -> ok). +-spec(safe_publish(emqx_types:message()) -> ok | emqx_types:publish_result()). safe_publish(Msg) when is_record(Msg, message) -> try publish(Msg) diff --git a/src/emqx_channel.erl b/src/emqx_channel.erl index 630c4de6a..c8a598ef1 100644 --- a/src/emqx_channel.erl +++ b/src/emqx_channel.erl @@ -674,4 +674,3 @@ shutdown(Reason, State) -> stop(Reason, State) -> {stop, Reason, State}. - diff --git a/src/emqx_client.erl b/src/emqx_client.erl index 511b36b6c..655218329 100644 --- a/src/emqx_client.erl +++ b/src/emqx_client.erl @@ -144,15 +144,7 @@ | {force_ping, boolean()} | {properties, properties()}). --record(mqtt_msg, { - qos = ?QOS_0, - retain = false, - dup = false, - packet_id, - topic, - props, - payload - }). + -opaque(mqtt_msg() :: #mqtt_msg{}). diff --git a/src/emqx_plugins.erl b/src/emqx_plugins.erl index 7b7b1b895..e75a70e83 100644 --- a/src/emqx_plugins.erl +++ b/src/emqx_plugins.erl @@ -81,9 +81,9 @@ load_expand_plugins() -> load_expand_plugin(PluginDir) -> init_expand_plugin_config(PluginDir), - Ebin = PluginDir ++ "/ebin", + Ebin = filename:join([PluginDir, "ebin"]), code:add_patha(Ebin), - Modules = filelib:wildcard(Ebin ++ "/*.beam"), + Modules = filelib:wildcard(filename:join([Ebin ++ "*.beam"])), lists:foreach(fun(Mod) -> Module = list_to_atom(filename:basename(Mod, ".beam")), code:load_file(Module) @@ -308,14 +308,11 @@ read_loaded() -> read_loaded(File) -> file:consult(File). write_loaded(AppNames) -> - File = emqx_config:get_env(plugins_loaded_file), - case file:open(File, [binary, write]) of - {ok, Fd} -> - lists:foreach(fun(Name) -> - file:write(Fd, iolist_to_binary(io_lib:format("~p.~n", [Name]))) - end, AppNames); + FilePath = emqx_config:get_env(plugins_loaded_file), + case file:write_file(FilePath, [io_lib:format("~p.~n", [Name]) || Name <- AppNames]) of + ok -> ok; {error, Error} -> - ?LOG(error, "Open File ~p Error: ~p", [File, Error]), + ?LOG(error, "Write File ~p Error: ~p", [FilePath, Error]), {error, Error} end. @@ -324,4 +321,3 @@ plugin_type(protocol) -> protocol; plugin_type(backend) -> backend; plugin_type(bridge) -> bridge; plugin_type(_) -> feature. - diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 3fa92ac70..6e0ac8336 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -181,7 +181,7 @@ handle_in(?CONNECT_PACKET( handle_out({disconnect, ReasonCode}, NPState) end; -handle_in(Packet = ?PUBLISH_PACKET(QoS, Topic, PacketId), PState) -> +handle_in(Packet = ?PUBLISH_PACKET(QoS, Topic, PacketId), PState= #protocol{proto_ver = Ver}) -> case pipeline([fun validate_in/2, fun process_alias/2, fun check_publish/2], Packet, PState) of @@ -189,7 +189,7 @@ handle_in(Packet = ?PUBLISH_PACKET(QoS, Topic, PacketId), PState) -> process_publish(NPacket, NPState); {error, ReasonCode, NPState} -> ?LOG(warning, "Cannot publish message to ~s due to ~s", - [Topic, emqx_reason_codes:text(ReasonCode)]), + [Topic, emqx_reason_codes:text(ReasonCode, Ver)]), puback(QoS, PacketId, ReasonCode, NPState) end; diff --git a/src/emqx_reason_codes.erl b/src/emqx_reason_codes.erl index ef4c0cedd..387208ebe 100644 --- a/src/emqx_reason_codes.erl +++ b/src/emqx_reason_codes.erl @@ -22,6 +22,7 @@ -export([ name/1 , name/2 , text/1 + , text/2 , connack_error/1 , puback/1 ]). @@ -30,7 +31,7 @@ name(I, Ver) when Ver >= ?MQTT_PROTO_V5 -> name(I); -name(0, _Ver) -> connection_acceptd; +name(0, _Ver) -> connection_accepted; name(1, _Ver) -> unacceptable_protocol_version; name(2, _Ver) -> client_identifier_not_valid; name(3, _Ver) -> server_unavaliable; @@ -83,6 +84,16 @@ name(16#A1) -> subscription_identifiers_not_supported; name(16#A2) -> wildcard_subscriptions_not_supported; name(_Code) -> unknown_error. +text(I, Ver) when Ver >= ?MQTT_PROTO_V5 -> + text(I); +text(0, _Ver) -> <<"Connection accepted">>; +text(1, _Ver) -> <<"unacceptable_protocol_version">>; +text(2, _Ver) -> <<"client_identifier_not_valid">>; +text(3, _Ver) -> <<"server_unavaliable">>; +text(4, _Ver) -> <<"malformed_username_or_password">>; +text(5, _Ver) -> <<"unauthorized_client">>; +text(_, _Ver) -> <<"unknown_error">>. + text(16#00) -> <<"Success">>; text(16#01) -> <<"Granted QoS 1">>; text(16#02) -> <<"Granted QoS 2">>; @@ -150,7 +161,8 @@ compat(connack, 16#9F) -> ?CONNACK_SERVER; compat(suback, Code) when Code =< ?QOS_2 -> Code; compat(suback, Code) when Code >= 16#80 -> 16#80; -compat(unsuback, _Code) -> undefined. +compat(unsuback, _Code) -> undefined; +compat(_Other, _Code) -> undefined. connack_error(client_identifier_not_valid) -> ?RC_CLIENT_IDENTIFIER_NOT_VALID; connack_error(bad_username_or_password) -> ?RC_BAD_USER_NAME_OR_PASSWORD; @@ -167,4 +179,3 @@ connack_error(_) -> ?RC_NOT_AUTHORIZED. %%TODO: This function should be removed. puback([]) -> ?RC_NO_MATCHING_SUBSCRIBERS; puback(L) when is_list(L) -> ?RC_SUCCESS. - diff --git a/src/emqx_rpc.erl b/src/emqx_rpc.erl index c66d938e2..6af676802 100644 --- a/src/emqx_rpc.erl +++ b/src/emqx_rpc.erl @@ -29,6 +29,8 @@ -define(RPC, gen_rpc). +-define(DefaultClientNum, 1). + call(Node, Mod, Fun, Args) -> filter_result(?RPC:call(rpc_node(Node), Mod, Fun, Args)). @@ -39,7 +41,7 @@ cast(Node, Mod, Fun, Args) -> filter_result(?RPC:cast(rpc_node(Node), Mod, Fun, Args)). rpc_node(Node) -> - {ok, ClientNum} = application:get_env(gen_rpc, tcp_client_num), + ClientNum = application:get_env(gen_rpc, tcp_client_num, ?DefaultClientNum), {Node, rand:uniform(ClientNum)}. rpc_nodes(Nodes) -> @@ -55,4 +57,3 @@ filter_result({Error, Reason}) {badrpc, Reason}; filter_result(Delivery) -> Delivery. - diff --git a/src/emqx_sys.erl b/src/emqx_sys.erl index f024d2504..41b27729b 100644 --- a/src/emqx_sys.erl +++ b/src/emqx_sys.erl @@ -23,7 +23,9 @@ -logger_header("[SYS]"). --export([start_link/0]). +-export([ start_link/0 + , stop/0 + ]). -export([ version/0 , uptime/0 @@ -41,23 +43,36 @@ , handle_cast/2 , handle_info/2 , terminate/2 - , code_change/3 ]). -import(emqx_topic, [systop/1]). -import(emqx_misc, [start_timer/2]). --record(state, {start_time, heartbeat, ticker, version, sysdescr}). +-type(timeref() :: reference()). + +-type(tickeref() :: reference()). + +-type(version() :: string()). + +-type(sysdescr() :: string()). + +-record(state, + { start_time :: erlang:timestamp() + , heartbeat :: timeref() + , ticker :: tickeref() + , version :: version() + , sysdescr :: sysdescr() + }). -define(APP, emqx). -define(SYS, ?MODULE). --define(INFO_KEYS, [ - version, % Broker version - uptime, % Broker uptime - datetime, % Broker local datetime - sysdescr % Broker description -]). +-define(INFO_KEYS, + [ version % Broker version + , uptime % Broker uptime + , datetime % Broker local datetime + , sysdescr % Broker description + ]). %%------------------------------------------------------------------------------ %% APIs @@ -67,6 +82,9 @@ start_link() -> gen_server:start_link({local, ?SYS}, ?MODULE, [], []). +stop() -> + gen_server:stop(?SYS). + %% @doc Get sys version -spec(version() -> string()). version() -> @@ -93,12 +111,12 @@ datetime() -> %% @doc Get sys interval -spec(sys_interval() -> pos_integer()). sys_interval() -> - application:get_env(?APP, broker_sys_interval, 60000). + emqx_config:get_env(broker_sys_interval, 60000). %% @doc Get sys heatbeat interval -spec(sys_heatbeat_interval() -> pos_integer()). sys_heatbeat_interval() -> - application:get_env(?APP, broker_sys_heartbeat, 30000). + emqx_config:get_env(broker_sys_heartbeat, 30000). %% @doc Get sys info -spec(info() -> list(tuple())). @@ -154,9 +172,6 @@ handle_info(Info, State) -> terminate(_Reason, #state{heartbeat = TRef1, ticker = TRef2}) -> lists:foreach(fun emqx_misc:cancel_timer/1, [TRef1, TRef2]). -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - %%----------------------------------------------------------------------------- %% Internal functions %%----------------------------------------------------------------------------- @@ -207,4 +222,3 @@ safe_publish(Topic, Flags, Payload) -> emqx_message:set_flags( maps:merge(#{sys => true}, Flags), emqx_message:make(?SYS, Topic, iolist_to_binary(Payload)))). - diff --git a/test/prop_base62.erl b/test/emqx_base62_SUITE.erl similarity index 58% rename from test/prop_base62.erl rename to test/emqx_base62_SUITE.erl index a660012db..83d11ae1c 100644 --- a/test/prop_base62.erl +++ b/test/emqx_base62_SUITE.erl @@ -1,5 +1,33 @@ --module(prop_base62). +%%-------------------------------------------------------------------- +%% 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_base62_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + -include_lib("proper/include/proper.hrl"). +-include_lib("eunit/include/eunit.hrl"). + +all() -> emqx_ct:all(?MODULE). + +t_proper_base62(_) -> + Opts = [{numtests, 100}, {to_file, user}], + ?assert(proper:quickcheck(prop_symmetric(), Opts)), + ?assert(proper:quickcheck(prop_size(), Opts)). %%%%%%%%%%%%%%%%%% %%% Properties %%% diff --git a/test/emqx_ctl_SUTIES.erl b/test/emqx_ctl_SUTIES.erl new file mode 100644 index 000000000..a3ce8e8b0 --- /dev/null +++ b/test/emqx_ctl_SUTIES.erl @@ -0,0 +1,17 @@ +%%-------------------------------------------------------------------- +%% 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_ctl_SUTIES). diff --git a/test/emqx_plugins_SUITE.erl b/test/emqx_plugins_SUITE.erl new file mode 100644 index 000000000..5c4d43f24 --- /dev/null +++ b/test/emqx_plugins_SUITE.erl @@ -0,0 +1,26 @@ +%%-------------------------------------------------------------------- +%% 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_plugins_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("proper/include/proper.hrl"). +-include_lib("eunit/include/eunit.hrl"). + + +all() -> emqx_ct:all(?MODULE). diff --git a/test/emqx_psk_SUITE.erl b/test/emqx_psk_SUITE.erl new file mode 100644 index 000000000..2cb4b5894 --- /dev/null +++ b/test/emqx_psk_SUITE.erl @@ -0,0 +1,67 @@ +%%-------------------------------------------------------------------- +%% 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_psk_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("proper/include/proper.hrl"). +-include_lib("eunit/include/eunit.hrl"). +-include_lib("common_test/include/ct.hrl"). + +all() -> emqx_ct:all(?MODULE). + +t_lookup(_) -> + ok = load(), + ok = emqx_logger:set_log_level(emergency), + Opts = [{to_file, user}, {numtests, 10}], + ?assert(proper:quickcheck(prop_lookup(), Opts)), + ok = unload(), + ok = emqx_logger:set_log_level(error). + +prop_lookup() -> + ?FORALL({ClientPSKID, UserState}, + {client_pskid(), user_state()}, + begin + case emqx_psk:lookup(psk, ClientPSKID, UserState) of + {ok, _Result} -> true; + error -> true; + _Other -> false + end + end). + +%%-------------------------------------------------------------------- +%% Helper +%%-------------------------------------------------------------------- + +load() -> + ok = meck:new(emqx_hooks, [passthrough, no_history]), + ok = meck:expect(emqx_hooks, run_fold, + fun('tls_handshake.psk_lookup', [ClientPSKID], not_found) -> + unicode:characters_to_binary(ClientPSKID) + end). + +unload() -> + ok = meck:unload(emqx_hooks). + +%%-------------------------------------------------------------------- +%% Generator +%%-------------------------------------------------------------------- + +client_pskid() -> oneof([string(), integer(), [1, [-1]]]). + +user_state() -> term(). diff --git a/test/emqx_reason_codes_SUITE.erl b/test/emqx_reason_codes_SUITE.erl index c0f1c0e74..82acb8091 100644 --- a/test/emqx_reason_codes_SUITE.erl +++ b/test/emqx_reason_codes_SUITE.erl @@ -20,121 +20,126 @@ -compile(nowarn_export_all). -include("emqx_mqtt.hrl"). +-include_lib("proper/include/proper.hrl"). -include_lib("eunit/include/eunit.hrl"). --import(lists, [seq/2, zip/2, foreach/2]). - --define(MQTTV4_CODE_NAMES, [connection_acceptd, - unacceptable_protocol_version, - client_identifier_not_valid, - server_unavaliable, - malformed_username_or_password, - unauthorized_client, - unknown_error]). - --define(MQTTV5_CODE_NAMES, [success, granted_qos1, granted_qos2, disconnect_with_will_message, - no_matching_subscribers, no_subscription_existed, continue_authentication, - re_authenticate, unspecified_error, malformed_Packet, protocol_error, - implementation_specific_error, unsupported_protocol_version, - client_identifier_not_valid, bad_username_or_password, not_authorized, - server_unavailable, server_busy, banned,server_shutting_down, - bad_authentication_method, keepalive_timeout, session_taken_over, - topic_filter_invalid, topic_name_invalid, packet_identifier_inuse, - packet_identifier_not_found, receive_maximum_exceeded, topic_alias_invalid, - packet_too_large, message_rate_too_high, quota_exceeded, - administrative_action, payload_format_invalid, retain_not_supported, - qos_not_supported, use_another_server, server_moved, - shared_subscriptions_not_supported, connection_rate_exceeded, - maximum_connect_time, subscription_identifiers_not_supported, - wildcard_subscriptions_not_supported, unknown_error]). - --define(MQTTV5_CODES, [16#00, 16#01, 16#02, 16#04, 16#10, 16#11, 16#18, 16#19, 16#80, 16#81, 16#82, - 16#83, 16#84, 16#85, 16#86, 16#87, 16#88, 16#89, 16#8A, 16#8B, 16#8C, 16#8D, - 16#8E, 16#8F, 16#90, 16#91, 16#92, 16#93, 16#94, 16#95, 16#96, 16#97, 16#98, - 16#99, 16#9A, 16#9B, 16#9C, 16#9D, 16#9E, 16#9F, 16#A0, 16#A1, 16#A2, code]). - --define(MQTTV5_TXT, [<<"Success">>, <<"Granted QoS 1">>, <<"Granted QoS 2">>, - <<"Disconnect with Will Message">>, <<"No matching subscribers">>, - <<"No subscription existed">>, <<"Continue authentication">>, - <<"Re-authenticate">>, <<"Unspecified error">>, <<"Malformed Packet">>, - <<"Protocol Error">>, <<"Implementation specific error">>, - <<"Unsupported Protocol Version">>, <<"Client Identifier not valid">>, - <<"Bad User Name or Password">>, <<"Not authorized">>, - <<"Server unavailable">>, <<"Server busy">>, <<"Banned">>, - <<"Server shutting down">>, <<"Bad authentication method">>, - <<"Keep Alive timeout">>, <<"Session taken over">>, - <<"Topic Filter invalid">>, <<"Topic Name invalid">>, - <<"Packet Identifier in use">>, <<"Packet Identifier not found">>, - <<"Receive Maximum exceeded">>, <<"Topic Alias invalid">>, - <<"Packet too large">>, <<"Message rate too high">>, <<"Quota exceeded">>, - <<"Administrative action">>, <<"Payload format invalid">>, - <<"Retain not supported">>, <<"QoS not supported">>, - <<"Use another server">>, <<"Server moved">>, - <<"Shared Subscriptions not supported">>, <<"Connection rate exceeded">>, - <<"Maximum connect time">>, <<"Subscription Identifiers not supported">>, - <<"Wildcard Subscriptions not supported">>, <<"Unknown error">>]). - --define(COMPAT_CODES_V5, [16#80, 16#81, 16#82, 16#83, 16#84, 16#85, 16#86, 16#87, - 16#88, 16#89, 16#8A, 16#8B, 16#8C, 16#97, 16#9C, 16#9D, - 16#9F]). - --define(COMPAT_CODES_V4, [?CONNACK_PROTO_VER, ?CONNACK_PROTO_VER, ?CONNACK_PROTO_VER, - ?CONNACK_PROTO_VER, ?CONNACK_PROTO_VER, - ?CONNACK_INVALID_ID, - ?CONNACK_CREDENTIALS, - ?CONNACK_AUTH, - ?CONNACK_SERVER, - ?CONNACK_SERVER, - ?CONNACK_AUTH, - ?CONNACK_SERVER, - ?CONNACK_AUTH, - ?CONNACK_SERVER, ?CONNACK_SERVER, ?CONNACK_SERVER, ?CONNACK_SERVER]). - all() -> emqx_ct:all(?MODULE). -t_mqttv4_name(_) -> - (((codes_test(?MQTT_PROTO_V4)) - (seq(0,6))) - (?MQTTV4_CODE_NAMES)) - (fun emqx_reason_codes:name/2). +t_prop_name_text(_) -> + ?assert(proper:quickcheck(prop_name_text(), prop_name_text(opts))). -t_mqttv5_name(_) -> - (((codes_test(?MQTT_PROTO_V5)) - (?MQTTV5_CODES)) - (?MQTTV5_CODE_NAMES)) - (fun emqx_reason_codes:name/2). +t_prop_compat(_) -> + ?assert(proper:quickcheck(prop_compat(), prop_compat(opts))). -t_text(_) -> - (((codes_test(?MQTT_PROTO_V5)) - (?MQTTV5_CODES)) - (?MQTTV5_TXT)) - (fun emqx_reason_codes:text/1). +t_prop_connack_error(_) -> + ?assert(proper:quickcheck(prop_connack_error(), default_opts([]))). -t_compat(_) -> - (((codes_test(connack)) - (?COMPAT_CODES_V5)) - (?COMPAT_CODES_V4)) - (fun emqx_reason_codes:compat/2), - (((codes_test(suback)) - ([0,1,2, 16#80])) - ([0,1,2, 16#80])) - (fun emqx_reason_codes:compat/2), - (((codes_test(unsuback)) - ([0, 1, 2])) - ([undefined, undefined, undefined])) - (fun emqx_reason_codes:compat/2). +prop_name_text(opts) -> + default_opts([{numtests, 1000}]). -codes_test(AsistVar) -> - fun(CODES) -> - fun(NAMES) -> - fun(Procedure) -> - foreach(fun({Code, Result}) -> - ?assertEqual(Result, case erlang:fun_info(Procedure, name) of - {name, text} -> Procedure(Code); - {name, name} -> Procedure(Code, AsistVar); - {name, compat} -> Procedure(AsistVar, Code) - end) - end, zip(CODES, NAMES)) - end - end - end. +prop_name_text() -> + ?FORALL(UnionArgs, union_args(), + is_atom(apply_fun(name, UnionArgs)) andalso + is_binary(apply_fun(text, UnionArgs))). + +prop_compat(opts) -> + default_opts([{numtests, 512}]). + +prop_compat() -> + ?FORALL(CompatArgs, compat_args(), + begin + Result = apply_fun(compat, CompatArgs), + is_number(Result) orelse Result =:= undefined + end). + +prop_connack_error() -> + ?FORALL(CONNACK_ERROR_ARGS, connack_error_args(), + is_integer(apply_fun(connack_error, CONNACK_ERROR_ARGS))). + +%%-------------------------------------------------------------------- +%% Helper +%%-------------------------------------------------------------------- +default_opts() -> + default_opts([]). + +default_opts(AdditionalOpts) -> + [{to_file, user} | AdditionalOpts]. + +apply_fun(Fun, Args) -> + apply(emqx_reason_codes, Fun, Args). + +%%-------------------------------------------------------------------- +%% Generator +%%-------------------------------------------------------------------- + +union_args() -> + frequency([{6, [real_mqttv3_rc(), mqttv3_version()]}, + {43, [real_mqttv5_rc(), mqttv5_version()]}]). + +compat_args() -> + frequency([{18, [connack, compat_rc()]}, + {2, [suback, compat_rc()]}, + {1, [unsuback, compat_rc()]}]). + +connack_error_args() -> + [frequency([{10, connack_error()}, + {1, unexpected_connack_error()}])]. + +connack_error() -> + oneof([client_identifier_not_valid, + bad_username_or_password, + bad_clientid_or_password, + username_or_password_undefined, + password_error, + not_authorized, + server_unavailable, + server_busy, + banned, + bad_authentication_method]). + +unexpected_connack_error() -> + oneof([who_knows]). + + +real_mqttv3_rc() -> + frequency([{6, mqttv3_rc()}, + {1, unexpected_rc()}]). + +real_mqttv5_rc() -> + frequency([{43, mqttv5_rc()}, + {2, unexpected_rc()}]). + +compat_rc() -> + frequency([{95, ?SUCHTHAT(RC , mqttv5_rc(), RC >= 16#80 orelse RC =< 2)}, + {5, unexpected_rc()}]). + +mqttv3_rc() -> + oneof(mqttv3_rcs()). + +mqttv5_rc() -> + oneof(mqttv5_rcs()). + +unexpected_rc() -> + oneof(unexpected_rcs()). + +mqttv3_rcs() -> + [0, 1, 2, 3, 4, 5]. + +mqttv5_rcs() -> + [16#00, 16#01, 16#02, 16#04, 16#10, 16#11, 16#18, 16#19, + 16#80, 16#81, 16#82, 16#83, 16#84, 16#85, 16#86, 16#87, + 16#88, 16#89, 16#8A, 16#8B, 16#8C, 16#8D, 16#8E, 16#8F, + 16#90, 16#91, 16#92, 16#93, 16#94, 16#95, 16#96, 16#97, + 16#98, 16#99, 16#9A, 16#9B, 16#9C, 16#9D, 16#9E, 16#9F, + 16#A0, 16#A1, 16#A2]. + +unexpected_rcs() -> + ReasonCodes = mqttv3_rcs() ++ mqttv5_rcs(), + Unexpected = lists:seq(0, 16#FF) -- ReasonCodes, + lists:sublist(Unexpected, 5). + +mqttv5_version() -> + ?MQTT_PROTO_V5. + +mqttv3_version() -> + oneof([?MQTT_PROTO_V3, ?MQTT_PROTO_V4]). diff --git a/test/emqx_request_handler.erl b/test/emqx_request_handler.erl new file mode 100644 index 000000000..f2f218638 --- /dev/null +++ b/test/emqx_request_handler.erl @@ -0,0 +1,94 @@ +%%-------------------------------------------------------------------- +%% 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_request_handler). + +-export([start_link/4, stop/1]). + +-include("emqx_mqtt.hrl"). + +-type qos() :: emqx_mqtt_types:qos_name() | emqx_mqtt_types:qos(). +-type topic() :: emqx_topic:topic(). +-type handler() :: fun((CorrData :: binary(), ReqPayload :: binary()) -> RspPayload :: binary()). + +-spec start_link(topic(), qos(), handler(), emqx_client:options()) -> + {ok, pid()} | {error, any()}. +start_link(RequestTopic, QoS, RequestHandler, Options0) -> + Parent = self(), + MsgHandler = make_msg_handler(RequestHandler, Parent), + Options = [{msg_handler, MsgHandler} | Options0], + case emqx_client:start_link(Options) of + {ok, Pid} -> + {ok, _} = emqx_client:connect(Pid), + try subscribe(Pid, RequestTopic, QoS) of + ok -> {ok, Pid}; + {error, _} = Error -> Error + catch + C : E : S -> + emqx_client:stop(Pid), + {error, {C, E, S}} + end; + {error, _} = Error -> Error + end. + +stop(Pid) -> + emqx_client:disconnect(Pid). + +make_msg_handler(RequestHandler, Parent) -> + #{publish => fun(Msg) -> handle_msg(Msg, RequestHandler, Parent) end, + puback => fun(_Ack) -> ok end, + disconnected => fun(_Reason) -> ok end + }. + +handle_msg(ReqMsg, RequestHandler, Parent) -> + #{qos := QoS, properties := Props, payload := ReqPayload} = ReqMsg, + case maps:find('Response-Topic', Props) of + {ok, RspTopic} when RspTopic =/= <<>> -> + CorrData = maps:get('Correlation-Data', Props), + RspProps = maps:without(['Response-Topic'], Props), + RspPayload = RequestHandler(CorrData, ReqPayload), + RspMsg = #mqtt_msg{qos = QoS, + topic = RspTopic, + props = RspProps, + payload = RspPayload + }, + emqx_logger:debug("~p sending response msg to topic ~s with~n" + "corr-data=~p~npayload=~p", + [?MODULE, RspTopic, CorrData, RspPayload]), + ok = send_response(RspMsg); + _ -> + Parent ! {discarded, ReqPayload}, + ok + end. + +send_response(Msg) -> + %% This function is evaluated by emqx_client itself. + %% hence delegate to another temp process for the loopback gen_statem call. + Client = self(), + _ = spawn_link(fun() -> + case emqx_client:publish(Client, Msg) of + ok -> ok; + {ok, _} -> ok; + {error, Reason} -> exit({failed_to_publish_response, Reason}) + end + end), + ok. + +subscribe(Client, Topic, QoS) -> + {ok, _Props, _QoS} = + emqx_client:subscribe(Client, [{Topic, [{rh, 2}, {rap, false}, + {nl, true}, {qos, QoS}]}]), + ok. diff --git a/test/emqx_request_responser_SUITE.erl b/test/emqx_request_responser_SUITE.erl new file mode 100644 index 000000000..4912f72ea --- /dev/null +++ b/test/emqx_request_responser_SUITE.erl @@ -0,0 +1,69 @@ +%% 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_request_responser_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"). + +init_per_suite(Config) -> + emqx_ct_helpers:start_apps([]), + Config. + +end_per_suite(_Config) -> + emqx_ct_helpers:stop_apps([]). + +all() -> + [request_response]. + +request_response(_Config) -> + request_response_per_qos(?QOS_0), + request_response_per_qos(?QOS_1), + request_response_per_qos(?QOS_2). + +request_response_per_qos(QoS) -> + ReqTopic = <<"request_topic">>, + RspTopic = <<"response_topic">>, + {ok, Requester} = emqx_request_sender:start_link(RspTopic, QoS, + [{proto_ver, v5}, + {client_id, <<"requester">>}, + {properties, #{ 'Request-Response-Information' => 1}}]), + %% This is a square service + Square = fun(_CorrData, ReqBin) -> + I = b2i(ReqBin), + i2b(I * I) + end, + {ok, Responser} = emqx_request_handler:start_link(ReqTopic, QoS, Square, + [{proto_ver, v5}, + {client_id, <<"responser">>} + ]), + ok = emqx_request_sender:send(Requester, ReqTopic, RspTopic, <<"corr-1">>, <<"2">>, QoS), + receive + {response, <<"corr-1">>, <<"4">>} -> + ok; + Other -> + erlang:error({unexpected, Other}) + after + 100 -> + erlang:error(timeout) + end, + ok = emqx_request_sender:stop(Requester), + ok = emqx_request_handler:stop(Responser). + +b2i(B) -> binary_to_integer(B). +i2b(I) -> integer_to_binary(I). diff --git a/test/emqx_request_sender.erl b/test/emqx_request_sender.erl new file mode 100644 index 000000000..27f3a45ac --- /dev/null +++ b/test/emqx_request_sender.erl @@ -0,0 +1,77 @@ +%%-------------------------------------------------------------------- +%% 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_request_sender). + +-export([start_link/3, stop/1, send/6]). + +-include("emqx_mqtt.hrl"). + +start_link(ResponseTopic, QoS, Options0) -> + Parent = self(), + MsgHandler = make_msg_handler(Parent), + Options = [{msg_handler, MsgHandler} | Options0], + case emqx_client:start_link(Options) of + {ok, Pid} -> + {ok, _} = emqx_client:connect(Pid), + try subscribe(Pid, ResponseTopic, QoS) of + ok -> {ok, Pid}; + {error, _} = Error -> Error + catch + C : E : S -> + emqx_client:stop(Pid), + {error, {C, E, S}} + end; + {error, _} = Error -> Error + end. + +%% @doc Send a message to request topic with correlation-data `CorrData'. +%% Response should be delivered as a `{response, CorrData, Payload}' +send(Client, ReqTopic, RspTopic, CorrData, Payload, QoS) -> + Props = #{'Response-Topic' => RspTopic, + 'Correlation-Data' => CorrData + }, + Msg = #mqtt_msg{qos = QoS, + topic = ReqTopic, + props = Props, + payload = Payload + }, + case emqx_client:publish(Client, Msg) of + ok -> ok; %% QoS = 0 + {ok, _} -> ok; + {error, _} = E -> E + end. + +stop(Pid) -> + emqx_client:disconnect(Pid). + +subscribe(Client, Topic, QoS) -> + case emqx_client:subscribe(Client, Topic, QoS) of + {ok, _, _} -> ok; + {error, _} = Error -> Error + end. + +make_msg_handler(Parent) -> + #{publish => fun(Msg) -> handle_msg(Msg, Parent) end, + puback => fun(_Ack) -> ok end, + disconnected => fun(_Reason) -> ok end + }. + +handle_msg(Msg, Parent) -> + #{properties := Props, payload := Payload} = Msg, + CorrData = maps:get('Correlation-Data', Props), + Parent ! {response, CorrData, Payload}, + ok. diff --git a/test/emqx_rpc_SUITE.erl b/test/emqx_rpc_SUITE.erl new file mode 100644 index 000000000..62b597e1e --- /dev/null +++ b/test/emqx_rpc_SUITE.erl @@ -0,0 +1,124 @@ +%%-------------------------------------------------------------------- +%% 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_rpc_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("proper/include/proper.hrl"). +-include_lib("eunit/include/eunit.hrl"). + +all() -> emqx_ct:all(?MODULE). + +t_prop_rpc(_) -> + ok = load(), + Opts = [{to_file, user}, {numtests, 10}], + {ok, _Apps} = application:ensure_all_started(gen_rpc), + ok = application:set_env(gen_rpc, call_receive_timeout, 1), + ok = emqx_logger:set_log_level(emergency), + ?assert(proper:quickcheck(prop_node(), Opts)), + ?assert(proper:quickcheck(prop_nodes(), Opts)), + ok = application:stop(gen_rpc), + ok = unload(). + +prop_node() -> + ?FORALL(Node, nodename(), + begin + ?assert(emqx_rpc:cast(Node, erlang, system_time, [])), + case emqx_rpc:call(Node, erlang, system_time, []) of + {badrpc, _Reason} -> true; + Delivery when is_integer(Delivery) -> true; + _Other -> false + end + end). + +prop_nodes() -> + ?FORALL(Nodes, nodesname(), + begin + case emqx_rpc:multicall(Nodes, erlang, system_time, []) of + {badrpc, _Reason} -> true; + {RealResults, RealBadNodes} + when is_list(RealResults); + is_list(RealBadNodes) -> + true; + _Other -> false + end + end). + +%%-------------------------------------------------------------------- +%% helper +%%-------------------------------------------------------------------- + +load() -> + ok = meck:new(gen_rpc, [passthrough, no_history]), + ok = meck:expect(gen_rpc, multicall, + fun(Nodes, Mod, Fun, Args) -> + gen_rpc:multicall(Nodes, Mod, Fun, Args, 1) + end). + +unload() -> + ok = meck:unload(gen_rpc). + +%%-------------------------------------------------------------------- +%% Generator +%%-------------------------------------------------------------------- + +nodename() -> + ?LET({NodePrefix, HostName}, + {node_prefix(), hostname()}, + begin + Node = NodePrefix ++ "@" ++ HostName, + list_to_atom(Node) + end). + +nodesname() -> + oneof([list(nodename()), ["emqxct@127.0.0.1"]]). + +node_prefix() -> + oneof(["emqxct", text_like()]). + +text_like() -> + ?SUCHTHAT(Text, list(range($a, $z)), (length(Text) =< 5 andalso length(Text) > 0)). + +hostname() -> + oneof([ipv4_address(), ipv6_address(), "127.0.0.1", "localhost"]). + +ipv4_address() -> + ?LET({Num1, Num2, Num3, Num4}, + { choose(0, 255) + , choose(0, 255) + , choose(0, 255) + , choose(0, 255)}, + make_ip([Num1, Num2, Num3, Num4], ipv4)). + +ipv6_address() -> + ?LET({Num1, Num2, Num3, Num4, Num5, Num6}, + { choose(0, 65535) + , choose(0, 65535) + , choose(0, 65535) + , choose(0, 65535) + , choose(0, 65535) + , choose(0, 65535)}, + make_ip([Num1, Num2, Num3, Num4, Num5, Num6], ipv6)). + + +make_ip(NumList, ipv4) when is_list(NumList) -> + string:join([integer_to_list(Num) || Num <- NumList], "."); +make_ip(NumList, ipv6) when is_list(NumList) -> + string:join([integer_to_list(Num) || Num <- NumList], ":"); +make_ip(_List, _protocol) -> + "127.0.0.1". diff --git a/test/emqx_session_SUITE.erl b/test/emqx_session_SUITE.erl index 8736d69b7..224b8afaf 100644 --- a/test/emqx_session_SUITE.erl +++ b/test/emqx_session_SUITE.erl @@ -19,87 +19,333 @@ -compile(export_all). -compile(nowarn_export_all). +-include("emqx_mqtt.hrl"). +-include_lib("proper/include/proper.hrl"). -include_lib("eunit/include/eunit.hrl"). +-define(mock_modules, + [ emqx_metrics + , emqx_broker + , emqx_misc + , emqx_message + , emqx_hooks + , emqx_zone + , emqx_pd + ]). + all() -> emqx_ct:all(?MODULE). -init_per_suite(Config) -> - emqx_ct_helpers:start_apps([]), - Config. +t_proper_session(_) -> + Opts = [{numtests, 1000}, {to_file, user}], + ok = emqx_logger:set_log_level(emergency), + ok = before_proper(), + ?assert(proper:quickcheck(prop_session(), Opts)), + ok = after_proper(). -end_per_suite(_Config) -> - emqx_ct_helpers:stop_apps([]). +before_proper() -> + load(?mock_modules). -t_info(_) -> - 'TODO'. +after_proper() -> + unload(?mock_modules), + emqx_logger:set_log_level(error). -t_attrs(_) -> - 'TODO'. +prop_session() -> + ?FORALL({Session, OpList}, {session(), session_op_list()}, + begin + try + apply_ops(Session, OpList), + true + after + true + end + end). -t_stats(_) -> - 'TODO'. +%%%%%%%%%%%%%%% +%%% Helpers %%% +%%%%%%%%%%%%%%% -t_subscribe(_) -> - 'TODO'. +apply_ops(Session, []) -> + ?assertEqual(session, element(1, Session)); +apply_ops(Session, [Op | Rest]) -> + NSession = apply_op(Session, Op), + apply_ops(NSession, Rest). -t_unsubscribe(_) -> - 'TODO'. +apply_op(Session, info) -> + Info = emqx_session:info(Session), + ?assert(is_map(Info)), + ?assertEqual(16, maps:size(Info)), + Session; +apply_op(Session, attrs) -> + Attrs = emqx_session:attrs(Session), + ?assert(is_map(Attrs)), + ?assertEqual(3, maps:size(Attrs)), + Session; +apply_op(Session, stats) -> + Stats = emqx_session:stats(Session), + ?assert(is_list(Stats)), + ?assertEqual(9, length(Stats)), + Session; +apply_op(Session, {info, InfoArg}) -> + _Ret = emqx_session:info(InfoArg, Session), + Session; +apply_op(Session, {subscribe, {Client, TopicFilter, SubOpts}}) -> + case emqx_session:subscribe(Client, TopicFilter, SubOpts, Session) of + {ok, NSession} -> + NSession; + {error, ?RC_QUOTA_EXCEEDED} -> + Session + end; +apply_op(Session, {unsubscribe, {Client, TopicFilter}}) -> + case emqx_session:unsubscribe(Client, TopicFilter, Session) of + {ok, NSession} -> + NSession; + {error, ?RC_NO_SUBSCRIPTION_EXISTED} -> + Session + end; +apply_op(Session, {publish, {PacketId, Msg}}) -> + case emqx_session:publish(PacketId, Msg, Session) of + {ok, _Msg} -> + Session; + {ok, _Deliver, NSession} -> + NSession; + {error, _ErrorCode} -> + Session + end; +apply_op(Session, {puback, PacketId}) -> + case emqx_session:puback(PacketId, Session) of + {ok, _Msg} -> + Session; + {ok, _Deliver, NSession} -> + NSession; + {error, _ErrorCode} -> + Session + end; +apply_op(Session, {pubrec, PacketId}) -> + case emqx_session:pubrec(PacketId, Session) of + {ok, NSession} -> + NSession; + {error, _ErrorCode} -> + Session + end; +apply_op(Session, {pubrel, PacketId}) -> + case emqx_session:pubrel(PacketId, Session) of + {ok, NSession} -> + NSession; + {error, _ErrorCode} -> + Session + end; +apply_op(Session, {pubcomp, PacketId}) -> + case emqx_session:pubcomp(PacketId, Session) of + {ok, _Msgs} -> + Session; + {ok, _Msgs, NSession} -> + NSession; + {error, _ErrorCode} -> + Session + end; +apply_op(Session, {deliver, Delivers}) -> + {ok, _Msgs, NSession} = emqx_session:deliver(Delivers, Session), + NSession; +apply_op(Session, {timeout, {TRef, TimeoutMsg}}) -> + case emqx_session:timeout(TRef, TimeoutMsg, Session) of + {ok, NSession} -> + NSession; + {ok, _Msg, NSession} -> + NSession + end. -t_publish(_) -> - 'TODO'. +%%%%%%%%%%%%%%%%%% +%%% Generators %%% +%%%%%%%%%%%%%%%%%% +session_op_list() -> + Union = [info, + attrs, + stats, + {info, info_args()}, + {subscribe, sub_args()}, + {unsubscribe, unsub_args()}, + {publish, publish_args()}, + {puback, puback_args()}, + {pubrec, pubrec_args()}, + {pubrel, pubrel_args()}, + {pubcomp, pubcomp_args()}, + {deliver, deliver_args()}, + {timeout, timeout_args()} + ], + list(?LAZY(oneof(Union))). -t_puback(_) -> - 'TODO'. +deliver_args() -> + list({deliver, topic(), message()}). -t_pubrec(_) -> - 'TODO'. +timeout_args() -> + {tref(), timeout_msg()}. -t_pubrel(_) -> - 'TODO'. +info_args() -> + oneof([clean_start, + subscriptions, + max_subscriptions, + upgrade_qos, + inflight, + max_inflight, + retry_interval, + mqueue_len, + max_mqueue, + mqueue_dropped, + next_pkt_id, + awaiting_rel, + max_awaiting_rel, + await_rel_timeout, + expiry_interval, + created_at + ]). -t_pubcomp(_) -> - 'TODO'. +sub_args() -> + ?LET({ClientId, TopicFilter, SubOpts}, + {clientid(), topic(), sub_opts()}, + {#{client_id => ClientId}, TopicFilter, SubOpts}). -t_deliver(_) -> - 'TODO'. +unsub_args() -> + ?LET({ClientId, TopicFilter}, + {clientid(), topic()}, + {#{client_id => ClientId}, TopicFilter}). -t_timeout(_) -> - 'TODO'. +publish_args() -> + ?LET({PacketId, Message}, + {packetid(), message()}, + {PacketId, Message}). -ignore_loop(_Config) -> - emqx_zone:set_env(external, ignore_loop_deliver, true), - {ok, Client} = emqx_client:start_link(), - {ok, _} = emqx_client:connect(Client), - TestTopic = <<"Self">>, - {ok, _, [2]} = emqx_client:subscribe(Client, TestTopic, qos2), - ok = emqx_client:publish(Client, TestTopic, <<"testmsg">>, 0), - {ok, _} = emqx_client:publish(Client, TestTopic, <<"testmsg">>, 1), - {ok, _} = emqx_client:publish(Client, TestTopic, <<"testmsg">>, 2), - ?assertEqual(0, length(emqx_client_SUITE:receive_messages(3))), - ok = emqx_client:disconnect(Client), - emqx_zone:set_env(external, ignore_loop_deliver, false). +puback_args() -> + packetid(). -session_all(_) -> - emqx_zone:set_env(internal, idle_timeout, 1000), - ClientId = <<"ClientId">>, - {ok, ConnPid} = emqx_mock_client:start_link(ClientId), - {ok, SPid} = emqx_mock_client:open_session(ConnPid, ClientId, internal), - Message1 = emqx_message:make(<<"ClientId">>, 2, <<"topic">>, <<"hello">>), - emqx_session:subscribe(SPid, [{<<"topic">>, #{qos => 2}}]), - emqx_session:subscribe(SPid, [{<<"topic">>, #{qos => 1}}]), - timer:sleep(200), - [{<<"topic">>, _}] = emqx:subscriptions(SPid), - emqx_session:publish(SPid, 1, Message1), - timer:sleep(200), - [{publish, 1, _}] = emqx_mock_client:get_last_message(ConnPid), - Attrs = emqx_session:attrs(SPid), - Info = emqx_session:info(SPid), - Stats = emqx_session:stats(SPid), - ClientId = proplists:get_value(client_id, Attrs), - ClientId = proplists:get_value(client_id, Info), - 1 = proplists:get_value(subscriptions_count, Stats), - emqx_session:unsubscribe(SPid, [<<"topic">>]), - timer:sleep(200), - [] = emqx:subscriptions(SPid), - emqx_mock_client:close_session(ConnPid). +pubrec_args() -> + packetid(). +pubrel_args() -> + packetid(). + +pubcomp_args() -> + packetid(). + +timeout_msg() -> + oneof([retry_delivery, check_awaiting_rel]). + +tref() -> oneof([tref, undefined]). + +sub_opts() -> + ?LET({RH, RAP, NL, QOS, SHARE, SUBID}, + {rh(), rap(), nl(), qos(), share(), subid()} + , make_subopts(RH, RAP, NL, QOS, SHARE, SUBID)). + +message() -> + ?LET({QoS, Topic, Payload}, + {qos(), topic(), payload()}, + emqx_message:make(proper, QoS, Topic, Payload)). + +subid() -> integer(). + +rh() -> oneof([0, 1, 2]). + +rap() -> oneof([0, 1]). + +nl() -> oneof([0, 1]). + +qos() -> oneof([0, 1, 2]). + +share() -> binary(). + +clientid() -> binary(). + +topic() -> ?LET(No, choose(1, 10), + begin + NoBin = integer_to_binary(No), + <<"topic/", NoBin/binary>> + end). + +payload() -> binary(). + +packetid() -> choose(1, 30). + +zone() -> + ?LET(Zone, [{max_subscriptions, max_subscription()}, + {upgrade_qos, upgrade_qos()}, + {retry_interval, retry_interval()}, + {max_awaiting_rel, max_awaiting_rel()}, + {await_rel_timeout, await_rel_timeout()}] + , maps:from_list(Zone)). + +max_subscription() -> + frequency([{33, 0}, + {33, 1}, + {34, choose(0,10)}]). + +upgrade_qos() -> bool(). + +retry_interval() -> ?LET(Interval, choose(0, 20), Interval*1000). + +max_awaiting_rel() -> choose(0, 10). + +await_rel_timeout() -> ?LET(Interval, choose(0, 150), Interval*1000). + +max_inflight() -> choose(0, 10). + +expiry_interval() -> ?LET(EI, choose(1, 10), EI * 3600). + +option() -> + ?LET(Option, [{max_inflight, max_inflight()}, + {expiry_interval, expiry_interval()}] + , maps:from_list(Option)). + +cleanstart() -> bool(). + +session() -> + ?LET({CleanStart, Zone, Options}, + {cleanstart(), zone(), option()}, + begin + Session = emqx_session:init(CleanStart, #{zone => Zone}, Options), + emqx_session:set_pkt_id(Session, 16#ffff) + end). + +%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% Internal functions %%% +%%%%%%%%%%%%%%%%%%%%%%%%%% + +make_subopts(RH, RAP, NL, QOS, SHARE, SubId) -> + #{rh => RH, + rap => RAP, + nl => NL, + qos => QOS, + share => SHARE, + subid => SubId}. + + +load(Modules) -> + [mock(Module) || Module <- Modules], + ok. + +unload(Modules) -> + lists:foreach(fun(Module) -> + ok = meck:unload(Module) + end, Modules). + +mock(Module) -> + ok = meck:new(Module, [passthrough, no_history]), + do_mock(Module). + +do_mock(emqx_metrics) -> + meck:expect(emqx_metrics, inc, fun(_Anything) -> ok end); +do_mock(emqx_broker) -> + meck:expect(emqx_broker, subscribe, fun(_, _, _) -> ok end), + meck:expect(emqx_broker, set_subopts, fun(_, _) -> ok end), + meck:expect(emqx_broker, unsubscribe, fun(_) -> ok end), + meck:expect(emqx_broker, publish, fun(_) -> ok end); +do_mock(emqx_misc) -> + meck:expect(emqx_misc, start_timer, fun(_, _) -> tref end); +do_mock(emqx_message) -> + meck:expect(emqx_message, set_header, fun(_Hdr, _Val, Msg) -> Msg end), + meck:expect(emqx_message, is_expired, fun(_Msg) -> (rand:uniform(16) > 8) end); +do_mock(emqx_hooks) -> + meck:expect(emqx_hooks, run, fun(_Hook, _Args) -> ok end); +do_mock(emqx_zone) -> + meck:expect(emqx_zone, get_env, fun(Env, Key, Default) -> maps:get(Key, Env, Default) end); +do_mock(emqx_pd) -> + meck:expect(emqx_pd, update_counter, fun(_stats, _num) -> ok end). diff --git a/test/emqx_sys_SUITE.erl b/test/emqx_sys_SUITE.erl new file mode 100644 index 000000000..26ec26fc2 --- /dev/null +++ b/test/emqx_sys_SUITE.erl @@ -0,0 +1,136 @@ +%%-------------------------------------------------------------------- +%% 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_sys_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("proper/include/proper.hrl"). +-include_lib("eunit/include/eunit.hrl"). + +-define(mock_modules, + [ emqx_metrics + , emqx_stats + , emqx_broker + , ekka_mnesia + ]). + +all() -> emqx_ct:all(?MODULE). + +init_per_suite(Config) -> + application:load(emqx), + ok = application:set_env(emqx, broker_sys_interval, 1), + ok = application:set_env(emqx, broker_sys_heartbeat, 1), + ok = emqx_logger:set_log_level(emergency), + Config. + +end_per_suite(_Config) -> + application:unload(emqx), + ok = emqx_logger:set_log_level(error), + ok. + +t_prop_sys(_) -> + Opts = [{numtests, 100}, {to_file, user}], + ok = load(?mock_modules), + ?assert(proper:quickcheck(prop_sys(), Opts)), + ok = unload(?mock_modules). + +prop_sys() -> + ?FORALL(Cmds, commands(?MODULE), + begin + {ok, _Pid} = emqx_sys:start_link(), + {History, State, Result} = run_commands(?MODULE, Cmds), + ok = emqx_sys:stop(), + ?WHENFAIL(io:format("History: ~p\nState: ~p\nResult: ~p\n", + [History,State,Result]), + aggregate(command_names(Cmds), true)) + end). + +load(Modules) -> + [mock(Module) || Module <- Modules], + ok. + +unload(Modules) -> + lists:foreach(fun(Module) -> + ok = meck:unload(Module) + end, Modules). + +mock(Module) -> + ok = meck:new(Module, [passthrough, no_history]), + do_mock(Module). + +do_mock(emqx_broker) -> + meck:expect(emqx_broker, publish, + fun(Msg) -> {node(), <<"test">>, Msg} end), + meck:expect(emqx_broker, safe_publish, + fun(Msg) -> {node(), <<"test">>, Msg} end); +do_mock(emqx_stats) -> + meck:expect(emqx_stats, getstats, fun() -> [0] end); +do_mock(ekka_mnesia) -> + meck:expect(ekka_mnesia, running_nodes, fun() -> [node()] end); +do_mock(emqx_metrics) -> + meck:expect(emqx_metrics, all, fun() -> [{hello, 3}] end). + +unmock() -> + meck:unload(emqx_broker). + +%%%%%%%%%%%%% +%%% MODEL %%% +%%%%%%%%%%%%% +%% @doc Initial model value at system start. Should be deterministic. +initial_state() -> + #{}. + +%% @doc List of possible commands to run against the system +command(_State) -> + oneof([{call, emqx_sys, info, []}, + {call, emqx_sys, version, []}, + {call, emqx_sys, uptime, []}, + {call, emqx_sys, datetime, []}, + {call, emqx_sys, sysdescr, []}, + {call, emqx_sys, sys_interval, []}, + {call, emqx_sys, sys_heatbeat_interval, []}, + %------------ unexpected message ----------------------% + {call, emqx_sys, handle_call, [emqx_sys, other, state]}, + {call, emqx_sys, handle_cast, [emqx_sys, other]}, + {call, emqx_sys, handle_info, [info, state]} + ]). + +precondition(_State, {call, _Mod, _Fun, _Args}) -> + timer:sleep(1), + true. + +postcondition(_State, {call, emqx_sys, info, []}, Info) -> + is_list(Info) andalso length(Info) =:= 4; +postcondition(_State, {call, emqx_sys, version, []}, Version) -> + is_list(Version); +postcondition(_State, {call, emqx_sys, uptime, []}, Uptime) -> + is_list(Uptime); +postcondition(_State, {call, emqx_sys, datetime, []}, Datetime) -> + is_list(Datetime); +postcondition(_State, {call, emqx_sys, sysdescr, []}, Sysdescr) -> + is_list(Sysdescr); +postcondition(_State, {call, emqx_sys, sys_interval, []}, SysInterval) -> + is_integer(SysInterval) andalso SysInterval > 0; +postcondition(_State, {call, emqx_sys, sys_heartbeat_interval, []}, SysHeartInterval) -> + is_integer(SysHeartInterval) andalso SysHeartInterval > 0; +postcondition(_State, {call, _Mod, _Fun, _Args}, _Res) -> + true. + +next_state(State, _Res, {call, _Mod, _Fun, _Args}) -> + NewState = State, + NewState. diff --git a/test/prop_emqx_session.erl b/test/prop_emqx_session.erl deleted file mode 100644 index 5e137ee12..000000000 --- a/test/prop_emqx_session.erl +++ /dev/null @@ -1,327 +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(prop_emqx_session). - --include("emqx_mqtt.hrl"). --include_lib("proper/include/proper.hrl"). --include_lib("eunit/include/eunit.hrl"). - --define(mock_modules, - [ emqx_metrics - , emqx_broker - , emqx_misc - , emqx_message - , emqx_hooks - , emqx_zone - , emqx_pd - ]). - --compile(export_all). --compile(nowarn_export_all). - -%%%%%%%%%%%%%%%%%% -%%% Properties %%% -%%%%%%%%%%%%%%%%%% -prop_session_pub(opts) -> [{numtests, 1000}]. - -prop_session_pub() -> - emqx_logger:set_log_level(emergency), - - ?SETUP(fun() -> - ok = load(?mock_modules), - fun() -> ok = unload(?mock_modules) end - end, - ?FORALL({Session, OpList}, {session(), session_op_list()}, - begin - try - apply_ops(Session, OpList), - true - after - ok - end - end)). - -%%%%%%%%%%%%%%% -%%% Helpers %%% -%%%%%%%%%%%%%%% - -apply_ops(Session, []) -> - ?assertEqual(session, element(1, Session)); -apply_ops(Session, [Op | Rest]) -> - NSession = apply_op(Session, Op), - apply_ops(NSession, Rest). - -apply_op(Session, info) -> - Info = emqx_session:info(Session), - ?assert(is_map(Info)), - ?assertEqual(16, maps:size(Info)), - Session; -apply_op(Session, attrs) -> - Attrs = emqx_session:attrs(Session), - ?assert(is_map(Attrs)), - ?assertEqual(3, maps:size(Attrs)), - Session; -apply_op(Session, stats) -> - Stats = emqx_session:stats(Session), - ?assert(is_list(Stats)), - ?assertEqual(9, length(Stats)), - Session; -apply_op(Session, {subscribe, {Client, TopicFilter, SubOpts}}) -> - case emqx_session:subscribe(Client, TopicFilter, SubOpts, Session) of - {ok, NSession} -> - NSession; - {error, ?RC_QUOTA_EXCEEDED} -> - Session - end; -apply_op(Session, {unsubscribe, {Client, TopicFilter}}) -> - case emqx_session:unsubscribe(Client, TopicFilter, Session) of - {ok, NSession} -> - NSession; - {error, ?RC_NO_SUBSCRIPTION_EXISTED} -> - Session - end; -apply_op(Session, {publish, {PacketId, Msg}}) -> - case emqx_session:publish(PacketId, Msg, Session) of - {ok, _Msg} -> - Session; - {ok, _Deliver, NSession} -> - NSession; - {error, _ErrorCode} -> - Session - end; -apply_op(Session, {puback, PacketId}) -> - case emqx_session:puback(PacketId, Session) of - {ok, _Msg} -> - Session; - {ok, _Deliver, NSession} -> - NSession; - {error, _ErrorCode} -> - Session - end; -apply_op(Session, {pubrec, PacketId}) -> - case emqx_session:pubrec(PacketId, Session) of - {ok, NSession} -> - NSession; - {error, _ErrorCode} -> - Session - end; -apply_op(Session, {pubrel, PacketId}) -> - case emqx_session:pubrel(PacketId, Session) of - {ok, NSession} -> - NSession; - {error, _ErrorCode} -> - Session - end; -apply_op(Session, {pubcomp, PacketId}) -> - case emqx_session:pubcomp(PacketId, Session) of - {ok, _Msgs} -> - Session; - {ok, _Msgs, NSession} -> - NSession; - {error, _ErrorCode} -> - Session - end; -apply_op(Session, {deliver, Delivers}) -> - {ok, _Msgs, NSession} = emqx_session:deliver(Delivers, Session), - NSession; -apply_op(Session, {timeout, {TRef, TimeoutMsg}}) -> - case emqx_session:timeout(TRef, TimeoutMsg, Session) of - {ok, NSession} -> - NSession; - {ok, _Msg, NSession} -> - NSession - end. - -%%%%%%%%%%%%%%%%%% -%%% Generators %%% -%%%%%%%%%%%%%%%%%% -session_op_list() -> - Union = [info, - attrs, - stats, - {subscribe, sub_args()}, - {unsubscribe, unsub_args()}, - {publish, publish_args()}, - {puback, puback_args()}, - {pubrec, pubrec_args()}, - {pubrel, pubrel_args()}, - {pubcomp, pubcomp_args()}, - {deliver, deliver_args()}, - {timeout, timeout_args()} - ], - list(?LAZY(oneof(Union))). - -deliver_args() -> - list({deliver, topic(), message()}). - -timeout_args() -> - {tref(), timeout_msg()}. - -sub_args() -> - ?LET({ClientId, TopicFilter, SubOpts}, - {clientid(), topic(), sub_opts()}, - {#{client_id => ClientId}, TopicFilter, SubOpts}). - -unsub_args() -> - ?LET({ClientId, TopicFilter}, - {clientid(), topic()}, - {#{client_id => ClientId}, TopicFilter}). - -publish_args() -> - ?LET({PacketId, Message}, - {packetid(), message()}, - {PacketId, Message}). - -puback_args() -> - packetid(). - -pubrec_args() -> - packetid(). - -pubrel_args() -> - packetid(). - -pubcomp_args() -> - packetid(). - -timeout_msg() -> - oneof([retry_delivery, check_awaiting_rel]). - -tref() -> oneof([tref, undefined]). - -sub_opts() -> - ?LET({RH, RAP, NL, QOS, SHARE, SUBID}, - {rh(), rap(), nl(), qos(), share(), subid()} - , make_subopts(RH, RAP, NL, QOS, SHARE, SUBID)). - -message() -> - ?LET({QoS, Topic, Payload}, - {qos(), topic(), payload()}, - emqx_message:make(proper, QoS, Topic, Payload)). - -subid() -> integer(). - -rh() -> oneof([0, 1, 2]). - -rap() -> oneof([0, 1]). - -nl() -> oneof([0, 1]). - -qos() -> oneof([0, 1, 2]). - -share() -> binary(). - -clientid() -> binary(). - -topic() -> ?LET(No, choose(1, 10), begin - NoBin = integer_to_binary(No), - <<"topic/", NoBin/binary>> - end). - -payload() -> binary(). - -packetid() -> choose(1, 30). - -zone() -> - ?LET(Zone, [{max_subscriptions, max_subscription()}, - {upgrade_qos, upgrade_qos()}, - {retry_interval, retry_interval()}, - {max_awaiting_rel, max_awaiting_rel()}, - {await_rel_timeout, await_rel_timeout()}] - , maps:from_list(Zone)). - -max_subscription() -> frequency([{33, 0}, - {33, 1}, - {34, choose(0,10)}]). - -upgrade_qos() -> bool(). - -retry_interval() -> ?LET(Interval, choose(0, 20), Interval*1000). - -max_awaiting_rel() -> choose(0, 10). - -await_rel_timeout() -> ?LET(Interval, choose(0, 150), Interval*1000). - -max_inflight() -> choose(0, 10). - -expiry_interval() -> ?LET(EI, choose(1, 10), EI * 3600). - -option() -> - ?LET(Option, [{max_inflight, max_inflight()}, - {expiry_interval, expiry_interval()}] - , maps:from_list(Option)). - -cleanstart() -> bool(). - -session() -> - ?LET({CleanStart, Zone, Options}, - {cleanstart(), zone(), option()}, - begin - Session = emqx_session:init(CleanStart, #{zone => Zone}, Options), - emqx_session:set_pkt_id(Session, 16#ffff) - end). - -%%%%%%%%%%%%%%%%%%%%%%%%%% -%%% Internal functions %%% -%%%%%%%%%%%%%%%%%%%%%%%%%% - -make_subopts(RH, RAP, NL, QOS, SHARE, SubId) -> - #{rh => RH, - rap => RAP, - nl => NL, - qos => QOS, - share => SHARE, - subid => SubId}. - - -load(Modules) -> - [mock(Module) || Module <- Modules], - ok. - -unload(Modules) -> - lists:foreach(fun(Module) -> - ok = meck:unload(Module) - end, Modules), - ok. - -mock(Module) -> - ok = meck:new(Module, [passthrough, no_history]), - do_mock(Module, expect(Module)). - -do_mock(emqx_metrics, Expect) -> - Expect(inc, fun(_Anything) -> ok end); -do_mock(emqx_broker, Expect) -> - Expect(subscribe, fun(_, _, _) -> ok end), - Expect(set_subopts, fun(_, _) -> ok end), - Expect(unsubscribe, fun(_) -> ok end), - Expect(publish, fun(_) -> ok end); -do_mock(emqx_misc, Expect) -> - Expect(start_timer, fun(_, _) -> tref end); -do_mock(emqx_message, Expect) -> - Expect(set_header, fun(_Hdr, _Val, Msg) -> Msg end), - Expect(is_expired, fun(_Msg) -> (rand:uniform(16) > 8) end); -do_mock(emqx_hooks, Expect) -> - Expect(run, fun(_Hook, _Args) -> ok end); -do_mock(emqx_zone, Expect) -> - Expect(get_env, fun(Env, Key, Default) -> maps:get(Key, Env, Default) end); -do_mock(emqx_pd, Expect) -> - Expect(update_counter, fun(_stats, _num) -> ok end). - -expect(Module) -> - fun(OldFun, NewFun) -> - ok = meck:expect(Module, OldFun, NewFun) - end. From acf54509f55cf95b5d3bacecc97416213ecc3c78 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Fri, 16 Aug 2019 18:21:51 +0800 Subject: [PATCH 06/36] Add emqx_plugins module test case (#2801) --- test/emqx_plugins_SUITE.erl | 37 +++++++++++++++- .../emqx_mini_plugin/Makefile | 35 ++++++++++++++++ .../etc/emqx_mini_plugin.conf | 1 + .../priv/emqx_mini_plugin.schema | 5 +++ .../emqx_mini_plugin/rebar.config | 25 +++++++++++ .../src/emqx_mini_plugin.app.src | 14 +++++++ .../src/emqx_mini_plugin_app.erl | 42 +++++++++++++++++++ 7 files changed, 157 insertions(+), 2 deletions(-) create mode 100644 test/emqx_plugins_SUITE_data/emqx_mini_plugin/Makefile create mode 100644 test/emqx_plugins_SUITE_data/emqx_mini_plugin/etc/emqx_mini_plugin.conf create mode 100644 test/emqx_plugins_SUITE_data/emqx_mini_plugin/priv/emqx_mini_plugin.schema create mode 100644 test/emqx_plugins_SUITE_data/emqx_mini_plugin/rebar.config create mode 100644 test/emqx_plugins_SUITE_data/emqx_mini_plugin/src/emqx_mini_plugin.app.src create mode 100644 test/emqx_plugins_SUITE_data/emqx_mini_plugin/src/emqx_mini_plugin_app.erl diff --git a/test/emqx_plugins_SUITE.erl b/test/emqx_plugins_SUITE.erl index 5c4d43f24..5cab5b362 100644 --- a/test/emqx_plugins_SUITE.erl +++ b/test/emqx_plugins_SUITE.erl @@ -19,8 +19,41 @@ -compile(export_all). -compile(nowarn_export_all). --include_lib("proper/include/proper.hrl"). -include_lib("eunit/include/eunit.hrl"). - all() -> emqx_ct:all(?MODULE). + +init_per_suite(Config) -> + + %% Compile extra plugin code + + DataPath = proplists:get_value(data_dir, Config), + AppPath = filename:join([DataPath, "emqx_mini_plugin"]), + Cmd = lists:flatten(io_lib:format("cd ~s && make", [AppPath])), + + ct:pal("Executing ~s~n", [Cmd]), + ct:pal("~n ~s~n", [os:cmd(Cmd)]), + + code:add_path(filename:join([AppPath, "_build", "default", "lib", "emqx_mini_plugin", "ebin"])), + + put(loaded_file, filename:join([DataPath, "loaded_plugins"])), + emqx_ct_helpers:start_apps([], fun set_sepecial_cfg/1), + + Config. + +set_sepecial_cfg(_) -> + ExpandPath = filename:dirname(code:lib_dir(emqx_mini_plugin)), + + application:set_env(emqx, plugins_loaded_file, get(loaded_file)), + application:set_env(emqx, expand_plugins_dir, ExpandPath), + ok. + +end_per_suite(_Config) -> + emqx_ct_helpers:stop_apps([]). + +t_load(_) -> + {error, load_app_fail} = emqx_plugins:load_expand_plugin("./not_existed_path/"), + + {error, not_started} = emqx_plugins:unload(emqx_mini_plugin), + {ok, _} = emqx_plugins:load(emqx_mini_plugin), + ok = emqx_plugins:unload(emqx_mini_plugin). diff --git a/test/emqx_plugins_SUITE_data/emqx_mini_plugin/Makefile b/test/emqx_plugins_SUITE_data/emqx_mini_plugin/Makefile new file mode 100644 index 000000000..ad02951a3 --- /dev/null +++ b/test/emqx_plugins_SUITE_data/emqx_mini_plugin/Makefile @@ -0,0 +1,35 @@ +## shallow clone for speed + +REBAR_GIT_CLONE_OPTIONS += --depth 1 +export REBAR_GIT_CLONE_OPTIONS + +REBAR = rebar3 +all: compile + +compile: + $(REBAR) compile + +clean: distclean + +ct: compile + $(REBAR) as test ct -v + +eunit: compile + $(REBAR) as test eunit + +xref: + $(REBAR) xref + +distclean: + @rm -rf _build + @rm -f data/app.*.config data/vm.*.args rebar.lock + +CUTTLEFISH_SCRIPT = _build/default/lib/cuttlefish/cuttlefish + +$(CUTTLEFISH_SCRIPT): + @${REBAR} get-deps + @if [ ! -f cuttlefish ]; then make -C _build/default/lib/cuttlefish; fi + +app.config: $(CUTTLEFISH_SCRIPT) etc/emqx_mini_plugin.conf + $(verbose) $(CUTTLEFISH_SCRIPT) -l info -e etc/ -c etc/emqx_mini_plugin.conf -i priv/emqx_mini_plugin.schema -d data + diff --git a/test/emqx_plugins_SUITE_data/emqx_mini_plugin/etc/emqx_mini_plugin.conf b/test/emqx_plugins_SUITE_data/emqx_mini_plugin/etc/emqx_mini_plugin.conf new file mode 100644 index 000000000..423ea099f --- /dev/null +++ b/test/emqx_plugins_SUITE_data/emqx_mini_plugin/etc/emqx_mini_plugin.conf @@ -0,0 +1 @@ +mini.name = test diff --git a/test/emqx_plugins_SUITE_data/emqx_mini_plugin/priv/emqx_mini_plugin.schema b/test/emqx_plugins_SUITE_data/emqx_mini_plugin/priv/emqx_mini_plugin.schema new file mode 100644 index 000000000..f936265c1 --- /dev/null +++ b/test/emqx_plugins_SUITE_data/emqx_mini_plugin/priv/emqx_mini_plugin.schema @@ -0,0 +1,5 @@ +%%-*- mode: erlang -*- + +{mapping, "mini.name", "emqx_mini_plugin.name", [ + {datatype, string} +]}. diff --git a/test/emqx_plugins_SUITE_data/emqx_mini_plugin/rebar.config b/test/emqx_plugins_SUITE_data/emqx_mini_plugin/rebar.config new file mode 100644 index 000000000..c690b88b1 --- /dev/null +++ b/test/emqx_plugins_SUITE_data/emqx_mini_plugin/rebar.config @@ -0,0 +1,25 @@ +{deps, + []}. + +{edoc_opts, [{preprocess, true}]}. +{erl_opts, [warn_unused_vars, + warn_shadow_vars, + warn_unused_import, + warn_obsolete_guard, + debug_info, + {parse_transform}]}. + +{xref_checks, [undefined_function_calls, undefined_functions, + locals_not_used, deprecated_function_calls, + warnings_as_errors, deprecated_functions]}. +{cover_enabled, true}. +{cover_opts, [verbose]}. +{cover_export_enabled, true}. + +{profiles, + [{test, [ + {deps, [ {emqx_ct_helper, {git, "https://github.com/emqx/emqx-ct-helpers", {tag, "v1.1.4"}}} + , {cuttlefish, {git, "https://github.com/emqx/cuttlefish", {tag, "v3.0.0"}}} + ]} + ]} +]}. diff --git a/test/emqx_plugins_SUITE_data/emqx_mini_plugin/src/emqx_mini_plugin.app.src b/test/emqx_plugins_SUITE_data/emqx_mini_plugin/src/emqx_mini_plugin.app.src new file mode 100644 index 000000000..20e17efda --- /dev/null +++ b/test/emqx_plugins_SUITE_data/emqx_mini_plugin/src/emqx_mini_plugin.app.src @@ -0,0 +1,14 @@ +{application, emqx_mini_plugin, + [{description, "An EMQ X plugin for testcase"}, + {vsn, "git"}, + {modules, []}, + {registered, []}, + {mod, {emqx_mini_plugin_app, []}}, + {applications, + [kernel, + stdlib + ]}, + {env,[]}, + {licenses, ["Apache 2.0"]}, + {links, []} + ]}. diff --git a/test/emqx_plugins_SUITE_data/emqx_mini_plugin/src/emqx_mini_plugin_app.erl b/test/emqx_plugins_SUITE_data/emqx_mini_plugin/src/emqx_mini_plugin_app.erl new file mode 100644 index 000000000..6801a10e3 --- /dev/null +++ b/test/emqx_plugins_SUITE_data/emqx_mini_plugin/src/emqx_mini_plugin_app.erl @@ -0,0 +1,42 @@ +%%%------------------------------------------------------------------- +%% @doc emqx_mini_plugin public API +%% @end +%%%------------------------------------------------------------------- + +-module(emqx_mini_plugin_app). + +-behaviour(application). +-behaviour(supervisor). + +-emqx_plugin(?MODULE). + +%% Application APIs +-export([ start/2 + , stop/1 + ]). + +%% Supervisor callback +-export([init/1]). + + +%% -- Application + +start(_StartType, _StartArgs) -> + {ok, Sup} = start_link(), + {ok, Sup}. + +stop(_State) -> + ok. + +%% --- Supervisor + +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + +init([]) -> + SupFlags = #{strategy => one_for_all, + intensity => 0, + period => 1}, + ChildSpecs = [], + {ok, {SupFlags, ChildSpecs}}. + From d63eccd8b84d376c288ed7b056f574d34a5ffc4a Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 16 Aug 2019 18:24:39 +0800 Subject: [PATCH 07/36] Add test cases for emqx_keepalive module (#2784) * Improve the keepalive module --- src/emqx_keepalive.erl | 34 ++++++++++++++++++---------------- test/emqx_keepalive_SUITE.erl | 4 ++++ 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/emqx_keepalive.erl b/src/emqx_keepalive.erl index 25170067d..88848f7ac 100644 --- a/src/emqx_keepalive.erl +++ b/src/emqx_keepalive.erl @@ -25,14 +25,16 @@ -export_type([keepalive/0]). -record(keepalive, { - statfun, - statval, - tsec, - tmsg, - tref, - repeat = 0 + statfun :: statfun(), + statval :: integer(), + tsec :: pos_integer(), + tmsg :: term(), + tref :: reference(), + repeat = 0 :: non_neg_integer() }). +-type(statfun() :: fun(() -> {ok, integer()} | {error, term()})). + -opaque(keepalive() :: #keepalive{}). %%-------------------------------------------------------------------- @@ -40,15 +42,17 @@ %%-------------------------------------------------------------------- %% @doc Start a keepalive --spec(start(fun(), integer(), any()) -> {ok, keepalive()} | {error, term()}). -start(_, 0, _) -> - {ok, #keepalive{}}; -start(StatFun, TimeoutSec, TimeoutMsg) -> +-spec(start(statfun(), pos_integer(), term()) + -> {ok, keepalive()} | {error, term()}). +start(StatFun, TimeoutSec, TimeoutMsg) when TimeoutSec > 0 -> try StatFun() of {ok, StatVal} -> - {ok, #keepalive{statfun = StatFun, statval = StatVal, - tsec = TimeoutSec, tmsg = TimeoutMsg, - tref = timer(TimeoutSec, TimeoutMsg)}}; + TRef = timer(TimeoutSec, TimeoutMsg), + {ok, #keepalive{statfun = StatFun, + statval = StatVal, + tsec = TimeoutSec, + tmsg = TimeoutMsg, + tref = TRef}}; {error, Error} -> {error, Error} catch @@ -82,9 +86,7 @@ resume(KeepAlive = #keepalive{tsec = TimeoutSec, tmsg = TimeoutMsg}) -> %% @doc Cancel Keepalive -spec(cancel(keepalive()) -> ok). cancel(#keepalive{tref = TRef}) when is_reference(TRef) -> - catch erlang:cancel_timer(TRef), ok; -cancel(_) -> - ok. + catch erlang:cancel_timer(TRef), ok. timer(Secs, Msg) -> erlang:send_after(timer:seconds(Secs), self(), Msg). diff --git a/test/emqx_keepalive_SUITE.erl b/test/emqx_keepalive_SUITE.erl index abfd8ee2b..f140913ec 100644 --- a/test/emqx_keepalive_SUITE.erl +++ b/test/emqx_keepalive_SUITE.erl @@ -35,3 +35,7 @@ keepalive_recv(KA, Acc) -> after 4000 -> Acc end. +t_cancel(_) -> + {ok, KA} = emqx_keepalive:start(fun() -> {ok, 1} end, 1, {keepalive, timeout}), + ok = emqx_keepalive:cancel(KA). + From 2e26cd244aa80d5bedfa76e963e7203e0d78949b Mon Sep 17 00:00:00 2001 From: tigercl Date: Fri, 16 Aug 2019 18:25:49 +0800 Subject: [PATCH 08/36] Improve test coverage (#2799) * Improve test coverage * Improve test coverage for emqx_cm * Improve test coverage for emqx_cm_registry * Fix emqx_client_SUITE --- src/emqx_ctl.erl | 21 -------- src/emqx_mqtt_props.erl | 4 +- src/emqx_session.erl | 2 +- test/emqx_SUITE.erl | 81 ++++++++++++++++++++++++++++ test/emqx_client_SUITE.erl | 25 +++++++++ test/emqx_ctl_SUITE.erl | 54 +++++++++++++++++++ test/emqx_mod_subscription_SUITE.erl | 51 ++++++++++++++++++ 7 files changed, 215 insertions(+), 23 deletions(-) create mode 100644 test/emqx_SUITE.erl create mode 100644 test/emqx_ctl_SUITE.erl create mode 100644 test/emqx_mod_subscription_SUITE.erl diff --git a/src/emqx_ctl.erl b/src/emqx_ctl.erl index 08b9a56fd..60ee37039 100644 --- a/src/emqx_ctl.erl +++ b/src/emqx_ctl.erl @@ -151,24 +151,3 @@ noreply(State) -> next_seq(State = #state{seq = Seq}) -> State#state{seq = Seq + 1}. --ifdef(TEST). - --include_lib("eunit/include/eunit.hrl"). - -register_command_test_() -> - {setup, - fun() -> - {ok, InitState} = emqx_ctl:init([]), - InitState - end, - fun(State) -> - ok = emqx_ctl:terminate(shutdown, State) - end, - fun(State = #state{seq = Seq}) -> - emqx_ctl:handle_cast({register_command, test0, {?MODULE, test0}, []}, State), - [?_assertMatch([{{0,test0},{?MODULE, test0}, []}], ets:lookup(?TAB, {Seq,test0}))] - end - }. - --endif. - diff --git a/src/emqx_mqtt_props.erl b/src/emqx_mqtt_props.erl index 1b75425a7..163d9baf1 100644 --- a/src/emqx_mqtt_props.erl +++ b/src/emqx_mqtt_props.erl @@ -48,7 +48,9 @@ 16#19 => {'Request-Response-Information', 'Byte', [?CONNECT]}, 16#1A => {'Response-Information', 'UTF8-Encoded-String', [?CONNACK]}, 16#1C => {'Server-Reference', 'UTF8-Encoded-String', [?CONNACK, ?DISCONNECT]}, - 16#1F => {'Reason-String', 'UTF8-Encoded-String', 'ALL'}, + 16#1F => {'Reason-String', 'UTF8-Encoded-String', [?CONNACK, ?DISCONNECT, ?PUBACK, + ?PUBREC, ?PUBREL, ?PUBCOMP, + ?SUBACK, ?UNSUBACK, ?AUTH]}, 16#21 => {'Receive-Maximum', 'Two-Byte-Integer', [?CONNECT, ?CONNACK]}, 16#22 => {'Topic-Alias-Maximum', 'Two-Byte-Integer', [?CONNECT, ?CONNACK]}, 16#23 => {'Topic-Alias', 'Two-Byte-Integer', [?PUBLISH]}, diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 2530f3a42..116f5756e 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -206,7 +206,7 @@ info(#session{clean_start = CleanStart, awaiting_rel => maps:size(AwaitingRel), max_awaiting_rel => MaxAwaitingRel, await_rel_timeout => AwaitRelTimeout, - expiry_interval => ExpiryInterval div 1000, + expiry_interval => ExpiryInterval, created_at => CreatedAt }. diff --git a/test/emqx_SUITE.erl b/test/emqx_SUITE.erl new file mode 100644 index 000000000..a971c7633 --- /dev/null +++ b/test/emqx_SUITE.erl @@ -0,0 +1,81 @@ +%%-------------------------------------------------------------------- +%% 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_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include("emqx.hrl"). +-include_lib("eunit/include/eunit.hrl"). +-include_lib("common_test/include/ct.hrl"). + +all() -> emqx_ct:all(?MODULE). + +init_per_suite(Config) -> + emqx_ct_helpers:start_apps([]), + Config. + +end_per_suite(_Config) -> + emqx_ct_helpers:stop_apps([]). + +t_emqx_pubsub_api(_) -> + emqx:start(), + true = emqx:is_running(node()), + {ok, C} = emqx_client:start_link([{host, "localhost"}, {client_id, "myclient"}]), + {ok, _} = emqx_client:connect(C), + ClientId = <<"myclient">>, + Topic = <<"mytopic">>, + Payload = <<"Hello World">>, + Topic1 = <<"mytopic1">>, + emqx:subscribe(Topic, ClientId), + ?assertEqual([Topic], emqx:topics()), + ?assertEqual([self()], emqx:subscribers(Topic)), + ?assertEqual([{Topic,#{qos => 0,subid => ClientId}}], emqx:subscriptions(self())), + ?assertEqual(true, emqx:subscribed(self(), Topic)), + ?assertEqual(true, emqx:subscribed(ClientId, Topic)), + ?assertEqual(false, emqx:subscribed(self(), Topic1)), + ?assertEqual(false, emqx:subscribed(ClientId, Topic1)), + emqx:publish(emqx_message:make(Topic, Payload)), + receive + {deliver, Topic, #message{payload = Payload}} -> + ok + after 100 -> + ct:fail("no_message") + end, + emqx:unsubscribe(Topic), + ct:sleep(20), + ?assertEqual([], emqx:topics()). + +t_emqx_hook_api(_) -> + InitArgs = ['arg2', 'arg3'], + emqx:hook('hook.run', fun run/3, InitArgs), + ok = emqx:run_hook('hook.run', ['arg1']), + emqx:unhook('hook.run', fun run/3), + + emqx:hook('hook.run_fold', fun add1/1), + emqx:hook('hook.run_fold', fun add2/1), + 4 = emqx:run_fold_hook('hook.run_fold', [], 1), + emqx:unhook('hook.run_fold', fun add1/1), + emqx:unhook('hook.run_fold', fun add2/1). + +run('arg1', 'arg2', 'arg3') -> + ok; +run(_, _, _) -> + ct:fail("no_match"). + +add1(N) -> {ok, N + 1}. +add2(N) -> {ok, N + 2}. \ No newline at end of file diff --git a/test/emqx_client_SUITE.erl b/test/emqx_client_SUITE.erl index 3ed8a5dff..17ade0616 100644 --- a/test/emqx_client_SUITE.erl +++ b/test/emqx_client_SUITE.erl @@ -54,6 +54,8 @@ groups() -> ]}, {mqttv4, [non_parallel_tests], [t_basic_v4, + t_cm, + t_cm_registry, %% t_will_message, %% t_offline_message_queueing, t_overlapping_subscriptions, @@ -88,6 +90,29 @@ t_basic_v3(_) -> t_basic_v4(_Config) -> t_basic([{proto_ver, v4}]). +t_cm(_) -> + IdleTimeout = emqx_zone:get_env(external, idle_timeout, 30000), + emqx_zone:set_env(external, idle_timeout, 1000), + ClientId = <<"myclient">>, + {ok, C} = emqx_client:start_link([{client_id, ClientId}]), + {ok, _} = emqx_client:connect(C), + #{client := #{client_id := ClientId}} = emqx_cm:get_chan_attrs(ClientId), + emqx_client:subscribe(C, <<"mytopic">>, 0), + ct:sleep(1200), + Stats = emqx_cm:get_chan_stats(ClientId), + ?assertEqual(1, proplists:get_value(subscriptions, Stats)), + emqx_zone:set_env(external, idle_timeout, IdleTimeout). + +t_cm_registry(_) -> + Info = supervisor:which_children(emqx_cm_sup), + {_, Pid, _, _} = lists:keyfind(registry, 1, Info), + ignored = gen_server:call(Pid, <<"Unexpected call">>), + gen_server:cast(Pid, <<"Unexpected cast">>), + Pid ! <<"Unexpected info">>, + ok = application:stop(mnesia), + emqx_ct_helpers:stop_apps([]), + emqx_ct_helpers:start_apps([]). + t_will_message(_Config) -> {ok, C1} = emqx_client:start_link([{clean_start, true}, {will_topic, nth(3, ?TOPICS)}, diff --git a/test/emqx_ctl_SUITE.erl b/test/emqx_ctl_SUITE.erl new file mode 100644 index 000000000..b4c3d953d --- /dev/null +++ b/test/emqx_ctl_SUITE.erl @@ -0,0 +1,54 @@ +%%-------------------------------------------------------------------- +%% 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_ctl_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("eunit/include/eunit.hrl"). +-include_lib("common_test/include/ct.hrl"). + +all() -> emqx_ct:all(?MODULE). + +init_per_suite(Config) -> + emqx_ct_helpers:start_apps([]), + Config. + +end_per_suite(_Config) -> + emqx_ct_helpers:stop_apps([]). + +t_command(_) -> + emqx_ctl:start_link(), + emqx_ctl:register_command(test, {?MODULE, test}), + ct:sleep(50), + ?assertEqual([{emqx_ctl_SUITE,test}], emqx_ctl:lookup_command(test)), + ?assertEqual(ok, emqx_ctl:run_command(["test", "ok"])), + ?assertEqual({error, test_failed}, emqx_ctl:run_command(["test", "error"])), + ?assertEqual({error, cmd_not_found}, emqx_ctl:run_command(["test2", "ok"])), + emqx_ctl:unregister_command(test), + ct:sleep(50), + ?assertEqual([], emqx_ctl:lookup_command(test)). + +test(["ok"]) -> + ok; +test(["error"]) -> + error(test_failed); +test(_) -> + io:format("Hello world"). + + + diff --git a/test/emqx_mod_subscription_SUITE.erl b/test/emqx_mod_subscription_SUITE.erl new file mode 100644 index 000000000..f955bbbf2 --- /dev/null +++ b/test/emqx_mod_subscription_SUITE.erl @@ -0,0 +1,51 @@ +%%-------------------------------------------------------------------- +%% 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_mod_subscription_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include("emqx_mqtt.hrl"). +-include("emqx.hrl"). + +-include_lib("eunit/include/eunit.hrl"). +-include_lib("common_test/include/ct.hrl"). + +all() -> emqx_ct:all(?MODULE). + +init_per_suite(Config) -> + emqx_ct_helpers:start_apps([emqx]), + Config. + +end_per_suite(_Config) -> + emqx_ct_helpers:stop_apps([emqx]). + +t_mod_subscription(_) -> + emqx_mod_subscription:load([{<<"connected/%c/%u">>, ?QOS_0}]), + {ok, C} = emqx_client:start_link([{host, "localhost"}, {client_id, "myclient"}, {username, "admin"}]), + {ok, _} = emqx_client:connect(C), + % ct:sleep(100), + emqx_client:publish(C, <<"connected/myclient/admin">>, <<"Hello world">>, ?QOS_0), + receive + {publish, #{topic := Topic, payload := Payload}} -> + ?assertEqual(<<"connected/myclient/admin">>, Topic), + ?assertEqual(<<"Hello world">>, Payload) + after 100 -> + ct:fail("no_message") + end, + ok = emqx_client:disconnect(C), + emqx_mod_subscription:unload([]). From c1fd5f89f17892f55df405980275c83aa0bf26d1 Mon Sep 17 00:00:00 2001 From: tigercl Date: Sat, 17 Aug 2019 09:15:43 +0800 Subject: [PATCH 09/36] Make codes compliance with mqtt protocol specifications (#2790) * Make codes compliance with mqtt protocol specifications * Fix test case * Remove emqx_protocol:puback/4 --- src/emqx_channel.erl | 6 +-- src/emqx_logger.erl | 2 + src/emqx_mod_presence.erl | 26 ++++----- src/emqx_mqtt_caps.erl | 4 -- src/emqx_packet.erl | 3 +- src/emqx_protocol.erl | 99 ++++++++++++++++------------------- src/emqx_session.erl | 2 + test/emqx_mqtt_caps_SUITE.erl | 10 +--- 8 files changed, 65 insertions(+), 87 deletions(-) diff --git a/src/emqx_channel.erl b/src/emqx_channel.erl index c8a598ef1..10e3c78b6 100644 --- a/src/emqx_channel.erl +++ b/src/emqx_channel.erl @@ -267,9 +267,9 @@ connected(enter, _PrevSt, State = #state{proto_state = ProtoState}) -> shutdown(Reason, NState) end; -connected(cast, {incoming, Packet = ?PACKET(?CONNECT)}, State) -> - ?LOG(warning, "Unexpected connect: ~p", [Packet]), - shutdown(unexpected_incoming_connect, State); +connected(cast, {incoming, ?PACKET(?CONNECT)}, State) -> + Shutdown = fun(NewSt) -> shutdown(?RC_PROTOCOL_ERROR, NewSt) end, + handle_outgoing(?DISCONNECT_PACKET(?RC_PROTOCOL_ERROR), Shutdown, State); connected(cast, {incoming, Packet}, State) when is_record(Packet, mqtt_packet) -> handle_incoming(Packet, fun keep_state/1, State); diff --git a/src/emqx_logger.erl b/src/emqx_logger.erl index 2f65d1222..0b3038523 100644 --- a/src/emqx_logger.erl +++ b/src/emqx_logger.erl @@ -122,6 +122,8 @@ critical(Metadata, Format, Args) when is_map(Metadata) -> logger:critical(Format, Args, Metadata). -spec(set_metadata_client_id(emqx_types:client_id()) -> ok). +set_metadata_client_id(<<>>) -> + ok; set_metadata_client_id(ClientId) -> set_proc_metadata(#{client_id => ClientId}). diff --git a/src/emqx_mod_presence.erl b/src/emqx_mod_presence.erl index 97f7a9929..bec9dbaa0 100644 --- a/src/emqx_mod_presence.erl +++ b/src/emqx_mod_presence.erl @@ -45,25 +45,21 @@ on_client_connected(#{client_id := ClientId, username := Username, peername := {IpAddr, _} }, ConnAck, - #{session := #{clean_start := CleanStart, - expiry_interval := Interval - }, + #{session := Session, proto_name := ProtoName, proto_ver := ProtoVer, keepalive := Keepalive }, Env) -> - - case emqx_json:safe_encode(#{clientid => ClientId, - username => Username, - ipaddress => iolist_to_binary(esockd_net:ntoa(IpAddr)), - proto_name => ProtoName, - proto_ver => ProtoVer, - keepalive => Keepalive, - clean_start => CleanStart, - expiry_interval => Interval, - connack => ConnAck, - ts => erlang:system_time(millisecond) - }) of + + case emqx_json:safe_encode(maps:merge(#{clientid => ClientId, + username => Username, + ipaddress => iolist_to_binary(esockd_net:ntoa(IpAddr)), + proto_name => ProtoName, + proto_ver => ProtoVer, + keepalive => Keepalive, + connack => ConnAck, + ts => erlang:system_time(millisecond) + }, maps:with([clean_start, expiry_interval], Session))) of {ok, Payload} -> emqx:publish(message(qos(Env), topic(connected, ClientId), Payload)); {error, Reason} -> diff --git a/src/emqx_mqtt_caps.erl b/src/emqx_mqtt_caps.erl index 0aa3a2a23..ec7f55330 100644 --- a/src/emqx_mqtt_caps.erl +++ b/src/emqx_mqtt_caps.erl @@ -79,10 +79,6 @@ do_check_pub(#{qos := QoS}, #{max_qos_allowed := MaxQoS}) {error, ?RC_QOS_NOT_SUPPORTED}; do_check_pub(#{retain := true}, #{retain_available := false}) -> {error, ?RC_RETAIN_NOT_SUPPORTED}; -do_check_pub(#{topic_alias := TopicAlias}, - #{max_topic_alias := MaxTopicAlias}) - when 0 == TopicAlias; TopicAlias >= MaxTopicAlias -> - {error, ?RC_TOPIC_ALIAS_INVALID}; do_check_pub(_Flags, _Caps) -> ok. -spec(check_sub(emqx_types:zone(), diff --git a/src/emqx_packet.erl b/src/emqx_packet.erl index e0351b8a9..d8205b98a 100644 --- a/src/emqx_packet.erl +++ b/src/emqx_packet.erl @@ -82,8 +82,7 @@ validate_packet_id(_) -> validate_properties(?SUBSCRIBE, #{'Subscription-Identifier' := I}) when I =< 0; I >= 16#FFFFFFF -> error(subscription_identifier_invalid); -validate_properties(?PUBLISH, #{'Topic-Alias':= I}) - when I =:= 0 -> +validate_properties(?PUBLISH, #{'Topic-Alias':= 0}) -> error(topic_alias_invalid); validate_properties(?PUBLISH, #{'Subscription-Identifier' := _I}) -> error(protocol_error); diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 6e0ac8336..9f792a097 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -137,6 +137,7 @@ init(ConnInfo, Options) -> MountPoint = emqx_zone:get_env(Zone, mountpoint), Client = maps:merge(#{zone => Zone, username => Username, + client_id => <<>>, mountpoint => MountPoint, is_bridge => false, is_superuser => false @@ -175,13 +176,14 @@ handle_in(?CONNECT_PACKET( fun check_connect/2, fun enrich_client/2, fun auth_connect/2], ConnPkt, PState1) of - {ok, NConnPkt, NPState} -> - process_connect(NConnPkt, maybe_assign_clientid(NPState)); + {ok, NConnPkt, NPState = #protocol{client = #{client_id := ClientId1}}} -> + ok = emqx_logger:set_metadata_client_id(ClientId1), + process_connect(NConnPkt, NPState); {error, ReasonCode, NPState} -> - handle_out({disconnect, ReasonCode}, NPState) + handle_out({connack, ReasonCode}, NPState) end; -handle_in(Packet = ?PUBLISH_PACKET(QoS, Topic, PacketId), PState= #protocol{proto_ver = Ver}) -> +handle_in(Packet = ?PUBLISH_PACKET(_QoS, Topic, _PacketId), PState= #protocol{proto_ver = Ver}) -> case pipeline([fun validate_in/2, fun process_alias/2, fun check_publish/2], Packet, PState) of @@ -190,7 +192,7 @@ handle_in(Packet = ?PUBLISH_PACKET(QoS, Topic, PacketId), PState= #protocol{prot {error, ReasonCode, NPState} -> ?LOG(warning, "Cannot publish message to ~s due to ~s", [Topic, emqx_reason_codes:text(ReasonCode, Ver)]), - puback(QoS, PacketId, ReasonCode, NPState) + handle_out({disconnect, ReasonCode}, NPState) end; handle_in(?PUBACK_PACKET(PacketId, _ReasonCode), PState = #protocol{session = Session}) -> @@ -380,10 +382,6 @@ handle_out({publish, PacketId, Msg}, PState = #protocol{client = Client}) -> Packet = emqx_packet:from_message(PacketId, unmount(Client, Msg1)), {ok, Packet, PState}; -%% TODO: How to handle the err? -handle_out({puberr, _ReasonCode}, PState) -> - {ok, PState}; - handle_out({puback, PacketId, ReasonCode}, PState) -> {ok, ?PUBACK_PACKET(PacketId, ReasonCode), PState}; @@ -500,7 +498,7 @@ check_connect(ConnPkt, PState) -> fun check_banned/2, fun check_will_topic/2, fun check_will_retain/2], ConnPkt, PState) of - ok -> {ok, PState}; + {ok, NConnPkt, NPState} -> {ok, NConnPkt, NPState}; Error -> Error end. @@ -508,7 +506,7 @@ 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} + false -> {error, ?RC_UNSUPPORTED_PROTOCOL_VERSION} end. %% MQTT3.1 does not allow null clientId @@ -571,29 +569,43 @@ check_will_retain(#mqtt_packet_connect{will_retain = true}, %% Enrich client %%-------------------------------------------------------------------- -enrich_client(#mqtt_packet_connect{client_id = ClientId, - username = Username, - is_bridge = IsBridge - }, - PState = #protocol{client = Client}) -> - Client1 = set_username(Username, Client#{client_id => ClientId, - is_bridge => IsBridge - }), - {ok, PState#protocol{client = maybe_username_as_clientid(Client1)}}. +enrich_client(ConnPkt, PState) -> + case pipeline([fun set_username/2, + fun maybe_use_username_as_clientid/2, + fun maybe_assign_clientid/2, + fun set_rest_client_fields/2], ConnPkt, PState) of + {ok, NConnPkt, NPState} -> {ok, NConnPkt, NPState}; + Error -> Error + end. + +maybe_use_username_as_clientid(_ConnPkt, PState = #protocol{client = #{username := undefined}}) -> + {ok, PState}; +maybe_use_username_as_clientid(_ConnPkt, PState = #protocol{client = Client = #{zone := Zone, + username := Username}}) -> + NClient = + case emqx_zone:get_env(Zone, use_username_as_clientid, false) of + true -> Client#{client_id => Username}; + false -> Client + end, + {ok, PState#protocol{client = NClient}}. + +maybe_assign_clientid(#mqtt_packet_connect{client_id = <<>>}, + PState = #protocol{client = Client, ack_props = AckProps}) -> + ClientId = emqx_guid:to_base62(emqx_guid:gen()), + AckProps1 = set_property('Assigned-Client-Identifier', ClientId, AckProps), + {ok, PState#protocol{client = Client#{client_id => ClientId}, ack_props = AckProps1}}; +maybe_assign_clientid(#mqtt_packet_connect{client_id = ClientId}, PState = #protocol{client = Client}) -> + {ok, PState#protocol{client = Client#{client_id => ClientId}}}. %% Username maybe not undefined if peer_cert_as_username -set_username(Username, Client = #{username := undefined}) -> - Client#{username => Username}; -set_username(_Username, Client) -> Client. +set_username(#mqtt_packet_connect{username = Username}, + PState = #protocol{client = Client = #{username := undefined}}) -> + {ok, PState#protocol{client = Client#{username => Username}}}; +set_username(_ConnPkt, PState) -> + {ok, PState}. -maybe_username_as_clientid(Client = #{username := undefined}) -> - Client; -maybe_username_as_clientid(Client = #{zone := Zone, - username := Username}) -> - case emqx_zone:get_env(Zone, use_username_as_clientid, false) of - true -> Client#{client_id => Username}; - false -> Client - end. +set_rest_client_fields(#mqtt_packet_connect{is_bridge = IsBridge}, PState = #protocol{client = Client}) -> + {ok, PState#protocol{client = Client#{is_bridge => IsBridge}}}. %%-------------------------------------------------------------------- %% Auth Connect @@ -612,18 +624,6 @@ auth_connect(#mqtt_packet_connect{client_id = ClientId, {error, emqx_reason_codes:connack_error(Reason)} end. -%%-------------------------------------------------------------------- -%% Assign a random clientId -%%-------------------------------------------------------------------- - -maybe_assign_clientid(PState = #protocol{client = Client = #{client_id := <<>>}, - ack_props = AckProps}) -> - ClientId = emqx_guid:to_base62(emqx_guid:gen()), - Client1 = Client#{client_id => ClientId}, - AckProps1 = set_property('Assigned-Client-Identifier', ClientId, AckProps), - PState#protocol{client = Client1, ack_props = AckProps1}; -maybe_assign_clientid(PState) -> PState. - %%-------------------------------------------------------------------- %% Process Connect %%-------------------------------------------------------------------- @@ -671,7 +671,7 @@ process_alias(Packet = #mqtt_packet{ {ok, Packet#mqtt_packet{ variable = Publish#mqtt_packet_publish{ topic_name = Topic}}, PState}; - false -> {error, ?RC_TOPIC_ALIAS_INVALID} + false -> {error, ?RC_PROTOCOL_ERROR} end; process_alias(#mqtt_packet{ @@ -760,17 +760,6 @@ process_publish(PacketId, Msg = #message{qos = ?QOS_2}, handle_out({pubrec, PacketId, ReasonCode}, PState) end. -%%-------------------------------------------------------------------- -%% Puback -%%-------------------------------------------------------------------- - -puback(?QOS_0, _PacketId, ReasonCode, PState) -> - handle_out({puberr, ReasonCode}, PState); -puback(?QOS_1, PacketId, ReasonCode, PState) -> - handle_out({puback, PacketId, ReasonCode}, PState); -puback(?QOS_2, PacketId, ReasonCode, PState) -> - handle_out({pubrec, PacketId, ReasonCode}, PState). - %%-------------------------------------------------------------------- %% Process subscribe request %%-------------------------------------------------------------------- diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 116f5756e..81db5305c 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -248,6 +248,8 @@ info(created_at, #session{created_at = CreatedAt}) -> %%-------------------------------------------------------------------- -spec(attrs(session()) -> emqx_types:attrs()). +attrs(undefined) -> + #{}; attrs(#session{clean_start = CleanStart, expiry_interval = ExpiryInterval, created_at = CreatedAt}) -> diff --git a/test/emqx_mqtt_caps_SUITE.erl b/test/emqx_mqtt_caps_SUITE.erl index c9cc0b073..961229ed0 100644 --- a/test/emqx_mqtt_caps_SUITE.erl +++ b/test/emqx_mqtt_caps_SUITE.erl @@ -26,23 +26,17 @@ all() -> emqx_ct:all(?MODULE). t_check_pub(_) -> PubCaps = #{max_qos_allowed => ?QOS_1, - retain_available => false, - max_topic_alias => 4 + retain_available => false }, ok = emqx_zone:set_env(zone, '$mqtt_pub_caps', PubCaps), ok = emqx_mqtt_caps:check_pub(zone, #{qos => ?QOS_1, - retain => false, - topic_alias => 1 - }), + retain => false}), PubFlags1 = #{qos => ?QOS_2, retain => false}, ?assertEqual({error, ?RC_QOS_NOT_SUPPORTED}, emqx_mqtt_caps:check_pub(zone, PubFlags1)), PubFlags2 = #{qos => ?QOS_1, retain => true}, ?assertEqual({error, ?RC_RETAIN_NOT_SUPPORTED}, emqx_mqtt_caps:check_pub(zone, PubFlags2)), - PubFlags3 = #{qos => ?QOS_1, retain => false, topic_alias => 5}, - ?assertEqual({error, ?RC_TOPIC_ALIAS_INVALID}, - emqx_mqtt_caps:check_pub(zone, PubFlags3)), true = emqx_zone:unset_env(zone, '$mqtt_pub_caps'). t_check_sub(_) -> From f26505d40ae036617581977a06fd6e0e2243052b Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sat, 17 Aug 2019 13:37:48 +0800 Subject: [PATCH 10/36] Implement session takeover and resumption. - Implement session takeover - Remove `init_proc_mng_policy/1' from emqx_misc module - Remove `conn_proc_mng_policy/1' from emqx_misc module - Add `emqx_oom' module to monitor OOM of channel process --- src/emqx_channel.erl | 1577 +++++++++++------ src/emqx_cm.erl | 63 +- src/emqx_connection.erl | 632 +++++++ src/emqx_listeners.erl | 4 +- src/emqx_misc.erl | 54 - src/emqx_mod_presence.erl | 7 +- src/emqx_oom.erl | 102 ++ src/emqx_protocol.erl | 924 ---------- src/emqx_session.erl | 25 +- ..._ws_channel.erl => emqx_ws_connection.erl} | 281 ++- test/emqx_channel_SUITE.erl | 276 ++- test/emqx_connection_SUITE.erl | 57 + test/emqx_oom_SUITE.erl | 34 + test/emqx_protocol_SUITE.erl | 287 --- ...SUITE.erl => emqx_ws_connection_SUITE.erl} | 2 +- 15 files changed, 2251 insertions(+), 2074 deletions(-) create mode 100644 src/emqx_connection.erl create mode 100644 src/emqx_oom.erl delete mode 100644 src/emqx_protocol.erl rename src/{emqx_ws_channel.erl => emqx_ws_connection.erl} (60%) create mode 100644 test/emqx_connection_SUITE.erl create mode 100644 test/emqx_oom_SUITE.erl delete mode 100644 test/emqx_protocol_SUITE.erl rename test/{emqx_ws_channel_SUITE.erl => emqx_ws_connection_SUITE.erl} (98%) diff --git a/src/emqx_channel.erl b/src/emqx_channel.erl index 630c4de6a..b8cf74ee1 100644 --- a/src/emqx_channel.erl +++ b/src/emqx_channel.erl @@ -14,11 +14,9 @@ %% limitations under the License. %%-------------------------------------------------------------------- -%% MQTT TCP/SSL Channel +%% MQTT Channel -module(emqx_channel). --behaviour(gen_statem). - -include("emqx.hrl"). -include("emqx_mqtt.hrl"). -include("logger.hrl"). @@ -26,652 +24,1063 @@ -logger_header("[Channel]"). --export([start_link/3]). - -%% APIs -export([ info/1 + , info/2 , attrs/1 - , stats/1 + , caps/1 ]). -%% for Debug --export([state/1]). +%% for tests +-export([set/3]). -%% state callbacks --export([ idle/3 - , connected/3 - , disconnected/3 +-export([takeover/2]). + +-export([ init/2 + , handle_in/2 + , handle_out/2 + , handle_out/3 + , handle_call/2 + , handle_cast/2 + , handle_info/2 + , timeout/3 + , terminate/2 ]). -%% gen_statem callbacks --export([ init/1 - , callback_mode/0 - , code_change/4 - , terminate/3 +-export([gc/3]). + +-import(emqx_access_control, + [ authenticate/1 + , check_acl/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()), - 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(), - connected :: boolean(), - connected_at :: erlang:timestamp() - }). +-export_type([channel/0]). --type(state() :: #state{}). +-record(channel, { + client :: emqx_types:client(), + session :: emqx_session:session(), + proto_name :: binary(), + proto_ver :: emqx_types:ver(), + keepalive :: non_neg_integer(), + will_msg :: emqx_types:message(), + topic_aliases :: maybe(map()), + alias_maximum :: maybe(map()), + ack_props :: maybe(emqx_types:properties()), + idle_timeout :: timeout(), + retry_timer :: maybe(reference()), + alive_timer :: maybe(reference()), + stats_timer :: disabled | maybe(reference()), + expiry_timer :: maybe(reference()), + gc_state :: emqx_gc:gc_state(), %% GC State + oom_policy :: emqx_oom:oom_policy(), %% OOM Policy + connected :: boolean(), + connected_at :: erlang:timestamp(), + resuming :: boolean(), + pendings :: list() + }). --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]). +-opaque(channel() :: #channel{}). --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}])}. +-define(NO_PROPS, undefined). %%-------------------------------------------------------------------- -%% API +%% Info, Attrs and Caps %%-------------------------------------------------------------------- -%% @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)). +-spec(info(channel()) -> emqx_types:infos()). +info(#channel{client = Client, + session = Session, + proto_name = ProtoName, + proto_ver = ProtoVer, + keepalive = Keepalive, + will_msg = WillMsg, + topic_aliases = Aliases, + stats_timer = StatsTimer, + idle_timeout = IdleTimeout, + gc_state = GCState, + connected = Connected, + connected_at = ConnectedAt}) -> + #{client => Client, + session => if Session == undefined -> + undefined; + true -> emqx_session:info(Session) + end, + proto_name => ProtoName, + proto_ver => ProtoVer, + keepalive => Keepalive, + will_msg => WillMsg, + topic_aliases => Aliases, + enable_stats => case StatsTimer of + disabled -> false; + _Otherwise -> true + end, + idle_timeout => IdleTimeout, + gc_state => emqx_gc:info(GCState), + connected => Connected, + connected_at => ConnectedAt, + resuming => false, + pendings => [] + }. -limit_info(undefined) -> - undefined; -limit_info(Limit) -> - esockd_rate_limit:info(Limit). +-spec(info(atom(), channel()) -> term()). +info(client, #channel{client = Client}) -> + Client; +info(zone, #channel{client = #{zone := Zone}}) -> + Zone; +info(client_id, #channel{client = #{client_id := ClientId}}) -> + ClientId; +info(session, #channel{session = Session}) -> + Session; +info(proto_name, #channel{proto_name = ProtoName}) -> + ProtoName; +info(proto_ver, #channel{proto_ver = ProtoVer}) -> + ProtoVer; +info(keepalive, #channel{keepalive = Keepalive}) -> + Keepalive; +info(will_msg, #channel{will_msg = WillMsg}) -> + WillMsg; +info(topic_aliases, #channel{topic_aliases = Aliases}) -> + Aliases; +info(enable_stats, #channel{stats_timer = disabled}) -> + false; +info(enable_stats, #channel{stats_timer = _TRef}) -> + true; +info(idle_timeout, #channel{idle_timeout = IdleTimeout}) -> + IdleTimeout; +info(gc_state, #channel{gc_state = GCState}) -> + emqx_gc:info(GCState); +info(connected, #channel{connected = Connected}) -> + Connected; +info(connected_at, #channel{connected_at = ConnectedAt}) -> + ConnectedAt. -%% @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)). +-spec(attrs(channel()) -> emqx_types:attrs()). +attrs(#channel{client = Client, + session = Session, + proto_name = ProtoName, + proto_ver = ProtoVer, + keepalive = Keepalive, + connected = Connected, + connected_at = ConnectedAt}) -> + #{client => Client, + session => if Session == undefined -> + undefined; + true -> emqx_session:attrs(Session) + end, + proto_name => ProtoName, + proto_ver => ProtoVer, + keepalive => Keepalive, + connected => Connected, + connected_at => ConnectedAt + }. -%% @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, - 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], - 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) -> - gen_statem:call(CPid, Req, infinity). +-spec(caps(channel()) -> emqx_types:caps()). +caps(#channel{client = #{zone := Zone}}) -> + emqx_mqtt_caps:get_caps(Zone). %%-------------------------------------------------------------------- -%% gen_statem callbacks +%% For unit tests %%-------------------------------------------------------------------- -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)), +set(client, Client, Channel) -> + Channel#channel{client = Client}; +set(session, Session, Channel) -> + Channel#channel{session = Session}. + +%%-------------------------------------------------------------------- +%% Takeover session +%%-------------------------------------------------------------------- + +takeover('begin', Channel = #channel{session = Session}) -> + {ok, Session, Channel#channel{resuming = true}}; + +takeover('end', Channel = #channel{session = Session, + pendings = Pendings}) -> + ok = emqx_session:takeover(Session), + {ok, Pendings, Channel}. + +%%-------------------------------------------------------------------- +%% Init a channel +%%-------------------------------------------------------------------- + +-spec(init(emqx_types:conn(), proplists:proplist()) -> channel()). +init(ConnInfo, Options) -> 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}), - 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, + Peercert = maps:get(peercert, ConnInfo, undefined), + Username = case peer_cert_as_username(Options) of + cn -> esockd_peercert:common_name(Peercert); + dn -> esockd_peercert:subject(Peercert); + crt -> Peercert; + _ -> undefined + end, + MountPoint = emqx_zone:get_env(Zone, mountpoint), + Client = maps:merge(#{zone => Zone, + username => Username, + mountpoint => MountPoint, + is_bridge => false, + is_superuser => false}, ConnInfo), 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, - sockname = Sockname, - 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, - connected = false - }, - gen_statem:enter_loop(?MODULE, [{hibernate_after, 2 * IdleTimout}], - idle, State, self(), [IdleTimout]). + EnableStats = emqx_zone:get_env(Zone, enable_stats, true), + StatsTimer = if EnableStats -> undefined; + ?Otherwise -> disabled + end, + GcState = emqx_gc:init(emqx_zone:get_env(Zone, force_gc_policy, false)), + OomPolicy = emqx_oom:init(emqx_zone:get_env(Zone, force_shutdown_policy)), + #channel{client = Client, + proto_name = <<"MQTT">>, + proto_ver = ?MQTT_PROTO_V4, + keepalive = 0, + idle_timeout = IdleTimout, + stats_timer = StatsTimer, + gc_state = GcState, + oom_policy = OomPolicy, + connected = false + }. -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}) -> - 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, info(NState)), - %% Ensure keepalive after connected successfully. - Interval = emqx_protocol:info(keepalive, ProtoState), - case ensure_keepalive(Interval, NState) of - ignore -> keep_state(NState); - {ok, KeepAlive} -> - keep_state(NState#state{keepalive = KeepAlive}); - {error, Reason} -> - shutdown(Reason, NState) - 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; - -%% TODO: Improve later. -connected(info, {subscribe, TopicFilters}, State) -> - handle_request({subscribe, TopicFilters}, State); - -connected(info, {unsubscribe, TopicFilters}, State) -> - handle_request({unsubscribe, TopicFilters}, State); - -%% 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}, 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); - -%%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: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), - 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). - -%%-------------------------------------------------------------------- -%% Handle internal request - -handle_request(Req, State = #state{proto_state = ProtoState}) -> - case emqx_protocol:handle_req(Req, ProtoState) of - {ok, _Result, NProtoState} -> %% TODO:: how to handle the result? - keep_state(State#state{proto_state = NProtoState}); - {error, Reason, NProtoState} -> - shutdown(Reason, State#state{proto_state = NProtoState}) - end. - -%%-------------------------------------------------------------------- -%% 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)]. +peer_cert_as_username(Options) -> + proplists:get_value(peer_cert_as_username, Options). %%-------------------------------------------------------------------- %% Handle incoming packet +%%-------------------------------------------------------------------- -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}); - {error, Reason, OutPacket, NProtoState} -> - Shutdown = fun(NewSt) -> shutdown(Reason, NewSt) end, - handle_outgoing(OutPacket, Shutdown, State#state{proto_state = NProtoState}); - {stop, Error, NProtoState} -> - stop(Error, State#state{proto_state = NProtoState}) - end. +-spec(handle_in(emqx_types:packet(), channel()) + -> {ok, channel()} + | {ok, emqx_types:packet(), channel()} + | {ok, list(emqx_types:packet()), channel()} + | {stop, Error :: term(), channel()} + | {stop, Error :: term(), emqx_types:packet(), channel()}). +handle_in(?CONNECT_PACKET(_), Channel = #channel{connected = true}) -> + handle_out(disconnect, ?RC_PROTOCOL_ERROR, Channel); + +handle_in(?CONNECT_PACKET( + #mqtt_packet_connect{proto_name = ProtoName, + proto_ver = ProtoVer, + keepalive = Keepalive, + client_id = ClientId + } = ConnPkt), Channel) -> + Channel1 = Channel#channel{proto_name = ProtoName, + proto_ver = ProtoVer, + keepalive = Keepalive + }, + ok = emqx_logger:set_metadata_client_id(ClientId), + case pipeline([fun validate_in/2, + fun process_props/2, + fun check_connect/2, + fun enrich_client/2, + fun auth_connect/2], ConnPkt, Channel1) of + {ok, NConnPkt, NChannel} -> + process_connect(NConnPkt, maybe_assign_clientid(NChannel)); + {error, ReasonCode, NChannel} -> + handle_out(disconnect, ReasonCode, NChannel) + end; + +handle_in(Packet = ?PUBLISH_PACKET(QoS, Topic, PacketId), Channel) -> + case pipeline([fun validate_in/2, + fun process_alias/2, + fun check_publish/2], Packet, Channel) of + {ok, NPacket, NChannel} -> + process_publish(NPacket, NChannel); + {error, ReasonCode, NChannel} -> + ?LOG(warning, "Cannot publish message to ~s due to ~s", + [Topic, emqx_reason_codes:text(ReasonCode)]), + case QoS of + ?QOS_0 -> handle_out(puberr, ReasonCode, NChannel); + ?QOS_1 -> handle_out(puback, {PacketId, ReasonCode}, NChannel); + ?QOS_2 -> handle_out(pubrec, {PacketId, ReasonCode}, NChannel) + end + end; + +%%TODO: How to handle the ReasonCode? +handle_in(?PUBACK_PACKET(PacketId, _ReasonCode), Channel = #channel{session = Session}) -> + case emqx_session:puback(PacketId, Session) of + {ok, Publishes, NSession} -> + handle_out(publish, Publishes, Channel#channel{session = NSession}); + {ok, NSession} -> + {ok, Channel#channel{session = NSession}}; + {error, _NotFound} -> + %%TODO: How to handle NotFound, inc metrics? + {ok, Channel} + end; + +%%TODO: How to handle the ReasonCode? +handle_in(?PUBREC_PACKET(PacketId, _ReasonCode), Channel = #channel{session = Session}) -> + case emqx_session:pubrec(PacketId, Session) of + {ok, NSession} -> + handle_out(pubrel, {PacketId, ?RC_SUCCESS}, Channel#channel{session = NSession}); + {error, ReasonCode} -> + handle_out(pubrel, {PacketId, ReasonCode}, Channel) + end; + +%%TODO: How to handle the ReasonCode? +handle_in(?PUBREL_PACKET(PacketId, _ReasonCode), Channel = #channel{session = Session}) -> + case emqx_session:pubrel(PacketId, Session) of + {ok, NSession} -> + handle_out(pubcomp, {PacketId, ?RC_SUCCESS}, 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, Session) of + {ok, Publishes, NSession} -> + handle_out(publish, Publishes, Channel#channel{session = NSession}); + {ok, NSession} -> + {ok, Channel#channel{session = NSession}}; + {error, _NotFound} -> + %% TODO: how to handle NotFound? + {ok, Channel} + end; + +handle_in(Packet = ?SUBSCRIBE_PACKET(PacketId, Properties, TopicFilters), + Channel = #channel{client = Client}) -> + case validate_in(Packet, Channel) of + ok -> + TopicFilters1 = [emqx_topic:parse(TopicFilter, SubOpts) + || {TopicFilter, SubOpts} <- TopicFilters], + TopicFilters2 = emqx_hooks:run_fold('client.subscribe', + [Client, Properties], + TopicFilters1), + TopicFilters3 = enrich_subid(Properties, TopicFilters2), + {ReasonCodes, NChannel} = process_subscribe(TopicFilters3, Channel), + handle_out(suback, {PacketId, ReasonCodes}, NChannel); + {error, ReasonCode} -> + handle_out(disconnect, ReasonCode, Channel) + end; + +handle_in(Packet = ?UNSUBSCRIBE_PACKET(PacketId, Properties, TopicFilters), + Channel = #channel{client = Client}) -> + case validate_in(Packet, Channel) of + ok -> + TopicFilters1 = lists:map(fun emqx_topic:parse/1, TopicFilters), + TopicFilters2 = emqx_hooks:run_fold('client.unsubscribe', + [Client, Properties], + TopicFilters1), + {ReasonCodes, NChannel} = process_unsubscribe(TopicFilters2, Channel), + handle_out(unsuback, {PacketId, ReasonCodes}, NChannel); + {error, ReasonCode} -> + handle_out(disconnect, ReasonCode, Channel) + end; + +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}) -> + {stop, {shutdown, emqx_reason_codes:name(RC, Ver)}, Channel}; + +handle_in(?AUTH_PACKET(), Channel) -> + %%TODO: implement later. + {ok, Channel}; + +handle_in(Packet, Channel) -> + ?LOG(error, "Unexpected incoming: ~p", [Packet]), + {stop, {shutdown, unexpected_incoming_packet}, Channel}. %%-------------------------------------------------------------------- -%% Handle outgoing packets - -handle_outgoing(Packets, SuccFun, State = #state{serialize = Serialize}) - when is_list(Packets) -> - send(lists:map(Serialize, Packets), SuccFun, State); - -handle_outgoing(Packet, SuccFun, State = #state{serialize = Serialize}) -> - send(Serialize(Packet), SuccFun, State). - +%% Process Connect %%-------------------------------------------------------------------- -%% 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. - -%%-------------------------------------------------------------------- -%% 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)); +process_connect(ConnPkt, Channel) -> + case open_session(ConnPkt, Channel) of + {ok, Session, SP} -> + WillMsg = emqx_packet:will_msg(ConnPkt), + NChannel = Channel#channel{session = Session, + will_msg = WillMsg, + connected = true, + connected_at = os:timestamp() + }, + handle_out(connack, {?RC_SUCCESS, sp(SP)}, NChannel); {error, Reason} -> - shutdown(Reason, State) + %% TODO: Unknown error? + ?LOG(error, "Failed to open session: ~p", [Reason]), + handle_out(connack, ?RC_UNSPECIFIED_ERROR, Channel) end. %%-------------------------------------------------------------------- -%% Ensure keepalive - -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}). - +%% Process 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). +%% Process Publish +process_publish(Packet = ?PUBLISH_PACKET(_QoS, _Topic, PacketId), + Channel = #channel{client = Client}) -> + Msg = emqx_packet:to_message(Client, Packet), + %%TODO: Improve later. + Msg1 = emqx_message:set_flag(dup, false, Msg), + process_publish(PacketId, mount(Client, Msg1), Channel). -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) +process_publish(_PacketId, Msg = #message{qos = ?QOS_0}, Channel) -> + _ = emqx_broker:publish(Msg), + {ok, Channel}; + +process_publish(PacketId, Msg = #message{qos = ?QOS_1}, Channel) -> + Deliveries = emqx_broker:publish(Msg), + ReasonCode = emqx_reason_codes:puback(Deliveries), + handle_out(puback, {PacketId, ReasonCode}, Channel); + +process_publish(PacketId, Msg = #message{qos = ?QOS_2}, + Channel = #channel{session = Session}) -> + case emqx_session:publish(PacketId, Msg, Session) of + {ok, Deliveries, NSession} -> + ReasonCode = emqx_reason_codes:puback(Deliveries), + handle_out(pubrec, {PacketId, ReasonCode}, + Channel#channel{session = NSession}); + {error, ReasonCode} -> + handle_out(pubrec, {PacketId, ReasonCode}, Channel) end. %%-------------------------------------------------------------------- -%% Activate Socket +%% Process Subscribe +%%-------------------------------------------------------------------- -activate_socket(#state{conn_state = blocked}) -> +process_subscribe(TopicFilters, Channel) -> + process_subscribe(TopicFilters, [], Channel). + +process_subscribe([], Acc, Channel) -> + {lists:reverse(Acc), Channel}; + +process_subscribe([{TopicFilter, SubOpts}|More], Acc, Channel) -> + {RC, NChannel} = do_subscribe(TopicFilter, SubOpts, Channel), + process_subscribe(More, [RC|Acc], NChannel). + +do_subscribe(TopicFilter, SubOpts = #{qos := QoS}, + Channel = #channel{client = Client, session = Session}) -> + case check_subscribe(TopicFilter, SubOpts, Channel) of + ok -> TopicFilter1 = mount(Client, TopicFilter), + SubOpts1 = enrich_subopts(maps:merge(?DEFAULT_SUBOPTS, SubOpts), Channel), + case emqx_session:subscribe(Client, TopicFilter1, SubOpts1, Session) of + {ok, NSession} -> + {QoS, Channel#channel{session = NSession}}; + {error, RC} -> {RC, Channel} + end; + {error, RC} -> {RC, Channel} + end. + +%%-------------------------------------------------------------------- +%% Process Unsubscribe +%%-------------------------------------------------------------------- + +process_unsubscribe(TopicFilters, Channel) -> + process_unsubscribe(TopicFilters, [], Channel). + +process_unsubscribe([], Acc, Channel) -> + {lists:reverse(Acc), Channel}; + +process_unsubscribe([{TopicFilter, SubOpts}|More], Acc, Channel) -> + {RC, Channel1} = do_unsubscribe(TopicFilter, SubOpts, Channel), + process_unsubscribe(More, [RC|Acc], Channel1). + +do_unsubscribe(TopicFilter, _SubOpts, Channel = #channel{client = Client, + session = Session}) -> + case emqx_session:unsubscribe(Client, mount(Client, TopicFilter), Session) of + {ok, NSession} -> + {?RC_SUCCESS, Channel#channel{session = NSession}}; + {error, RC} -> {RC, Channel} + end. + +%%-------------------------------------------------------------------- +%% Handle outgoing packet +%%-------------------------------------------------------------------- + +handle_out(Deliver = {deliver, _Topic, _Msg}, + Channel = #channel{resuming = true, pendings = Pendings}) -> + Delivers = emqx_misc:drain_deliver([Deliver]), + {ok, Channel#channel{pendings = lists:append(Pendings, Delivers)}}; + +handle_out(Deliver = {deliver, _Topic, _Msg}, Channel = #channel{session = Session}) -> + Delivers = emqx_misc:drain_deliver([Deliver]), + case emqx_session:deliver(Delivers, Session) of + {ok, Publishes, NSession} -> + handle_out(publish, Publishes, Channel#channel{session = NSession}); + {ok, NSession} -> + {ok, Channel#channel{session = NSession}} + end; + +handle_out({publish, PacketId, Msg}, Channel = #channel{client = Client}) -> + Msg1 = emqx_hooks:run_fold('message.deliver', [Client], + emqx_message:update_expiry(Msg)), + Packet = emqx_packet:from_message(PacketId, unmount(Client, Msg1)), + {ok, Packet, Channel}. + +handle_out(connack, {?RC_SUCCESS, SP}, + Channel = #channel{client = Client = #{zone := Zone}, + ack_props = AckProps, + alias_maximum = AliasMaximum}) -> + ok = emqx_hooks:run('client.connected', [Client, ?RC_SUCCESS, attrs(Channel)]), + #{max_packet_size := MaxPktSize, + max_qos_allowed := MaxQoS, + retain_available := Retain, + max_topic_alias := MaxAlias, + shared_subscription := Shared, + wildcard_subscription := Wildcard + } = caps(Channel), + %% 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. + AckProps1 = if AckProps == undefined -> #{}; true -> AckProps end, + AckProps2 = AckProps1#{'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), + 'Maximum-QoS' => MaxQoS + }, + AckProps3 = case emqx_zone:get_env(Zone, server_keepalive) of + undefined -> AckProps2; + Keepalive -> AckProps2#{'Server-Keep-Alive' => Keepalive} + end, + AliasMaximum1 = set_property(inbound, MaxAlias, AliasMaximum), + Channel1 = Channel#channel{alias_maximum = AliasMaximum1, + ack_props = undefined + }, + {ok, ?CONNACK_PACKET(?RC_SUCCESS, SP, AckProps3), Channel1}; + +handle_out(connack, ReasonCode, Channel = #channel{client = Client, + proto_ver = ProtoVer}) -> + ok = emqx_hooks:run('client.connected', [Client, 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), + {stop, {shutdown, Reason}, ?CONNACK_PACKET(ReasonCode1), Channel}; + +handle_out(publish, Publishes, Channel) -> + Packets = [element(2, handle_out(Publish, Channel)) || Publish <- Publishes], + {ok, Packets, Channel}; + +%% TODO: How to handle the puberr? +handle_out(puberr, _ReasonCode, Channel) -> + {ok, Channel}; + +handle_out(puback, {PacketId, ReasonCode}, Channel) -> + {ok, ?PUBACK_PACKET(PacketId, ReasonCode), 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, 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(disconnect, ReasonCode, Channel = #channel{proto_ver = ?MQTT_PROTO_V5}) -> + Reason = emqx_reason_codes:name(ReasonCode), + {stop, {shutdown, Reason}, ?DISCONNECT_PACKET(ReasonCode), Channel}; + +handle_out(disconnect, ReasonCode, Channel = #channel{proto_ver = ProtoVer}) -> + {stop, {shutdown, emqx_reason_codes:name(ReasonCode, ProtoVer)}, Channel}; + +handle_out(Type, Data, Channel) -> + ?LOG(error, "Unexpected outgoing: ~s, ~p", [Type, Data]), + {ok, Channel}. + +%%-------------------------------------------------------------------- +%% Handle call +%%-------------------------------------------------------------------- + +handle_call(Req, Channel) -> + ?LOG(error, "Unexpected call: Req", [Req]), + {ok, ignored, Channel}. + +%%-------------------------------------------------------------------- +%% Handle cast +%%-------------------------------------------------------------------- + +handle_cast(discard, Channel) -> + {stop, {shutdown, discarded}, Channel}; + +handle_cast(Msg, Channel) -> + ?LOG(error, "Unexpected cast: ~p", [Msg]), + {ok, Channel}. + +%%-------------------------------------------------------------------- +%% Handle Info +%%-------------------------------------------------------------------- + +-spec(handle_info(Info :: term(), channel()) + -> {ok, channel()} | {stop, Reason :: term(), channel()}). +handle_info({subscribe, TopicFilters}, Channel = #channel{client = Client}) -> + TopicFilters1 = emqx_hooks:run_fold('client.subscribe', + [Client, #{'Internal' => true}], + parse(subscribe, TopicFilters)), + {_ReasonCodes, NChannel} = process_subscribe(TopicFilters1, Channel), + {ok, NChannel}; + +handle_info({unsubscribe, TopicFilters}, Channel = #channel{client = Client}) -> + TopicFilters1 = emqx_hooks:run_fold('client.unsubscribe', + [Client, #{'Internal' => true}], + parse(unsubscribe, TopicFilters)), + {_ReasonCodes, NChannel} = process_unsubscribe(TopicFilters1, Channel), + {ok, NChannel}; + +handle_info(Info, Channel) -> + ?LOG(error, "Unexpected info: ~p~n", [Info]), + {ok, Channel}. + +%%-------------------------------------------------------------------- +%% Handle timeout +%%-------------------------------------------------------------------- + +-spec(timeout(reference(), Msg :: term(), channel()) + -> {ok, channel()} + | {ok, Result :: term(), channel()} + | {stop, Reason :: term(), channel()}). +timeout(TRef, retry_deliver, Channel = #channel{%%session = Session, + retry_timer = TRef}) -> + %% case emqx_session:retry(Session) of + %% TODO: ... + {ok, Channel#channel{retry_timer = undefined}}; + +timeout(TRef, emit_stats, Channel = #channel{stats_timer = TRef}) -> + ClientId = info(client_id, Channel), + %% ok = emqx_cm:set_chan_stats(ClientId, stats(Channel)), + {ok, Channel#channel{stats_timer = undefined}}; + +timeout(_TRef, Msg, Channel) -> + ?LOG(error, "Unexpected timeout: ~p~n", [Msg]), + {ok, Channel}. + +%%-------------------------------------------------------------------- +%% Ensure timers +%%-------------------------------------------------------------------- + +ensure_timer(retry, Channel = #channel{session = Session, + retry_timer = undefined}) -> + Interval = emqx_session:info(retry_interval, Session), + TRef = emqx_misc:start_timer(Interval, retry_deliver), + Channel#channel{retry_timer = TRef}; + +ensure_timer(stats, Channel = #channel{stats_timer = undefined, + idle_timeout = IdleTimeout}) -> + TRef = emqx_misc:start_timer(IdleTimeout, emit_stats), + Channel#channel{stats_timer = TRef}; + +%% disabled or timer existed +ensure_timer(_Name, Channel) -> + Channel. + +%%-------------------------------------------------------------------- +%% Terminate +%%-------------------------------------------------------------------- + +terminate(normal, #channel{client = Client}) -> + ok = emqx_hooks:run('client.disconnected', [Client, normal]); +terminate(Reason, #channel{client = Client, will_msg = WillMsg}) -> + ok = emqx_hooks:run('client.disconnected', [Client, Reason]), + publish_will_msg(WillMsg). + +%%TODO: Improve will msg:) +publish_will_msg(undefined) -> ok; -activate_socket(#state{transport = Transport, - socket = Socket, - active_n = N}) -> - Transport:setopts(Socket, [{active, N}]). +publish_will_msg(Msg) -> + emqx_broker:publish(Msg). %%-------------------------------------------------------------------- -%% Inc incoming/outgoing stats +%% GC the channel. +%%-------------------------------------------------------------------- --compile({inline, - [ inc_incoming_stats/1 - , inc_outgoing_stats/1 - ]}). +gc(_Cnt, _Oct, Channel = #channel{gc_state = undefined}) -> + Channel; +gc(Cnt, Oct, Channel = #channel{gc_state = GCSt}) -> + {Ok, GCSt1} = emqx_gc:run(Cnt, Oct, GCSt), + Ok andalso emqx_metrics:inc('channel.gc.cnt'), + Channel#channel{gc_state = GCSt1}. -inc_incoming_stats(Type) -> - emqx_pd:update_counter(recv_pkt, 1), - case Type == ?PUBLISH of +%%-------------------------------------------------------------------- +%% Validate incoming packet +%%-------------------------------------------------------------------- + +-spec(validate_in(emqx_types:packet(), channel()) + -> ok | {error, emqx_types:reason_code()}). +validate_in(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 properties +%%-------------------------------------------------------------------- + +process_props(#mqtt_packet_connect{ + properties = #{'Topic-Alias-Maximum' := Max} + }, + Channel = #channel{alias_maximum = AliasMaximum}) -> + NAliasMaximum = if AliasMaximum == undefined -> + #{outbound => Max}; + true -> AliasMaximum#{outbound => Max} + end, + {ok, Channel#channel{alias_maximum = NAliasMaximum}}; + +process_props(Packet, Channel) -> + {ok, Packet, Channel}. + +%%-------------------------------------------------------------------- +%% Check connect packet +%%-------------------------------------------------------------------- + +check_connect(ConnPkt, Channel) -> + case 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, Channel) of + ok -> {ok, Channel}; + Error -> Error + end. + +check_proto_ver(#mqtt_packet_connect{proto_ver = Ver, + proto_name = Name}, _Channel) -> + 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 = <<>> + }, _Channel) -> + {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}, _Channel) -> + {error, ?RC_CLIENT_IDENTIFIER_NOT_VALID}; + +check_client_id(#mqtt_packet_connect{client_id = <<>>, + clean_start = true}, _Channel) -> + ok; + +check_client_id(#mqtt_packet_connect{client_id = ClientId}, + #channel{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. + +%%TODO: check banned... +check_banned(#mqtt_packet_connect{client_id = ClientId, + username = Username}, + #channel{client = Client = #{zone := Zone}}) -> + case emqx_zone:get_env(Zone, enable_ban, false) of true -> - emqx_pd:update_counter(recv_msg, 1), - emqx_pd:update_counter(incoming_pubs, 1); + case emqx_banned:check(Client#{client_id => ClientId, + username => Username}) of + true -> {error, ?RC_BANNED}; + false -> ok + end; false -> ok end. -inc_outgoing_stats(Type) -> - emqx_pd:update_counter(send_pkt, 1), - (Type == ?PUBLISH) - andalso emqx_pd:update_counter(send_msg, 1). +check_will_topic(#mqtt_packet_connect{will_flag = false}, _Channel) -> + ok; +check_will_topic(#mqtt_packet_connect{will_topic = WillTopic}, _Channel) -> + 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}, _Channel) -> + ok; +check_will_retain(#mqtt_packet_connect{will_retain = true}, + #channel{client = #{zone := Zone}}) -> + case emqx_zone:get_env(Zone, mqtt_retain_available, true) of + true -> ok; + false -> {error, ?RC_RETAIN_NOT_SUPPORTED} + end. %%-------------------------------------------------------------------- -%% Ensure stats timer +%% Enrich client +%%-------------------------------------------------------------------- -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. +enrich_client(#mqtt_packet_connect{client_id = ClientId, + username = Username, + is_bridge = IsBridge + }, + Channel = #channel{client = Client}) -> + Client1 = set_username(Username, Client#{client_id => ClientId, + is_bridge => IsBridge + }), + {ok, Channel#channel{client = maybe_username_as_clientid(Client1)}}. + +%% Username maybe not undefined if peer_cert_as_username +set_username(Username, Client = #{username := undefined}) -> + Client#{username => Username}; +set_username(_Username, Client) -> Client. + +maybe_username_as_clientid(Client = #{username := undefined}) -> + Client; +maybe_username_as_clientid(Client = #{zone := Zone, + username := Username}) -> + case emqx_zone:get_env(Zone, use_username_as_clientid, false) of + true -> Client#{client_id => Username}; + false -> Client + end. %%-------------------------------------------------------------------- -%% Maybe GC +%% Auth Connect +%%-------------------------------------------------------------------- -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}. +auth_connect(#mqtt_packet_connect{client_id = ClientId, + username = Username, + password = Password}, + Channel = #channel{client = Client}) -> + case authenticate(Client#{password => Password}) of + {ok, AuthResult} -> + {ok, Channel#channel{client = maps:merge(Client, AuthResult)}}; + {error, Reason} -> + ?LOG(warning, "Client ~s (Username: '~s') login failed for ~0p", + [ClientId, Username, Reason]), + {error, emqx_reason_codes:connack_error(Reason)} + end. + +%%-------------------------------------------------------------------- +%% Open session +%%-------------------------------------------------------------------- + +open_session(#mqtt_packet_connect{clean_start = CleanStart, + properties = ConnProps}, + #channel{client = Client = #{zone := Zone}}) -> + MaxInflight = get_property('Receive-Maximum', ConnProps, + emqx_zone:get_env(Zone, max_inflight, 65535)), + Interval = get_property('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 + }). + +%%-------------------------------------------------------------------- +%% Assign a random clientId +%%-------------------------------------------------------------------- + +maybe_assign_clientid(Channel = #channel{client = Client = #{client_id := <<>>}, + ack_props = AckProps}) -> + ClientId = emqx_guid:to_base62(emqx_guid:gen()), + Client1 = Client#{client_id => ClientId}, + AckProps1 = set_property('Assigned-Client-Identifier', ClientId, AckProps), + Channel#channel{client = Client1, ack_props = AckProps1}; +maybe_assign_clientid(Channel) -> Channel. + +%%-------------------------------------------------------------------- +%% Process publish message: Client -> Broker +%%-------------------------------------------------------------------- + +process_alias(Packet = #mqtt_packet{ + variable = #mqtt_packet_publish{topic_name = <<>>, + properties = #{'Topic-Alias' := AliasId} + } = Publish + }, Channel = #channel{topic_aliases = Aliases}) -> + case find_alias(AliasId, Aliases) of + {ok, Topic} -> + {ok, Packet#mqtt_packet{ + variable = Publish#mqtt_packet_publish{ + topic_name = Topic}}, Channel}; + false -> {error, ?RC_TOPIC_ALIAS_INVALID} + end; + +process_alias(#mqtt_packet{ + variable = #mqtt_packet_publish{topic_name = Topic, + properties = #{'Topic-Alias' := AliasId} + } + }, Channel = #channel{topic_aliases = Aliases}) -> + {ok, Channel#channel{topic_aliases = save_alias(AliasId, Topic, Aliases)}}; + +process_alias(_Packet, Channel) -> + {ok, Channel}. + +find_alias(_AliasId, undefined) -> + false; +find_alias(AliasId, Aliases) -> + maps:find(AliasId, Aliases). + +save_alias(AliasId, Topic, undefined) -> + #{AliasId => Topic}; +save_alias(AliasId, Topic, Aliases) -> + maps:put(AliasId, Topic, Aliases). + +%% Check Publish +check_publish(Packet, Channel) -> + pipeline([fun check_pub_acl/2, + fun check_pub_alias/2, + fun check_pub_caps/2], Packet, Channel). + +%% Check Pub ACL +check_pub_acl(#mqtt_packet{variable = #mqtt_packet_publish{topic_name = Topic}}, + #channel{client = Client}) -> + case is_acl_enabled(Client) andalso check_acl(Client, publish, Topic) of + false -> ok; + allow -> ok; + deny -> {error, ?RC_NOT_AUTHORIZED} + end. + +%% Check Pub Alias +check_pub_alias(#mqtt_packet{ + variable = #mqtt_packet_publish{ + properties = #{'Topic-Alias' := AliasId} + } + }, + #channel{alias_maximum = Limits}) -> + case (Limits == undefined) + orelse (Max = maps:get(inbound, Limits, 0)) == 0 + orelse (AliasId > Max) of + false -> ok; + true -> {error, ?RC_TOPIC_ALIAS_INVALID} + end; +check_pub_alias(_Packet, _Channel) -> ok. + +%% Check Pub Caps +check_pub_caps(#mqtt_packet{header = #mqtt_packet_header{qos = QoS, + retain = Retain + } + }, + #channel{client = #{zone := Zone}}) -> + emqx_mqtt_caps:check_pub(Zone, #{qos => QoS, retain => Retain}). + +%% Check Sub +check_subscribe(TopicFilter, SubOpts, Channel) -> + case check_sub_acl(TopicFilter, Channel) of + allow -> check_sub_caps(TopicFilter, SubOpts, Channel); + deny -> {error, ?RC_NOT_AUTHORIZED} + end. + +%% Check Sub ACL +check_sub_acl(TopicFilter, #channel{client = Client}) -> + case is_acl_enabled(Client) andalso + check_acl(Client, subscribe, TopicFilter) of + false -> allow; + Result -> Result + end. + +%% Check Sub Caps +check_sub_caps(TopicFilter, SubOpts, #channel{client = #{zone := Zone}}) -> + emqx_mqtt_caps:check_sub(Zone, TopicFilter, SubOpts). + +enrich_subid(#{'Subscription-Identifier' := SubId}, TopicFilters) -> + [{Topic, SubOpts#{subid => SubId}} || {Topic, SubOpts} <- TopicFilters]; +enrich_subid(_Properties, TopicFilters) -> + TopicFilters. + +enrich_subopts(SubOpts, #channel{proto_ver = ?MQTT_PROTO_V5}) -> + SubOpts; +enrich_subopts(SubOpts, #channel{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}. + +%%-------------------------------------------------------------------- +%% Is ACL enabled? +%%-------------------------------------------------------------------- + +is_acl_enabled(#{zone := Zone, is_superuser := IsSuperuser}) -> + (not IsSuperuser) andalso emqx_zone:get_env(Zone, enable_acl, true). + +%%-------------------------------------------------------------------- +%% Parse Topic Filters +%%-------------------------------------------------------------------- + +parse(subscribe, TopicFilters) -> + [emqx_topic:parse(TopicFilter, SubOpts) || {TopicFilter, SubOpts} <- TopicFilters]; + +parse(unsubscribe, TopicFilters) -> + lists:map(fun emqx_topic:parse/1, TopicFilters). + +%%-------------------------------------------------------------------- +%% Mount/Unmount +%%-------------------------------------------------------------------- + +mount(Client = #{mountpoint := MountPoint}, TopicOrMsg) -> + emqx_mountpoint:mount( + emqx_mountpoint:replvar(MountPoint, Client), TopicOrMsg). + +unmount(Client = #{mountpoint := MountPoint}, TopicOrMsg) -> + emqx_mountpoint:unmount( + emqx_mountpoint:replvar(MountPoint, Client), TopicOrMsg). + +%%-------------------------------------------------------------------- +%% Pipeline +%%-------------------------------------------------------------------- + +pipeline([], Packet, Channel) -> + {ok, 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, ReasonCode} -> + {error, ReasonCode, Channel}; + {error, ReasonCode, NChannel} -> + {error, ReasonCode, NChannel} + end. %%-------------------------------------------------------------------- %% Helper functions +%%-------------------------------------------------------------------- --compile({inline, - [ reply/3 - , keep_state/1 - , next_event/2 - , shutdown/2 - , stop/2 - ]}). +set_property(Name, Value, ?NO_PROPS) -> + #{Name => Value}; +set_property(Name, Value, Props) -> + Props#{Name => Value}. -reply(From, Reply, State) -> - {keep_state, State, [{reply, From, Reply}]}. +get_property(_Name, undefined, Default) -> + Default; +get_property(Name, Props, Default) -> + maps:get(Name, Props, Default). -keep_state(State) -> - {keep_state, State}. +sp(true) -> 1; +sp(false) -> 0. -next_event(Type, Content) -> - {next_event, Type, Content}. - -shutdown(Reason, State) -> - stop({shutdown, Reason}, State). - -stop(Reason, State) -> - {stop, Reason, State}. +flag(true) -> 1; +flag(false) -> 0. diff --git a/src/emqx_cm.erl b/src/emqx_cm.erl index a4800e839..43831eea5 100644 --- a/src/emqx_cm.erl +++ b/src/emqx_cm.erl @@ -44,7 +44,7 @@ -export([ open_session/3 , discard_session/1 - , resume_session/1 + , takeover_session/1 ]). -export([ lookup_channels/1 @@ -179,35 +179,47 @@ open_session(true, Client = #{client_id := ClientId}, Options) -> open_session(false, Client = #{client_id := ClientId}, Options) -> ResumeStart = fun(_) -> - case resume_session(ClientId) of - {ok, Session} -> - {ok, Session, true}; + case takeover_session(ClientId) of + {ok, ConnMod, ChanPid, Session} -> + {ok, NSession} = emqx_session:resume(ClientId, Session), + {ok, Pendings} = ConnMod:takeover(ChanPid, 'end'), + io:format("Pending Delivers: ~p~n", [Pendings]), + {ok, NSession, true}; {error, not_found} -> {ok, emqx_session:init(false, Client, Options), false} end end, emqx_cm_locker:trans(ClientId, ResumeStart). -%% @doc Try to resume a session. --spec(resume_session(emqx_types:client_id()) +%% @doc Try to takeover a session. +-spec(takeover_session(emqx_types:client_id()) -> {ok, emqx_session:session()} | {error, Reason :: term()}). -resume_session(ClientId) -> +takeover_session(ClientId) -> case lookup_channels(ClientId) of [] -> {error, not_found}; - [_ChanPid] -> - ok; - % emqx_channel:resume(ChanPid); + [ChanPid] -> + takeover_session(ClientId, ChanPid); ChanPids -> - [_ChanPid|StalePids] = lists:reverse(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) - ok + lists:foreach(fun(StalePid) -> + catch discard_session(ClientId, StalePid) end, StalePids), - % emqx_channel:resume(ChanPid) - ok + takeover_session(ClientId, ChanPid) end. +takeover_session(ClientId, ChanPid) when node(ChanPid) == node() -> + case get_chan_attrs(ClientId, ChanPid) of + #{client := #{conn_mod := ConnMod}} -> + {ok, Session} = ConnMod:takeover(ChanPid, 'begin'), + {ok, ConnMod, ChanPid, Session}; + undefined -> + {error, not_found} + end; + +takeover_session(ClientId, ChanPid) -> + rpc_call(node(ChanPid), takeover_session, [ClientId, ChanPid]). + %% @doc Discard all the sessions identified by the ClientId. -spec(discard_session(emqx_types:client_id()) -> ok). discard_session(ClientId) when is_binary(ClientId) -> @@ -216,15 +228,25 @@ discard_session(ClientId) when is_binary(ClientId) -> ChanPids -> lists:foreach( fun(ChanPid) -> - try ok - % emqx_channel:discard(ChanPid) + try + discard_session(ClientId, ChanPid) catch _:Error:_Stk -> - ?LOG(warning, "[SM] Failed to discard ~p: ~p", [ChanPid, Error]) + ?LOG(error, "[SM] Failed to discard ~p: ~p", [ChanPid, Error]) end end, ChanPids) end. +discard_session(ClientId, ChanPid) when node(ChanPid) == node() -> + case get_chan_attrs(ClientId, ChanPid) of + #{conn_mod := ConnMod} -> + ConnMod:discard(ChanPid); + undefined -> ok + end; + +discard_session(ClientId, ChanPid) -> + rpc_call(node(ChanPid), discard_session, [ClientId, ChanPid]). + %% @doc Is clean start? % is_clean_start(#{clean_start := false}) -> false; % is_clean_start(_Attrs) -> true. @@ -314,8 +336,7 @@ code_change(_OldVsn, State, _Extra) -> %%-------------------------------------------------------------------- clean_down({ChanPid, ClientId}) -> - Chan = {ClientId, ChanPid}, - do_unregister_channel(Chan). + do_unregister_channel({ClientId, ChanPid}). stats_fun() -> lists:foreach(fun update_stats/1, ?CHAN_STATS). diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl new file mode 100644 index 000000000..c525a1fc7 --- /dev/null +++ b/src/emqx_connection.erl @@ -0,0 +1,632 @@ +%%-------------------------------------------------------------------- +%% 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("[Connection]"). + +-export([start_link/3]). + +%% APIs +-export([ info/1 + , attrs/1 + , stats/1 + ]). + +%% For Debug +-export([state/1]). + +-export([ kick/1 + , discard/1 + , takeover/2 + ]). + +%% state callbacks +-export([ idle/3 + , connected/3 + , disconnected/3 + , takeovering/3 + ]). + +%% gen_statem callbacks +-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(), + serialize :: fun((emqx_types:packet()) -> iodata()), + chan_state :: emqx_channel:channel(), + keepalive :: maybe(emqx_keepalive:keepalive()) + }). + +-type(state() :: #state{}). + +-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 +%%-------------------------------------------------------------------- + +%% @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, + chan_state = ChanState}) -> + ConnInfo = #{socktype => Transport:type(Socket), + peername => Peername, + sockname => Sockname, + conn_state => ConnState, + active_n => ActiveN, + rate_limit => limit_info(RateLimit), + pub_limit => limit_info(PubLimit) + }, + maps:merge(ConnInfo, emqx_channel:info(ChanState)). + +limit_info(undefined) -> + undefined; +limit_info(Limit) -> + esockd_rate_limit:info(Limit). + +%% @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, + chan_state = ChanState}) -> + ConnAttrs = #{socktype => Transport:type(Socket), + peername => Peername, + sockname => Sockname + }, + maps:merge(ConnAttrs, emqx_channel:attrs(ChanState)). + +%% @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, + chan_state = ChanState}) -> + SockStats = case Transport:getstat(Socket, ?SOCK_STATS) of + {ok, Ss} -> Ss; + {error, _} -> [] + end, + ChanStats = [{Name, emqx_pd:get_counter(Name)} || Name <- ?CHAN_STATS], + SessStats = emqx_session:stats(emqx_channel:info(session, ChanState)), + lists:append([SockStats, ChanStats, SessStats, emqx_misc:proc_stats()]). + +state(CPid) -> + call(CPid, get_state). + +-spec(kick(pid()) -> ok). +kick(CPid) -> + call(CPid, kick). + +-spec(discard(pid()) -> ok). +discard(CPid) -> + gen_statem:cast(CPid, discard). + +%% TODO: +-spec(takeover(pid(), 'begin'|'end') -> {ok, Result :: term()}). +takeover(CPid, Phase) -> + gen_statem:call(CPid, {takeover, Phase}). + +%% @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), + 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), + IdleTimout = emqx_zone:get_env(Zone, idle_timeout, 30000), + State = #state{transport = Transport, + socket = Socket, + peername = Peername, + sockname = Sockname, + conn_state = running, + active_n = ActiveN, + rate_limit = RateLimit, + pub_limit = PubLimit, + parse_state = ParseState, + chan_state = ChanState + }, + 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( + #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{chan_state = ChanState}) -> + ClientId = emqx_channel:info(client_id, ChanState), + ok = emqx_cm:register_channel(ClientId), + ok = emqx_cm:set_chan_attrs(ClientId, info(State)), + %% Ensure keepalive after connected successfully. + Interval = emqx_channel:info(keepalive, ChanState), + case ensure_keepalive(Interval, State) of + ignore -> keep_state(State); + {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{chan_state = ChanState}) -> + case emqx_channel:handle_out(Deliver, 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); + {stop, Reason, NChanState} -> + stop(Reason, State#state{chan_state = NChanState}) + 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). + +%%-------------------------------------------------------------------- +%% Takeovering State + +takeovering(enter, _PreState, State) -> + {keep_state, State}; + +takeovering(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}, get_state, State) -> + reply(From, State, State); + +handle({call, From}, kick, State) -> + ok = gen_statem:reply(From, ok), + shutdown(kicked, State); + +handle({call, From}, {takeover, 'begin'}, State = #state{chan_state = ChanState}) -> + {ok, Session, NChanState} = emqx_channel:takeover('begin', ChanState), + ok = gen_statem:reply(From, {ok, Session}), + {next_state, takeovering, State#state{chan_state = NChanState}}; + +handle({call, From}, {takeover, 'end'}, State = #state{chan_state = ChanState}) -> + {ok, Delivers, NChanState} = emqx_channel:takeover('end', ChanState), + ok = gen_statem:reply(From, {ok, Delivers}), + shutdown(takeovered, State#state{chan_state = NChanState}); + +handle({call, From}, Req, State = #state{chan_state = ChanState}) -> + case emqx_channel:handle_call(Req, ChanState) of + {ok, Reply, NChanState} -> + reply(From, Reply, State#state{chan_state = NChanState}); + {stop, Reason, Reply, NChanState} -> + ok = gen_statem:reply(From, Reply), + stop(Reason, State#state{chan_state = NChanState}) + end; + +%% Handle cast +handle(cast, Msg, State = #state{chan_state = ChanState}) -> + case emqx_channel:handle_cast(Msg, ChanState) of + {ok, NChanState} -> + keep_state(State#state{chan_state = NChanState}); + {stop, Reason, NChanState} -> + stop(Reason, State#state{chan_state = NChanState}) + end; + +%% Handle incoming data +handle(info, {Inet, _Sock, Data}, State = #state{chan_state = ChanState}) + 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), + NChanState = emqx_channel:gc(1, Oct, ChanState), + process_incoming(Data, State#state{chan_state = NChanState}); + +handle(info, {Error, _Sock, Reason}, State) + when Error == tcp_error; Error == ssl_error -> + shutdown(Reason, State); + +handle(info, {Closed, _Sock}, State = #state{chan_state = ChanState}) + when Closed == tcp_closed; Closed == ssl_closed -> + case emqx_channel:info(session, ChanState) of + undefined -> shutdown(closed, State); + Session -> + case emqx_session:info(clean_start, Session) of + true -> shutdown(closed, State); + false -> {next_state, disconnected, State} + end + end; + +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(State); + +handle(info, {inet_reply, _Sock, {error, Reason}}, State) -> + shutdown(Reason, State); + +handle(info, {timeout, TRef, Msg}, State = #state{chan_state = ChanState}) + when is_reference(TRef) -> + case emqx_channel:timeout(TRef, Msg, ChanState) of + {ok, NChanState} -> + keep_state(State#state{chan_state = NChanState}); + {ok, Packets, NChanState} -> + handle_outgoing(Packets, fun keep_state/1, + State#state{chan_state = NChanState}); + {stop, Reason, NChanState} -> + stop(Reason, State#state{chan_state = NChanState}) + end; + +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 = #state{chan_state = ChanState}) -> + case emqx_channel:handle_info(Info, ChanState) of + {ok, NChanState} -> + keep_state(State#state{chan_state = NChanState}); + {stop, Reason, NChanState} -> + stop(Reason, State#state{chan_state = NChanState}) + end. + +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), + KeepAlive =/= undefined + andalso emqx_keepalive:cancel(KeepAlive), + emqx_channel:terminate(Reason, ChanState). + +%%-------------------------------------------------------------------- +%% 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 <- Packets]. + +%%-------------------------------------------------------------------- +%% 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, OutPackets, NChanState} -> + handle_outgoing(OutPackets, SuccFun, State#state{chan_state = NChanState}); + {stop, Reason, NChanState} -> + stop(Reason, State#state{chan_state = NChanState}); + {stop, Reason, OutPacket, NChanState} -> + Shutdown = fun(NewSt) -> shutdown(Reason, NewSt) end, + handle_outgoing(OutPacket, Shutdown, State#state{chan_state = NChanState}) + end. + +%%-------------------------------------------------------------------- +%% Handle outgoing packets + +handle_outgoing(Packets, SuccFun, State = #state{serialize = Serialize}) + when is_list(Packets) -> + send(lists:map(Serialize, Packets), SuccFun, State); + +handle_outgoing(Packet, SuccFun, State = #state{serialize = Serialize}) -> + send(Serialize(Packet), SuccFun, 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. + +%%-------------------------------------------------------------------- +%% 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(State); + {error, Reason} -> + shutdown(Reason, State) + end. + +%% TODO: maybe_gc(1, Oct, State) + +%%-------------------------------------------------------------------- +%% Ensure keepalive + +ensure_keepalive(0, _State) -> + ignore; +ensure_keepalive(Interval, #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:info(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 + +-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. + +inc_outgoing_stats(Type) -> + emqx_pd:update_counter(send_pkt, 1), + (Type == ?PUBLISH) + andalso emqx_pd:update_counter(send_msg, 1). + +%%-------------------------------------------------------------------- +%% Helper functions + +-compile({inline, + [ reply/3 + , keep_state/1 + , next_event/2 + , shutdown/2 + , stop/2 + ]}). + +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_listeners.erl b/src/emqx_listeners.erl index b39873879..802179065 100644 --- a/src/emqx_listeners.erl +++ b/src/emqx_listeners.erl @@ -79,7 +79,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). @@ -88,7 +88,7 @@ mqtt_path(Options) -> proplists:get_value(mqtt_path, Options, "/mqtt"). ws_opts(Options) -> - WsPaths = [{mqtt_path(Options), emqx_ws_channel, Options}], + WsPaths = [{mqtt_path(Options), emqx_ws_connection, Options}], Dispatch = cowboy_router:compile([{'_', WsPaths}]), ProxyProto = proplists:get_value(proxy_protocol, Options, false), #{env => #{dispatch => Dispatch}, proxy_header => ProxyProto}. diff --git a/src/emqx_misc.erl b/src/emqx_misc.erl index 42e88d850..007f444f4 100644 --- a/src/emqx_misc.erl +++ b/src/emqx_misc.erl @@ -25,10 +25,6 @@ , proc_stats/1 ]). --export([ init_proc_mng_policy/1 - , conn_proc_mng_policy/1 - ]). - -export([ drain_deliver/1 , drain_down/1 ]). @@ -82,56 +78,6 @@ proc_stats(Pid) -> [{mailbox_len, Len}|Stats] end. --define(DISABLED, 0). - -init_proc_mng_policy(undefined) -> ok; -init_proc_mng_policy(Zone) -> - #{max_heap_size := MaxHeapSizeInBytes} - = ShutdownPolicy - = emqx_zone:get_env(Zone, force_shutdown_policy), - MaxHeapSize = MaxHeapSizeInBytes div erlang:system_info(wordsize), - _ = erlang:process_flag(max_heap_size, MaxHeapSize), % zero is discarded - erlang:put(force_shutdown_policy, ShutdownPolicy), - ok. - -%% @doc Check self() process status against connection/session process management policy, -%% return `continue | hibernate | {shutdown, Reason}' accordingly. -%% `continue': There is nothing out of the ordinary. -%% `hibernate': Nothing to process in my mailbox, and since this check is triggered -%% by a timer, we assume it is a fat chance to continue idel, hence hibernate. -%% `shutdown': Some numbers (message queue length hit the limit), -%% hence shutdown for greater good (system stability). --spec(conn_proc_mng_policy(#{message_queue_len => integer()} | false) -> - continue | hibernate | {shutdown, _}). -conn_proc_mng_policy(#{message_queue_len := MaxMsgQueueLen}) -> - Qlength = proc_info(message_queue_len), - Checks = - [{fun() -> is_message_queue_too_long(Qlength, MaxMsgQueueLen) end, - {shutdown, message_queue_too_long}}, - {fun() -> Qlength > 0 end, continue}, - {fun() -> true end, hibernate} - ], - check(Checks); -conn_proc_mng_policy(_) -> - %% disable by default - conn_proc_mng_policy(#{message_queue_len => 0}). - -check([{Pred, Result} | Rest]) -> - case Pred() of - true -> Result; - false -> check(Rest) - end. - -is_message_queue_too_long(Qlength, Max) -> - is_enabled(Max) andalso Qlength > Max. - -is_enabled(Max) -> - is_integer(Max) andalso Max > ?DISABLED. - -proc_info(Key) -> - {Key, Value} = erlang:process_info(self(), Key), - Value. - %% @doc Drain delivers from the channel's mailbox. drain_deliver(Acc) -> receive diff --git a/src/emqx_mod_presence.erl b/src/emqx_mod_presence.erl index 97f7a9929..30f0ad334 100644 --- a/src/emqx_mod_presence.erl +++ b/src/emqx_mod_presence.erl @@ -37,9 +37,10 @@ %% APIs %%-------------------------------------------------------------------- -load(Env) -> - emqx_hooks:add('client.connected', {?MODULE, on_client_connected, [Env]}), - emqx_hooks:add('client.disconnected', {?MODULE, on_client_disconnected, [Env]}). +load(_Env) -> + ok. + %% emqx_hooks:add('client.connected', {?MODULE, on_client_connected, [Env]}), + %% emqx_hooks:add('client.disconnected', {?MODULE, on_client_disconnected, [Env]}). on_client_connected(#{client_id := ClientId, username := Username, diff --git a/src/emqx_oom.erl b/src/emqx_oom.erl new file mode 100644 index 000000000..5e14547f0 --- /dev/null +++ b/src/emqx_oom.erl @@ -0,0 +1,102 @@ +%%-------------------------------------------------------------------- +%% 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. +%%-------------------------------------------------------------------- + +%%-------------------------------------------------------------------- +%% @doc OOM (Out Of Memory) monitor for the channel process. +%% @end +%%-------------------------------------------------------------------- + +-module(emqx_oom). + +-include("types.hrl"). + +-export([ init/1 + , check/1 + , info/1 + ]). + +-export_type([oom_policy/0]). + +-type(opts() :: #{message_queue_len => non_neg_integer(), + max_heap_size => non_neg_integer() + }). + +-opaque(oom_policy() :: {oom_policy, opts()}). + +-type(reason() :: message_queue_too_long|proc_heap_too_large). + +-define(DISABLED, 0). + +%% @doc Init the OOM policy. +-spec(init(maybe(opts())) -> oom_policy()). +init(undefined) -> undefined; +init(#{message_queue_len := MaxQLen, + max_heap_size := MaxHeapSizeInBytes}) -> + MaxHeapSize = MaxHeapSizeInBytes div erlang:system_info(wordsize), + %% If set to zero, the limit is disabled. + _ = erlang:process_flag(max_heap_size, #{size => MaxHeapSize, + kill => false, + error_logger => true + }), + {oom_policy, #{message_queue_len => MaxQLen, + max_heap_size => MaxHeapSize + }}. + +%% @doc Check self() process status against channel process management policy, +%% return `ok | {shutdown, Reason}' accordingly. +%% `ok': There is nothing out of the ordinary. +%% `shutdown': Some numbers (message queue length hit the limit), +%% hence shutdown for greater good (system stability). +-spec(check(maybe(oom_policy())) -> ok | {shutdown, reason()}). +check(undefined) -> ok; +check({oom_policy, #{message_queue_len := MaxQLen, + max_heap_size := MaxHeapSize}}) -> + Qlength = proc_info(message_queue_len), + HeapSize = proc_info(total_heap_size), + do_check([{fun() -> is_exceeded(Qlength, MaxQLen) end, + {shutdown, message_queue_too_long}}, + {fun() -> is_exceeded(HeapSize, MaxHeapSize) end, + {shutdown, proc_heap_too_large}}]). + +do_check([]) -> + ok; +do_check([{Pred, Result} | Rest]) -> + case Pred() of + true -> Result; + false -> do_check(Rest) + end. + +-spec(info(maybe(oom_policy())) -> maybe(opts())). +info(undefined) -> undefined; +info({oom_policy, Opts}) -> + Opts. + +-compile({inline, + [ is_exceeded/2 + , is_enabled/1 + , proc_info/1 + ]}). + +is_exceeded(Val, Max) -> + is_enabled(Max) andalso Val > Max. + +is_enabled(Max) -> + is_integer(Max) andalso Max > ?DISABLED. + +proc_info(Key) -> + {Key, Value} = erlang:process_info(self(), Key), + Value. + diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl deleted file mode 100644 index 3fa92ac70..000000000 --- a/src/emqx_protocol.erl +++ /dev/null @@ -1,924 +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 - , info/2 - , attrs/1 - , caps/1 - ]). - -%% for tests --export([set/3]). - --export([ init/2 - , handle_in/2 - , handle_req/2 - , handle_deliver/2 - , handle_out/2 - , handle_timeout/3 - , terminate/2 - ]). - --import(emqx_access_control, - [ authenticate/1 - , check_acl/3 - ]). - --export_type([proto_state/0]). - --record(protocol, { - client :: emqx_types:client(), - session :: emqx_session:session(), - proto_name :: binary(), - proto_ver :: emqx_types:ver(), - keepalive :: non_neg_integer(), - will_msg :: emqx_types:message(), - topic_aliases :: maybe(map()), - alias_maximum :: maybe(map()), - ack_props :: maybe(emqx_types:properties()) %% Tmp props - }). - --opaque(proto_state() :: #protocol{}). - --define(NO_PROPS, undefined). - --spec(info(proto_state()) -> emqx_types:infos()). -info(#protocol{client = Client, - session = Session, - proto_name = ProtoName, - proto_ver = ProtoVer, - keepalive = Keepalive, - will_msg = WillMsg, - topic_aliases = Aliases}) -> - #{client => Client, - session => session_info(Session), - proto_name => ProtoName, - proto_ver => ProtoVer, - keepalive => Keepalive, - will_msg => WillMsg, - topic_aliases => Aliases - }. - --spec(info(atom(), proto_state()) -> term()). -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(proto_name, #protocol{proto_name = ProtoName}) -> - ProtoName; -info(proto_ver, #protocol{proto_ver = ProtoVer}) -> - ProtoVer; -info(keepalive, #protocol{keepalive = Keepalive}) -> - Keepalive; -info(will_msg, #protocol{will_msg = WillMsg}) -> - WillMsg; -info(topic_aliases, #protocol{topic_aliases = Aliases}) -> - Aliases. - -%% For tests -set(client, Client, PState) -> - PState#protocol{client = Client}; -set(session, Session, PState) -> - PState#protocol{session = Session}. - -attrs(#protocol{client = Client, - session = Session, - proto_name = ProtoName, - proto_ver = ProtoVer, - keepalive = Keepalive}) -> - #{client => Client, - session => emqx_session:attrs(Session), - proto_name => ProtoName, - proto_ver => ProtoVer, - keepalive => Keepalive - }. - -caps(#protocol{client = #{zone := Zone}}) -> - emqx_mqtt_caps:get_caps(Zone). - - --spec(init(emqx_types:conn(), proplists:proplist()) -> proto_state()). -init(ConnInfo, Options) -> - Zone = proplists:get_value(zone, Options), - Peercert = maps:get(peercert, ConnInfo, undefined), - Username = case peer_cert_as_username(Options) of - cn -> esockd_peercert:common_name(Peercert); - dn -> esockd_peercert:subject(Peercert); - crt -> Peercert; - _ -> undefined - end, - MountPoint = emqx_zone:get_env(Zone, mountpoint), - Client = maps:merge(#{zone => Zone, - username => Username, - mountpoint => MountPoint, - is_bridge => false, - is_superuser => false - }, ConnInfo), - #protocol{client = Client, - proto_name = <<"MQTT">>, - proto_ver = ?MQTT_PROTO_V4 - }. - -peer_cert_as_username(Options) -> - proplists:get_value(peer_cert_as_username, Options). - -%%-------------------------------------------------------------------- -%% Handle incoming packet -%%-------------------------------------------------------------------- - --spec(handle_in(emqx_types:packet(), proto_state()) - -> {ok, proto_state()} - | {ok, emqx_types:packet(), proto_state()} - | {ok, list(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, - keepalive = Keepalive, - client_id = ClientId - } = ConnPkt), PState) -> - PState1 = PState#protocol{proto_name = ProtoName, - proto_ver = ProtoVer, - keepalive = Keepalive - }, - ok = emqx_logger:set_metadata_client_id(ClientId), - case pipeline([fun validate_in/2, - fun process_props/2, - fun check_connect/2, - fun enrich_client/2, - fun auth_connect/2], ConnPkt, PState1) of - {ok, NConnPkt, NPState} -> - process_connect(NConnPkt, maybe_assign_clientid(NPState)); - {error, ReasonCode, NPState} -> - handle_out({disconnect, ReasonCode}, NPState) - end; - -handle_in(Packet = ?PUBLISH_PACKET(QoS, Topic, PacketId), PState) -> - case pipeline([fun validate_in/2, - fun process_alias/2, - fun check_publish/2], Packet, PState) of - {ok, NPacket, NPState} -> - process_publish(NPacket, NPState); - {error, ReasonCode, NPState} -> - ?LOG(warning, "Cannot publish message to ~s due to ~s", - [Topic, emqx_reason_codes:text(ReasonCode)]), - puback(QoS, PacketId, ReasonCode, NPState) - end; - -handle_in(?PUBACK_PACKET(PacketId, _ReasonCode), PState = #protocol{session = Session}) -> - case emqx_session:puback(PacketId, Session) of - {ok, Publishes, NSession} -> - handle_out({publish, Publishes}, PState#protocol{session = NSession}); - {ok, NSession} -> - {ok, PState#protocol{session = NSession}}; - {error, _NotFound} -> - {ok, PState} - end; - -handle_in(?PUBREC_PACKET(PacketId, _ReasonCode), PState = #protocol{session = Session}) -> - case emqx_session:pubrec(PacketId, Session) of - {ok, NSession} -> - handle_out({pubrel, PacketId}, PState#protocol{session = NSession}); - {error, ReasonCode1} -> - handle_out({pubrel, PacketId, ReasonCode1}, PState) - end; - -handle_in(?PUBREL_PACKET(PacketId, _ReasonCode), PState = #protocol{session = Session}) -> - case emqx_session:pubrel(PacketId, Session) of - {ok, NSession} -> - handle_out({pubcomp, PacketId}, PState#protocol{session = NSession}); - {error, ReasonCode1} -> - handle_out({pubcomp, PacketId, ReasonCode1}, PState) - end; - -handle_in(?PUBCOMP_PACKET(PacketId, _ReasonCode), PState = #protocol{session = Session}) -> - case emqx_session:pubcomp(PacketId, Session) of - {ok, Publishes, NSession} -> - handle_out({publish, Publishes}, PState#protocol{session = NSession}); - {ok, NSession} -> - {ok, PState#protocol{session = NSession}}; - {error, _NotFound} -> - {ok, PState} - end; - -handle_in(Packet = ?SUBSCRIBE_PACKET(PacketId, Properties, TopicFilters), - PState = #protocol{client = Client}) -> - case validate_in(Packet, PState) of - ok -> TopicFilters1 = [emqx_topic:parse(TopicFilter, SubOpts) - || {TopicFilter, SubOpts} <- TopicFilters], - TopicFilters2 = emqx_hooks:run_fold('client.subscribe', - [Client, Properties], - TopicFilters1), - TopicFilters3 = enrich_subid(Properties, TopicFilters2), - {ReasonCodes, NPState} = process_subscribe(TopicFilters3, PState), - handle_out({suback, PacketId, ReasonCodes}, NPState); - {error, ReasonCode} -> - handle_out({disconnect, ReasonCode}, PState) - end; - -handle_in(Packet = ?UNSUBSCRIBE_PACKET(PacketId, Properties, TopicFilters), - PState = #protocol{client = Client}) -> - case validate_in(Packet, PState) of - ok -> TopicFilters1 = lists:map(fun emqx_topic:parse/1, TopicFilters), - TopicFilters2 = emqx_hooks:run_fold('client.unsubscribe', - [Client, Properties], - TopicFilters1), - {ReasonCodes, NPState} = process_unsubscribe(TopicFilters2, PState), - handle_out({unsuback, PacketId, ReasonCodes}, NPState); - {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}) -> - {stop, {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 internal request -%%-------------------------------------------------------------------- - --spec(handle_req(Req:: term(), proto_state()) - -> {ok, Result :: term(), proto_state()} | - {error, Reason :: term(), proto_state()}). -handle_req({subscribe, TopicFilters}, PState = #protocol{client = Client}) -> - TopicFilters1 = emqx_hooks:run_fold('client.subscribe', - [Client, #{'Internal' => true}], - parse(subscribe, TopicFilters)), - {ReasonCodes, NPState} = process_subscribe(TopicFilters1, PState), - {ok, ReasonCodes, NPState}; - -handle_req({unsubscribe, TopicFilters}, PState = #protocol{client = Client}) -> - TopicFilters1 = emqx_hooks:run_fold('client.unsubscribe', - [Client, #{'Internal' => true}], - parse(unsubscribe, TopicFilters)), - {ReasonCodes, NPState} = process_unsubscribe(TopicFilters1, PState), - {ok, ReasonCodes, NPState}; - -handle_req(Req, PState) -> - ?LOG(error, "Unexpected request: ~p~n", [Req]), - {ok, ignored, PState}. - -%%-------------------------------------------------------------------- -%% Handle delivers -%%-------------------------------------------------------------------- - -handle_deliver(Delivers, PState = #protocol{session = Session}) - when is_list(Delivers) -> - case emqx_session:deliver(Delivers, Session) of - {ok, Publishes, NSession} -> - handle_out({publish, Publishes}, 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 = #{zone := Zone}, - ack_props = AckProps, - alias_maximum = AliasMaximum}) -> - ok = emqx_hooks:run('client.connected', [Client, ?RC_SUCCESS, attrs(PState)]), - #{max_packet_size := MaxPktSize, - max_qos_allowed := MaxQoS, - retain_available := Retain, - max_topic_alias := MaxAlias, - shared_subscription := Shared, - 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. - AckProps1 = if AckProps == undefined -> #{}; true -> AckProps end, - AckProps2 = AckProps1#{'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), - 'Maximum-QoS' => MaxQoS - }, - AckProps3 = case emqx_zone:get_env(Zone, server_keepalive) of - undefined -> AckProps2; - Keepalive -> AckProps2#{'Server-Keep-Alive' => Keepalive} - end, - AliasMaximum1 = set_property(inbound, MaxAlias, AliasMaximum), - PState1 = PState#protocol{alias_maximum = AliasMaximum1, - ack_props = undefined - }, - {ok, ?CONNACK_PACKET(?RC_SUCCESS, SP, AckProps3), PState1}; - -handle_out({connack, ReasonCode}, PState = #protocol{client = Client, - proto_ver = ProtoVer}) -> - ok = emqx_hooks:run('client.connected', [Client, ReasonCode, attrs(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, Publishes}, PState) -> - Packets = [element(2, handle_out(Publish, PState)) || Publish <- Publishes], - {ok, Packets, PState}; - -handle_out({publish, PacketId, Msg}, PState = #protocol{client = Client}) -> - Msg1 = emqx_hooks:run_fold('message.deliver', [Client], - emqx_message:update_expiry(Msg)), - Packet = emqx_packet:from_message(PacketId, unmount(Client, Msg1)), - {ok, Packet, PState}; - -%% TODO: How to handle the err? -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({disconnect, ReasonCode}, PState = #protocol{proto_ver = ?MQTT_PROTO_V5}) -> - Reason = emqx_reason_codes:name(ReasonCode), - {error, Reason, ?DISCONNECT_PACKET(ReasonCode), PState}; - -handle_out({disconnect, ReasonCode}, PState = #protocol{proto_ver = ProtoVer}) -> - {error, emqx_reason_codes:name(ReasonCode, ProtoVer), PState}; - -handle_out(Packet, PState) -> - ?LOG(error, "Unexpected out:~p", [Packet]), - {ok, PState}. - -%%-------------------------------------------------------------------- -%% 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} -> - handle_out({publish, Publishes}, PState#protocol{session = NSession}) - end. - -terminate(normal, #protocol{client = Client}) -> - ok = emqx_hooks:run('client.disconnected', [Client, normal]); -terminate(Reason, #protocol{client = Client, will_msg = WillMsg}) -> - ok = emqx_hooks:run('client.disconnected', [Client, Reason]), - publish_will_msg(WillMsg). - -publish_will_msg(undefined) -> - ok; -publish_will_msg(Msg) -> - emqx_broker:publish(Msg). - -%%-------------------------------------------------------------------- -%% 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 -%%-------------------------------------------------------------------- - -process_props(#mqtt_packet_connect{ - properties = #{'Topic-Alias-Maximum' := Max} - }, - PState = #protocol{alias_maximum = AliasMaximum}) -> - NAliasMaximum = if AliasMaximum == undefined -> - #{outbound => Max}; - true -> AliasMaximum#{outbound => Max} - end, - {ok, PState#protocol{alias_maximum = NAliasMaximum}}; - -process_props(Packet, PState) -> - {ok, Packet, PState}. - -%%-------------------------------------------------------------------- -%% Check Connect Packet -%%-------------------------------------------------------------------- - -check_connect(ConnPkt, PState) -> - case 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) of - ok -> {ok, PState}; - Error -> Error - end. - -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{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. - -%%TODO: check banned... -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 client -%%-------------------------------------------------------------------- - -enrich_client(#mqtt_packet_connect{client_id = ClientId, - username = Username, - is_bridge = IsBridge - }, - PState = #protocol{client = Client}) -> - Client1 = set_username(Username, Client#{client_id => ClientId, - is_bridge => IsBridge - }), - {ok, PState#protocol{client = maybe_username_as_clientid(Client1)}}. - -%% Username maybe not undefined if peer_cert_as_username -set_username(Username, Client = #{username := undefined}) -> - Client#{username => Username}; -set_username(_Username, Client) -> Client. - -maybe_username_as_clientid(Client = #{username := undefined}) -> - Client; -maybe_username_as_clientid(Client = #{zone := Zone, - username := Username}) -> - case emqx_zone:get_env(Zone, use_username_as_clientid, false) of - true -> Client#{client_id => Username}; - false -> Client - end. - -%%-------------------------------------------------------------------- -%% Auth Connect -%%-------------------------------------------------------------------- - -auth_connect(#mqtt_packet_connect{client_id = ClientId, - username = Username, - password = Password}, - PState = #protocol{client = Client}) -> - case 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 ~0p", - [ClientId, Username, Reason]), - {error, emqx_reason_codes:connack_error(Reason)} - end. - -%%-------------------------------------------------------------------- -%% Assign a random clientId -%%-------------------------------------------------------------------- - -maybe_assign_clientid(PState = #protocol{client = Client = #{client_id := <<>>}, - ack_props = AckProps}) -> - ClientId = emqx_guid:to_base62(emqx_guid:gen()), - Client1 = Client#{client_id => ClientId}, - AckProps1 = set_property('Assigned-Client-Identifier', ClientId, AckProps), - PState#protocol{client = Client1, ack_props = AckProps1}; -maybe_assign_clientid(PState) -> PState. - -%%-------------------------------------------------------------------- -%% Process Connect -%%-------------------------------------------------------------------- - -process_connect(ConnPkt, PState) -> - case open_session(ConnPkt, PState) of - {ok, Session, SP} -> - WillMsg = emqx_packet:will_msg(ConnPkt), - NPState = PState#protocol{session = Session, - will_msg = WillMsg - }, - handle_out({connack, ?RC_SUCCESS, sp(SP)}, NPState); - {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}, - #protocol{client = Client = #{zone := Zone}}) -> - MaxInflight = get_property('Receive-Maximum', ConnProps, - emqx_zone:get_env(Zone, max_inflight, 65535)), - Interval = get_property('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 - }). - -%%-------------------------------------------------------------------- -%% Process publish message: Client -> Broker -%%-------------------------------------------------------------------- - -process_alias(Packet = #mqtt_packet{ - variable = #mqtt_packet_publish{topic_name = <<>>, - properties = #{'Topic-Alias' := AliasId} - } = Publish - }, PState = #protocol{topic_aliases = Aliases}) -> - case find_alias(AliasId, Aliases) of - {ok, Topic} -> - {ok, Packet#mqtt_packet{ - variable = Publish#mqtt_packet_publish{ - topic_name = Topic}}, PState}; - false -> {error, ?RC_TOPIC_ALIAS_INVALID} - end; - -process_alias(#mqtt_packet{ - variable = #mqtt_packet_publish{topic_name = Topic, - properties = #{'Topic-Alias' := AliasId} - } - }, PState = #protocol{topic_aliases = Aliases}) -> - {ok, PState#protocol{topic_aliases = save_alias(AliasId, Topic, Aliases)}}; - -process_alias(_Packet, PState) -> - {ok, PState}. - -find_alias(_AliasId, undefined) -> - false; -find_alias(AliasId, Aliases) -> - maps:find(AliasId, Aliases). - -save_alias(AliasId, Topic, undefined) -> - #{AliasId => Topic}; -save_alias(AliasId, Topic, Aliases) -> - maps:put(AliasId, Topic, Aliases). - -%% Check Publish -check_publish(Packet, PState) -> - pipeline([fun check_pub_acl/2, - fun check_pub_alias/2, - fun check_pub_caps/2], Packet, PState). - -%% Check Pub ACL -check_pub_acl(#mqtt_packet{variable = #mqtt_packet_publish{topic_name = Topic}}, - #protocol{client = Client}) -> - case is_acl_enabled(Client) andalso check_acl(Client, publish, Topic) of - false -> ok; - allow -> ok; - deny -> {error, ?RC_NOT_AUTHORIZED} - end. - -%% 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 - false -> ok; - true -> {error, ?RC_TOPIC_ALIAS_INVALID} - 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}). - -%% Process Publish -process_publish(Packet = ?PUBLISH_PACKET(_QoS, _Topic, PacketId), - PState = #protocol{client = Client}) -> - Msg = emqx_packet:to_message(Client, Packet), - %%TODO: Improve later. - Msg1 = emqx_message:set_flag(dup, false, Msg), - process_publish(PacketId, mount(Client, Msg1), PState). - -process_publish(_PacketId, Msg = #message{qos = ?QOS_0}, PState) -> - _ = emqx_broker:publish(Msg), - {ok, PState}; - -process_publish(PacketId, Msg = #message{qos = ?QOS_1}, PState) -> - Deliveries = emqx_broker:publish(Msg), - ReasonCode = emqx_reason_codes:puback(Deliveries), - handle_out({puback, PacketId, ReasonCode}, PState); - -process_publish(PacketId, Msg = #message{qos = ?QOS_2}, - PState = #protocol{session = Session}) -> - case emqx_session:publish(PacketId, Msg, Session) of - {ok, Deliveries, NSession} -> - ReasonCode = emqx_reason_codes:puback(Deliveries), - handle_out({pubrec, PacketId, ReasonCode}, - PState#protocol{session = NSession}); - {error, ReasonCode} -> - handle_out({pubrec, PacketId, ReasonCode}, PState) - end. - -%%-------------------------------------------------------------------- -%% Puback -%%-------------------------------------------------------------------- - -puback(?QOS_0, _PacketId, ReasonCode, PState) -> - handle_out({puberr, ReasonCode}, PState); -puback(?QOS_1, PacketId, ReasonCode, PState) -> - handle_out({puback, PacketId, ReasonCode}, PState); -puback(?QOS_2, PacketId, ReasonCode, PState) -> - handle_out({pubrec, PacketId, ReasonCode}, PState). - -%%-------------------------------------------------------------------- -%% Process subscribe request -%%-------------------------------------------------------------------- - -process_subscribe(TopicFilters, PState) -> - process_subscribe(TopicFilters, [], PState). - -process_subscribe([], Acc, PState) -> - {lists:reverse(Acc), PState}; - -process_subscribe([{TopicFilter, SubOpts}|More], Acc, PState) -> - {RC, NPState} = do_subscribe(TopicFilter, SubOpts, PState), - process_subscribe(More, [RC|Acc], NPState). - -do_subscribe(TopicFilter, SubOpts = #{qos := QoS}, - PState = #protocol{client = Client, session = Session}) -> - case check_subscribe(TopicFilter, SubOpts, PState) of - ok -> TopicFilter1 = mount(Client, TopicFilter), - SubOpts1 = enrich_subopts(maps:merge(?DEFAULT_SUBOPTS, SubOpts), PState), - case emqx_session:subscribe(Client, TopicFilter1, SubOpts1, 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 Sub -check_subscribe(TopicFilter, SubOpts, PState) -> - case check_sub_acl(TopicFilter, PState) of - allow -> check_sub_caps(TopicFilter, SubOpts, PState); - deny -> {error, ?RC_NOT_AUTHORIZED} - end. - -%% Check Sub ACL -check_sub_acl(TopicFilter, #protocol{client = Client}) -> - case is_acl_enabled(Client) andalso - check_acl(Client, subscribe, TopicFilter) of - false -> allow; - Result -> Result - end. - -%% Check Sub Caps -check_sub_caps(TopicFilter, SubOpts, #protocol{client = #{zone := Zone}}) -> - emqx_mqtt_caps:check_sub(Zone, TopicFilter, SubOpts). - -%%-------------------------------------------------------------------- -%% Process unsubscribe request -%%-------------------------------------------------------------------- - -process_unsubscribe(TopicFilters, PState) -> - process_unsubscribe(TopicFilters, [], PState). - -process_unsubscribe([], Acc, PState) -> - {lists:reverse(Acc), PState}; - -process_unsubscribe([{TopicFilter, SubOpts}|More], Acc, PState) -> - {RC, PState1} = do_unsubscribe(TopicFilter, SubOpts, PState), - process_unsubscribe(More, [RC|Acc], PState1). - -do_unsubscribe(TopicFilter, _SubOpts, PState = #protocol{client = Client, - session = Session}) -> - case emqx_session:unsubscribe(Client, mount(Client, TopicFilter), Session) of - {ok, NSession} -> - {?RC_SUCCESS, PState#protocol{session = NSession}}; - {error, RC} -> {RC, PState} - end. - -%%-------------------------------------------------------------------- -%% Is ACL enabled? -%%-------------------------------------------------------------------- - -is_acl_enabled(#{zone := Zone, is_superuser := IsSuperuser}) -> - (not IsSuperuser) andalso emqx_zone:get_env(Zone, enable_acl, true). - -%%-------------------------------------------------------------------- -%% Parse topic filters -%%-------------------------------------------------------------------- - -parse(subscribe, TopicFilters) -> - [emqx_topic:parse(TopicFilter, SubOpts) || {TopicFilter, SubOpts} <- TopicFilters]; - -parse(unsubscribe, TopicFilters) -> - lists:map(fun emqx_topic:parse/1, TopicFilters). - -%%-------------------------------------------------------------------- -%% Mount/Unmount -%%-------------------------------------------------------------------- - -mount(Client = #{mountpoint := MountPoint}, TopicOrMsg) -> - emqx_mountpoint:mount(emqx_mountpoint:replvar(MountPoint, Client), TopicOrMsg). - -unmount(Client = #{mountpoint := MountPoint}, TopicOrMsg) -> - emqx_mountpoint:unmount(emqx_mountpoint:replvar(MountPoint, Client), TopicOrMsg). - -%%-------------------------------------------------------------------- -%% Pipeline -%%-------------------------------------------------------------------- - -pipeline([], Packet, PState) -> - {ok, 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, ReasonCode} -> - {error, ReasonCode, PState}; - {error, ReasonCode, NPState} -> - {error, ReasonCode, NPState} - end. - -%%-------------------------------------------------------------------- -%% Helper functions -%%-------------------------------------------------------------------- - -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). - -sp(true) -> 1; -sp(false) -> 0. - -flag(true) -> 1; -flag(false) -> 0. - -session_info(undefined) -> - undefined; -session_info(Session) -> - emqx_session:info(Session). diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 2530f3a42..fcc404a18 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -58,6 +58,10 @@ , stats/1 ]). +-export([ takeover/1 + , resume/2 + ]). + -export([ subscribe/4 , unsubscribe/3 ]). @@ -278,6 +282,25 @@ stats(#session{subscriptions = Subscriptions, {awaiting_rel, maps:size(AwaitingRel)}, {max_awaiting_rel, MaxAwaitingRel}]. +-spec(takeover(session()) -> ok). +takeover(#session{subscriptions = Subs}) -> + lists:foreach(fun({TopicFilter, _SubOpts}) -> + ok = emqx_broker:unsubscribe(TopicFilter) + end, maps:to_list(Subs)). + +-spec(resume(emqx_types:client_id(), session()) -> {ok, session()}). +resume(ClientId, Session = #session{subscriptions = Subs}) -> + ?LOG(info, "Session is resumed."), + %% 1. Subscribe again + ok = lists:foreach(fun({TopicFilter, SubOpts}) -> + ok = emqx_broker:subscribe(TopicFilter, ClientId, SubOpts) + end, maps:to_list(Subs)), + %% 2. Run hooks. + ok = emqx_hooks:run('session.resumed', [#{client_id => ClientId}, attrs(Session)]), + %% TODO: 3. Redeliver: Replay delivery and Dequeue pending messages + %% noreply(ensure_stats_timer(dequeue(retry_delivery(true, State1)))); + {ok, Session}. + %%-------------------------------------------------------------------- %% Client -> Broker: SUBSCRIBE %%-------------------------------------------------------------------- @@ -683,6 +706,6 @@ next_pkt_id(Session = #session{next_pkt_id = Id}) -> %% For Test case %%--------------------------------------------------------------------- - set_pkt_id(Session, PktId) -> Session#session{next_pkt_id = PktId}. + diff --git a/src/emqx_ws_channel.erl b/src/emqx_ws_connection.erl similarity index 60% rename from src/emqx_ws_channel.erl rename to src/emqx_ws_connection.erl index 3bc067525..2597ec7fe 100644 --- a/src/emqx_ws_channel.erl +++ b/src/emqx_ws_connection.erl @@ -14,21 +14,26 @@ %% 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("[WsChannel]"). +-logger_header("[WsConn]"). -export([ info/1 , attrs/1 , stats/1 ]). +-export([ kick/1 + , discard/1 + , takeover/2 + ]). + %% WebSocket callbacks -export([ init/2 , websocket_init/1 @@ -38,20 +43,15 @@ ]). -record(state, { - 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(), - 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(), - connected :: boolean(), - connected_at :: erlang:timestamp(), - reason :: term() + 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(), + chan_state :: emqx_channel:channel(), + keepalive :: maybe(emqx_keepalive:keepalive()), + pendings :: list(), + reason :: term() }). -type(state() :: #state{}). @@ -68,51 +68,47 @@ info(WSPid) when is_pid(WSPid) -> call(WSPid, info); info(#state{peername = Peername, sockname = Sockname, - proto_state = ProtoState, - gc_state = GCState, - stats_timer = StatsTimer, - idle_timeout = IdleTimeout, - connected = Connected, - connected_at = ConnectedAt}) -> - ChanInfo = #{socktype => websocket, + chan_state = ChanState + }) -> + ConnInfo = #{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 + conn_state => running }, - maps:merge(ChanInfo, emqx_protocol:info(ProtoState)). - -enable_stats(disabled) -> false; -enable_stats(_MaybeRef) -> true. + maps:merge(ConnInfo, emqx_channel:info(ChanState)). -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, - connected = Connected, - connected_at = ConnectedAt}) -> + chan_state = ChanState}) -> ConnAttrs = #{socktype => websocket, peername => Peername, - sockname => Sockname, - connected => Connected, - connected_at => ConnectedAt + sockname => Sockname }, - maps:merge(ConnAttrs, emqx_protocol:attrs(ProtoState)). + maps:merge(ConnAttrs, emqx_channel:attrs(ChanState)). -spec(stats(pid() | state()) -> emqx_types:stats()). stats(WSPid) when is_pid(WSPid) -> call(WSPid, stats); -stats(#state{proto_state = ProtoState}) -> +stats(#state{chan_state = ChanState}) -> ProcStats = emqx_misc:proc_stats(), - SessStats = emqx_session:stats(emqx_protocol:info(session, ProtoState)), + SessStats = emqx_session:stats(emqx_channel:info(session, ChanState)), lists:append([ProcStats, SessStats, chan_stats(), wsock_stats()]). +-spec(kick(pid()) -> ok). +kick(CPid) -> + call(CPid, kick). + +-spec(discard(pid()) -> ok). +discard(WSPid) -> + WSPid ! {cast, discard}, ok. + +-spec(takeover(pid(), 'begin'|'end') -> {ok, Result :: term()}). +takeover(CPid, Phase) -> + call(CPid, {takeover, Phase}). + %% @private call(WSPid, Req) when is_pid(WSPid) -> Mref = erlang:monitor(process, WSPid), @@ -171,31 +167,23 @@ websocket_init([Req, Opts]) -> [Error, Reason]), undefined end, - ProtoState = emqx_protocol:init(#{peername => Peername, - sockname => Sockname, - peercert => Peercert, - ws_cookie => WsCookie, - conn_mod => ?MODULE}, Opts), + ChanState = emqx_channel:init(#{peername => Peername, + sockname => Sockname, + peercert => Peercert, + ws_cookie => WsCookie, + 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), - 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), {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, - connected = false + chan_state = ChanState, + pendings = [] }}. stat_fun() -> @@ -204,14 +192,15 @@ stat_fun() -> 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) -> +websocket_handle({binary, Data}, State = #state{chan_state = ChanState}) + when is_binary(Data) -> ?LOG(debug, "RECV ~p", [Data]), 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)); + NChanState = emqx_channel:gc(1, Oct, ChanState), + process_incoming(Data, State#state{chan_state = NChanState}); %% Pings should be replied with pongs, cowboy does it automatically %% Pongs can be safely ignored. Clause here simply prevents crash. @@ -240,7 +229,15 @@ websocket_info({call, From, stats}, State) -> websocket_info({call, From, kick}, State) -> gen_server:reply(From, ok), - stop(kick, State); + stop(kicked, State); + +websocket_info({cast, Msg}, State = #state{chan_state = ChanState}) -> + case emqx_channel:handle_cast(Msg, ChanState) of + {ok, NChanState} -> + {ok, State#state{chan_state = NChanState}}; + {stop, Reason, NChanState} -> + stop(Reason, State#state{chan_state = NChanState}) + end; websocket_info({incoming, Packet = ?CONNECT_PACKET( #mqtt_packet_connect{ @@ -264,17 +261,14 @@ websocket_info({incoming, Packet}, State = #state{fsm_state = connected}) handle_incoming(Packet, fun reply/1, 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} -> - reply(State#state{proto_state = NProtoState}); - {ok, Packets, NProtoState} -> - reply(enqueue(Packets, State#state{proto_state = NProtoState})); - {error, Reason} -> - stop(Reason, State); - {error, Reason, NProtoState} -> - stop(Reason, State#state{proto_state = NProtoState}) + State = #state{chan_state = ChanState}) -> + case emqx_channel:handle_out(Deliver, ChanState) of + {ok, NChanState} -> + reply(State#state{chan_state = NChanState}); + {ok, Packets, NChanState} -> + reply(enqueue(Packets, State#state{chan_state = NChanState})); + {stop, Reason, NChanState} -> + stop(Reason, State#state{chan_state = NChanState}) end; websocket_info({keepalive, check}, State = #state{keepalive = KeepAlive}) -> @@ -288,45 +282,17 @@ websocket_info({keepalive, check}, State = #state{keepalive = KeepAlive}) -> 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: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), - 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]), - stop(Reason, NState) +websocket_info({timeout, TRef, Msg}, State = #state{chan_state = ChanState}) + when is_reference(TRef) -> + case emqx_channel:timeout(TRef, Msg, ChanState) of + {ok, NChanState} -> + {ok, State#state{chan_state = NChanState}}; + {ok, Packets, NChanState} -> + reply(enqueue(Packets, State#state{chan_state = NChanState})); + {stop, Reason, NChanState} -> + stop(Reason, State#state{chan_state = NChanState}) 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} -> - reply(enqueue(Packets, State#state{proto_state = NProtoState})); - {error, Reason} -> - stop(Reason, State); - {error, Reason, NProtoState} -> - stop(Reason, State#state{proto_state = NProtoState}) - end; - -websocket_info({subscribe, TopicFilters}, State) -> - handle_request({subscribe, TopicFilters}, State); - -websocket_info({unsubscribe, TopicFilters}, State) -> - handle_request({unsubscribe, TopicFilters}, State); - websocket_info({shutdown, discard, {ClientId, ByPid}}, State) -> ?LOG(warning, "Discarded by ~s:~p", [ClientId, ByPid]), stop(discard, State); @@ -335,40 +301,39 @@ websocket_info({shutdown, conflict, {ClientId, NewPid}}, State) -> ?LOG(warning, "Clientid '~s' conflict with ~p", [ClientId, NewPid]), stop(conflict, State); -%% websocket_info({binary, Data}, State) -> -%% {reply, {binary, Data}, State}; - websocket_info({shutdown, Reason}, State) -> stop(Reason, State); websocket_info({stop, Reason}, State) -> stop(Reason, State); -websocket_info(Info, State) -> - ?LOG(error, "Unexpected info: ~p", [Info]), - {ok, State}. +websocket_info(Info, State = #state{chan_state = ChanState}) -> + case emqx_channel:handle_info(Info, ChanState) of + {ok, NChanState} -> + {ok, State#state{chan_state = NChanState}}; + {stop, Reason, NChanState} -> + stop(Reason, State#state{chan_state = NChanState}) + end. -terminate(SockError, _Req, #state{keepalive = Keepalive, - proto_state = ProtoState, - reason = Reason}) -> +terminate(SockError, _Req, #state{keepalive = KeepAlive, + chan_state = ChanState, + reason = Reason}) -> ?LOG(debug, "Terminated for ~p, sockerror: ~p", [Reason, SockError]), - emqx_keepalive:cancel(Keepalive), - emqx_protocol:terminate(Reason, ProtoState). + KeepAlive =/= undefined + andalso emqx_keepalive:cancel(KeepAlive), + emqx_channel:terminate(Reason, ChanState). %%-------------------------------------------------------------------- %% 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), +connected(State = #state{chan_state = ChanState}) -> + NState = State#state{fsm_state = connected}, + ClientId = emqx_channel:info(client_id, ChanState), ok = emqx_cm:register_channel(ClientId), ok = emqx_cm:set_chan_attrs(ClientId, info(NState)), %% Ensure keepalive after connected successfully. - Interval = emqx_protocol:info(keepalive, ProtoState), + Interval = emqx_channel:info(keepalive, ChanState), case ensure_keepalive(Interval, NState) of ignore -> reply(NState); {ok, KeepAlive} -> @@ -382,22 +347,11 @@ connected(State = #state{proto_state = ProtoState}) -> ensure_keepalive(0, _State) -> ignore; -ensure_keepalive(Interval, #state{proto_state = ProtoState}) -> - Backoff = emqx_zone:get_env(emqx_protocol:info(zone, ProtoState), +ensure_keepalive(Interval, #state{chan_state = ChanState}) -> + Backoff = emqx_zone:get_env(emqx_channel:info(zone, ChanState), keepalive_backoff, 0.75), emqx_keepalive:start(stat_fun(), round(Interval * Backoff), {keepalive, check}). -%%-------------------------------------------------------------------- -%% Handle internal request - -handle_request(Req, State = #state{proto_state = ProtoState}) -> - case emqx_protocol:handle_req(Req, ProtoState) of - {ok, _Result, NProtoState} -> %% TODO:: how to handle the result? - {ok, State#state{proto_state = NProtoState}}; - {error, Reason, NProtoState} -> - stop(Reason, State#state{proto_state = NProtoState}) - end. - %%-------------------------------------------------------------------- %% Process incoming data @@ -424,22 +378,19 @@ process_incoming(Data, State = #state{parse_state = ParseState}) -> %%-------------------------------------------------------------------- %% Handle incoming packets -handle_incoming(Packet = ?PACKET(Type), SuccFun, - State = #state{proto_state = ProtoState}) -> +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_protocol:handle_in(Packet, ProtoState) of - {ok, NProtoState} -> - SuccFun(State#state{proto_state = NProtoState}); - {ok, OutPackets, NProtoState} -> - SuccFun(enqueue(OutPackets, State#state{proto_state = NProtoState})); - {error, Reason, NProtoState} -> - stop(Reason, State#state{proto_state = NProtoState}); - {error, Reason, OutPacket, NProtoState} -> - stop(Reason, enqueue(OutPacket, State#state{proto_state = NProtoState})); - {stop, Error, NProtoState} -> - stop(Error, State#state{proto_state = NProtoState}) + case emqx_channel:handle_in(Packet, ChanState) of + {ok, NChanState} -> + SuccFun(State#state{chan_state= NChanState}); + {ok, OutPackets, NChanState} -> + SuccFun(enqueue(OutPackets, State#state{chan_state= NChanState})); + {stop, Reason, NChanState} -> + stop(Reason, State#state{chan_state= NChanState}); + {stop, Reason, OutPacket, NChanState} -> + stop(Reason, enqueue(OutPacket, State#state{chan_state= NChanState})) end. %%-------------------------------------------------------------------- @@ -495,29 +446,9 @@ enqueue(Packet, State) when is_record(Packet, mqtt_packet) -> enqueue(Packets, State = #state{pendings = Pendings}) -> State#state{pendings = lists:append(Pendings, Packets)}. -%%-------------------------------------------------------------------- -%% Ensure stats timer - -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. - wsock_stats() -> [{Key, emqx_pd:get_counter(Key)} || Key <- ?SOCK_STATS]. chan_stats() -> [{Name, emqx_pd:get_counter(Name)} || Name <- ?CHAN_STATS]. -%%-------------------------------------------------------------------- -%% 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}. - diff --git a/test/emqx_channel_SUITE.erl b/test/emqx_channel_SUITE.erl index 580b7a0fa..55b6f0323 100644 --- a/test/emqx_channel_SUITE.erl +++ b/test/emqx_channel_SUITE.erl @@ -19,6 +19,14 @@ -compile(export_all). -compile(nowarn_export_all). +-import(emqx_channel, + [ handle_in/2 + , handle_out/2 + , handle_out/3 + ]). + +-include("emqx.hrl"). +-include("emqx_mqtt.hrl"). -include_lib("eunit/include/eunit.hrl"). all() -> emqx_ct:all(?MODULE). @@ -30,28 +38,252 @@ init_per_suite(Config) -> end_per_suite(_Config) -> emqx_ct_helpers:stop_apps([]). -t_basic(_) -> - Topic = <<"TopicA">>, - {ok, C} = emqtt:start_link([{port, 1883}, {client_id, <<"hello">>}]), - {ok, _} = emqtt:connect(C), - {ok, _, [1]} = emqtt:subscribe(C, Topic, qos1), - {ok, _, [2]} = emqtt:subscribe(C, Topic, qos2), - {ok, _} = emqtt:publish(C, Topic, <<"qos 2">>, 2), - {ok, _} = emqtt:publish(C, Topic, <<"qos 2">>, 2), - {ok, _} = emqtt:publish(C, Topic, <<"qos 2">>, 2), - ?assertEqual(3, length(recv_msgs(3))), - ok = emqtt:disconnect(C). +%%-------------------------------------------------------------------- +%% Test cases for handle_in +%%-------------------------------------------------------------------- -recv_msgs(Count) -> - recv_msgs(Count, []). +t_handle_connect(_) -> + ConnPkt = #mqtt_packet_connect{ + proto_name = <<"MQTT">>, + proto_ver = ?MQTT_PROTO_V4, + is_bridge = false, + clean_start = true, + keepalive = 30, + properties = #{}, + client_id = <<"clientid">>, + username = <<"username">>, + password = <<"passwd">> + }, + with_channel( + fun(Channel) -> + {ok, ?CONNACK_PACKET(?RC_SUCCESS), Channel1} + = handle_in(?CONNECT_PACKET(ConnPkt), Channel), + Client = emqx_channel:info(client, Channel1), + ?assertEqual(<<"clientid">>, maps:get(client_id, Client)), + ?assertEqual(<<"username">>, maps:get(username, Client)) + end). -recv_msgs(0, Msgs) -> - Msgs; -recv_msgs(Count, Msgs) -> - receive - {publish, Msg} -> - recv_msgs(Count-1, [Msg|Msgs]) - after 100 -> - Msgs - end. +t_handle_publish_qos0(_) -> + with_channel( + fun(Channel) -> + Publish = ?PUBLISH_PACKET(?QOS_0, <<"topic">>, undefined, <<"payload">>), + {ok, Channel} = handle_in(Publish, Channel) + end). + +t_handle_publish_qos1(_) -> + with_channel( + fun(Channel) -> + Publish = ?PUBLISH_PACKET(?QOS_1, <<"topic">>, 1, <<"payload">>), + {ok, ?PUBACK_PACKET(1, RC), _} = handle_in(Publish, Channel), + ?assert((RC == ?RC_SUCCESS) orelse (RC == ?RC_NO_MATCHING_SUBSCRIBERS)) + end). + +t_handle_publish_qos2(_) -> + with_channel( + fun(Channel) -> + Publish1 = ?PUBLISH_PACKET(?QOS_2, <<"topic">>, 1, <<"payload">>), + {ok, ?PUBREC_PACKET(1, RC), Channel1} = handle_in(Publish1, Channel), + Publish2 = ?PUBLISH_PACKET(?QOS_2, <<"topic">>, 2, <<"payload">>), + {ok, ?PUBREC_PACKET(2, RC), Channel2} = handle_in(Publish2, Channel1), + ?assert((RC == ?RC_SUCCESS) orelse (RC == ?RC_NO_MATCHING_SUBSCRIBERS)), + Session = emqx_channel:info(session, Channel2), + ?assertEqual(2, emqx_session:info(awaiting_rel, Session)) + end). + +t_handle_puback(_) -> + with_channel( + fun(Channel) -> + {ok, Channel} = handle_in(?PUBACK_PACKET(1, ?RC_SUCCESS), Channel) + end). + +t_handle_pubrec(_) -> + with_channel( + fun(Channel) -> + {ok, ?PUBREL_PACKET(1, ?RC_PACKET_IDENTIFIER_NOT_FOUND), Channel} + = handle_in(?PUBREC_PACKET(1, ?RC_SUCCESS), Channel) + end). + +t_handle_pubrel(_) -> + with_channel( + fun(Channel) -> + {ok, ?PUBCOMP_PACKET(1, ?RC_PACKET_IDENTIFIER_NOT_FOUND), Channel} + = handle_in(?PUBREL_PACKET(1, ?RC_SUCCESS), Channel) + end). + +t_handle_pubcomp(_) -> + with_channel( + fun(Channel) -> + {ok, Channel} = handle_in(?PUBCOMP_PACKET(1, ?RC_SUCCESS), Channel) + end). + +t_handle_subscribe(_) -> + with_channel( + fun(Channel) -> + TopicFilters = [{<<"+">>, ?DEFAULT_SUBOPTS}], + {ok, ?SUBACK_PACKET(10, [?QOS_0]), Channel1} + = handle_in(?SUBSCRIBE_PACKET(10, #{}, TopicFilters), Channel), + Session = emqx_channel:info(session, Channel1), + ?assertEqual(maps:from_list(TopicFilters), + emqx_session:info(subscriptions, Session)) + + end). + +t_handle_unsubscribe(_) -> + with_channel( + fun(Channel) -> + {ok, ?UNSUBACK_PACKET(11), Channel} + = handle_in(?UNSUBSCRIBE_PACKET(11, #{}, [<<"+">>]), Channel) + end). + +t_handle_pingreq(_) -> + with_channel( + fun(Channel) -> + {ok, ?PACKET(?PINGRESP), Channel} = handle_in(?PACKET(?PINGREQ), Channel) + end). + +t_handle_disconnect(_) -> + with_channel( + fun(Channel) -> + {stop, normal, Channel1} = handle_in(?DISCONNECT_PACKET(?RC_SUCCESS), Channel), + ?assertEqual(undefined, emqx_channel:info(will_msg, Channel1)) + end). + +t_handle_auth(_) -> + with_channel( + fun(Channel) -> + {ok, Channel} = handle_in(?AUTH_PACKET(), Channel) + end). + +%%-------------------------------------------------------------------- +%% Test cases for handle_deliver +%%-------------------------------------------------------------------- + +t_handle_deliver(_) -> + with_channel( + fun(Channel) -> + TopicFilters = [{<<"+">>, ?DEFAULT_SUBOPTS#{qos => ?QOS_2}}], + {ok, ?SUBACK_PACKET(1, [?QOS_2]), Channel1} + = handle_in(?SUBSCRIBE_PACKET(1, #{}, TopicFilters), Channel), + Msg0 = emqx_message:make(<<"clientx">>, ?QOS_0, <<"t0">>, <<"qos0">>), + Msg1 = emqx_message:make(<<"clientx">>, ?QOS_1, <<"t1">>, <<"qos1">>), + Delivers = [{deliver, <<"+">>, Msg0}, + {deliver, <<"+">>, Msg1}], + {ok, Packets, _Channel2} = emqx_channel:handle_deliver(Delivers, Channel1), + ?assertMatch([?PUBLISH_PACKET(?QOS_0, <<"t0">>, undefined, <<"qos0">>), + ?PUBLISH_PACKET(?QOS_1, <<"t1">>, 1, <<"qos1">>) + ], Packets) + end). + +%%-------------------------------------------------------------------- +%% Test cases for handle_out +%%-------------------------------------------------------------------- + +t_handle_conack(_) -> + with_channel( + fun(Channel) -> + {ok, ?CONNACK_PACKET(?RC_SUCCESS, SP, _), _} + = handle_out(connack, {?RC_SUCCESS, 0}, Channel), + {error, unauthorized_client, ?CONNACK_PACKET(5), _} + = handle_out(connack, ?RC_NOT_AUTHORIZED, Channel) + end). + +t_handle_out_publish(_) -> + with_channel( + fun(Channel) -> + Pub0 = {publish, undefined, emqx_message:make(<<"t">>, <<"qos0">>)}, + Pub1 = {publish, 1, emqx_message:make(<<"c">>, ?QOS_1, <<"t">>, <<"qos1">>)}, + {ok, ?PUBLISH_PACKET(?QOS_0), Channel} = handle_out(Pub0, Channel), + {ok, ?PUBLISH_PACKET(?QOS_1), Channel} = handle_out(Pub1, Channel), + {ok, Packets, Channel} = handle_out(publish, [Pub0, Pub1], Channel), + ?assertEqual(2, length(Packets)) + end). + +t_handle_out_puback(_) -> + with_channel( + fun(Channel) -> + {ok, Channel} = handle_out(puberr, ?RC_NOT_AUTHORIZED, Channel), + {ok, ?PUBACK_PACKET(1, ?RC_SUCCESS), Channel} + = handle_out(puback, {1, ?RC_SUCCESS}, Channel) + end). + +t_handle_out_pubrec(_) -> + with_channel( + fun(Channel) -> + {ok, ?PUBREC_PACKET(4, ?RC_SUCCESS), Channel} + = handle_out(pubrec, {4, ?RC_SUCCESS}, Channel) + end). + +t_handle_out_pubrel(_) -> + with_channel( + fun(Channel) -> + {ok, ?PUBREL_PACKET(2), Channel} + = handle_out(pubrel, {2, ?RC_SUCCESS}, Channel), + {ok, ?PUBREL_PACKET(3, ?RC_SUCCESS), Channel} + = handle_out(pubrel, {3, ?RC_SUCCESS}, Channel) + end). + +t_handle_out_pubcomp(_) -> + with_channel( + fun(Channel) -> + {ok, ?PUBCOMP_PACKET(5, ?RC_SUCCESS), Channel} + = handle_out(pubcomp, {5, ?RC_SUCCESS}, Channel) + end). + +t_handle_out_suback(_) -> + with_channel( + fun(Channel) -> + {ok, ?SUBACK_PACKET(1, [?QOS_2]), Channel} + = handle_out(suback, {1, [?QOS_2]}, Channel) + end). + +t_handle_out_unsuback(_) -> + with_channel( + fun(Channel) -> + {ok, ?UNSUBACK_PACKET(1), Channel} + = handle_out(unsuback, {1, [?RC_SUCCESS]}, Channel) + end). + +t_handle_out_disconnect(_) -> + with_channel( + fun(Channel) -> + handle_out(disconnect, ?RC_SUCCESS, Channel) + end). + +%%-------------------------------------------------------------------- +%% Test cases for handle_timeout +%%-------------------------------------------------------------------- + +t_handle_timeout(_) -> + with_channel( + fun(Channel) -> + 'TODO' + end). + +%%-------------------------------------------------------------------- +%% Test cases for terminate +%%-------------------------------------------------------------------- + +t_terminate(_) -> + with_channel( + fun(Channel) -> + 'TODO' + end). + +%%-------------------------------------------------------------------- +%% Helper functions +%%-------------------------------------------------------------------- + +with_channel(Fun) -> + ConnInfo = #{peername => {{127,0,0,1}, 3456}, + sockname => {{127,0,0,1}, 1883}, + client_id => <<"clientid">>, + username => <<"username">> + }, + Options = [{zone, testing}], + Channel = emqx_channel:init(ConnInfo, Options), + Session = emqx_session:init(false, #{zone => testing}, + #{max_inflight => 100, + expiry_interval => 0 + }), + Fun(emqx_channel:set(session, Session, Channel)). diff --git a/test/emqx_connection_SUITE.erl b/test/emqx_connection_SUITE.erl new file mode 100644 index 000000000..8e595b8b2 --- /dev/null +++ b/test/emqx_connection_SUITE.erl @@ -0,0 +1,57 @@ +%%-------------------------------------------------------------------- +%% 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_connection_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("eunit/include/eunit.hrl"). + +all() -> emqx_ct:all(?MODULE). + +init_per_suite(Config) -> + emqx_ct_helpers:start_apps([]), + Config. + +end_per_suite(_Config) -> + emqx_ct_helpers:stop_apps([]). + +t_basic(_) -> + Topic = <<"TopicA">>, + {ok, C} = emqtt:start_link([{port, 1883}, {client_id, <<"hello">>}]), + {ok, _} = emqtt:connect(C), + {ok, _, [1]} = emqtt:subscribe(C, Topic, qos1), + {ok, _, [2]} = emqtt:subscribe(C, Topic, qos2), + {ok, _} = emqtt:publish(C, Topic, <<"qos 2">>, 2), + {ok, _} = emqtt:publish(C, Topic, <<"qos 2">>, 2), + {ok, _} = emqtt:publish(C, Topic, <<"qos 2">>, 2), + ?assertEqual(3, length(recv_msgs(3))), + ok = emqtt:disconnect(C). + +recv_msgs(Count) -> + recv_msgs(Count, []). + +recv_msgs(0, Msgs) -> + Msgs; +recv_msgs(Count, Msgs) -> + receive + {publish, Msg} -> + recv_msgs(Count-1, [Msg|Msgs]) + after 100 -> + Msgs + end. + diff --git a/test/emqx_oom_SUITE.erl b/test/emqx_oom_SUITE.erl new file mode 100644 index 000000000..a4be93129 --- /dev/null +++ b/test/emqx_oom_SUITE.erl @@ -0,0 +1,34 @@ +%%-------------------------------------------------------------------- +%% 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_oom_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("eunit/include/eunit.hrl"). + +all() -> emqx_ct:all(?MODULE). + +t_init(_) -> + 'TODO'. + +t_check(_) -> + 'TODO'. + +t_info(_) -> + 'TODO'. + diff --git a/test/emqx_protocol_SUITE.erl b/test/emqx_protocol_SUITE.erl deleted file mode 100644 index 4edcbe3f7..000000000 --- a/test/emqx_protocol_SUITE.erl +++ /dev/null @@ -1,287 +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_protocol_SUITE). - --compile(export_all). --compile(nowarn_export_all). - --import(emqx_protocol, - [ handle_in/2 - , handle_out/2 - ]). - --include("emqx.hrl"). --include("emqx_mqtt.hrl"). - --include_lib("eunit/include/eunit.hrl"). - -all() -> emqx_ct:all(?MODULE). - -init_per_suite(Config) -> - emqx_ct_helpers:start_apps([]), - Config. - -end_per_suite(_Config) -> - emqx_ct_helpers:stop_apps([]). - -%%-------------------------------------------------------------------- -%% Test cases for handle_in -%%-------------------------------------------------------------------- - -t_handle_connect(_) -> - ConnPkt = #mqtt_packet_connect{ - proto_name = <<"MQTT">>, - proto_ver = ?MQTT_PROTO_V4, - is_bridge = false, - clean_start = true, - keepalive = 30, - properties = #{}, - client_id = <<"clientid">>, - username = <<"username">>, - password = <<"passwd">> - }, - with_proto( - fun(PState) -> - {ok, ?CONNACK_PACKET(?RC_SUCCESS), PState1} - = handle_in(?CONNECT_PACKET(ConnPkt), PState), - Client = emqx_protocol:info(client, PState1), - ?assertEqual(<<"clientid">>, maps:get(client_id, Client)), - ?assertEqual(<<"username">>, maps:get(username, Client)) - end). - -t_handle_publish_qos0(_) -> - with_proto( - fun(PState) -> - Publish = ?PUBLISH_PACKET(?QOS_0, <<"topic">>, undefined, <<"payload">>), - {ok, PState} = handle_in(Publish, PState) - end). - -t_handle_publish_qos1(_) -> - with_proto( - fun(PState) -> - Publish = ?PUBLISH_PACKET(?QOS_1, <<"topic">>, 1, <<"payload">>), - {ok, ?PUBACK_PACKET(1, RC), _} = handle_in(Publish, PState), - ?assert((RC == ?RC_SUCCESS) orelse (RC == ?RC_NO_MATCHING_SUBSCRIBERS)) - end). - -t_handle_publish_qos2(_) -> - with_proto( - fun(PState) -> - Publish1 = ?PUBLISH_PACKET(?QOS_2, <<"topic">>, 1, <<"payload">>), - {ok, ?PUBREC_PACKET(1, RC), PState1} = handle_in(Publish1, PState), - Publish2 = ?PUBLISH_PACKET(?QOS_2, <<"topic">>, 2, <<"payload">>), - {ok, ?PUBREC_PACKET(2, RC), PState2} = handle_in(Publish2, PState1), - ?assert((RC == ?RC_SUCCESS) orelse (RC == ?RC_NO_MATCHING_SUBSCRIBERS)), - Session = emqx_protocol:info(session, PState2), - ?assertEqual(2, emqx_session:info(awaiting_rel, Session)) - end). - -t_handle_puback(_) -> - with_proto( - fun(PState) -> - {ok, PState} = handle_in(?PUBACK_PACKET(1, ?RC_SUCCESS), PState) - end). - -t_handle_pubrec(_) -> - with_proto( - fun(PState) -> - {ok, ?PUBREL_PACKET(1, ?RC_PACKET_IDENTIFIER_NOT_FOUND), PState} - = handle_in(?PUBREC_PACKET(1, ?RC_SUCCESS), PState) - end). - -t_handle_pubrel(_) -> - with_proto( - fun(PState) -> - {ok, ?PUBCOMP_PACKET(1, ?RC_PACKET_IDENTIFIER_NOT_FOUND), PState} - = handle_in(?PUBREL_PACKET(1, ?RC_SUCCESS), PState) - end). - -t_handle_pubcomp(_) -> - with_proto( - fun(PState) -> - {ok, PState} = handle_in(?PUBCOMP_PACKET(1, ?RC_SUCCESS), PState) - end). - -t_handle_subscribe(_) -> - with_proto( - fun(PState) -> - TopicFilters = [{<<"+">>, ?DEFAULT_SUBOPTS}], - {ok, ?SUBACK_PACKET(10, [?QOS_0]), PState1} - = handle_in(?SUBSCRIBE_PACKET(10, #{}, TopicFilters), PState), - Session = emqx_protocol:info(session, PState1), - ?assertEqual(maps:from_list(TopicFilters), - emqx_session:info(subscriptions, Session)) - - end). - -t_handle_unsubscribe(_) -> - with_proto( - fun(PState) -> - {ok, ?UNSUBACK_PACKET(11), PState} - = handle_in(?UNSUBSCRIBE_PACKET(11, #{}, [<<"+">>]), PState) - end). - -t_handle_pingreq(_) -> - with_proto( - fun(PState) -> - {ok, ?PACKET(?PINGRESP), PState} = handle_in(?PACKET(?PINGREQ), PState) - end). - -t_handle_disconnect(_) -> - with_proto( - fun(PState) -> - {stop, normal, PState1} = handle_in(?DISCONNECT_PACKET(?RC_SUCCESS), PState), - ?assertEqual(undefined, emqx_protocol:info(will_msg, PState1)) - end). - -t_handle_auth(_) -> - with_proto( - fun(PState) -> - {ok, PState} = handle_in(?AUTH_PACKET(), PState) - end). - -%%-------------------------------------------------------------------- -%% Test cases for handle_deliver -%%-------------------------------------------------------------------- - -t_handle_deliver(_) -> - with_proto( - fun(PState) -> - TopicFilters = [{<<"+">>, ?DEFAULT_SUBOPTS#{qos => ?QOS_2}}], - {ok, ?SUBACK_PACKET(1, [?QOS_2]), PState1} - = handle_in(?SUBSCRIBE_PACKET(1, #{}, TopicFilters), PState), - Msg0 = emqx_message:make(<<"clientx">>, ?QOS_0, <<"t0">>, <<"qos0">>), - Msg1 = emqx_message:make(<<"clientx">>, ?QOS_1, <<"t1">>, <<"qos1">>), - Delivers = [{deliver, <<"+">>, Msg0}, - {deliver, <<"+">>, Msg1}], - {ok, Packets, _PState2} = emqx_protocol:handle_deliver(Delivers, PState1), - ?assertMatch([?PUBLISH_PACKET(?QOS_0, <<"t0">>, undefined, <<"qos0">>), - ?PUBLISH_PACKET(?QOS_1, <<"t1">>, 1, <<"qos1">>) - ], Packets) - end). - -%%-------------------------------------------------------------------- -%% Test cases for handle_out -%%-------------------------------------------------------------------- - -t_handle_conack(_) -> - with_proto( - fun(PState) -> - {ok, ?CONNACK_PACKET(?RC_SUCCESS, SP, _), _} - = handle_out({connack, ?RC_SUCCESS, 0}, PState), - {error, unauthorized_client, ?CONNACK_PACKET(5), _} - = handle_out({connack, ?RC_NOT_AUTHORIZED}, PState) - end). - -t_handle_out_publish(_) -> - with_proto( - fun(PState) -> - Pub0 = {publish, undefined, emqx_message:make(<<"t">>, <<"qos0">>)}, - Pub1 = {publish, 1, emqx_message:make(<<"c">>, ?QOS_1, <<"t">>, <<"qos1">>)}, - {ok, ?PUBLISH_PACKET(?QOS_0), PState} = handle_out(Pub0, PState), - {ok, ?PUBLISH_PACKET(?QOS_1), PState} = handle_out(Pub1, PState), - {ok, Packets, PState} = handle_out({publish, [Pub0, Pub1]}, PState), - ?assertEqual(2, length(Packets)) - end). - -t_handle_out_puback(_) -> - with_proto( - fun(PState) -> - {ok, PState} = handle_out({puberr, ?RC_NOT_AUTHORIZED}, PState), - {ok, ?PUBACK_PACKET(1, ?RC_SUCCESS), PState} - = handle_out({puback, 1, ?RC_SUCCESS}, PState) - end). - -t_handle_out_pubrec(_) -> - with_proto( - fun(PState) -> - {ok, ?PUBREC_PACKET(4, ?RC_SUCCESS), PState} - = handle_out({pubrec, 4, ?RC_SUCCESS}, PState) - end). - -t_handle_out_pubrel(_) -> - with_proto( - fun(PState) -> - {ok, ?PUBREL_PACKET(2), PState} = handle_out({pubrel, 2}, PState), - {ok, ?PUBREL_PACKET(3, ?RC_SUCCESS), PState} - = handle_out({pubrel, 3, ?RC_SUCCESS}, PState) - end). - -t_handle_out_pubcomp(_) -> - with_proto( - fun(PState) -> - {ok, ?PUBCOMP_PACKET(5, ?RC_SUCCESS), PState} - = handle_out({pubcomp, 5, ?RC_SUCCESS}, PState) - end). - -t_handle_out_suback(_) -> - with_proto( - fun(PState) -> - {ok, ?SUBACK_PACKET(1, [?QOS_2]), PState} - = handle_out({suback, 1, [?QOS_2]}, PState) - end). - -t_handle_out_unsuback(_) -> - with_proto( - fun(PState) -> - {ok, ?UNSUBACK_PACKET(1), PState} = handle_out({unsuback, 1, [?RC_SUCCESS]}, PState) - end). - -t_handle_out_disconnect(_) -> - with_proto( - fun(PState) -> - handle_out({disconnect, 0}, PState) - end). - -%%-------------------------------------------------------------------- -%% Test cases for handle_timeout -%%-------------------------------------------------------------------- - -t_handle_timeout(_) -> - with_proto( - fun(PState) -> - 'TODO' - end). - -%%-------------------------------------------------------------------- -%% Test cases for terminate -%%-------------------------------------------------------------------- - -t_terminate(_) -> - with_proto( - fun(PState) -> - 'TODO' - end). - -%%-------------------------------------------------------------------- -%% Helper functions -%%-------------------------------------------------------------------- - -with_proto(Fun) -> - ConnInfo = #{peername => {{127,0,0,1}, 3456}, - sockname => {{127,0,0,1}, 1883}, - client_id => <<"clientid">>, - username => <<"username">> - }, - Options = [{zone, testing}], - PState = emqx_protocol:init(ConnInfo, Options), - Session = emqx_session:init(false, #{zone => testing}, - #{max_inflight => 100, - expiry_interval => 0 - }), - Fun(emqx_protocol:set(session, Session, PState)). - diff --git a/test/emqx_ws_channel_SUITE.erl b/test/emqx_ws_connection_SUITE.erl similarity index 98% rename from test/emqx_ws_channel_SUITE.erl rename to test/emqx_ws_connection_SUITE.erl index f634e633e..2e2db7728 100644 --- a/test/emqx_ws_channel_SUITE.erl +++ b/test/emqx_ws_connection_SUITE.erl @@ -14,7 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_ws_channel_SUITE). +-module(emqx_ws_connection_SUITE). -compile(export_all). -compile(nowarn_export_all). From ae33b6037e9cf39346eb269af29d98d23571eec6 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sat, 17 Aug 2019 14:39:04 +0800 Subject: [PATCH 11/36] Fix the test suites. --- src/emqx_ws_connection.erl | 1 - test/emqx_channel_SUITE.erl | 8 ++++---- test/emqx_misc_SUITE.erl | 20 -------------------- test/emqx_oom_SUITE.erl | 22 +++++++++++++++++----- 4 files changed, 21 insertions(+), 30 deletions(-) diff --git a/src/emqx_ws_connection.erl b/src/emqx_ws_connection.erl index 2597ec7fe..981fad4c3 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_connection.erl @@ -177,7 +177,6 @@ websocket_init([Req, Opts]) -> MaxSize = emqx_zone:get_env(Zone, max_packet_size, ?MAX_PACKET_SIZE), ParseState = emqx_frame:initial_parse_state(#{max_size => MaxSize}), emqx_logger:set_metadata_peername(esockd_net:format(Peername)), - ok = emqx_misc:init_proc_mng_policy(Zone), {ok, #state{peername = Peername, sockname = Sockname, fsm_state = idle, diff --git a/test/emqx_channel_SUITE.erl b/test/emqx_channel_SUITE.erl index 55b6f0323..02f9aa975 100644 --- a/test/emqx_channel_SUITE.erl +++ b/test/emqx_channel_SUITE.erl @@ -166,9 +166,9 @@ t_handle_deliver(_) -> = handle_in(?SUBSCRIBE_PACKET(1, #{}, TopicFilters), Channel), Msg0 = emqx_message:make(<<"clientx">>, ?QOS_0, <<"t0">>, <<"qos0">>), Msg1 = emqx_message:make(<<"clientx">>, ?QOS_1, <<"t1">>, <<"qos1">>), - Delivers = [{deliver, <<"+">>, Msg0}, - {deliver, <<"+">>, Msg1}], - {ok, Packets, _Channel2} = emqx_channel:handle_deliver(Delivers, Channel1), + %% TODO: Fixme later. + self() ! {deliver, <<"+">>, Msg1}, + {ok, Packets, _Channel2} = emqx_channel:handle_out({deliver, <<"+">>, Msg0}, Channel1), ?assertMatch([?PUBLISH_PACKET(?QOS_0, <<"t0">>, undefined, <<"qos0">>), ?PUBLISH_PACKET(?QOS_1, <<"t1">>, 1, <<"qos1">>) ], Packets) @@ -183,7 +183,7 @@ t_handle_conack(_) -> fun(Channel) -> {ok, ?CONNACK_PACKET(?RC_SUCCESS, SP, _), _} = handle_out(connack, {?RC_SUCCESS, 0}, Channel), - {error, unauthorized_client, ?CONNACK_PACKET(5), _} + {stop, {shutdown, unauthorized_client}, ?CONNACK_PACKET(5), _} = handle_out(connack, ?RC_NOT_AUTHORIZED, Channel) end). diff --git a/test/emqx_misc_SUITE.erl b/test/emqx_misc_SUITE.erl index c9a3929b6..a8337c060 100644 --- a/test/emqx_misc_SUITE.erl +++ b/test/emqx_misc_SUITE.erl @@ -55,26 +55,6 @@ t_timer_cancel_flush() -> after 0 -> ok end. -t_shutdown_disabled() -> - ok = drain(), - self() ! foo, - ?assertEqual(continue, emqx_misc:conn_proc_mng_policy(0)), - receive foo -> ok end, - ?assertEqual(hibernate, emqx_misc:conn_proc_mng_policy(0)). - -t_message_queue_too_long() -> - ok = drain(), - self() ! foo, - self() ! bar, - ?assertEqual({shutdown, message_queue_too_long}, - emqx_misc:conn_proc_mng_policy(1)), - receive foo -> ok end, - ?assertEqual(continue, emqx_misc:conn_proc_mng_policy(1)), - receive bar -> ok end. - -t_conn_proc_mng_policy(L) -> - emqx_misc:conn_proc_mng_policy(#{message_queue_len => L}). - t_proc_name(_) -> 'TODO'. diff --git a/test/emqx_oom_SUITE.erl b/test/emqx_oom_SUITE.erl index a4be93129..90eb4a253 100644 --- a/test/emqx_oom_SUITE.erl +++ b/test/emqx_oom_SUITE.erl @@ -24,11 +24,23 @@ all() -> emqx_ct:all(?MODULE). t_init(_) -> - 'TODO'. + ?assertEqual(undefined, emqx_oom:init(undefined)), + Opts = #{message_queue_len => 10, + max_heap_size => 1024*1024*8 + }, + Oom = emqx_oom:init(Opts), + ?assertEqual(#{message_queue_len => 10, + max_heap_size => 1024*1024 + }, emqx_oom:info(Oom)). t_check(_) -> - 'TODO'. - -t_info(_) -> - 'TODO'. + ?assertEqual(ok, emqx_oom:check(undefined)), + Opts = #{message_queue_len => 10, + max_heap_size => 1024*1024*8 + }, + Oom = emqx_oom:init(Opts), + [self() ! {msg, I} || I <- lists:seq(1, 5)], + ?assertEqual(ok, emqx_oom:check(Oom)), + [self() ! {msg, I} || I <- lists:seq(1, 6)], + ?assertEqual({shutdown, message_queue_too_long}, emqx_oom:check(Oom)). From f2b552e29e32e0891ecec8a4e7a2990bb0740d5e Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sat, 17 Aug 2019 17:50:35 +0800 Subject: [PATCH 12/36] Ensure stats timer --- src/emqx_channel.erl | 24 ++++++++++++++---------- src/emqx_connection.erl | 38 +++++++++++++++++++++++--------------- src/emqx_ws_connection.erl | 36 +++++++++++++++++++++++------------- 3 files changed, 60 insertions(+), 38 deletions(-) diff --git a/src/emqx_channel.erl b/src/emqx_channel.erl index d8a69d0e0..b9b18cd8d 100644 --- a/src/emqx_channel.erl +++ b/src/emqx_channel.erl @@ -46,6 +46,8 @@ , terminate/2 ]). +-export([ensure_timer/2]). + -export([gc/3]). -import(emqx_access_control, @@ -53,6 +55,8 @@ , check_acl/3 ]). +-import(emqx_misc, [start_timer/2]). + -export_type([channel/0]). -record(channel, { @@ -659,17 +663,17 @@ handle_info(Info, Channel) -> -> {ok, channel()} | {ok, Result :: term(), channel()} | {stop, Reason :: term(), channel()}). +timeout(TRef, {emit_stats, Stats}, Channel = #channel{stats_timer = TRef}) -> + ClientId = info(client_id, Channel), + ok = emqx_cm:set_chan_stats(ClientId, Stats), + {ok, Channel#channel{stats_timer = undefined}}; + timeout(TRef, retry_deliver, Channel = #channel{%%session = Session, retry_timer = TRef}) -> %% case emqx_session:retry(Session) of %% TODO: ... {ok, Channel#channel{retry_timer = undefined}}; -timeout(TRef, emit_stats, Channel = #channel{stats_timer = TRef}) -> - ClientId = info(client_id, Channel), - %% ok = emqx_cm:set_chan_stats(ClientId, stats(Channel)), - {ok, Channel#channel{stats_timer = undefined}}; - timeout(_TRef, Msg, Channel) -> ?LOG(error, "Unexpected timeout: ~p~n", [Msg]), {ok, Channel}. @@ -678,17 +682,17 @@ timeout(_TRef, Msg, Channel) -> %% Ensure timers %%-------------------------------------------------------------------- +ensure_timer(emit_stats, Channel = #channel{stats_timer = undefined, + idle_timeout = IdleTimeout + }) -> + Channel#channel{stats_timer = start_timer(IdleTimeout, emit_stats)}; + ensure_timer(retry, Channel = #channel{session = Session, retry_timer = undefined}) -> Interval = emqx_session:info(retry_interval, Session), TRef = emqx_misc:start_timer(Interval, retry_deliver), Channel#channel{retry_timer = TRef}; -ensure_timer(stats, Channel = #channel{stats_timer = undefined, - idle_timeout = IdleTimeout}) -> - TRef = emqx_misc:start_timer(IdleTimeout, emit_stats), - Channel#channel{stats_timer = TRef}; - %% disabled or timer existed ensure_timer(_Name, Channel) -> Channel. diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index c525a1fc7..cfb743a25 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -360,7 +360,8 @@ handle(info, {Inet, _Sock, Data}, State = #state{chan_state = ChanState}) ?LOG(debug, "RECV ~p", [Data]), emqx_pd:update_counter(incoming_bytes, Oct), ok = emqx_metrics:inc('bytes.received', Oct), - NChanState = emqx_channel:gc(1, Oct, ChanState), + NChanState = emqx_channel:ensure_timer( + emit_stats, emqx_channel:gc(1, Oct, ChanState)), process_incoming(Data, State#state{chan_state = NChanState}); handle(info, {Error, _Sock, Reason}, State) @@ -398,24 +399,19 @@ handle(info, activate_socket, State) -> shutdown(Reason, NState) end; -handle(info, {inet_reply, _Sock, ok}, State) -> +handle(info, {inet_reply, _Sock, ok}, State = #state{chan_state = ChanState}) -> %% something sent - keep_state(State); + NChanState = emqx_channel:ensure_timer(emit_stats, ChanState), + keep_state(State#state{chan_state = NChanState}); handle(info, {inet_reply, _Sock, {error, Reason}}, State) -> shutdown(Reason, State); -handle(info, {timeout, TRef, Msg}, State = #state{chan_state = ChanState}) - when is_reference(TRef) -> - case emqx_channel:timeout(TRef, Msg, ChanState) of - {ok, NChanState} -> - keep_state(State#state{chan_state = NChanState}); - {ok, Packets, NChanState} -> - handle_outgoing(Packets, fun keep_state/1, - State#state{chan_state = NChanState}); - {stop, Reason, NChanState} -> - stop(Reason, State#state{chan_state = NChanState}) - end; +handle(info, {timeout, TRef, emit_stats}, State) when is_reference(TRef) -> + handle_timeout(TRef, {emit_stats, stats(State)}, State); + +handle(info, {timeout, TRef, Msg}, State) when is_reference(TRef) -> + handle_timeout(TRef, Msg, State); handle(info, {shutdown, conflict, {ClientId, NewPid}}, State) -> ?LOG(warning, "Clientid '~s' conflict with ~p", [ClientId, NewPid]), @@ -528,7 +524,19 @@ send(IoData, SuccFun, State = #state{transport = Transport, shutdown(Reason, State) end. -%% TODO: maybe_gc(1, Oct, State) +%%-------------------------------------------------------------------- +%% Handle timeout + +handle_timeout(TRef, Msg, State = #state{chan_state = ChanState}) -> + case emqx_channel:timeout(TRef, Msg, ChanState) of + {ok, NChanState} -> + keep_state(State#state{chan_state = NChanState}); + {ok, Packets, NChanState} -> + handle_outgoing(Packets, fun keep_state/1, + State#state{chan_state = NChanState}); + {stop, Reason, NChanState} -> + stop(Reason, State#state{chan_state = NChanState}) + end. %%-------------------------------------------------------------------- %% Ensure keepalive diff --git a/src/emqx_ws_connection.erl b/src/emqx_ws_connection.erl index 981fad4c3..530a300f8 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_connection.erl @@ -198,7 +198,8 @@ websocket_handle({binary, Data}, State = #state{chan_state = ChanState}) emqx_pd:update_counter(recv_cnt, 1), emqx_pd:update_counter(recv_oct, Oct), ok = emqx_metrics:inc('bytes.received', Oct), - NChanState = emqx_channel:gc(1, Oct, ChanState), + NChanState = emqx_channel:ensure_timer( + emit_stats, emqx_channel:gc(1, Oct, ChanState)), process_incoming(Data, State#state{chan_state = NChanState}); %% Pings should be replied with pongs, cowboy does it automatically @@ -281,16 +282,11 @@ websocket_info({keepalive, check}, State = #state{keepalive = KeepAlive}) -> stop(keepalive_error, State) end; -websocket_info({timeout, TRef, Msg}, State = #state{chan_state = ChanState}) - when is_reference(TRef) -> - case emqx_channel:timeout(TRef, Msg, ChanState) of - {ok, NChanState} -> - {ok, State#state{chan_state = NChanState}}; - {ok, Packets, NChanState} -> - reply(enqueue(Packets, State#state{chan_state = NChanState})); - {stop, Reason, NChanState} -> - stop(Reason, State#state{chan_state = NChanState}) - end; +websocket_info({timeout, TRef, emit_stats}, State) when is_reference(TRef) -> + handle_timeout(TRef, {emit_stats, stats(State)}, State); + +websocket_info({timeout, TRef, Msg}, State) when is_reference(TRef) -> + handle_timeout(TRef, Msg, State); websocket_info({shutdown, discard, {ClientId, ByPid}}, State) -> ?LOG(warning, "Discarded by ~s:~p", [ClientId, ByPid]), @@ -341,6 +337,19 @@ connected(State = #state{chan_state = ChanState}) -> stop(Reason, NState) end. +%%-------------------------------------------------------------------- +%% Handle timeout + +handle_timeout(TRef, Msg, State = #state{chan_state = ChanState}) -> + case emqx_channel:timeout(TRef, Msg, ChanState) of + {ok, NChanState} -> + {ok, State#state{chan_state = NChanState}}; + {ok, Packets, NChanState} -> + reply(enqueue(Packets, State#state{chan_state = NChanState})); + {stop, Reason, NChanState} -> + stop(Reason, State#state{chan_state = NChanState}) + end. + %%-------------------------------------------------------------------- %% Ensure keepalive @@ -429,9 +438,10 @@ inc_outgoing_stats(Type) -> reply(State = #state{pendings = []}) -> {ok, State}; -reply(State = #state{pendings = Pendings}) -> +reply(State = #state{chan_state = ChanState, pendings = Pendings}) -> Reply = handle_outgoing(Pendings, State), - {reply, Reply, State#state{pendings = []}}. + NChanState = emqx_channel:ensure_timer(emit_stats, ChanState), + {reply, Reply, State#state{chan_state = NChanState, pendings = []}}. stop(Reason, State = #state{pendings = []}) -> {stop, State#state{reason = Reason}}; From 749ef823abaebb4b9e969b9bb4c60efc88d7860a Mon Sep 17 00:00:00 2001 From: tigercl Date: Mon, 19 Aug 2019 10:50:52 +0800 Subject: [PATCH 13/36] Refix code about mqtt spec (#2806) Refix code about mqtt spec --- src/emqx_channel.erl | 19 ++++++------------- src/emqx_cm.erl | 4 ++-- src/emqx_connection.erl | 3 ++- 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/src/emqx_channel.erl b/src/emqx_channel.erl index b9b18cd8d..57ff91a16 100644 --- a/src/emqx_channel.erl +++ b/src/emqx_channel.erl @@ -280,7 +280,7 @@ handle_in(?CONNECT_PACKET( handle_out(connack, ReasonCode, NChannel) end; -handle_in(Packet = ?PUBLISH_PACKET(QoS, Topic, PacketId), Channel = #channel{proto_ver = Ver}) -> +handle_in(Packet = ?PUBLISH_PACKET(_QoS, Topic, _PacketId), Channel = #channel{proto_ver = Ver}) -> case pipeline([fun validate_in/2, fun process_alias/2, fun check_publish/2], Packet, Channel) of @@ -289,11 +289,7 @@ handle_in(Packet = ?PUBLISH_PACKET(QoS, Topic, PacketId), Channel = #channel{pro {error, ReasonCode, NChannel} -> ?LOG(warning, "Cannot publish message to ~s due to ~s", [Topic, emqx_reason_codes:text(ReasonCode, Ver)]), - case QoS of - ?QOS_0 -> handle_out(puberr, ReasonCode, NChannel); - ?QOS_1 -> handle_out(puback, {PacketId, ReasonCode}, NChannel); - ?QOS_2 -> handle_out(pubrec, {PacketId, ReasonCode}, NChannel) - end + handle_out(disconnect, ReasonCode, NChannel) end; %%TODO: How to handle the ReasonCode? @@ -845,13 +841,10 @@ check_will_retain(#mqtt_packet_connect{will_retain = true}, %%-------------------------------------------------------------------- enrich_client(ConnPkt, Channel) -> - case pipeline([fun set_username/2, - fun maybe_use_username_as_clientid/2, - fun maybe_assign_clientid/2, - fun set_rest_client_fields/2], ConnPkt, Channel) of - {ok, NConnPkt, NChannel} -> {ok, NConnPkt, NChannel}; - Error -> Error - end. + pipeline([fun set_username/2, + fun maybe_use_username_as_clientid/2, + fun maybe_assign_clientid/2, + fun set_rest_client_fields/2], ConnPkt, Channel). maybe_use_username_as_clientid(_ConnPkt, Channel = #channel{client = #{username := undefined}}) -> {ok, Channel}; diff --git a/src/emqx_cm.erl b/src/emqx_cm.erl index dbfb38d0d..c393d6ab0 100644 --- a/src/emqx_cm.erl +++ b/src/emqx_cm.erl @@ -189,7 +189,7 @@ takeover_session(ClientId) -> takeover_session(ClientId, ChanPid); ChanPids -> [ChanPid|StalePids] = lists:reverse(ChanPids), - ?LOG(error, "[SM] More than one channel found: ~p", [ChanPids]), + ?LOG(error, "More than one channel found: ~p", [ChanPids]), lists:foreach(fun(StalePid) -> catch discard_session(ClientId, StalePid) end, StalePids), @@ -220,7 +220,7 @@ discard_session(ClientId) when is_binary(ClientId) -> discard_session(ClientId, ChanPid) catch _:Error:_Stk -> - ?LOG(error, "[SM] Failed to discard ~p: ~p", [ChanPid, Error]) + ?LOG(error, "Failed to discard ~p: ~p", [ChanPid, Error]) end end, ChanPids) end. diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index cfb743a25..5ff534a2b 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -257,7 +257,8 @@ connected(enter, _PrevSt, State = #state{chan_state = ChanState}) -> connected(cast, {incoming, Packet = ?PACKET(?CONNECT)}, State) -> ?LOG(warning, "Unexpected connect: ~p", [Packet]), - shutdown(unexpected_incoming_connect, State); + Shutdown = fun(NewSt) -> shutdown(?RC_PROTOCOL_ERROR, NewSt) end, + handle_outgoing(?DISCONNECT_PACKET(?RC_PROTOCOL_ERROR), Shutdown, State); connected(cast, {incoming, Packet}, State) when is_record(Packet, mqtt_packet) -> handle_incoming(Packet, fun keep_state/1, State); From fb1b47de6e5fa6db1168ab22a7b60adfdc2e36b5 Mon Sep 17 00:00:00 2001 From: CrazyWisdom Date: Tue, 20 Aug 2019 15:23:00 +0800 Subject: [PATCH 14/36] fix(README): fix FAQ url (#2815) --- README-CN.md | 4 ++-- README.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README-CN.md b/README-CN.md index bdba664b4..5901eed1b 100644 --- a/README-CN.md +++ b/README-CN.md @@ -54,7 +54,7 @@ cd _rel/emqx && ./bin/emqx console ## FAQ -访问 [FAQ](https://docs.emqx.io/tutorial/zh/faq/faq.html) 以获取常见问题的帮助。 +访问 [FAQ](https://docs.emqx.io/tutorial/v3/cn/faq/faq.html) 以获取常见问题的帮助。 ## 产品路线 @@ -84,4 +84,4 @@ cd _rel/emqx && ./bin/emqx console ## 开源许可 -Apache License 2.0, 详见 [LICENSE](./LICENSE)。 \ No newline at end of file +Apache License 2.0, 详见 [LICENSE](./LICENSE)。 diff --git a/README.md b/README.md index 8371297de..9847c8038 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ To view the dashboard after running, use your browser to open: http://localhost: ## FAQ -Visiting [FAQ](https://docs.emqx.io/tutorial/en/faq/faq.html) to get help of common problems. +Visiting [FAQ](https://docs.emqx.io/tutorial/v3/en/faq/faq.html) to get help of common problems. ## Roadmap @@ -85,4 +85,4 @@ You can read the mqtt protocol via the following links: ## License -Apache License 2.0, see [LICENSE](./LICENSE). \ No newline at end of file +Apache License 2.0, see [LICENSE](./LICENSE). From 94c56b5e31299f825e9c99b49beee94452c90a04 Mon Sep 17 00:00:00 2001 From: zhouzb Date: Wed, 21 Aug 2019 14:12:34 +0800 Subject: [PATCH 15/36] Remove redundant code and add test case --- src/emqx_config.erl | 111 +------------------------------------ test/emqx_config_SUITE.erl | 39 +++++++++++++ 2 files changed, 40 insertions(+), 110 deletions(-) create mode 100644 test/emqx_config_SUITE.erl diff --git a/src/emqx_config.erl b/src/emqx_config.erl index 3d4c33369..bef0302f6 100644 --- a/src/emqx_config.erl +++ b/src/emqx_config.erl @@ -14,33 +14,12 @@ %% limitations under the License. %%-------------------------------------------------------------------- -%% @doc Hot Configuration -%% -%% TODO: How to persist the configuration? -%% -%% 1. Store in mnesia database? -%% 2. Store in dets? -%% 3. Store in data/app.config? - -module(emqx_config). --export([populate/1]). - --export([ read/1 - , write/2 - , dump/2 - , reload/1 - ]). - --export([ set/3 - , get/2 - , get/3 - , get_env/1 +-export([ get_env/1 , get_env/2 ]). --type(env() :: {atom(), term()}). - -define(APP, emqx). %% @doc Get environment @@ -51,91 +30,3 @@ get_env(Key) -> -spec(get_env(Key :: atom(), Default :: term()) -> term()). get_env(Key, Default) -> application:get_env(?APP, Key, Default). - -%% TODO: -populate(_App) -> - ok. - -%% @doc Read the configuration of an application. --spec(read(atom()) -> {ok, list(env())} | {error, term()}). -read(App) -> - %% TODO: - %% 1. Read the app.conf from etc folder - %% 2. Cuttlefish to read the conf - %% 3. Return the terms and schema - % {error, unsupported}. - {ok, read_(App)}. - -%% @doc Reload configuration of an application. --spec(reload(atom()) -> ok | {error, term()}). -reload(_App) -> - %% TODO - %% 1. Read the app.conf from etc folder - %% 2. Cuttlefish to generate config terms. - %% 3. set/3 to apply the config - ok. - --spec(write(atom(), list(env())) -> ok | {error, term()}). -write(_App, _Terms) -> ok. - % Configs = lists:map(fun({Key, Val}) -> - % {cuttlefish_variable:tokenize(binary_to_list(Key)), binary_to_list(Val)} - % end, Terms), - % Path = lists:concat([code:priv_dir(App), "/", App, ".schema"]), - % Schema = cuttlefish_schema:files([Path]), - % case cuttlefish_generator:map(Schema, Configs) of - % [{App, Configs1}] -> - % emqx_cli_config:write_config(App, Configs), - % lists:foreach(fun({Key, Val}) -> application:set_env(App, Key, Val) end, Configs1); - % _ -> - % error - % end. - --spec(dump(atom(), list(env())) -> ok | {error, term()}). -dump(_App, _Terms) -> - %% TODO - ok. - --spec(set(atom(), list(), list()) -> ok). -set(_App, _Par, _Val) -> ok. - % emqx_cli_config:run(["config", - % "set", - % lists:concat([Par, "=", Val]), - % lists:concat(["--app=", App])]). - --spec(get(atom(), list()) -> undefined | {ok, term()}). -get(_App, _Par) -> error(no_impl). - % case emqx_cli_config:get_cfg(App, Par) of - % undefined -> undefined; - % Val -> {ok, Val} - % end. - --spec(get(atom(), list(), atom()) -> term()). -get(_App, _Par, _Def) -> error(no_impl). - % emqx_cli_config:get_cfg(App, Par, Def). - - -read_(_App) -> error(no_impl). - % Configs = emqx_cli_config:read_config(App), - % Path = lists:concat([code:priv_dir(App), "/", App, ".schema"]), - % case filelib:is_file(Path) of - % false -> - % []; - % true -> - % {_, Mappings, _} = cuttlefish_schema:files([Path]), - % OptionalCfg = lists:foldl(fun(Map, Acc) -> - % Key = cuttlefish_mapping:variable(Map), - % case proplists:get_value(Key, Configs) of - % undefined -> - % [{cuttlefish_variable:format(Key), "", cuttlefish_mapping:doc(Map), false} | Acc]; - % _ -> Acc - % end - % end, [], Mappings), - % RequiredCfg = lists:foldl(fun({Key, Val}, Acc) -> - % case lists:keyfind(Key, 2, Mappings) of - % false -> Acc; - % Map -> - % [{cuttlefish_variable:format(Key), Val, cuttlefish_mapping:doc(Map), true} | Acc] - % end - % end, [], Configs), - % RequiredCfg ++ OptionalCfg - % end. diff --git a/test/emqx_config_SUITE.erl b/test/emqx_config_SUITE.erl new file mode 100644 index 000000000..5014e3ef3 --- /dev/null +++ b/test/emqx_config_SUITE.erl @@ -0,0 +1,39 @@ +%%-------------------------------------------------------------------- +%% 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_config_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("eunit/include/eunit.hrl"). + +all() -> emqx_ct:all(?MODULE). + +init_per_suite(Config) -> + emqx_ct_helpers:start_apps([]), + Config. + +end_per_suite(_Config) -> + emqx_ct_helpers:stop_apps([]). + +t_get_env(_) -> + ?assertEqual(undefined, emqx_config:get_env(undefined_key)), + ?assertEqual(default_value, emqx_config:get_env(undefined_key, default_value)), + application:set_env(emqx, undefined_key, hello), + ?assertEqual(hello, emqx_config:get_env(undefined_key)), + ?assertEqual(hello, emqx_config:get_env(undefined_key, default_value)), + application:unset_env(emqx, undefined_key). \ No newline at end of file From 6d0c3c3684f67cbee5fb49cfd137b5d681c9162d Mon Sep 17 00:00:00 2001 From: CrazyWisdom Date: Tue, 20 Aug 2019 14:22:35 +0800 Subject: [PATCH 16/36] fix(README): fix FAQ url --- README-CN.md | 4 ++-- README.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README-CN.md b/README-CN.md index bdba664b4..5901eed1b 100644 --- a/README-CN.md +++ b/README-CN.md @@ -54,7 +54,7 @@ cd _rel/emqx && ./bin/emqx console ## FAQ -访问 [FAQ](https://docs.emqx.io/tutorial/zh/faq/faq.html) 以获取常见问题的帮助。 +访问 [FAQ](https://docs.emqx.io/tutorial/v3/cn/faq/faq.html) 以获取常见问题的帮助。 ## 产品路线 @@ -84,4 +84,4 @@ cd _rel/emqx && ./bin/emqx console ## 开源许可 -Apache License 2.0, 详见 [LICENSE](./LICENSE)。 \ No newline at end of file +Apache License 2.0, 详见 [LICENSE](./LICENSE)。 diff --git a/README.md b/README.md index 49de59d5a..c4bbaa60c 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ To view the dashboard after running, use your browser to open: http://localhost: ## FAQ -Visiting [FAQ](https://docs.emqx.io/tutorial/en/faq/faq.html) to get help of common problems. +Visiting [FAQ](https://docs.emqx.io/tutorial/v3/en/faq/faq.html) to get help of common problems. ## Roadmap @@ -85,4 +85,4 @@ You can read the mqtt protocol via the following links: ## License -Apache License 2.0, see [LICENSE](./LICENSE). \ No newline at end of file +Apache License 2.0, see [LICENSE](./LICENSE). From 3f20bcf58fdf35ee9b1b0dbc8c07dce8a0740e0d Mon Sep 17 00:00:00 2001 From: GilbertWong Date: Tue, 20 Aug 2019 10:21:20 +0800 Subject: [PATCH 17/36] Relace emqx_client with emqtt --- src/emqx_client.erl | 1250 -------------------------- src/emqx_client_sock.erl | 110 --- test/emqx_SUITE.erl | 6 +- test/emqx_alarm_handler_SUITE.erl | 9 +- test/emqx_client_SUITE.erl | 116 +-- test/emqx_flapping_SUITE.erl | 10 +- test/emqx_mod_subscription_SUITE.erl | 8 +- test/emqx_request_handler.erl | 16 +- test/emqx_request_sender.erl | 12 +- test/emqx_shared_sub_SUITE.erl | 38 +- test/emqx_sys_mon_SUITE.erl | 8 +- test/emqx_tracer_SUITE.erl | 8 +- test/emqx_ws_connection_SUITE.erl | 2 +- 13 files changed, 116 insertions(+), 1477 deletions(-) delete mode 100644 src/emqx_client.erl delete mode 100644 src/emqx_client_sock.erl diff --git a/src/emqx_client.erl b/src/emqx_client.erl deleted file mode 100644 index 655218329..000000000 --- a/src/emqx_client.erl +++ /dev/null @@ -1,1250 +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_client). - --behaviour(gen_statem). - --include("logger.hrl"). --include("types.hrl"). --include("emqx_mqtt.hrl"). - --logger_header("[Client]"). - --export([ start_link/0 - , start_link/1 - ]). - --export([ connect/1 - , disconnect/1 - , disconnect/2 - , disconnect/3 - ]). - --export([ping/1]). - -%% PubSub --export([ subscribe/2 - , subscribe/3 - , subscribe/4 - , publish/2 - , publish/3 - , publish/4 - , publish/5 - , unsubscribe/2 - , unsubscribe/3 - ]). - -%% Puback... --export([ puback/2 - , puback/3 - , puback/4 - , pubrec/2 - , pubrec/3 - , pubrec/4 - , pubrel/2 - , pubrel/3 - , pubrel/4 - , pubcomp/2 - , pubcomp/3 - , pubcomp/4 - ]). - --export([subscriptions/1]). - --export([info/1, stop/1]). - -%% For test cases --export([pause/1, resume/1]). - --export([ initialized/3 - , waiting_for_connack/3 - , connected/3 - , inflight_full/3 - ]). - --export([ init/1 - , callback_mode/0 - , handle_event/4 - , terminate/3 - , code_change/4 - ]). - --export_type([ host/0 - , client/0 - , option/0 - , properties/0 - , payload/0 - , pubopt/0 - , subopt/0 - , mqtt_msg/0 - ]). - -%% Default timeout --define(DEFAULT_KEEPALIVE, 60). --define(DEFAULT_ACK_TIMEOUT, 30000). --define(DEFAULT_CONNECT_TIMEOUT, 60000). - --define(PROPERTY(Name, Val), #state{properties = #{Name := Val}}). - --define(WILL_MSG(QoS, Retain, Topic, Props, Payload), - #mqtt_msg{qos = QoS, retain = Retain, topic = Topic, props = Props, payload = Payload}). - --define(NO_CLIENT_ID, <<>>). - --type(host() :: inet:ip_address() | inet:hostname()). - -%% Message handler is a set of callbacks defined to handle MQTT messages -%% as well as the disconnect event. --define(NO_MSG_HDLR, undefined). --type(msg_handler() :: #{puback := fun((_) -> any()), - publish := fun((emqx_types:message()) -> any()), - disconnected := fun(({reason_code(), _Properties :: term()}) -> any()) - }). - --type(option() :: {name, atom()} - | {owner, pid()} - | {msg_handler, msg_handler()} - | {host, host()} - | {hosts, [{host(), inet:port_number()}]} - | {port, inet:port_number()} - | {tcp_opts, [gen_tcp:option()]} - | {ssl, boolean()} - | {ssl_opts, [ssl:ssl_option()]} - | {connect_timeout, pos_integer()} - | {bridge_mode, boolean()} - | {client_id, iodata()} - | {clean_start, boolean()} - | {username, iodata()} - | {password, iodata()} - | {proto_ver, v3 | v4 | v5} - | {keepalive, non_neg_integer()} - | {max_inflight, pos_integer()} - | {retry_interval, timeout()} - | {will_topic, iodata()} - | {will_payload, iodata()} - | {will_retain, boolean()} - | {will_qos, qos()} - | {will_props, properties()} - | {auto_ack, boolean()} - | {ack_timeout, pos_integer()} - | {force_ping, boolean()} - | {properties, properties()}). - - - --opaque(mqtt_msg() :: #mqtt_msg{}). - --record(state, {name :: atom(), - owner :: pid(), - msg_handler :: ?NO_MSG_HDLR | msg_handler(), - host :: host(), - port :: inet:port_number(), - hosts :: [{host(), inet:port_number()}], - socket :: inet:socket(), - sock_opts :: [emqx_client_sock:option()], - connect_timeout :: pos_integer(), - bridge_mode :: boolean(), - client_id :: binary(), - clean_start :: boolean(), - username :: maybe(binary()), - password :: maybe(binary()), - proto_ver :: emqx_types:mqtt_ver(), - proto_name :: iodata(), - keepalive :: non_neg_integer(), - keepalive_timer :: maybe(reference()), - force_ping :: boolean(), - paused :: boolean(), - will_flag :: boolean(), - will_msg :: mqtt_msg(), - properties :: properties(), - pending_calls :: list(), - subscriptions :: map(), - max_inflight :: infinity | pos_integer(), - inflight :: emqx_inflight:inflight(), - awaiting_rel :: map(), - auto_ack :: boolean(), - ack_timeout :: pos_integer(), - ack_timer :: reference(), - retry_interval :: pos_integer(), - retry_timer :: reference(), - session_present :: boolean(), - last_packet_id :: packet_id(), - parse_state :: emqx_frame:state() - }). - --record(call, {id, from, req, ts}). - --type(client() :: pid() | atom()). - --type(topic() :: emqx_topic:topic()). - --type(payload() :: iodata()). - --type(packet_id() :: emqx_types:packet_id()). - --type(properties() :: emqx_types:properties()). - --type(qos() :: emqx_types:qos_name() | emqx_types:qos()). - --type(pubopt() :: {retain, boolean()} | {qos, qos()} | {timeout, timeout()}). - --type(subopt() :: {rh, 0 | 1 | 2} - | {rap, boolean()} - | {nl, boolean()} - | {qos, qos()}). - --type(reason_code() :: emqx_types:reason_code()). - --type(subscribe_ret() :: {ok, properties(), [reason_code()]} | {error, term()}). - -%%-------------------------------------------------------------------- -%% API -%%-------------------------------------------------------------------- - --spec(start_link() -> gen_statem:start_ret()). -start_link() -> start_link([]). - --spec(start_link(map() | [option()]) -> gen_statem:start_ret()). -start_link(Options) when is_map(Options) -> - start_link(maps:to_list(Options)); -start_link(Options) when is_list(Options) -> - ok = emqx_mqtt_props:validate( - proplists:get_value(properties, Options, #{})), - case proplists:get_value(name, Options) of - undefined -> - gen_statem:start_link(?MODULE, [with_owner(Options)], []); - Name when is_atom(Name) -> - gen_statem:start_link({local, Name}, ?MODULE, [with_owner(Options)], []) - end. - -with_owner(Options) -> - case proplists:get_value(owner, Options) of - Owner when is_pid(Owner) -> Options; - undefined -> [{owner, self()} | Options] - end. - --spec(connect(client()) -> {ok, properties()} | {error, term()}). -connect(Client) -> - gen_statem:call(Client, connect, infinity). - --spec(subscribe(client(), topic() | {topic(), qos() | [subopt()]} | [{topic(), qos()}]) - -> subscribe_ret()). -subscribe(Client, Topic) when is_binary(Topic) -> - subscribe(Client, {Topic, ?QOS_0}); -subscribe(Client, {Topic, QoS}) when is_binary(Topic), is_atom(QoS) -> - subscribe(Client, {Topic, ?QOS_I(QoS)}); -subscribe(Client, {Topic, QoS}) when is_binary(Topic), ?IS_QOS(QoS) -> - subscribe(Client, [{Topic, ?QOS_I(QoS)}]); -subscribe(Client, Topics) when is_list(Topics) -> - subscribe(Client, #{}, lists:map( - fun({Topic, QoS}) when is_binary(Topic), is_atom(QoS) -> - {Topic, [{qos, ?QOS_I(QoS)}]}; - ({Topic, QoS}) when is_binary(Topic), ?IS_QOS(QoS) -> - {Topic, [{qos, ?QOS_I(QoS)}]}; - ({Topic, Opts}) when is_binary(Topic), is_list(Opts) -> - {Topic, Opts} - end, Topics)). - --spec(subscribe(client(), topic(), qos() | [subopt()]) -> - subscribe_ret(); - (client(), properties(), [{topic(), qos() | [subopt()]}]) -> - subscribe_ret()). -subscribe(Client, Topic, QoS) when is_binary(Topic), is_atom(QoS) -> - subscribe(Client, Topic, ?QOS_I(QoS)); -subscribe(Client, Topic, QoS) when is_binary(Topic), ?IS_QOS(QoS) -> - subscribe(Client, Topic, [{qos, QoS}]); -subscribe(Client, Topic, Opts) when is_binary(Topic), is_list(Opts) -> - subscribe(Client, #{}, [{Topic, Opts}]); -subscribe(Client, Properties, Topics) when is_map(Properties), is_list(Topics) -> - Topics1 = [{Topic, parse_subopt(Opts)} || {Topic, Opts} <- Topics], - gen_statem:call(Client, {subscribe, Properties, Topics1}). - --spec(subscribe(client(), properties(), topic(), qos() | [subopt()]) - -> subscribe_ret()). -subscribe(Client, Properties, Topic, QoS) - when is_map(Properties), is_binary(Topic), is_atom(QoS) -> - subscribe(Client, Properties, Topic, ?QOS_I(QoS)); -subscribe(Client, Properties, Topic, QoS) - when is_map(Properties), is_binary(Topic), ?IS_QOS(QoS) -> - subscribe(Client, Properties, Topic, [{qos, QoS}]); -subscribe(Client, Properties, Topic, Opts) - when is_map(Properties), is_binary(Topic), is_list(Opts) -> - subscribe(Client, Properties, [{Topic, Opts}]). - -parse_subopt(Opts) -> - parse_subopt(Opts, #{rh => 0, rap => 0, nl => 0, qos => ?QOS_0}). - -parse_subopt([], Result) -> - Result; -parse_subopt([{rh, I} | Opts], Result) when I >= 0, I =< 2 -> - parse_subopt(Opts, Result#{rh := I}); -parse_subopt([{rap, true} | Opts], Result) -> - parse_subopt(Opts, Result#{rap := 1}); -parse_subopt([{rap, false} | Opts], Result) -> - parse_subopt(Opts, Result#{rap := 0}); -parse_subopt([{nl, true} | Opts], Result) -> - parse_subopt(Opts, Result#{nl := 1}); -parse_subopt([{nl, false} | Opts], Result) -> - parse_subopt(Opts, Result#{nl := 0}); -parse_subopt([{qos, QoS} | Opts], Result) -> - parse_subopt(Opts, Result#{qos := ?QOS_I(QoS)}). - --spec(publish(client(), topic(), payload()) -> ok | {error, term()}). -publish(Client, Topic, Payload) when is_binary(Topic) -> - publish(Client, #mqtt_msg{topic = Topic, qos = ?QOS_0, payload = iolist_to_binary(Payload)}). - --spec(publish(client(), topic(), payload(), qos() | [pubopt()]) - -> ok | {ok, packet_id()} | {error, term()}). -publish(Client, Topic, Payload, QoS) when is_binary(Topic), is_atom(QoS) -> - publish(Client, Topic, Payload, [{qos, ?QOS_I(QoS)}]); -publish(Client, Topic, Payload, QoS) when is_binary(Topic), ?IS_QOS(QoS) -> - publish(Client, Topic, Payload, [{qos, QoS}]); -publish(Client, Topic, Payload, Opts) when is_binary(Topic), is_list(Opts) -> - publish(Client, Topic, #{}, Payload, Opts). - --spec(publish(client(), topic(), properties(), payload(), [pubopt()]) - -> ok | {ok, packet_id()} | {error, term()}). -publish(Client, Topic, Properties, Payload, Opts) - when is_binary(Topic), is_map(Properties), is_list(Opts) -> - ok = emqx_mqtt_props:validate(Properties), - Retain = proplists:get_bool(retain, Opts), - QoS = ?QOS_I(proplists:get_value(qos, Opts, ?QOS_0)), - publish(Client, #mqtt_msg{qos = QoS, - retain = Retain, - topic = Topic, - props = Properties, - payload = iolist_to_binary(Payload)}). - --spec(publish(client(), #mqtt_msg{}) -> ok | {ok, packet_id()} | {error, term()}). -publish(Client, Msg) -> - gen_statem:call(Client, {publish, Msg}). - --spec(unsubscribe(client(), topic() | [topic()]) -> subscribe_ret()). -unsubscribe(Client, Topic) when is_binary(Topic) -> - unsubscribe(Client, [Topic]); -unsubscribe(Client, Topics) when is_list(Topics) -> - unsubscribe(Client, #{}, Topics). - --spec(unsubscribe(client(), properties(), topic() | [topic()]) -> subscribe_ret()). -unsubscribe(Client, Properties, Topic) when is_map(Properties), is_binary(Topic) -> - unsubscribe(Client, Properties, [Topic]); -unsubscribe(Client, Properties, Topics) when is_map(Properties), is_list(Topics) -> - gen_statem:call(Client, {unsubscribe, Properties, Topics}). - --spec(ping(client()) -> pong). -ping(Client) -> - gen_statem:call(Client, ping). - --spec(disconnect(client()) -> ok). -disconnect(Client) -> - disconnect(Client, ?RC_SUCCESS). - --spec(disconnect(client(), reason_code()) -> ok). -disconnect(Client, ReasonCode) -> - disconnect(Client, ReasonCode, #{}). - --spec(disconnect(client(), reason_code(), properties()) -> ok). -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). -puback(Client, PacketId, ReasonCode) - when is_integer(PacketId), is_integer(ReasonCode) -> - puback(Client, PacketId, ReasonCode, #{}). -puback(Client, PacketId, ReasonCode, Properties) - when is_integer(PacketId), is_integer(ReasonCode), is_map(Properties) -> - gen_statem:cast(Client, {puback, PacketId, ReasonCode, Properties}). - -pubrec(Client, PacketId) when is_integer(PacketId) -> - pubrec(Client, PacketId, ?RC_SUCCESS). -pubrec(Client, PacketId, ReasonCode) - when is_integer(PacketId), is_integer(ReasonCode) -> - pubrec(Client, PacketId, ReasonCode, #{}). -pubrec(Client, PacketId, ReasonCode, Properties) - when is_integer(PacketId), is_integer(ReasonCode), is_map(Properties) -> - gen_statem:cast(Client, {pubrec, PacketId, ReasonCode, Properties}). - -pubrel(Client, PacketId) when is_integer(PacketId) -> - pubrel(Client, PacketId, ?RC_SUCCESS). -pubrel(Client, PacketId, ReasonCode) - when is_integer(PacketId), is_integer(ReasonCode) -> - pubrel(Client, PacketId, ReasonCode, #{}). -pubrel(Client, PacketId, ReasonCode, Properties) - when is_integer(PacketId), is_integer(ReasonCode), is_map(Properties) -> - gen_statem:cast(Client, {pubrel, PacketId, ReasonCode, Properties}). - -pubcomp(Client, PacketId) when is_integer(PacketId) -> - pubcomp(Client, PacketId, ?RC_SUCCESS). -pubcomp(Client, PacketId, ReasonCode) - when is_integer(PacketId), is_integer(ReasonCode) -> - pubcomp(Client, PacketId, ReasonCode, #{}). -pubcomp(Client, PacketId, ReasonCode, Properties) - when is_integer(PacketId), is_integer(ReasonCode), is_map(Properties) -> - gen_statem:cast(Client, {pubcomp, PacketId, ReasonCode, Properties}). - -subscriptions(Client) -> - gen_statem:call(Client, subscriptions). - -info(Client) -> - gen_statem:call(Client, info). - -stop(Client) -> - gen_statem:call(Client, stop). - -pause(Client) -> - gen_statem:call(Client, pause). - -resume(Client) -> - gen_statem:call(Client, resume). - -%%-------------------------------------------------------------------- -%% gen_statem callbacks -%%-------------------------------------------------------------------- - -init([Options]) -> - process_flag(trap_exit, true), - ClientId = case {proplists:get_value(proto_ver, Options, v4), - proplists:get_value(client_id, Options)} of - {v5, undefined} -> ?NO_CLIENT_ID; - {_ver, undefined} -> random_client_id(); - {_ver, Id} -> iolist_to_binary(Id) - end, - State = init(Options, #state{host = {127,0,0,1}, - port = 1883, - hosts = [], - sock_opts = [], - bridge_mode = false, - client_id = ClientId, - clean_start = true, - proto_ver = ?MQTT_PROTO_V4, - proto_name = <<"MQTT">>, - keepalive = ?DEFAULT_KEEPALIVE, - force_ping = false, - paused = false, - will_flag = false, - will_msg = #mqtt_msg{}, - pending_calls = [], - subscriptions = #{}, - max_inflight = infinity, - inflight = emqx_inflight:new(0), - awaiting_rel = #{}, - properties = #{}, - auto_ack = true, - ack_timeout = ?DEFAULT_ACK_TIMEOUT, - retry_interval = 0, - connect_timeout = ?DEFAULT_CONNECT_TIMEOUT, - last_packet_id = 1 - }), - {ok, initialized, init_parse_state(State)}. - -random_client_id() -> - rand:seed(exsplus, erlang:timestamp()), - I1 = rand:uniform(round(math:pow(2, 48))) - 1, - I2 = rand:uniform(round(math:pow(2, 32))) - 1, - {ok, Host} = inet:gethostname(), - iolist_to_binary(["emqx-client-", Host, "-", io_lib:format("~12.16.0b~8.16.0b", [I1, I2])]). - -init([], State) -> - State; -init([{name, Name} | Opts], State) -> - init(Opts, State#state{name = Name}); -init([{owner, Owner} | Opts], State) when is_pid(Owner) -> - link(Owner), - init(Opts, State#state{owner = Owner}); -init([{msg_handler, Hdlr} | Opts], State) -> - init(Opts, State#state{msg_handler = Hdlr}); -init([{host, Host} | Opts], State) -> - init(Opts, State#state{host = Host}); -init([{port, Port} | Opts], State) -> - init(Opts, State#state{port = Port}); -init([{hosts, Hosts} | Opts], State) -> - Hosts1 = - lists:foldl(fun({Host, Port}, Acc) -> - [{Host, Port}|Acc]; - (Host, Acc) -> - [{Host, 1883}|Acc] - end, [], Hosts), - init(Opts, State#state{hosts = Hosts1}); -init([{tcp_opts, TcpOpts} | Opts], State = #state{sock_opts = SockOpts}) -> - init(Opts, State#state{sock_opts = emqx_misc:merge_opts(SockOpts, TcpOpts)}); -init([{ssl, EnableSsl} | Opts], State) -> - case lists:keytake(ssl_opts, 1, Opts) of - {value, SslOpts, WithOutSslOpts} -> - init([SslOpts, {ssl, EnableSsl}| WithOutSslOpts], State); - false -> - init([{ssl_opts, []}, {ssl, EnableSsl}| Opts], State) - end; -init([{ssl_opts, SslOpts} | Opts], State = #state{sock_opts = SockOpts}) -> - case lists:keytake(ssl, 1, Opts) of - {value, {ssl, true}, WithOutEnableSsl} -> - ok = ssl:start(), - SockOpts1 = emqx_misc:merge_opts(SockOpts, [{ssl_opts, SslOpts}]), - init(WithOutEnableSsl, State#state{sock_opts = SockOpts1}); - {value, {ssl, false}, WithOutEnableSsl} -> - init(WithOutEnableSsl, State); - false -> - init(Opts, State) - end; -init([{client_id, ClientId} | Opts], State) -> - init(Opts, State#state{client_id = iolist_to_binary(ClientId)}); -init([{clean_start, CleanStart} | Opts], State) when is_boolean(CleanStart) -> - init(Opts, State#state{clean_start = CleanStart}); -init([{username, Username} | Opts], State) -> - init(Opts, State#state{username = iolist_to_binary(Username)}); -init([{password, Password} | Opts], State) -> - init(Opts, State#state{password = iolist_to_binary(Password)}); -init([{keepalive, Secs} | Opts], State) -> - init(Opts, State#state{keepalive = Secs}); -init([{proto_ver, v3} | Opts], State) -> - init(Opts, State#state{proto_ver = ?MQTT_PROTO_V3, - proto_name = <<"MQIsdp">>}); -init([{proto_ver, v4} | Opts], State) -> - init(Opts, State#state{proto_ver = ?MQTT_PROTO_V4, - proto_name = <<"MQTT">>}); -init([{proto_ver, v5} | Opts], State) -> - init(Opts, State#state{proto_ver = ?MQTT_PROTO_V5, - proto_name = <<"MQTT">>}); -init([{will_topic, Topic} | Opts], State = #state{will_msg = WillMsg}) -> - WillMsg1 = init_will_msg({topic, Topic}, WillMsg), - init(Opts, State#state{will_flag = true, will_msg = WillMsg1}); -init([{will_props, Properties} | Opts], State = #state{will_msg = WillMsg}) -> - init(Opts, State#state{will_msg = init_will_msg({props, Properties}, WillMsg)}); -init([{will_payload, Payload} | Opts], State = #state{will_msg = WillMsg}) -> - init(Opts, State#state{will_msg = init_will_msg({payload, Payload}, WillMsg)}); -init([{will_retain, Retain} | Opts], State = #state{will_msg = WillMsg}) -> - init(Opts, State#state{will_msg = init_will_msg({retain, Retain}, WillMsg)}); -init([{will_qos, QoS} | Opts], State = #state{will_msg = WillMsg}) -> - init(Opts, State#state{will_msg = init_will_msg({qos, QoS}, WillMsg)}); -init([{connect_timeout, Timeout}| Opts], State) -> - init(Opts, State#state{connect_timeout = timer:seconds(Timeout)}); -init([{ack_timeout, Timeout}| Opts], State) -> - init(Opts, State#state{ack_timeout = timer:seconds(Timeout)}); -init([force_ping | Opts], State) -> - init(Opts, State#state{force_ping = true}); -init([{force_ping, ForcePing} | Opts], State) when is_boolean(ForcePing) -> - init(Opts, State#state{force_ping = ForcePing}); -init([{properties, Properties} | Opts], State = #state{properties = InitProps}) -> - init(Opts, State#state{properties = maps:merge(InitProps, Properties)}); -init([{max_inflight, infinity} | Opts], State) -> - init(Opts, State#state{max_inflight = infinity, - inflight = emqx_inflight:new(0)}); -init([{max_inflight, I} | Opts], State) when is_integer(I) -> - init(Opts, State#state{max_inflight = I, - inflight = emqx_inflight:new(I)}); -init([auto_ack | Opts], State) -> - init(Opts, State#state{auto_ack = true}); -init([{auto_ack, AutoAck} | Opts], State) when is_boolean(AutoAck) -> - init(Opts, State#state{auto_ack = AutoAck}); -init([{retry_interval, I} | Opts], State) -> - init(Opts, State#state{retry_interval = timer:seconds(I)}); -init([{bridge_mode, Mode} | Opts], State) when is_boolean(Mode) -> - init(Opts, State#state{bridge_mode = Mode}); -init([_Opt | Opts], State) -> - init(Opts, State). - -init_will_msg({topic, Topic}, WillMsg) -> - WillMsg#mqtt_msg{topic = iolist_to_binary(Topic)}; -init_will_msg({props, Props}, WillMsg) -> - WillMsg#mqtt_msg{props = Props}; -init_will_msg({payload, Payload}, WillMsg) -> - WillMsg#mqtt_msg{payload = iolist_to_binary(Payload)}; -init_will_msg({retain, Retain}, WillMsg) when is_boolean(Retain) -> - WillMsg#mqtt_msg{retain = Retain}; -init_will_msg({qos, QoS}, WillMsg) -> - WillMsg#mqtt_msg{qos = ?QOS_I(QoS)}. - -init_parse_state(State = #state{proto_ver = Ver, properties = Properties}) -> - 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. - -initialized({call, From}, connect, State = #state{sock_opts = SockOpts, - connect_timeout = Timeout}) -> - case sock_connect(hosts(State), SockOpts, Timeout) of - {ok, Sock} -> - case mqtt_connect(run_sock(State#state{socket = Sock})) of - {ok, NewState} -> - {next_state, waiting_for_connack, - add_call(new_call(connect, From), NewState), [Timeout]}; - Error = {error, Reason} -> - {stop_and_reply, Reason, [{reply, From, Error}]} - end; - Error = {error, Reason} -> - {stop_and_reply, Reason, [{reply, From, Error}]} - end; - -initialized(EventType, EventContent, State) -> - handle_event(EventType, EventContent, initialized, State). - -mqtt_connect(State = #state{client_id = ClientId, - clean_start = CleanStart, - bridge_mode = IsBridge, - username = Username, - password = Password, - proto_ver = ProtoVer, - proto_name = ProtoName, - keepalive = KeepAlive, - will_flag = WillFlag, - will_msg = WillMsg, - properties = Properties}) -> - ?WILL_MSG(WillQoS, WillRetain, WillTopic, WillProps, WillPayload) = WillMsg, - ConnProps = emqx_mqtt_props:filter(?CONNECT, Properties), - send(?CONNECT_PACKET( - #mqtt_packet_connect{proto_ver = ProtoVer, - proto_name = ProtoName, - is_bridge = IsBridge, - clean_start = CleanStart, - will_flag = WillFlag, - will_qos = WillQoS, - will_retain = WillRetain, - keepalive = KeepAlive, - properties = ConnProps, - client_id = ClientId, - will_props = WillProps, - will_topic = WillTopic, - will_payload = WillPayload, - username = Username, - password = Password}), State). - -waiting_for_connack(cast, ?CONNACK_PACKET(?RC_SUCCESS, - SessPresent, - Properties), - State = #state{properties = AllProps, - client_id = ClientId}) -> - case take_call(connect, State) of - {value, #call{from = From}, State1} -> - AllProps1 = case Properties of - undefined -> AllProps; - _ -> maps:merge(AllProps, Properties) - end, - Reply = {ok, Properties}, - State2 = State1#state{client_id = assign_id(ClientId, AllProps1), - properties = AllProps1, - session_present = SessPresent}, - {next_state, connected, ensure_keepalive_timer(State2), - [{reply, From, Reply}]}; - false -> - {stop, bad_connack} - end; - -waiting_for_connack(cast, ?CONNACK_PACKET(ReasonCode, - _SessPresent, - Properties), - State = #state{proto_ver = ProtoVer}) -> - Reason = emqx_reason_codes:name(ReasonCode, ProtoVer), - case take_call(connect, State) of - {value, #call{from = From}, _State} -> - Reply = {error, {Reason, Properties}}, - {stop_and_reply, {shutdown, Reason}, [{reply, From, Reply}]}; - false -> {stop, connack_error} - end; - -waiting_for_connack(timeout, _Timeout, State) -> - case take_call(connect, State) of - {value, #call{from = From}, _State} -> - Reply = {error, connack_timeout}, - {stop_and_reply, connack_timeout, [{reply, From, Reply}]}; - false -> {stop, connack_timeout} - end; - -waiting_for_connack(EventType, EventContent, State) -> - case take_call(connect, State) of - {value, #call{from = From}, _State} -> - case handle_event(EventType, EventContent, waiting_for_connack, State) of - {stop, Reason, State} -> - Reply = {error, {Reason, EventContent}}, - {stop_and_reply, Reason, [{reply, From, Reply}]}; - StateCallbackResult -> - StateCallbackResult - end; - false -> {stop, connack_timeout} - end. - -connected({call, From}, subscriptions, #state{subscriptions = Subscriptions}) -> - {keep_state_and_data, [{reply, From, maps:to_list(Subscriptions)}]}; - -connected({call, From}, info, State) -> - Info = lists:zip(record_info(fields, state), tl(tuple_to_list(State))), - {keep_state_and_data, [{reply, From, Info}]}; - -connected({call, From}, pause, State) -> - {keep_state, State#state{paused = true}, [{reply, From, ok}]}; - -connected({call, From}, resume, State) -> - {keep_state, State#state{paused = false}, [{reply, From, ok}]}; - -connected({call, From}, client_id, #state{client_id = ClientId}) -> - {keep_state_and_data, [{reply, From, ClientId}]}; - -connected({call, From}, SubReq = {subscribe, Properties, Topics}, - State = #state{last_packet_id = PacketId, subscriptions = Subscriptions}) -> - case send(?SUBSCRIBE_PACKET(PacketId, Properties, Topics), State) of - {ok, NewState} -> - Call = new_call({subscribe, PacketId}, From, SubReq), - Subscriptions1 = - lists:foldl(fun({Topic, Opts}, Acc) -> - maps:put(Topic, Opts, Acc) - end, Subscriptions, Topics), - {keep_state, ensure_ack_timer(add_call(Call,NewState#state{subscriptions = Subscriptions1}))}; - Error = {error, Reason} -> - {stop_and_reply, Reason, [{reply, From, Error}]} - end; - -connected({call, From}, {publish, Msg = #mqtt_msg{qos = ?QOS_0}}, State) -> - case send(Msg, State) of - {ok, NewState} -> - {keep_state, NewState, [{reply, From, ok}]}; - Error = {error, Reason} -> - {stop_and_reply, Reason, [{reply, From, Error}]} - end; - -connected({call, From}, {publish, Msg = #mqtt_msg{qos = QoS}}, - State = #state{inflight = Inflight, last_packet_id = PacketId}) - when (QoS =:= ?QOS_1); (QoS =:= ?QOS_2) -> - Msg1 = Msg#mqtt_msg{packet_id = PacketId}, - case send(Msg1, State) of - {ok, NewState} -> - Inflight1 = emqx_inflight:insert(PacketId, {publish, Msg1, os:timestamp()}, Inflight), - State1 = ensure_retry_timer(NewState#state{inflight = Inflight1}), - Actions = [{reply, From, {ok, PacketId}}], - case emqx_inflight:is_full(Inflight1) of - true -> {next_state, inflight_full, State1, Actions}; - false -> {keep_state, State1, Actions} - end; - {error, Reason} -> - {stop_and_reply, Reason, [{reply, From, {error, {PacketId, Reason}}}]} - end; - -connected({call, From}, UnsubReq = {unsubscribe, Properties, Topics}, - State = #state{last_packet_id = PacketId}) -> - case send(?UNSUBSCRIBE_PACKET(PacketId, Properties, Topics), State) of - {ok, NewState} -> - Call = new_call({unsubscribe, PacketId}, From, UnsubReq), - {keep_state, ensure_ack_timer(add_call(Call, NewState))}; - Error = {error, Reason} -> - {stop_and_reply, Reason, [{reply, From, Error}]} - end; - -connected({call, From}, ping, State) -> - case send(?PACKET(?PINGREQ), State) of - {ok, NewState} -> - Call = new_call(ping, From), - {keep_state, ensure_ack_timer(add_call(Call, NewState))}; - Error = {error, Reason} -> - {stop_and_reply, Reason, [{reply, From, Error}]} - end; - -connected({call, From}, {disconnect, ReasonCode, Properties}, State) -> - case send(?DISCONNECT_PACKET(ReasonCode, Properties), State) of - {ok, NewState} -> - {stop_and_reply, normal, [{reply, From, ok}], NewState}; - Error = {error, Reason} -> - {stop_and_reply, Reason, [{reply, From, Error}]} - end; - -connected(cast, {puback, PacketId, ReasonCode, Properties}, State) -> - send_puback(?PUBACK_PACKET(PacketId, ReasonCode, Properties), State); - -connected(cast, {pubrec, PacketId, ReasonCode, Properties}, State) -> - send_puback(?PUBREC_PACKET(PacketId, ReasonCode, Properties), State); - -connected(cast, {pubrel, PacketId, ReasonCode, Properties}, State) -> - send_puback(?PUBREL_PACKET(PacketId, ReasonCode, Properties), State); - -connected(cast, {pubcomp, PacketId, ReasonCode, Properties}, State) -> - send_puback(?PUBCOMP_PACKET(PacketId, ReasonCode, Properties), State); - -connected(cast, ?PUBLISH_PACKET(_QoS, _PacketId), #state{paused = true}) -> - keep_state_and_data; - -connected(cast, Packet = ?PUBLISH_PACKET(?QOS_0, _PacketId), State) -> - {keep_state, deliver(packet_to_msg(Packet), State)}; - -connected(cast, Packet = ?PUBLISH_PACKET(?QOS_1, _PacketId), State) -> - publish_process(?QOS_1, Packet, State); - -connected(cast, Packet = ?PUBLISH_PACKET(?QOS_2, _PacketId), State) -> - publish_process(?QOS_2, Packet, State); - -connected(cast, ?PUBACK_PACKET(_PacketId, _ReasonCode, _Properties) = PubAck, State) -> - {keep_state, delete_inflight(PubAck, State)}; - -connected(cast, ?PUBREC_PACKET(PacketId), State = #state{inflight = Inflight}) -> - send_puback(?PUBREL_PACKET(PacketId), - case emqx_inflight:lookup(PacketId, Inflight) of - {value, {publish, _Msg, _Ts}} -> - Inflight1 = emqx_inflight:update(PacketId, {pubrel, PacketId, os:timestamp()}, Inflight), - State#state{inflight = Inflight1}; - {value, {pubrel, _Ref, _Ts}} -> - ?LOG(notice, "Duplicated PUBREC Packet: ~p", [PacketId]), - State; - none -> - ?LOG(warning, "Unexpected PUBREC Packet: ~p", [PacketId]), - State - end); - -%%TODO::... if auto_ack is false, should we take PacketId from the map? -connected(cast, ?PUBREL_PACKET(PacketId), - State = #state{awaiting_rel = AwaitingRel, auto_ack = AutoAck}) -> - case maps:take(PacketId, AwaitingRel) of - {Packet, AwaitingRel1} -> - NewState = deliver(packet_to_msg(Packet), State#state{awaiting_rel = AwaitingRel1}), - case AutoAck of - true -> send_puback(?PUBCOMP_PACKET(PacketId), NewState); - false -> {keep_state, NewState} - end; - error -> - ?LOG(warning, "Unexpected PUBREL: ~p", [PacketId]), - keep_state_and_data - end; - -connected(cast, ?PUBCOMP_PACKET(_PacketId, _ReasonCode, _Properties) = PubComp, State) -> - {keep_state, delete_inflight(PubComp, State)}; - -connected(cast, ?SUBACK_PACKET(PacketId, Properties, ReasonCodes), - State = #state{subscriptions = _Subscriptions}) -> - case take_call({subscribe, PacketId}, State) of - {value, #call{from = From}, NewState} -> - %%TODO: Merge reason codes to subscriptions? - Reply = {ok, Properties, ReasonCodes}, - {keep_state, NewState, [{reply, From, Reply}]}; - false -> - keep_state_and_data - end; - -connected(cast, ?UNSUBACK_PACKET(PacketId, Properties, ReasonCodes), - State = #state{subscriptions = Subscriptions}) -> - case take_call({unsubscribe, PacketId}, State) of - {value, #call{from = From, req = {_, _, Topics}}, NewState} -> - Subscriptions1 = - lists:foldl(fun(Topic, Acc) -> - maps:remove(Topic, Acc) - end, Subscriptions, Topics), - {keep_state, NewState#state{subscriptions = Subscriptions1}, - [{reply, From, {ok, Properties, ReasonCodes}}]}; - false -> - keep_state_and_data - end; - -connected(cast, ?PACKET(?PINGRESP), #state{pending_calls = []}) -> - keep_state_and_data; -connected(cast, ?PACKET(?PINGRESP), State) -> - case take_call(ping, State) of - {value, #call{from = From}, NewState} -> - {keep_state, NewState, [{reply, From, pong}]}; - false -> - keep_state_and_data - end; - -connected(cast, ?DISCONNECT_PACKET(ReasonCode, Properties), State) -> - {stop, {disconnected, ReasonCode, Properties}, State}; - -connected(info, {timeout, _TRef, keepalive}, State = #state{force_ping = true}) -> - case send(?PACKET(?PINGREQ), State) of - {ok, NewState} -> - {keep_state, ensure_keepalive_timer(NewState)}; - Error -> {stop, Error} - end; - -connected(info, {timeout, TRef, keepalive}, - State = #state{socket = Sock, paused = Paused, keepalive_timer = TRef}) -> - case (not Paused) andalso should_ping(Sock) of - true -> - case send(?PACKET(?PINGREQ), State) of - {ok, NewState} -> - {keep_state, ensure_keepalive_timer(NewState), [hibernate]}; - Error -> {stop, Error} - end; - false -> - {keep_state, ensure_keepalive_timer(State), [hibernate]}; - {error, Reason} -> - {stop, Reason} - end; - -connected(info, {timeout, TRef, ack}, State = #state{ack_timer = TRef, - ack_timeout = Timeout, - pending_calls = Calls}) -> - NewState = State#state{ack_timer = undefined, - pending_calls = timeout_calls(Timeout, Calls)}, - {keep_state, ensure_ack_timer(NewState)}; - -connected(info, {timeout, TRef, retry}, State = #state{retry_timer = TRef, - inflight = Inflight}) -> - case emqx_inflight:is_empty(Inflight) of - true -> {keep_state, State#state{retry_timer = undefined}}; - false -> retry_send(State) - end; - -connected(EventType, EventContent, Data) -> - handle_event(EventType, EventContent, connected, Data). - -inflight_full({call, _From}, {publish, #mqtt_msg{qos = QoS}}, _State) when (QoS =:= ?QOS_1); (QoS =:= ?QOS_2) -> - {keep_state_and_data, [postpone]}; -inflight_full(cast, ?PUBACK_PACKET(_PacketId, _ReasonCode, _Properties) = PubAck, State) -> - delete_inflight_when_full(PubAck, State); -inflight_full(cast, ?PUBCOMP_PACKET(_PacketId, _ReasonCode, _Properties) = PubComp, State) -> - delete_inflight_when_full(PubComp, State); -inflight_full(EventType, EventContent, Data) -> - %% inflight_full is a sub-state of connected state, - %% delegate all other events to connected state. - connected(EventType, EventContent, Data). - -handle_event({call, From}, stop, _StateName, _State) -> - {stop_and_reply, normal, [{reply, From, ok}]}; -handle_event(info, {TcpOrSsL, _Sock, Data}, _StateName, State) - when TcpOrSsL =:= tcp; TcpOrSsL =:= ssl -> - ?LOG(debug, "RECV Data: ~p", [Data]), - process_incoming(Data, [], run_sock(State)); - -handle_event(info, {Error, _Sock, Reason}, _StateName, State) - when Error =:= tcp_error; Error =:= ssl_error -> - ?LOG(error, "The connection error occured ~p, reason:~p", [Error, Reason]), - {stop, {shutdown, Reason}, State}; - -handle_event(info, {Closed, _Sock}, _StateName, State) - when Closed =:= tcp_closed; Closed =:= ssl_closed -> - ?LOG(debug, "~p", [Closed]), - {stop, {shutdown, Closed}, State}; - -handle_event(info, {'EXIT', Owner, Reason}, _, State = #state{owner = Owner}) -> - ?LOG(debug, "Got EXIT from owner, Reason: ~p", [Reason]), - {stop, {shutdown, Reason}, State}; - -handle_event(info, {inet_reply, _Sock, ok}, _, _State) -> - keep_state_and_data; - -handle_event(info, {inet_reply, _Sock, {error, Reason}}, _, State) -> - ?LOG(error, "Got tcp error: ~p", [Reason]), - {stop, {shutdown, Reason}, State}; - -handle_event(info, EventContent = {'EXIT', _Pid, normal}, StateName, _State) -> - ?LOG(info, "State: ~s, Unexpected Event: (info, ~p)", - [StateName, EventContent]), - keep_state_and_data; - -handle_event(EventType, EventContent, StateName, _StateData) -> - ?LOG(error, "State: ~s, Unexpected Event: (~p, ~p)", - [StateName, EventType, EventContent]), - keep_state_and_data. - -%% Mandatory callback functions -terminate(Reason, _StateName, State = #state{socket = Socket}) -> - case Reason of - {disconnected, ReasonCode, Properties} -> - %% backward compatible - ok = eval_msg_handler(State, disconnected, {ReasonCode, Properties}); - _ -> - ok = eval_msg_handler(State, disconnected, Reason) - end, - case Socket =:= undefined of - true -> ok; - _ -> emqx_client_sock:close(Socket) - end. - -code_change(_Vsn, State, Data, _Extra) -> - {ok, State, Data}. - -%%-------------------------------------------------------------------- -%% Internal functions -%%-------------------------------------------------------------------- - -should_ping(Sock) -> - case emqx_client_sock:getstat(Sock, [send_oct]) of - {ok, [{send_oct, Val}]} -> - OldVal = get(send_oct), put(send_oct, Val), - OldVal == undefined orelse OldVal == Val; - Error = {error, _Reason} -> - Error - end. - -delete_inflight(?PUBACK_PACKET(PacketId, ReasonCode, Properties), - State = #state{inflight = Inflight}) -> - case emqx_inflight:lookup(PacketId, Inflight) of - {value, {publish, #mqtt_msg{packet_id = PacketId}, _Ts}} -> - ok = eval_msg_handler(State, puback, #{packet_id => PacketId, - reason_code => ReasonCode, - properties => Properties}), - State#state{inflight = emqx_inflight:delete(PacketId, Inflight)}; - none -> - ?LOG(warning, "Unexpected PUBACK: ~p", [PacketId]), - State - end; -delete_inflight(?PUBCOMP_PACKET(PacketId, ReasonCode, Properties), - State = #state{inflight = Inflight}) -> - case emqx_inflight:lookup(PacketId, Inflight) of - {value, {pubrel, _PacketId, _Ts}} -> - ok = eval_msg_handler(State, puback, #{packet_id => PacketId, - reason_code => ReasonCode, - properties => Properties}), - State#state{inflight = emqx_inflight:delete(PacketId, Inflight)}; - none -> - ?LOG(warning, "Unexpected PUBCOMP Packet: ~p", [PacketId]), - State - end. - -delete_inflight_when_full(Packet, State0) -> - State = #state{inflight = Inflight} = delete_inflight(Packet, State0), - case emqx_inflight:is_full(Inflight) of - true -> {keep_state, State}; - false -> {next_state, connected, State} - end. - -assign_id(?NO_CLIENT_ID, Props) -> - case maps:find('Assigned-Client-Identifier', Props) of - {ok, Value} -> - Value; - _ -> - error(bad_client_id) - end; -assign_id(Id, _Props) -> - Id. - -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); - false -> {keep_state, State} - end; -publish_process(?QOS_2, Packet = ?PUBLISH_PACKET(?QOS_2, PacketId), - State = #state{awaiting_rel = AwaitingRel}) -> - case send_puback(?PUBREC_PACKET(PacketId), State) of - {keep_state, NewState} -> - AwaitingRel1 = maps:put(PacketId, Packet, AwaitingRel), - {keep_state, NewState#state{awaiting_rel = AwaitingRel1}}; - Stop -> Stop - end. - -ensure_keepalive_timer(State = ?PROPERTY('Server-Keep-Alive', Secs)) -> - ensure_keepalive_timer(timer:seconds(Secs), State#state{keepalive = Secs}); -ensure_keepalive_timer(State = #state{keepalive = 0}) -> - State; -ensure_keepalive_timer(State = #state{keepalive = I}) -> - ensure_keepalive_timer(timer:seconds(I), State). -ensure_keepalive_timer(I, State) when is_integer(I) -> - State#state{keepalive_timer = erlang:start_timer(I, self(), keepalive)}. - -new_call(Id, From) -> - new_call(Id, From, undefined). -new_call(Id, From, Req) -> - #call{id = Id, from = From, req = Req, ts = os:timestamp()}. - -add_call(Call, Data = #state{pending_calls = Calls}) -> - Data#state{pending_calls = [Call | Calls]}. - -take_call(Id, Data = #state{pending_calls = Calls}) -> - case lists:keytake(Id, #call.id, Calls) of - {value, Call, Left} -> - {value, Call, Data#state{pending_calls = Left}}; - false -> false - end. - -timeout_calls(Timeout, Calls) -> - timeout_calls(os:timestamp(), Timeout, Calls). -timeout_calls(Now, Timeout, Calls) -> - lists:foldl(fun(C = #call{from = From, ts = Ts}, Acc) -> - case (timer:now_diff(Now, Ts) div 1000) >= Timeout of - true -> From ! {error, ack_timeout}, - Acc; - false -> [C | Acc] - end - end, [], Calls). - -ensure_ack_timer(State = #state{ack_timer = undefined, - ack_timeout = Timeout, - pending_calls = Calls}) when length(Calls) > 0 -> - State#state{ack_timer = erlang:start_timer(Timeout, self(), ack)}; -ensure_ack_timer(State) -> State. - -ensure_retry_timer(State = #state{retry_interval = Interval}) -> - do_ensure_retry_timer(Interval, State). - -do_ensure_retry_timer(Interval, State = #state{retry_timer = undefined}) - when Interval > 0 -> - State#state{retry_timer = erlang:start_timer(Interval, self(), retry)}; -do_ensure_retry_timer(_Interval, State) -> - State. - -retry_send(State = #state{inflight = Inflight}) -> - SortFun = fun({_, _, Ts1}, {_, _, Ts2}) -> Ts1 < Ts2 end, - Msgs = lists:sort(SortFun, emqx_inflight:values(Inflight)), - retry_send(Msgs, os:timestamp(), State ). - -retry_send([], _Now, State) -> - {keep_state, ensure_retry_timer(State)}; -retry_send([{Type, Msg, Ts} | Msgs], Now, State = #state{retry_interval = Interval}) -> - Diff = timer:now_diff(Now, Ts) div 1000, %% micro -> ms - case (Diff >= Interval) of - true -> case retry_send(Type, Msg, Now, State) of - {ok, NewState} -> retry_send(Msgs, Now, NewState); - {error, Error} -> {stop, Error} - end; - false -> {keep_state, do_ensure_retry_timer(Interval - Diff, State)} - end. - -retry_send(publish, Msg = #mqtt_msg{qos = QoS, packet_id = PacketId}, - Now, State = #state{inflight = Inflight}) -> - Msg1 = Msg#mqtt_msg{dup = (QoS =:= ?QOS_1)}, - case send(Msg1, State) of - {ok, NewState} -> - Inflight1 = emqx_inflight:update(PacketId, {publish, Msg1, Now}, Inflight), - {ok, NewState#state{inflight = Inflight1}}; - Error = {error, _Reason} -> - Error - end; -retry_send(pubrel, PacketId, Now, State = #state{inflight = Inflight}) -> - case send(?PUBREL_PACKET(PacketId), State) of - {ok, NewState} -> - Inflight1 = emqx_inflight:update(PacketId, {pubrel, PacketId, Now}, Inflight), - {ok, NewState#state{inflight = Inflight1}}; - Error = {error, _Reason} -> - Error - end. - -deliver(#mqtt_msg{qos = QoS, dup = Dup, retain = Retain, packet_id = PacketId, - topic = Topic, props = Props, payload = Payload}, - State) -> - Msg = #{qos => QoS, dup => Dup, retain => Retain, packet_id => PacketId, - topic => Topic, properties => Props, payload => Payload, - client_pid => self()}, - ok = eval_msg_handler(State, publish, Msg), - State. - -eval_msg_handler(#state{msg_handler = ?NO_MSG_HDLR, - owner = Owner}, - disconnected, {ReasonCode, Properties}) -> - %% Special handling for disconnected message when there is no handler callback - Owner ! {disconnected, ReasonCode, Properties}, - ok; -eval_msg_handler(#state{msg_handler = ?NO_MSG_HDLR}, - disconnected, _OtherReason) -> - %% do nothing to be backward compatible - ok; -eval_msg_handler(#state{msg_handler = ?NO_MSG_HDLR, - owner = Owner}, Kind, Msg) -> - Owner ! {Kind, Msg}, - ok; -eval_msg_handler(#state{msg_handler = Handler}, Kind, Msg) -> - F = maps:get(Kind, Handler), - _ = F(Msg), - ok. - -packet_to_msg(#mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, - dup = Dup, - qos = QoS, - retain = R}, - variable = #mqtt_packet_publish{topic_name = Topic, - packet_id = PacketId, - properties = Props}, - payload = Payload}) -> - #mqtt_msg{qos = QoS, retain = R, dup = Dup, packet_id = PacketId, - topic = Topic, props = Props, payload = Payload}. - -msg_to_packet(#mqtt_msg{qos = QoS, dup = Dup, retain = Retain, packet_id = PacketId, - topic = Topic, props = Props, payload = Payload}) -> - #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, - qos = QoS, - retain = Retain, - dup = Dup}, - variable = #mqtt_packet_publish{topic_name = Topic, - packet_id = PacketId, - properties = Props}, - payload = Payload}. - -%%-------------------------------------------------------------------- -%% Socket Connect/Send - -sock_connect(Hosts, SockOpts, Timeout) -> - sock_connect(Hosts, SockOpts, Timeout, {error, no_hosts}). - -sock_connect([], _SockOpts, _Timeout, LastErr) -> - LastErr; -sock_connect([{Host, Port} | Hosts], SockOpts, Timeout, _LastErr) -> - case emqx_client_sock:connect(Host, Port, SockOpts, Timeout) of - {ok, Socket} -> {ok, Socket}; - Err = {error, _Reason} -> - sock_connect(Hosts, SockOpts, Timeout, Err) - end. - -hosts(#state{hosts = [], host = Host, port = Port}) -> - [{Host, Port}]; -hosts(#state{hosts = Hosts}) -> Hosts. - -send_puback(Packet, State) -> - case send(Packet, State) of - {ok, NewState} -> {keep_state, NewState}; - {error, Reason} -> {stop, {shutdown, Reason}} - end. - -send(Msg, State) when is_record(Msg, mqtt_msg) -> - send(msg_to_packet(Msg), State); - -send(Packet, State = #state{socket = Sock, proto_ver = Ver}) - when is_record(Packet, mqtt_packet) -> - Data = emqx_frame:serialize(Packet, Ver), - ?LOG(debug, "SEND Data: ~1000p", [Packet]), - case emqx_client_sock:send(Sock, Data) of - ok -> {ok, bump_last_packet_id(State)}; - Error -> Error - end. - -run_sock(State = #state{socket = Sock}) -> - emqx_client_sock:setopts(Sock, [{active, once}]), State. - -%%-------------------------------------------------------------------- -%% Process incomming - -process_incoming(<<>>, Packets, State) -> - {keep_state, State, next_events(Packets)}; - -process_incoming(Bytes, Packets, State = #state{parse_state = ParseState}) -> - try emqx_frame:parse(Bytes, ParseState) of - {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 - error:Error -> - {stop, Error} - end. - -next_events([]) -> - []; -next_events([Packet]) -> - {next_event, cast, 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}) -> - State#state{last_packet_id = next_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/src/emqx_client_sock.erl b/src/emqx_client_sock.erl deleted file mode 100644 index eb938910c..000000000 --- a/src/emqx_client_sock.erl +++ /dev/null @@ -1,110 +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_client_sock). - --export([ connect/4 - , send/2 - , close/1 - ]). - --export([ sockname/1 - , setopts/2 - , getstat/2 - ]). - --export_type([socket/0, option/0]). - --record(ssl_socket, {tcp, ssl}). - --type(socket() :: inet:socket() | #ssl_socket{}). - --type(sockname() :: {inet:ip_address(), inet:port_number()}). - --type(option() :: gen_tcp:connect_option() | {ssl_opts, [ssl:ssl_option()]}). - --define(DEFAULT_TCP_OPTIONS, [binary, {packet, raw}, {active, false}, - {nodelay, true}, {reuseaddr, true}]). - --spec(connect(inet:ip_address() | inet:hostname(), - inet:port_number(), [option()], timeout()) - -> {ok, socket()} | {error, term()}). -connect(Host, Port, SockOpts, Timeout) -> - TcpOpts = emqx_misc:merge_opts(?DEFAULT_TCP_OPTIONS, - lists:keydelete(ssl_opts, 1, SockOpts)), - case gen_tcp:connect(Host, Port, TcpOpts, Timeout) of - {ok, Sock} -> - case lists:keyfind(ssl_opts, 1, SockOpts) of - {ssl_opts, SslOpts} -> - ssl_upgrade(Sock, SslOpts, Timeout); - false -> {ok, Sock} - end; - {error, Reason} -> - {error, Reason} - end. - -ssl_upgrade(Sock, SslOpts, Timeout) -> - TlsVersions = proplists:get_value(versions, SslOpts, []), - Ciphers = proplists:get_value(ciphers, SslOpts, default_ciphers(TlsVersions)), - SslOpts2 = emqx_misc:merge_opts(SslOpts, [{ciphers, Ciphers}]), - case ssl:connect(Sock, SslOpts2, Timeout) of - {ok, SslSock} -> - ok = ssl:controlling_process(SslSock, self()), - {ok, #ssl_socket{tcp = Sock, ssl = SslSock}}; - {error, Reason} -> {error, Reason} - end. - --spec(send(socket(), iodata()) -> ok | {error, einval | closed}). -send(Sock, Data) when is_port(Sock) -> - try erlang:port_command(Sock, Data) of - true -> ok - catch - error:badarg -> {error, einval} - end; -send(#ssl_socket{ssl = SslSock}, Data) -> - ssl:send(SslSock, Data). - --spec(close(socket()) -> ok). -close(Sock) when is_port(Sock) -> - gen_tcp:close(Sock); -close(#ssl_socket{ssl = SslSock}) -> - ssl:close(SslSock). - --spec(setopts(socket(), [gen_tcp:option() | ssl:socketoption()]) -> ok). -setopts(Sock, Opts) when is_port(Sock) -> - inet:setopts(Sock, Opts); -setopts(#ssl_socket{ssl = SslSock}, Opts) -> - ssl:setopts(SslSock, Opts). - --spec(getstat(socket(), [atom()]) - -> {ok, [{atom(), integer()}]} | {error, term()}). -getstat(Sock, Options) when is_port(Sock) -> - inet:getstat(Sock, Options); -getstat(#ssl_socket{tcp = Sock}, Options) -> - inet:getstat(Sock, Options). - --spec(sockname(socket()) -> {ok, sockname()} | {error, term()}). -sockname(Sock) when is_port(Sock) -> - inet:sockname(Sock); -sockname(#ssl_socket{ssl = SslSock}) -> - ssl:sockname(SslSock). - -default_ciphers(TlsVersions) -> - lists:foldl( - fun(TlsVer, Ciphers) -> - Ciphers ++ ssl:cipher_suites(all, TlsVer) - end, [], TlsVersions). - diff --git a/test/emqx_SUITE.erl b/test/emqx_SUITE.erl index a971c7633..c26028c76 100644 --- a/test/emqx_SUITE.erl +++ b/test/emqx_SUITE.erl @@ -35,8 +35,8 @@ end_per_suite(_Config) -> t_emqx_pubsub_api(_) -> emqx:start(), true = emqx:is_running(node()), - {ok, C} = emqx_client:start_link([{host, "localhost"}, {client_id, "myclient"}]), - {ok, _} = emqx_client:connect(C), + {ok, C} = emqtt:start_link([{host, "localhost"}, {client_id, "myclient"}]), + {ok, _} = emqtt:connect(C), ClientId = <<"myclient">>, Topic = <<"mytopic">>, Payload = <<"Hello World">>, @@ -78,4 +78,4 @@ run(_, _, _) -> ct:fail("no_match"). add1(N) -> {ok, N + 1}. -add2(N) -> {ok, N + 2}. \ No newline at end of file +add2(N) -> {ok, N + 2}. diff --git a/test/emqx_alarm_handler_SUITE.erl b/test/emqx_alarm_handler_SUITE.erl index 91cde5c11..6dd367b4a 100644 --- a/test/emqx_alarm_handler_SUITE.erl +++ b/test/emqx_alarm_handler_SUITE.erl @@ -40,7 +40,7 @@ set_special_configs(_App) -> ok. t_alarm_handler(_) -> with_connection( fun(Sock) -> - emqx_client_sock:send(Sock, + emqtt_sock:send(Sock, raw_send_serialize( ?CONNECT_PACKET( #mqtt_packet_connect{ @@ -52,7 +52,7 @@ t_alarm_handler(_) -> Topic1 = emqx_topic:systop(<<"alarms/alert">>), Topic2 = emqx_topic:systop(<<"alarms/clear">>), SubOpts = #{rh => 1, qos => ?QOS_2, rap => 0, nl => 0, rc => 0}, - emqx_client_sock:send(Sock, + emqtt_sock:send(Sock, raw_send_serialize( ?SUBSCRIBE_PACKET( 1, @@ -86,13 +86,13 @@ t_alarm_handler(_) -> end). with_connection(DoFun) -> - {ok, Sock} = emqx_client_sock:connect({127, 0, 0, 1}, 1883, + {ok, Sock} = emqtt_sock:connect({127, 0, 0, 1}, 1883, [binary, {packet, raw}, {active, false}], 3000), try DoFun(Sock) after - emqx_client_sock:close(Sock) + emqtt_sock:close(Sock) end. raw_send_serialize(Packet) -> @@ -100,4 +100,3 @@ raw_send_serialize(Packet) -> raw_recv_parse(Bin) -> emqx_frame:parse(Bin, emqx_frame:initial_parse_state(#{version => ?MQTT_PROTO_V5})). - diff --git a/test/emqx_client_SUITE.erl b/test/emqx_client_SUITE.erl index 17ade0616..a00d95603 100644 --- a/test/emqx_client_SUITE.erl +++ b/test/emqx_client_SUITE.erl @@ -94,10 +94,10 @@ t_cm(_) -> IdleTimeout = emqx_zone:get_env(external, idle_timeout, 30000), emqx_zone:set_env(external, idle_timeout, 1000), ClientId = <<"myclient">>, - {ok, C} = emqx_client:start_link([{client_id, ClientId}]), - {ok, _} = emqx_client:connect(C), + {ok, C} = emqtt:start_link([{client_id, ClientId}]), + {ok, _} = emqtt:connect(C), #{client := #{client_id := ClientId}} = emqx_cm:get_chan_attrs(ClientId), - emqx_client:subscribe(C, <<"mytopic">>, 0), + emqtt:subscribe(C, <<"mytopic">>, 0), ct:sleep(1200), Stats = emqx_cm:get_chan_stats(ClientId), ?assertEqual(1, proplists:get_value(subscriptions, Stats)), @@ -114,55 +114,55 @@ t_cm_registry(_) -> emqx_ct_helpers:start_apps([]). t_will_message(_Config) -> - {ok, C1} = emqx_client:start_link([{clean_start, true}, + {ok, C1} = emqtt:start_link([{clean_start, true}, {will_topic, nth(3, ?TOPICS)}, {will_payload, <<"client disconnected">>}, {keepalive, 1}]), - {ok, _} = emqx_client:connect(C1), + {ok, _} = emqtt:connect(C1), - {ok, C2} = emqx_client:start_link(), - {ok, _} = emqx_client:connect(C2), + {ok, C2} = emqtt:start_link(), + {ok, _} = emqtt:connect(C2), - {ok, _, [2]} = emqx_client:subscribe(C2, nth(3, ?TOPICS), 2), + {ok, _, [2]} = emqtt:subscribe(C2, nth(3, ?TOPICS), 2), timer:sleep(5), - ok = emqx_client:stop(C1), + ok = emqtt:stop(C1), timer:sleep(5), ?assertEqual(1, length(recv_msgs(1))), - ok = emqx_client:disconnect(C2), + ok = emqtt:disconnect(C2), ct:pal("Will message test succeeded"). t_offline_message_queueing(_) -> - {ok, C1} = emqx_client:start_link([{clean_start, false}, + {ok, C1} = emqtt:start_link([{clean_start, false}, {client_id, <<"c1">>}]), - {ok, _} = emqx_client:connect(C1), + {ok, _} = emqtt:connect(C1), - {ok, _, [2]} = emqx_client:subscribe(C1, nth(6, ?WILD_TOPICS), 2), - ok = emqx_client:disconnect(C1), - {ok, C2} = emqx_client:start_link([{clean_start, true}, + {ok, _, [2]} = emqtt:subscribe(C1, nth(6, ?WILD_TOPICS), 2), + ok = emqtt:disconnect(C1), + {ok, C2} = emqtt:start_link([{clean_start, true}, {client_id, <<"c2">>}]), - {ok, _} = emqx_client:connect(C2), + {ok, _} = emqtt:connect(C2), - ok = emqx_client:publish(C2, nth(2, ?TOPICS), <<"qos 0">>, 0), - {ok, _} = emqx_client:publish(C2, nth(3, ?TOPICS), <<"qos 1">>, 1), - {ok, _} = emqx_client:publish(C2, nth(4, ?TOPICS), <<"qos 2">>, 2), + ok = emqtt:publish(C2, nth(2, ?TOPICS), <<"qos 0">>, 0), + {ok, _} = emqtt:publish(C2, nth(3, ?TOPICS), <<"qos 1">>, 1), + {ok, _} = emqtt:publish(C2, nth(4, ?TOPICS), <<"qos 2">>, 2), timer:sleep(10), - emqx_client:disconnect(C2), - {ok, C3} = emqx_client:start_link([{clean_start, false}, + emqtt:disconnect(C2), + {ok, C3} = emqtt:start_link([{clean_start, false}, {client_id, <<"c1">>}]), - {ok, _} = emqx_client:connect(C3), + {ok, _} = emqtt:connect(C3), timer:sleep(10), - emqx_client:disconnect(C3), + emqtt:disconnect(C3), ?assertEqual(3, length(recv_msgs(3))). t_overlapping_subscriptions(_) -> - {ok, C} = emqx_client:start_link([]), - {ok, _} = emqx_client:connect(C), + {ok, C} = emqtt:start_link([]), + {ok, _} = emqtt:connect(C), - {ok, _, [2, 1]} = emqx_client:subscribe(C, [{nth(7, ?WILD_TOPICS), 2}, + {ok, _, [2, 1]} = emqtt:subscribe(C, [{nth(7, ?WILD_TOPICS), 2}, {nth(1, ?WILD_TOPICS), 1}]), timer:sleep(10), - {ok, _} = emqx_client:publish(C, nth(4, ?TOPICS), <<"overlapping topic filters">>, 2), + {ok, _} = emqtt:publish(C, nth(4, ?TOPICS), <<"overlapping topic filters">>, 2), timer:sleep(10), Num = length(recv_msgs(2)), @@ -176,67 +176,67 @@ t_overlapping_subscriptions(_) -> matching overlapping subscription."); true -> ok end, - emqx_client:disconnect(C). + emqtt:disconnect(C). %% t_keepalive_test(_) -> %% ct:print("Keepalive test starting"), -%% {ok, C1, _} = emqx_client:start_link([{clean_start, true}, +%% {ok, C1, _} = emqtt:start_link([{clean_start, true}, %% {keepalive, 5}, %% {will_flag, true}, %% {will_topic, nth(5, ?TOPICS)}, %% %% {will_qos, 2}, %% {will_payload, <<"keepalive expiry">>}]), -%% ok = emqx_client:pause(C1), -%% {ok, C2, _} = emqx_client:start_link([{clean_start, true}, +%% ok = emqtt:pause(C1), +%% {ok, C2, _} = emqtt:start_link([{clean_start, true}, %% {keepalive, 0}]), -%% {ok, _, [2]} = emqx_client:subscribe(C2, nth(5, ?TOPICS), 2), -%% ok = emqx_client:disconnect(C2), +%% {ok, _, [2]} = emqtt:subscribe(C2, nth(5, ?TOPICS), 2), +%% ok = emqtt:disconnect(C2), %% ?assertEqual(1, length(recv_msgs(1))), %% ct:print("Keepalive test succeeded"). t_redelivery_on_reconnect(_) -> ct:pal("Redelivery on reconnect test starting"), - {ok, C1} = emqx_client:start_link([{clean_start, false}, + {ok, C1} = emqtt:start_link([{clean_start, false}, {client_id, <<"c">>}]), - {ok, _} = emqx_client:connect(C1), + {ok, _} = emqtt:connect(C1), - {ok, _, [2]} = emqx_client:subscribe(C1, nth(7, ?WILD_TOPICS), 2), + {ok, _, [2]} = emqtt:subscribe(C1, nth(7, ?WILD_TOPICS), 2), timer:sleep(10), - ok = emqx_client:pause(C1), - {ok, _} = emqx_client:publish(C1, nth(2, ?TOPICS), <<>>, + ok = emqtt:pause(C1), + {ok, _} = emqtt:publish(C1, nth(2, ?TOPICS), <<>>, [{qos, 1}, {retain, false}]), - {ok, _} = emqx_client:publish(C1, nth(4, ?TOPICS), <<>>, + {ok, _} = emqtt:publish(C1, nth(4, ?TOPICS), <<>>, [{qos, 2}, {retain, false}]), timer:sleep(10), - ok = emqx_client:disconnect(C1), + ok = emqtt:disconnect(C1), ?assertEqual(0, length(recv_msgs(2))), - {ok, C2} = emqx_client:start_link([{clean_start, false}, + {ok, C2} = emqtt:start_link([{clean_start, false}, {client_id, <<"c">>}]), - {ok, _} = emqx_client:connect(C2), + {ok, _} = emqtt:connect(C2), timer:sleep(10), - ok = emqx_client:disconnect(C2), + ok = emqtt:disconnect(C2), ?assertEqual(2, length(recv_msgs(2))). %% t_subscribe_sys_topics(_) -> %% ct:print("Subscribe failure test starting"), -%% {ok, C, _} = emqx_client:start_link([]), -%% {ok, _, [2]} = emqx_client:subscribe(C, <<"$SYS/#">>, 2), +%% {ok, C, _} = emqtt:start_link([]), +%% {ok, _, [2]} = emqtt:subscribe(C, <<"$SYS/#">>, 2), %% timer:sleep(10), %% ct:print("Subscribe failure test succeeded"). t_dollar_topics(_) -> ct:pal("$ topics test starting"), - {ok, C} = emqx_client:start_link([{clean_start, true}, + {ok, C} = emqtt:start_link([{clean_start, true}, {keepalive, 0}]), - {ok, _} = emqx_client:connect(C), + {ok, _} = emqtt:connect(C), - {ok, _, [1]} = emqx_client:subscribe(C, nth(6, ?WILD_TOPICS), 1), - {ok, _} = emqx_client:publish(C, << <<"$">>/binary, (nth(2, ?TOPICS))/binary>>, + {ok, _, [1]} = emqtt:subscribe(C, nth(6, ?WILD_TOPICS), 1), + {ok, _} = emqtt:publish(C, << <<"$">>/binary, (nth(2, ?TOPICS))/binary>>, <<"test">>, [{qos, 1}, {retain, false}]), timer:sleep(10), ?assertEqual(0, length(recv_msgs(1))), - ok = emqx_client:disconnect(C), + ok = emqtt:disconnect(C), ct:pal("$ topics test succeeded"). %%-------------------------------------------------------------------- @@ -254,15 +254,15 @@ t_basic_with_props_v5(_) -> t_basic(Opts) -> Topic = nth(1, ?TOPICS), - {ok, C} = emqx_client:start_link([{proto_ver, v4}]), - {ok, _} = emqx_client:connect(C), - {ok, _, [1]} = emqx_client:subscribe(C, Topic, qos1), - {ok, _, [2]} = emqx_client:subscribe(C, Topic, qos2), - {ok, _} = emqx_client:publish(C, Topic, <<"qos 2">>, 2), - {ok, _} = emqx_client:publish(C, Topic, <<"qos 2">>, 2), - {ok, _} = emqx_client:publish(C, Topic, <<"qos 2">>, 2), + {ok, C} = emqtt:start_link([{proto_ver, v4}]), + {ok, _} = emqtt:connect(C), + {ok, _, [1]} = emqtt:subscribe(C, Topic, qos1), + {ok, _, [2]} = emqtt:subscribe(C, Topic, qos2), + {ok, _} = emqtt:publish(C, Topic, <<"qos 2">>, 2), + {ok, _} = emqtt:publish(C, Topic, <<"qos 2">>, 2), + {ok, _} = emqtt:publish(C, Topic, <<"qos 2">>, 2), ?assertEqual(3, length(recv_msgs(3))), - ok = emqx_client:disconnect(C). + ok = emqtt:disconnect(C). %%-------------------------------------------------------------------- %% Helper functions diff --git a/test/emqx_flapping_SUITE.erl b/test/emqx_flapping_SUITE.erl index 0d4bbc1e6..a36d21c2a 100644 --- a/test/emqx_flapping_SUITE.erl +++ b/test/emqx_flapping_SUITE.erl @@ -32,8 +32,8 @@ end_per_suite(_Config) -> %% t_flapping(_Config) -> %% process_flag(trap_exit, true), %% flapping_connect(5), -%% {ok, C} = emqx_client:start_link([{client_id, <<"Client">>}]), -%% {error, _} = emqx_client:connect(C), +%% {ok, C} = emqtt:start_link([{client_id, <<"Client">>}]), +%% {error, _} = emqtt:connect(C), %% receive %% {'EXIT', Client, _Reason} -> %% ct:log("receive exit signal, Client: ~p", [Client]) @@ -45,9 +45,9 @@ flapping_connect(Times) -> lists:foreach(fun do_connect/1, lists:seq(1, Times)). do_connect(_I) -> - {ok, C} = emqx_client:start_link([{client_id, <<"Client">>}]), - {ok, _} = emqx_client:connect(C), - ok = emqx_client:disconnect(C). + {ok, C} = emqtt:start_link([{client_id, <<"Client">>}]), + {ok, _} = emqtt:connect(C), + ok = emqtt:disconnect(C). prepare_for_test() -> ok = emqx_zone:set_env(external, enable_flapping_detect, true), diff --git a/test/emqx_mod_subscription_SUITE.erl b/test/emqx_mod_subscription_SUITE.erl index f955bbbf2..464bfcc76 100644 --- a/test/emqx_mod_subscription_SUITE.erl +++ b/test/emqx_mod_subscription_SUITE.erl @@ -36,10 +36,10 @@ end_per_suite(_Config) -> t_mod_subscription(_) -> emqx_mod_subscription:load([{<<"connected/%c/%u">>, ?QOS_0}]), - {ok, C} = emqx_client:start_link([{host, "localhost"}, {client_id, "myclient"}, {username, "admin"}]), - {ok, _} = emqx_client:connect(C), + {ok, C} = emqtt:start_link([{host, "localhost"}, {client_id, "myclient"}, {username, "admin"}]), + {ok, _} = emqtt:connect(C), % ct:sleep(100), - emqx_client:publish(C, <<"connected/myclient/admin">>, <<"Hello world">>, ?QOS_0), + emqtt:publish(C, <<"connected/myclient/admin">>, <<"Hello world">>, ?QOS_0), receive {publish, #{topic := Topic, payload := Payload}} -> ?assertEqual(<<"connected/myclient/admin">>, Topic), @@ -47,5 +47,5 @@ t_mod_subscription(_) -> after 100 -> ct:fail("no_message") end, - ok = emqx_client:disconnect(C), + ok = emqtt:disconnect(C), emqx_mod_subscription:unload([]). diff --git a/test/emqx_request_handler.erl b/test/emqx_request_handler.erl index f2f218638..567570506 100644 --- a/test/emqx_request_handler.erl +++ b/test/emqx_request_handler.erl @@ -24,28 +24,28 @@ -type topic() :: emqx_topic:topic(). -type handler() :: fun((CorrData :: binary(), ReqPayload :: binary()) -> RspPayload :: binary()). --spec start_link(topic(), qos(), handler(), emqx_client:options()) -> +-spec start_link(topic(), qos(), handler(), emqtt:options()) -> {ok, pid()} | {error, any()}. start_link(RequestTopic, QoS, RequestHandler, Options0) -> Parent = self(), MsgHandler = make_msg_handler(RequestHandler, Parent), Options = [{msg_handler, MsgHandler} | Options0], - case emqx_client:start_link(Options) of + case emqtt:start_link(Options) of {ok, Pid} -> - {ok, _} = emqx_client:connect(Pid), + {ok, _} = emqtt:connect(Pid), try subscribe(Pid, RequestTopic, QoS) of ok -> {ok, Pid}; {error, _} = Error -> Error catch C : E : S -> - emqx_client:stop(Pid), + emqtt:stop(Pid), {error, {C, E, S}} end; {error, _} = Error -> Error end. stop(Pid) -> - emqx_client:disconnect(Pid). + emqtt:disconnect(Pid). make_msg_handler(RequestHandler, Parent) -> #{publish => fun(Msg) -> handle_msg(Msg, RequestHandler, Parent) end, @@ -75,11 +75,11 @@ handle_msg(ReqMsg, RequestHandler, Parent) -> end. send_response(Msg) -> - %% This function is evaluated by emqx_client itself. + %% This function is evaluated by emqtt itself. %% hence delegate to another temp process for the loopback gen_statem call. Client = self(), _ = spawn_link(fun() -> - case emqx_client:publish(Client, Msg) of + case emqtt:publish(Client, Msg) of ok -> ok; {ok, _} -> ok; {error, Reason} -> exit({failed_to_publish_response, Reason}) @@ -89,6 +89,6 @@ send_response(Msg) -> subscribe(Client, Topic, QoS) -> {ok, _Props, _QoS} = - emqx_client:subscribe(Client, [{Topic, [{rh, 2}, {rap, false}, + emqtt:subscribe(Client, [{Topic, [{rh, 2}, {rap, false}, {nl, true}, {qos, QoS}]}]), ok. diff --git a/test/emqx_request_sender.erl b/test/emqx_request_sender.erl index 27f3a45ac..2316a2f43 100644 --- a/test/emqx_request_sender.erl +++ b/test/emqx_request_sender.erl @@ -24,15 +24,15 @@ start_link(ResponseTopic, QoS, Options0) -> Parent = self(), MsgHandler = make_msg_handler(Parent), Options = [{msg_handler, MsgHandler} | Options0], - case emqx_client:start_link(Options) of + case emqtt:start_link(Options) of {ok, Pid} -> - {ok, _} = emqx_client:connect(Pid), + {ok, _} = emqtt:connect(Pid), try subscribe(Pid, ResponseTopic, QoS) of ok -> {ok, Pid}; {error, _} = Error -> Error catch C : E : S -> - emqx_client:stop(Pid), + emqtt:stop(Pid), {error, {C, E, S}} end; {error, _} = Error -> Error @@ -49,17 +49,17 @@ send(Client, ReqTopic, RspTopic, CorrData, Payload, QoS) -> props = Props, payload = Payload }, - case emqx_client:publish(Client, Msg) of + case emqtt:publish(Client, Msg) of ok -> ok; %% QoS = 0 {ok, _} -> ok; {error, _} = E -> E end. stop(Pid) -> - emqx_client:disconnect(Pid). + emqtt:disconnect(Pid). subscribe(Client, Topic, QoS) -> - case emqx_client:subscribe(Client, Topic, QoS) of + case emqtt:subscribe(Client, Topic, QoS) of {ok, _, _} -> ok; {error, _} = Error -> Error end. diff --git a/test/emqx_shared_sub_SUITE.erl b/test/emqx_shared_sub_SUITE.erl index 62b7c441b..1834fdbaf 100644 --- a/test/emqx_shared_sub_SUITE.erl +++ b/test/emqx_shared_sub_SUITE.erl @@ -150,24 +150,24 @@ t_not_so_sticky(_) -> ok = ensure_config(sticky), ClientId1 = <<"ClientId1">>, ClientId2 = <<"ClientId2">>, - {ok, C1} = emqx_client:start_link([{client_id, ClientId1}]), - {ok, _} = emqx_client:connect(C1), - {ok, C2} = emqx_client:start_link([{client_id, ClientId2}]), - {ok, _} = emqx_client:connect(C2), + {ok, C1} = emqtt:start_link([{client_id, ClientId1}]), + {ok, _} = emqtt:connect(C1), + {ok, C2} = emqtt:start_link([{client_id, ClientId2}]), + {ok, _} = emqtt:connect(C2), - emqx_client:subscribe(C1, {<<"$share/group1/foo/bar">>, 0}), + emqtt:subscribe(C1, {<<"$share/group1/foo/bar">>, 0}), timer:sleep(50), - emqx_client:publish(C2, <<"foo/bar">>, <<"hello1">>), + emqtt:publish(C2, <<"foo/bar">>, <<"hello1">>), ?assertMatch([#{payload := <<"hello1">>}], recv_msgs(1)), - emqx_client:unsubscribe(C1, <<"$share/group1/foo/bar">>), + emqtt:unsubscribe(C1, <<"$share/group1/foo/bar">>), timer:sleep(50), - emqx_client:subscribe(C1, {<<"$share/group1/foo/#">>, 0}), + emqtt:subscribe(C1, {<<"$share/group1/foo/#">>, 0}), timer:sleep(50), - emqx_client:publish(C2, <<"foo/bar">>, <<"hello2">>), + emqtt:publish(C2, <<"foo/bar">>, <<"hello2">>), ?assertMatch([#{payload := <<"hello2">>}], recv_msgs(1)), - emqx_client:disconnect(C1), - emqx_client:disconnect(C2), + emqtt:disconnect(C1), + emqtt:disconnect(C2), ok. test_two_messages(Strategy) -> @@ -178,15 +178,15 @@ test_two_messages(Strategy, WithAck) -> Topic = <<"foo/bar">>, ClientId1 = <<"ClientId1">>, ClientId2 = <<"ClientId2">>, - {ok, ConnPid1} = emqx_client:start_link([{client_id, ClientId1}]), - {ok, _} = emqx_client:connect(ConnPid1), - {ok, ConnPid2} = emqx_client:start_link([{client_id, ClientId2}]), - {ok, _} = emqx_client:connect(ConnPid2), + {ok, ConnPid1} = emqtt:start_link([{client_id, ClientId1}]), + {ok, _} = emqtt:connect(ConnPid1), + {ok, ConnPid2} = emqtt:start_link([{client_id, ClientId2}]), + {ok, _} = emqtt:connect(ConnPid2), Message1 = emqx_message:make(ClientId1, 0, Topic, <<"hello1">>), Message2 = emqx_message:make(ClientId1, 0, Topic, <<"hello2">>), - emqx_client:subscribe(ConnPid1, {<<"$share/group1/foo/bar">>, 0}), - emqx_client:subscribe(ConnPid2, {<<"$share/group1/foo/bar">>, 0}), + emqtt:subscribe(ConnPid1, {<<"$share/group1/foo/bar">>, 0}), + emqtt:subscribe(ConnPid2, {<<"$share/group1/foo/bar">>, 0}), ct:sleep(100), emqx:publish(Message1), Me = self(), @@ -210,8 +210,8 @@ test_two_messages(Strategy, WithAck) -> hash -> ?assert(UsedSubPid1 =:= UsedSubPid2); _ -> ok end, - emqx_client:stop(ConnPid1), - emqx_client:stop(ConnPid2), + emqtt:stop(ConnPid1), + emqtt:stop(ConnPid2), ok. last_message(ExpectedPayload, Pids) -> diff --git a/test/emqx_sys_mon_SUITE.erl b/test/emqx_sys_mon_SUITE.erl index ef54ea630..7f9025b58 100644 --- a/test/emqx_sys_mon_SUITE.erl +++ b/test/emqx_sys_mon_SUITE.erl @@ -55,9 +55,9 @@ t_sys_mon(_Config) -> end, ?INPUTINFO). validate_sys_mon_info(PidOrPort, SysMonName,ValidateInfo, InfoOrPort) -> - {ok, C} = emqx_client:start_link([{host, "localhost"}]), - {ok, _} = emqx_client:connect(C), - emqx_client:subscribe(C, emqx_topic:systop(lists:concat(['sysmon/', SysMonName])), qos1), + {ok, C} = emqtt:start_link([{host, "localhost"}]), + {ok, _} = emqtt:connect(C), + emqtt:subscribe(C, emqx_topic:systop(lists:concat(['sysmon/', SysMonName])), qos1), timer:sleep(100), ?SYSMON ! {monitor, PidOrPort, SysMonName, InfoOrPort}, receive @@ -68,7 +68,7 @@ validate_sys_mon_info(PidOrPort, SysMonName,ValidateInfo, InfoOrPort) -> 1000 -> ct:fail("flase") end, - emqx_client:stop(C). + emqtt:stop(C). concat_str(ValidateInfo, InfoOrPort, Info) -> WarnInfo = io_lib:format(ValidateInfo, [InfoOrPort, Info]), diff --git a/test/emqx_tracer_SUITE.erl b/test/emqx_tracer_SUITE.erl index 87f6d73f2..684f17e7f 100644 --- a/test/emqx_tracer_SUITE.erl +++ b/test/emqx_tracer_SUITE.erl @@ -33,11 +33,11 @@ end_per_suite(_Config) -> emqx_ct_helpers:stop_apps([]). t_start_traces(_Config) -> - {ok, T} = emqx_client:start_link([{host, "localhost"}, + {ok, T} = emqtt:start_link([{host, "localhost"}, {client_id, <<"client">>}, {username, <<"testuser">>}, {password, <<"pass">>}]), - emqx_client:connect(T), + emqtt:connect(T), %% Start tracing emqx_logger:set_log_level(error), @@ -63,7 +63,7 @@ t_start_traces(_Config) -> emqx_logger:set_log_level(debug), %% Client with clientid = "client" publishes a "hi" message to "a/b/c". - emqx_client:publish(T, <<"a/b/c">>, <<"hi">>), + emqtt:publish(T, <<"a/b/c">>, <<"hi">>), ct:sleep(200), %% Verify messages are logged to "tmp/client.log" and "tmp/topic_trace.log", but not "tmp/client2.log". @@ -75,6 +75,6 @@ t_start_traces(_Config) -> ok = emqx_tracer:stop_trace({client_id, <<"client">>}), ok = emqx_tracer:stop_trace({client_id, <<"client2">>}), ok = emqx_tracer:stop_trace({topic, <<"a/#">>}), - emqx_client:disconnect(T), + emqtt:disconnect(T), emqx_logger:set_log_level(warning). diff --git a/test/emqx_ws_connection_SUITE.erl b/test/emqx_ws_connection_SUITE.erl index 2e2db7728..dfb348253 100644 --- a/test/emqx_ws_connection_SUITE.erl +++ b/test/emqx_ws_connection_SUITE.erl @@ -40,7 +40,7 @@ t_basic(_) -> {ok, _} = emqtt:publish(C, Topic, <<"qos 2">>, 2), {ok, _} = emqtt:publish(C, Topic, <<"qos 2">>, 2), ?assertEqual(3, length(recv_msgs(3))), - ok = emqx_client:disconnect(C). + ok = emqtt:disconnect(C). recv_msgs(Count) -> recv_msgs(Count, []). From bf942e4bec923b587d4cc3e138f88a7bee0ea8d6 Mon Sep 17 00:00:00 2001 From: Mousse Date: Thu, 22 Aug 2019 16:21:27 +0800 Subject: [PATCH 18/36] Handle the retained flag correctly (#2811) Handle the retained flag correctly --- src/emqx_channel.erl | 4 ++-- src/emqx_packet.erl | 5 +++-- src/emqx_session.erl | 10 ++++++---- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/emqx_channel.erl b/src/emqx_channel.erl index 57ff91a16..8c4412f42 100644 --- a/src/emqx_channel.erl +++ b/src/emqx_channel.erl @@ -407,10 +407,10 @@ process_connect(ConnPkt, Channel) -> %% Process Publish process_publish(Packet = ?PUBLISH_PACKET(_QoS, _Topic, PacketId), - Channel = #channel{client = Client}) -> + Channel = #channel{client = Client, proto_ver = ProtoVer}) -> Msg = emqx_packet:to_message(Client, Packet), %%TODO: Improve later. - Msg1 = emqx_message:set_flag(dup, false, Msg), + Msg1 = emqx_message:set_flag(dup, false, emqx_message:set_header(proto_ver, ProtoVer, Msg)), process_publish(PacketId, mount(Client, Msg1), Channel). process_publish(_PacketId, Msg = #message{qos = ?QOS_0}, Channel) -> diff --git a/src/emqx_packet.erl b/src/emqx_packet.erl index d8205b98a..ea5657f41 100644 --- a/src/emqx_packet.erl +++ b/src/emqx_packet.erl @@ -165,10 +165,11 @@ will_msg(#mqtt_packet_connect{client_id = ClientId, will_qos = QoS, will_topic = Topic, will_props = Properties, - will_payload = Payload}) -> + will_payload = Payload, + proto_ver = ProtoVer}) -> Msg = emqx_message:make(ClientId, QoS, Topic, Payload), Msg#message{flags = #{dup => false, retain => Retain}, - headers = merge_props(#{username => Username}, Properties)}. + headers = merge_props(#{username => Username, proto_ver => ProtoVer}, Properties)}. merge_props(Headers, undefined) -> Headers; diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 1cb449290..72a683b2a 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -567,12 +567,14 @@ enrich([{qos, SubQoS}|Opts], Msg = #message{qos = PubQoS}, Session = #session{up 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([{rap, 0}|Opts], Msg = #message{flags = Flags, headers = #{proto_ver := ?MQTT_PROTO_V5}}, Session) -> enrich(Opts, Msg#message{flags = maps:put(retain, false, Flags)}, Session); -enrich([{rap, _}|Opts], Msg, Session) -> +enrich([{rap, _}|Opts], Msg = #message{headers = #{proto_ver := ?MQTT_PROTO_V5}}, Session) -> enrich(Opts, Msg, Session); +enrich([{rap, _}|Opts], Msg = #message{headers = #{retained := true}}, Session = #session{}) -> + enrich(Opts, Msg, Session); +enrich([{rap, _}|Opts], Msg = #message{flags = Flags}, Session) -> + enrich(Opts, Msg#message{flags = maps:put(retain, false, Flags)}, Session); enrich([{subid, SubId}|Opts], Msg, Session) -> enrich(Opts, emqx_message:set_header('Subscription-Identifier', SubId, Msg), Session). From 8b03371a4a42d7d86ff656e167ac825415dedcb6 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 22 Aug 2019 16:38:25 +0800 Subject: [PATCH 19/36] Improve the keepalive, connection, channel and session modules (#2813) --- src/emqx_channel.erl | 788 ++++++++++++++++++---------------- src/emqx_cm.erl | 10 +- src/emqx_connection.erl | 169 +++----- src/emqx_keepalive.erl | 106 ++--- src/emqx_misc.erl | 19 +- src/emqx_packet.erl | 18 +- src/emqx_protocol.erl | 136 ++++++ src/emqx_session.erl | 149 ++----- src/emqx_ws_connection.erl | 112 ++--- test/emqx_channel_SUITE.erl | 69 +-- test/emqx_keepalive_SUITE.erl | 35 +- test/emqx_net_SUITE.erl | 45 -- test/emqx_packet_SUITE.erl | 6 +- test/emqx_protocol_SUITE.erl | 49 +++ test/emqx_session_SUITE.erl | 15 +- 15 files changed, 881 insertions(+), 845 deletions(-) create mode 100644 src/emqx_protocol.erl delete mode 100644 test/emqx_net_SUITE.erl create mode 100644 test/emqx_protocol_SUITE.erl diff --git a/src/emqx_channel.erl b/src/emqx_channel.erl index 8c4412f42..b1671489e 100644 --- a/src/emqx_channel.erl +++ b/src/emqx_channel.erl @@ -24,21 +24,20 @@ -logger_header("[Channel]"). +-export([init/2]). + -export([ info/1 , info/2 , attrs/1 + , stats/1 , caps/1 ]). %% for tests -export([set/3]). --export([takeover/2]). - --export([ init/2 - , handle_in/2 +-export([ handle_in/2 , handle_out/2 - , handle_out/3 , handle_call/2 , handle_cast/2 , handle_info/2 @@ -46,164 +45,52 @@ , terminate/2 ]). +%% Ensure timer -export([ensure_timer/2]). -export([gc/3]). --import(emqx_access_control, - [ authenticate/1 - , check_acl/3 - ]). +-import(emqx_misc, [maybe_apply/2]). --import(emqx_misc, [start_timer/2]). +-import(emqx_access_control, [check_acl/3]). -export_type([channel/0]). -record(channel, { - client :: emqx_types:client(), - session :: emqx_session:session(), - proto_name :: binary(), - proto_ver :: emqx_types:ver(), - keepalive :: non_neg_integer(), - will_msg :: emqx_types:message(), - topic_aliases :: maybe(map()), - alias_maximum :: maybe(map()), - ack_props :: maybe(emqx_types:properties()), - idle_timeout :: timeout(), - retry_timer :: maybe(reference()), - alive_timer :: maybe(reference()), - stats_timer :: disabled | maybe(reference()), - expiry_timer :: maybe(reference()), - gc_state :: emqx_gc:gc_state(), %% GC State - oom_policy :: emqx_oom:oom_policy(), %% OOM Policy - connected :: boolean(), - connected_at :: erlang:timestamp(), - resuming :: boolean(), - pendings :: list() + %% MQTT Client + client :: emqx_types:client(), + %% MQTT Session + session :: emqx_session:session(), + %% MQTT Protocol + protocol :: emqx_protocol:protocol(), + %% Keepalive + keepalive :: emqx_keepalive:keepalive(), + %% Timers + timers :: #{atom() => disabled | maybe(reference())}, + %% GC State + gc_state :: emqx_gc:gc_state(), + %% OOM Policy + oom_policy :: emqx_oom:oom_policy(), + %% Connected + connected :: boolean(), + connected_at :: erlang:timestamp(), + %% Takeover/Resume + resuming :: boolean(), + pendings :: list() }). -opaque(channel() :: #channel{}). --define(NO_PROPS, undefined). +-define(TIMER_TABLE, #{ + stats_timer => emit_stats, + alive_timer => keepalive, + retry_timer => retry_delivery, + await_timer => expire_awaiting_rel, + expire_timer => expire_session + }). %%-------------------------------------------------------------------- -%% Info, Attrs and Caps -%%-------------------------------------------------------------------- - --spec(info(channel()) -> emqx_types:infos()). -info(#channel{client = Client, - session = Session, - proto_name = ProtoName, - proto_ver = ProtoVer, - keepalive = Keepalive, - will_msg = WillMsg, - topic_aliases = Aliases, - stats_timer = StatsTimer, - idle_timeout = IdleTimeout, - gc_state = GCState, - connected = Connected, - connected_at = ConnectedAt}) -> - #{client => Client, - session => if Session == undefined -> - undefined; - true -> emqx_session:info(Session) - end, - proto_name => ProtoName, - proto_ver => ProtoVer, - keepalive => Keepalive, - will_msg => WillMsg, - topic_aliases => Aliases, - enable_stats => case StatsTimer of - disabled -> false; - _Otherwise -> true - end, - idle_timeout => IdleTimeout, - gc_state => emqx_gc:info(GCState), - connected => Connected, - connected_at => ConnectedAt, - resuming => false, - pendings => [] - }. - --spec(info(atom(), channel()) -> term()). -info(client, #channel{client = Client}) -> - Client; -info(zone, #channel{client = #{zone := Zone}}) -> - Zone; -info(client_id, #channel{client = #{client_id := ClientId}}) -> - ClientId; -info(session, #channel{session = Session}) -> - Session; -info(proto_name, #channel{proto_name = ProtoName}) -> - ProtoName; -info(proto_ver, #channel{proto_ver = ProtoVer}) -> - ProtoVer; -info(keepalive, #channel{keepalive = Keepalive}) -> - Keepalive; -info(will_msg, #channel{will_msg = WillMsg}) -> - WillMsg; -info(topic_aliases, #channel{topic_aliases = Aliases}) -> - Aliases; -info(enable_stats, #channel{stats_timer = disabled}) -> - false; -info(enable_stats, #channel{stats_timer = _TRef}) -> - true; -info(idle_timeout, #channel{idle_timeout = IdleTimeout}) -> - IdleTimeout; -info(gc_state, #channel{gc_state = GCState}) -> - emqx_gc:info(GCState); -info(connected, #channel{connected = Connected}) -> - Connected; -info(connected_at, #channel{connected_at = ConnectedAt}) -> - ConnectedAt. - --spec(attrs(channel()) -> emqx_types:attrs()). -attrs(#channel{client = Client, - session = Session, - proto_name = ProtoName, - proto_ver = ProtoVer, - keepalive = Keepalive, - connected = Connected, - connected_at = ConnectedAt}) -> - #{client => Client, - session => if Session == undefined -> - undefined; - true -> emqx_session:attrs(Session) - end, - proto_name => ProtoName, - proto_ver => ProtoVer, - keepalive => Keepalive, - connected => Connected, - connected_at => ConnectedAt - }. - --spec(caps(channel()) -> emqx_types:caps()). -caps(#channel{client = #{zone := Zone}}) -> - emqx_mqtt_caps:get_caps(Zone). - -%%-------------------------------------------------------------------- -%% For unit tests -%%-------------------------------------------------------------------- - -set(client, Client, Channel) -> - Channel#channel{client = Client}; -set(session, Session, Channel) -> - Channel#channel{session = Session}. - -%%-------------------------------------------------------------------- -%% Takeover session -%%-------------------------------------------------------------------- - -takeover('begin', Channel = #channel{session = Session}) -> - {ok, Session, Channel#channel{resuming = true}}; - -takeover('end', Channel = #channel{session = Session, - pendings = Pendings}) -> - ok = emqx_session:takeover(Session), - {ok, Pendings, Channel}. - -%%-------------------------------------------------------------------- -%% Init a channel +%% Init the channel %%-------------------------------------------------------------------- -spec(init(emqx_types:conn(), proplists:proplist()) -> channel()). @@ -223,27 +110,100 @@ init(ConnInfo, Options) -> mountpoint => MountPoint, is_bridge => false, is_superuser => false}, ConnInfo), - IdleTimout = emqx_zone:get_env(Zone, idle_timeout, 30000), EnableStats = emqx_zone:get_env(Zone, enable_stats, true), - StatsTimer = if EnableStats -> undefined; - ?Otherwise -> disabled + StatsTimer = if + EnableStats -> undefined; + ?Otherwise -> disabled end, GcState = emqx_gc:init(emqx_zone:get_env(Zone, force_gc_policy, false)), OomPolicy = emqx_oom:init(emqx_zone:get_env(Zone, force_shutdown_policy)), - #channel{client = Client, - proto_name = <<"MQTT">>, - proto_ver = ?MQTT_PROTO_V4, - keepalive = 0, - idle_timeout = IdleTimout, - stats_timer = StatsTimer, - gc_state = GcState, - oom_policy = OomPolicy, - connected = false + #channel{client = Client, + session = undefined, + protocol = undefined, + gc_state = GcState, + oom_policy = OomPolicy, + timers = #{stats_timer => StatsTimer}, + connected = false }. peer_cert_as_username(Options) -> proplists:get_value(peer_cert_as_username, Options). +%%-------------------------------------------------------------------- +%% Info, Attrs and Caps +%%-------------------------------------------------------------------- + +-spec(info(channel()) -> emqx_types:infos()). +info(#channel{client = Client, + session = Session, + protocol = Protocol, + keepalive = Keepalive, + gc_state = GCState, + oom_policy = OomPolicy, + connected = Connected, + connected_at = ConnectedAt + }) -> + #{client => Client, + session => maybe_apply(fun emqx_session:info/1, Session), + protocol => maybe_apply(fun emqx_protocol:info/1, Protocol), + keepalive => maybe_apply(fun emqx_keepalive:info/1, Keepalive), + gc_state => emqx_gc:info(GCState), + oom_policy => emqx_oom:info(OomPolicy), + connected => Connected, + connected_at => ConnectedAt + }. + +-spec(info(atom(), channel()) -> term()). +info(client, #channel{client = Client}) -> + Client; +info(session, #channel{session = Session}) -> + maybe_apply(fun emqx_session:info/1, Session); +info(protocol, #channel{protocol = Protocol}) -> + maybe_apply(fun emqx_protocol:info/1, Protocol); +info(keepalive, #channel{keepalive = Keepalive}) -> + maybe_apply(fun emqx_keepalive:info/1, Keepalive); +info(gc_state, #channel{gc_state = GCState}) -> + emqx_gc:info(GCState); +info(oom_policy, #channel{oom_policy = Policy}) -> + emqx_oom:info(Policy); +info(connected, #channel{connected = Connected}) -> + Connected; +info(connected_at, #channel{connected_at = ConnectedAt}) -> + ConnectedAt. + +-spec(attrs(channel()) -> emqx_types:attrs()). +attrs(#channel{client = Client, + session = Session, + protocol = Protocol, + connected = Connected, + connected_at = ConnectedAt}) -> + #{client => Client, + session => maybe_apply(fun emqx_session:attrs/1, Session), + protocol => maybe_apply(fun emqx_protocol:attrs/1, Protocol), + connected => Connected, + connected_at => ConnectedAt + }. + +%%TODO: ChanStats? +-spec(stats(channel()) -> emqx_types:stats()). +stats(#channel{session = Session}) -> + emqx_session:stats(Session). + +-spec(caps(channel()) -> emqx_types:caps()). +caps(#channel{client = #{zone := Zone}}) -> + emqx_mqtt_caps:get_caps(Zone). + +%%-------------------------------------------------------------------- +%% For unit tests +%%-------------------------------------------------------------------- + +set(client, Client, Channel) -> + Channel#channel{client = Client}; +set(session, Session, Channel) -> + Channel#channel{session = Session}; +set(protocol, Protocol, Channel) -> + Channel#channel{protocol = Protocol}. + %%-------------------------------------------------------------------- %% Handle incoming packet %%-------------------------------------------------------------------- @@ -255,48 +215,43 @@ peer_cert_as_username(Options) -> | {stop, Error :: term(), channel()} | {stop, Error :: term(), emqx_types:packet(), channel()}). handle_in(?CONNECT_PACKET(_), Channel = #channel{connected = true}) -> - handle_out(disconnect, ?RC_PROTOCOL_ERROR, Channel); + handle_out({disconnect, ?RC_PROTOCOL_ERROR}, Channel); -handle_in(?CONNECT_PACKET( - #mqtt_packet_connect{proto_name = ProtoName, - proto_ver = ProtoVer, - keepalive = Keepalive, - client_id = ClientId - } = ConnPkt), Channel) -> - Channel1 = Channel#channel{proto_name = ProtoName, - proto_ver = ProtoVer, - keepalive = Keepalive - }, - ok = emqx_logger:set_metadata_client_id(ClientId), - case pipeline([fun validate_in/2, - fun process_props/2, +handle_in(?CONNECT_PACKET(ConnPkt), Channel) -> + case pipeline([fun validate_packet/2, fun check_connect/2, + fun init_protocol/2, fun enrich_client/2, - fun auth_connect/2], ConnPkt, Channel1) of - {ok, NConnPkt, NChannel = #channel{client = #{client_id := ClientId1}}} -> - ok = emqx_logger:set_metadata_client_id(ClientId1), + fun set_logger_meta/2, + fun auth_connect/2], ConnPkt, Channel) of + {ok, NConnPkt, NChannel} -> process_connect(NConnPkt, NChannel); {error, ReasonCode, NChannel} -> - handle_out(connack, ReasonCode, NChannel) + handle_out({connack, ReasonCode}, NChannel) end; -handle_in(Packet = ?PUBLISH_PACKET(_QoS, Topic, _PacketId), Channel = #channel{proto_ver = Ver}) -> - case pipeline([fun validate_in/2, +handle_in(Packet = ?PUBLISH_PACKET(QoS, Topic, PacketId), Channel = #channel{protocol = Protocol}) -> + case pipeline([fun validate_packet/2, fun process_alias/2, fun check_publish/2], Packet, Channel) of {ok, NPacket, NChannel} -> process_publish(NPacket, NChannel); {error, ReasonCode, NChannel} -> + ProtoVer = emqx_protocol:info(proto_ver, Protocol), ?LOG(warning, "Cannot publish message to ~s due to ~s", - [Topic, emqx_reason_codes:text(ReasonCode, Ver)]), - handle_out(disconnect, ReasonCode, NChannel) + [Topic, emqx_reason_codes:text(ReasonCode, ProtoVer)]), + case QoS of + ?QOS_0 -> handle_out({puberr, ReasonCode}, NChannel); + ?QOS_1 -> handle_out({puback, PacketId, ReasonCode}, NChannel); + ?QOS_2 -> handle_out({pubrec, PacketId, ReasonCode}, NChannel) + end end; %%TODO: How to handle the ReasonCode? handle_in(?PUBACK_PACKET(PacketId, _ReasonCode), Channel = #channel{session = Session}) -> case emqx_session:puback(PacketId, Session) of {ok, Publishes, NSession} -> - handle_out(publish, Publishes, Channel#channel{session = NSession}); + handle_out({publish, Publishes}, Channel#channel{session = NSession}); {ok, NSession} -> {ok, Channel#channel{session = NSession}}; {error, _NotFound} -> @@ -308,24 +263,24 @@ handle_in(?PUBACK_PACKET(PacketId, _ReasonCode), Channel = #channel{session = Se handle_in(?PUBREC_PACKET(PacketId, _ReasonCode), Channel = #channel{session = Session}) -> case emqx_session:pubrec(PacketId, Session) of {ok, NSession} -> - handle_out(pubrel, {PacketId, ?RC_SUCCESS}, Channel#channel{session = NSession}); + handle_out({pubrel, PacketId, ?RC_SUCCESS}, Channel#channel{session = NSession}); {error, ReasonCode} -> - handle_out(pubrel, {PacketId, ReasonCode}, Channel) + handle_out({pubrel, PacketId, ReasonCode}, Channel) end; %%TODO: How to handle the ReasonCode? handle_in(?PUBREL_PACKET(PacketId, _ReasonCode), Channel = #channel{session = Session}) -> case emqx_session:pubrel(PacketId, Session) of {ok, NSession} -> - handle_out(pubcomp, {PacketId, ?RC_SUCCESS}, Channel#channel{session = NSession}); + handle_out({pubcomp, PacketId, ?RC_SUCCESS}, Channel#channel{session = NSession}); {error, ReasonCode} -> - handle_out(pubcomp, {PacketId, ReasonCode}, Channel) + handle_out({pubcomp, PacketId, ReasonCode}, Channel) end; handle_in(?PUBCOMP_PACKET(PacketId, _ReasonCode), Channel = #channel{session = Session}) -> case emqx_session:pubcomp(PacketId, Session) of {ok, Publishes, NSession} -> - handle_out(publish, Publishes, Channel#channel{session = NSession}); + handle_out({publish, Publishes}, Channel#channel{session = NSession}); {ok, NSession} -> {ok, Channel#channel{session = NSession}}; {error, _NotFound} -> @@ -335,7 +290,7 @@ handle_in(?PUBCOMP_PACKET(PacketId, _ReasonCode), Channel = #channel{session = S handle_in(Packet = ?SUBSCRIBE_PACKET(PacketId, Properties, TopicFilters), Channel = #channel{client = Client}) -> - case validate_in(Packet, Channel) of + case validate_packet(Packet, Channel) of ok -> TopicFilters1 = [emqx_topic:parse(TopicFilter, SubOpts) || {TopicFilter, SubOpts} <- TopicFilters], @@ -344,23 +299,23 @@ handle_in(Packet = ?SUBSCRIBE_PACKET(PacketId, Properties, TopicFilters), TopicFilters1), TopicFilters3 = enrich_subid(Properties, TopicFilters2), {ReasonCodes, NChannel} = process_subscribe(TopicFilters3, Channel), - handle_out(suback, {PacketId, ReasonCodes}, NChannel); + handle_out({suback, PacketId, ReasonCodes}, NChannel); {error, ReasonCode} -> - handle_out(disconnect, ReasonCode, Channel) + handle_out({disconnect, ReasonCode}, Channel) end; handle_in(Packet = ?UNSUBSCRIBE_PACKET(PacketId, Properties, TopicFilters), Channel = #channel{client = Client}) -> - case validate_in(Packet, Channel) of + case validate_packet(Packet, Channel) of ok -> TopicFilters1 = lists:map(fun emqx_topic:parse/1, TopicFilters), TopicFilters2 = emqx_hooks:run_fold('client.unsubscribe', [Client, Properties], TopicFilters1), {ReasonCodes, NChannel} = process_unsubscribe(TopicFilters2, Channel), - handle_out(unsuback, {PacketId, ReasonCodes}, NChannel); + handle_out({unsuback, PacketId, ReasonCodes}, NChannel); {error, ReasonCode} -> - handle_out(disconnect, ReasonCode, Channel) + handle_out({disconnect, ReasonCode}, Channel) end; handle_in(?PACKET(?PINGREQ), Channel) -> @@ -368,9 +323,10 @@ handle_in(?PACKET(?PINGREQ), Channel) -> handle_in(?DISCONNECT_PACKET(?RC_SUCCESS), Channel) -> %% Clear will msg - {stop, normal, Channel#channel{will_msg = undefined}}; + {stop, normal, Channel}; -handle_in(?DISCONNECT_PACKET(RC), Channel = #channel{proto_ver = Ver}) -> +handle_in(?DISCONNECT_PACKET(RC), Channel = #channel{protocol = Protocol}) -> + Ver = emqx_protocol:info(proto_ver, Protocol), {stop, {shutdown, emqx_reason_codes:name(RC, Ver)}, Channel}; handle_in(?AUTH_PACKET(), Channel) -> @@ -388,17 +344,12 @@ handle_in(Packet, Channel) -> process_connect(ConnPkt, Channel) -> case open_session(ConnPkt, Channel) of {ok, Session, SP} -> - WillMsg = emqx_packet:will_msg(ConnPkt), - NChannel = Channel#channel{session = Session, - will_msg = WillMsg, - connected = true, - connected_at = os:timestamp() - }, - handle_out(connack, {?RC_SUCCESS, sp(SP)}, NChannel); + NChannel = Channel#channel{session = Session}, + handle_out({connack, ?RC_SUCCESS, sp(SP)}, NChannel); {error, Reason} -> %% TODO: Unknown error? ?LOG(error, "Failed to open session: ~p", [Reason]), - handle_out(connack, ?RC_UNSPECIFIED_ERROR, Channel) + handle_out({connack, ?RC_UNSPECIFIED_ERROR}, Channel) end. %%-------------------------------------------------------------------- @@ -420,17 +371,18 @@ process_publish(_PacketId, Msg = #message{qos = ?QOS_0}, Channel) -> process_publish(PacketId, Msg = #message{qos = ?QOS_1}, Channel) -> Deliveries = emqx_broker:publish(Msg), ReasonCode = emqx_reason_codes:puback(Deliveries), - handle_out(puback, {PacketId, ReasonCode}, Channel); + handle_out({puback, PacketId, ReasonCode}, Channel); process_publish(PacketId, Msg = #message{qos = ?QOS_2}, Channel = #channel{session = Session}) -> case emqx_session:publish(PacketId, Msg, Session) of {ok, Deliveries, NSession} -> ReasonCode = emqx_reason_codes:puback(Deliveries), - handle_out(pubrec, {PacketId, ReasonCode}, - Channel#channel{session = NSession}); + NChannel = Channel#channel{session = NSession}, + handle_out({pubrec, PacketId, ReasonCode}, + ensure_timer(await_timer, NChannel)); {error, ReasonCode} -> - handle_out(pubrec, {PacketId, ReasonCode}, Channel) + handle_out({pubrec, PacketId, ReasonCode}, Channel) end. %%-------------------------------------------------------------------- @@ -474,8 +426,8 @@ process_unsubscribe([{TopicFilter, SubOpts}|More], Acc, Channel) -> {RC, Channel1} = do_unsubscribe(TopicFilter, SubOpts, Channel), process_unsubscribe(More, [RC|Acc], Channel1). -do_unsubscribe(TopicFilter, _SubOpts, Channel = #channel{client = Client, - session = Session}) -> +do_unsubscribe(TopicFilter, _SubOpts, + Channel = #channel{client = Client, session = Session}) -> case emqx_session:unsubscribe(Client, mount(Client, TopicFilter), Session) of {ok, NSession} -> {?RC_SUCCESS, Channel#channel{session = NSession}}; @@ -486,72 +438,21 @@ do_unsubscribe(TopicFilter, _SubOpts, Channel = #channel{client = Client, %% Handle outgoing packet %%-------------------------------------------------------------------- -handle_out(Deliver = {deliver, _Topic, _Msg}, - Channel = #channel{resuming = true, pendings = Pendings}) -> - Delivers = emqx_misc:drain_deliver([Deliver]), - {ok, Channel#channel{pendings = lists:append(Pendings, Delivers)}}; +handle_out({connack, ?RC_SUCCESS, SP}, Channel = #channel{client = Client}) -> + ok = emqx_hooks:run('client.connected', + [Client, ?RC_SUCCESS, attrs(Channel)]), + AckProps = emqx_misc:run_fold([fun enrich_caps/2, + fun enrich_server_keepalive/2, + fun enrich_assigned_clientid/2 + ], #{}, Channel), + NChannel = ensure_keepalive(AckProps, ensure_connected(Channel)), + {ok, ?CONNACK_PACKET(?RC_SUCCESS, SP, AckProps), NChannel}; -handle_out(Deliver = {deliver, _Topic, _Msg}, Channel = #channel{session = Session}) -> - Delivers = emqx_misc:drain_deliver([Deliver]), - case emqx_session:deliver(Delivers, Session) of - {ok, Publishes, NSession} -> - handle_out(publish, Publishes, Channel#channel{session = NSession}); - {ok, NSession} -> - {ok, Channel#channel{session = NSession}} - end; - -handle_out({publish, PacketId, Msg}, Channel = #channel{client = Client}) -> - Msg1 = emqx_hooks:run_fold('message.deliver', [Client], - emqx_message:update_expiry(Msg)), - Packet = emqx_packet:from_message(PacketId, unmount(Client, Msg1)), - {ok, Packet, Channel}. - -handle_out(connack, {?RC_SUCCESS, SP}, - Channel = #channel{client = Client = #{zone := Zone}, - ack_props = AckProps, - alias_maximum = AliasMaximum}) -> - ok = emqx_hooks:run('client.connected', [Client, ?RC_SUCCESS, attrs(Channel)]), - #{max_packet_size := MaxPktSize, - max_qos_allowed := MaxQoS, - retain_available := Retain, - max_topic_alias := MaxAlias, - shared_subscription := Shared, - wildcard_subscription := Wildcard - } = caps(Channel), - %% 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. - AckProps1 = if AckProps == undefined -> #{}; true -> AckProps end, - AckProps2 = AckProps1#{'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), - 'Maximum-QoS' => MaxQoS - }, - AckProps3 = case emqx_zone:get_env(Zone, server_keepalive) of - undefined -> AckProps2; - Keepalive -> AckProps2#{'Server-Keep-Alive' => Keepalive} - end, - AliasMaximum1 = set_property(inbound, MaxAlias, AliasMaximum), - Channel1 = Channel#channel{alias_maximum = AliasMaximum1, - ack_props = undefined - }, - {ok, ?CONNACK_PACKET(?RC_SUCCESS, SP, AckProps3), Channel1}; - -handle_out(connack, ReasonCode, Channel = #channel{client = Client, - proto_ver = ProtoVer}) -> +handle_out({connack, ReasonCode}, Channel = #channel{client = Client, + protocol = Protocol + }) -> ok = emqx_hooks:run('client.connected', [Client, ReasonCode, attrs(Channel)]), + ProtoVer = emqx_protocol:info(proto_ver, Protocol), ReasonCode1 = if ProtoVer == ?MQTT_PROTO_V5 -> ReasonCode; true -> emqx_reason_codes:compat(connack, ReasonCode) @@ -559,52 +460,81 @@ handle_out(connack, ReasonCode, Channel = #channel{client = Client, Reason = emqx_reason_codes:name(ReasonCode1, ProtoVer), {stop, {shutdown, Reason}, ?CONNACK_PACKET(ReasonCode1), Channel}; -handle_out(publish, Publishes, Channel) -> - Packets = [element(2, handle_out(Publish, Channel)) || Publish <- Publishes], +handle_out({deliver, Delivers}, Channel = #channel{resuming = true, + pendings = Pendings + }) -> + {ok, Channel#channel{pendings = lists:append(Pendings, Delivers)}}; + +handle_out({deliver, Delivers}, Channel = #channel{session = Session}) -> + case emqx_session:deliver(Delivers, Session) of + {ok, Publishes, NSession} -> + NChannel = Channel#channel{session = NSession}, + handle_out({publish, Publishes}, ensure_timer(retry_timer, NChannel)); + {ok, NSession} -> + {ok, Channel#channel{session = NSession}} + end; + +handle_out({publish, Publishes}, Channel) -> + Packets = lists:map( + fun(Publish) -> + element(2, handle_out(Publish, Channel)) + end, Publishes), {ok, Packets, Channel}; +handle_out({publish, PacketId, Msg}, Channel = #channel{client = Client}) -> + Msg1 = emqx_hooks:run_fold('message.deliver', [Client], + emqx_message:update_expiry(Msg)), + Packet = emqx_packet:from_message(PacketId, unmount(Client, Msg1)), + {ok, Packet, Channel}; + %% TODO: How to handle the puberr? -handle_out(puberr, _ReasonCode, Channel) -> +handle_out({puberr, _ReasonCode}, Channel) -> {ok, Channel}; -handle_out(puback, {PacketId, ReasonCode}, Channel) -> +handle_out({puback, PacketId, ReasonCode}, Channel) -> {ok, ?PUBACK_PACKET(PacketId, ReasonCode), Channel}; -handle_out(pubrel, {PacketId, ReasonCode}, Channel) -> +handle_out({pubrel, PacketId, ReasonCode}, Channel) -> {ok, ?PUBREL_PACKET(PacketId, ReasonCode), Channel}; -handle_out(pubrec, {PacketId, ReasonCode}, Channel) -> +handle_out({pubrec, PacketId, ReasonCode}, Channel) -> {ok, ?PUBREC_PACKET(PacketId, ReasonCode), Channel}; -handle_out(pubcomp, {PacketId, ReasonCode}, 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], +handle_out({suback, PacketId, ReasonCodes}, + Channel = #channel{protocol = Protocol}) -> + ReasonCodes1 = + case emqx_protocol:info(proto_ver, Protocol) of + ?MQTT_PROTO_V5 -> ReasonCodes; + _Ver -> + [emqx_reason_codes:compat(suback, RC) || RC <- ReasonCodes] + end, {ok, ?SUBACK_PACKET(PacketId, ReasonCodes1), Channel}; -handle_out(unsuback, {PacketId, ReasonCodes}, - Channel = #channel{proto_ver = ?MQTT_PROTO_V5}) -> - {ok, ?UNSUBACK_PACKET(PacketId, ReasonCodes), Channel}; +handle_out({unsuback, PacketId, ReasonCodes}, + Channel = #channel{protocol = Protocol}) -> + Packet = case emqx_protocol:info(proto_ver, Protocol) of + ?MQTT_PROTO_V5 -> + ?UNSUBACK_PACKET(PacketId, ReasonCodes); + %% Ignore reason codes if not MQTT5 + _Ver -> ?UNSUBACK_PACKET(PacketId) + end, + {ok, Packet, Channel}; -%% Ignore reason codes if not MQTT5 -handle_out(unsuback, {PacketId, _ReasonCodes}, Channel) -> - {ok, ?UNSUBACK_PACKET(PacketId), Channel}; +handle_out({disconnect, ReasonCode}, Channel = #channel{protocol = Protocol}) -> + case emqx_protocol:info(proto_ver, Protocol) of + ?MQTT_PROTO_V5 -> + Reason = emqx_reason_codes:name(ReasonCode), + Packet = ?DISCONNECT_PACKET(ReasonCode), + {stop, {shutdown, Reason}, Packet, Channel}; + ProtoVer -> + Reason = emqx_reason_codes:name(ReasonCode, ProtoVer), + {stop, {shutdown, Reason}, Channel} + end; -handle_out(disconnect, ReasonCode, Channel = #channel{proto_ver = ?MQTT_PROTO_V5}) -> - Reason = emqx_reason_codes:name(ReasonCode), - {stop, {shutdown, Reason}, ?DISCONNECT_PACKET(ReasonCode), Channel}; - -handle_out(disconnect, ReasonCode, Channel = #channel{proto_ver = ProtoVer}) -> - {stop, {shutdown, emqx_reason_codes:name(ReasonCode, ProtoVer)}, Channel}; - -handle_out(Type, Data, Channel) -> +handle_out({Type, Data}, Channel) -> ?LOG(error, "Unexpected outgoing: ~s, ~p", [Type, Data]), {ok, Channel}. @@ -612,6 +542,18 @@ handle_out(Type, Data, Channel) -> %% Handle call %%-------------------------------------------------------------------- +%%-------------------------------------------------------------------- +%% Takeover session +%%-------------------------------------------------------------------- + +handle_call({takeover, 'begin'}, Channel = #channel{session = Session}) -> + {ok, Session, Channel#channel{resuming = true}}; + +handle_call({takeover, 'end'}, Channel = #channel{session = Session, + pendings = Pendings}) -> + ok = emqx_session:takeover(Session), + {stop, {shutdown, takeovered}, Pendings, Channel}; + handle_call(Req, Channel) -> ?LOG(error, "Unexpected call: Req", [Req]), {ok, ignored, Channel}. @@ -659,16 +601,49 @@ handle_info(Info, Channel) -> -> {ok, channel()} | {ok, Result :: term(), channel()} | {stop, Reason :: term(), channel()}). -timeout(TRef, {emit_stats, Stats}, Channel = #channel{stats_timer = TRef}) -> - ClientId = info(client_id, Channel), +timeout(TRef, {emit_stats, Stats}, + Channel = #channel{client = #{client_id := ClientId}, + timers = #{stats_timer := TRef} + }) -> ok = emqx_cm:set_chan_stats(ClientId, Stats), - {ok, Channel#channel{stats_timer = undefined}}; + {ok, clean_timer(stats_timer, Channel)}; -timeout(TRef, retry_deliver, Channel = #channel{%%session = Session, - retry_timer = TRef}) -> - %% case emqx_session:retry(Session) of - %% TODO: ... - {ok, Channel#channel{retry_timer = undefined}}; +timeout(TRef, {keepalive, StatVal}, Channel = #channel{keepalive = Keepalive, + timers = #{alive_timer := TRef} + }) -> + case emqx_keepalive:check(StatVal, Keepalive) of + {ok, NKeepalive} -> + NChannel = Channel#channel{keepalive = NKeepalive}, + {ok, reset_timer(alive_timer, NChannel)}; + {error, timeout} -> + {stop, {shutdown, keepalive_timeout}, Channel} + end; + +timeout(TRef, retry_delivery, Channel = #channel{session = Session, + timers = #{retry_timer := TRef} + }) -> + case emqx_session:retry(Session) of + {ok, NSession} -> + {ok, clean_timer(retry_timer, Channel#channel{session = NSession})}; + {ok, Publishes, NSession} -> + NChannel = Channel#channel{session = NSession}, + handle_out({publish, Publishes}, reset_timer(retry_timer, NChannel)); + {ok, Publishes, Timeout, NSession} -> + NChannel = Channel#channel{session = NSession}, + handle_out({publish, Publishes}, reset_timer(retry_timer, Timeout, NChannel)) + end; + +timeout(TRef, expire_awaiting_rel, Channel = #channel{session = Session, + timers = #{await_timer := TRef}}) -> + case emqx_session:expire(awaiting_rel, Session) of + {ok, Session} -> + {ok, clean_timer(await_timer, Channel#channel{session = Session})}; + {ok, Timeout, Session} -> + {ok, reset_timer(await_timer, Timeout, Channel#channel{session = Session})} + end; + +timeout(_TRef, expire_session, Channel) -> + {ok, Channel}; timeout(_TRef, Msg, Channel) -> ?LOG(error, "Unexpected timeout: ~p~n", [Msg]), @@ -678,20 +653,39 @@ timeout(_TRef, Msg, Channel) -> %% Ensure timers %%-------------------------------------------------------------------- -ensure_timer(emit_stats, Channel = #channel{stats_timer = undefined, - idle_timeout = IdleTimeout - }) -> - Channel#channel{stats_timer = start_timer(IdleTimeout, emit_stats)}; +ensure_timer(Name, Channel = #channel{timers = Timers}) -> + TRef = maps:get(Name, Timers, undefined), + Time = interval(Name, Channel), + case TRef == undefined andalso Time > 0 of + true -> + ensure_timer(Name, Time, Channel); + false -> Channel %% Timer disabled or exists + end. -ensure_timer(retry, Channel = #channel{session = Session, - retry_timer = undefined}) -> - Interval = emqx_session:info(retry_interval, Session), - TRef = emqx_misc:start_timer(Interval, retry_deliver), - Channel#channel{retry_timer = TRef}; +ensure_timer(Name, Time, Channel = #channel{timers = Timers}) -> + Msg = maps:get(Name, ?TIMER_TABLE), + TRef = emqx_misc:start_timer(Time, Msg), + Channel#channel{timers = Timers#{Name => TRef}}. -%% disabled or timer existed -ensure_timer(_Name, Channel) -> - Channel. +reset_timer(Name, Channel) -> + ensure_timer(Name, clean_timer(Name, Channel)). + +reset_timer(Name, Time, Channel) -> + ensure_timer(Name, Time, clean_timer(Name, Channel)). + +clean_timer(Name, Channel = #channel{timers = Timers}) -> + Channel#channel{timers = maps:remove(Name, Timers)}. + +interval(stats_timer, #channel{client = #{zone := Zone}}) -> + emqx_zone:get_env(Zone, idle_timeout, 30000); +interval(alive_timer, #channel{keepalive = KeepAlive}) -> + emqx_keepalive:info(interval, KeepAlive); +interval(retry_timer, #channel{session = Session}) -> + emqx_session:info(retry_interval, Session); +interval(await_timer, #channel{session = Session}) -> + emqx_session:info(await_rel_timeout, Session); +interval(expire_timer, #channel{session = Session}) -> + emqx_session:info(expiry_interval, Session). %%-------------------------------------------------------------------- %% Terminate @@ -699,9 +693,14 @@ ensure_timer(_Name, Channel) -> terminate(normal, #channel{client = Client}) -> ok = emqx_hooks:run('client.disconnected', [Client, normal]); -terminate(Reason, #channel{client = Client, will_msg = WillMsg}) -> +terminate(Reason, #channel{client = Client, + protocol = Protocol + }) -> ok = emqx_hooks:run('client.disconnected', [Client, Reason]), - publish_will_msg(WillMsg). + if + Protocol == undefined -> ok; + true -> publish_will_msg(emqx_protocol:info(will_msg, Protocol)) + end. %%TODO: Improve will msg:) publish_will_msg(undefined) -> @@ -720,13 +719,10 @@ gc(Cnt, Oct, Channel = #channel{gc_state = GCSt}) -> Ok andalso emqx_metrics:inc('channel.gc.cnt'), Channel#channel{gc_state = GCSt1}. -%%-------------------------------------------------------------------- -%% Validate incoming packet -%%-------------------------------------------------------------------- - --spec(validate_in(emqx_types:packet(), channel()) +%% @doc Validate incoming packet. +-spec(validate_packet(emqx_types:packet(), channel()) -> ok | {error, emqx_types:reason_code()}). -validate_in(Packet, _Channel) -> +validate_packet(Packet, _Channel) -> try emqx_packet:validate(Packet) of true -> ok catch @@ -744,23 +740,6 @@ validate_in(Packet, _Channel) -> {error, ?RC_MALFORMED_PACKET} end. -%%-------------------------------------------------------------------- -%% Preprocess properties -%%-------------------------------------------------------------------- - -process_props(#mqtt_packet_connect{ - properties = #{'Topic-Alias-Maximum' := Max} - }, - Channel = #channel{alias_maximum = AliasMaximum}) -> - NAliasMaximum = if AliasMaximum == undefined -> - #{outbound => Max}; - true -> AliasMaximum#{outbound => Max} - end, - {ok, Channel#channel{alias_maximum = NAliasMaximum}}; - -process_props(Packet, Channel) -> - {ok, Packet, Channel}. - %%-------------------------------------------------------------------- %% Check connect packet %%-------------------------------------------------------------------- @@ -836,6 +815,9 @@ check_will_retain(#mqtt_packet_connect{will_retain = true}, false -> {error, ?RC_RETAIN_NOT_SUPPORTED} end. +init_protocol(ConnPkt, Channel) -> + {ok, Channel#channel{protocol = emqx_protocol:init(ConnPkt)}}. + %%-------------------------------------------------------------------- %% Enrich client %%-------------------------------------------------------------------- @@ -858,11 +840,10 @@ maybe_use_username_as_clientid(_ConnPkt, Channel = #channel{client = Client = #{ {ok, Channel#channel{client = NClient}}. maybe_assign_clientid(#mqtt_packet_connect{client_id = <<>>}, - Channel = #channel{client = Client, - ack_props = AckProps}) -> - ClientId = emqx_guid:to_base62(emqx_guid:gen()), - AckProps1 = set_property('Assigned-Client-Identifier', ClientId, AckProps), - {ok, Channel#channel{client = Client#{client_id => ClientId}, ack_props = AckProps1}}; + Channel = #channel{client = Client}) -> + RandClientId = emqx_guid:to_base62(emqx_guid:gen()), + {ok, Channel#channel{client = Client#{client_id => RandClientId}}}; + maybe_assign_clientid(#mqtt_packet_connect{client_id = ClientId}, Channel = #channel{client = Client}) -> {ok, Channel#channel{client = Client#{client_id => ClientId}}}. @@ -878,6 +859,10 @@ set_rest_client_fields(#mqtt_packet_connect{is_bridge = IsBridge}, Channel = #channel{client = Client}) -> {ok, Channel#channel{client = Client#{is_bridge => IsBridge}}}. +%% @doc Set logger metadata. +set_logger_meta(_ConnPkt, #channel{client = #{client_id := ClientId}}) -> + emqx_logger:set_metadata_client_id(ClientId). + %%-------------------------------------------------------------------- %% Auth Connect %%-------------------------------------------------------------------- @@ -886,7 +871,7 @@ auth_connect(#mqtt_packet_connect{client_id = ClientId, username = Username, password = Password}, Channel = #channel{client = Client}) -> - case authenticate(Client#{password => Password}) of + case emqx_access_control:authenticate(Client#{password => Password}) of {ok, AuthResult} -> {ok, Channel#channel{client = maps:merge(Client, AuthResult)}}; {error, Reason} -> @@ -906,7 +891,7 @@ open_session(#mqtt_packet_connect{clean_start = CleanStart, emqx_zone:get_env(Zone, max_inflight, 65535)), Interval = get_property('Session-Expiry-Interval', ConnProps, emqx_zone:get_env(Zone, session_expiry_interval, 0)), - emqx_cm:open_session(CleanStart, Client, #{max_inflight => MaxInflight, + emqx_cm:open_session(CleanStart, Client, #{max_inflight => MaxInflight, expiry_interval => Interval }). @@ -918,8 +903,9 @@ process_alias(Packet = #mqtt_packet{ variable = #mqtt_packet_publish{topic_name = <<>>, properties = #{'Topic-Alias' := AliasId} } = Publish - }, Channel = #channel{topic_aliases = Aliases}) -> - case find_alias(AliasId, Aliases) of + }, + Channel = #channel{protocol = Protocol}) -> + case emqx_protocol:find_alias(AliasId, Protocol) of {ok, Topic} -> {ok, Packet#mqtt_packet{ variable = Publish#mqtt_packet_publish{ @@ -931,22 +917,12 @@ process_alias(#mqtt_packet{ variable = #mqtt_packet_publish{topic_name = Topic, properties = #{'Topic-Alias' := AliasId} } - }, Channel = #channel{topic_aliases = Aliases}) -> - {ok, Channel#channel{topic_aliases = save_alias(AliasId, Topic, Aliases)}}; + }, Channel = #channel{protocol = Protocol}) -> + {ok, Channel#channel{protocol = emqx_protocol:save_alias(AliasId, Topic, Protocol)}}; process_alias(_Packet, Channel) -> {ok, Channel}. -find_alias(_AliasId, undefined) -> - false; -find_alias(AliasId, Aliases) -> - maps:find(AliasId, Aliases). - -save_alias(AliasId, Topic, undefined) -> - #{AliasId => Topic}; -save_alias(AliasId, Topic, Aliases) -> - maps:put(AliasId, Topic, Aliases). - %% Check Publish check_publish(Packet, Channel) -> pipeline([fun check_pub_acl/2, @@ -968,7 +944,9 @@ check_pub_alias(#mqtt_packet{ properties = #{'Topic-Alias' := AliasId} } }, - #channel{alias_maximum = Limits}) -> + #channel{protocol = Protocol}) -> + %% TODO: Move to Protocol + Limits = emqx_protocol:info(alias_maximum, Protocol), case (Limits == undefined) orelse (Max = maps:get(inbound, Limits, 0)) == 0 orelse (AliasId > Max) of @@ -1009,12 +987,65 @@ enrich_subid(#{'Subscription-Identifier' := SubId}, TopicFilters) -> enrich_subid(_Properties, TopicFilters) -> TopicFilters. -enrich_subopts(SubOpts, #channel{proto_ver = ?MQTT_PROTO_V5}) -> - SubOpts; -enrich_subopts(SubOpts, #channel{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}. +enrich_subopts(SubOpts, #channel{client = Client, protocol = Proto}) -> + #{zone := Zone, is_bridge := IsBridge} = Client, + case emqx_protocol:info(proto_ver, Proto) of + ?MQTT_PROTO_V5 -> SubOpts; + _Ver -> Rap = flag(IsBridge), + Nl = flag(emqx_zone:get_env(Zone, ignore_loop_deliver, false)), + SubOpts#{rap => Rap, nl => Nl} + end. + +enrich_caps(AckProps, #channel{client = #{zone := Zone}, protocol = Protocol}) -> + case emqx_protocol:info(proto_ver, Protocol) of + ?MQTT_PROTO_V5 -> + #{max_packet_size := MaxPktSize, + max_qos_allowed := MaxQoS, + retain_available := Retain, + max_topic_alias := MaxAlias, + shared_subscription := Shared, + wildcard_subscription := Wildcard + } = emqx_mqtt_caps:get_caps(Zone), + AckProps#{'Retain-Available' => flag(Retain), + 'Maximum-Packet-Size' => MaxPktSize, + 'Topic-Alias-Maximum' => MaxAlias, + 'Wildcard-Subscription-Available' => flag(Wildcard), + 'Subscription-Identifier-Available' => 1, + 'Shared-Subscription-Available' => flag(Shared), + 'Maximum-QoS' => MaxQoS + }; + _Ver -> AckProps + end. + +enrich_server_keepalive(AckProps, #channel{client = #{zone := Zone}}) -> + case emqx_zone:get_env(Zone, server_keepalive) of + undefined -> AckProps; + Keepalive -> AckProps#{'Server-Keep-Alive' => Keepalive} + end. + +enrich_assigned_clientid(AckProps, #channel{client = #{client_id := ClientId}, + protocol = Protocol}) -> + case emqx_protocol:info(client_id, Protocol) of + <<>> -> %% Original ClientId. + AckProps#{'Assigned-Client-Identifier' => ClientId}; + _Origin -> AckProps + end. + +ensure_connected(Channel) -> + Channel#channel{connected = true, connected_at = os:timestamp()}. + +ensure_keepalive(#{'Server-Keep-Alive' := Interval}, Channel) -> + ensure_keepalive_timer(Interval, Channel); +ensure_keepalive(_AckProp, Channel = #channel{protocol = Protocol}) -> + case emqx_protocol:info(keepalive, Protocol) of + 0 -> Channel; + Interval -> ensure_keepalive_timer(Interval, Channel) + end. + +ensure_keepalive_timer(Interval, Channel = #channel{client = #{zone := Zone}}) -> + Backoff = emqx_zone:get_env(Zone, keepalive_backoff, 0.75), + Keepalive = emqx_keepalive:init(round(timer:seconds(Interval) * Backoff)), + ensure_timer(alive_timer, Channel#channel{keepalive = Keepalive}). %%-------------------------------------------------------------------- %% Is ACL enabled? @@ -1069,11 +1100,6 @@ pipeline([Fun|More], Packet, Channel) -> %% Helper functions %%-------------------------------------------------------------------- -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) -> diff --git a/src/emqx_cm.erl b/src/emqx_cm.erl index c393d6ab0..1e5842ec4 100644 --- a/src/emqx_cm.erl +++ b/src/emqx_cm.erl @@ -161,7 +161,7 @@ set_chan_stats(ClientId, ChanPid, Stats) -> open_session(true, Client = #{client_id := ClientId}, Options) -> CleanStart = fun(_) -> ok = discard_session(ClientId), - {ok, emqx_session:init(true, Client, Options), false} + {ok, emqx_session:init(Client, Options), false} end, emqx_cm_locker:trans(ClientId, CleanStart); @@ -169,12 +169,12 @@ open_session(false, Client = #{client_id := ClientId}, Options) -> ResumeStart = fun(_) -> case takeover_session(ClientId) of {ok, ConnMod, ChanPid, Session} -> - {ok, NSession} = emqx_session:resume(ClientId, Session), - {ok, Pendings} = ConnMod:takeover(ChanPid, 'end'), + NSession = emqx_session:resume(ClientId, Session), + Pendings = ConnMod:takeover(ChanPid, 'end'), io:format("Pending Delivers: ~p~n", [Pendings]), {ok, NSession, true}; {error, not_found} -> - {ok, emqx_session:init(false, Client, Options), false} + {ok, emqx_session:init(Client, Options), false} end end, emqx_cm_locker:trans(ClientId, ResumeStart). @@ -199,7 +199,7 @@ takeover_session(ClientId) -> takeover_session(ClientId, ChanPid) when node(ChanPid) == node() -> case get_chan_attrs(ClientId, ChanPid) of #{client := #{conn_mod := ConnMod}} -> - {ok, Session} = ConnMod:takeover(ChanPid, 'begin'), + Session = ConnMod:takeover(ChanPid, 'begin'), {ok, ConnMod, ChanPid, Session}; undefined -> {error, not_found} diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index 5ff534a2b..f6cd96108 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -35,7 +35,7 @@ ]). %% For Debug --export([state/1]). +-export([get_state/1]). -export([ kick/1 , discard/1 @@ -68,15 +68,14 @@ limit_timer :: maybe(reference()), parse_state :: emqx_frame:parse_state(), serialize :: fun((emqx_types:packet()) -> iodata()), - chan_state :: emqx_channel:channel(), - keepalive :: maybe(emqx_keepalive:keepalive()) + chan_state :: emqx_channel:channel() }). -type(state() :: #state{}). -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(CONN_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()) @@ -92,61 +91,63 @@ start_link(Transport, Socket, Options) -> -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, +info(#state{transport = Transport, + socket = Socket, + peername = Peername, + sockname = Sockname, conn_state = ConnState, - active_n = ActiveN, + active_n = ActiveN, rate_limit = RateLimit, - pub_limit = PubLimit, + pub_limit = PubLimit, chan_state = ChanState}) -> - ConnInfo = #{socktype => Transport:type(Socket), - peername => Peername, - sockname => Sockname, + ConnInfo = #{socktype => Transport:type(Socket), + peername => Peername, + sockname => Sockname, conn_state => ConnState, - active_n => ActiveN, + active_n => ActiveN, rate_limit => limit_info(RateLimit), - pub_limit => limit_info(PubLimit) + pub_limit => limit_info(PubLimit) }, - maps:merge(ConnInfo, emqx_channel:info(ChanState)). + ChanInfo = emqx_channel:info(ChanState), + maps:merge(ConnInfo, ChanInfo). -limit_info(undefined) -> - undefined; limit_info(Limit) -> - esockd_rate_limit:info(Limit). + emqx_misc:maybe_apply(fun esockd_rate_limit:info/1, Limit). %% @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, +attrs(#state{transport = Transport, + socket = Socket, + peername = Peername, + sockname = Sockname, chan_state = ChanState}) -> ConnAttrs = #{socktype => Transport:type(Socket), peername => Peername, sockname => Sockname }, - maps:merge(ConnAttrs, emqx_channel:attrs(ChanState)). + ChanAttrs = emqx_channel:attrs(ChanState), + maps:merge(ConnAttrs, ChanAttrs). %% @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, chan_state = ChanState}) -> + ProcStats = emqx_misc:proc_stats(), SockStats = case Transport:getstat(Socket, ?SOCK_STATS) of {ok, Ss} -> Ss; {error, _} -> [] end, - ChanStats = [{Name, emqx_pd:get_counter(Name)} || Name <- ?CHAN_STATS], - SessStats = emqx_session:stats(emqx_channel:info(session, ChanState)), - lists:append([SockStats, ChanStats, SessStats, emqx_misc:proc_stats()]). + ConnStats = [{Name, emqx_pd:get_counter(Name)} || Name <- ?CONN_STATS], + ChanStats = emqx_channel:stats(ChanState), + lists:append([ProcStats, SockStats, ConnStats, ChanStats]). -state(CPid) -> +-spec(get_state(pid()) -> state()). +get_state(CPid) -> call(CPid, get_state). -spec(kick(pid()) -> ok). @@ -157,8 +158,7 @@ kick(CPid) -> discard(CPid) -> gen_statem:cast(CPid, discard). -%% TODO: --spec(takeover(pid(), 'begin'|'end') -> {ok, Result :: term()}). +-spec(takeover(pid(), 'begin'|'end') -> Result :: term()). takeover(CPid, Phase) -> gen_statem:call(CPid, {takeover, Phase}). @@ -187,16 +187,16 @@ init({Transport, RawSocket, Options}) -> peercert => Peercert, conn_mod => ?MODULE}, Options), IdleTimout = emqx_zone:get_env(Zone, idle_timeout, 30000), - State = #state{transport = Transport, - socket = Socket, - peername = Peername, - sockname = Sockname, - conn_state = running, - active_n = ActiveN, - rate_limit = RateLimit, - pub_limit = PubLimit, - parse_state = ParseState, - chan_state = ChanState + State = #state{transport = Transport, + socket = Socket, + peername = Peername, + sockname = Sockname, + conn_state = running, + active_n = ActiveN, + rate_limit = RateLimit, + pub_limit = PubLimit, + parse_state = ParseState, + chan_state = ChanState }, gen_statem:enter_loop(?MODULE, [{hibernate_after, 2 * IdleTimout}], idle, State, self(), [IdleTimout]). @@ -242,18 +242,10 @@ idle(EventType, Content, State) -> %% Connected State connected(enter, _PrevSt, State = #state{chan_state = ChanState}) -> - ClientId = emqx_channel:info(client_id, ChanState), + #{client_id := ClientId} = emqx_channel:info(client, ChanState), ok = emqx_cm:register_channel(ClientId), - ok = emqx_cm:set_chan_attrs(ClientId, info(State)), - %% Ensure keepalive after connected successfully. - Interval = emqx_channel:info(keepalive, ChanState), - case ensure_keepalive(Interval, State) of - ignore -> keep_state(State); - {ok, KeepAlive} -> - keep_state(State#state{keepalive = KeepAlive}); - {error, Reason} -> - shutdown(Reason, State) - end; + ok = emqx_cm:set_chan_attrs(ClientId, attrs(State)), + keep_state_and_data; connected(cast, {incoming, Packet = ?PACKET(?CONNECT)}, State) -> ?LOG(warning, "Unexpected connect: ~p", [Packet]), @@ -265,7 +257,8 @@ connected(cast, {incoming, Packet}, State) when is_record(Packet, mqtt_packet) - connected(info, Deliver = {deliver, _Topic, _Msg}, State = #state{chan_state = ChanState}) -> - case emqx_channel:handle_out(Deliver, ChanState) of + Delivers = emqx_misc:drain_deliver([Deliver]), + case emqx_channel:handle_out({deliver, Delivers}, ChanState) of {ok, NChanState} -> keep_state(State#state{chan_state = NChanState}); {ok, Packets, NChanState} -> @@ -275,17 +268,6 @@ connected(info, Deliver = {deliver, _Topic, _Msg}, stop(Reason, State#state{chan_state = NChanState}) 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). @@ -326,16 +308,6 @@ handle({call, From}, kick, State) -> ok = gen_statem:reply(From, ok), shutdown(kicked, State); -handle({call, From}, {takeover, 'begin'}, State = #state{chan_state = ChanState}) -> - {ok, Session, NChanState} = emqx_channel:takeover('begin', ChanState), - ok = gen_statem:reply(From, {ok, Session}), - {next_state, takeovering, State#state{chan_state = NChanState}}; - -handle({call, From}, {takeover, 'end'}, State = #state{chan_state = ChanState}) -> - {ok, Delivers, NChanState} = emqx_channel:takeover('end', ChanState), - ok = gen_statem:reply(From, {ok, Delivers}), - shutdown(takeovered, State#state{chan_state = NChanState}); - handle({call, From}, Req, State = #state{chan_state = ChanState}) -> case emqx_channel:handle_call(Req, ChanState) of {ok, Reply, NChanState} -> @@ -362,22 +334,22 @@ handle(info, {Inet, _Sock, Data}, State = #state{chan_state = ChanState}) emqx_pd:update_counter(incoming_bytes, Oct), ok = emqx_metrics:inc('bytes.received', Oct), NChanState = emqx_channel:ensure_timer( - emit_stats, emqx_channel:gc(1, Oct, ChanState)), + stats_timer, emqx_channel:gc(1, Oct, ChanState)), process_incoming(Data, State#state{chan_state = NChanState}); handle(info, {Error, _Sock, Reason}, State) when Error == tcp_error; Error == ssl_error -> shutdown(Reason, State); +%%TODO: fixme later. handle(info, {Closed, _Sock}, State = #state{chan_state = ChanState}) when Closed == tcp_closed; Closed == ssl_closed -> - case emqx_channel:info(session, ChanState) of + case emqx_channel:info(protocol, ChanState) of undefined -> shutdown(closed, State); - Session -> - case emqx_session:info(clean_start, Session) of - true -> shutdown(closed, State); - false -> {next_state, disconnected, State} - end + #{clean_start := true} -> + shutdown(closed, State); + #{clean_start := false} -> + {next_state, disconnected, State} end; handle(info, {Passive, _Sock}, State) when Passive == tcp_passive; @@ -402,12 +374,22 @@ handle(info, activate_socket, State) -> handle(info, {inet_reply, _Sock, ok}, State = #state{chan_state = ChanState}) -> %% something sent - NChanState = emqx_channel:ensure_timer(emit_stats, ChanState), + NChanState = emqx_channel:ensure_timer(stats_timer, ChanState), keep_state(State#state{chan_state = NChanState}); handle(info, {inet_reply, _Sock, {error, Reason}}, State) -> shutdown(Reason, State); +handle(info, {timeout, TRef, keepalive}, + State = #state{transport = Transport, socket = Socket}) + when is_reference(TRef) -> + case Transport:getstat(Socket, [recv_oct]) of + {ok, [{recv_oct, RecvOct}]} -> + handle_timeout(TRef, {keepalive, RecvOct}, State); + {error, Reason} -> + shutdown(Reason, State) + end; + handle(info, {timeout, TRef, emit_stats}, State) when is_reference(TRef) -> handle_timeout(TRef, {emit_stats, stats(State)}, State); @@ -434,12 +416,9 @@ code_change(_Vsn, State, Data, _Extra) -> terminate(Reason, _StateName, #state{transport = Transport, socket = Socket, - keepalive = KeepAlive, chan_state = ChanState}) -> ?LOG(debug, "Terminated for ~p", [Reason]), ok = Transport:fast_close(Socket), - KeepAlive =/= undefined - andalso emqx_keepalive:cancel(KeepAlive), emqx_channel:terminate(Reason, ChanState). %%-------------------------------------------------------------------- @@ -539,24 +518,6 @@ handle_timeout(TRef, Msg, State = #state{chan_state = ChanState}) -> stop(Reason, State#state{chan_state = NChanState}) end. -%%-------------------------------------------------------------------- -%% Ensure keepalive - -ensure_keepalive(0, _State) -> - ignore; -ensure_keepalive(Interval, #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:info(zone, ChanState), - keepalive_backoff, 0.75), - emqx_keepalive:start(StatFun, round(Interval * Backoff), {keepalive, check}). %%-------------------------------------------------------------------- %% Ensure rate limit diff --git a/src/emqx_keepalive.erl b/src/emqx_keepalive.erl index 88848f7ac..6ce970b54 100644 --- a/src/emqx_keepalive.erl +++ b/src/emqx_keepalive.erl @@ -16,78 +16,58 @@ -module(emqx_keepalive). -%% APIs --export([ start/3 - , check/1 - , cancel/1 +-export([ init/1 + , info/1 + , info/2 + , check/2 ]). -export_type([keepalive/0]). -record(keepalive, { - statfun :: statfun(), - statval :: integer(), - tsec :: pos_integer(), - tmsg :: term(), - tref :: reference(), - repeat = 0 :: non_neg_integer() + interval :: pos_integer(), + statval :: non_neg_integer(), + repeat :: non_neg_integer() }). --type(statfun() :: fun(() -> {ok, integer()} | {error, term()})). - -opaque(keepalive() :: #keepalive{}). -%%-------------------------------------------------------------------- -%% APIs -%%-------------------------------------------------------------------- +%% @doc Init keepalive. +-spec(init(Interval :: non_neg_integer()) -> keepalive()). +init(Interval) when Interval > 0 -> + #keepalive{interval = Interval, + statval = 0, + repeat = 0}. -%% @doc Start a keepalive --spec(start(statfun(), pos_integer(), term()) - -> {ok, keepalive()} | {error, term()}). -start(StatFun, TimeoutSec, TimeoutMsg) when TimeoutSec > 0 -> - try StatFun() of - {ok, StatVal} -> - TRef = timer(TimeoutSec, TimeoutMsg), - {ok, #keepalive{statfun = StatFun, - statval = StatVal, - tsec = TimeoutSec, - tmsg = TimeoutMsg, - tref = TRef}}; - {error, Error} -> - {error, Error} - catch - _Error:Reason -> - {error, Reason} +%% @doc Get Info of the keepalive. +-spec(info(keepalive()) -> emqx_types:infos()). +info(#keepalive{interval = Interval, + statval = StatVal, + repeat = Repeat}) -> + #{interval => Interval, + statval => StatVal, + repeat => Repeat + }. + +-spec(info(interval|statval|repeat, keepalive()) + -> non_neg_integer()). +info(interval, #keepalive{interval = Interval}) -> + Interval; +info(statval, #keepalive{statval = StatVal}) -> + StatVal; +info(repeat, #keepalive{repeat = Repeat}) -> + Repeat. + +%% @doc Check keepalive. +-spec(check(non_neg_integer(), keepalive()) + -> {ok, keepalive()} | {error, timeout}). +check(NewVal, KeepAlive = #keepalive{statval = OldVal, + repeat = Repeat}) -> + if + NewVal =/= OldVal -> + {ok, KeepAlive#keepalive{statval = NewVal, repeat = 0}}; + Repeat < 1 -> + {ok, KeepAlive#keepalive{repeat = Repeat + 1}}; + true -> {error, timeout} end. -%% @doc Check keepalive, called when timeout... --spec(check(keepalive()) -> {ok, keepalive()} | {error, term()}). -check(KeepAlive = #keepalive{statfun = StatFun, statval = LastVal, repeat = Repeat}) -> - try StatFun() of - {ok, NewVal} -> - if NewVal =/= LastVal -> - {ok, resume(KeepAlive#keepalive{statval = NewVal, repeat = 0})}; - Repeat < 1 -> - {ok, resume(KeepAlive#keepalive{statval = NewVal, repeat = Repeat + 1})}; - true -> - {error, timeout} - end; - {error, Error} -> - {error, Error} - catch - _Error:Reason -> - {error, Reason} - end. - --spec(resume(keepalive()) -> keepalive()). -resume(KeepAlive = #keepalive{tsec = TimeoutSec, tmsg = TimeoutMsg}) -> - KeepAlive#keepalive{tref = timer(TimeoutSec, TimeoutMsg)}. - -%% @doc Cancel Keepalive --spec(cancel(keepalive()) -> ok). -cancel(#keepalive{tref = TRef}) when is_reference(TRef) -> - catch erlang:cancel_timer(TRef), ok. - -timer(Secs, Msg) -> - erlang:send_after(timer:seconds(Secs), self(), Msg). - diff --git a/src/emqx_misc.erl b/src/emqx_misc.erl index 007f444f4..94f859a82 100644 --- a/src/emqx_misc.erl +++ b/src/emqx_misc.erl @@ -16,7 +16,11 @@ -module(emqx_misc). +-include("types.hrl"). + -export([ merge_opts/2 + , maybe_apply/2 + , run_fold/3 , start_timer/2 , start_timer/3 , cancel_timer/1 @@ -44,6 +48,19 @@ merge_opts(Defaults, Options) -> lists:usort([Opt | Acc]) end, Defaults, Options). +%% @doc Apply a function to a maybe argument. +-spec(maybe_apply(fun((maybe(A)) -> maybe(A)), maybe(A)) + -> maybe(A) when A :: any()). +maybe_apply(_Fun, undefined) -> + undefined; +maybe_apply(Fun, Arg) when is_function(Fun) -> + erlang:apply(Fun, [Arg]). + +run_fold([], Acc, _State) -> + Acc; +run_fold([Fun|More], Acc, State) -> + run_fold(More, Fun(Acc, State), State). + -spec(start_timer(integer(), term()) -> reference()). start_timer(Interval, Msg) -> start_timer(Interval, self(), Msg). @@ -52,7 +69,7 @@ start_timer(Interval, Msg) -> start_timer(Interval, Dest, Msg) -> erlang:start_timer(Interval, Dest, Msg). --spec(cancel_timer(undefined | reference()) -> ok). +-spec(cancel_timer(maybe(reference())) -> ok). cancel_timer(Timer) when is_reference(Timer) -> case erlang:cancel_timer(Timer) of false -> diff --git a/src/emqx_packet.erl b/src/emqx_packet.erl index ea5657f41..4b0912d3f 100644 --- a/src/emqx_packet.erl +++ b/src/emqx_packet.erl @@ -19,7 +19,7 @@ -include("emqx.hrl"). -include("emqx_mqtt.hrl"). --export([ protocol_name/1 +-export([ proto_name/1 , type_name/1 , validate/1 , format/1 @@ -28,18 +28,20 @@ , will_msg/1 ]). -%% @doc Protocol name of version --spec(protocol_name(emqx_types:version()) -> binary()). -protocol_name(?MQTT_PROTO_V3) -> +-compile(inline). + +%% @doc Protocol name of the version. +-spec(proto_name(emqx_types:version()) -> binary()). +proto_name(?MQTT_PROTO_V3) -> <<"MQIsdp">>; -protocol_name(?MQTT_PROTO_V4) -> +proto_name(?MQTT_PROTO_V4) -> <<"MQTT">>; -protocol_name(?MQTT_PROTO_V5) -> +proto_name(?MQTT_PROTO_V5) -> <<"MQTT">>. -%% @doc Name of MQTT packet type +%% @doc Name of MQTT packet type. -spec(type_name(emqx_types:packet_type()) -> atom()). -type_name(Type) when Type > ?RESERVED andalso Type =< ?AUTH -> +type_name(Type) when ?RESERVED < Type, Type =< ?AUTH -> lists:nth(Type, ?TYPE_NAMES). %%-------------------------------------------------------------------- diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl new file mode 100644 index 000000000..ebd59106d --- /dev/null +++ b/src/emqx_protocol.erl @@ -0,0 +1,136 @@ +%%-------------------------------------------------------------------- +%% 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("types.hrl"). +-include("emqx_mqtt.hrl"). + +-export([ init/1 + , info/1 + , info/2 + , attrs/1 + ]). + +-export([ find_alias/2 + , save_alias/3 + ]). + +-export_type([protocol/0]). + +-record(protocol, { + %% MQTT Proto Name + proto_name :: binary(), + %% MQTT Proto Version + proto_ver :: emqx_types:ver(), + %% Clean Start Flag + clean_start :: boolean(), + %% MQTT Keepalive interval + keepalive :: non_neg_integer(), + %% ClientId in CONNECT Packet + client_id :: emqx_types:client_id(), + %% Username in CONNECT Packet + username :: emqx_types:username(), + %% MQTT Will Msg + will_msg :: emqx_types:message(), + %% MQTT Conn Properties + conn_props :: maybe(emqx_types:properties()), + %% MQTT Topic Aliases + topic_aliases :: maybe(map()) + }). + +-opaque(protocol() :: #protocol{}). + +-spec(init(#mqtt_packet_connect{}) -> protocol()). +init(#mqtt_packet_connect{proto_name = ProtoName, + proto_ver = ProtoVer, + clean_start = CleanStart, + keepalive = Keepalive, + properties = Properties, + client_id = ClientId, + username = Username + } = ConnPkt) -> + WillMsg = emqx_packet:will_msg(ConnPkt), + #protocol{proto_name = ProtoName, + proto_ver = ProtoVer, + clean_start = CleanStart, + keepalive = Keepalive, + client_id = ClientId, + username = Username, + will_msg = WillMsg, + conn_props = Properties + }. + +info(#protocol{proto_name = ProtoName, + proto_ver = ProtoVer, + clean_start = CleanStart, + keepalive = Keepalive, + client_id = ClientId, + username = Username, + will_msg = WillMsg, + conn_props = ConnProps, + topic_aliases = Aliases }) -> + #{proto_name => ProtoName, + proto_ver => ProtoVer, + clean_start => CleanStart, + keepalive => Keepalive, + client_id => ClientId, + username => Username, + will_msg => WillMsg, + conn_props => ConnProps, + topic_aliases => Aliases + }. + +info(proto_name, #protocol{proto_name = ProtoName}) -> + ProtoName; +info(proto_ver, #protocol{proto_ver = ProtoVer}) -> + ProtoVer; +info(clean_start, #protocol{clean_start = CleanStart}) -> + CleanStart; +info(keepalive, #protocol{keepalive = Keepalive}) -> + Keepalive; +info(client_id, #protocol{client_id = ClientId}) -> + ClientId; +info(username, #protocol{username = Username}) -> + Username; +info(will_msg, #protocol{will_msg = WillMsg}) -> + WillMsg; +info(conn_props, #protocol{conn_props = ConnProps}) -> + ConnProps; +info(topic_aliases, #protocol{topic_aliases = Aliases}) -> + Aliases. + +attrs(#protocol{proto_name = ProtoName, + proto_ver = ProtoVer, + clean_start = CleanStart, + keepalive = Keepalive}) -> + #{proto_name => ProtoName, + proto_ver => ProtoVer, + clean_start => CleanStart, + keepalive => Keepalive + }. + +find_alias(_AliasId, #protocol{topic_aliases = undefined}) -> + false; +find_alias(AliasId, #protocol{topic_aliases = Aliases}) -> + maps:find(AliasId, Aliases). + +save_alias(AliasId, Topic, Protocol = #protocol{topic_aliases = undefined}) -> + Protocol#protocol{topic_aliases = #{AliasId => Topic}}; +save_alias(AliasId, Topic, Protocol = #protocol{topic_aliases = Aliases}) -> + Protocol#protocol{topic_aliases = maps:put(AliasId, Topic, Aliases)}. + diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 72a683b2a..98afade52 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -50,7 +50,7 @@ -logger_header("[Session]"). --export([init/3]). +-export([init/2]). -export([ info/1 , info/2 @@ -58,10 +58,6 @@ , stats/1 ]). --export([ takeover/1 - , resume/2 - ]). - -export([ subscribe/4 , unsubscribe/3 ]). @@ -73,71 +69,51 @@ , pubcomp/2 ]). --export([deliver/2]). +-export([ deliver/2 + , retry/1 + ]). --export([timeout/3]). +-export([ takeover/1 + , resume/2 + ]). + +-export([expire/2]). -export_type([session/0]). --import(emqx_zone, - [ get_env/2 - , get_env/3 - ]). - %% For test case -export([set_pkt_id/2]). --record(session, { - %% Clean Start Flag - clean_start :: boolean(), +-import(emqx_zone, [get_env/3]). +-record(session, { %% Client’s Subscriptions. subscriptions :: map(), - %% Max subscriptions allowed max_subscriptions :: non_neg_integer(), - %% Upgrade QoS? upgrade_qos :: boolean(), - %% Client <- Broker: %% Inflight QoS1, QoS2 messages sent to the client but unacked. inflight :: emqx_inflight:inflight(), - %% 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(), - %% Next packet id of the session next_pkt_id = 1 :: emqx_types:packet_id(), - %% Retry interval for redelivering QoS1/2 messages retry_interval :: timeout(), - - %% Retry delivery timer - retry_timer :: maybe(reference()), - %% Client -> Broker: %% Inflight QoS2 messages received from client and waiting for pubrel. awaiting_rel :: map(), - %% 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(), - %% Session Expiry Interval expiry_interval :: timeout(), - - %% Expired Timer - expiry_timer :: maybe(reference()), - %% Created at created_at :: erlang:timestamp() }). @@ -153,11 +129,10 @@ %%-------------------------------------------------------------------- %% @doc Init a session. --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), +-spec(init(emqx_types:client(), Options :: map()) -> session()). +init(#{zone := Zone}, #{max_inflight := MaxInflight, + expiry_interval := ExpiryInterval}) -> + #session{max_subscriptions = get_env(Zone, max_subscriptions, 0), subscriptions = #{}, upgrade_qos = get_env(Zone, upgrade_qos, false), inflight = emqx_inflight:new(MaxInflight), @@ -183,8 +158,7 @@ init_mqueue(Zone) -> %%-------------------------------------------------------------------- -spec(info(session()) -> emqx_types:infos()). -info(#session{clean_start = CleanStart, - max_subscriptions = MaxSubscriptions, +info(#session{max_subscriptions = MaxSubscriptions, subscriptions = Subscriptions, upgrade_qos = UpgradeQoS, inflight = Inflight, @@ -196,8 +170,7 @@ info(#session{clean_start = CleanStart, await_rel_timeout = AwaitRelTimeout, expiry_interval = ExpiryInterval, created_at = CreatedAt}) -> - #{clean_start => CleanStart, - subscriptions => Subscriptions, + #{subscriptions => Subscriptions, max_subscriptions => MaxSubscriptions, upgrade_qos => UpgradeQoS, inflight => emqx_inflight:size(Inflight), @@ -214,8 +187,6 @@ info(#session{clean_start = CleanStart, created_at => CreatedAt }. -info(clean_start, #session{clean_start = CleanStart}) -> - CleanStart; info(subscriptions, #session{subscriptions = Subs}) -> Subs; info(max_subscriptions, #session{max_subscriptions = MaxSubs}) -> @@ -254,11 +225,9 @@ info(created_at, #session{created_at = CreatedAt}) -> -spec(attrs(session()) -> emqx_types:attrs()). attrs(undefined) -> #{}; -attrs(#session{clean_start = CleanStart, - expiry_interval = ExpiryInterval, +attrs(#session{expiry_interval = ExpiryInterval, created_at = CreatedAt}) -> - #{clean_start => CleanStart, - expiry_interval => ExpiryInterval, + #{expiry_interval => ExpiryInterval, created_at => CreatedAt }. @@ -290,7 +259,7 @@ takeover(#session{subscriptions = Subs}) -> ok = emqx_broker:unsubscribe(TopicFilter) end, maps:to_list(Subs)). --spec(resume(emqx_types:client_id(), session()) -> {ok, session()}). +-spec(resume(emqx_types:client_id(), session()) -> session()). resume(ClientId, Session = #session{subscriptions = Subs}) -> ?LOG(info, "Session is resumed."), %% 1. Subscribe again @@ -300,8 +269,8 @@ resume(ClientId, Session = #session{subscriptions = Subs}) -> %% 2. Run hooks. ok = emqx_hooks:run('session.resumed', [#{client_id => ClientId}, attrs(Session)]), %% TODO: 3. Redeliver: Replay delivery and Dequeue pending messages - %% noreply(ensure_stats_timer(dequeue(retry_delivery(true, State1)))); - {ok, Session}. + %% noreply(dequeue(retry_delivery(true, State1))); + Session. %%-------------------------------------------------------------------- %% Client -> Broker: SUBSCRIBE @@ -388,7 +357,7 @@ do_publish(PacketId, Msg = #message{timestamp = Ts}, DeliverResults = emqx_broker:publish(Msg), AwaitingRel1 = maps:put(PacketId, Ts, AwaitingRel), Session1 = Session#session{awaiting_rel = AwaitingRel1}, - {ok, DeliverResults, ensure_await_rel_timer(Session1)}; + {ok, DeliverResults, Session1}; true -> {error, ?RC_PACKET_IDENTIFIER_IN_USE} end. @@ -544,9 +513,8 @@ enqueue(Msg, Session = #session{mqueue = Q}) -> %%-------------------------------------------------------------------- await(PacketId, Msg, Session = #session{inflight = Inflight}) -> - Inflight1 = emqx_inflight:insert( - PacketId, {Msg, os:timestamp()}, Inflight), - ensure_retry_timer(Session#session{inflight = Inflight1}). + Inflight1 = emqx_inflight:insert(PacketId, {Msg, os:timestamp()}, Inflight), + Session#session{inflight = Inflight1}. get_subopts(Topic, SubMap) -> case maps:find(Topic, SubMap) of @@ -578,44 +546,12 @@ enrich([{rap, _}|Opts], Msg = #message{flags = Flags}, 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(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. - %%-------------------------------------------------------------------- %% Retry Delivery %%-------------------------------------------------------------------- %% Redeliver at once if force is true -retry_delivery(Session = #session{inflight = Inflight}) -> +retry(Session = #session{inflight = Inflight}) -> case emqx_inflight:is_empty(Inflight) of true -> {ok, Session}; false -> @@ -626,10 +562,11 @@ retry_delivery(Session = #session{inflight = Inflight}) -> retry_delivery([], _Now, Acc, Session) -> %% Retry again... - {ok, lists:reverse(Acc), ensure_retry_timer(Session)}; + {ok, lists:reverse(Acc), Session}; retry_delivery([{PacketId, {Val, Ts}}|More], Now, Acc, - Session = #session{retry_interval = Interval, inflight = Inflight}) -> + Session = #session{retry_interval = Interval, + inflight = Inflight}) -> %% Microseconds -> MilliSeconds Age = timer:now_diff(Now, Ts) div 1000, if @@ -637,7 +574,7 @@ retry_delivery([{PacketId, {Val, Ts}}|More], Now, Acc, {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)} + {ok, lists:reverse(Acc), Interval - max(0, Age), Session} end. retry_delivery(PacketId, Msg, Now, Acc, Inflight) when is_record(Msg, message) -> @@ -654,34 +591,20 @@ retry_delivery(PacketId, pubrel, Now, Acc, Inflight) -> Inflight1 = emqx_inflight:update(PacketId, {pubrel, Now}, Inflight), {[{pubrel, PacketId}|Acc], Inflight1}. -%%-------------------------------------------------------------------- -%% 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}) -> +expire(awaiting_rel, Session = #session{awaiting_rel = AwaitingRel}) -> case maps:size(AwaitingRel) of 0 -> {ok, Session}; - _ -> expire_awaiting_rel(lists:keysort(2, maps:to_list(AwaitingRel)), os:timestamp(), Session) + _ -> + AwaitingRel1 = lists:keysort(2, maps:to_list(AwaitingRel)), + expire_awaiting_rel(AwaitingRel1, os:timestamp(), Session) end. expire_awaiting_rel([], _Now, Session) -> - {ok, Session#session{await_rel_timer = undefined}}; + {ok, Session}; expire_awaiting_rel([{PacketId, Ts} | More], Now, Session = #session{awaiting_rel = AwaitingRel, @@ -693,7 +616,7 @@ expire_awaiting_rel([{PacketId, Ts} | More], Now, Session1 = Session#session{awaiting_rel = maps:remove(PacketId, AwaitingRel)}, expire_awaiting_rel(More, Now, Session1); Age -> - {ok, ensure_await_rel_timer(Timeout - max(0, Age), Session)} + {ok, Timeout - max(0, Age), Session} end. %%-------------------------------------------------------------------- diff --git a/src/emqx_ws_connection.erl b/src/emqx_ws_connection.erl index 530a300f8..6dc709000 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_connection.erl @@ -22,7 +22,7 @@ -include("logger.hrl"). -include("types.hrl"). --logger_header("[WsConn]"). +-logger_header("[WsConnection]"). -export([ info/1 , attrs/1 @@ -49,7 +49,6 @@ serialize :: fun((emqx_types:packet()) -> iodata()), parse_state :: emqx_frame:parse_state(), chan_state :: emqx_channel:channel(), - keepalive :: maybe(emqx_keepalive:keepalive()), pendings :: list(), reason :: term() }). @@ -57,7 +56,7 @@ -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]). +-define(CONN_STATS, [recv_pkt, recv_msg, send_pkt, send_msg]). %%-------------------------------------------------------------------- %% API @@ -66,36 +65,37 @@ -spec(info(pid() | state()) -> emqx_types:infos()). info(WSPid) when is_pid(WSPid) -> call(WSPid, info); -info(#state{peername = Peername, - sockname = Sockname, - chan_state = ChanState - }) -> - ConnInfo = #{socktype => websocket, - peername => Peername, - sockname => Sockname, +info(#state{peername = Peername, + sockname = Sockname, + chan_state = ChanState}) -> + ConnInfo = #{socktype => websocket, + peername => Peername, + sockname => Sockname, conn_state => running }, - maps:merge(ConnInfo, emqx_channel:info(ChanState)). + ChanInfo = emqx_channel:info(ChanState), + maps:merge(ConnInfo, ChanInfo). -spec(attrs(pid() | state()) -> emqx_types:attrs()). attrs(WSPid) when is_pid(WSPid) -> call(WSPid, attrs); -attrs(#state{peername = Peername, - sockname = Sockname, +attrs(#state{peername = Peername, + sockname = Sockname, chan_state = ChanState}) -> ConnAttrs = #{socktype => websocket, peername => Peername, sockname => Sockname }, - maps:merge(ConnAttrs, emqx_channel:attrs(ChanState)). + ChanAttrs = emqx_channel:attrs(ChanState), + maps:merge(ConnAttrs, ChanAttrs). -spec(stats(pid() | state()) -> emqx_types:stats()). stats(WSPid) when is_pid(WSPid) -> call(WSPid, stats); stats(#state{chan_state = ChanState}) -> ProcStats = emqx_misc:proc_stats(), - SessStats = emqx_session:stats(emqx_channel:info(session, ChanState)), - lists:append([ProcStats, SessStats, chan_stats(), wsock_stats()]). + ChanStats = emqx_channel:stats(ChanState), + lists:append([ProcStats, wsock_stats(), conn_stats(), ChanStats]). -spec(kick(pid()) -> ok). kick(CPid) -> @@ -105,7 +105,7 @@ kick(CPid) -> discard(WSPid) -> WSPid ! {cast, discard}, ok. --spec(takeover(pid(), 'begin'|'end') -> {ok, Result :: term()}). +-spec(takeover(pid(), 'begin'|'end') -> Result :: term()). takeover(CPid, Phase) -> call(CPid, {takeover, Phase}). @@ -177,17 +177,14 @@ websocket_init([Req, Opts]) -> MaxSize = emqx_zone:get_env(Zone, max_packet_size, ?MAX_PACKET_SIZE), ParseState = emqx_frame:initial_parse_state(#{max_size => MaxSize}), emqx_logger:set_metadata_peername(esockd_net:format(Peername)), - {ok, #state{peername = Peername, - sockname = Sockname, - fsm_state = idle, - parse_state = ParseState, - chan_state = ChanState, - pendings = [] + {ok, #state{peername = Peername, + sockname = Sockname, + fsm_state = idle, + parse_state = ParseState, + chan_state = ChanState, + pendings = [] }}. -stat_fun() -> - fun() -> {ok, emqx_pd:get_counter(recv_oct)} end. - websocket_handle({binary, Data}, State) when is_list(Data) -> websocket_handle({binary, iolist_to_binary(Data)}, State); @@ -199,7 +196,7 @@ websocket_handle({binary, Data}, State = #state{chan_state = ChanState}) emqx_pd:update_counter(recv_oct, Oct), ok = emqx_metrics:inc('bytes.received', Oct), NChanState = emqx_channel:ensure_timer( - emit_stats, emqx_channel:gc(1, Oct, ChanState)), + stats_timer, emqx_channel:gc(1, Oct, ChanState)), process_incoming(Data, State#state{chan_state = NChanState}); %% Pings should be replied with pongs, cowboy does it automatically @@ -231,6 +228,16 @@ websocket_info({call, From, kick}, State) -> gen_server:reply(From, ok), stop(kicked, State); +websocket_info({call, From, Req}, State = #state{chan_state = ChanState}) -> + case emqx_channel:handle_call(Req, ChanState) of + {ok, Reply, NChanState} -> + _ = gen_server:reply(From, Reply), + {ok, State#state{chan_state = NChanState}}; + {stop, Reason, Reply, NChanState} -> + _ = gen_server:reply(From, Reply), + stop(Reason, State#state{chan_state = NChanState}) + end; + websocket_info({cast, Msg}, State = #state{chan_state = ChanState}) -> case emqx_channel:handle_cast(Msg, ChanState) of {ok, NChanState} -> @@ -262,7 +269,8 @@ websocket_info({incoming, Packet}, State = #state{fsm_state = connected}) websocket_info(Deliver = {deliver, _Topic, _Msg}, State = #state{chan_state = ChanState}) -> - case emqx_channel:handle_out(Deliver, ChanState) of + Delivers = emqx_misc:drain_deliver([Deliver]), + case emqx_channel:handle_out({deliver, Delivers}, ChanState) of {ok, NChanState} -> reply(State#state{chan_state = NChanState}); {ok, Packets, NChanState} -> @@ -271,16 +279,9 @@ websocket_info(Deliver = {deliver, _Topic, _Msg}, stop(Reason, State#state{chan_state = NChanState}) end; -websocket_info({keepalive, check}, State = #state{keepalive = KeepAlive}) -> - case emqx_keepalive:check(KeepAlive) of - {ok, KeepAlive1} -> - {ok, State#state{keepalive = KeepAlive1}}; - {error, timeout} -> - stop(keepalive_timeout, State); - {error, Error} -> - ?LOG(error, "Keepalive error: ~p", [Error]), - stop(keepalive_error, State) - end; +websocket_info({timeout, TRef, keepalive}, State) when is_reference(TRef) -> + RecvOct = emqx_pd:get_counter(recv_oct), + handle_timeout(TRef, {keepalive, RecvOct}, State); websocket_info({timeout, TRef, emit_stats}, State) when is_reference(TRef) -> handle_timeout(TRef, {emit_stats, stats(State)}, State); @@ -310,13 +311,10 @@ websocket_info(Info, State = #state{chan_state = ChanState}) -> stop(Reason, State#state{chan_state = NChanState}) end. -terminate(SockError, _Req, #state{keepalive = KeepAlive, - chan_state = ChanState, +terminate(SockError, _Req, #state{chan_state = ChanState, reason = Reason}) -> ?LOG(debug, "Terminated for ~p, sockerror: ~p", [Reason, SockError]), - KeepAlive =/= undefined - andalso emqx_keepalive:cancel(KeepAlive), emqx_channel:terminate(Reason, ChanState). %%-------------------------------------------------------------------- @@ -324,18 +322,10 @@ terminate(SockError, _Req, #state{keepalive = KeepAlive, connected(State = #state{chan_state = ChanState}) -> NState = State#state{fsm_state = connected}, - ClientId = emqx_channel:info(client_id, ChanState), + #{client_id := ClientId} = emqx_channel:info(client, ChanState), ok = emqx_cm:register_channel(ClientId), - ok = emqx_cm:set_chan_attrs(ClientId, info(NState)), - %% Ensure keepalive after connected successfully. - Interval = emqx_channel:info(keepalive, ChanState), - case ensure_keepalive(Interval, NState) of - ignore -> reply(NState); - {ok, KeepAlive} -> - reply(NState#state{keepalive = KeepAlive}); - {error, Reason} -> - stop(Reason, NState) - end. + ok = emqx_cm:set_chan_attrs(ClientId, attrs(NState)), + reply(NState). %%-------------------------------------------------------------------- %% Handle timeout @@ -350,16 +340,6 @@ handle_timeout(TRef, Msg, State = #state{chan_state = ChanState}) -> stop(Reason, State#state{chan_state = NChanState}) end. -%%-------------------------------------------------------------------- -%% Ensure keepalive - -ensure_keepalive(0, _State) -> - ignore; -ensure_keepalive(Interval, #state{chan_state = ChanState}) -> - Backoff = emqx_zone:get_env(emqx_channel:info(zone, ChanState), - keepalive_backoff, 0.75), - emqx_keepalive:start(stat_fun(), round(Interval * Backoff), {keepalive, check}). - %%-------------------------------------------------------------------- %% Process incoming data @@ -440,7 +420,7 @@ reply(State = #state{pendings = []}) -> {ok, State}; reply(State = #state{chan_state = ChanState, pendings = Pendings}) -> Reply = handle_outgoing(Pendings, State), - NChanState = emqx_channel:ensure_timer(emit_stats, ChanState), + NChanState = emqx_channel:ensure_timer(stats_timer, ChanState), {reply, Reply, State#state{chan_state = NChanState, pendings = []}}. stop(Reason, State = #state{pendings = []}) -> @@ -458,6 +438,6 @@ enqueue(Packets, State = #state{pendings = Pendings}) -> wsock_stats() -> [{Key, emqx_pd:get_counter(Key)} || Key <- ?SOCK_STATS]. -chan_stats() -> - [{Name, emqx_pd:get_counter(Name)} || Name <- ?CHAN_STATS]. +conn_stats() -> + [{Name, emqx_pd:get_counter(Name)} || Name <- ?CONN_STATS]. diff --git a/test/emqx_channel_SUITE.erl b/test/emqx_channel_SUITE.erl index 02f9aa975..e7326cd76 100644 --- a/test/emqx_channel_SUITE.erl +++ b/test/emqx_channel_SUITE.erl @@ -22,7 +22,6 @@ -import(emqx_channel, [ handle_in/2 , handle_out/2 - , handle_out/3 ]). -include("emqx.hrl"). @@ -58,9 +57,10 @@ t_handle_connect(_) -> fun(Channel) -> {ok, ?CONNACK_PACKET(?RC_SUCCESS), Channel1} = handle_in(?CONNECT_PACKET(ConnPkt), Channel), - Client = emqx_channel:info(client, Channel1), - ?assertEqual(<<"clientid">>, maps:get(client_id, Client)), - ?assertEqual(<<"username">>, maps:get(username, Client)) + #{client_id := ClientId, username := Username} + = emqx_channel:info(client, Channel1), + ?assertEqual(<<"clientid">>, ClientId), + ?assertEqual(<<"username">>, Username) end). t_handle_publish_qos0(_) -> @@ -86,8 +86,8 @@ t_handle_publish_qos2(_) -> Publish2 = ?PUBLISH_PACKET(?QOS_2, <<"topic">>, 2, <<"payload">>), {ok, ?PUBREC_PACKET(2, RC), Channel2} = handle_in(Publish2, Channel1), ?assert((RC == ?RC_SUCCESS) orelse (RC == ?RC_NO_MATCHING_SUBSCRIBERS)), - Session = emqx_channel:info(session, Channel2), - ?assertEqual(2, emqx_session:info(awaiting_rel, Session)) + #{awaiting_rel := AwaitingRel} = emqx_channel:info(session, Channel2), + ?assertEqual(2, AwaitingRel) end). t_handle_puback(_) -> @@ -122,10 +122,9 @@ t_handle_subscribe(_) -> TopicFilters = [{<<"+">>, ?DEFAULT_SUBOPTS}], {ok, ?SUBACK_PACKET(10, [?QOS_0]), Channel1} = handle_in(?SUBSCRIBE_PACKET(10, #{}, TopicFilters), Channel), - Session = emqx_channel:info(session, Channel1), - ?assertEqual(maps:from_list(TopicFilters), - emqx_session:info(subscriptions, Session)) - + #{subscriptions := Subscriptions} + = emqx_channel:info(session, Channel1), + ?assertEqual(maps:from_list(TopicFilters), Subscriptions) end). t_handle_unsubscribe(_) -> @@ -145,7 +144,7 @@ t_handle_disconnect(_) -> with_channel( fun(Channel) -> {stop, normal, Channel1} = handle_in(?DISCONNECT_PACKET(?RC_SUCCESS), Channel), - ?assertEqual(undefined, emqx_channel:info(will_msg, Channel1)) + ?assertMatch(#{will_msg := undefined}, emqx_channel:info(protocol, Channel1)) end). t_handle_auth(_) -> @@ -166,9 +165,8 @@ t_handle_deliver(_) -> = handle_in(?SUBSCRIBE_PACKET(1, #{}, TopicFilters), Channel), Msg0 = emqx_message:make(<<"clientx">>, ?QOS_0, <<"t0">>, <<"qos0">>), Msg1 = emqx_message:make(<<"clientx">>, ?QOS_1, <<"t1">>, <<"qos1">>), - %% TODO: Fixme later. - self() ! {deliver, <<"+">>, Msg1}, - {ok, Packets, _Channel2} = emqx_channel:handle_out({deliver, <<"+">>, Msg0}, Channel1), + Delivers = [{deliver, <<"+">>, Msg0}, {deliver, <<"+">>, Msg1}], + {ok, Packets, _Ch} = emqx_channel:handle_out({deliver, Delivers}, Channel1), ?assertMatch([?PUBLISH_PACKET(?QOS_0, <<"t0">>, undefined, <<"qos0">>), ?PUBLISH_PACKET(?QOS_1, <<"t1">>, 1, <<"qos1">>) ], Packets) @@ -178,13 +176,13 @@ t_handle_deliver(_) -> %% Test cases for handle_out %%-------------------------------------------------------------------- -t_handle_conack(_) -> +t_handle_connack(_) -> with_channel( fun(Channel) -> {ok, ?CONNACK_PACKET(?RC_SUCCESS, SP, _), _} - = handle_out(connack, {?RC_SUCCESS, 0}, Channel), + = handle_out({connack, ?RC_SUCCESS, 0}, Channel), {stop, {shutdown, unauthorized_client}, ?CONNACK_PACKET(5), _} - = handle_out(connack, ?RC_NOT_AUTHORIZED, Channel) + = handle_out({connack, ?RC_NOT_AUTHORIZED}, Channel) end). t_handle_out_publish(_) -> @@ -194,59 +192,59 @@ t_handle_out_publish(_) -> Pub1 = {publish, 1, emqx_message:make(<<"c">>, ?QOS_1, <<"t">>, <<"qos1">>)}, {ok, ?PUBLISH_PACKET(?QOS_0), Channel} = handle_out(Pub0, Channel), {ok, ?PUBLISH_PACKET(?QOS_1), Channel} = handle_out(Pub1, Channel), - {ok, Packets, Channel} = handle_out(publish, [Pub0, Pub1], Channel), + {ok, Packets, Channel} = handle_out({publish, [Pub0, Pub1]}, Channel), ?assertEqual(2, length(Packets)) end). t_handle_out_puback(_) -> with_channel( fun(Channel) -> - {ok, Channel} = handle_out(puberr, ?RC_NOT_AUTHORIZED, Channel), + {ok, Channel} = handle_out({puberr, ?RC_NOT_AUTHORIZED}, Channel), {ok, ?PUBACK_PACKET(1, ?RC_SUCCESS), Channel} - = handle_out(puback, {1, ?RC_SUCCESS}, Channel) + = handle_out({puback, 1, ?RC_SUCCESS}, Channel) end). t_handle_out_pubrec(_) -> with_channel( fun(Channel) -> {ok, ?PUBREC_PACKET(4, ?RC_SUCCESS), Channel} - = handle_out(pubrec, {4, ?RC_SUCCESS}, Channel) + = handle_out({pubrec, 4, ?RC_SUCCESS}, Channel) end). t_handle_out_pubrel(_) -> with_channel( fun(Channel) -> {ok, ?PUBREL_PACKET(2), Channel} - = handle_out(pubrel, {2, ?RC_SUCCESS}, Channel), + = handle_out({pubrel, 2, ?RC_SUCCESS}, Channel), {ok, ?PUBREL_PACKET(3, ?RC_SUCCESS), Channel} - = handle_out(pubrel, {3, ?RC_SUCCESS}, Channel) + = handle_out({pubrel, 3, ?RC_SUCCESS}, Channel) end). t_handle_out_pubcomp(_) -> with_channel( fun(Channel) -> {ok, ?PUBCOMP_PACKET(5, ?RC_SUCCESS), Channel} - = handle_out(pubcomp, {5, ?RC_SUCCESS}, Channel) + = handle_out({pubcomp, 5, ?RC_SUCCESS}, Channel) end). t_handle_out_suback(_) -> with_channel( fun(Channel) -> {ok, ?SUBACK_PACKET(1, [?QOS_2]), Channel} - = handle_out(suback, {1, [?QOS_2]}, Channel) + = handle_out({suback, 1, [?QOS_2]}, Channel) end). t_handle_out_unsuback(_) -> with_channel( fun(Channel) -> {ok, ?UNSUBACK_PACKET(1), Channel} - = handle_out(unsuback, {1, [?RC_SUCCESS]}, Channel) + = handle_out({unsuback, 1, [?RC_SUCCESS]}, Channel) end). t_handle_out_disconnect(_) -> with_channel( fun(Channel) -> - handle_out(disconnect, ?RC_SUCCESS, Channel) + handle_out({disconnect, ?RC_SUCCESS}, Channel) end). %%-------------------------------------------------------------------- @@ -281,9 +279,20 @@ with_channel(Fun) -> }, Options = [{zone, testing}], Channel = emqx_channel:init(ConnInfo, Options), - Session = emqx_session:init(false, #{zone => testing}, - #{max_inflight => 100, + ConnPkt = #mqtt_packet_connect{ + proto_name = <<"MQTT">>, + proto_ver = ?MQTT_PROTO_V4, + clean_start = true, + keepalive = 30, + properties = #{}, + client_id = <<"clientid">>, + username = <<"username">> + }, + Protocol = emqx_protocol:init(ConnPkt), + Session = emqx_session:init(#{zone => testing}, + #{max_inflight => 100, expiry_interval => 0 }), - Fun(emqx_channel:set(session, Session, Channel)). + Fun(emqx_channel:set(protocol, Protocol, + emqx_channel:set(session, Session, Channel))). diff --git a/test/emqx_keepalive_SUITE.erl b/test/emqx_keepalive_SUITE.erl index f140913ec..0bdc79f60 100644 --- a/test/emqx_keepalive_SUITE.erl +++ b/test/emqx_keepalive_SUITE.erl @@ -19,23 +19,24 @@ -compile(export_all). -compile(nowarn_export_all). +-include_lib("eunit/include/eunit.hrl"). + all() -> emqx_ct:all(?MODULE). -t_keepalive(_) -> - {ok, KA} = emqx_keepalive:start(fun() -> {ok, 1} end, 1, {keepalive, timeout}), - [resumed, timeout] = lists:reverse(keepalive_recv(KA, [])). - -keepalive_recv(KA, Acc) -> - receive - {keepalive, timeout} -> - case emqx_keepalive:check(KA) of - {ok, KA1} -> keepalive_recv(KA1, [resumed | Acc]); - {error, timeout} -> [timeout | Acc] - end - after 4000 -> Acc - end. - -t_cancel(_) -> - {ok, KA} = emqx_keepalive:start(fun() -> {ok, 1} end, 1, {keepalive, timeout}), - ok = emqx_keepalive:cancel(KA). +t_check(_) -> + Keepalive = emqx_keepalive:init(60), + ?assertEqual(60, emqx_keepalive:info(interval, Keepalive)), + ?assertEqual(0, emqx_keepalive:info(statval, Keepalive)), + ?assertEqual(0, emqx_keepalive:info(repeat, Keepalive)), + Info = emqx_keepalive:info(Keepalive), + ?assertEqual(#{interval => 60, + statval => 0, + repeat => 0}, Info), + {ok, Keepalive1} = emqx_keepalive:check(1, Keepalive), + ?assertEqual(1, emqx_keepalive:info(statval, Keepalive1)), + ?assertEqual(0, emqx_keepalive:info(repeat, Keepalive1)), + {ok, Keepalive2} = emqx_keepalive:check(1, Keepalive1), + ?assertEqual(1, emqx_keepalive:info(statval, Keepalive2)), + ?assertEqual(1, emqx_keepalive:info(repeat, Keepalive2)), + ?assertEqual({error, timeout}, emqx_keepalive:check(1, Keepalive2)). diff --git a/test/emqx_net_SUITE.erl b/test/emqx_net_SUITE.erl deleted file mode 100644 index 439ac6c70..000000000 --- a/test/emqx_net_SUITE.erl +++ /dev/null @@ -1,45 +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_net_SUITE). - -%% CT --compile(export_all). --compile(nowarn_export_all). - -all() -> [{group, keepalive}]. - -groups() -> [{keepalive, [], [t_keepalive]}]. - -%%-------------------------------------------------------------------- -%% Keepalive -%%-------------------------------------------------------------------- - -t_keepalive(_) -> - {ok, KA} = emqx_keepalive:start(fun() -> {ok, 1} end, 1, {keepalive, timeout}), - [resumed, timeout] = lists:reverse(keepalive_recv(KA, [])). - -keepalive_recv(KA, Acc) -> - receive - {keepalive, timeout} -> - case emqx_keepalive:check(KA) of - {ok, KA1} -> keepalive_recv(KA1, [resumed | Acc]); - {error, timeout} -> [timeout | Acc] - end - after 4000 -> - Acc - end. - diff --git a/test/emqx_packet_SUITE.erl b/test/emqx_packet_SUITE.erl index b334093b8..732c48fe3 100644 --- a/test/emqx_packet_SUITE.erl +++ b/test/emqx_packet_SUITE.erl @@ -27,9 +27,9 @@ all() -> emqx_ct:all(?MODULE). t_proto_name(_) -> - ?assertEqual(<<"MQIsdp">>, emqx_packet:protocol_name(3)), - ?assertEqual(<<"MQTT">>, emqx_packet:protocol_name(4)), - ?assertEqual(<<"MQTT">>, emqx_packet:protocol_name(5)). + ?assertEqual(<<"MQIsdp">>, emqx_packet:proto_name(3)), + ?assertEqual(<<"MQTT">>, emqx_packet:proto_name(4)), + ?assertEqual(<<"MQTT">>, emqx_packet:proto_name(5)). t_type_name(_) -> ?assertEqual('CONNECT', emqx_packet:type_name(?CONNECT)), diff --git a/test/emqx_protocol_SUITE.erl b/test/emqx_protocol_SUITE.erl new file mode 100644 index 000000000..89f1d7344 --- /dev/null +++ b/test/emqx_protocol_SUITE.erl @@ -0,0 +1,49 @@ +%%-------------------------------------------------------------------- +%% 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_protocol_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include("emqx_mqtt.hrl"). +-include_lib("eunit/include/eunit.hrl"). + +all() -> emqx_ct:all(?MODULE). + +t_init_and_info(_) -> + ConnPkt = #mqtt_packet_connect{ + proto_name = <<"MQTT">>, + proto_ver = ?MQTT_PROTO_V4, + is_bridge = false, + clean_start = true, + keepalive = 30, + properties = #{}, + client_id = <<"clientid">>, + username = <<"username">>, + password = <<"passwd">> + }, + Proto = emqx_protocol:init(ConnPkt), + ?assertEqual(<<"MQTT">>, emqx_protocol:info(proto_name, Proto)), + ?assertEqual(?MQTT_PROTO_V4, emqx_protocol:info(proto_ver, Proto)), + ?assertEqual(true, emqx_protocol:info(clean_start, Proto)), + ?assertEqual(<<"clientid">>, emqx_protocol:info(client_id, Proto)), + ?assertEqual(<<"username">>, emqx_protocol:info(username, Proto)), + ?assertEqual(undefined, emqx_protocol:info(will_msg, Proto)), + ?assertEqual(#{}, emqx_protocol:info(conn_props, Proto)). + + + diff --git a/test/emqx_session_SUITE.erl b/test/emqx_session_SUITE.erl index 224b8afaf..c142284c1 100644 --- a/test/emqx_session_SUITE.erl +++ b/test/emqx_session_SUITE.erl @@ -181,8 +181,7 @@ timeout_args() -> {tref(), timeout_msg()}. info_args() -> - oneof([clean_start, - subscriptions, + oneof([subscriptions, max_subscriptions, upgrade_qos, inflight, @@ -292,16 +291,14 @@ expiry_interval() -> ?LET(EI, choose(1, 10), EI * 3600). option() -> ?LET(Option, [{max_inflight, max_inflight()}, - {expiry_interval, expiry_interval()}] - , maps:from_list(Option)). - -cleanstart() -> bool(). + {expiry_interval, expiry_interval()}], + maps:from_list(Option)). session() -> - ?LET({CleanStart, Zone, Options}, - {cleanstart(), zone(), option()}, + ?LET({Zone, Options}, + {zone(), option()}, begin - Session = emqx_session:init(CleanStart, #{zone => Zone}, Options), + Session = emqx_session:init(#{zone => Zone}, Options), emqx_session:set_pkt_id(Session, 16#ffff) end). From 7454e46934bd3ba57731ae6df837c3889cca389b Mon Sep 17 00:00:00 2001 From: zhouzb Date: Thu, 22 Aug 2019 16:59:13 +0800 Subject: [PATCH 20/36] Fix compile error --- src/emqx_channel.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/emqx_channel.erl b/src/emqx_channel.erl index b1671489e..c1ae31e1d 100644 --- a/src/emqx_channel.erl +++ b/src/emqx_channel.erl @@ -358,10 +358,10 @@ process_connect(ConnPkt, Channel) -> %% Process Publish process_publish(Packet = ?PUBLISH_PACKET(_QoS, _Topic, PacketId), - Channel = #channel{client = Client, proto_ver = ProtoVer}) -> + Channel = #channel{client = Client, protocol = Protocol}) -> Msg = emqx_packet:to_message(Client, Packet), %%TODO: Improve later. - Msg1 = emqx_message:set_flag(dup, false, emqx_message:set_header(proto_ver, ProtoVer, Msg)), + Msg1 = emqx_message:set_flag(dup, false, emqx_message:set_header(proto_ver, emqx_protocol:info(proto_ver, Protocol), Msg)), process_publish(PacketId, mount(Client, Msg1), Channel). process_publish(_PacketId, Msg = #message{qos = ?QOS_0}, Channel) -> From 41e9dad70a1d940e561c0f421e0496e353b72aa1 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 22 Aug 2019 17:10:12 +0800 Subject: [PATCH 21/36] Rename `reason` field to `stop_reason` --- src/emqx_ws_connection.erl | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/emqx_ws_connection.erl b/src/emqx_ws_connection.erl index 6dc709000..d682d74ab 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_connection.erl @@ -50,7 +50,7 @@ parse_state :: emqx_frame:parse_state(), chan_state :: emqx_channel:channel(), pendings :: list(), - reason :: term() + stop_reason :: term() }). -type(state() :: #state{}). @@ -97,6 +97,12 @@ stats(#state{chan_state = ChanState}) -> ChanStats = emqx_channel:stats(ChanState), lists:append([ProcStats, wsock_stats(), conn_stats(), ChanStats]). +wsock_stats() -> + [{Key, emqx_pd:get_counter(Key)} || Key <- ?SOCK_STATS]. + +conn_stats() -> + [{Name, emqx_pd:get_counter(Name)} || Name <- ?CONN_STATS]. + -spec(kick(pid()) -> ok). kick(CPid) -> call(CPid, kick). @@ -311,8 +317,8 @@ websocket_info(Info, State = #state{chan_state = ChanState}) -> stop(Reason, State#state{chan_state = NChanState}) end. -terminate(SockError, _Req, #state{chan_state = ChanState, - reason = Reason}) -> +terminate(SockError, _Req, #state{chan_state = ChanState, + stop_reason = Reason}) -> ?LOG(debug, "Terminated for ~p, sockerror: ~p", [Reason, SockError]), emqx_channel:terminate(Reason, ChanState). @@ -424,20 +430,14 @@ reply(State = #state{chan_state = ChanState, pendings = Pendings}) -> {reply, Reply, State#state{chan_state = NChanState, pendings = []}}. stop(Reason, State = #state{pendings = []}) -> - {stop, State#state{reason = Reason}}; + {stop, State#state{stop_reason = Reason}}; stop(Reason, State = #state{pendings = Pendings}) -> Reply = handle_outgoing(Pendings, State), {reply, [Reply, close], - State#state{pendings = [], reason = Reason}}. + State#state{pendings = [], stop_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)}. -wsock_stats() -> - [{Key, emqx_pd:get_counter(Key)} || Key <- ?SOCK_STATS]. - -conn_stats() -> - [{Name, emqx_pd:get_counter(Name)} || Name <- ?CONN_STATS]. - From c69a2b1b485ad555ea02f55c9e48bdba24b6c769 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 23 Aug 2019 09:35:29 +0800 Subject: [PATCH 22/36] Ensure session expiration (#2825) Ensure session expiration --- include/emqx_mqtt.hrl | 2 ++ src/emqx_channel.erl | 52 +++++++++++++++++++++++++++++-------- src/emqx_connection.erl | 12 ++++----- src/emqx_session.erl | 2 +- test/emqx_session_SUITE.erl | 24 +++-------------- 5 files changed, 53 insertions(+), 39 deletions(-) diff --git a/include/emqx_mqtt.hrl b/include/emqx_mqtt.hrl index cbffe27ee..3a138f393 100644 --- a/include/emqx_mqtt.hrl +++ b/include/emqx_mqtt.hrl @@ -17,6 +17,8 @@ -ifndef(EMQ_X_MQTT_HRL). -define(EMQ_X_MQTT_HRL, true). +-define(UINT_MAX, 16#FFFFFFFF). + %%-------------------------------------------------------------------- %% MQTT SockOpts %%-------------------------------------------------------------------- diff --git a/src/emqx_channel.erl b/src/emqx_channel.erl index c1ae31e1d..84864fd07 100644 --- a/src/emqx_channel.erl +++ b/src/emqx_channel.erl @@ -74,6 +74,7 @@ %% Connected connected :: boolean(), connected_at :: erlang:timestamp(), + disconnected_at :: erlang:timestamp(), %% Takeover/Resume resuming :: boolean(), pendings :: list() @@ -169,7 +170,9 @@ info(oom_policy, #channel{oom_policy = Policy}) -> info(connected, #channel{connected = Connected}) -> Connected; info(connected_at, #channel{connected_at = ConnectedAt}) -> - ConnectedAt. + ConnectedAt; +info(disconnected_at, #channel{disconnected_at = DisconnectedAt}) -> + DisconnectedAt. -spec(attrs(channel()) -> emqx_types:attrs()). attrs(#channel{client = Client, @@ -240,11 +243,12 @@ handle_in(Packet = ?PUBLISH_PACKET(QoS, Topic, PacketId), Channel = #channel{pro ProtoVer = emqx_protocol:info(proto_ver, Protocol), ?LOG(warning, "Cannot publish message to ~s due to ~s", [Topic, emqx_reason_codes:text(ReasonCode, ProtoVer)]), - case QoS of - ?QOS_0 -> handle_out({puberr, ReasonCode}, NChannel); - ?QOS_1 -> handle_out({puback, PacketId, ReasonCode}, NChannel); - ?QOS_2 -> handle_out({pubrec, PacketId, ReasonCode}, NChannel) - end + handle_out({disconnect, ReasonCode}, NChannel) + % case QoS of + % ?QOS_0 -> handle_out({puberr, ReasonCode}, NChannel); + % ?QOS_1 -> handle_out({puback, PacketId, ReasonCode}, NChannel); + % ?QOS_2 -> handle_out({pubrec, PacketId, ReasonCode}, NChannel) + % end end; %%TODO: How to handle the ReasonCode? @@ -589,6 +593,18 @@ handle_info({unsubscribe, TopicFilters}, Channel = #channel{client = Client}) -> {_ReasonCodes, NChannel} = process_unsubscribe(TopicFilters1, Channel), {ok, NChannel}; +handle_info(sock_closed, Channel = #channel{connected = false}) -> + shutdown(closed, Channel); +handle_info(sock_closed, Channel = #channel{session = Session}) -> + Interval = emqx_session:info(expiry_interval, Session), + case Interval of + ?UINT_MAX -> + {ok, ensure_disconnected(Channel)}; + Int when Int > 0 -> + {ok, ensure_timer(expire_timer, ensure_disconnected(Channel))}; + _Other -> shutdown(closed, Channel) + end; + handle_info(Info, Channel) -> ?LOG(error, "Unexpected info: ~p~n", [Info]), {ok, Channel}. @@ -643,7 +659,7 @@ timeout(TRef, expire_awaiting_rel, Channel = #channel{session = Session, end; timeout(_TRef, expire_session, Channel) -> - {ok, Channel}; + shutdown(expired, Channel); timeout(_TRef, Msg, Channel) -> ?LOG(error, "Unexpected timeout: ~p~n", [Msg]), @@ -685,7 +701,7 @@ interval(retry_timer, #channel{session = Session}) -> interval(await_timer, #channel{session = Session}) -> emqx_session:info(await_rel_timeout, Session); interval(expire_timer, #channel{session = Session}) -> - emqx_session:info(expiry_interval, Session). + timer:seconds(emqx_session:info(expiry_interval, Session)). %%-------------------------------------------------------------------- %% Terminate @@ -886,11 +902,19 @@ auth_connect(#mqtt_packet_connect{client_id = ClientId, open_session(#mqtt_packet_connect{clean_start = CleanStart, properties = ConnProps}, - #channel{client = Client = #{zone := Zone}}) -> + #channel{client = Client = #{zone := Zone}, protocol = Protocol}) -> MaxInflight = get_property('Receive-Maximum', ConnProps, emqx_zone:get_env(Zone, max_inflight, 65535)), - Interval = get_property('Session-Expiry-Interval', ConnProps, - emqx_zone:get_env(Zone, session_expiry_interval, 0)), + + Interval = + case emqx_protocol:info(proto_ver, Protocol) of + ?MQTT_PROTO_V5 -> get_property('Session-Expiry-Interval', ConnProps, 0); + _ -> + case CleanStart of + true -> 0; + false -> emqx_zone:get_env(Zone, session_expiry_interval, 0) + end + end, emqx_cm:open_session(CleanStart, Client, #{max_inflight => MaxInflight, expiry_interval => Interval }). @@ -1034,6 +1058,9 @@ enrich_assigned_clientid(AckProps, #channel{client = #{client_id := ClientId}, ensure_connected(Channel) -> Channel#channel{connected = true, connected_at = os:timestamp()}. +ensure_disconnected(Channel) -> + Channel#channel{connected = false, disconnected_at = os:timestamp()}. + ensure_keepalive(#{'Server-Keep-Alive' := Interval}, Channel) -> ensure_keepalive_timer(Interval, Channel); ensure_keepalive(_AckProp, Channel = #channel{protocol = Protocol}) -> @@ -1111,3 +1138,6 @@ sp(false) -> 0. flag(true) -> 1; flag(false) -> 0. +shutdown(Reason, Channel) -> + {stop, {shutdown, Reason}, Channel}. + diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index f6cd96108..b6cdf5f8b 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -341,15 +341,13 @@ handle(info, {Error, _Sock, Reason}, State) when Error == tcp_error; Error == ssl_error -> shutdown(Reason, State); -%%TODO: fixme later. handle(info, {Closed, _Sock}, State = #state{chan_state = ChanState}) when Closed == tcp_closed; Closed == ssl_closed -> - case emqx_channel:info(protocol, ChanState) of - undefined -> shutdown(closed, State); - #{clean_start := true} -> - shutdown(closed, State); - #{clean_start := false} -> - {next_state, disconnected, State} + case emqx_channel:handle_info(sock_closed, ChanState) of + {ok, NChanState} -> + {next_state, disconnected, State#state{chan_state = NChanState}}; + {stop, Reason, NChanState} -> + stop(Reason, State#state{chan_state = NChanState}) end; handle(info, {Passive, _Sock}, State) when Passive == tcp_passive; diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 98afade52..32a782ce0 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -214,7 +214,7 @@ info(max_awaiting_rel, #session{max_awaiting_rel = MaxAwaitingRel}) -> info(await_rel_timeout, #session{await_rel_timeout = Timeout}) -> Timeout; info(expiry_interval, #session{expiry_interval = Interval}) -> - Interval div 1000; + Interval; info(created_at, #session{created_at = CreatedAt}) -> CreatedAt. diff --git a/test/emqx_session_SUITE.erl b/test/emqx_session_SUITE.erl index c142284c1..9f537ff19 100644 --- a/test/emqx_session_SUITE.erl +++ b/test/emqx_session_SUITE.erl @@ -73,12 +73,12 @@ apply_ops(Session, [Op | Rest]) -> apply_op(Session, info) -> Info = emqx_session:info(Session), ?assert(is_map(Info)), - ?assertEqual(16, maps:size(Info)), + ?assertEqual(15, maps:size(Info)), Session; apply_op(Session, attrs) -> Attrs = emqx_session:attrs(Session), ?assert(is_map(Attrs)), - ?assertEqual(3, maps:size(Attrs)), + ?assertEqual(2, maps:size(Attrs)), Session; apply_op(Session, stats) -> Stats = emqx_session:stats(Session), @@ -145,14 +145,7 @@ apply_op(Session, {pubcomp, PacketId}) -> end; apply_op(Session, {deliver, Delivers}) -> {ok, _Msgs, NSession} = emqx_session:deliver(Delivers, Session), - NSession; -apply_op(Session, {timeout, {TRef, TimeoutMsg}}) -> - case emqx_session:timeout(TRef, TimeoutMsg, Session) of - {ok, NSession} -> - NSession; - {ok, _Msg, NSession} -> - NSession - end. + NSession. %%%%%%%%%%%%%%%%%% %%% Generators %%% @@ -169,17 +162,13 @@ session_op_list() -> {pubrec, pubrec_args()}, {pubrel, pubrel_args()}, {pubcomp, pubcomp_args()}, - {deliver, deliver_args()}, - {timeout, timeout_args()} + {deliver, deliver_args()} ], list(?LAZY(oneof(Union))). deliver_args() -> list({deliver, topic(), message()}). -timeout_args() -> - {tref(), timeout_msg()}. - info_args() -> oneof([subscriptions, max_subscriptions, @@ -225,11 +214,6 @@ pubrel_args() -> pubcomp_args() -> packetid(). -timeout_msg() -> - oneof([retry_delivery, check_awaiting_rel]). - -tref() -> oneof([tref, undefined]). - sub_opts() -> ?LET({RH, RAP, NL, QOS, SHARE, SUBID}, {rh(), rap(), nl(), qos(), share(), subid()} From 8710df31b27a99ecc8ffcfc4f4d3e818fef16c0f Mon Sep 17 00:00:00 2001 From: Zhiwei Yu Date: Fri, 23 Aug 2019 09:42:55 +0800 Subject: [PATCH 23/36] Monitor cluster partition event (#2814) --- rebar.config | 2 +- src/emqx_sys_mon.erl | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/rebar.config b/rebar.config index 7ae5f1d87..65dcb2f0f 100644 --- a/rebar.config +++ b/rebar.config @@ -3,7 +3,7 @@ {cowboy, "2.6.1"}, % hex {gproc, "0.8.0"}, % hex {esockd, "5.5.0"}, %hex - {ekka, "0.6.0"}, %hex + {ekka, "0.6.1"}, %hex {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.4.1"}}}, {cuttlefish, {git, "https://github.com/emqx/cuttlefish", {tag, "v3.0.0"}}} ]}. diff --git a/src/emqx_sys_mon.erl b/src/emqx_sys_mon.erl index 7d7bdae21..f63dd95f6 100644 --- a/src/emqx_sys_mon.erl +++ b/src/emqx_sys_mon.erl @@ -57,6 +57,10 @@ start_link(Opts) -> init([Opts]) -> erlang:system_monitor(self(), parse_opt(Opts)), emqx_logger:set_proc_metadata(#{sysmon => true}), + + %% Monitor cluster partition event + ekka:monitor(partition, fun handle_partition_event/1), + {ok, start_timer(#{timer => undefined, events => []})}. start_timer(State) -> @@ -156,6 +160,15 @@ terminate(_Reason, #{timer := TRef}) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. +%%-------------------------------------------------------------------- +%% Internal Functions +%%-------------------------------------------------------------------- + +handle_partition_event({partition, {occurred, Node}}) -> + alarm_handler:set_alarm({partitioned, Node}); +handle_partition_event({partition, {healed, Node}}) -> + alarm_handler:clear_alarm(partitioned). + suppress(Key, SuccFun, State = #{events := Events}) -> case lists:member(Key, Events) of true -> {noreply, State}; From 7e95efecf83faf00c54ba90e9b119d81e9566c83 Mon Sep 17 00:00:00 2001 From: CrazyWisdom Date: Fri, 23 Aug 2019 10:05:12 +0800 Subject: [PATCH 24/36] docs(README.md): add slack invite url --- README-CN.md | 10 +++++----- README.md | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README-CN.md b/README-CN.md index 5901eed1b..4c8a6e20e 100644 --- a/README-CN.md +++ b/README-CN.md @@ -1,10 +1,10 @@ # EMQ X Broker -[![GitHub Release](https://img.shields.io/github/release/emqx/emqx?color=brightgreen)](https://github.com/emqx/emqx/releases) -[![Build Status](https://travis-ci.org/emqx/emqx.svg)](https://travis-ci.org/emqx/emqx) -[![Coverage Status](https://coveralls.io/repos/github/emqx/emqx/badge.svg)](https://coveralls.io/github/emqx/emqx) -[![Docker Pulls](https://img.shields.io/docker/pulls/emqx/emqx)](https://hub.docker.com/r/emqx/emqx) -[![Slack](https://img.shields.io/badge/Slack-EMQ%20X-brightgreen?logo=slack&style=flat&color=7E4798)](https://emqx.slack.com) +[![GitHub Release](https://img.shields.io/github/release/emqx/emqx?color=brightgreen)](https://github.com/emqx/emqx/releases) +[![Build Status](https://travis-ci.org/emqx/emqx.svg)](https://travis-ci.org/emqx/emqx) +[![Coverage Status](https://coveralls.io/repos/github/emqx/emqx/badge.svg)](https://coveralls.io/github/emqx/emqx) +[![Docker Pulls](https://img.shields.io/docker/pulls/emqx/emqx)](https://hub.docker.com/r/emqx/emqx) +[![Slack Invite]()](https://slack-invite.emqx.io) [![Twitter](https://img.shields.io/badge/Twiiter-EMQ%20X-1DA1F2?logo=twitter)](https://twitter.com/emqtt) [English](./README.md) | 简体中文 diff --git a/README.md b/README.md index c4bbaa60c..34e1221f2 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # EMQ X Broker -[![GitHub Release](https://img.shields.io/github/release/emqx/emqx?color=brightgreen)](https://github.com/emqx/emqx/releases) -[![Build Status](https://travis-ci.org/emqx/emqx.svg)](https://travis-ci.org/emqx/emqx) -[![Coverage Status](https://coveralls.io/repos/github/emqx/emqx/badge.svg)](https://coveralls.io/github/emqx/emqx) -[![Docker Pulls](https://img.shields.io/docker/pulls/emqx/emqx)](https://hub.docker.com/r/emqx/emqx) -[![Slack](https://img.shields.io/badge/Slack-EMQ%20X-brightgreen?logo=slack&style=flat&color=7E4798)](https://emqx.slack.com) +[![GitHub Release](https://img.shields.io/github/release/emqx/emqx?color=brightgreen)](https://github.com/emqx/emqx/releases) +[![Build Status](https://travis-ci.org/emqx/emqx.svg)](https://travis-ci.org/emqx/emqx) +[![Coverage Status](https://coveralls.io/repos/github/emqx/emqx/badge.svg)](https://coveralls.io/github/emqx/emqx) +[![Docker Pulls](https://img.shields.io/docker/pulls/emqx/emqx)](https://hub.docker.com/r/emqx/emqx) +[![Slack Invite]()](https://slack-invite.emqx.io) [![Twitter](https://img.shields.io/badge/Twiiter-EMQ%20X-1DA1F2?logo=twitter)](https://twitter.com/emqtt) English | [简体中文](./README-CN.md) From 7052993d00d7aaf44cb77e185a458ddb8b28d84a Mon Sep 17 00:00:00 2001 From: CrazyWisdom Date: Fri, 23 Aug 2019 10:05:12 +0800 Subject: [PATCH 25/36] docs(README.md): add slack invite url --- README-CN.md | 10 +++++----- README.md | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README-CN.md b/README-CN.md index 5901eed1b..4c8a6e20e 100644 --- a/README-CN.md +++ b/README-CN.md @@ -1,10 +1,10 @@ # EMQ X Broker -[![GitHub Release](https://img.shields.io/github/release/emqx/emqx?color=brightgreen)](https://github.com/emqx/emqx/releases) -[![Build Status](https://travis-ci.org/emqx/emqx.svg)](https://travis-ci.org/emqx/emqx) -[![Coverage Status](https://coveralls.io/repos/github/emqx/emqx/badge.svg)](https://coveralls.io/github/emqx/emqx) -[![Docker Pulls](https://img.shields.io/docker/pulls/emqx/emqx)](https://hub.docker.com/r/emqx/emqx) -[![Slack](https://img.shields.io/badge/Slack-EMQ%20X-brightgreen?logo=slack&style=flat&color=7E4798)](https://emqx.slack.com) +[![GitHub Release](https://img.shields.io/github/release/emqx/emqx?color=brightgreen)](https://github.com/emqx/emqx/releases) +[![Build Status](https://travis-ci.org/emqx/emqx.svg)](https://travis-ci.org/emqx/emqx) +[![Coverage Status](https://coveralls.io/repos/github/emqx/emqx/badge.svg)](https://coveralls.io/github/emqx/emqx) +[![Docker Pulls](https://img.shields.io/docker/pulls/emqx/emqx)](https://hub.docker.com/r/emqx/emqx) +[![Slack Invite]()](https://slack-invite.emqx.io) [![Twitter](https://img.shields.io/badge/Twiiter-EMQ%20X-1DA1F2?logo=twitter)](https://twitter.com/emqtt) [English](./README.md) | 简体中文 diff --git a/README.md b/README.md index 9847c8038..950dd64fc 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # EMQ X Broker -[![GitHub Release](https://img.shields.io/github/release/emqx/emqx?color=brightgreen)](https://github.com/emqx/emqx/releases) -[![Build Status](https://travis-ci.org/emqx/emqx.svg)](https://travis-ci.org/emqx/emqx) -[![Coverage Status](https://coveralls.io/repos/github/emqx/emqx/badge.svg)](https://coveralls.io/github/emqx/emqx) -[![Docker Pulls](https://img.shields.io/docker/pulls/emqx/emqx)](https://hub.docker.com/r/emqx/emqx) -[![Slack](https://img.shields.io/badge/Slack-EMQ%20X-brightgreen?logo=slack&style=flat&color=7E4798)](https://emqx.slack.com) +[![GitHub Release](https://img.shields.io/github/release/emqx/emqx?color=brightgreen)](https://github.com/emqx/emqx/releases) +[![Build Status](https://travis-ci.org/emqx/emqx.svg)](https://travis-ci.org/emqx/emqx) +[![Coverage Status](https://coveralls.io/repos/github/emqx/emqx/badge.svg)](https://coveralls.io/github/emqx/emqx) +[![Docker Pulls](https://img.shields.io/docker/pulls/emqx/emqx)](https://hub.docker.com/r/emqx/emqx) +[![Slack Invite]()](https://slack-invite.emqx.io) [![Twitter](https://img.shields.io/badge/Twiiter-EMQ%20X-1DA1F2?logo=twitter)](https://twitter.com/emqtt) English | [简体中文](./README-CN.md) From cff120c6d0553fbb27cedea9a08ea1650458d9e6 Mon Sep 17 00:00:00 2001 From: zhouzb Date: Fri, 23 Aug 2019 11:52:15 +0800 Subject: [PATCH 26/36] Handle session expiry interval correctly --- src/emqx_channel.erl | 37 +++++++++++++++++++++++++++++-------- src/emqx_protocol.erl | 3 +++ src/emqx_session.erl | 5 +++++ 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/emqx_channel.erl b/src/emqx_channel.erl index 84864fd07..137770c73 100644 --- a/src/emqx_channel.erl +++ b/src/emqx_channel.erl @@ -325,13 +325,35 @@ handle_in(Packet = ?UNSUBSCRIBE_PACKET(PacketId, Properties, TopicFilters), handle_in(?PACKET(?PINGREQ), Channel) -> {ok, ?PACKET(?PINGRESP), Channel}; -handle_in(?DISCONNECT_PACKET(?RC_SUCCESS), Channel) -> - %% Clear will msg - {stop, normal, Channel}; - -handle_in(?DISCONNECT_PACKET(RC), Channel = #channel{protocol = Protocol}) -> - Ver = emqx_protocol:info(proto_ver, Protocol), - {stop, {shutdown, emqx_reason_codes:name(RC, Ver)}, Channel}; +handle_in(?DISCONNECT_PACKET(RC, Properties), Channel = #channel{session = Session, protocol = Protocol}) -> + OldInterval = emqx_session:info(expiry_interval, Session), + Interval = maps:get('Session-Expiry-Interval', case Properties of + undefined -> #{}; + _ -> Properties + end, OldInterval), + case OldInterval =:= 0 andalso Interval =/= OldInterval of + true -> + handle_out({disconnect, ?RC_PROTOCOL_ERROR}, Channel); + false -> + NChannel = ensure_disconnected(case RC of + ?RC_SUCCESS -> + Channel#channel{protocol = emqx_protocol:clear_will_msg(Protocol), + session = emqx_session:update_expiry_interval(Interval, Session)}; + _ -> Channel#channel{session = emqx_session:update_expiry_interval(Interval, Session)} + end), + case Interval of + ?UINT_MAX -> {ok, NChannel}; + Int when Int > 0 -> {ok, ensure_timer(expire_timer, NChannel)}; + _Other -> + Reason = case RC of + ?RC_SUCCESS -> closed; + _ -> + Ver = emqx_protocol:info(proto_ver, Protocol), + emqx_reason_codes:name(RC, Ver) + end, + {stop, {shutdown, Reason}, Channel} + end + end; handle_in(?AUTH_PACKET(), Channel) -> %%TODO: implement later. @@ -905,7 +927,6 @@ open_session(#mqtt_packet_connect{clean_start = CleanStart, #channel{client = Client = #{zone := Zone}, protocol = Protocol}) -> MaxInflight = get_property('Receive-Maximum', ConnProps, emqx_zone:get_env(Zone, max_inflight, 65535)), - Interval = case emqx_protocol:info(proto_ver, Protocol) of ?MQTT_PROTO_V5 -> get_property('Session-Expiry-Interval', ConnProps, 0); diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index ebd59106d..8bb5df7bf 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -28,6 +28,7 @@ -export([ find_alias/2 , save_alias/3 + , clear_will_msg/1 ]). -export_type([protocol/0]). @@ -134,3 +135,5 @@ save_alias(AliasId, Topic, Protocol = #protocol{topic_aliases = undefined}) -> save_alias(AliasId, Topic, Protocol = #protocol{topic_aliases = Aliases}) -> Protocol#protocol{topic_aliases = maps:put(AliasId, Topic, Aliases)}. +clear_will_msg(Protocol) -> + Protocol#protocol{will_msg = undefined}. diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 32a782ce0..6f2df1b8c 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -58,6 +58,8 @@ , stats/1 ]). +-export([update_expiry_interval/2]). + -export([ subscribe/4 , unsubscribe/3 ]). @@ -218,6 +220,9 @@ info(expiry_interval, #session{expiry_interval = Interval}) -> info(created_at, #session{created_at = CreatedAt}) -> CreatedAt. +update_expiry_interval(ExpiryInterval, Session) -> + Session#session{expiry_interval = ExpiryInterval}. + %%-------------------------------------------------------------------- %% Attrs of the session %%-------------------------------------------------------------------- From f3a92f35f6f4d0202cdeaec57a041204ad5cedb3 Mon Sep 17 00:00:00 2001 From: zhouzb Date: Fri, 23 Aug 2019 13:21:45 +0800 Subject: [PATCH 27/36] Handle will message correctly --- src/emqx_channel.erl | 29 ++++++++++++++++++++++------- src/emqx_protocol.erl | 23 ++++++++++++++++++++++- 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/src/emqx_channel.erl b/src/emqx_channel.erl index 137770c73..38e3513cb 100644 --- a/src/emqx_channel.erl +++ b/src/emqx_channel.erl @@ -87,7 +87,8 @@ alive_timer => keepalive, retry_timer => retry_delivery, await_timer => expire_awaiting_rel, - expire_timer => expire_session + expire_timer => expire_session, + will_timer => will_message }). %%-------------------------------------------------------------------- @@ -342,16 +343,18 @@ handle_in(?DISCONNECT_PACKET(RC, Properties), Channel = #channel{session = Sessi _ -> Channel#channel{session = emqx_session:update_expiry_interval(Interval, Session)} end), case Interval of - ?UINT_MAX -> {ok, NChannel}; - Int when Int > 0 -> {ok, ensure_timer(expire_timer, NChannel)}; + ?UINT_MAX -> + {ok, ensure_timer(will_timer, NChannel)}; + Int when Int > 0 -> + {ok, ensure_timer([will_timer, expire_timer], NChannel)}; _Other -> Reason = case RC of - ?RC_SUCCESS -> closed; + ?RC_SUCCESS -> normal; _ -> Ver = emqx_protocol:info(proto_ver, Protocol), emqx_reason_codes:name(RC, Ver) end, - {stop, {shutdown, Reason}, Channel} + {stop, {shutdown, Reason}, NChannel} end end; @@ -680,9 +683,14 @@ timeout(TRef, expire_awaiting_rel, Channel = #channel{session = Session, {ok, reset_timer(await_timer, Timeout, Channel#channel{session = Session})} end; -timeout(_TRef, expire_session, Channel) -> +timeout(TRef, expire_session, Channel = #channel{timers = #{expire_timer := TRef}}) -> shutdown(expired, Channel); +timeout(TRef, will_message, Channel = #channel{protocol = Protocol, + timers = #{will_timer := TRef}}) -> + publish_will_msg(emqx_protocol:info(will_msg, Protocol)), + {ok, clean_timer(will_timer, Channel#channel{protocol = emqx_protocol:clear_will_msg(Protocol)})}; + timeout(_TRef, Msg, Channel) -> ?LOG(error, "Unexpected timeout: ~p~n", [Msg]), {ok, Channel}. @@ -691,6 +699,11 @@ timeout(_TRef, Msg, Channel) -> %% Ensure timers %%-------------------------------------------------------------------- +ensure_timer([Name], Channel) -> + ensure_timer(Name, Channel); +ensure_timer([Name | Rest], Channel) -> + ensure_timer(Rest, ensure_timer(Name, Channel)); + ensure_timer(Name, Channel = #channel{timers = Timers}) -> TRef = maps:get(Name, Timers, undefined), Time = interval(Name, Channel), @@ -723,7 +736,9 @@ interval(retry_timer, #channel{session = Session}) -> interval(await_timer, #channel{session = Session}) -> emqx_session:info(await_rel_timeout, Session); interval(expire_timer, #channel{session = Session}) -> - timer:seconds(emqx_session:info(expiry_interval, Session)). + timer:seconds(emqx_session:info(expiry_interval, Session)); +interval(will_timer, #channel{protocol = Protocol}) -> + timer:seconds(emqx_protocol:info(will_delay_interval, Protocol)). %%-------------------------------------------------------------------- %% Terminate diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 8bb5df7bf..1007db0b5 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -59,13 +59,22 @@ -spec(init(#mqtt_packet_connect{}) -> protocol()). init(#mqtt_packet_connect{proto_name = ProtoName, proto_ver = ProtoVer, + will_props = WillProps, clean_start = CleanStart, keepalive = Keepalive, properties = Properties, client_id = ClientId, username = Username } = ConnPkt) -> - WillMsg = emqx_packet:will_msg(ConnPkt), + WillMsg = 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), #protocol{proto_name = ProtoName, proto_ver = ProtoVer, clean_start = CleanStart, @@ -110,6 +119,8 @@ info(username, #protocol{username = Username}) -> Username; info(will_msg, #protocol{will_msg = WillMsg}) -> WillMsg; +info(will_delay_interval, #protocol{will_msg = WillMsg}) -> + emqx_message:get_header('Will-Delay-Interval', WillMsg, 0); info(conn_props, #protocol{conn_props = ConnProps}) -> ConnProps; info(topic_aliases, #protocol{topic_aliases = Aliases}) -> @@ -137,3 +148,13 @@ save_alias(AliasId, Topic, Protocol = #protocol{topic_aliases = Aliases}) -> clear_will_msg(Protocol) -> Protocol#protocol{will_msg = undefined}. + +set_property(Name, Value, undefined) -> + #{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). From 44d53ecb332aafa1fa8f0d7dc94e8329621d2d5e Mon Sep 17 00:00:00 2001 From: zhouzb Date: Fri, 23 Aug 2019 13:48:17 +0800 Subject: [PATCH 28/36] Update emqx_channel.erl --- src/emqx_channel.erl | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/src/emqx_channel.erl b/src/emqx_channel.erl index 38e3513cb..fc64ba71c 100644 --- a/src/emqx_channel.erl +++ b/src/emqx_channel.erl @@ -328,25 +328,21 @@ handle_in(?PACKET(?PINGREQ), Channel) -> handle_in(?DISCONNECT_PACKET(RC, Properties), Channel = #channel{session = Session, protocol = Protocol}) -> OldInterval = emqx_session:info(expiry_interval, Session), - Interval = maps:get('Session-Expiry-Interval', case Properties of - undefined -> #{}; - _ -> Properties - end, OldInterval), + Interval = get_property('Session-Expiry-Interval', Properties, OldInterval), case OldInterval =:= 0 andalso Interval =/= OldInterval of true -> handle_out({disconnect, ?RC_PROTOCOL_ERROR}, Channel); false -> - NChannel = ensure_disconnected(case RC of - ?RC_SUCCESS -> - Channel#channel{protocol = emqx_protocol:clear_will_msg(Protocol), - session = emqx_session:update_expiry_interval(Interval, Session)}; - _ -> Channel#channel{session = emqx_session:update_expiry_interval(Interval, Session)} - end), + Channel1 = case RC of + ?RC_SUCCESS -> Channel#channel{protocol = emqx_protocol:clear_will_msg(Protocol)}; + _ -> Channel + end, + Channel2 = ensure_disconnected(Channel1#channel{session = emqx_session:update_expiry_interval(Interval, Session)}), case Interval of ?UINT_MAX -> - {ok, ensure_timer(will_timer, NChannel)}; + {ok, ensure_timer(will_timer, Channel2)}; Int when Int > 0 -> - {ok, ensure_timer([will_timer, expire_timer], NChannel)}; + {ok, ensure_timer([will_timer, expire_timer], Channel2)}; _Other -> Reason = case RC of ?RC_SUCCESS -> normal; @@ -354,7 +350,7 @@ handle_in(?DISCONNECT_PACKET(RC, Properties), Channel = #channel{session = Sessi Ver = emqx_protocol:info(proto_ver, Protocol), emqx_reason_codes:name(RC, Ver) end, - {stop, {shutdown, Reason}, NChannel} + {stop, {shutdown, Reason}, Channel2} end end; From 82b9a7c3012f16b25369e83dbd28f751e96f61b9 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 23 Aug 2019 14:09:00 +0800 Subject: [PATCH 29/36] Improve session takeover (#2831) Implement the session takover/resumption across nodes --- src/emqx_channel.erl | 70 +++++++++++++++++++++++++++++++---------- src/emqx_cm.erl | 18 +++++++---- src/emqx_connection.erl | 31 +++++++++++------- src/emqx_misc.erl | 6 +++- src/emqx_session.erl | 40 +++++++++++++++++------ 5 files changed, 119 insertions(+), 46 deletions(-) diff --git a/src/emqx_channel.erl b/src/emqx_channel.erl index 38e3513cb..5154d8462 100644 --- a/src/emqx_channel.erl +++ b/src/emqx_channel.erl @@ -73,10 +73,14 @@ oom_policy :: emqx_oom:oom_policy(), %% Connected connected :: boolean(), + %% Connected at connected_at :: erlang:timestamp(), disconnected_at :: erlang:timestamp(), - %% Takeover/Resume + %% Takeover + takeover :: boolean(), + %% Resume resuming :: boolean(), + %% Pending delivers when takeovering pendings :: list() }). @@ -125,7 +129,10 @@ init(ConnInfo, Options) -> gc_state = GcState, oom_policy = OomPolicy, timers = #{stats_timer => StatsTimer}, - connected = false + connected = false, + takeover = false, + resuming = false, + pendings = [] }. peer_cert_as_username(Options) -> @@ -234,7 +241,7 @@ handle_in(?CONNECT_PACKET(ConnPkt), Channel) -> handle_out({connack, ReasonCode}, NChannel) end; -handle_in(Packet = ?PUBLISH_PACKET(QoS, Topic, PacketId), Channel = #channel{protocol = Protocol}) -> +handle_in(Packet = ?PUBLISH_PACKET(_QoS, Topic, _PacketId), Channel = #channel{protocol = Protocol}) -> case pipeline([fun validate_packet/2, fun process_alias/2, fun check_publish/2], Packet, Channel) of @@ -372,9 +379,15 @@ handle_in(Packet, Channel) -> process_connect(ConnPkt, Channel) -> case open_session(ConnPkt, Channel) of - {ok, Session, SP} -> + {ok, #{session := Session, present := false}} -> NChannel = Channel#channel{session = Session}, - handle_out({connack, ?RC_SUCCESS, sp(SP)}, NChannel); + handle_out({connack, ?RC_SUCCESS, sp(false)}, NChannel); + {ok, #{session := Session, present := true, pendings := Pendings}} -> + NPendings = lists:usort(lists:append(Pendings, emqx_misc:drain_deliver())), + NChannel = Channel#channel{session = Session, + resuming = true, + pendings = NPendings}, + handle_out({connack, ?RC_SUCCESS, sp(true)}, NChannel); {error, Reason} -> %% TODO: Unknown error? ?LOG(error, "Failed to open session: ~p", [Reason]), @@ -474,8 +487,17 @@ handle_out({connack, ?RC_SUCCESS, SP}, Channel = #channel{client = Client}) -> fun enrich_server_keepalive/2, fun enrich_assigned_clientid/2 ], #{}, Channel), - NChannel = ensure_keepalive(AckProps, ensure_connected(Channel)), - {ok, ?CONNACK_PACKET(?RC_SUCCESS, SP, AckProps), NChannel}; + AckPacket = ?CONNACK_PACKET(?RC_SUCCESS, SP, AckProps), + Channel1 = ensure_keepalive(AckProps, ensure_connected(Channel)), + case maybe_resume_session(Channel1) of + ignore -> {ok, AckPacket, Channel1}; + {ok, Publishes, NSession} -> + Channel2 = Channel1#channel{session = NSession, + resuming = false, + pendings = []}, + {ok, Packets, _} = handle_out({publish, Publishes}, Channel2), + {ok, [AckPacket|Packets], Channel2} + end; handle_out({connack, ReasonCode}, Channel = #channel{client = Client, protocol = Protocol @@ -489,9 +511,12 @@ handle_out({connack, ReasonCode}, Channel = #channel{client = Client, Reason = emqx_reason_codes:name(ReasonCode1, ProtoVer), {stop, {shutdown, Reason}, ?CONNACK_PACKET(ReasonCode1), Channel}; -handle_out({deliver, Delivers}, Channel = #channel{resuming = true, - pendings = Pendings - }) -> +handle_out({deliver, Delivers}, Channel = #channel{session = Session, + connected = false}) -> + {ok, Channel#channel{session = emqx_session:enqueue(Delivers, Session)}}; + +handle_out({deliver, Delivers}, Channel = #channel{takeover = true, + pendings = Pendings}) -> {ok, Channel#channel{pendings = lists:append(Pendings, Delivers)}}; handle_out({deliver, Delivers}, Channel = #channel{session = Session}) -> @@ -571,20 +596,18 @@ handle_out({Type, Data}, Channel) -> %% Handle call %%-------------------------------------------------------------------- -%%-------------------------------------------------------------------- -%% Takeover session -%%-------------------------------------------------------------------- - +%% Session Takeover handle_call({takeover, 'begin'}, Channel = #channel{session = Session}) -> - {ok, Session, Channel#channel{resuming = true}}; + {ok, Session, Channel#channel{takeover = true}}; handle_call({takeover, 'end'}, Channel = #channel{session = Session, pendings = Pendings}) -> ok = emqx_session:takeover(Session), - {stop, {shutdown, takeovered}, Pendings, Channel}; + AllPendings = lists:append(emqx_misc:drain_deliver(), Pendings), + {stop, {shutdown, takeovered}, AllPendings, Channel}; handle_call(Req, Channel) -> - ?LOG(error, "Unexpected call: Req", [Req]), + ?LOG(error, "Unexpected call: ~p", [Req]), {ok, ignored, Channel}. %%-------------------------------------------------------------------- @@ -1110,6 +1133,19 @@ ensure_keepalive_timer(Interval, Channel = #channel{client = #{zone := Zone}}) - Keepalive = emqx_keepalive:init(round(timer:seconds(Interval) * Backoff)), ensure_timer(alive_timer, Channel#channel{keepalive = Keepalive}). +maybe_resume_session(#channel{resuming = false}) -> + ignore; +maybe_resume_session(#channel{session = Session, + resuming = true, + pendings = Pendings}) -> + {ok, Publishes, Session1} = emqx_session:redeliver(Session), + case emqx_session:deliver(Pendings, Session1) of + {ok, Session2} -> + {ok, Publishes, Session2}; + {ok, More, Session2} -> + {ok, lists:append(Publishes, More), Session2} + end. + %%-------------------------------------------------------------------- %% Is ACL enabled? %%-------------------------------------------------------------------- diff --git a/src/emqx_cm.erl b/src/emqx_cm.erl index 1e5842ec4..9c3b58068 100644 --- a/src/emqx_cm.erl +++ b/src/emqx_cm.erl @@ -157,11 +157,15 @@ set_chan_stats(ClientId, ChanPid, Stats) -> %% @doc Open a session. -spec(open_session(boolean(), emqx_types:client(), map()) - -> {ok, emqx_session:session()} | {error, Reason :: term()}). + -> {ok, #{session := emqx_session:session(), + present := boolean(), + pendings => list()}} + | {error, Reason :: term()}). open_session(true, Client = #{client_id := ClientId}, Options) -> CleanStart = fun(_) -> ok = discard_session(ClientId), - {ok, emqx_session:init(Client, Options), false} + Session = emqx_session:init(Client, Options), + {ok, #{session => Session, present => false}} end, emqx_cm_locker:trans(ClientId, CleanStart); @@ -169,12 +173,14 @@ open_session(false, Client = #{client_id := ClientId}, Options) -> ResumeStart = fun(_) -> case takeover_session(ClientId) of {ok, ConnMod, ChanPid, Session} -> - NSession = emqx_session:resume(ClientId, Session), + ok = emqx_session:resume(ClientId, Session), Pendings = ConnMod:takeover(ChanPid, 'end'), - io:format("Pending Delivers: ~p~n", [Pendings]), - {ok, NSession, true}; + {ok, #{session => Session, + present => true, + pendings => Pendings}}; {error, not_found} -> - {ok, emqx_session:init(Client, Options), false} + Session = emqx_session:init(Client, Options), + {ok, #{session => Session, present => false}} end end, emqx_cm_locker:trans(ClientId, ResumeStart). diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index b6cdf5f8b..a3cbc1157 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -255,18 +255,8 @@ connected(cast, {incoming, Packet = ?PACKET(?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{chan_state = ChanState}) -> - Delivers = emqx_misc:drain_deliver([Deliver]), - case emqx_channel:handle_out({deliver, 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); - {stop, Reason, NChanState} -> - stop(Reason, State#state{chan_state = NChanState}) - end; +connected(info, Deliver = {deliver, _Topic, _Msg}, State) -> + handle_deliver(emqx_misc:drain_deliver([Deliver]), State); connected(EventType, Content, State) -> ?HANDLE(EventType, Content, State). @@ -279,6 +269,9 @@ disconnected(enter, _, _State) -> %% CleanStart is true keep_state_and_data; +disconnected(info, Deliver = {deliver, _Topic, _Msg}, State) -> + handle_deliver([Deliver], State); + disconnected(EventType, Content, State) -> ?HANDLE(EventType, Content, State). @@ -469,6 +462,20 @@ handle_incoming(Packet = ?PACKET(Type), SuccFun, handle_outgoing(OutPacket, Shutdown, State#state{chan_state = NChanState}) end. +%%------------------------------------------------------------------- +%% Handle deliver + +handle_deliver(Delivers, State = #state{chan_state = ChanState}) -> + case emqx_channel:handle_out({deliver, 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); + {stop, Reason, NChanState} -> + stop(Reason, State#state{chan_state = NChanState}) + end. + %%-------------------------------------------------------------------- %% Handle outgoing packets diff --git a/src/emqx_misc.erl b/src/emqx_misc.erl index 94f859a82..a325dc94b 100644 --- a/src/emqx_misc.erl +++ b/src/emqx_misc.erl @@ -29,7 +29,8 @@ , proc_stats/1 ]). --export([ drain_deliver/1 +-export([ drain_deliver/0 + , drain_deliver/1 , drain_down/1 ]). @@ -96,6 +97,9 @@ proc_stats(Pid) -> end. %% @doc Drain delivers from the channel's mailbox. +drain_deliver() -> + drain_deliver([]). + drain_deliver(Acc) -> receive Deliver = {deliver, _Topic, _Msg} -> diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 6f2df1b8c..cddaabe35 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -72,11 +72,13 @@ ]). -export([ deliver/2 + , enqueue/2 , retry/1 ]). -export([ takeover/1 , resume/2 + , redeliver/1 ]). -export([expire/2]). @@ -264,18 +266,30 @@ takeover(#session{subscriptions = Subs}) -> ok = emqx_broker:unsubscribe(TopicFilter) end, maps:to_list(Subs)). --spec(resume(emqx_types:client_id(), session()) -> session()). -resume(ClientId, Session = #session{subscriptions = Subs}) -> +-spec(resume(emqx_types:client_id(), session()) -> ok). +resume(ClientId, #session{subscriptions = Subs}) -> ?LOG(info, "Session is resumed."), - %% 1. Subscribe again - ok = lists:foreach(fun({TopicFilter, SubOpts}) -> - ok = emqx_broker:subscribe(TopicFilter, ClientId, SubOpts) - end, maps:to_list(Subs)), + %% 1. Subscribe again. + lists:foreach(fun({TopicFilter, SubOpts}) -> + ok = emqx_broker:subscribe(TopicFilter, ClientId, SubOpts) + end, maps:to_list(Subs)). %% 2. Run hooks. - ok = emqx_hooks:run('session.resumed', [#{client_id => ClientId}, attrs(Session)]), + %% ok = emqx_hooks:run('session.resumed', [#{client_id => ClientId}, attrs(Session)]), %% TODO: 3. Redeliver: Replay delivery and Dequeue pending messages - %% noreply(dequeue(retry_delivery(true, State1))); - Session. + %%Session. + +redeliver(Session = #session{inflight = Inflight}) -> + Publishes = lists:map(fun({PacketId, {pubrel, _Ts}}) -> + {pubrel, PacketId, ?RC_SUCCESS}; + ({PacketId, {Msg, _Ts}}) -> + {publish, PacketId, Msg} + end, emqx_inflight:to_list(Inflight)), + case dequeue(Session) of + {ok, NSession} -> + {ok, Publishes, NSession}; + {ok, More, NSession} -> + {ok, lists:append(Publishes, More), NSession} + end. %%-------------------------------------------------------------------- %% Client -> Broker: SUBSCRIBE @@ -501,7 +515,13 @@ deliver([Msg = #message{qos = QoS}|More], Acc, deliver(More, [Publish|Acc], next_pkt_id(Session1)) end. -enqueue(Msg, Session = #session{mqueue = Q}) -> +enqueue(Delivers, Session = #session{subscriptions = Subs}) + when is_list(Delivers) -> + Msgs = [enrich(get_subopts(Topic, Subs), Msg, Session) + || {deliver, Topic, Msg} <- Delivers], + lists:foldl(fun enqueue/2, Session, Msgs); + +enqueue(Msg, Session = #session{mqueue = Q}) when is_record(Msg, message) -> emqx_pd:update_counter(enqueue_stats, 1), {Dropped, NewQ} = emqx_mqueue:in(Msg, Q), if From 738145677afb62c1857ac364489e0d388e4e842a Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 23 Aug 2019 14:31:33 +0800 Subject: [PATCH 30/36] Handle the 'discard' cast. --- src/emqx_connection.erl | 3 +++ src/emqx_ws_connection.erl | 7 +++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index a3cbc1157..93daf4cba 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -310,6 +310,9 @@ handle({call, From}, Req, State = #state{chan_state = ChanState}) -> stop(Reason, State#state{chan_state = NChanState}) end; +handle(cast, discard, State) -> + shutdown(discarded, State); + %% Handle cast handle(cast, Msg, State = #state{chan_state = ChanState}) -> case emqx_channel:handle_cast(Msg, ChanState) of diff --git a/src/emqx_ws_connection.erl b/src/emqx_ws_connection.erl index d682d74ab..e28f74b06 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_connection.erl @@ -244,6 +244,9 @@ websocket_info({call, From, Req}, State = #state{chan_state = ChanState}) -> stop(Reason, State#state{chan_state = NChanState}) end; +websocket_info({cast, discard}, State) -> + stop(discarded, State); + websocket_info({cast, Msg}, State = #state{chan_state = ChanState}) -> case emqx_channel:handle_cast(Msg, ChanState) of {ok, NChanState} -> @@ -295,10 +298,6 @@ websocket_info({timeout, TRef, emit_stats}, State) when is_reference(TRef) -> websocket_info({timeout, TRef, Msg}, State) when is_reference(TRef) -> handle_timeout(TRef, Msg, State); -websocket_info({shutdown, discard, {ClientId, ByPid}}, State) -> - ?LOG(warning, "Discarded by ~s:~p", [ClientId, ByPid]), - stop(discard, State); - websocket_info({shutdown, conflict, {ClientId, NewPid}}, State) -> ?LOG(warning, "Clientid '~s' conflict with ~p", [ClientId, NewPid]), stop(conflict, State); From ebc1bd77a9e7124e52db152467a1e0ac685433a4 Mon Sep 17 00:00:00 2001 From: tigercl Date: Fri, 23 Aug 2019 14:41:52 +0800 Subject: [PATCH 31/36] Send the will message immediately when the network connection is closed by the client (#2834) Send the will message immediately when the network connection is closed by the client --- src/emqx_channel.erl | 11 +++++++---- test/emqx_channel_SUITE.erl | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/emqx_channel.erl b/src/emqx_channel.erl index c11b1e651..5b9a89a31 100644 --- a/src/emqx_channel.erl +++ b/src/emqx_channel.erl @@ -639,14 +639,17 @@ handle_info({unsubscribe, TopicFilters}, Channel = #channel{client = Client}) -> handle_info(sock_closed, Channel = #channel{connected = false}) -> shutdown(closed, Channel); -handle_info(sock_closed, Channel = #channel{session = Session}) -> +handle_info(sock_closed, Channel = #channel{protocol = Protocol, + session = Session}) -> + publish_will_msg(emqx_protocol:info(will_msg, Protocol)), + NChannel = Channel#channel{protocol = emqx_protocol:clear_will_msg(Protocol)}, Interval = emqx_session:info(expiry_interval, Session), case Interval of ?UINT_MAX -> - {ok, ensure_disconnected(Channel)}; + {ok, ensure_disconnected(NChannel)}; Int when Int > 0 -> - {ok, ensure_timer(expire_timer, ensure_disconnected(Channel))}; - _Other -> shutdown(closed, Channel) + {ok, ensure_timer(expire_timer, ensure_disconnected(NChannel))}; + _Other -> shutdown(closed, NChannel) end; handle_info(Info, Channel) -> diff --git a/test/emqx_channel_SUITE.erl b/test/emqx_channel_SUITE.erl index e7326cd76..1423046ab 100644 --- a/test/emqx_channel_SUITE.erl +++ b/test/emqx_channel_SUITE.erl @@ -143,7 +143,7 @@ t_handle_pingreq(_) -> t_handle_disconnect(_) -> with_channel( fun(Channel) -> - {stop, normal, Channel1} = handle_in(?DISCONNECT_PACKET(?RC_SUCCESS), Channel), + {stop, {shutdown, normal}, Channel1} = handle_in(?DISCONNECT_PACKET(?RC_SUCCESS), Channel), ?assertMatch(#{will_msg := undefined}, emqx_channel:info(protocol, Channel1)) end). From 710302f377e07056fad0d2330fb11dd87197e483 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 23 Aug 2019 14:52:11 +0800 Subject: [PATCH 32/36] Fix case clause error --- src/emqx_channel.erl | 3 --- src/emqx_cm.erl | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/emqx_channel.erl b/src/emqx_channel.erl index c11b1e651..e1a38e43e 100644 --- a/src/emqx_channel.erl +++ b/src/emqx_channel.erl @@ -610,9 +610,6 @@ handle_call(Req, Channel) -> %% Handle cast %%-------------------------------------------------------------------- -handle_cast(discard, Channel) -> - {stop, {shutdown, discarded}, Channel}; - handle_cast(Msg, Channel) -> ?LOG(error, "Unexpected cast: ~p", [Msg]), {ok, Channel}. diff --git a/src/emqx_cm.erl b/src/emqx_cm.erl index 9c3b58068..3673085ac 100644 --- a/src/emqx_cm.erl +++ b/src/emqx_cm.erl @@ -233,7 +233,7 @@ discard_session(ClientId) when is_binary(ClientId) -> discard_session(ClientId, ChanPid) when node(ChanPid) == node() -> case get_chan_attrs(ClientId, ChanPid) of - #{conn_mod := ConnMod} -> + #{client := #{conn_mod := ConnMod}} -> ConnMod:discard(ChanPid); undefined -> ok end; From 6352f9e2dccac164fa0ba8d84823e6b101d8627b Mon Sep 17 00:00:00 2001 From: terry-xiaoyu <506895667@qq.com> Date: Fri, 23 Aug 2019 15:19:51 +0800 Subject: [PATCH 33/36] Update testcase emqx_channel:handle_out --- test/emqx_channel_SUITE.erl | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test/emqx_channel_SUITE.erl b/test/emqx_channel_SUITE.erl index 1423046ab..0c8e61091 100644 --- a/test/emqx_channel_SUITE.erl +++ b/test/emqx_channel_SUITE.erl @@ -166,10 +166,7 @@ t_handle_deliver(_) -> Msg0 = emqx_message:make(<<"clientx">>, ?QOS_0, <<"t0">>, <<"qos0">>), Msg1 = emqx_message:make(<<"clientx">>, ?QOS_1, <<"t1">>, <<"qos1">>), Delivers = [{deliver, <<"+">>, Msg0}, {deliver, <<"+">>, Msg1}], - {ok, Packets, _Ch} = emqx_channel:handle_out({deliver, Delivers}, Channel1), - ?assertMatch([?PUBLISH_PACKET(?QOS_0, <<"t0">>, undefined, <<"qos0">>), - ?PUBLISH_PACKET(?QOS_1, <<"t1">>, 1, <<"qos1">>) - ], Packets) + {ok, _Ch} = emqx_channel:handle_out({deliver, Delivers}, Channel1) end). %%-------------------------------------------------------------------- From 970d243d948f130a9a13554005ce73d39ceab535 Mon Sep 17 00:00:00 2001 From: zhouzb Date: Fri, 23 Aug 2019 15:21:20 +0800 Subject: [PATCH 34/36] Fix function clause --- src/emqx_protocol.erl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index 1007db0b5..ef7bbafe2 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -119,6 +119,8 @@ info(username, #protocol{username = Username}) -> Username; info(will_msg, #protocol{will_msg = WillMsg}) -> WillMsg; +info(will_delay_interval, #protocol{will_msg = undefined}) -> + 0; info(will_delay_interval, #protocol{will_msg = WillMsg}) -> emqx_message:get_header('Will-Delay-Interval', WillMsg, 0); info(conn_props, #protocol{conn_props = ConnProps}) -> From 9c171f5d9c8b07cacaeba99ebbd7cfee76effeaa Mon Sep 17 00:00:00 2001 From: zhouzb Date: Fri, 23 Aug 2019 16:08:26 +0800 Subject: [PATCH 35/36] Fix a bug that session terminates immediately when received DISCONNECT packet --- src/emqx_channel.erl | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/emqx_channel.erl b/src/emqx_channel.erl index 5bb4b5fb8..95588d6ed 100644 --- a/src/emqx_channel.erl +++ b/src/emqx_channel.erl @@ -73,6 +73,8 @@ oom_policy :: emqx_oom:oom_policy(), %% Connected connected :: boolean(), + %% Disonnected + disconnected :: boolean(), %% Connected at connected_at :: erlang:timestamp(), disconnected_at :: erlang:timestamp(), @@ -123,16 +125,17 @@ init(ConnInfo, Options) -> end, GcState = emqx_gc:init(emqx_zone:get_env(Zone, force_gc_policy, false)), OomPolicy = emqx_oom:init(emqx_zone:get_env(Zone, force_shutdown_policy)), - #channel{client = Client, - session = undefined, - protocol = undefined, - gc_state = GcState, - oom_policy = OomPolicy, - timers = #{stats_timer => StatsTimer}, - connected = false, - takeover = false, - resuming = false, - pendings = [] + #channel{client = Client, + session = undefined, + protocol = undefined, + gc_state = GcState, + oom_policy = OomPolicy, + timers = #{stats_timer => StatsTimer}, + connected = false, + disconnected = false, + takeover = false, + resuming = false, + pendings = [] }. peer_cert_as_username(Options) -> @@ -634,6 +637,8 @@ handle_info({unsubscribe, TopicFilters}, Channel = #channel{client = Client}) -> {_ReasonCodes, NChannel} = process_unsubscribe(TopicFilters1, Channel), {ok, NChannel}; +handle_info(sock_closed, Channel = #channel{disconnected = true}) -> + {ok, Channel}; handle_info(sock_closed, Channel = #channel{connected = false}) -> shutdown(closed, Channel); handle_info(sock_closed, Channel = #channel{protocol = Protocol, @@ -1111,10 +1116,10 @@ enrich_assigned_clientid(AckProps, #channel{client = #{client_id := ClientId}, end. ensure_connected(Channel) -> - Channel#channel{connected = true, connected_at = os:timestamp()}. + Channel#channel{connected = true, connected_at = os:timestamp(), disconnected = false}. ensure_disconnected(Channel) -> - Channel#channel{connected = false, disconnected_at = os:timestamp()}. + Channel#channel{connected = false, disconnected_at = os:timestamp(), disconnected = true}. ensure_keepalive(#{'Server-Keep-Alive' := Interval}, Channel) -> ensure_keepalive_timer(Interval, Channel); From 5cfd8b74bcea772a6433abdf2cfe9c753ae1ad01 Mon Sep 17 00:00:00 2001 From: zhouzb Date: Fri, 23 Aug 2019 17:29:48 +0800 Subject: [PATCH 36/36] Fix a bug that crash when protocol is undefined --- src/emqx_protocol.erl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index ef7bbafe2..6d11ce5b6 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -107,6 +107,8 @@ info(#protocol{proto_name = ProtoName, info(proto_name, #protocol{proto_name = ProtoName}) -> ProtoName; +info(proto_ver, undefined) -> + ?MQTT_PROTO_V4; info(proto_ver, #protocol{proto_ver = ProtoVer}) -> ProtoVer; info(clean_start, #protocol{clean_start = CleanStart}) ->