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/README-CN.md b/README-CN.md index d0ad2f556..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) | 简体中文 @@ -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/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 c6dc53fe7..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) @@ -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/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). diff --git a/include/emqx_mqtt.hrl b/include/emqx_mqtt.hrl index 854399999..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 %%-------------------------------------------------------------------- @@ -298,6 +300,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..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"}}} ]}. @@ -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..95588d6ed 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,1193 @@ -logger_header("[Channel]"). --export([start_link/3]). +-export([init/2]). -%% 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([ handle_in/2 + , handle_out/2 + , 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 - ]). +%% Ensure timer +-export([ensure_timer/2]). --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([gc/3]). --type(state() :: #state{}). +-import(emqx_misc, [maybe_apply/2]). --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]). +-import(emqx_access_control, [check_acl/3]). --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}])}. +-export_type([channel/0]). + +-record(channel, { + %% 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(), + %% Disonnected + disconnected :: boolean(), + %% Connected at + connected_at :: erlang:timestamp(), + disconnected_at :: erlang:timestamp(), + %% Takeover + takeover :: boolean(), + %% Resume + resuming :: boolean(), + %% Pending delivers when takeovering + pendings :: list() + }). + +-opaque(channel() :: #channel{}). + +-define(TIMER_TABLE, #{ + stats_timer => emit_stats, + alive_timer => keepalive, + retry_timer => retry_delivery, + await_timer => expire_awaiting_rel, + expire_timer => expire_session, + will_timer => will_message + }). %%-------------------------------------------------------------------- -%% API +%% Init the channel %%-------------------------------------------------------------------- -%% @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)). - -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, - proto_state = ProtoState, - connected = Connected, - connected_at = ConnectedAt}) -> - ConnAttrs = #{socktype => Transport:type(Socket), - peername => Peername, - sockname => Sockname, - connected => Connected, - connected_at => ConnectedAt}, - maps:merge(ConnAttrs, emqx_protocol:attrs(ProtoState)). - -%% @doc Get stats of the channel. --spec(stats(pid() | state()) -> emqx_types:stats()). -stats(CPid) when is_pid(CPid) -> - call(CPid, stats); -stats(#state{transport = Transport, - socket = Socket, - 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). - -%%-------------------------------------------------------------------- -%% 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)), +-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), + 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, + client_id => <<>>, + mountpoint => MountPoint, + is_bridge => false, + is_superuser => false}, ConnInfo), EnableStats = emqx_zone:get_env(Zone, enable_stats, true), - StatsTimer = if EnableStats -> undefined; ?Otherwise -> disabled end, - IdleTimout = emqx_zone:get_env(Zone, idle_timeout, 30000), - ok = emqx_misc:init_proc_mng_policy(Zone), - State = #state{transport = Transport, - socket = Socket, - peername = Peername, - 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]). + 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, + session = undefined, + protocol = undefined, + gc_state = GcState, + oom_policy = OomPolicy, + timers = #{stats_timer => StatsTimer}, + connected = false, + disconnected = false, + takeover = false, + resuming = false, + pendings = [] + }. -init_limiter(undefined) -> - undefined; -init_limiter({Rate, Burst}) -> - esockd_rate_limit:new(Rate, Burst). - -callback_mode() -> - [state_functions, state_enter]. +peer_cert_as_username(Options) -> + proplists:get_value(peer_cert_as_username, Options). %%-------------------------------------------------------------------- -%% Idle State +%% Info, Attrs and Caps +%%-------------------------------------------------------------------- -idle(enter, _, State) -> - case activate_socket(State) of - ok -> keep_state_and_data; - {error, Reason} -> - shutdown(Reason, State) - end; +-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 + }. -idle(timeout, _Timeout, State) -> - stop(idle_timeout, State); +-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; +info(disconnected_at, #channel{disconnected_at = DisconnectedAt}) -> + DisconnectedAt. -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); +-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 + }. -idle(cast, {incoming, Packet}, State) -> - ?LOG(warning, "Unexpected incoming: ~p", [Packet]), - shutdown(unexpected_incoming_packet, State); +%%TODO: ChanStats? +-spec(stats(channel()) -> emqx_types:stats()). +stats(#channel{session = Session}) -> + emqx_session:stats(Session). -idle(EventType, Content, State) -> - ?HANDLE(EventType, Content, State). +-spec(caps(channel()) -> emqx_types:caps()). +caps(#channel{client = #{zone := Zone}}) -> + emqx_mqtt_caps:get_caps(Zone). %%-------------------------------------------------------------------- -%% 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). - +%% For unit tests %%-------------------------------------------------------------------- -%% 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)]. +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 - -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. - %%-------------------------------------------------------------------- -%% Handle outgoing packets -handle_outgoing(Packets, SuccFun, State = #state{serialize = Serialize}) - when is_list(Packets) -> - send(lists:map(Serialize, Packets), SuccFun, State); +-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_outgoing(Packet, SuccFun, State = #state{serialize = Serialize}) -> - send(Serialize(Packet), SuccFun, State). +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 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) + end; -%%-------------------------------------------------------------------- -%% Serialize fun +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, ProtoVer)]), + 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; -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. +%%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; -%%-------------------------------------------------------------------- -%% Send data +%%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; -send(IoData, SuccFun, State = #state{transport = Transport, - socket = Socket}) -> - Oct = iolist_size(IoData), - ok = emqx_metrics:inc('bytes.sent', Oct), - case Transport:async_send(Socket, IoData) of - ok -> SuccFun(maybe_gc(1, Oct, State)); - {error, Reason} -> - shutdown(Reason, State) - end. +%%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; -%%-------------------------------------------------------------------- -%% Ensure keepalive +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; -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}). +handle_in(Packet = ?SUBSCRIBE_PACKET(PacketId, Properties, TopicFilters), + Channel = #channel{client = Client}) -> + case validate_packet(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; -%%-------------------------------------------------------------------- -%% Ensure rate limit +handle_in(Packet = ?UNSUBSCRIBE_PACKET(PacketId, Properties, TopicFilters), + Channel = #channel{client = Client}) -> + 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); + {error, ReasonCode} -> + handle_out({disconnect, ReasonCode}, Channel) + end; -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). +handle_in(?PACKET(?PINGREQ), Channel) -> + {ok, ?PACKET(?PINGRESP), 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) - 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 +handle_in(?DISCONNECT_PACKET(RC, Properties), Channel = #channel{session = Session, protocol = Protocol}) -> + OldInterval = emqx_session:info(expiry_interval, Session), + Interval = get_property('Session-Expiry-Interval', Properties, OldInterval), + case OldInterval =:= 0 andalso Interval =/= OldInterval of true -> - emqx_pd:update_counter(recv_msg, 1), - emqx_pd:update_counter(incoming_pubs, 1); + handle_out({disconnect, ?RC_PROTOCOL_ERROR}, Channel); + false -> + 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, Channel2)}; + Int when Int > 0 -> + {ok, ensure_timer([will_timer, expire_timer], Channel2)}; + _Other -> + Reason = case RC of + ?RC_SUCCESS -> normal; + _ -> + Ver = emqx_protocol:info(proto_ver, Protocol), + emqx_reason_codes:name(RC, Ver) + end, + {stop, {shutdown, Reason}, Channel2} + end + end; + +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}. + +%%-------------------------------------------------------------------- +%% Process Connect +%%-------------------------------------------------------------------- + +process_connect(ConnPkt, Channel) -> + case open_session(ConnPkt, Channel) of + {ok, #{session := Session, present := false}} -> + NChannel = Channel#channel{session = Session}, + 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]), + handle_out({connack, ?RC_UNSPECIFIED_ERROR}, Channel) + end. + +%%-------------------------------------------------------------------- +%% Process Publish +%%-------------------------------------------------------------------- + +%% Process Publish +process_publish(Packet = ?PUBLISH_PACKET(_QoS, _Topic, PacketId), + 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, emqx_protocol:info(proto_ver, Protocol), Msg)), + process_publish(PacketId, mount(Client, Msg1), Channel). + +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), + NChannel = Channel#channel{session = NSession}, + handle_out({pubrec, PacketId, ReasonCode}, + ensure_timer(await_timer, NChannel)); + {error, ReasonCode} -> + handle_out({pubrec, PacketId, ReasonCode}, Channel) + end. + +%%-------------------------------------------------------------------- +%% Process Subscribe +%%-------------------------------------------------------------------- + +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({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), + 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 + }) -> + 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) + end, + Reason = emqx_reason_codes:name(ReasonCode1, ProtoVer), + {stop, {shutdown, Reason}, ?CONNACK_PACKET(ReasonCode1), Channel}; + +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}) -> + 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) -> + {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{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{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}; + +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({Type, Data}, Channel) -> + ?LOG(error, "Unexpected outgoing: ~s, ~p", [Type, Data]), + {ok, Channel}. + +%%-------------------------------------------------------------------- +%% Handle call +%%-------------------------------------------------------------------- + +%% Session Takeover +handle_call({takeover, 'begin'}, Channel = #channel{session = Session}) -> + {ok, Session, Channel#channel{takeover = true}}; + +handle_call({takeover, 'end'}, Channel = #channel{session = Session, + pendings = Pendings}) -> + ok = emqx_session:takeover(Session), + AllPendings = lists:append(emqx_misc:drain_deliver(), Pendings), + {stop, {shutdown, takeovered}, AllPendings, Channel}; + +handle_call(Req, Channel) -> + ?LOG(error, "Unexpected call: ~p", [Req]), + {ok, ignored, Channel}. + +%%-------------------------------------------------------------------- +%% Handle cast +%%-------------------------------------------------------------------- + +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(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, + 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(NChannel)}; + Int when Int > 0 -> + {ok, ensure_timer(expire_timer, ensure_disconnected(NChannel))}; + _Other -> shutdown(closed, NChannel) + end; + +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, {emit_stats, Stats}, + Channel = #channel{client = #{client_id := ClientId}, + timers = #{stats_timer := TRef} + }) -> + ok = emqx_cm:set_chan_stats(ClientId, Stats), + {ok, clean_timer(stats_timer, Channel)}; + +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 = #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}. + +%%-------------------------------------------------------------------- +%% 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), + case TRef == undefined andalso Time > 0 of + true -> + ensure_timer(Name, Time, Channel); + false -> Channel %% Timer disabled or exists + end. + +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}}. + +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}) -> + timer:seconds(emqx_session:info(expiry_interval, Session)); +interval(will_timer, #channel{protocol = Protocol}) -> + timer:seconds(emqx_protocol:info(will_delay_interval, Protocol)). + +%%-------------------------------------------------------------------- +%% Terminate +%%-------------------------------------------------------------------- + +terminate(normal, #channel{client = Client}) -> + ok = emqx_hooks:run('client.disconnected', [Client, normal]); +terminate(Reason, #channel{client = Client, + protocol = Protocol + }) -> + ok = emqx_hooks:run('client.disconnected', [Client, Reason]), + if + Protocol == undefined -> ok; + true -> publish_will_msg(emqx_protocol:info(will_msg, Protocol)) + end. + +%%TODO: Improve will msg:) +publish_will_msg(undefined) -> + ok; +publish_will_msg(Msg) -> + emqx_broker:publish(Msg). + +%%-------------------------------------------------------------------- +%% GC the channel. +%%-------------------------------------------------------------------- + +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}. + +%% @doc Validate incoming packet. +-spec(validate_packet(emqx_types:packet(), channel()) + -> ok | {error, emqx_types:reason_code()}). +validate_packet(Packet, _Channel) -> + try emqx_packet:validate(Packet) of + true -> ok + catch + error:protocol_error -> + {error, ?RC_PROTOCOL_ERROR}; + error:subscription_identifier_invalid -> + {error, ?RC_SUBSCRIPTION_IDENTIFIERS_NOT_SUPPORTED}; + error:topic_alias_invalid -> + {error, ?RC_TOPIC_ALIAS_INVALID}; + error:topic_filters_invalid -> + {error, ?RC_TOPIC_FILTER_INVALID}; + error:topic_name_invalid -> + {error, ?RC_TOPIC_FILTER_INVALID}; + error:_Reason -> + {error, ?RC_MALFORMED_PACKET} + end. + +%%-------------------------------------------------------------------- +%% Check connect packet +%%-------------------------------------------------------------------- + +check_connect(ConnPkt, Channel) -> + 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). + +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_UNSUPPORTED_PROTOCOL_VERSION} + 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 -> + 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. + +init_protocol(ConnPkt, Channel) -> + {ok, Channel#channel{protocol = emqx_protocol:init(ConnPkt)}}. %%-------------------------------------------------------------------- -%% 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(ConnPkt, Channel) -> + 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}; +maybe_use_username_as_clientid(_ConnPkt, Channel = #channel{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, Channel#channel{client = NClient}}. + +maybe_assign_clientid(#mqtt_packet_connect{client_id = <<>>}, + 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}}}. + +%% Username maybe not undefined if peer_cert_as_username +set_username(#mqtt_packet_connect{username = Username}, + Channel = #channel{client = Client = #{username := undefined}}) -> + {ok, Channel#channel{client = Client#{username => Username}}}; +set_username(_ConnPkt, Channel) -> + {ok, Channel}. + +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). %%-------------------------------------------------------------------- -%% 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 emqx_access_control: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}, 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); + _ -> + 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 + }). + +%%-------------------------------------------------------------------- +%% Process publish message: Client -> Broker +%%-------------------------------------------------------------------- + +process_alias(Packet = #mqtt_packet{ + variable = #mqtt_packet_publish{topic_name = <<>>, + properties = #{'Topic-Alias' := AliasId} + } = Publish + }, + Channel = #channel{protocol = Protocol}) -> + case emqx_protocol:find_alias(AliasId, Protocol) of + {ok, Topic} -> + {ok, Packet#mqtt_packet{ + variable = Publish#mqtt_packet_publish{ + topic_name = Topic}}, Channel}; + false -> {error, ?RC_PROTOCOL_ERROR} + end; + +process_alias(#mqtt_packet{ + variable = #mqtt_packet_publish{topic_name = Topic, + properties = #{'Topic-Alias' := AliasId} + } + }, Channel = #channel{protocol = Protocol}) -> + {ok, Channel#channel{protocol = emqx_protocol:save_alias(AliasId, Topic, Protocol)}}; + +process_alias(_Packet, Channel) -> + {ok, Channel}. + +%% 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{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 + 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{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(), disconnected = false}. + +ensure_disconnected(Channel) -> + Channel#channel{connected = false, disconnected_at = os:timestamp(), disconnected = true}. + +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}). + +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? +%%-------------------------------------------------------------------- + +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 - ]}). +get_property(_Name, undefined, Default) -> + Default; +get_property(Name, Props, Default) -> + maps:get(Name, Props, Default). -reply(From, Reply, State) -> - {keep_state, State, [{reply, From, Reply}]}. +sp(true) -> 1; +sp(false) -> 0. -keep_state(State) -> - {keep_state, State}. +flag(true) -> 1; +flag(false) -> 0. -next_event(Type, Content) -> - {next_event, Type, Content}. - -shutdown(Reason, State) -> - stop({shutdown, Reason}, State). - -stop(Reason, State) -> - {stop, Reason, State}. +shutdown(Reason, Channel) -> + {stop, {shutdown, Reason}, Channel}. diff --git a/src/emqx_client.erl b/src/emqx_client.erl deleted file mode 100644 index 511b36b6c..000000000 --- a/src/emqx_client.erl +++ /dev/null @@ -1,1258 +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()}). - --record(mqtt_msg, { - qos = ?QOS_0, - retain = false, - dup = false, - packet_id, - topic, - props, - payload - }). - --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/src/emqx_cm.erl b/src/emqx_cm.erl index a4800e839..3673085ac 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 @@ -44,7 +42,7 @@ -export([ open_session/3 , discard_session/1 - , resume_session/1 + , takeover_session/1 ]). -export([ lookup_channels/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), @@ -169,45 +157,63 @@ 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(true, Client, Options), false} + Session = emqx_session:init(Client, Options), + {ok, #{session => Session, present => false}} end, emqx_cm_locker:trans(ClientId, CleanStart); 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 = emqx_session:resume(ClientId, Session), + Pendings = ConnMod:takeover(ChanPid, 'end'), + {ok, #{session => Session, + present => true, + pendings => Pendings}}; {error, not_found} -> - {ok, emqx_session:init(false, Client, Options), false} + Session = emqx_session:init(Client, Options), + {ok, #{session => Session, present => 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), - ?LOG(error, "[SM] More than one channel found: ~p", [ChanPids]), - lists:foreach(fun(_StalePid) -> - % catch emqx_channel:discard(StalePid) - ok + [ChanPid|StalePids] = lists:reverse(ChanPids), + ?LOG(error, "More than one channel found: ~p", [ChanPids]), + 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}} -> + 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 +222,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, "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 + #{client := #{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. @@ -285,10 +301,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}. @@ -314,8 +326,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). @@ -325,4 +336,3 @@ update_stats({Tab, Stat, MaxStat}) -> undefined -> ok; Size -> emqx_stats:setstat(Stat, MaxStat, Size) end. - 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/src/emqx_connection.erl b/src/emqx_connection.erl new file mode 100644 index 000000000..93daf4cba --- /dev/null +++ b/src/emqx_connection.erl @@ -0,0 +1,610 @@ +%%-------------------------------------------------------------------- +%% 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([get_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() + }). + +-type(state() :: #state{}). + +-define(ACTIVE_N, 100). +-define(HANDLE(T, C, D), handle((T), (C), (D))). +-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()) + -> {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) + }, + ChanInfo = emqx_channel:info(ChanState), + maps:merge(ConnInfo, ChanInfo). + +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, + chan_state = ChanState}) -> + ConnAttrs = #{socktype => Transport:type(Socket), + peername => Peername, + sockname => Sockname + }, + 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, + chan_state = ChanState}) -> + ProcStats = emqx_misc:proc_stats(), + SockStats = case Transport:getstat(Socket, ?SOCK_STATS) of + {ok, Ss} -> Ss; + {error, _} -> [] + end, + ConnStats = [{Name, emqx_pd:get_counter(Name)} || Name <- ?CONN_STATS], + ChanStats = emqx_channel:stats(ChanState), + lists:append([ProcStats, SockStats, ConnStats, ChanStats]). + +-spec(get_state(pid()) -> state()). +get_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). + +-spec(takeover(pid(), 'begin'|'end') -> 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}) -> + #{client_id := ClientId} = emqx_channel:info(client, ChanState), + ok = emqx_cm:register_channel(ClientId), + 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]), + 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); + +connected(info, Deliver = {deliver, _Topic, _Msg}, State) -> + handle_deliver(emqx_misc:drain_deliver([Deliver]), State); + +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(info, Deliver = {deliver, _Topic, _Msg}, State) -> + handle_deliver([Deliver], State); + +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}, 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, discard, State) -> + shutdown(discarded, State); + +%% 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:ensure_timer( + 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); + +handle(info, {Closed, _Sock}, State = #state{chan_state = ChanState}) + when Closed == tcp_closed; Closed == ssl_closed -> + 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; + 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 = #state{chan_state = ChanState}) -> + %% something sent + 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); + +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]), + 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, + chan_state = ChanState}) -> + ?LOG(debug, "Terminated for ~p", [Reason]), + ok = Transport:fast_close(Socket), + 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 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 + +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. + +%%-------------------------------------------------------------------- +%% 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 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_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_keepalive.erl b/src/emqx_keepalive.erl index 25170067d..6ce970b54 100644 --- a/src/emqx_keepalive.erl +++ b/src/emqx_keepalive.erl @@ -16,76 +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, - statval, - tsec, - tmsg, - tref, - repeat = 0 + interval :: pos_integer(), + statval :: non_neg_integer(), + repeat :: non_neg_integer() }). -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(fun(), integer(), any()) -> {ok, keepalive()} | {error, term()}). -start(_, 0, _) -> - {ok, #keepalive{}}; -start(StatFun, TimeoutSec, TimeoutMsg) -> - try StatFun() of - {ok, StatVal} -> - {ok, #keepalive{statfun = StatFun, statval = StatVal, - tsec = TimeoutSec, tmsg = TimeoutMsg, - tref = timer(TimeoutSec, TimeoutMsg)}}; - {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; -cancel(_) -> - ok. - -timer(Secs, Msg) -> - erlang:send_after(timer:seconds(Secs), self(), Msg). - 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_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_misc.erl b/src/emqx_misc.erl index 42e88d850..a325dc94b 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 @@ -25,11 +29,8 @@ , proc_stats/1 ]). --export([ init_proc_mng_policy/1 - , conn_proc_mng_policy/1 - ]). - --export([ drain_deliver/1 +-export([ drain_deliver/0 + , drain_deliver/1 , drain_down/1 ]). @@ -48,6 +49,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). @@ -56,7 +70,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 -> @@ -82,57 +96,10 @@ 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() -> + drain_deliver([]). + drain_deliver(Acc) -> receive Deliver = {deliver, _Topic, _Msg} -> diff --git a/src/emqx_mod_presence.erl b/src/emqx_mod_presence.erl index 97f7a9929..013a335cd 100644 --- a/src/emqx_mod_presence.erl +++ b/src/emqx_mod_presence.erl @@ -37,33 +37,30 @@ %% 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, 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_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/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_mqtt_props.erl b/src/emqx_mqtt_props.erl index 47a368714..163d9baf1 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]}, @@ -42,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]}, @@ -52,36 +60,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 +90,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 +142,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 +150,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/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_packet.erl b/src/emqx_packet.erl index e0351b8a9..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). %%-------------------------------------------------------------------- @@ -82,8 +84,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); @@ -166,10 +167,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_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..6d11ce5b6 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -17,892 +17,143 @@ %% MQTT Protocol -module(emqx_protocol). --include("emqx.hrl"). --include("emqx_mqtt.hrl"). --include("logger.hrl"). -include("types.hrl"). +-include("emqx_mqtt.hrl"). --logger_header("[Protocol]"). - --export([ info/1 +-export([ init/1 + , 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 +-export([ find_alias/2 + , save_alias/3 + , clear_will_msg/1 ]). --import(emqx_access_control, - [ authenticate/1 - , check_acl/3 - ]). - --export_type([proto_state/0]). +-export_type([protocol/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 + %% 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(proto_state() :: #protocol{}). +-opaque(protocol() :: #protocol{}). --define(NO_PROPS, undefined). +-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( + 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, + keepalive = Keepalive, + client_id = ClientId, + username = Username, + will_msg = WillMsg, + conn_props = Properties + }. --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, +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 }. --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, undefined) -> + ?MQTT_PROTO_V4; 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(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}) -> + ConnProps; 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 +attrs(#protocol{proto_name = ProtoName, + proto_ver = ProtoVer, + clean_start = CleanStart, + keepalive = Keepalive}) -> + #{proto_name => ProtoName, + proto_ver => ProtoVer, + clean_start => CleanStart, + 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) -> +find_alias(_AliasId, #protocol{topic_aliases = undefined}) -> false; -find_alias(AliasId, Aliases) -> +find_alias(AliasId, #protocol{topic_aliases = Aliases}) -> maps:find(AliasId, Aliases). -save_alias(AliasId, Topic, undefined) -> - #{AliasId => Topic}; -save_alias(AliasId, Topic, Aliases) -> - maps:put(AliasId, Topic, 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)}. -%% Check Publish -check_publish(Packet, PState) -> - pipeline([fun check_pub_acl/2, - fun check_pub_alias/2, - fun check_pub_caps/2], Packet, PState). +clear_will_msg(Protocol) -> + Protocol#protocol{will_msg = undefined}. -%% 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) -> +set_property(Name, Value, undefined) -> #{Name => Value}; set_property(Name, Value, Props) -> Props#{Name => Value}. @@ -911,14 +162,3 @@ 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_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_session.erl b/src/emqx_session.erl index 2530f3a42..cddaabe35 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,6 +58,8 @@ , stats/1 ]). +-export([update_expiry_interval/2]). + -export([ subscribe/4 , unsubscribe/3 ]). @@ -69,71 +71,53 @@ , pubcomp/2 ]). --export([deliver/2]). +-export([ deliver/2 + , enqueue/2 + , retry/1 + ]). --export([timeout/3]). +-export([ takeover/1 + , resume/2 + , redeliver/1 + ]). + +-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() }). @@ -149,11 +133,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), @@ -179,8 +162,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, @@ -192,8 +174,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), @@ -206,12 +187,10 @@ 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 }. -info(clean_start, #session{clean_start = CleanStart}) -> - CleanStart; info(subscriptions, #session{subscriptions = Subs}) -> Subs; info(max_subscriptions, #session{max_subscriptions = MaxSubs}) -> @@ -239,20 +218,23 @@ 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. +update_expiry_interval(ExpiryInterval, Session) -> + Session#session{expiry_interval = ExpiryInterval}. + %%-------------------------------------------------------------------- %% Attrs of the session %%-------------------------------------------------------------------- -spec(attrs(session()) -> emqx_types:attrs()). -attrs(#session{clean_start = CleanStart, - expiry_interval = ExpiryInterval, +attrs(undefined) -> + #{}; +attrs(#session{expiry_interval = ExpiryInterval, created_at = CreatedAt}) -> - #{clean_start => CleanStart, - expiry_interval => ExpiryInterval, + #{expiry_interval => ExpiryInterval, created_at => CreatedAt }. @@ -278,6 +260,37 @@ 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). +resume(ClientId, #session{subscriptions = Subs}) -> + ?LOG(info, "Session is resumed."), + %% 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)]), + %% TODO: 3. Redeliver: Replay delivery and Dequeue pending messages + %%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 %%-------------------------------------------------------------------- @@ -363,7 +376,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. @@ -502,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 @@ -519,9 +538,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 @@ -542,53 +560,23 @@ 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). -%%-------------------------------------------------------------------- -%% 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 -> @@ -599,10 +587,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 @@ -610,7 +599,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) -> @@ -627,34 +616,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, @@ -666,7 +641,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. %%-------------------------------------------------------------------- @@ -683,6 +658,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_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/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}; diff --git a/src/emqx_ws_channel.erl b/src/emqx_ws_connection.erl similarity index 50% rename from src/emqx_ws_channel.erl rename to src/emqx_ws_connection.erl index 3bc067525..e28f74b06 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("[WsConnection]"). -export([ info/1 , attrs/1 , stats/1 ]). +-export([ kick/1 + , discard/1 + , takeover/2 + ]). + %% WebSocket callbacks -export([ init/2 , websocket_init/1 @@ -38,26 +43,20 @@ ]). -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(), + pendings :: list(), + stop_reason :: term() }). -type(state() :: #state{}). -define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt]). --define(CHAN_STATS, [recv_pkt, recv_msg, send_pkt, send_msg]). +-define(CONN_STATS, [recv_pkt, recv_msg, send_pkt, send_msg]). %%-------------------------------------------------------------------- %% API @@ -66,52 +65,55 @@ -spec(info(pid() | state()) -> emqx_types:infos()). info(WSPid) when is_pid(WSPid) -> call(WSPid, info); -info(#state{peername = Peername, - sockname = Sockname, - proto_state = ProtoState, - gc_state = GCState, - stats_timer = StatsTimer, - idle_timeout = IdleTimeout, - connected = Connected, - connected_at = ConnectedAt}) -> - ChanInfo = #{socktype => websocket, - peername => Peername, - sockname => Sockname, - conn_state => running, - gc_state => emqx_gc:info(GCState), - enable_stats => enable_stats(StatsTimer), - idle_timeout => IdleTimeout, - connected => Connected, - connected_at => ConnectedAt +info(#state{peername = Peername, + sockname = Sockname, + chan_state = ChanState}) -> + ConnInfo = #{socktype => websocket, + peername => Peername, + sockname => Sockname, + conn_state => running }, - maps:merge(ChanInfo, emqx_protocol:info(ProtoState)). - -enable_stats(disabled) -> false; -enable_stats(_MaybeRef) -> true. + 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, - proto_state = ProtoState, - connected = Connected, - connected_at = ConnectedAt}) -> +attrs(#state{peername = Peername, + sockname = Sockname, + chan_state = ChanState}) -> ConnAttrs = #{socktype => websocket, peername => Peername, - sockname => Sockname, - connected => Connected, - connected_at => ConnectedAt + sockname => Sockname }, - maps:merge(ConnAttrs, emqx_protocol:attrs(ProtoState)). + 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{proto_state = ProtoState}) -> +stats(#state{chan_state = ChanState}) -> ProcStats = emqx_misc:proc_stats(), - SessStats = emqx_session:stats(emqx_protocol:info(session, ProtoState)), - lists:append([ProcStats, SessStats, chan_stats(), wsock_stats()]). + 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). + +-spec(discard(pid()) -> ok). +discard(WSPid) -> + WSPid ! {cast, discard}, ok. + +-spec(takeover(pid(), 'begin'|'end') -> Result :: term()). +takeover(CPid, Phase) -> + call(CPid, {takeover, Phase}). %% @private call(WSPid, Req) when is_pid(WSPid) -> @@ -171,47 +173,37 @@ 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 + {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); -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:ensure_timer( + 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 %% Pongs can be safely ignored. Clause here simply prevents crash. @@ -240,7 +232,28 @@ 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({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, discard}, State) -> + stop(discarded, 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,138 +277,72 @@ 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}) -> + State = #state{chan_state = ChanState}) -> 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}) + case emqx_channel:handle_out({deliver, Delivers}, 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}) -> - 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, 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) - end; +websocket_info({timeout, TRef, emit_stats}, State) when is_reference(TRef) -> + handle_timeout(TRef, {emit_stats, stats(State)}, State); -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); +websocket_info({timeout, TRef, Msg}, State) when is_reference(TRef) -> + handle_timeout(TRef, Msg, State); 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{chan_state = ChanState, + stop_reason = Reason}) -> ?LOG(debug, "Terminated for ~p, sockerror: ~p", [Reason, SockError]), - emqx_keepalive:cancel(Keepalive), - emqx_protocol:terminate(Reason, ProtoState). + 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}, + #{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_protocol:info(keepalive, ProtoState), - case ensure_keepalive(Interval, NState) of - ignore -> reply(NState); - {ok, KeepAlive} -> - reply(NState#state{keepalive = KeepAlive}); - {error, Reason} -> - stop(Reason, NState) - end. + ok = emqx_cm:set_chan_attrs(ClientId, attrs(NState)), + reply(NState). %%-------------------------------------------------------------------- -%% Ensure keepalive +%% Handle timeout -ensure_keepalive(0, _State) -> - ignore; -ensure_keepalive(Interval, #state{proto_state = ProtoState}) -> - Backoff = emqx_zone:get_env(emqx_protocol:info(zone, ProtoState), - 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}) +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. %%-------------------------------------------------------------------- @@ -424,22 +371,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. %%-------------------------------------------------------------------- @@ -479,45 +423,20 @@ 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(stats_timer, ChanState), + {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)}. -%%-------------------------------------------------------------------- -%% 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_SUITE.erl b/test/emqx_SUITE.erl new file mode 100644 index 000000000..c26028c76 --- /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} = emqtt:start_link([{host, "localhost"}, {client_id, "myclient"}]), + {ok, _} = emqtt: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}. 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/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_channel_SUITE.erl b/test/emqx_channel_SUITE.erl index 580b7a0fa..0c8e61091 100644 --- a/test/emqx_channel_SUITE.erl +++ b/test/emqx_channel_SUITE.erl @@ -19,6 +19,13 @@ -compile(export_all). -compile(nowarn_export_all). +-import(emqx_channel, + [ handle_in/2 + , handle_out/2 + ]). + +-include("emqx.hrl"). +-include("emqx_mqtt.hrl"). -include_lib("eunit/include/eunit.hrl"). all() -> emqx_ct:all(?MODULE). @@ -30,28 +37,259 @@ 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_id := ClientId, username := Username} + = emqx_channel:info(client, Channel1), + ?assertEqual(<<"clientid">>, ClientId), + ?assertEqual(<<"username">>, Username) + 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)), + #{awaiting_rel := AwaitingRel} = emqx_channel:info(session, Channel2), + ?assertEqual(2, AwaitingRel) + 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), + #{subscriptions := Subscriptions} + = emqx_channel:info(session, Channel1), + ?assertEqual(maps:from_list(TopicFilters), Subscriptions) + 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, {shutdown, normal}, Channel1} = handle_in(?DISCONNECT_PACKET(?RC_SUCCESS), Channel), + ?assertMatch(#{will_msg := undefined}, emqx_channel:info(protocol, 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, _Ch} = emqx_channel:handle_out({deliver, Delivers}, Channel1) + end). + +%%-------------------------------------------------------------------- +%% Test cases for handle_out +%%-------------------------------------------------------------------- + +t_handle_connack(_) -> + with_channel( + fun(Channel) -> + {ok, ?CONNACK_PACKET(?RC_SUCCESS, SP, _), _} + = handle_out({connack, ?RC_SUCCESS, 0}, Channel), + {stop, {shutdown, 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), + 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(protocol, Protocol, + emqx_channel:set(session, Session, Channel))). diff --git a/test/emqx_client_SUITE.erl b/test/emqx_client_SUITE.erl index 3ed8a5dff..a00d95603 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,56 +90,79 @@ 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} = emqtt:start_link([{client_id, ClientId}]), + {ok, _} = emqtt:connect(C), + #{client := #{client_id := ClientId}} = emqx_cm:get_chan_attrs(ClientId), + emqtt: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}, + {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)), @@ -151,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"). %%-------------------------------------------------------------------- @@ -229,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_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 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_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_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_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_keepalive_SUITE.erl b/test/emqx_keepalive_SUITE.erl index abfd8ee2b..0bdc79f60 100644 --- a/test/emqx_keepalive_SUITE.erl +++ b/test/emqx_keepalive_SUITE.erl @@ -19,19 +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_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_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 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_mod_subscription_SUITE.erl b/test/emqx_mod_subscription_SUITE.erl new file mode 100644 index 000000000..464bfcc76 --- /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} = emqtt:start_link([{host, "localhost"}, {client_id, "myclient"}, {username, "admin"}]), + {ok, _} = emqtt:connect(C), + % ct:sleep(100), + emqtt: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 = emqtt:disconnect(C), + emqx_mod_subscription:unload([]). diff --git a/test/emqx_net_SUITE.erl b/test/emqx_mod_sup_SUITE.erl similarity index 54% rename from test/emqx_net_SUITE.erl rename to test/emqx_mod_sup_SUITE.erl index 439ac6c70..fac49363e 100644 --- a/test/emqx_net_SUITE.erl +++ b/test/emqx_mod_sup_SUITE.erl @@ -14,32 +14,38 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_net_SUITE). +-module(emqx_mod_sup_SUITE). -%% CT -compile(export_all). -compile(nowarn_export_all). -all() -> [{group, keepalive}]. +-include_lib("eunit/include/eunit.hrl"). -groups() -> [{keepalive, [], [t_keepalive]}]. +all() -> emqx_ct:all(?MODULE). %%-------------------------------------------------------------------- -%% Keepalive +%% Test cases %%-------------------------------------------------------------------- -t_keepalive(_) -> - {ok, KA} = emqx_keepalive:start(fun() -> {ok, 1} end, 1, {keepalive, timeout}), - [resumed, timeout] = lists:reverse(keepalive_recv(KA, [])). +t_start(_) -> + {ok, _} = emqx_mod_sup:start_link(), + ?assertEqual([], supervisor:which_children(emqx_mod_sup)). -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_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. 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(_) -> 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())). diff --git a/test/emqx_oom_SUITE.erl b/test/emqx_oom_SUITE.erl new file mode 100644 index 000000000..90eb4a253 --- /dev/null +++ b/test/emqx_oom_SUITE.erl @@ -0,0 +1,46 @@ +%%-------------------------------------------------------------------- +%% 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(_) -> + ?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(_) -> + ?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)). + 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_plugins_SUITE.erl b/test/emqx_plugins_SUITE.erl new file mode 100644 index 000000000..5cab5b362 --- /dev/null +++ b/test/emqx_plugins_SUITE.erl @@ -0,0 +1,59 @@ +%%-------------------------------------------------------------------- +%% 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("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}}. + diff --git a/test/emqx_protocol_SUITE.erl b/test/emqx_protocol_SUITE.erl index 4edcbe3f7..89f1d7344 100644 --- a/test/emqx_protocol_SUITE.erl +++ b/test/emqx_protocol_SUITE.erl @@ -19,30 +19,12 @@ -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(_) -> +t_init_and_info(_) -> ConnPkt = #mqtt_packet_connect{ proto_name = <<"MQTT">>, proto_ver = ?MQTT_PROTO_V4, @@ -54,234 +36,14 @@ t_handle_connect(_) -> 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). + 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)). -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_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..567570506 --- /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(), 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 emqtt:start_link(Options) of + {ok, Pid} -> + {ok, _} = emqtt:connect(Pid), + try subscribe(Pid, RequestTopic, QoS) of + ok -> {ok, Pid}; + {error, _} = Error -> Error + catch + C : E : S -> + emqtt:stop(Pid), + {error, {C, E, S}} + end; + {error, _} = Error -> Error + end. + +stop(Pid) -> + emqtt: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 emqtt itself. + %% hence delegate to another temp process for the loopback gen_statem call. + Client = self(), + _ = spawn_link(fun() -> + case emqtt: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} = + emqtt: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..2316a2f43 --- /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 emqtt:start_link(Options) of + {ok, Pid} -> + {ok, _} = emqtt:connect(Pid), + try subscribe(Pid, ResponseTopic, QoS) of + ok -> {ok, Pid}; + {error, _} = Error -> Error + catch + C : E : S -> + emqtt: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 emqtt:publish(Client, Msg) of + ok -> ok; %% QoS = 0 + {ok, _} -> ok; + {error, _} = E -> E + end. + +stop(Pid) -> + emqtt:disconnect(Pid). + +subscribe(Client, Topic, QoS) -> + case emqtt: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..9f537ff19 100644 --- a/test/emqx_session_SUITE.erl +++ b/test/emqx_session_SUITE.erl @@ -19,87 +19,314 @@ -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(15, maps:size(Info)), + Session; +apply_op(Session, attrs) -> + Attrs = emqx_session:attrs(Session), + ?assert(is_map(Attrs)), + ?assertEqual(2, 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. -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()} + ], + list(?LAZY(oneof(Union))). -t_puback(_) -> - 'TODO'. +deliver_args() -> + list({deliver, topic(), message()}). -t_pubrec(_) -> - 'TODO'. +info_args() -> + oneof([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_pubrel(_) -> - 'TODO'. +sub_args() -> + ?LET({ClientId, TopicFilter, SubOpts}, + {clientid(), topic(), sub_opts()}, + {#{client_id => ClientId}, TopicFilter, SubOpts}). -t_pubcomp(_) -> - 'TODO'. +unsub_args() -> + ?LET({ClientId, TopicFilter}, + {clientid(), topic()}, + {#{client_id => ClientId}, TopicFilter}). -t_deliver(_) -> - 'TODO'. +publish_args() -> + ?LET({PacketId, Message}, + {packetid(), message()}, + {PacketId, Message}). -t_timeout(_) -> - 'TODO'. +puback_args() -> + packetid(). -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). +pubrec_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). +pubrel_args() -> + packetid(). +pubcomp_args() -> + packetid(). + +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)). + +session() -> + ?LET({Zone, Options}, + {zone(), option()}, + begin + Session = emqx_session:init(#{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_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_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/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_channel_SUITE.erl b/test/emqx_ws_connection_SUITE.erl similarity index 96% rename from test/emqx_ws_channel_SUITE.erl rename to test/emqx_ws_connection_SUITE.erl index f634e633e..dfb348253 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). @@ -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, []). 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.