Merge pull request #2839 from emqx/master

Auto-pull-request-by-2019-08-24
This commit is contained in:
turtleDeng 2019-08-23 20:06:23 +08:00 committed by GitHub
commit e4a5121e86
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
72 changed files with 4582 additions and 4521 deletions

View File

@ -11,7 +11,6 @@ script:
- make xref - make xref
- make eunit - make eunit
- make ct - make ct
- make proper
- make cover - make cover
after_success: after_success:

View File

@ -15,11 +15,7 @@ RUN_NODE_NAME = emqxdebug@127.0.0.1
all: compile all: compile
.PHONY: tests .PHONY: tests
tests: eunit ct proper tests: eunit ct
.PHONY: proper
proper:
@rebar3 proper
.PHONY: run .PHONY: run
run: run_setup unlock run: run_setup unlock
@ -99,7 +95,7 @@ ct: ct_setup
## e.g. make ct-one-suite suite=emqx_bridge ## e.g. make ct-one-suite suite=emqx_bridge
.PHONY: $(SUITES:%=ct-%) .PHONY: $(SUITES:%=ct-%)
$(CT_SUITES:%=ct-%): ct_setup $(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 .PHONY: app.config
app.config: $(CUTTLEFISH_SCRIPT) etc/gen.emqx.conf app.config: $(CUTTLEFISH_SCRIPT) etc/gen.emqx.conf

View File

@ -4,7 +4,7 @@
[![Build Status](https://travis-ci.org/emqx/emqx.svg)](https://travis-ci.org/emqx/emqx) [![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) [![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) [![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) [![Slack Invite](<https://slack-invite.emqx.io/badge.svg>)](https://slack-invite.emqx.io)
[![Twitter](https://img.shields.io/badge/Twiiter-EMQ%20X-1DA1F2?logo=twitter)](https://twitter.com/emqtt) [![Twitter](https://img.shields.io/badge/Twiiter-EMQ%20X-1DA1F2?logo=twitter)](https://twitter.com/emqtt)
[English](./README.md) | 简体中文 [English](./README.md) | 简体中文
@ -22,8 +22,8 @@
获取适合你的二进制软件包,[点此下载](https://emqx.io/downloads)。 获取适合你的二进制软件包,[点此下载](https://emqx.io/downloads)。
- [单节点安装](https://developer.emqx.io/docs/emq/v3/en/install.html) - [单节点安装](https://docs.emqx.io/broker/v3/cn/install.html)
- [集群安装](https://developer.emqx.io/docs/emq/v3/en/cluster.html) - [集群安装](https://docs.emqx.io/broker/v3/cn/cluster.html)
## 从源码构建 ## 从源码构建
@ -54,7 +54,7 @@ cd _rel/emqx && ./bin/emqx console
## FAQ ## FAQ
访问 [FAQ](https://developer.emqx.io/docs/tutorial/zh/faq/faq.html) 以获取常见问题的帮助。 访问 [FAQ](https://docs.emqx.io/tutorial/v3/cn/faq/faq.html) 以获取常见问题的帮助。
## 产品路线 ## 产品路线

View File

@ -4,7 +4,7 @@
[![Build Status](https://travis-ci.org/emqx/emqx.svg)](https://travis-ci.org/emqx/emqx) [![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) [![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) [![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) [![Slack Invite](<https://slack-invite.emqx.io/badge.svg>)](https://slack-invite.emqx.io)
[![Twitter](https://img.shields.io/badge/Twiiter-EMQ%20X-1DA1F2?logo=twitter)](https://twitter.com/emqtt) [![Twitter](https://img.shields.io/badge/Twiiter-EMQ%20X-1DA1F2?logo=twitter)](https://twitter.com/emqtt)
English | [简体中文](./README-CN.md) 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). 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) - [Single Node Install](https://docs.emqx.io/broker/v3/en/install.html)
- [Multi Node Install](https://developer.emqx.io/docs/emq/v3/en/cluster.html) - [Multi Node Install](https://docs.emqx.io/broker/v3/en/cluster.html)
## Build From Source ## Build From Source
@ -56,7 +56,7 @@ To view the dashboard after running, use your browser to open: http://localhost:
## FAQ ## 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 ## Roadmap

View File

@ -17,6 +17,8 @@
-ifndef(EMQ_X_MQTT_HRL). -ifndef(EMQ_X_MQTT_HRL).
-define(EMQ_X_MQTT_HRL, true). -define(EMQ_X_MQTT_HRL, true).
-define(UINT_MAX, 16#FFFFFFFF).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% MQTT SockOpts %% MQTT SockOpts
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
@ -298,6 +300,20 @@
payload :: binary() | undefined payload :: binary() | undefined
}). }).
%%--------------------------------------------------------------------
%% MQTT Message Internal
%%--------------------------------------------------------------------
-record(mqtt_msg, {
qos = ?QOS_0,
retain = false,
dup = false,
packet_id,
topic,
props,
payload
}).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% MQTT Packet Match %% MQTT Packet Match
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------

View File

@ -3,7 +3,7 @@
{cowboy, "2.6.1"}, % hex {cowboy, "2.6.1"}, % hex
{gproc, "0.8.0"}, % hex {gproc, "0.8.0"}, % hex
{esockd, "5.5.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"}}}, {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.4.1"}}},
{cuttlefish, {git, "https://github.com/emqx/cuttlefish", {tag, "v3.0.0"}}} {cuttlefish, {git, "https://github.com/emqx/cuttlefish", {tag, "v3.0.0"}}}
]}. ]}.
@ -21,8 +21,7 @@
{cover_opts, [verbose]}. {cover_opts, [verbose]}.
{cover_export_enabled, true}. {cover_export_enabled, true}.
{plugins, [coveralls, {plugins, [coveralls]}.
rebar3_proper]}.
{erl_first_files, ["src/emqx_logger.erl"]}. {erl_first_files, ["src/emqx_logger.erl"]}.

View File

@ -206,7 +206,7 @@ publish(Msg) when is_record(Msg, message) ->
end. end.
%% Called internally %% 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) -> safe_publish(Msg) when is_record(Msg, message) ->
try try
publish(Msg) publish(Msg)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -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).

View File

@ -28,8 +28,6 @@
-export([start_link/0]). -export([start_link/0]).
-export([ register_channel/1 -export([ register_channel/1
, unregister_channel/1
, unregister_channel/2
]). ]).
-export([ get_chan_attrs/1 -export([ get_chan_attrs/1
@ -44,7 +42,7 @@
-export([ open_session/3 -export([ open_session/3
, discard_session/1 , discard_session/1
, resume_session/1 , takeover_session/1
]). ]).
-export([ lookup_channels/1 -export([ lookup_channels/1
@ -94,6 +92,7 @@ start_link() ->
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% @doc Register a channel. %% @doc Register a channel.
%% Channel will be unregistered automatically when the channel process dies
-spec(register_channel(emqx_types:client_id()) -> ok). -spec(register_channel(emqx_types:client_id()) -> ok).
register_channel(ClientId) when is_binary(ClientId) -> register_channel(ClientId) when is_binary(ClientId) ->
register_channel(ClientId, self()). register_channel(ClientId, self()).
@ -106,17 +105,6 @@ register_channel(ClientId, ChanPid) ->
ok = emqx_cm_registry:register_channel(Chan), ok = emqx_cm_registry:register_channel(Chan),
cast({registered, 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 %% @private
do_unregister_channel(Chan) -> do_unregister_channel(Chan) ->
ok = emqx_cm_registry:unregister_channel(Chan), ok = emqx_cm_registry:unregister_channel(Chan),
@ -169,45 +157,63 @@ set_chan_stats(ClientId, ChanPid, Stats) ->
%% @doc Open a session. %% @doc Open a session.
-spec(open_session(boolean(), emqx_types:client(), map()) -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) -> open_session(true, Client = #{client_id := ClientId}, Options) ->
CleanStart = fun(_) -> CleanStart = fun(_) ->
ok = discard_session(ClientId), ok = discard_session(ClientId),
{ok, emqx_session:init(true, Client, Options), false} Session = emqx_session:init(Client, Options),
{ok, #{session => Session, present => false}}
end, end,
emqx_cm_locker:trans(ClientId, CleanStart); emqx_cm_locker:trans(ClientId, CleanStart);
open_session(false, Client = #{client_id := ClientId}, Options) -> open_session(false, Client = #{client_id := ClientId}, Options) ->
ResumeStart = fun(_) -> ResumeStart = fun(_) ->
case resume_session(ClientId) of case takeover_session(ClientId) of
{ok, Session} -> {ok, ConnMod, ChanPid, Session} ->
{ok, Session, true}; ok = emqx_session:resume(ClientId, Session),
Pendings = ConnMod:takeover(ChanPid, 'end'),
{ok, #{session => Session,
present => true,
pendings => Pendings}};
{error, not_found} -> {error, not_found} ->
{ok, emqx_session:init(false, Client, Options), false} Session = emqx_session:init(Client, Options),
{ok, #{session => Session, present => false}}
end end
end, end,
emqx_cm_locker:trans(ClientId, ResumeStart). emqx_cm_locker:trans(ClientId, ResumeStart).
%% @doc Try to resume a session. %% @doc Try to takeover a session.
-spec(resume_session(emqx_types:client_id()) -spec(takeover_session(emqx_types:client_id())
-> {ok, emqx_session:session()} | {error, Reason :: term()}). -> {ok, emqx_session:session()} | {error, Reason :: term()}).
resume_session(ClientId) -> takeover_session(ClientId) ->
case lookup_channels(ClientId) of case lookup_channels(ClientId) of
[] -> {error, not_found}; [] -> {error, not_found};
[_ChanPid] -> [ChanPid] ->
ok; takeover_session(ClientId, ChanPid);
% emqx_channel:resume(ChanPid);
ChanPids -> ChanPids ->
[_ChanPid|StalePids] = lists:reverse(ChanPids), [ChanPid|StalePids] = lists:reverse(ChanPids),
?LOG(error, "[SM] More than one channel found: ~p", [ChanPids]), ?LOG(error, "More than one channel found: ~p", [ChanPids]),
lists:foreach(fun(_StalePid) -> lists:foreach(fun(StalePid) ->
% catch emqx_channel:discard(StalePid) catch discard_session(ClientId, StalePid)
ok
end, StalePids), end, StalePids),
% emqx_channel:resume(ChanPid) takeover_session(ClientId, ChanPid)
ok
end. 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. %% @doc Discard all the sessions identified by the ClientId.
-spec(discard_session(emqx_types:client_id()) -> ok). -spec(discard_session(emqx_types:client_id()) -> ok).
discard_session(ClientId) when is_binary(ClientId) -> discard_session(ClientId) when is_binary(ClientId) ->
@ -216,15 +222,25 @@ discard_session(ClientId) when is_binary(ClientId) ->
ChanPids -> ChanPids ->
lists:foreach( lists:foreach(
fun(ChanPid) -> fun(ChanPid) ->
try ok try
% emqx_channel:discard(ChanPid) discard_session(ClientId, ChanPid)
catch catch
_:Error:_Stk -> _:Error:_Stk ->
?LOG(warning, "[SM] Failed to discard ~p: ~p", [ChanPid, Error]) ?LOG(error, "Failed to discard ~p: ~p", [ChanPid, Error])
end end
end, ChanPids) end, ChanPids)
end. 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? %% @doc Is clean start?
% is_clean_start(#{clean_start := false}) -> false; % is_clean_start(#{clean_start := false}) -> false;
% is_clean_start(_Attrs) -> true. % 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), PMon1 = emqx_pmon:monitor(ChanPid, ClientId, PMon),
{noreply, State#{chan_pmon := PMon1}}; {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) -> handle_cast(Msg, State) ->
?LOG(error, "Unexpected cast: ~p", [Msg]), ?LOG(error, "Unexpected cast: ~p", [Msg]),
{noreply, State}. {noreply, State}.
@ -314,8 +326,7 @@ code_change(_OldVsn, State, _Extra) ->
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
clean_down({ChanPid, ClientId}) -> clean_down({ChanPid, ClientId}) ->
Chan = {ClientId, ChanPid}, do_unregister_channel({ClientId, ChanPid}).
do_unregister_channel(Chan).
stats_fun() -> stats_fun() ->
lists:foreach(fun update_stats/1, ?CHAN_STATS). lists:foreach(fun update_stats/1, ?CHAN_STATS).
@ -325,4 +336,3 @@ update_stats({Tab, Stat, MaxStat}) ->
undefined -> ok; undefined -> ok;
Size -> emqx_stats:setstat(Stat, MaxStat, Size) Size -> emqx_stats:setstat(Stat, MaxStat, Size)
end. end.

View File

@ -14,33 +14,12 @@
%% limitations under the License. %% 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). -module(emqx_config).
-export([populate/1]). -export([ get_env/1
-export([ read/1
, write/2
, dump/2
, reload/1
]).
-export([ set/3
, get/2
, get/3
, get_env/1
, get_env/2 , get_env/2
]). ]).
-type(env() :: {atom(), term()}).
-define(APP, emqx). -define(APP, emqx).
%% @doc Get environment %% @doc Get environment
@ -51,91 +30,3 @@ get_env(Key) ->
-spec(get_env(Key :: atom(), Default :: term()) -> term()). -spec(get_env(Key :: atom(), Default :: term()) -> term()).
get_env(Key, Default) -> get_env(Key, Default) ->
application:get_env(?APP, 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.

610
src/emqx_connection.erl Normal file
View File

@ -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}.

View File

@ -151,24 +151,3 @@ noreply(State) ->
next_seq(State = #state{seq = Seq}) -> next_seq(State = #state{seq = Seq}) ->
State#state{seq = Seq + 1}. 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.

View File

@ -16,76 +16,58 @@
-module(emqx_keepalive). -module(emqx_keepalive).
%% APIs -export([ init/1
-export([ start/3 , info/1
, check/1 , info/2
, cancel/1 , check/2
]). ]).
-export_type([keepalive/0]). -export_type([keepalive/0]).
-record(keepalive, { -record(keepalive, {
statfun, interval :: pos_integer(),
statval, statval :: non_neg_integer(),
tsec, repeat :: non_neg_integer()
tmsg,
tref,
repeat = 0
}). }).
-opaque(keepalive() :: #keepalive{}). -opaque(keepalive() :: #keepalive{}).
%%-------------------------------------------------------------------- %% @doc Init keepalive.
%% APIs -spec(init(Interval :: non_neg_integer()) -> keepalive()).
%%-------------------------------------------------------------------- init(Interval) when Interval > 0 ->
#keepalive{interval = Interval,
statval = 0,
repeat = 0}.
%% @doc Start a keepalive %% @doc Get Info of the keepalive.
-spec(start(fun(), integer(), any()) -> {ok, keepalive()} | {error, term()}). -spec(info(keepalive()) -> emqx_types:infos()).
start(_, 0, _) -> info(#keepalive{interval = Interval,
{ok, #keepalive{}}; statval = StatVal,
start(StatFun, TimeoutSec, TimeoutMsg) -> repeat = Repeat}) ->
try StatFun() of #{interval => Interval,
{ok, StatVal} -> statval => StatVal,
{ok, #keepalive{statfun = StatFun, statval = StatVal, repeat => Repeat
tsec = TimeoutSec, tmsg = TimeoutMsg, }.
tref = timer(TimeoutSec, TimeoutMsg)}};
{error, Error} ->
{error, Error}
catch
_Error:Reason ->
{error, Reason}
end.
%% @doc Check keepalive, called when timeout... -spec(info(interval|statval|repeat, keepalive())
-spec(check(keepalive()) -> {ok, keepalive()} | {error, term()}). -> non_neg_integer()).
check(KeepAlive = #keepalive{statfun = StatFun, statval = LastVal, repeat = Repeat}) -> info(interval, #keepalive{interval = Interval}) ->
try StatFun() of Interval;
{ok, NewVal} -> info(statval, #keepalive{statval = StatVal}) ->
if NewVal =/= LastVal -> StatVal;
{ok, resume(KeepAlive#keepalive{statval = NewVal, repeat = 0})}; 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 -> Repeat < 1 ->
{ok, resume(KeepAlive#keepalive{statval = NewVal, repeat = Repeat + 1})}; {ok, KeepAlive#keepalive{repeat = Repeat + 1}};
true -> true -> {error, timeout}
{error, timeout}
end;
{error, Error} ->
{error, Error}
catch
_Error:Reason ->
{error, Reason}
end. 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).

View File

@ -79,7 +79,7 @@ start_listener(Proto, ListenOn, Options) when Proto == https; Proto == wss ->
start_mqtt_listener(Name, ListenOn, Options) -> start_mqtt_listener(Name, ListenOn, Options) ->
SockOpts = esockd:parse_opt(Options), SockOpts = esockd:parse_opt(Options),
esockd:open(Name, ListenOn, merge_default(SockOpts), 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_http_listener(Start, Name, ListenOn, RanchOpts, ProtoOpts) ->
Start(Name, with_port(ListenOn, RanchOpts), ProtoOpts). Start(Name, with_port(ListenOn, RanchOpts), ProtoOpts).
@ -88,7 +88,7 @@ mqtt_path(Options) ->
proplists:get_value(mqtt_path, Options, "/mqtt"). proplists:get_value(mqtt_path, Options, "/mqtt").
ws_opts(Options) -> ws_opts(Options) ->
WsPaths = [{mqtt_path(Options), emqx_ws_channel, Options}], WsPaths = [{mqtt_path(Options), emqx_ws_connection, Options}],
Dispatch = cowboy_router:compile([{'_', WsPaths}]), Dispatch = cowboy_router:compile([{'_', WsPaths}]),
ProxyProto = proplists:get_value(proxy_protocol, Options, false), ProxyProto = proplists:get_value(proxy_protocol, Options, false),
#{env => #{dispatch => Dispatch}, proxy_header => ProxyProto}. #{env => #{dispatch => Dispatch}, proxy_header => ProxyProto}.

View File

@ -122,6 +122,8 @@ critical(Metadata, Format, Args) when is_map(Metadata) ->
logger:critical(Format, Args, Metadata). logger:critical(Format, Args, Metadata).
-spec(set_metadata_client_id(emqx_types:client_id()) -> ok). -spec(set_metadata_client_id(emqx_types:client_id()) -> ok).
set_metadata_client_id(<<>>) ->
ok;
set_metadata_client_id(ClientId) -> set_metadata_client_id(ClientId) ->
set_proc_metadata(#{client_id => ClientId}). set_proc_metadata(#{client_id => ClientId}).

View File

@ -16,7 +16,11 @@
-module(emqx_misc). -module(emqx_misc).
-include("types.hrl").
-export([ merge_opts/2 -export([ merge_opts/2
, maybe_apply/2
, run_fold/3
, start_timer/2 , start_timer/2
, start_timer/3 , start_timer/3
, cancel_timer/1 , cancel_timer/1
@ -25,11 +29,8 @@
, proc_stats/1 , proc_stats/1
]). ]).
-export([ init_proc_mng_policy/1 -export([ drain_deliver/0
, conn_proc_mng_policy/1 , drain_deliver/1
]).
-export([ drain_deliver/1
, drain_down/1 , drain_down/1
]). ]).
@ -48,6 +49,19 @@ merge_opts(Defaults, Options) ->
lists:usort([Opt | Acc]) lists:usort([Opt | Acc])
end, Defaults, Options). 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()). -spec(start_timer(integer(), term()) -> reference()).
start_timer(Interval, Msg) -> start_timer(Interval, Msg) ->
start_timer(Interval, self(), Msg). start_timer(Interval, self(), Msg).
@ -56,7 +70,7 @@ start_timer(Interval, Msg) ->
start_timer(Interval, Dest, Msg) -> start_timer(Interval, Dest, Msg) ->
erlang: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) -> cancel_timer(Timer) when is_reference(Timer) ->
case erlang:cancel_timer(Timer) of case erlang:cancel_timer(Timer) of
false -> false ->
@ -82,57 +96,10 @@ proc_stats(Pid) ->
[{mailbox_len, Len}|Stats] [{mailbox_len, Len}|Stats]
end. 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. %% @doc Drain delivers from the channel's mailbox.
drain_deliver() ->
drain_deliver([]).
drain_deliver(Acc) -> drain_deliver(Acc) ->
receive receive
Deliver = {deliver, _Topic, _Msg} -> Deliver = {deliver, _Topic, _Msg} ->

View File

@ -37,33 +37,30 @@
%% APIs %% APIs
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
load(Env) -> load(_Env) ->
emqx_hooks:add('client.connected', {?MODULE, on_client_connected, [Env]}), ok.
emqx_hooks:add('client.disconnected', {?MODULE, on_client_disconnected, [Env]}). %% 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, on_client_connected(#{client_id := ClientId,
username := Username, username := Username,
peername := {IpAddr, _} peername := {IpAddr, _}
}, ConnAck, }, ConnAck,
#{session := #{clean_start := CleanStart, #{session := Session,
expiry_interval := Interval
},
proto_name := ProtoName, proto_name := ProtoName,
proto_ver := ProtoVer, proto_ver := ProtoVer,
keepalive := Keepalive keepalive := Keepalive
}, Env) -> }, Env) ->
case emqx_json:safe_encode(#{clientid => ClientId, case emqx_json:safe_encode(maps:merge(#{clientid => ClientId,
username => Username, username => Username,
ipaddress => iolist_to_binary(esockd_net:ntoa(IpAddr)), ipaddress => iolist_to_binary(esockd_net:ntoa(IpAddr)),
proto_name => ProtoName, proto_name => ProtoName,
proto_ver => ProtoVer, proto_ver => ProtoVer,
keepalive => Keepalive, keepalive => Keepalive,
clean_start => CleanStart,
expiry_interval => Interval,
connack => ConnAck, connack => ConnAck,
ts => erlang:system_time(millisecond) ts => erlang:system_time(millisecond)
}) of }, maps:with([clean_start, expiry_interval], Session))) of
{ok, Payload} -> {ok, Payload} ->
emqx:publish(message(qos(Env), topic(connected, ClientId), Payload)); emqx:publish(message(qos(Env), topic(connected, ClientId), Payload));
{error, Reason} -> {error, Reason} ->

View File

@ -40,7 +40,7 @@
start_link() -> start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []). 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). supervisor:start_child(?MODULE, ChildSpec).
start_child(Mod, Type) when is_atom(Mod) andalso is_atom(Type) -> start_child(Mod, Type) when is_atom(Mod) andalso is_atom(Type) ->

View File

@ -79,10 +79,6 @@ do_check_pub(#{qos := QoS}, #{max_qos_allowed := MaxQoS})
{error, ?RC_QOS_NOT_SUPPORTED}; {error, ?RC_QOS_NOT_SUPPORTED};
do_check_pub(#{retain := true}, #{retain_available := false}) -> do_check_pub(#{retain := true}, #{retain_available := false}) ->
{error, ?RC_RETAIN_NOT_SUPPORTED}; {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. do_check_pub(_Flags, _Caps) -> ok.
-spec(check_sub(emqx_types:zone(), -spec(check_sub(emqx_types:zone(),

View File

@ -25,6 +25,12 @@
, validate/1 , validate/1
]). ]).
%% For tests
-export([all/0]).
-type(prop_name() :: atom()).
-type(prop_id() :: pos_integer()).
-define(PROPS_TABLE, -define(PROPS_TABLE,
#{16#01 => {'Payload-Format-Indicator', 'Byte', [?PUBLISH]}, #{16#01 => {'Payload-Format-Indicator', 'Byte', [?PUBLISH]},
16#02 => {'Message-Expiry-Interval', 'Four-Byte-Integer', [?PUBLISH]}, 16#02 => {'Message-Expiry-Interval', 'Four-Byte-Integer', [?PUBLISH]},
@ -42,7 +48,9 @@
16#19 => {'Request-Response-Information', 'Byte', [?CONNECT]}, 16#19 => {'Request-Response-Information', 'Byte', [?CONNECT]},
16#1A => {'Response-Information', 'UTF8-Encoded-String', [?CONNACK]}, 16#1A => {'Response-Information', 'UTF8-Encoded-String', [?CONNACK]},
16#1C => {'Server-Reference', 'UTF8-Encoded-String', [?CONNACK, ?DISCONNECT]}, 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#21 => {'Receive-Maximum', 'Two-Byte-Integer', [?CONNECT, ?CONNACK]},
16#22 => {'Topic-Alias-Maximum', 'Two-Byte-Integer', [?CONNECT, ?CONNACK]}, 16#22 => {'Topic-Alias-Maximum', 'Two-Byte-Integer', [?CONNECT, ?CONNACK]},
16#23 => {'Topic-Alias', 'Two-Byte-Integer', [?PUBLISH]}, 16#23 => {'Topic-Alias', 'Two-Byte-Integer', [?PUBLISH]},
@ -52,36 +60,10 @@
16#27 => {'Maximum-Packet-Size', 'Four-Byte-Integer', [?CONNECT, ?CONNACK]}, 16#27 => {'Maximum-Packet-Size', 'Four-Byte-Integer', [?CONNECT, ?CONNACK]},
16#28 => {'Wildcard-Subscription-Available', 'Byte', [?CONNACK]}, 16#28 => {'Wildcard-Subscription-Available', 'Byte', [?CONNACK]},
16#29 => {'Subscription-Identifier-Available', 'Byte', [?CONNACK]}, 16#29 => {'Subscription-Identifier-Available', 'Byte', [?CONNACK]},
16#2A => {'Shared-Subscription-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'.
-spec(id(prop_name()) -> prop_id()).
id('Payload-Format-Indicator') -> 16#01; id('Payload-Format-Indicator') -> 16#01;
id('Message-Expiry-Interval') -> 16#02; id('Message-Expiry-Interval') -> 16#02;
id('Content-Type') -> 16#03; id('Content-Type') -> 16#03;
@ -108,12 +90,47 @@ id('User-Property') -> 16#26;
id('Maximum-Packet-Size') -> 16#27; id('Maximum-Packet-Size') -> 16#27;
id('Wildcard-Subscription-Available') -> 16#28; id('Wildcard-Subscription-Available') -> 16#28;
id('Subscription-Identifier-Available') -> 16#29; 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) -> filter(PacketType, Props) when is_map(Props) ->
maps:from_list(filter(PacketType, maps:to_list(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) -> Filter = fun(Name) ->
case maps:find(id(Name), ?PROPS_TABLE) of case maps:find(id(Name), ?PROPS_TABLE) of
{ok, {Name, _Type, 'ALL'}} -> {ok, {Name, _Type, 'ALL'}} ->
@ -125,6 +142,7 @@ filter(PacketType, Props) when ?CONNECT =< PacketType, PacketType =< ?AUTH, is_l
end, end,
[Prop || Prop = {Name, _} <- Props, Filter(Name)]. [Prop || Prop = {Name, _} <- Props, Filter(Name)].
-spec(validate(emqx_types:properties()) -> ok).
validate(Props) when is_map(Props) -> validate(Props) when is_map(Props) ->
lists:foreach(fun validate_prop/1, maps:to_list(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 case maps:find(id(Name), ?PROPS_TABLE) of
{ok, {Name, Type, _}} -> {ok, {Name, Type, _}} ->
validate_value(Type, Val) validate_value(Type, Val)
orelse error(bad_property, Prop); orelse error({bad_property_value, Prop});
error -> error ->
error({bad_property, Prop}) error({bad_property, Name})
end. end.
validate_value('Byte', Val) -> validate_value('Byte', Val) ->
is_integer(Val); is_integer(Val) andalso Val =< 16#FF;
validate_value('Two-Byte-Integer', Val) -> 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) -> 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) -> 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) -> validate_value('UTF8-Encoded-String', Val) ->
is_binary(Val); is_binary(Val);
validate_value('Binary-Data', Val) -> validate_value('Binary-Data', Val) ->
is_binary(Val); is_binary(Val);
validate_value('UTF8-String-Pair', Val) -> validate_value(_Type, _Val) -> false.
is_tuple(Val) orelse is_list(Val).
-spec(all() -> map()).
all() -> ?PROPS_TABLE.

102
src/emqx_oom.erl Normal file
View File

@ -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.

View File

@ -19,7 +19,7 @@
-include("emqx.hrl"). -include("emqx.hrl").
-include("emqx_mqtt.hrl"). -include("emqx_mqtt.hrl").
-export([ protocol_name/1 -export([ proto_name/1
, type_name/1 , type_name/1
, validate/1 , validate/1
, format/1 , format/1
@ -28,18 +28,20 @@
, will_msg/1 , will_msg/1
]). ]).
%% @doc Protocol name of version -compile(inline).
-spec(protocol_name(emqx_types:version()) -> binary()).
protocol_name(?MQTT_PROTO_V3) -> %% @doc Protocol name of the version.
-spec(proto_name(emqx_types:version()) -> binary()).
proto_name(?MQTT_PROTO_V3) ->
<<"MQIsdp">>; <<"MQIsdp">>;
protocol_name(?MQTT_PROTO_V4) -> proto_name(?MQTT_PROTO_V4) ->
<<"MQTT">>; <<"MQTT">>;
protocol_name(?MQTT_PROTO_V5) -> proto_name(?MQTT_PROTO_V5) ->
<<"MQTT">>. <<"MQTT">>.
%% @doc Name of MQTT packet type %% @doc Name of MQTT packet type.
-spec(type_name(emqx_types:packet_type()) -> atom()). -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). lists:nth(Type, ?TYPE_NAMES).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
@ -82,8 +84,7 @@ validate_packet_id(_) ->
validate_properties(?SUBSCRIBE, #{'Subscription-Identifier' := I}) validate_properties(?SUBSCRIBE, #{'Subscription-Identifier' := I})
when I =< 0; I >= 16#FFFFFFF -> when I =< 0; I >= 16#FFFFFFF ->
error(subscription_identifier_invalid); error(subscription_identifier_invalid);
validate_properties(?PUBLISH, #{'Topic-Alias':= I}) validate_properties(?PUBLISH, #{'Topic-Alias':= 0}) ->
when I =:= 0 ->
error(topic_alias_invalid); error(topic_alias_invalid);
validate_properties(?PUBLISH, #{'Subscription-Identifier' := _I}) -> validate_properties(?PUBLISH, #{'Subscription-Identifier' := _I}) ->
error(protocol_error); error(protocol_error);
@ -166,10 +167,11 @@ will_msg(#mqtt_packet_connect{client_id = ClientId,
will_qos = QoS, will_qos = QoS,
will_topic = Topic, will_topic = Topic,
will_props = Properties, will_props = Properties,
will_payload = Payload}) -> will_payload = Payload,
proto_ver = ProtoVer}) ->
Msg = emqx_message:make(ClientId, QoS, Topic, Payload), Msg = emqx_message:make(ClientId, QoS, Topic, Payload),
Msg#message{flags = #{dup => false, retain => Retain}, 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) -> merge_props(Headers, undefined) ->
Headers; Headers;

View File

@ -81,9 +81,9 @@ load_expand_plugins() ->
load_expand_plugin(PluginDir) -> load_expand_plugin(PluginDir) ->
init_expand_plugin_config(PluginDir), init_expand_plugin_config(PluginDir),
Ebin = PluginDir ++ "/ebin", Ebin = filename:join([PluginDir, "ebin"]),
code:add_patha(Ebin), code:add_patha(Ebin),
Modules = filelib:wildcard(Ebin ++ "/*.beam"), Modules = filelib:wildcard(filename:join([Ebin ++ "*.beam"])),
lists:foreach(fun(Mod) -> lists:foreach(fun(Mod) ->
Module = list_to_atom(filename:basename(Mod, ".beam")), Module = list_to_atom(filename:basename(Mod, ".beam")),
code:load_file(Module) code:load_file(Module)
@ -308,14 +308,11 @@ read_loaded() ->
read_loaded(File) -> file:consult(File). read_loaded(File) -> file:consult(File).
write_loaded(AppNames) -> write_loaded(AppNames) ->
File = emqx_config:get_env(plugins_loaded_file), FilePath = emqx_config:get_env(plugins_loaded_file),
case file:open(File, [binary, write]) of case file:write_file(FilePath, [io_lib:format("~p.~n", [Name]) || Name <- AppNames]) of
{ok, Fd} -> ok -> ok;
lists:foreach(fun(Name) ->
file:write(Fd, iolist_to_binary(io_lib:format("~p.~n", [Name])))
end, AppNames);
{error, Error} -> {error, Error} ->
?LOG(error, "Open File ~p Error: ~p", [File, Error]), ?LOG(error, "Write File ~p Error: ~p", [FilePath, Error]),
{error, Error} {error, Error}
end. end.
@ -324,4 +321,3 @@ plugin_type(protocol) -> protocol;
plugin_type(backend) -> backend; plugin_type(backend) -> backend;
plugin_type(bridge) -> bridge; plugin_type(bridge) -> bridge;
plugin_type(_) -> feature. plugin_type(_) -> feature.

View File

@ -17,892 +17,143 @@
%% MQTT Protocol %% MQTT Protocol
-module(emqx_protocol). -module(emqx_protocol).
-include("emqx.hrl").
-include("emqx_mqtt.hrl").
-include("logger.hrl").
-include("types.hrl"). -include("types.hrl").
-include("emqx_mqtt.hrl").
-logger_header("[Protocol]"). -export([ init/1
, info/1
-export([ info/1
, info/2 , info/2
, attrs/1 , attrs/1
, caps/1
]). ]).
%% for tests -export([ find_alias/2
-export([set/3]). , save_alias/3
, clear_will_msg/1
-export([ init/2
, handle_in/2
, handle_req/2
, handle_deliver/2
, handle_out/2
, handle_timeout/3
, terminate/2
]). ]).
-import(emqx_access_control, -export_type([protocol/0]).
[ authenticate/1
, check_acl/3
]).
-export_type([proto_state/0]).
-record(protocol, { -record(protocol, {
client :: emqx_types:client(), %% MQTT Proto Name
session :: emqx_session:session(),
proto_name :: binary(), proto_name :: binary(),
%% MQTT Proto Version
proto_ver :: emqx_types:ver(), proto_ver :: emqx_types:ver(),
%% Clean Start Flag
clean_start :: boolean(),
%% MQTT Keepalive interval
keepalive :: non_neg_integer(), 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(), will_msg :: emqx_types:message(),
topic_aliases :: maybe(map()), %% MQTT Conn Properties
alias_maximum :: maybe(map()), conn_props :: maybe(emqx_types:properties()),
ack_props :: maybe(emqx_types:properties()) %% Tmp props %% 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,
-spec(info(proto_state()) -> emqx_types:infos()).
info(#protocol{client = Client,
session = Session,
proto_name = ProtoName,
proto_ver = ProtoVer, proto_ver = ProtoVer,
will_props = WillProps,
clean_start = CleanStart,
keepalive = Keepalive, 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, will_msg = WillMsg,
topic_aliases = Aliases}) -> conn_props = Properties
#{client => Client, }.
session => session_info(Session),
proto_name => ProtoName, 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, proto_ver => ProtoVer,
clean_start => CleanStart,
keepalive => Keepalive, keepalive => Keepalive,
client_id => ClientId,
username => Username,
will_msg => WillMsg, will_msg => WillMsg,
conn_props => ConnProps,
topic_aliases => Aliases 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}) -> info(proto_name, #protocol{proto_name = ProtoName}) ->
ProtoName; ProtoName;
info(proto_ver, undefined) ->
?MQTT_PROTO_V4;
info(proto_ver, #protocol{proto_ver = ProtoVer}) -> info(proto_ver, #protocol{proto_ver = ProtoVer}) ->
ProtoVer; ProtoVer;
info(clean_start, #protocol{clean_start = CleanStart}) ->
CleanStart;
info(keepalive, #protocol{keepalive = Keepalive}) -> info(keepalive, #protocol{keepalive = Keepalive}) ->
Keepalive; Keepalive;
info(client_id, #protocol{client_id = ClientId}) ->
ClientId;
info(username, #protocol{username = Username}) ->
Username;
info(will_msg, #protocol{will_msg = WillMsg}) -> info(will_msg, #protocol{will_msg = WillMsg}) ->
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}) -> info(topic_aliases, #protocol{topic_aliases = Aliases}) ->
Aliases. Aliases.
%% For tests attrs(#protocol{proto_name = ProtoName,
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, proto_ver = ProtoVer,
clean_start = CleanStart,
keepalive = Keepalive}) -> keepalive = Keepalive}) ->
#{client => Client, #{proto_name => ProtoName,
session => emqx_session:attrs(Session),
proto_name => ProtoName,
proto_ver => ProtoVer, proto_ver => ProtoVer,
clean_start => CleanStart,
keepalive => Keepalive keepalive => Keepalive
}. }.
caps(#protocol{client = #{zone := Zone}}) -> find_alias(_AliasId, #protocol{topic_aliases = undefined}) ->
emqx_mqtt_caps:get_caps(Zone).
-spec(init(emqx_types:conn(), proplists:proplist()) -> proto_state()).
init(ConnInfo, Options) ->
Zone = proplists:get_value(zone, Options),
Peercert = maps:get(peercert, ConnInfo, undefined),
Username = case peer_cert_as_username(Options) of
cn -> esockd_peercert:common_name(Peercert);
dn -> esockd_peercert:subject(Peercert);
crt -> Peercert;
_ -> undefined
end,
MountPoint = emqx_zone:get_env(Zone, mountpoint),
Client = maps:merge(#{zone => Zone,
username => Username,
mountpoint => MountPoint,
is_bridge => false,
is_superuser => false
}, ConnInfo),
#protocol{client = Client,
proto_name = <<"MQTT">>,
proto_ver = ?MQTT_PROTO_V4
}.
peer_cert_as_username(Options) ->
proplists:get_value(peer_cert_as_username, Options).
%%--------------------------------------------------------------------
%% Handle incoming packet
%%--------------------------------------------------------------------
-spec(handle_in(emqx_types:packet(), proto_state())
-> {ok, proto_state()}
| {ok, emqx_types:packet(), proto_state()}
| {ok, list(emqx_types:packet()), proto_state()}
| {error, Reason :: term(), proto_state()}
| {stop, Error :: atom(), proto_state()}).
handle_in(?CONNECT_PACKET(
#mqtt_packet_connect{proto_name = ProtoName,
proto_ver = ProtoVer,
keepalive = Keepalive,
client_id = ClientId
} = ConnPkt), PState) ->
PState1 = PState#protocol{proto_name = ProtoName,
proto_ver = ProtoVer,
keepalive = Keepalive
},
ok = emqx_logger:set_metadata_client_id(ClientId),
case pipeline([fun validate_in/2,
fun process_props/2,
fun check_connect/2,
fun enrich_client/2,
fun auth_connect/2], ConnPkt, PState1) of
{ok, NConnPkt, NPState} ->
process_connect(NConnPkt, maybe_assign_clientid(NPState));
{error, ReasonCode, NPState} ->
handle_out({disconnect, ReasonCode}, NPState)
end;
handle_in(Packet = ?PUBLISH_PACKET(QoS, Topic, PacketId), PState) ->
case pipeline([fun validate_in/2,
fun process_alias/2,
fun check_publish/2], Packet, PState) of
{ok, NPacket, NPState} ->
process_publish(NPacket, NPState);
{error, ReasonCode, NPState} ->
?LOG(warning, "Cannot publish message to ~s due to ~s",
[Topic, emqx_reason_codes:text(ReasonCode)]),
puback(QoS, PacketId, ReasonCode, NPState)
end;
handle_in(?PUBACK_PACKET(PacketId, _ReasonCode), PState = #protocol{session = Session}) ->
case emqx_session:puback(PacketId, Session) of
{ok, Publishes, NSession} ->
handle_out({publish, Publishes}, PState#protocol{session = NSession});
{ok, NSession} ->
{ok, PState#protocol{session = NSession}};
{error, _NotFound} ->
{ok, PState}
end;
handle_in(?PUBREC_PACKET(PacketId, _ReasonCode), PState = #protocol{session = Session}) ->
case emqx_session:pubrec(PacketId, Session) of
{ok, NSession} ->
handle_out({pubrel, PacketId}, PState#protocol{session = NSession});
{error, ReasonCode1} ->
handle_out({pubrel, PacketId, ReasonCode1}, PState)
end;
handle_in(?PUBREL_PACKET(PacketId, _ReasonCode), PState = #protocol{session = Session}) ->
case emqx_session:pubrel(PacketId, Session) of
{ok, NSession} ->
handle_out({pubcomp, PacketId}, PState#protocol{session = NSession});
{error, ReasonCode1} ->
handle_out({pubcomp, PacketId, ReasonCode1}, PState)
end;
handle_in(?PUBCOMP_PACKET(PacketId, _ReasonCode), PState = #protocol{session = Session}) ->
case emqx_session:pubcomp(PacketId, Session) of
{ok, Publishes, NSession} ->
handle_out({publish, Publishes}, PState#protocol{session = NSession});
{ok, NSession} ->
{ok, PState#protocol{session = NSession}};
{error, _NotFound} ->
{ok, PState}
end;
handle_in(Packet = ?SUBSCRIBE_PACKET(PacketId, Properties, TopicFilters),
PState = #protocol{client = Client}) ->
case validate_in(Packet, PState) of
ok -> TopicFilters1 = [emqx_topic:parse(TopicFilter, SubOpts)
|| {TopicFilter, SubOpts} <- TopicFilters],
TopicFilters2 = emqx_hooks:run_fold('client.subscribe',
[Client, Properties],
TopicFilters1),
TopicFilters3 = enrich_subid(Properties, TopicFilters2),
{ReasonCodes, NPState} = process_subscribe(TopicFilters3, PState),
handle_out({suback, PacketId, ReasonCodes}, NPState);
{error, ReasonCode} ->
handle_out({disconnect, ReasonCode}, PState)
end;
handle_in(Packet = ?UNSUBSCRIBE_PACKET(PacketId, Properties, TopicFilters),
PState = #protocol{client = Client}) ->
case validate_in(Packet, PState) of
ok -> TopicFilters1 = lists:map(fun emqx_topic:parse/1, TopicFilters),
TopicFilters2 = emqx_hooks:run_fold('client.unsubscribe',
[Client, Properties],
TopicFilters1),
{ReasonCodes, NPState} = process_unsubscribe(TopicFilters2, PState),
handle_out({unsuback, PacketId, ReasonCodes}, NPState);
{error, ReasonCode} ->
handle_out({disconnect, ReasonCode}, PState)
end;
handle_in(?PACKET(?PINGREQ), PState) ->
{ok, ?PACKET(?PINGRESP), PState};
handle_in(?DISCONNECT_PACKET(?RC_SUCCESS), PState) ->
%% Clear will msg
{stop, normal, PState#protocol{will_msg = undefined}};
handle_in(?DISCONNECT_PACKET(RC), PState = #protocol{proto_ver = Ver}) ->
{stop, {shutdown, emqx_reason_codes:name(RC, Ver)}, PState};
handle_in(?AUTH_PACKET(), PState) ->
%%TODO: implement later.
{ok, PState};
handle_in(Packet, PState) ->
io:format("In: ~p~n", [Packet]),
{ok, PState}.
%%--------------------------------------------------------------------
%% Handle internal request
%%--------------------------------------------------------------------
-spec(handle_req(Req:: term(), proto_state())
-> {ok, Result :: term(), proto_state()} |
{error, Reason :: term(), proto_state()}).
handle_req({subscribe, TopicFilters}, PState = #protocol{client = Client}) ->
TopicFilters1 = emqx_hooks:run_fold('client.subscribe',
[Client, #{'Internal' => true}],
parse(subscribe, TopicFilters)),
{ReasonCodes, NPState} = process_subscribe(TopicFilters1, PState),
{ok, ReasonCodes, NPState};
handle_req({unsubscribe, TopicFilters}, PState = #protocol{client = Client}) ->
TopicFilters1 = emqx_hooks:run_fold('client.unsubscribe',
[Client, #{'Internal' => true}],
parse(unsubscribe, TopicFilters)),
{ReasonCodes, NPState} = process_unsubscribe(TopicFilters1, PState),
{ok, ReasonCodes, NPState};
handle_req(Req, PState) ->
?LOG(error, "Unexpected request: ~p~n", [Req]),
{ok, ignored, PState}.
%%--------------------------------------------------------------------
%% Handle delivers
%%--------------------------------------------------------------------
handle_deliver(Delivers, PState = #protocol{session = Session})
when is_list(Delivers) ->
case emqx_session:deliver(Delivers, Session) of
{ok, Publishes, NSession} ->
handle_out({publish, Publishes}, PState#protocol{session = NSession});
{ok, NSession} ->
{ok, PState#protocol{session = NSession}}
end.
%%--------------------------------------------------------------------
%% Handle outgoing packet
%%--------------------------------------------------------------------
handle_out({connack, ?RC_SUCCESS, SP},
PState = #protocol{client = Client = #{zone := Zone},
ack_props = AckProps,
alias_maximum = AliasMaximum}) ->
ok = emqx_hooks:run('client.connected', [Client, ?RC_SUCCESS, attrs(PState)]),
#{max_packet_size := MaxPktSize,
max_qos_allowed := MaxQoS,
retain_available := Retain,
max_topic_alias := MaxAlias,
shared_subscription := Shared,
wildcard_subscription := Wildcard
} = caps(PState),
%% Response-Information is so far not set by broker.
%% i.e. It's a Client-to-Client contract for the request-response topic naming scheme.
%% According to MQTT 5.0 spec:
%% A common use of this is to pass a globally unique portion of the topic tree which
%% is reserved for this Client for at least the lifetime of its Session.
%% This often cannot just be a random name as both the requesting Client and the
%% responding Client need to be authorized to use it.
%% If we are to support it in the feature, the implementation should be flexible
%% to allow prefixing the response topic based on different ACL config.
%% e.g. prefix by username or client-id, so that unauthorized clients can not
%% subscribe requests or responses that are not intended for them.
AckProps1 = if AckProps == undefined -> #{}; true -> AckProps end,
AckProps2 = AckProps1#{'Retain-Available' => flag(Retain),
'Maximum-Packet-Size' => MaxPktSize,
'Topic-Alias-Maximum' => MaxAlias,
'Wildcard-Subscription-Available' => flag(Wildcard),
'Subscription-Identifier-Available' => 1,
%'Response-Information' =>
'Shared-Subscription-Available' => flag(Shared),
'Maximum-QoS' => MaxQoS
},
AckProps3 = case emqx_zone:get_env(Zone, server_keepalive) of
undefined -> AckProps2;
Keepalive -> AckProps2#{'Server-Keep-Alive' => Keepalive}
end,
AliasMaximum1 = set_property(inbound, MaxAlias, AliasMaximum),
PState1 = PState#protocol{alias_maximum = AliasMaximum1,
ack_props = undefined
},
{ok, ?CONNACK_PACKET(?RC_SUCCESS, SP, AckProps3), PState1};
handle_out({connack, ReasonCode}, PState = #protocol{client = Client,
proto_ver = ProtoVer}) ->
ok = emqx_hooks:run('client.connected', [Client, ReasonCode, attrs(PState)]),
ReasonCode1 = if
ProtoVer == ?MQTT_PROTO_V5 -> ReasonCode;
true -> emqx_reason_codes:compat(connack, ReasonCode)
end,
Reason = emqx_reason_codes:name(ReasonCode1, ProtoVer),
{error, Reason, ?CONNACK_PACKET(ReasonCode1), PState};
handle_out({publish, Publishes}, PState) ->
Packets = [element(2, handle_out(Publish, PState)) || Publish <- Publishes],
{ok, Packets, PState};
handle_out({publish, PacketId, Msg}, PState = #protocol{client = Client}) ->
Msg1 = emqx_hooks:run_fold('message.deliver', [Client],
emqx_message:update_expiry(Msg)),
Packet = emqx_packet:from_message(PacketId, unmount(Client, Msg1)),
{ok, Packet, PState};
%% TODO: How to handle the err?
handle_out({puberr, _ReasonCode}, PState) ->
{ok, PState};
handle_out({puback, PacketId, ReasonCode}, PState) ->
{ok, ?PUBACK_PACKET(PacketId, ReasonCode), PState};
handle_out({pubrel, PacketId}, PState) ->
{ok, ?PUBREL_PACKET(PacketId), PState};
handle_out({pubrel, PacketId, ReasonCode}, PState) ->
{ok, ?PUBREL_PACKET(PacketId, ReasonCode), PState};
handle_out({pubrec, PacketId, ReasonCode}, PState) ->
{ok, ?PUBREC_PACKET(PacketId, ReasonCode), PState};
handle_out({pubcomp, PacketId}, PState) ->
{ok, ?PUBCOMP_PACKET(PacketId), PState};
handle_out({pubcomp, PacketId, ReasonCode}, PState) ->
{ok, ?PUBCOMP_PACKET(PacketId, ReasonCode), PState};
handle_out({suback, PacketId, ReasonCodes}, PState = #protocol{proto_ver = ?MQTT_PROTO_V5}) ->
%% TODO: ACL Deny
{ok, ?SUBACK_PACKET(PacketId, ReasonCodes), PState};
handle_out({suback, PacketId, ReasonCodes}, PState) ->
%% TODO: ACL Deny
ReasonCodes1 = [emqx_reason_codes:compat(suback, RC) || RC <- ReasonCodes],
{ok, ?SUBACK_PACKET(PacketId, ReasonCodes1), PState};
handle_out({unsuback, PacketId, ReasonCodes}, PState = #protocol{proto_ver = ?MQTT_PROTO_V5}) ->
{ok, ?UNSUBACK_PACKET(PacketId, ReasonCodes), PState};
%% Ignore reason codes if not MQTT5
handle_out({unsuback, PacketId, _ReasonCodes}, PState) ->
{ok, ?UNSUBACK_PACKET(PacketId), PState};
handle_out({disconnect, ReasonCode}, PState = #protocol{proto_ver = ?MQTT_PROTO_V5}) ->
Reason = emqx_reason_codes:name(ReasonCode),
{error, Reason, ?DISCONNECT_PACKET(ReasonCode), PState};
handle_out({disconnect, ReasonCode}, PState = #protocol{proto_ver = ProtoVer}) ->
{error, emqx_reason_codes:name(ReasonCode, ProtoVer), PState};
handle_out(Packet, PState) ->
?LOG(error, "Unexpected out:~p", [Packet]),
{ok, PState}.
%%--------------------------------------------------------------------
%% Handle timeout
%%--------------------------------------------------------------------
handle_timeout(TRef, Msg, PState = #protocol{session = Session}) ->
case emqx_session:timeout(TRef, Msg, Session) of
{ok, NSession} ->
{ok, PState#protocol{session = NSession}};
{ok, Publishes, NSession} ->
handle_out({publish, Publishes}, PState#protocol{session = NSession})
end.
terminate(normal, #protocol{client = Client}) ->
ok = emqx_hooks:run('client.disconnected', [Client, normal]);
terminate(Reason, #protocol{client = Client, will_msg = WillMsg}) ->
ok = emqx_hooks:run('client.disconnected', [Client, Reason]),
publish_will_msg(WillMsg).
publish_will_msg(undefined) ->
ok;
publish_will_msg(Msg) ->
emqx_broker:publish(Msg).
%%--------------------------------------------------------------------
%% Validate incoming packet
%%--------------------------------------------------------------------
-spec(validate_in(emqx_types:packet(), proto_state())
-> ok | {error, emqx_types:reason_code()}).
validate_in(Packet, _PState) ->
try emqx_packet:validate(Packet) of
true -> ok
catch
error:protocol_error ->
{error, ?RC_PROTOCOL_ERROR};
error:subscription_identifier_invalid ->
{error, ?RC_SUBSCRIPTION_IDENTIFIERS_NOT_SUPPORTED};
error:topic_alias_invalid ->
{error, ?RC_TOPIC_ALIAS_INVALID};
error:topic_filters_invalid ->
{error, ?RC_TOPIC_FILTER_INVALID};
error:topic_name_invalid ->
{error, ?RC_TOPIC_FILTER_INVALID};
error:_Reason ->
{error, ?RC_MALFORMED_PACKET}
end.
%%--------------------------------------------------------------------
%% Preprocess properties
%%--------------------------------------------------------------------
process_props(#mqtt_packet_connect{
properties = #{'Topic-Alias-Maximum' := Max}
},
PState = #protocol{alias_maximum = AliasMaximum}) ->
NAliasMaximum = if AliasMaximum == undefined ->
#{outbound => Max};
true -> AliasMaximum#{outbound => Max}
end,
{ok, PState#protocol{alias_maximum = NAliasMaximum}};
process_props(Packet, PState) ->
{ok, Packet, PState}.
%%--------------------------------------------------------------------
%% Check Connect Packet
%%--------------------------------------------------------------------
check_connect(ConnPkt, PState) ->
case pipeline([fun check_proto_ver/2,
fun check_client_id/2,
%%fun check_flapping/2,
fun check_banned/2,
fun check_will_topic/2,
fun check_will_retain/2], ConnPkt, PState) of
ok -> {ok, PState};
Error -> Error
end.
check_proto_ver(#mqtt_packet_connect{proto_ver = Ver,
proto_name = Name}, _PState) ->
case lists:member({Ver, Name}, ?PROTOCOL_NAMES) of
true -> ok;
false -> {error, ?RC_PROTOCOL_ERROR}
end.
%% MQTT3.1 does not allow null clientId
check_client_id(#mqtt_packet_connect{proto_ver = ?MQTT_PROTO_V3,
client_id = <<>>
}, _PState) ->
{error, ?RC_CLIENT_IDENTIFIER_NOT_VALID};
%% Issue#599: Null clientId and clean_start = false
check_client_id(#mqtt_packet_connect{client_id = <<>>,
clean_start = false}, _PState) ->
{error, ?RC_CLIENT_IDENTIFIER_NOT_VALID};
check_client_id(#mqtt_packet_connect{client_id = <<>>,
clean_start = true}, _PState) ->
ok;
check_client_id(#mqtt_packet_connect{client_id = ClientId},
#protocol{client = #{zone := Zone}}) ->
Len = byte_size(ClientId),
MaxLen = emqx_zone:get_env(Zone, max_clientid_len),
case (1 =< Len) andalso (Len =< MaxLen) of
true -> ok;
false -> {error, ?RC_CLIENT_IDENTIFIER_NOT_VALID}
end.
%%TODO: check banned...
check_banned(#mqtt_packet_connect{client_id = ClientId,
username = Username},
#protocol{client = Client = #{zone := Zone}}) ->
case emqx_zone:get_env(Zone, enable_ban, false) of
true ->
case emqx_banned:check(Client#{client_id => ClientId,
username => Username}) of
true -> {error, ?RC_BANNED};
false -> ok
end;
false -> ok
end.
check_will_topic(#mqtt_packet_connect{will_flag = false}, _PState) ->
ok;
check_will_topic(#mqtt_packet_connect{will_topic = WillTopic}, _PState) ->
try emqx_topic:validate(WillTopic) of
true -> ok
catch error:_Error ->
{error, ?RC_TOPIC_NAME_INVALID}
end.
check_will_retain(#mqtt_packet_connect{will_retain = false}, _PState) ->
ok;
check_will_retain(#mqtt_packet_connect{will_retain = true},
#protocol{client = #{zone := Zone}}) ->
case emqx_zone:get_env(Zone, mqtt_retain_available, true) of
true -> ok;
false -> {error, ?RC_RETAIN_NOT_SUPPORTED}
end.
%%--------------------------------------------------------------------
%% Enrich client
%%--------------------------------------------------------------------
enrich_client(#mqtt_packet_connect{client_id = ClientId,
username = Username,
is_bridge = IsBridge
},
PState = #protocol{client = Client}) ->
Client1 = set_username(Username, Client#{client_id => ClientId,
is_bridge => IsBridge
}),
{ok, PState#protocol{client = maybe_username_as_clientid(Client1)}}.
%% Username maybe not undefined if peer_cert_as_username
set_username(Username, Client = #{username := undefined}) ->
Client#{username => Username};
set_username(_Username, Client) -> Client.
maybe_username_as_clientid(Client = #{username := undefined}) ->
Client;
maybe_username_as_clientid(Client = #{zone := Zone,
username := Username}) ->
case emqx_zone:get_env(Zone, use_username_as_clientid, false) of
true -> Client#{client_id => Username};
false -> Client
end.
%%--------------------------------------------------------------------
%% Auth Connect
%%--------------------------------------------------------------------
auth_connect(#mqtt_packet_connect{client_id = ClientId,
username = Username,
password = Password},
PState = #protocol{client = Client}) ->
case authenticate(Client#{password => Password}) of
{ok, AuthResult} ->
{ok, PState#protocol{client = maps:merge(Client, AuthResult)}};
{error, Reason} ->
?LOG(warning, "Client ~s (Username: '~s') login failed for ~0p",
[ClientId, Username, Reason]),
{error, emqx_reason_codes:connack_error(Reason)}
end.
%%--------------------------------------------------------------------
%% Assign a random clientId
%%--------------------------------------------------------------------
maybe_assign_clientid(PState = #protocol{client = Client = #{client_id := <<>>},
ack_props = AckProps}) ->
ClientId = emqx_guid:to_base62(emqx_guid:gen()),
Client1 = Client#{client_id => ClientId},
AckProps1 = set_property('Assigned-Client-Identifier', ClientId, AckProps),
PState#protocol{client = Client1, ack_props = AckProps1};
maybe_assign_clientid(PState) -> PState.
%%--------------------------------------------------------------------
%% Process Connect
%%--------------------------------------------------------------------
process_connect(ConnPkt, PState) ->
case open_session(ConnPkt, PState) of
{ok, Session, SP} ->
WillMsg = emqx_packet:will_msg(ConnPkt),
NPState = PState#protocol{session = Session,
will_msg = WillMsg
},
handle_out({connack, ?RC_SUCCESS, sp(SP)}, NPState);
{error, Reason} ->
%% TODO: Unknown error?
?LOG(error, "Failed to open session: ~p", [Reason]),
handle_out({connack, ?RC_UNSPECIFIED_ERROR}, PState)
end.
%%--------------------------------------------------------------------
%% Open session
%%--------------------------------------------------------------------
open_session(#mqtt_packet_connect{clean_start = CleanStart,
properties = ConnProps},
#protocol{client = Client = #{zone := Zone}}) ->
MaxInflight = get_property('Receive-Maximum', ConnProps,
emqx_zone:get_env(Zone, max_inflight, 65535)),
Interval = get_property('Session-Expiry-Interval', ConnProps,
emqx_zone:get_env(Zone, session_expiry_interval, 0)),
emqx_cm:open_session(CleanStart, Client, #{max_inflight => MaxInflight,
expiry_interval => Interval
}).
%%--------------------------------------------------------------------
%% Process publish message: Client -> Broker
%%--------------------------------------------------------------------
process_alias(Packet = #mqtt_packet{
variable = #mqtt_packet_publish{topic_name = <<>>,
properties = #{'Topic-Alias' := AliasId}
} = Publish
}, PState = #protocol{topic_aliases = Aliases}) ->
case find_alias(AliasId, Aliases) of
{ok, Topic} ->
{ok, Packet#mqtt_packet{
variable = Publish#mqtt_packet_publish{
topic_name = Topic}}, PState};
false -> {error, ?RC_TOPIC_ALIAS_INVALID}
end;
process_alias(#mqtt_packet{
variable = #mqtt_packet_publish{topic_name = Topic,
properties = #{'Topic-Alias' := AliasId}
}
}, PState = #protocol{topic_aliases = Aliases}) ->
{ok, PState#protocol{topic_aliases = save_alias(AliasId, Topic, Aliases)}};
process_alias(_Packet, PState) ->
{ok, PState}.
find_alias(_AliasId, undefined) ->
false; false;
find_alias(AliasId, Aliases) -> find_alias(AliasId, #protocol{topic_aliases = Aliases}) ->
maps:find(AliasId, Aliases). maps:find(AliasId, Aliases).
save_alias(AliasId, Topic, undefined) -> save_alias(AliasId, Topic, Protocol = #protocol{topic_aliases = undefined}) ->
#{AliasId => Topic}; Protocol#protocol{topic_aliases = #{AliasId => Topic}};
save_alias(AliasId, Topic, Aliases) -> save_alias(AliasId, Topic, Protocol = #protocol{topic_aliases = Aliases}) ->
maps:put(AliasId, Topic, Aliases). Protocol#protocol{topic_aliases = maps:put(AliasId, Topic, Aliases)}.
%% Check Publish clear_will_msg(Protocol) ->
check_publish(Packet, PState) -> Protocol#protocol{will_msg = undefined}.
pipeline([fun check_pub_acl/2,
fun check_pub_alias/2,
fun check_pub_caps/2], Packet, PState).
%% Check Pub ACL set_property(Name, Value, undefined) ->
check_pub_acl(#mqtt_packet{variable = #mqtt_packet_publish{topic_name = Topic}},
#protocol{client = Client}) ->
case is_acl_enabled(Client) andalso check_acl(Client, publish, Topic) of
false -> ok;
allow -> ok;
deny -> {error, ?RC_NOT_AUTHORIZED}
end.
%% Check Pub Alias
check_pub_alias(#mqtt_packet{
variable = #mqtt_packet_publish{
properties = #{'Topic-Alias' := AliasId}
}
},
#protocol{alias_maximum = Limits}) ->
case (Limits == undefined)
orelse (Max = maps:get(inbound, Limits, 0)) == 0
orelse (AliasId > Max) of
false -> ok;
true -> {error, ?RC_TOPIC_ALIAS_INVALID}
end;
check_pub_alias(_Packet, _PState) -> ok.
%% Check Pub Caps
check_pub_caps(#mqtt_packet{header = #mqtt_packet_header{qos = QoS,
retain = Retain
}
},
#protocol{client = #{zone := Zone}}) ->
emqx_mqtt_caps:check_pub(Zone, #{qos => QoS, retain => Retain}).
%% Process Publish
process_publish(Packet = ?PUBLISH_PACKET(_QoS, _Topic, PacketId),
PState = #protocol{client = Client}) ->
Msg = emqx_packet:to_message(Client, Packet),
%%TODO: Improve later.
Msg1 = emqx_message:set_flag(dup, false, Msg),
process_publish(PacketId, mount(Client, Msg1), PState).
process_publish(_PacketId, Msg = #message{qos = ?QOS_0}, PState) ->
_ = emqx_broker:publish(Msg),
{ok, PState};
process_publish(PacketId, Msg = #message{qos = ?QOS_1}, PState) ->
Deliveries = emqx_broker:publish(Msg),
ReasonCode = emqx_reason_codes:puback(Deliveries),
handle_out({puback, PacketId, ReasonCode}, PState);
process_publish(PacketId, Msg = #message{qos = ?QOS_2},
PState = #protocol{session = Session}) ->
case emqx_session:publish(PacketId, Msg, Session) of
{ok, Deliveries, NSession} ->
ReasonCode = emqx_reason_codes:puback(Deliveries),
handle_out({pubrec, PacketId, ReasonCode},
PState#protocol{session = NSession});
{error, ReasonCode} ->
handle_out({pubrec, PacketId, ReasonCode}, PState)
end.
%%--------------------------------------------------------------------
%% Puback
%%--------------------------------------------------------------------
puback(?QOS_0, _PacketId, ReasonCode, PState) ->
handle_out({puberr, ReasonCode}, PState);
puback(?QOS_1, PacketId, ReasonCode, PState) ->
handle_out({puback, PacketId, ReasonCode}, PState);
puback(?QOS_2, PacketId, ReasonCode, PState) ->
handle_out({pubrec, PacketId, ReasonCode}, PState).
%%--------------------------------------------------------------------
%% Process subscribe request
%%--------------------------------------------------------------------
process_subscribe(TopicFilters, PState) ->
process_subscribe(TopicFilters, [], PState).
process_subscribe([], Acc, PState) ->
{lists:reverse(Acc), PState};
process_subscribe([{TopicFilter, SubOpts}|More], Acc, PState) ->
{RC, NPState} = do_subscribe(TopicFilter, SubOpts, PState),
process_subscribe(More, [RC|Acc], NPState).
do_subscribe(TopicFilter, SubOpts = #{qos := QoS},
PState = #protocol{client = Client, session = Session}) ->
case check_subscribe(TopicFilter, SubOpts, PState) of
ok -> TopicFilter1 = mount(Client, TopicFilter),
SubOpts1 = enrich_subopts(maps:merge(?DEFAULT_SUBOPTS, SubOpts), PState),
case emqx_session:subscribe(Client, TopicFilter1, SubOpts1, Session) of
{ok, NSession} ->
{QoS, PState#protocol{session = NSession}};
{error, RC} -> {RC, PState}
end;
{error, RC} -> {RC, PState}
end.
enrich_subid(#{'Subscription-Identifier' := SubId}, TopicFilters) ->
[{Topic, SubOpts#{subid => SubId}} || {Topic, SubOpts} <- TopicFilters];
enrich_subid(_Properties, TopicFilters) ->
TopicFilters.
enrich_subopts(SubOpts, #protocol{proto_ver = ?MQTT_PROTO_V5}) ->
SubOpts;
enrich_subopts(SubOpts, #protocol{client = #{zone := Zone, is_bridge := IsBridge}}) ->
Rap = flag(IsBridge),
Nl = flag(emqx_zone:get_env(Zone, ignore_loop_deliver, false)),
SubOpts#{rap => Rap, nl => Nl}.
%% Check Sub
check_subscribe(TopicFilter, SubOpts, PState) ->
case check_sub_acl(TopicFilter, PState) of
allow -> check_sub_caps(TopicFilter, SubOpts, PState);
deny -> {error, ?RC_NOT_AUTHORIZED}
end.
%% Check Sub ACL
check_sub_acl(TopicFilter, #protocol{client = Client}) ->
case is_acl_enabled(Client) andalso
check_acl(Client, subscribe, TopicFilter) of
false -> allow;
Result -> Result
end.
%% Check Sub Caps
check_sub_caps(TopicFilter, SubOpts, #protocol{client = #{zone := Zone}}) ->
emqx_mqtt_caps:check_sub(Zone, TopicFilter, SubOpts).
%%--------------------------------------------------------------------
%% Process unsubscribe request
%%--------------------------------------------------------------------
process_unsubscribe(TopicFilters, PState) ->
process_unsubscribe(TopicFilters, [], PState).
process_unsubscribe([], Acc, PState) ->
{lists:reverse(Acc), PState};
process_unsubscribe([{TopicFilter, SubOpts}|More], Acc, PState) ->
{RC, PState1} = do_unsubscribe(TopicFilter, SubOpts, PState),
process_unsubscribe(More, [RC|Acc], PState1).
do_unsubscribe(TopicFilter, _SubOpts, PState = #protocol{client = Client,
session = Session}) ->
case emqx_session:unsubscribe(Client, mount(Client, TopicFilter), Session) of
{ok, NSession} ->
{?RC_SUCCESS, PState#protocol{session = NSession}};
{error, RC} -> {RC, PState}
end.
%%--------------------------------------------------------------------
%% Is ACL enabled?
%%--------------------------------------------------------------------
is_acl_enabled(#{zone := Zone, is_superuser := IsSuperuser}) ->
(not IsSuperuser) andalso emqx_zone:get_env(Zone, enable_acl, true).
%%--------------------------------------------------------------------
%% Parse topic filters
%%--------------------------------------------------------------------
parse(subscribe, TopicFilters) ->
[emqx_topic:parse(TopicFilter, SubOpts) || {TopicFilter, SubOpts} <- TopicFilters];
parse(unsubscribe, TopicFilters) ->
lists:map(fun emqx_topic:parse/1, TopicFilters).
%%--------------------------------------------------------------------
%% Mount/Unmount
%%--------------------------------------------------------------------
mount(Client = #{mountpoint := MountPoint}, TopicOrMsg) ->
emqx_mountpoint:mount(emqx_mountpoint:replvar(MountPoint, Client), TopicOrMsg).
unmount(Client = #{mountpoint := MountPoint}, TopicOrMsg) ->
emqx_mountpoint:unmount(emqx_mountpoint:replvar(MountPoint, Client), TopicOrMsg).
%%--------------------------------------------------------------------
%% Pipeline
%%--------------------------------------------------------------------
pipeline([], Packet, PState) ->
{ok, Packet, PState};
pipeline([Fun|More], Packet, PState) ->
case Fun(Packet, PState) of
ok -> pipeline(More, Packet, PState);
{ok, NPState} ->
pipeline(More, Packet, NPState);
{ok, NPacket, NPState} ->
pipeline(More, NPacket, NPState);
{error, ReasonCode} ->
{error, ReasonCode, PState};
{error, ReasonCode, NPState} ->
{error, ReasonCode, NPState}
end.
%%--------------------------------------------------------------------
%% Helper functions
%%--------------------------------------------------------------------
set_property(Name, Value, ?NO_PROPS) ->
#{Name => Value}; #{Name => Value};
set_property(Name, Value, Props) -> set_property(Name, Value, Props) ->
Props#{Name => Value}. Props#{Name => Value}.
@ -911,14 +162,3 @@ get_property(_Name, undefined, Default) ->
Default; Default;
get_property(Name, Props, Default) -> get_property(Name, Props, Default) ->
maps:get(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).

View File

@ -22,6 +22,7 @@
-export([ name/1 -export([ name/1
, name/2 , name/2
, text/1 , text/1
, text/2
, connack_error/1 , connack_error/1
, puback/1 , puback/1
]). ]).
@ -30,7 +31,7 @@
name(I, Ver) when Ver >= ?MQTT_PROTO_V5 -> name(I, Ver) when Ver >= ?MQTT_PROTO_V5 ->
name(I); name(I);
name(0, _Ver) -> connection_acceptd; name(0, _Ver) -> connection_accepted;
name(1, _Ver) -> unacceptable_protocol_version; name(1, _Ver) -> unacceptable_protocol_version;
name(2, _Ver) -> client_identifier_not_valid; name(2, _Ver) -> client_identifier_not_valid;
name(3, _Ver) -> server_unavaliable; name(3, _Ver) -> server_unavaliable;
@ -83,6 +84,16 @@ name(16#A1) -> subscription_identifiers_not_supported;
name(16#A2) -> wildcard_subscriptions_not_supported; name(16#A2) -> wildcard_subscriptions_not_supported;
name(_Code) -> unknown_error. 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#00) -> <<"Success">>;
text(16#01) -> <<"Granted QoS 1">>; text(16#01) -> <<"Granted QoS 1">>;
text(16#02) -> <<"Granted QoS 2">>; 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 =< ?QOS_2 -> Code;
compat(suback, Code) when Code >= 16#80 -> 16#80; 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(client_identifier_not_valid) -> ?RC_CLIENT_IDENTIFIER_NOT_VALID;
connack_error(bad_username_or_password) -> ?RC_BAD_USER_NAME_OR_PASSWORD; 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. %%TODO: This function should be removed.
puback([]) -> ?RC_NO_MATCHING_SUBSCRIBERS; puback([]) -> ?RC_NO_MATCHING_SUBSCRIBERS;
puback(L) when is_list(L) -> ?RC_SUCCESS. puback(L) when is_list(L) -> ?RC_SUCCESS.

View File

@ -29,6 +29,8 @@
-define(RPC, gen_rpc). -define(RPC, gen_rpc).
-define(DefaultClientNum, 1).
call(Node, Mod, Fun, Args) -> call(Node, Mod, Fun, Args) ->
filter_result(?RPC:call(rpc_node(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)). filter_result(?RPC:cast(rpc_node(Node), Mod, Fun, Args)).
rpc_node(Node) -> 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)}. {Node, rand:uniform(ClientNum)}.
rpc_nodes(Nodes) -> rpc_nodes(Nodes) ->
@ -55,4 +57,3 @@ filter_result({Error, Reason})
{badrpc, Reason}; {badrpc, Reason};
filter_result(Delivery) -> filter_result(Delivery) ->
Delivery. Delivery.

View File

@ -50,7 +50,7 @@
-logger_header("[Session]"). -logger_header("[Session]").
-export([init/3]). -export([init/2]).
-export([ info/1 -export([ info/1
, info/2 , info/2
@ -58,6 +58,8 @@
, stats/1 , stats/1
]). ]).
-export([update_expiry_interval/2]).
-export([ subscribe/4 -export([ subscribe/4
, unsubscribe/3 , unsubscribe/3
]). ]).
@ -69,71 +71,53 @@
, pubcomp/2 , 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]). -export_type([session/0]).
-import(emqx_zone,
[ get_env/2
, get_env/3
]).
%% For test case %% For test case
-export([set_pkt_id/2]). -export([set_pkt_id/2]).
-record(session, { -import(emqx_zone, [get_env/3]).
%% Clean Start Flag
clean_start :: boolean(),
-record(session, {
%% Clients Subscriptions. %% Clients Subscriptions.
subscriptions :: map(), subscriptions :: map(),
%% Max subscriptions allowed %% Max subscriptions allowed
max_subscriptions :: non_neg_integer(), max_subscriptions :: non_neg_integer(),
%% Upgrade QoS? %% Upgrade QoS?
upgrade_qos :: boolean(), upgrade_qos :: boolean(),
%% Client <- Broker: %% Client <- Broker:
%% Inflight QoS1, QoS2 messages sent to the client but unacked. %% Inflight QoS1, QoS2 messages sent to the client but unacked.
inflight :: emqx_inflight:inflight(), inflight :: emqx_inflight:inflight(),
%% All QoS1, QoS2 messages published to when client is disconnected. %% All QoS1, QoS2 messages published to when client is disconnected.
%% QoS 1 and QoS 2 messages pending transmission to the Client. %% QoS 1 and QoS 2 messages pending transmission to the Client.
%% %%
%% Optionally, QoS 0 messages pending transmission to the Client. %% Optionally, QoS 0 messages pending transmission to the Client.
mqueue :: emqx_mqueue:mqueue(), mqueue :: emqx_mqueue:mqueue(),
%% Next packet id of the session %% Next packet id of the session
next_pkt_id = 1 :: emqx_types:packet_id(), next_pkt_id = 1 :: emqx_types:packet_id(),
%% Retry interval for redelivering QoS1/2 messages %% Retry interval for redelivering QoS1/2 messages
retry_interval :: timeout(), retry_interval :: timeout(),
%% Retry delivery timer
retry_timer :: maybe(reference()),
%% Client -> Broker: %% Client -> Broker:
%% Inflight QoS2 messages received from client and waiting for pubrel. %% Inflight QoS2 messages received from client and waiting for pubrel.
awaiting_rel :: map(), awaiting_rel :: map(),
%% Max Packets Awaiting PUBREL %% Max Packets Awaiting PUBREL
max_awaiting_rel :: non_neg_integer(), max_awaiting_rel :: non_neg_integer(),
%% Awaiting PUBREL Timer
await_rel_timer :: maybe(reference()),
%% Awaiting PUBREL Timeout %% Awaiting PUBREL Timeout
await_rel_timeout :: timeout(), await_rel_timeout :: timeout(),
%% Session Expiry Interval %% Session Expiry Interval
expiry_interval :: timeout(), expiry_interval :: timeout(),
%% Expired Timer
expiry_timer :: maybe(reference()),
%% Created at %% Created at
created_at :: erlang:timestamp() created_at :: erlang:timestamp()
}). }).
@ -149,11 +133,10 @@
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% @doc Init a session. %% @doc Init a session.
-spec(init(boolean(), emqx_types:client(), Options :: map()) -> session()). -spec(init(emqx_types:client(), Options :: map()) -> session()).
init(CleanStart, #{zone := Zone}, #{max_inflight := MaxInflight, init(#{zone := Zone}, #{max_inflight := MaxInflight,
expiry_interval := ExpiryInterval}) -> expiry_interval := ExpiryInterval}) ->
#session{clean_start = CleanStart, #session{max_subscriptions = get_env(Zone, max_subscriptions, 0),
max_subscriptions = get_env(Zone, max_subscriptions, 0),
subscriptions = #{}, subscriptions = #{},
upgrade_qos = get_env(Zone, upgrade_qos, false), upgrade_qos = get_env(Zone, upgrade_qos, false),
inflight = emqx_inflight:new(MaxInflight), inflight = emqx_inflight:new(MaxInflight),
@ -179,8 +162,7 @@ init_mqueue(Zone) ->
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-spec(info(session()) -> emqx_types:infos()). -spec(info(session()) -> emqx_types:infos()).
info(#session{clean_start = CleanStart, info(#session{max_subscriptions = MaxSubscriptions,
max_subscriptions = MaxSubscriptions,
subscriptions = Subscriptions, subscriptions = Subscriptions,
upgrade_qos = UpgradeQoS, upgrade_qos = UpgradeQoS,
inflight = Inflight, inflight = Inflight,
@ -192,8 +174,7 @@ info(#session{clean_start = CleanStart,
await_rel_timeout = AwaitRelTimeout, await_rel_timeout = AwaitRelTimeout,
expiry_interval = ExpiryInterval, expiry_interval = ExpiryInterval,
created_at = CreatedAt}) -> created_at = CreatedAt}) ->
#{clean_start => CleanStart, #{subscriptions => Subscriptions,
subscriptions => Subscriptions,
max_subscriptions => MaxSubscriptions, max_subscriptions => MaxSubscriptions,
upgrade_qos => UpgradeQoS, upgrade_qos => UpgradeQoS,
inflight => emqx_inflight:size(Inflight), inflight => emqx_inflight:size(Inflight),
@ -206,12 +187,10 @@ info(#session{clean_start = CleanStart,
awaiting_rel => maps:size(AwaitingRel), awaiting_rel => maps:size(AwaitingRel),
max_awaiting_rel => MaxAwaitingRel, max_awaiting_rel => MaxAwaitingRel,
await_rel_timeout => AwaitRelTimeout, await_rel_timeout => AwaitRelTimeout,
expiry_interval => ExpiryInterval div 1000, expiry_interval => ExpiryInterval,
created_at => CreatedAt created_at => CreatedAt
}. }.
info(clean_start, #session{clean_start = CleanStart}) ->
CleanStart;
info(subscriptions, #session{subscriptions = Subs}) -> info(subscriptions, #session{subscriptions = Subs}) ->
Subs; Subs;
info(max_subscriptions, #session{max_subscriptions = MaxSubs}) -> 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}) -> info(await_rel_timeout, #session{await_rel_timeout = Timeout}) ->
Timeout; Timeout;
info(expiry_interval, #session{expiry_interval = Interval}) -> info(expiry_interval, #session{expiry_interval = Interval}) ->
Interval div 1000; Interval;
info(created_at, #session{created_at = CreatedAt}) -> info(created_at, #session{created_at = CreatedAt}) ->
CreatedAt. CreatedAt.
update_expiry_interval(ExpiryInterval, Session) ->
Session#session{expiry_interval = ExpiryInterval}.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Attrs of the session %% Attrs of the session
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-spec(attrs(session()) -> emqx_types:attrs()). -spec(attrs(session()) -> emqx_types:attrs()).
attrs(#session{clean_start = CleanStart, attrs(undefined) ->
expiry_interval = ExpiryInterval, #{};
attrs(#session{expiry_interval = ExpiryInterval,
created_at = CreatedAt}) -> created_at = CreatedAt}) ->
#{clean_start => CleanStart, #{expiry_interval => ExpiryInterval,
expiry_interval => ExpiryInterval,
created_at => CreatedAt created_at => CreatedAt
}. }.
@ -278,6 +260,37 @@ stats(#session{subscriptions = Subscriptions,
{awaiting_rel, maps:size(AwaitingRel)}, {awaiting_rel, maps:size(AwaitingRel)},
{max_awaiting_rel, MaxAwaitingRel}]. {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 %% Client -> Broker: SUBSCRIBE
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
@ -363,7 +376,7 @@ do_publish(PacketId, Msg = #message{timestamp = Ts},
DeliverResults = emqx_broker:publish(Msg), DeliverResults = emqx_broker:publish(Msg),
AwaitingRel1 = maps:put(PacketId, Ts, AwaitingRel), AwaitingRel1 = maps:put(PacketId, Ts, AwaitingRel),
Session1 = Session#session{awaiting_rel = AwaitingRel1}, Session1 = Session#session{awaiting_rel = AwaitingRel1},
{ok, DeliverResults, ensure_await_rel_timer(Session1)}; {ok, DeliverResults, Session1};
true -> true ->
{error, ?RC_PACKET_IDENTIFIER_IN_USE} {error, ?RC_PACKET_IDENTIFIER_IN_USE}
end. end.
@ -502,7 +515,13 @@ deliver([Msg = #message{qos = QoS}|More], Acc,
deliver(More, [Publish|Acc], next_pkt_id(Session1)) deliver(More, [Publish|Acc], next_pkt_id(Session1))
end. 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), emqx_pd:update_counter(enqueue_stats, 1),
{Dropped, NewQ} = emqx_mqueue:in(Msg, Q), {Dropped, NewQ} = emqx_mqueue:in(Msg, Q),
if if
@ -519,9 +538,8 @@ enqueue(Msg, Session = #session{mqueue = Q}) ->
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
await(PacketId, Msg, Session = #session{inflight = Inflight}) -> await(PacketId, Msg, Session = #session{inflight = Inflight}) ->
Inflight1 = emqx_inflight:insert( Inflight1 = emqx_inflight:insert(PacketId, {Msg, os:timestamp()}, Inflight),
PacketId, {Msg, os:timestamp()}, Inflight), Session#session{inflight = Inflight1}.
ensure_retry_timer(Session#session{inflight = Inflight1}).
get_subopts(Topic, SubMap) -> get_subopts(Topic, SubMap) ->
case maps:find(Topic, SubMap) of 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(Opts, Msg#message{qos = max(SubQoS, PubQoS)}, Session);
enrich([{qos, SubQoS}|Opts], Msg = #message{qos = PubQoS}, Session = #session{upgrade_qos= false}) -> enrich([{qos, SubQoS}|Opts], Msg = #message{qos = PubQoS}, Session = #session{upgrade_qos= false}) ->
enrich(Opts, Msg#message{qos = min(SubQoS, PubQoS)}, Session); enrich(Opts, Msg#message{qos = min(SubQoS, PubQoS)}, Session);
enrich([{rap, _Rap}|Opts], Msg = #message{flags = Flags, headers = #{retained := true}}, Session = #session{}) -> enrich([{rap, 0}|Opts], Msg = #message{flags = Flags, headers = #{proto_ver := ?MQTT_PROTO_V5}}, Session) ->
enrich(Opts, Msg#message{flags = maps:put(retain, true, Flags)}, Session);
enrich([{rap, 0}|Opts], Msg = #message{flags = Flags}, Session) ->
enrich(Opts, Msg#message{flags = maps:put(retain, false, Flags)}, Session); enrich(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(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([{subid, SubId}|Opts], Msg, Session) ->
enrich(Opts, emqx_message:set_header('Subscription-Identifier', SubId, 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 %% Retry Delivery
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Redeliver at once if force is true %% 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 case emqx_inflight:is_empty(Inflight) of
true -> {ok, Session}; true -> {ok, Session};
false -> false ->
@ -599,10 +587,11 @@ retry_delivery(Session = #session{inflight = Inflight}) ->
retry_delivery([], _Now, Acc, Session) -> retry_delivery([], _Now, Acc, Session) ->
%% Retry again... %% Retry again...
{ok, lists:reverse(Acc), ensure_retry_timer(Session)}; {ok, lists:reverse(Acc), Session};
retry_delivery([{PacketId, {Val, Ts}}|More], Now, Acc, retry_delivery([{PacketId, {Val, Ts}}|More], Now, Acc,
Session = #session{retry_interval = Interval, inflight = Inflight}) -> Session = #session{retry_interval = Interval,
inflight = Inflight}) ->
%% Microseconds -> MilliSeconds %% Microseconds -> MilliSeconds
Age = timer:now_diff(Now, Ts) div 1000, Age = timer:now_diff(Now, Ts) div 1000,
if if
@ -610,7 +599,7 @@ retry_delivery([{PacketId, {Val, Ts}}|More], Now, Acc,
{Acc1, Inflight1} = retry_delivery(PacketId, Val, Now, Acc, Inflight), {Acc1, Inflight1} = retry_delivery(PacketId, Val, Now, Acc, Inflight),
retry_delivery(More, Now, Acc1, Session#session{inflight = Inflight1}); retry_delivery(More, Now, Acc1, Session#session{inflight = Inflight1});
true -> true ->
{ok, lists:reverse(Acc), ensure_retry_timer(Interval - max(0, Age), Session)} {ok, lists:reverse(Acc), Interval - max(0, Age), Session}
end. end.
retry_delivery(PacketId, Msg, Now, Acc, Inflight) when is_record(Msg, message) -> 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), Inflight1 = emqx_inflight:update(PacketId, {pubrel, Now}, Inflight),
{[{pubrel, PacketId}|Acc], Inflight1}. {[{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
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
expire_awaiting_rel(Session = #session{awaiting_rel = AwaitingRel}) -> expire(awaiting_rel, Session = #session{awaiting_rel = AwaitingRel}) ->
case maps:size(AwaitingRel) of case maps:size(AwaitingRel) of
0 -> {ok, Session}; 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. end.
expire_awaiting_rel([], _Now, Session) -> expire_awaiting_rel([], _Now, Session) ->
{ok, Session#session{await_rel_timer = undefined}}; {ok, Session};
expire_awaiting_rel([{PacketId, Ts} | More], Now, expire_awaiting_rel([{PacketId, Ts} | More], Now,
Session = #session{awaiting_rel = AwaitingRel, 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)}, Session1 = Session#session{awaiting_rel = maps:remove(PacketId, AwaitingRel)},
expire_awaiting_rel(More, Now, Session1); expire_awaiting_rel(More, Now, Session1);
Age -> Age ->
{ok, ensure_await_rel_timer(Timeout - max(0, Age), Session)} {ok, Timeout - max(0, Age), Session}
end. end.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
@ -683,6 +658,6 @@ next_pkt_id(Session = #session{next_pkt_id = Id}) ->
%% For Test case %% For Test case
%%--------------------------------------------------------------------- %%---------------------------------------------------------------------
set_pkt_id(Session, PktId) -> set_pkt_id(Session, PktId) ->
Session#session{next_pkt_id = PktId}. Session#session{next_pkt_id = PktId}.

View File

@ -23,7 +23,9 @@
-logger_header("[SYS]"). -logger_header("[SYS]").
-export([start_link/0]). -export([ start_link/0
, stop/0
]).
-export([ version/0 -export([ version/0
, uptime/0 , uptime/0
@ -41,23 +43,36 @@
, handle_cast/2 , handle_cast/2
, handle_info/2 , handle_info/2
, terminate/2 , terminate/2
, code_change/3
]). ]).
-import(emqx_topic, [systop/1]). -import(emqx_topic, [systop/1]).
-import(emqx_misc, [start_timer/2]). -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(APP, emqx).
-define(SYS, ?MODULE). -define(SYS, ?MODULE).
-define(INFO_KEYS, [ -define(INFO_KEYS,
version, % Broker version [ version % Broker version
uptime, % Broker uptime , uptime % Broker uptime
datetime, % Broker local datetime , datetime % Broker local datetime
sysdescr % Broker description , sysdescr % Broker description
]). ]).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% APIs %% APIs
@ -67,6 +82,9 @@
start_link() -> start_link() ->
gen_server:start_link({local, ?SYS}, ?MODULE, [], []). gen_server:start_link({local, ?SYS}, ?MODULE, [], []).
stop() ->
gen_server:stop(?SYS).
%% @doc Get sys version %% @doc Get sys version
-spec(version() -> string()). -spec(version() -> string()).
version() -> version() ->
@ -93,12 +111,12 @@ datetime() ->
%% @doc Get sys interval %% @doc Get sys interval
-spec(sys_interval() -> pos_integer()). -spec(sys_interval() -> pos_integer()).
sys_interval() -> sys_interval() ->
application:get_env(?APP, broker_sys_interval, 60000). emqx_config:get_env(broker_sys_interval, 60000).
%% @doc Get sys heatbeat interval %% @doc Get sys heatbeat interval
-spec(sys_heatbeat_interval() -> pos_integer()). -spec(sys_heatbeat_interval() -> pos_integer()).
sys_heatbeat_interval() -> sys_heatbeat_interval() ->
application:get_env(?APP, broker_sys_heartbeat, 30000). emqx_config:get_env(broker_sys_heartbeat, 30000).
%% @doc Get sys info %% @doc Get sys info
-spec(info() -> list(tuple())). -spec(info() -> list(tuple())).
@ -154,9 +172,6 @@ handle_info(Info, State) ->
terminate(_Reason, #state{heartbeat = TRef1, ticker = TRef2}) -> terminate(_Reason, #state{heartbeat = TRef1, ticker = TRef2}) ->
lists:foreach(fun emqx_misc:cancel_timer/1, [TRef1, TRef2]). lists:foreach(fun emqx_misc:cancel_timer/1, [TRef1, TRef2]).
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%----------------------------------------------------------------------------- %%-----------------------------------------------------------------------------
%% Internal functions %% Internal functions
%%----------------------------------------------------------------------------- %%-----------------------------------------------------------------------------
@ -207,4 +222,3 @@ safe_publish(Topic, Flags, Payload) ->
emqx_message:set_flags( emqx_message:set_flags(
maps:merge(#{sys => true}, Flags), maps:merge(#{sys => true}, Flags),
emqx_message:make(?SYS, Topic, iolist_to_binary(Payload)))). emqx_message:make(?SYS, Topic, iolist_to_binary(Payload)))).

View File

@ -57,6 +57,10 @@ start_link(Opts) ->
init([Opts]) -> init([Opts]) ->
erlang:system_monitor(self(), parse_opt(Opts)), erlang:system_monitor(self(), parse_opt(Opts)),
emqx_logger:set_proc_metadata(#{sysmon => true}), 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 => []})}. {ok, start_timer(#{timer => undefined, events => []})}.
start_timer(State) -> start_timer(State) ->
@ -156,6 +160,15 @@ terminate(_Reason, #{timer := TRef}) ->
code_change(_OldVsn, State, _Extra) -> code_change(_OldVsn, State, _Extra) ->
{ok, State}. {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}) -> suppress(Key, SuccFun, State = #{events := Events}) ->
case lists:member(Key, Events) of case lists:member(Key, Events) of
true -> {noreply, State}; true -> {noreply, State};

View File

@ -14,21 +14,26 @@
%% limitations under the License. %% limitations under the License.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% MQTT WebSocket Channel %% MQTT WebSocket Connection
-module(emqx_ws_channel). -module(emqx_ws_connection).
-include("emqx.hrl"). -include("emqx.hrl").
-include("emqx_mqtt.hrl"). -include("emqx_mqtt.hrl").
-include("logger.hrl"). -include("logger.hrl").
-include("types.hrl"). -include("types.hrl").
-logger_header("[WsChannel]"). -logger_header("[WsConnection]").
-export([ info/1 -export([ info/1
, attrs/1 , attrs/1
, stats/1 , stats/1
]). ]).
-export([ kick/1
, discard/1
, takeover/2
]).
%% WebSocket callbacks %% WebSocket callbacks
-export([ init/2 -export([ init/2
, websocket_init/1 , websocket_init/1
@ -43,21 +48,15 @@
fsm_state :: idle | connected | disconnected, fsm_state :: idle | connected | disconnected,
serialize :: fun((emqx_types:packet()) -> iodata()), serialize :: fun((emqx_types:packet()) -> iodata()),
parse_state :: emqx_frame:parse_state(), parse_state :: emqx_frame:parse_state(),
proto_state :: emqx_protocol:proto_state(), chan_state :: emqx_channel:channel(),
gc_state :: emqx_gc:gc_state(),
keepalive :: maybe(emqx_keepalive:keepalive()),
pendings :: list(), pendings :: list(),
stats_timer :: disabled | maybe(reference()), stop_reason :: term()
idle_timeout :: timeout(),
connected :: boolean(),
connected_at :: erlang:timestamp(),
reason :: term()
}). }).
-type(state() :: #state{}). -type(state() :: #state{}).
-define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt]). -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 %% API
@ -68,50 +67,53 @@ info(WSPid) when is_pid(WSPid) ->
call(WSPid, info); call(WSPid, info);
info(#state{peername = Peername, info(#state{peername = Peername,
sockname = Sockname, sockname = Sockname,
proto_state = ProtoState, chan_state = ChanState}) ->
gc_state = GCState, ConnInfo = #{socktype => websocket,
stats_timer = StatsTimer,
idle_timeout = IdleTimeout,
connected = Connected,
connected_at = ConnectedAt}) ->
ChanInfo = #{socktype => websocket,
peername => Peername, peername => Peername,
sockname => Sockname, sockname => Sockname,
conn_state => running, conn_state => running
gc_state => emqx_gc:info(GCState),
enable_stats => enable_stats(StatsTimer),
idle_timeout => IdleTimeout,
connected => Connected,
connected_at => ConnectedAt
}, },
maps:merge(ChanInfo, emqx_protocol:info(ProtoState)). ChanInfo = emqx_channel:info(ChanState),
maps:merge(ConnInfo, ChanInfo).
enable_stats(disabled) -> false;
enable_stats(_MaybeRef) -> true.
-spec(attrs(pid() | state()) -> emqx_types:attrs()). -spec(attrs(pid() | state()) -> emqx_types:attrs()).
attrs(WSPid) when is_pid(WSPid) -> attrs(WSPid) when is_pid(WSPid) ->
call(WSPid, attrs); call(WSPid, attrs);
attrs(#state{peername = Peername, attrs(#state{peername = Peername,
sockname = Sockname, sockname = Sockname,
proto_state = ProtoState, chan_state = ChanState}) ->
connected = Connected,
connected_at = ConnectedAt}) ->
ConnAttrs = #{socktype => websocket, ConnAttrs = #{socktype => websocket,
peername => Peername, peername => Peername,
sockname => Sockname, sockname => Sockname
connected => Connected,
connected_at => ConnectedAt
}, },
maps:merge(ConnAttrs, emqx_protocol:attrs(ProtoState)). ChanAttrs = emqx_channel:attrs(ChanState),
maps:merge(ConnAttrs, ChanAttrs).
-spec(stats(pid() | state()) -> emqx_types:stats()). -spec(stats(pid() | state()) -> emqx_types:stats()).
stats(WSPid) when is_pid(WSPid) -> stats(WSPid) when is_pid(WSPid) ->
call(WSPid, stats); call(WSPid, stats);
stats(#state{proto_state = ProtoState}) -> stats(#state{chan_state = ChanState}) ->
ProcStats = emqx_misc:proc_stats(), ProcStats = emqx_misc:proc_stats(),
SessStats = emqx_session:stats(emqx_protocol:info(session, ProtoState)), ChanStats = emqx_channel:stats(ChanState),
lists:append([ProcStats, SessStats, chan_stats(), wsock_stats()]). 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 %% @private
call(WSPid, Req) when is_pid(WSPid) -> call(WSPid, Req) when is_pid(WSPid) ->
@ -171,47 +173,37 @@ websocket_init([Req, Opts]) ->
[Error, Reason]), [Error, Reason]),
undefined undefined
end, end,
ProtoState = emqx_protocol:init(#{peername => Peername, ChanState = emqx_channel:init(#{peername => Peername,
sockname => Sockname, sockname => Sockname,
peercert => Peercert, peercert => Peercert,
ws_cookie => WsCookie, ws_cookie => WsCookie,
conn_mod => ?MODULE}, Opts), conn_mod => ?MODULE
}, Opts),
Zone = proplists:get_value(zone, Opts), Zone = proplists:get_value(zone, Opts),
MaxSize = emqx_zone:get_env(Zone, max_packet_size, ?MAX_PACKET_SIZE), MaxSize = emqx_zone:get_env(Zone, max_packet_size, ?MAX_PACKET_SIZE),
ParseState = emqx_frame:initial_parse_state(#{max_size => MaxSize}), 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)), emqx_logger:set_metadata_peername(esockd_net:format(Peername)),
ok = emqx_misc:init_proc_mng_policy(Zone),
{ok, #state{peername = Peername, {ok, #state{peername = Peername,
sockname = Sockname, sockname = Sockname,
fsm_state = idle, fsm_state = idle,
parse_state = ParseState, parse_state = ParseState,
proto_state = ProtoState, chan_state = ChanState,
gc_state = GcState, pendings = []
pendings = [],
stats_timer = StatsTimer,
idle_timeout = IdleTimout,
connected = false
}}. }}.
stat_fun() ->
fun() -> {ok, emqx_pd:get_counter(recv_oct)} end.
websocket_handle({binary, Data}, State) when is_list(Data) -> websocket_handle({binary, Data}, State) when is_list(Data) ->
websocket_handle({binary, iolist_to_binary(Data)}, State); 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]), ?LOG(debug, "RECV ~p", [Data]),
Oct = iolist_size(Data), Oct = iolist_size(Data),
emqx_pd:update_counter(recv_cnt, 1), emqx_pd:update_counter(recv_cnt, 1),
emqx_pd:update_counter(recv_oct, Oct), emqx_pd:update_counter(recv_oct, Oct),
ok = emqx_metrics:inc('bytes.received', Oct), ok = emqx_metrics:inc('bytes.received', Oct),
NState = maybe_gc(1, Oct, State), NChanState = emqx_channel:ensure_timer(
process_incoming(Data, ensure_stats_timer(NState)); 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 %% Pings should be replied with pongs, cowboy does it automatically
%% Pongs can be safely ignored. Clause here simply prevents crash. %% 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) -> websocket_info({call, From, kick}, State) ->
gen_server:reply(From, ok), 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( websocket_info({incoming, Packet = ?CONNECT_PACKET(
#mqtt_packet_connect{ #mqtt_packet_connect{
@ -264,138 +277,72 @@ websocket_info({incoming, Packet}, State = #state{fsm_state = connected})
handle_incoming(Packet, fun reply/1, State); handle_incoming(Packet, fun reply/1, State);
websocket_info(Deliver = {deliver, _Topic, _Msg}, websocket_info(Deliver = {deliver, _Topic, _Msg},
State = #state{proto_state = ProtoState}) -> State = #state{chan_state = ChanState}) ->
Delivers = emqx_misc:drain_deliver([Deliver]), Delivers = emqx_misc:drain_deliver([Deliver]),
case emqx_protocol:handle_deliver(Delivers, ProtoState) of case emqx_channel:handle_out({deliver, Delivers}, ChanState) of
{ok, NProtoState} -> {ok, NChanState} ->
reply(State#state{proto_state = NProtoState}); reply(State#state{chan_state = NChanState});
{ok, Packets, NProtoState} -> {ok, Packets, NChanState} ->
reply(enqueue(Packets, State#state{proto_state = NProtoState})); reply(enqueue(Packets, State#state{chan_state = NChanState}));
{error, Reason} -> {stop, Reason, NChanState} ->
stop(Reason, State); stop(Reason, State#state{chan_state = NChanState})
{error, Reason, NProtoState} ->
stop(Reason, State#state{proto_state = NProtoState})
end; end;
websocket_info({keepalive, check}, State = #state{keepalive = KeepAlive}) -> websocket_info({timeout, TRef, keepalive}, State) when is_reference(TRef) ->
case emqx_keepalive:check(KeepAlive) of RecvOct = emqx_pd:get_counter(recv_oct),
{ok, KeepAlive1} -> handle_timeout(TRef, {keepalive, RecvOct}, State);
{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, Timer, emit_stats}, websocket_info({timeout, TRef, emit_stats}, State) when is_reference(TRef) ->
State = #state{stats_timer = Timer, handle_timeout(TRef, {emit_stats, stats(State)}, State);
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, Timer, Msg}, websocket_info({timeout, TRef, Msg}, State) when is_reference(TRef) ->
State = #state{proto_state = ProtoState}) -> handle_timeout(TRef, Msg, State);
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({shutdown, conflict, {ClientId, NewPid}}, State) -> websocket_info({shutdown, conflict, {ClientId, NewPid}}, State) ->
?LOG(warning, "Clientid '~s' conflict with ~p", [ClientId, NewPid]), ?LOG(warning, "Clientid '~s' conflict with ~p", [ClientId, NewPid]),
stop(conflict, State); stop(conflict, State);
%% websocket_info({binary, Data}, State) ->
%% {reply, {binary, Data}, State};
websocket_info({shutdown, Reason}, State) -> websocket_info({shutdown, Reason}, State) ->
stop(Reason, State); stop(Reason, State);
websocket_info({stop, Reason}, State) -> websocket_info({stop, Reason}, State) ->
stop(Reason, State); stop(Reason, State);
websocket_info(Info, State) -> websocket_info(Info, State = #state{chan_state = ChanState}) ->
?LOG(error, "Unexpected info: ~p", [Info]), case emqx_channel:handle_info(Info, ChanState) of
{ok, State}. {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, terminate(SockError, _Req, #state{chan_state = ChanState,
proto_state = ProtoState, stop_reason = Reason}) ->
reason = Reason}) ->
?LOG(debug, "Terminated for ~p, sockerror: ~p", ?LOG(debug, "Terminated for ~p, sockerror: ~p",
[Reason, SockError]), [Reason, SockError]),
emqx_keepalive:cancel(Keepalive), emqx_channel:terminate(Reason, ChanState).
emqx_protocol:terminate(Reason, ProtoState).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Connected callback %% Connected callback
connected(State = #state{proto_state = ProtoState}) -> connected(State = #state{chan_state = ChanState}) ->
NState = State#state{fsm_state = connected, NState = State#state{fsm_state = connected},
connected = true, #{client_id := ClientId} = emqx_channel:info(client, ChanState),
connected_at = os:timestamp()
},
ClientId = emqx_protocol:info(client_id, ProtoState),
ok = emqx_cm:register_channel(ClientId), ok = emqx_cm:register_channel(ClientId),
ok = emqx_cm:set_chan_attrs(ClientId, info(NState)), ok = emqx_cm:set_chan_attrs(ClientId, attrs(NState)),
%% Ensure keepalive after connected successfully. reply(NState).
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.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Ensure keepalive %% Handle timeout
ensure_keepalive(0, _State) -> handle_timeout(TRef, Msg, State = #state{chan_state = ChanState}) ->
ignore; case emqx_channel:timeout(TRef, Msg, ChanState) of
ensure_keepalive(Interval, #state{proto_state = ProtoState}) -> {ok, NChanState} ->
Backoff = emqx_zone:get_env(emqx_protocol:info(zone, ProtoState), {ok, State#state{chan_state = NChanState}};
keepalive_backoff, 0.75), {ok, Packets, NChanState} ->
emqx_keepalive:start(stat_fun(), round(Interval * Backoff), {keepalive, check}). reply(enqueue(Packets, State#state{chan_state = NChanState}));
{stop, Reason, NChanState} ->
%%-------------------------------------------------------------------- stop(Reason, State#state{chan_state = NChanState})
%% Handle internal request
handle_request(Req, State = #state{proto_state = ProtoState}) ->
case emqx_protocol:handle_req(Req, ProtoState) of
{ok, _Result, NProtoState} -> %% TODO:: how to handle the result?
{ok, State#state{proto_state = NProtoState}};
{error, Reason, NProtoState} ->
stop(Reason, State#state{proto_state = NProtoState})
end. end.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
@ -424,22 +371,19 @@ process_incoming(Data, State = #state{parse_state = ParseState}) ->
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Handle incoming packets %% Handle incoming packets
handle_incoming(Packet = ?PACKET(Type), SuccFun, handle_incoming(Packet = ?PACKET(Type), SuccFun, State = #state{chan_state = ChanState}) ->
State = #state{proto_state = ProtoState}) ->
_ = inc_incoming_stats(Type), _ = inc_incoming_stats(Type),
ok = emqx_metrics:inc_recv(Packet), ok = emqx_metrics:inc_recv(Packet),
?LOG(debug, "RECV ~s", [emqx_packet:format(Packet)]), ?LOG(debug, "RECV ~s", [emqx_packet:format(Packet)]),
case emqx_protocol:handle_in(Packet, ProtoState) of case emqx_channel:handle_in(Packet, ChanState) of
{ok, NProtoState} -> {ok, NChanState} ->
SuccFun(State#state{proto_state = NProtoState}); SuccFun(State#state{chan_state= NChanState});
{ok, OutPackets, NProtoState} -> {ok, OutPackets, NChanState} ->
SuccFun(enqueue(OutPackets, State#state{proto_state = NProtoState})); SuccFun(enqueue(OutPackets, State#state{chan_state= NChanState}));
{error, Reason, NProtoState} -> {stop, Reason, NChanState} ->
stop(Reason, State#state{proto_state = NProtoState}); stop(Reason, State#state{chan_state= NChanState});
{error, Reason, OutPacket, NProtoState} -> {stop, Reason, OutPacket, NChanState} ->
stop(Reason, enqueue(OutPacket, State#state{proto_state = NProtoState})); stop(Reason, enqueue(OutPacket, State#state{chan_state= NChanState}))
{stop, Error, NProtoState} ->
stop(Error, State#state{proto_state = NProtoState})
end. end.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
@ -479,45 +423,20 @@ inc_outgoing_stats(Type) ->
reply(State = #state{pendings = []}) -> reply(State = #state{pendings = []}) ->
{ok, State}; {ok, State};
reply(State = #state{pendings = Pendings}) -> reply(State = #state{chan_state = ChanState, pendings = Pendings}) ->
Reply = handle_outgoing(Pendings, State), 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(Reason, State = #state{pendings = []}) ->
{stop, State#state{reason = Reason}}; {stop, State#state{stop_reason = Reason}};
stop(Reason, State = #state{pendings = Pendings}) -> stop(Reason, State = #state{pendings = Pendings}) ->
Reply = handle_outgoing(Pendings, State), Reply = handle_outgoing(Pendings, State),
{reply, [Reply, close], {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) when is_record(Packet, mqtt_packet) ->
enqueue([Packet], State); enqueue([Packet], State);
enqueue(Packets, State = #state{pendings = Pendings}) -> enqueue(Packets, State = #state{pendings = Pendings}) ->
State#state{pendings = lists:append(Pendings, Packets)}. 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}.

81
test/emqx_SUITE.erl Normal file
View File

@ -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}.

View File

@ -40,7 +40,7 @@ set_special_configs(_App) -> ok.
t_alarm_handler(_) -> t_alarm_handler(_) ->
with_connection( with_connection(
fun(Sock) -> fun(Sock) ->
emqx_client_sock:send(Sock, emqtt_sock:send(Sock,
raw_send_serialize( raw_send_serialize(
?CONNECT_PACKET( ?CONNECT_PACKET(
#mqtt_packet_connect{ #mqtt_packet_connect{
@ -52,7 +52,7 @@ t_alarm_handler(_) ->
Topic1 = emqx_topic:systop(<<"alarms/alert">>), Topic1 = emqx_topic:systop(<<"alarms/alert">>),
Topic2 = emqx_topic:systop(<<"alarms/clear">>), Topic2 = emqx_topic:systop(<<"alarms/clear">>),
SubOpts = #{rh => 1, qos => ?QOS_2, rap => 0, nl => 0, rc => 0}, SubOpts = #{rh => 1, qos => ?QOS_2, rap => 0, nl => 0, rc => 0},
emqx_client_sock:send(Sock, emqtt_sock:send(Sock,
raw_send_serialize( raw_send_serialize(
?SUBSCRIBE_PACKET( ?SUBSCRIBE_PACKET(
1, 1,
@ -86,13 +86,13 @@ t_alarm_handler(_) ->
end). end).
with_connection(DoFun) -> 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}], [binary, {packet, raw}, {active, false}],
3000), 3000),
try try
DoFun(Sock) DoFun(Sock)
after after
emqx_client_sock:close(Sock) emqtt_sock:close(Sock)
end. end.
raw_send_serialize(Packet) -> raw_send_serialize(Packet) ->
@ -100,4 +100,3 @@ raw_send_serialize(Packet) ->
raw_recv_parse(Bin) -> raw_recv_parse(Bin) ->
emqx_frame:parse(Bin, emqx_frame:initial_parse_state(#{version => ?MQTT_PROTO_V5})). emqx_frame:parse(Bin, emqx_frame:initial_parse_state(#{version => ?MQTT_PROTO_V5})).

View File

@ -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("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 %%% %%% Properties %%%

View File

@ -19,6 +19,13 @@
-compile(export_all). -compile(export_all).
-compile(nowarn_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"). -include_lib("eunit/include/eunit.hrl").
all() -> emqx_ct:all(?MODULE). all() -> emqx_ct:all(?MODULE).
@ -30,28 +37,259 @@ init_per_suite(Config) ->
end_per_suite(_Config) -> end_per_suite(_Config) ->
emqx_ct_helpers:stop_apps([]). emqx_ct_helpers:stop_apps([]).
t_basic(_) -> %%--------------------------------------------------------------------
Topic = <<"TopicA">>, %% Test cases for handle_in
{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) -> t_handle_connect(_) ->
recv_msgs(Count, []). 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) -> t_handle_publish_qos0(_) ->
Msgs; with_channel(
recv_msgs(Count, Msgs) -> fun(Channel) ->
receive Publish = ?PUBLISH_PACKET(?QOS_0, <<"topic">>, undefined, <<"payload">>),
{publish, Msg} -> {ok, Channel} = handle_in(Publish, Channel)
recv_msgs(Count-1, [Msg|Msgs]) end).
after 100 ->
Msgs t_handle_publish_qos1(_) ->
end. 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))).

View File

@ -54,6 +54,8 @@ groups() ->
]}, ]},
{mqttv4, [non_parallel_tests], {mqttv4, [non_parallel_tests],
[t_basic_v4, [t_basic_v4,
t_cm,
t_cm_registry,
%% t_will_message, %% t_will_message,
%% t_offline_message_queueing, %% t_offline_message_queueing,
t_overlapping_subscriptions, t_overlapping_subscriptions,
@ -88,56 +90,79 @@ t_basic_v3(_) ->
t_basic_v4(_Config) -> t_basic_v4(_Config) ->
t_basic([{proto_ver, v4}]). 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) -> 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_topic, nth(3, ?TOPICS)},
{will_payload, <<"client disconnected">>}, {will_payload, <<"client disconnected">>},
{keepalive, 1}]), {keepalive, 1}]),
{ok, _} = emqx_client:connect(C1), {ok, _} = emqtt:connect(C1),
{ok, C2} = emqx_client:start_link(), {ok, C2} = emqtt:start_link(),
{ok, _} = emqx_client:connect(C2), {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), timer:sleep(5),
ok = emqx_client:stop(C1), ok = emqtt:stop(C1),
timer:sleep(5), timer:sleep(5),
?assertEqual(1, length(recv_msgs(1))), ?assertEqual(1, length(recv_msgs(1))),
ok = emqx_client:disconnect(C2), ok = emqtt:disconnect(C2),
ct:pal("Will message test succeeded"). ct:pal("Will message test succeeded").
t_offline_message_queueing(_) -> t_offline_message_queueing(_) ->
{ok, C1} = emqx_client:start_link([{clean_start, false}, {ok, C1} = emqtt:start_link([{clean_start, false},
{client_id, <<"c1">>}]), {client_id, <<"c1">>}]),
{ok, _} = emqx_client:connect(C1), {ok, _} = emqtt:connect(C1),
{ok, _, [2]} = emqx_client:subscribe(C1, nth(6, ?WILD_TOPICS), 2), {ok, _, [2]} = emqtt:subscribe(C1, nth(6, ?WILD_TOPICS), 2),
ok = emqx_client:disconnect(C1), ok = emqtt:disconnect(C1),
{ok, C2} = emqx_client:start_link([{clean_start, true}, {ok, C2} = emqtt:start_link([{clean_start, true},
{client_id, <<"c2">>}]), {client_id, <<"c2">>}]),
{ok, _} = emqx_client:connect(C2), {ok, _} = emqtt:connect(C2),
ok = emqx_client:publish(C2, nth(2, ?TOPICS), <<"qos 0">>, 0), ok = emqtt:publish(C2, nth(2, ?TOPICS), <<"qos 0">>, 0),
{ok, _} = emqx_client:publish(C2, nth(3, ?TOPICS), <<"qos 1">>, 1), {ok, _} = emqtt:publish(C2, nth(3, ?TOPICS), <<"qos 1">>, 1),
{ok, _} = emqx_client:publish(C2, nth(4, ?TOPICS), <<"qos 2">>, 2), {ok, _} = emqtt:publish(C2, nth(4, ?TOPICS), <<"qos 2">>, 2),
timer:sleep(10), timer:sleep(10),
emqx_client:disconnect(C2), emqtt:disconnect(C2),
{ok, C3} = emqx_client:start_link([{clean_start, false}, {ok, C3} = emqtt:start_link([{clean_start, false},
{client_id, <<"c1">>}]), {client_id, <<"c1">>}]),
{ok, _} = emqx_client:connect(C3), {ok, _} = emqtt:connect(C3),
timer:sleep(10), timer:sleep(10),
emqx_client:disconnect(C3), emqtt:disconnect(C3),
?assertEqual(3, length(recv_msgs(3))). ?assertEqual(3, length(recv_msgs(3))).
t_overlapping_subscriptions(_) -> t_overlapping_subscriptions(_) ->
{ok, C} = emqx_client:start_link([]), {ok, C} = emqtt:start_link([]),
{ok, _} = emqx_client:connect(C), {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}]), {nth(1, ?WILD_TOPICS), 1}]),
timer:sleep(10), 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), timer:sleep(10),
Num = length(recv_msgs(2)), Num = length(recv_msgs(2)),
@ -151,67 +176,67 @@ t_overlapping_subscriptions(_) ->
matching overlapping subscription."); matching overlapping subscription.");
true -> ok true -> ok
end, end,
emqx_client:disconnect(C). emqtt:disconnect(C).
%% t_keepalive_test(_) -> %% t_keepalive_test(_) ->
%% ct:print("Keepalive test starting"), %% ct:print("Keepalive test starting"),
%% {ok, C1, _} = emqx_client:start_link([{clean_start, true}, %% {ok, C1, _} = emqtt:start_link([{clean_start, true},
%% {keepalive, 5}, %% {keepalive, 5},
%% {will_flag, true}, %% {will_flag, true},
%% {will_topic, nth(5, ?TOPICS)}, %% {will_topic, nth(5, ?TOPICS)},
%% %% {will_qos, 2}, %% %% {will_qos, 2},
%% {will_payload, <<"keepalive expiry">>}]), %% {will_payload, <<"keepalive expiry">>}]),
%% ok = emqx_client:pause(C1), %% ok = emqtt:pause(C1),
%% {ok, C2, _} = emqx_client:start_link([{clean_start, true}, %% {ok, C2, _} = emqtt:start_link([{clean_start, true},
%% {keepalive, 0}]), %% {keepalive, 0}]),
%% {ok, _, [2]} = emqx_client:subscribe(C2, nth(5, ?TOPICS), 2), %% {ok, _, [2]} = emqtt:subscribe(C2, nth(5, ?TOPICS), 2),
%% ok = emqx_client:disconnect(C2), %% ok = emqtt:disconnect(C2),
%% ?assertEqual(1, length(recv_msgs(1))), %% ?assertEqual(1, length(recv_msgs(1))),
%% ct:print("Keepalive test succeeded"). %% ct:print("Keepalive test succeeded").
t_redelivery_on_reconnect(_) -> t_redelivery_on_reconnect(_) ->
ct:pal("Redelivery on reconnect test starting"), 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">>}]), {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), timer:sleep(10),
ok = emqx_client:pause(C1), ok = emqtt:pause(C1),
{ok, _} = emqx_client:publish(C1, nth(2, ?TOPICS), <<>>, {ok, _} = emqtt:publish(C1, nth(2, ?TOPICS), <<>>,
[{qos, 1}, {retain, false}]), [{qos, 1}, {retain, false}]),
{ok, _} = emqx_client:publish(C1, nth(4, ?TOPICS), <<>>, {ok, _} = emqtt:publish(C1, nth(4, ?TOPICS), <<>>,
[{qos, 2}, {retain, false}]), [{qos, 2}, {retain, false}]),
timer:sleep(10), timer:sleep(10),
ok = emqx_client:disconnect(C1), ok = emqtt:disconnect(C1),
?assertEqual(0, length(recv_msgs(2))), ?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">>}]), {client_id, <<"c">>}]),
{ok, _} = emqx_client:connect(C2), {ok, _} = emqtt:connect(C2),
timer:sleep(10), timer:sleep(10),
ok = emqx_client:disconnect(C2), ok = emqtt:disconnect(C2),
?assertEqual(2, length(recv_msgs(2))). ?assertEqual(2, length(recv_msgs(2))).
%% t_subscribe_sys_topics(_) -> %% t_subscribe_sys_topics(_) ->
%% ct:print("Subscribe failure test starting"), %% ct:print("Subscribe failure test starting"),
%% {ok, C, _} = emqx_client:start_link([]), %% {ok, C, _} = emqtt:start_link([]),
%% {ok, _, [2]} = emqx_client:subscribe(C, <<"$SYS/#">>, 2), %% {ok, _, [2]} = emqtt:subscribe(C, <<"$SYS/#">>, 2),
%% timer:sleep(10), %% timer:sleep(10),
%% ct:print("Subscribe failure test succeeded"). %% ct:print("Subscribe failure test succeeded").
t_dollar_topics(_) -> t_dollar_topics(_) ->
ct:pal("$ topics test starting"), ct:pal("$ topics test starting"),
{ok, C} = emqx_client:start_link([{clean_start, true}, {ok, C} = emqtt:start_link([{clean_start, true},
{keepalive, 0}]), {keepalive, 0}]),
{ok, _} = emqx_client:connect(C), {ok, _} = emqtt:connect(C),
{ok, _, [1]} = emqx_client:subscribe(C, nth(6, ?WILD_TOPICS), 1), {ok, _, [1]} = emqtt:subscribe(C, nth(6, ?WILD_TOPICS), 1),
{ok, _} = emqx_client:publish(C, << <<"$">>/binary, (nth(2, ?TOPICS))/binary>>, {ok, _} = emqtt:publish(C, << <<"$">>/binary, (nth(2, ?TOPICS))/binary>>,
<<"test">>, [{qos, 1}, {retain, false}]), <<"test">>, [{qos, 1}, {retain, false}]),
timer:sleep(10), timer:sleep(10),
?assertEqual(0, length(recv_msgs(1))), ?assertEqual(0, length(recv_msgs(1))),
ok = emqx_client:disconnect(C), ok = emqtt:disconnect(C),
ct:pal("$ topics test succeeded"). ct:pal("$ topics test succeeded").
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
@ -229,15 +254,15 @@ t_basic_with_props_v5(_) ->
t_basic(Opts) -> t_basic(Opts) ->
Topic = nth(1, ?TOPICS), Topic = nth(1, ?TOPICS),
{ok, C} = emqx_client:start_link([{proto_ver, v4}]), {ok, C} = emqtt:start_link([{proto_ver, v4}]),
{ok, _} = emqx_client:connect(C), {ok, _} = emqtt:connect(C),
{ok, _, [1]} = emqx_client:subscribe(C, Topic, qos1), {ok, _, [1]} = emqtt:subscribe(C, Topic, qos1),
{ok, _, [2]} = emqx_client:subscribe(C, Topic, qos2), {ok, _, [2]} = emqtt:subscribe(C, Topic, qos2),
{ok, _} = emqx_client:publish(C, Topic, <<"qos 2">>, 2), {ok, _} = emqtt:publish(C, Topic, <<"qos 2">>, 2),
{ok, _} = emqx_client:publish(C, Topic, <<"qos 2">>, 2), {ok, _} = emqtt:publish(C, Topic, <<"qos 2">>, 2),
{ok, _} = emqx_client:publish(C, Topic, <<"qos 2">>, 2), {ok, _} = emqtt:publish(C, Topic, <<"qos 2">>, 2),
?assertEqual(3, length(recv_msgs(3))), ?assertEqual(3, length(recv_msgs(3))),
ok = emqx_client:disconnect(C). ok = emqtt:disconnect(C).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Helper functions %% Helper functions

View File

@ -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).

View File

@ -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.

54
test/emqx_ctl_SUITE.erl Normal file
View File

@ -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").

17
test/emqx_ctl_SUTIES.erl Normal file
View File

@ -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).

View File

@ -32,8 +32,8 @@ end_per_suite(_Config) ->
%% t_flapping(_Config) -> %% t_flapping(_Config) ->
%% process_flag(trap_exit, true), %% process_flag(trap_exit, true),
%% flapping_connect(5), %% flapping_connect(5),
%% {ok, C} = emqx_client:start_link([{client_id, <<"Client">>}]), %% {ok, C} = emqtt:start_link([{client_id, <<"Client">>}]),
%% {error, _} = emqx_client:connect(C), %% {error, _} = emqtt:connect(C),
%% receive %% receive
%% {'EXIT', Client, _Reason} -> %% {'EXIT', Client, _Reason} ->
%% ct:log("receive exit signal, Client: ~p", [Client]) %% 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)). lists:foreach(fun do_connect/1, lists:seq(1, Times)).
do_connect(_I) -> do_connect(_I) ->
{ok, C} = emqx_client:start_link([{client_id, <<"Client">>}]), {ok, C} = emqtt:start_link([{client_id, <<"Client">>}]),
{ok, _} = emqx_client:connect(C), {ok, _} = emqtt:connect(C),
ok = emqx_client:disconnect(C). ok = emqtt:disconnect(C).
prepare_for_test() -> prepare_for_test() ->
ok = emqx_zone:set_env(external, enable_flapping_detect, true), ok = emqx_zone:set_env(external, enable_flapping_detect, true),

View File

@ -19,19 +19,24 @@
-compile(export_all). -compile(export_all).
-compile(nowarn_export_all). -compile(nowarn_export_all).
-include_lib("eunit/include/eunit.hrl").
all() -> emqx_ct:all(?MODULE). all() -> emqx_ct:all(?MODULE).
t_keepalive(_) -> t_check(_) ->
{ok, KA} = emqx_keepalive:start(fun() -> {ok, 1} end, 1, {keepalive, timeout}), Keepalive = emqx_keepalive:init(60),
[resumed, timeout] = lists:reverse(keepalive_recv(KA, [])). ?assertEqual(60, emqx_keepalive:info(interval, Keepalive)),
?assertEqual(0, emqx_keepalive:info(statval, Keepalive)),
keepalive_recv(KA, Acc) -> ?assertEqual(0, emqx_keepalive:info(repeat, Keepalive)),
receive Info = emqx_keepalive:info(Keepalive),
{keepalive, timeout} -> ?assertEqual(#{interval => 60,
case emqx_keepalive:check(KA) of statval => 0,
{ok, KA1} -> keepalive_recv(KA1, [resumed | Acc]); repeat => 0}, Info),
{error, timeout} -> [timeout | Acc] {ok, Keepalive1} = emqx_keepalive:check(1, Keepalive),
end ?assertEqual(1, emqx_keepalive:info(statval, Keepalive1)),
after 4000 -> Acc ?assertEqual(0, emqx_keepalive:info(repeat, Keepalive1)),
end. {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)).

View File

@ -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.

View File

@ -55,26 +55,6 @@ t_timer_cancel_flush() ->
after 0 -> ok after 0 -> ok
end. 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(_) -> t_proc_name(_) ->
'TODO'. 'TODO'.

View File

@ -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([]).

View File

@ -14,32 +14,38 @@
%% limitations under the License. %% limitations under the License.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-module(emqx_net_SUITE). -module(emqx_mod_sup_SUITE).
%% CT
-compile(export_all). -compile(export_all).
-compile(nowarn_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(_) -> t_start(_) ->
{ok, KA} = emqx_keepalive:start(fun() -> {ok, 1} end, 1, {keepalive, timeout}), {ok, _} = emqx_mod_sup:start_link(),
[resumed, timeout] = lists:reverse(keepalive_recv(KA, [])). ?assertEqual([], supervisor:which_children(emqx_mod_sup)).
keepalive_recv(KA, Acc) -> t_start_child(_) ->
receive %% Set the emqx_mod_sup child with emqx_hooks for test
{keepalive, timeout} -> Mod = emqx_hooks,
case emqx_keepalive:check(KA) of Spec = #{id => Mod,
{ok, KA1} -> keepalive_recv(KA1, [resumed | Acc]); start => {Mod, start_link, []},
{error, timeout} -> [timeout | Acc] restart => permanent,
end shutdown => 5000,
after 4000 -> type => worker,
Acc modules => [Mod]},
end.
{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.

View File

@ -26,23 +26,17 @@ all() -> emqx_ct:all(?MODULE).
t_check_pub(_) -> t_check_pub(_) ->
PubCaps = #{max_qos_allowed => ?QOS_1, PubCaps = #{max_qos_allowed => ?QOS_1,
retain_available => false, retain_available => false
max_topic_alias => 4
}, },
ok = emqx_zone:set_env(zone, '$mqtt_pub_caps', PubCaps), ok = emqx_zone:set_env(zone, '$mqtt_pub_caps', PubCaps),
ok = emqx_mqtt_caps:check_pub(zone, #{qos => ?QOS_1, ok = emqx_mqtt_caps:check_pub(zone, #{qos => ?QOS_1,
retain => false, retain => false}),
topic_alias => 1
}),
PubFlags1 = #{qos => ?QOS_2, retain => false}, PubFlags1 = #{qos => ?QOS_2, retain => false},
?assertEqual({error, ?RC_QOS_NOT_SUPPORTED}, ?assertEqual({error, ?RC_QOS_NOT_SUPPORTED},
emqx_mqtt_caps:check_pub(zone, PubFlags1)), emqx_mqtt_caps:check_pub(zone, PubFlags1)),
PubFlags2 = #{qos => ?QOS_1, retain => true}, PubFlags2 = #{qos => ?QOS_1, retain => true},
?assertEqual({error, ?RC_RETAIN_NOT_SUPPORTED}, ?assertEqual({error, ?RC_RETAIN_NOT_SUPPORTED},
emqx_mqtt_caps:check_pub(zone, PubFlags2)), 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'). true = emqx_zone:unset_env(zone, '$mqtt_pub_caps').
t_check_sub(_) -> t_check_sub(_) ->

View File

@ -20,23 +20,61 @@
-compile(nowarn_export_all). -compile(nowarn_export_all).
-include("emqx_mqtt.hrl"). -include("emqx_mqtt.hrl").
-include_lib("eunit/include/eunit.hrl").
-include_lib("emqx_ct_helpers/include/emqx_ct.hrl").
all() -> emqx_ct:all(?MODULE). all() -> emqx_ct:all(?MODULE).
t_id(_) -> 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(_) -> 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(_) -> 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(_) -> 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(_) -> t_validate_value(_) ->
Props = emqx_mqtt_props:filter(?CONNECT, #{'Session-Expiry-Interval' => 1, 'Maximum-Packet-Size' => 255}), ok = emqx_mqtt_props:validate(#{'Correlation-Data' => <<"correlation-id">>}),
ok = emqx_mqtt_props:validate(Props), ok = emqx_mqtt_props:validate(#{'Reason-String' => <<"Unknown Reason">>}),
#{} = emqx_mqtt_props:filter(?CONNECT, #{'Maximum-QoS' => ?QOS_2}). 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())).

46
test/emqx_oom_SUITE.erl Normal file
View File

@ -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)).

View File

@ -27,9 +27,9 @@
all() -> emqx_ct:all(?MODULE). all() -> emqx_ct:all(?MODULE).
t_proto_name(_) -> t_proto_name(_) ->
?assertEqual(<<"MQIsdp">>, emqx_packet:protocol_name(3)), ?assertEqual(<<"MQIsdp">>, emqx_packet:proto_name(3)),
?assertEqual(<<"MQTT">>, emqx_packet:protocol_name(4)), ?assertEqual(<<"MQTT">>, emqx_packet:proto_name(4)),
?assertEqual(<<"MQTT">>, emqx_packet:protocol_name(5)). ?assertEqual(<<"MQTT">>, emqx_packet:proto_name(5)).
t_type_name(_) -> t_type_name(_) ->
?assertEqual('CONNECT', emqx_packet:type_name(?CONNECT)), ?assertEqual('CONNECT', emqx_packet:type_name(?CONNECT)),

View File

@ -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).

View File

@ -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

View File

@ -0,0 +1 @@
mini.name = test

View File

@ -0,0 +1,5 @@
%%-*- mode: erlang -*-
{mapping, "mini.name", "emqx_mini_plugin.name", [
{datatype, string}
]}.

View File

@ -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"}}}
]}
]}
]}.

View File

@ -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, []}
]}.

View File

@ -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}}.

View File

@ -19,30 +19,12 @@
-compile(export_all). -compile(export_all).
-compile(nowarn_export_all). -compile(nowarn_export_all).
-import(emqx_protocol,
[ handle_in/2
, handle_out/2
]).
-include("emqx.hrl").
-include("emqx_mqtt.hrl"). -include("emqx_mqtt.hrl").
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
all() -> emqx_ct:all(?MODULE). all() -> emqx_ct:all(?MODULE).
init_per_suite(Config) -> t_init_and_info(_) ->
emqx_ct_helpers:start_apps([]),
Config.
end_per_suite(_Config) ->
emqx_ct_helpers:stop_apps([]).
%%--------------------------------------------------------------------
%% Test cases for handle_in
%%--------------------------------------------------------------------
t_handle_connect(_) ->
ConnPkt = #mqtt_packet_connect{ ConnPkt = #mqtt_packet_connect{
proto_name = <<"MQTT">>, proto_name = <<"MQTT">>,
proto_ver = ?MQTT_PROTO_V4, proto_ver = ?MQTT_PROTO_V4,
@ -54,234 +36,14 @@ t_handle_connect(_) ->
username = <<"username">>, username = <<"username">>,
password = <<"passwd">> password = <<"passwd">>
}, },
with_proto( Proto = emqx_protocol:init(ConnPkt),
fun(PState) -> ?assertEqual(<<"MQTT">>, emqx_protocol:info(proto_name, Proto)),
{ok, ?CONNACK_PACKET(?RC_SUCCESS), PState1} ?assertEqual(?MQTT_PROTO_V4, emqx_protocol:info(proto_ver, Proto)),
= handle_in(?CONNECT_PACKET(ConnPkt), PState), ?assertEqual(true, emqx_protocol:info(clean_start, Proto)),
Client = emqx_protocol:info(client, PState1), ?assertEqual(<<"clientid">>, emqx_protocol:info(client_id, Proto)),
?assertEqual(<<"clientid">>, maps:get(client_id, Client)), ?assertEqual(<<"username">>, emqx_protocol:info(username, Proto)),
?assertEqual(<<"username">>, maps:get(username, Client)) ?assertEqual(undefined, emqx_protocol:info(will_msg, Proto)),
end). ?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)).

67
test/emqx_psk_SUITE.erl Normal file
View File

@ -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().

View File

@ -20,121 +20,126 @@
-compile(nowarn_export_all). -compile(nowarn_export_all).
-include("emqx_mqtt.hrl"). -include("emqx_mqtt.hrl").
-include_lib("proper/include/proper.hrl").
-include_lib("eunit/include/eunit.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). all() -> emqx_ct:all(?MODULE).
t_mqttv4_name(_) -> t_prop_name_text(_) ->
(((codes_test(?MQTT_PROTO_V4)) ?assert(proper:quickcheck(prop_name_text(), prop_name_text(opts))).
(seq(0,6)))
(?MQTTV4_CODE_NAMES))
(fun emqx_reason_codes:name/2).
t_mqttv5_name(_) -> t_prop_compat(_) ->
(((codes_test(?MQTT_PROTO_V5)) ?assert(proper:quickcheck(prop_compat(), prop_compat(opts))).
(?MQTTV5_CODES))
(?MQTTV5_CODE_NAMES))
(fun emqx_reason_codes:name/2).
t_text(_) -> t_prop_connack_error(_) ->
(((codes_test(?MQTT_PROTO_V5)) ?assert(proper:quickcheck(prop_connack_error(), default_opts([]))).
(?MQTTV5_CODES))
(?MQTTV5_TXT))
(fun emqx_reason_codes:text/1).
t_compat(_) -> prop_name_text(opts) ->
(((codes_test(connack)) default_opts([{numtests, 1000}]).
(?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).
codes_test(AsistVar) -> prop_name_text() ->
fun(CODES) -> ?FORALL(UnionArgs, union_args(),
fun(NAMES) -> is_atom(apply_fun(name, UnionArgs)) andalso
fun(Procedure) -> is_binary(apply_fun(text, UnionArgs))).
foreach(fun({Code, Result}) ->
?assertEqual(Result, case erlang:fun_info(Procedure, name) of prop_compat(opts) ->
{name, text} -> Procedure(Code); default_opts([{numtests, 512}]).
{name, name} -> Procedure(Code, AsistVar);
{name, compat} -> Procedure(AsistVar, Code) prop_compat() ->
end) ?FORALL(CompatArgs, compat_args(),
end, zip(CODES, NAMES)) begin
end Result = apply_fun(compat, CompatArgs),
end is_number(Result) orelse Result =:= undefined
end. 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]).

View File

@ -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.

View File

@ -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).

View File

@ -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.

124
test/emqx_rpc_SUITE.erl Normal file
View File

@ -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".

View File

@ -19,87 +19,314 @@
-compile(export_all). -compile(export_all).
-compile(nowarn_export_all). -compile(nowarn_export_all).
-include("emqx_mqtt.hrl").
-include_lib("proper/include/proper.hrl").
-include_lib("eunit/include/eunit.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). all() -> emqx_ct:all(?MODULE).
init_per_suite(Config) -> t_proper_session(_) ->
emqx_ct_helpers:start_apps([]), Opts = [{numtests, 1000}, {to_file, user}],
Config. ok = emqx_logger:set_log_level(emergency),
ok = before_proper(),
?assert(proper:quickcheck(prop_session(), Opts)),
ok = after_proper().
end_per_suite(_Config) -> before_proper() ->
emqx_ct_helpers:stop_apps([]). load(?mock_modules).
t_info(_) -> after_proper() ->
'TODO'. unload(?mock_modules),
emqx_logger:set_log_level(error).
t_attrs(_) -> prop_session() ->
'TODO'. ?FORALL({Session, OpList}, {session(), session_op_list()},
begin
try
apply_ops(Session, OpList),
true
after
true
end
end).
t_stats(_) -> %%%%%%%%%%%%%%%
'TODO'. %%% Helpers %%%
%%%%%%%%%%%%%%%
t_subscribe(_) -> apply_ops(Session, []) ->
'TODO'. ?assertEqual(session, element(1, Session));
apply_ops(Session, [Op | Rest]) ->
NSession = apply_op(Session, Op),
apply_ops(NSession, Rest).
t_unsubscribe(_) -> apply_op(Session, info) ->
'TODO'. 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(_) -> deliver_args() ->
'TODO'. list({deliver, topic(), message()}).
t_pubrec(_) -> info_args() ->
'TODO'. 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(_) -> sub_args() ->
'TODO'. ?LET({ClientId, TopicFilter, SubOpts},
{clientid(), topic(), sub_opts()},
{#{client_id => ClientId}, TopicFilter, SubOpts}).
t_pubcomp(_) -> unsub_args() ->
'TODO'. ?LET({ClientId, TopicFilter},
{clientid(), topic()},
{#{client_id => ClientId}, TopicFilter}).
t_deliver(_) -> publish_args() ->
'TODO'. ?LET({PacketId, Message},
{packetid(), message()},
{PacketId, Message}).
t_timeout(_) -> puback_args() ->
'TODO'. packetid().
ignore_loop(_Config) -> pubrec_args() ->
emqx_zone:set_env(external, ignore_loop_deliver, true), packetid().
{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).
session_all(_) -> pubrel_args() ->
emqx_zone:set_env(internal, idle_timeout, 1000), packetid().
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).
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).

View File

@ -150,24 +150,24 @@ t_not_so_sticky(_) ->
ok = ensure_config(sticky), ok = ensure_config(sticky),
ClientId1 = <<"ClientId1">>, ClientId1 = <<"ClientId1">>,
ClientId2 = <<"ClientId2">>, ClientId2 = <<"ClientId2">>,
{ok, C1} = emqx_client:start_link([{client_id, ClientId1}]), {ok, C1} = emqtt:start_link([{client_id, ClientId1}]),
{ok, _} = emqx_client:connect(C1), {ok, _} = emqtt:connect(C1),
{ok, C2} = emqx_client:start_link([{client_id, ClientId2}]), {ok, C2} = emqtt:start_link([{client_id, ClientId2}]),
{ok, _} = emqx_client:connect(C2), {ok, _} = emqtt:connect(C2),
emqx_client:subscribe(C1, {<<"$share/group1/foo/bar">>, 0}), emqtt:subscribe(C1, {<<"$share/group1/foo/bar">>, 0}),
timer:sleep(50), timer:sleep(50),
emqx_client:publish(C2, <<"foo/bar">>, <<"hello1">>), emqtt:publish(C2, <<"foo/bar">>, <<"hello1">>),
?assertMatch([#{payload := <<"hello1">>}], recv_msgs(1)), ?assertMatch([#{payload := <<"hello1">>}], recv_msgs(1)),
emqx_client:unsubscribe(C1, <<"$share/group1/foo/bar">>), emqtt:unsubscribe(C1, <<"$share/group1/foo/bar">>),
timer:sleep(50), timer:sleep(50),
emqx_client:subscribe(C1, {<<"$share/group1/foo/#">>, 0}), emqtt:subscribe(C1, {<<"$share/group1/foo/#">>, 0}),
timer:sleep(50), timer:sleep(50),
emqx_client:publish(C2, <<"foo/bar">>, <<"hello2">>), emqtt:publish(C2, <<"foo/bar">>, <<"hello2">>),
?assertMatch([#{payload := <<"hello2">>}], recv_msgs(1)), ?assertMatch([#{payload := <<"hello2">>}], recv_msgs(1)),
emqx_client:disconnect(C1), emqtt:disconnect(C1),
emqx_client:disconnect(C2), emqtt:disconnect(C2),
ok. ok.
test_two_messages(Strategy) -> test_two_messages(Strategy) ->
@ -178,15 +178,15 @@ test_two_messages(Strategy, WithAck) ->
Topic = <<"foo/bar">>, Topic = <<"foo/bar">>,
ClientId1 = <<"ClientId1">>, ClientId1 = <<"ClientId1">>,
ClientId2 = <<"ClientId2">>, ClientId2 = <<"ClientId2">>,
{ok, ConnPid1} = emqx_client:start_link([{client_id, ClientId1}]), {ok, ConnPid1} = emqtt:start_link([{client_id, ClientId1}]),
{ok, _} = emqx_client:connect(ConnPid1), {ok, _} = emqtt:connect(ConnPid1),
{ok, ConnPid2} = emqx_client:start_link([{client_id, ClientId2}]), {ok, ConnPid2} = emqtt:start_link([{client_id, ClientId2}]),
{ok, _} = emqx_client:connect(ConnPid2), {ok, _} = emqtt:connect(ConnPid2),
Message1 = emqx_message:make(ClientId1, 0, Topic, <<"hello1">>), Message1 = emqx_message:make(ClientId1, 0, Topic, <<"hello1">>),
Message2 = emqx_message:make(ClientId1, 0, Topic, <<"hello2">>), Message2 = emqx_message:make(ClientId1, 0, Topic, <<"hello2">>),
emqx_client:subscribe(ConnPid1, {<<"$share/group1/foo/bar">>, 0}), emqtt:subscribe(ConnPid1, {<<"$share/group1/foo/bar">>, 0}),
emqx_client:subscribe(ConnPid2, {<<"$share/group1/foo/bar">>, 0}), emqtt:subscribe(ConnPid2, {<<"$share/group1/foo/bar">>, 0}),
ct:sleep(100), ct:sleep(100),
emqx:publish(Message1), emqx:publish(Message1),
Me = self(), Me = self(),
@ -210,8 +210,8 @@ test_two_messages(Strategy, WithAck) ->
hash -> ?assert(UsedSubPid1 =:= UsedSubPid2); hash -> ?assert(UsedSubPid1 =:= UsedSubPid2);
_ -> ok _ -> ok
end, end,
emqx_client:stop(ConnPid1), emqtt:stop(ConnPid1),
emqx_client:stop(ConnPid2), emqtt:stop(ConnPid2),
ok. ok.
last_message(ExpectedPayload, Pids) -> last_message(ExpectedPayload, Pids) ->

136
test/emqx_sys_SUITE.erl Normal file
View File

@ -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.

View File

@ -55,9 +55,9 @@ t_sys_mon(_Config) ->
end, ?INPUTINFO). end, ?INPUTINFO).
validate_sys_mon_info(PidOrPort, SysMonName,ValidateInfo, InfoOrPort) -> validate_sys_mon_info(PidOrPort, SysMonName,ValidateInfo, InfoOrPort) ->
{ok, C} = emqx_client:start_link([{host, "localhost"}]), {ok, C} = emqtt:start_link([{host, "localhost"}]),
{ok, _} = emqx_client:connect(C), {ok, _} = emqtt:connect(C),
emqx_client:subscribe(C, emqx_topic:systop(lists:concat(['sysmon/', SysMonName])), qos1), emqtt:subscribe(C, emqx_topic:systop(lists:concat(['sysmon/', SysMonName])), qos1),
timer:sleep(100), timer:sleep(100),
?SYSMON ! {monitor, PidOrPort, SysMonName, InfoOrPort}, ?SYSMON ! {monitor, PidOrPort, SysMonName, InfoOrPort},
receive receive
@ -68,7 +68,7 @@ validate_sys_mon_info(PidOrPort, SysMonName,ValidateInfo, InfoOrPort) ->
1000 -> 1000 ->
ct:fail("flase") ct:fail("flase")
end, end,
emqx_client:stop(C). emqtt:stop(C).
concat_str(ValidateInfo, InfoOrPort, Info) -> concat_str(ValidateInfo, InfoOrPort, Info) ->
WarnInfo = io_lib:format(ValidateInfo, [InfoOrPort, Info]), WarnInfo = io_lib:format(ValidateInfo, [InfoOrPort, Info]),

View File

@ -33,11 +33,11 @@ end_per_suite(_Config) ->
emqx_ct_helpers:stop_apps([]). emqx_ct_helpers:stop_apps([]).
t_start_traces(_Config) -> t_start_traces(_Config) ->
{ok, T} = emqx_client:start_link([{host, "localhost"}, {ok, T} = emqtt:start_link([{host, "localhost"},
{client_id, <<"client">>}, {client_id, <<"client">>},
{username, <<"testuser">>}, {username, <<"testuser">>},
{password, <<"pass">>}]), {password, <<"pass">>}]),
emqx_client:connect(T), emqtt:connect(T),
%% Start tracing %% Start tracing
emqx_logger:set_log_level(error), emqx_logger:set_log_level(error),
@ -63,7 +63,7 @@ t_start_traces(_Config) ->
emqx_logger:set_log_level(debug), emqx_logger:set_log_level(debug),
%% Client with clientid = "client" publishes a "hi" message to "a/b/c". %% 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), ct:sleep(200),
%% Verify messages are logged to "tmp/client.log" and "tmp/topic_trace.log", but not "tmp/client2.log". %% 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, <<"client">>}),
ok = emqx_tracer:stop_trace({client_id, <<"client2">>}), ok = emqx_tracer:stop_trace({client_id, <<"client2">>}),
ok = emqx_tracer:stop_trace({topic, <<"a/#">>}), ok = emqx_tracer:stop_trace({topic, <<"a/#">>}),
emqx_client:disconnect(T), emqtt:disconnect(T),
emqx_logger:set_log_level(warning). emqx_logger:set_log_level(warning).

View File

@ -14,7 +14,7 @@
%% limitations under the License. %% limitations under the License.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-module(emqx_ws_channel_SUITE). -module(emqx_ws_connection_SUITE).
-compile(export_all). -compile(export_all).
-compile(nowarn_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),
{ok, _} = emqtt:publish(C, Topic, <<"qos 2">>, 2), {ok, _} = emqtt:publish(C, Topic, <<"qos 2">>, 2),
?assertEqual(3, length(recv_msgs(3))), ?assertEqual(3, length(recv_msgs(3))),
ok = emqx_client:disconnect(C). ok = emqtt:disconnect(C).
recv_msgs(Count) -> recv_msgs(Count) ->
recv_msgs(Count, []). recv_msgs(Count, []).

View File

@ -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.