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 eunit
- make ct
- make proper
- make cover
after_success:

View File

@ -15,11 +15,7 @@ RUN_NODE_NAME = emqxdebug@127.0.0.1
all: compile
.PHONY: tests
tests: eunit ct proper
.PHONY: proper
proper:
@rebar3 proper
tests: eunit ct
.PHONY: run
run: run_setup unlock
@ -99,7 +95,7 @@ ct: ct_setup
## e.g. make ct-one-suite suite=emqx_bridge
.PHONY: $(SUITES:%=ct-%)
$(CT_SUITES:%=ct-%): ct_setup
@rebar3 ct -v --readable=false --name $(CT_NODE_NAME) --suite=$(@:ct-%=%)_SUITE
@rebar3 ct -v --readable=false --name $(CT_NODE_NAME) --suite=$(@:ct-%=%)_SUITE --cover
.PHONY: app.config
app.config: $(CUTTLEFISH_SCRIPT) etc/gen.emqx.conf

View File

@ -1,10 +1,10 @@
# EMQ X Broker
[![GitHub Release](https://img.shields.io/github/release/emqx/emqx?color=brightgreen)](https://github.com/emqx/emqx/releases)
[![Build Status](https://travis-ci.org/emqx/emqx.svg)](https://travis-ci.org/emqx/emqx)
[![Coverage Status](https://coveralls.io/repos/github/emqx/emqx/badge.svg)](https://coveralls.io/github/emqx/emqx)
[![Docker Pulls](https://img.shields.io/docker/pulls/emqx/emqx)](https://hub.docker.com/r/emqx/emqx)
[![Slack](https://img.shields.io/badge/Slack-EMQ%20X-brightgreen?logo=slack&style=flat&color=7E4798)](https://emqx.slack.com)
[![GitHub Release](https://img.shields.io/github/release/emqx/emqx?color=brightgreen)](https://github.com/emqx/emqx/releases)
[![Build Status](https://travis-ci.org/emqx/emqx.svg)](https://travis-ci.org/emqx/emqx)
[![Coverage Status](https://coveralls.io/repos/github/emqx/emqx/badge.svg)](https://coveralls.io/github/emqx/emqx)
[![Docker Pulls](https://img.shields.io/docker/pulls/emqx/emqx)](https://hub.docker.com/r/emqx/emqx)
[![Slack Invite](<https://slack-invite.emqx.io/badge.svg>)](https://slack-invite.emqx.io)
[![Twitter](https://img.shields.io/badge/Twiiter-EMQ%20X-1DA1F2?logo=twitter)](https://twitter.com/emqtt)
[English](./README.md) | 简体中文
@ -22,8 +22,8 @@
获取适合你的二进制软件包,[点此下载](https://emqx.io/downloads)。
- [单节点安装](https://developer.emqx.io/docs/emq/v3/en/install.html)
- [集群安装](https://developer.emqx.io/docs/emq/v3/en/cluster.html)
- [单节点安装](https://docs.emqx.io/broker/v3/cn/install.html)
- [集群安装](https://docs.emqx.io/broker/v3/cn/cluster.html)
## 从源码构建
@ -54,7 +54,7 @@ cd _rel/emqx && ./bin/emqx console
## FAQ
访问 [FAQ](https://developer.emqx.io/docs/tutorial/zh/faq/faq.html) 以获取常见问题的帮助。
访问 [FAQ](https://docs.emqx.io/tutorial/v3/cn/faq/faq.html) 以获取常见问题的帮助。
## 产品路线
@ -84,4 +84,4 @@ cd _rel/emqx && ./bin/emqx console
## 开源许可
Apache License 2.0, 详见 [LICENSE](./LICENSE)。
Apache License 2.0, 详见 [LICENSE](./LICENSE)。

View File

@ -1,10 +1,10 @@
# EMQ X Broker
[![GitHub Release](https://img.shields.io/github/release/emqx/emqx?color=brightgreen)](https://github.com/emqx/emqx/releases)
[![Build Status](https://travis-ci.org/emqx/emqx.svg)](https://travis-ci.org/emqx/emqx)
[![Coverage Status](https://coveralls.io/repos/github/emqx/emqx/badge.svg)](https://coveralls.io/github/emqx/emqx)
[![Docker Pulls](https://img.shields.io/docker/pulls/emqx/emqx)](https://hub.docker.com/r/emqx/emqx)
[![Slack](https://img.shields.io/badge/Slack-EMQ%20X-brightgreen?logo=slack&style=flat&color=7E4798)](https://emqx.slack.com)
[![GitHub Release](https://img.shields.io/github/release/emqx/emqx?color=brightgreen)](https://github.com/emqx/emqx/releases)
[![Build Status](https://travis-ci.org/emqx/emqx.svg)](https://travis-ci.org/emqx/emqx)
[![Coverage Status](https://coveralls.io/repos/github/emqx/emqx/badge.svg)](https://coveralls.io/github/emqx/emqx)
[![Docker Pulls](https://img.shields.io/docker/pulls/emqx/emqx)](https://hub.docker.com/r/emqx/emqx)
[![Slack Invite](<https://slack-invite.emqx.io/badge.svg>)](https://slack-invite.emqx.io)
[![Twitter](https://img.shields.io/badge/Twiiter-EMQ%20X-1DA1F2?logo=twitter)](https://twitter.com/emqtt)
English | [简体中文](./README-CN.md)
@ -22,8 +22,8 @@ The *EMQ X* broker is cross-platform, which supports Linux, Unix, Mac OS and Win
Download the binary package for your platform from [here](https://emqx.io/downloads).
- [Single Node Install](https://developer.emqx.io/docs/emq/v3/en/install.html)
- [Multi Node Install](https://developer.emqx.io/docs/emq/v3/en/cluster.html)
- [Single Node Install](https://docs.emqx.io/broker/v3/en/install.html)
- [Multi Node Install](https://docs.emqx.io/broker/v3/en/cluster.html)
## Build From Source
@ -56,7 +56,7 @@ To view the dashboard after running, use your browser to open: http://localhost:
## FAQ
Visiting [FAQ](https://developer.emqx.io/docs/tutorial/en/faq/faq.html) to get help of common problems.
Visiting [FAQ](https://docs.emqx.io/tutorial/v3/en/faq/faq.html) to get help of common problems.
## Roadmap
@ -85,4 +85,4 @@ You can read the mqtt protocol via the following links:
## License
Apache License 2.0, see [LICENSE](./LICENSE).
Apache License 2.0, see [LICENSE](./LICENSE).

View File

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

View File

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

View File

@ -206,7 +206,7 @@ publish(Msg) when is_record(Msg, message) ->
end.
%% Called internally
-spec(safe_publish(emqx_types:message()) -> ok).
-spec(safe_publish(emqx_types:message()) -> ok | emqx_types:publish_result()).
safe_publish(Msg) when is_record(Msg, message) ->
try
publish(Msg)

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

View File

@ -14,33 +14,12 @@
%% limitations under the License.
%%--------------------------------------------------------------------
%% @doc Hot Configuration
%%
%% TODO: How to persist the configuration?
%%
%% 1. Store in mnesia database?
%% 2. Store in dets?
%% 3. Store in data/app.config?
-module(emqx_config).
-export([populate/1]).
-export([ read/1
, write/2
, dump/2
, reload/1
]).
-export([ set/3
, get/2
, get/3
, get_env/1
-export([ get_env/1
, get_env/2
]).
-type(env() :: {atom(), term()}).
-define(APP, emqx).
%% @doc Get environment
@ -51,91 +30,3 @@ get_env(Key) ->
-spec(get_env(Key :: atom(), Default :: term()) -> term()).
get_env(Key, Default) ->
application:get_env(?APP, Key, Default).
%% TODO:
populate(_App) ->
ok.
%% @doc Read the configuration of an application.
-spec(read(atom()) -> {ok, list(env())} | {error, term()}).
read(App) ->
%% TODO:
%% 1. Read the app.conf from etc folder
%% 2. Cuttlefish to read the conf
%% 3. Return the terms and schema
% {error, unsupported}.
{ok, read_(App)}.
%% @doc Reload configuration of an application.
-spec(reload(atom()) -> ok | {error, term()}).
reload(_App) ->
%% TODO
%% 1. Read the app.conf from etc folder
%% 2. Cuttlefish to generate config terms.
%% 3. set/3 to apply the config
ok.
-spec(write(atom(), list(env())) -> ok | {error, term()}).
write(_App, _Terms) -> ok.
% Configs = lists:map(fun({Key, Val}) ->
% {cuttlefish_variable:tokenize(binary_to_list(Key)), binary_to_list(Val)}
% end, Terms),
% Path = lists:concat([code:priv_dir(App), "/", App, ".schema"]),
% Schema = cuttlefish_schema:files([Path]),
% case cuttlefish_generator:map(Schema, Configs) of
% [{App, Configs1}] ->
% emqx_cli_config:write_config(App, Configs),
% lists:foreach(fun({Key, Val}) -> application:set_env(App, Key, Val) end, Configs1);
% _ ->
% error
% end.
-spec(dump(atom(), list(env())) -> ok | {error, term()}).
dump(_App, _Terms) ->
%% TODO
ok.
-spec(set(atom(), list(), list()) -> ok).
set(_App, _Par, _Val) -> ok.
% emqx_cli_config:run(["config",
% "set",
% lists:concat([Par, "=", Val]),
% lists:concat(["--app=", App])]).
-spec(get(atom(), list()) -> undefined | {ok, term()}).
get(_App, _Par) -> error(no_impl).
% case emqx_cli_config:get_cfg(App, Par) of
% undefined -> undefined;
% Val -> {ok, Val}
% end.
-spec(get(atom(), list(), atom()) -> term()).
get(_App, _Par, _Def) -> error(no_impl).
% emqx_cli_config:get_cfg(App, Par, Def).
read_(_App) -> error(no_impl).
% Configs = emqx_cli_config:read_config(App),
% Path = lists:concat([code:priv_dir(App), "/", App, ".schema"]),
% case filelib:is_file(Path) of
% false ->
% [];
% true ->
% {_, Mappings, _} = cuttlefish_schema:files([Path]),
% OptionalCfg = lists:foldl(fun(Map, Acc) ->
% Key = cuttlefish_mapping:variable(Map),
% case proplists:get_value(Key, Configs) of
% undefined ->
% [{cuttlefish_variable:format(Key), "", cuttlefish_mapping:doc(Map), false} | Acc];
% _ -> Acc
% end
% end, [], Mappings),
% RequiredCfg = lists:foldl(fun({Key, Val}, Acc) ->
% case lists:keyfind(Key, 2, Mappings) of
% false -> Acc;
% Map ->
% [{cuttlefish_variable:format(Key), Val, cuttlefish_mapping:doc(Map), true} | Acc]
% end
% end, [], Configs),
% RequiredCfg ++ OptionalCfg
% end.

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

View File

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

View File

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

View File

@ -16,7 +16,11 @@
-module(emqx_misc).
-include("types.hrl").
-export([ merge_opts/2
, maybe_apply/2
, run_fold/3
, start_timer/2
, start_timer/3
, cancel_timer/1
@ -25,11 +29,8 @@
, proc_stats/1
]).
-export([ init_proc_mng_policy/1
, conn_proc_mng_policy/1
]).
-export([ drain_deliver/1
-export([ drain_deliver/0
, drain_deliver/1
, drain_down/1
]).
@ -48,6 +49,19 @@ merge_opts(Defaults, Options) ->
lists:usort([Opt | Acc])
end, Defaults, Options).
%% @doc Apply a function to a maybe argument.
-spec(maybe_apply(fun((maybe(A)) -> maybe(A)), maybe(A))
-> maybe(A) when A :: any()).
maybe_apply(_Fun, undefined) ->
undefined;
maybe_apply(Fun, Arg) when is_function(Fun) ->
erlang:apply(Fun, [Arg]).
run_fold([], Acc, _State) ->
Acc;
run_fold([Fun|More], Acc, State) ->
run_fold(More, Fun(Acc, State), State).
-spec(start_timer(integer(), term()) -> reference()).
start_timer(Interval, Msg) ->
start_timer(Interval, self(), Msg).
@ -56,7 +70,7 @@ start_timer(Interval, Msg) ->
start_timer(Interval, Dest, Msg) ->
erlang:start_timer(Interval, Dest, Msg).
-spec(cancel_timer(undefined | reference()) -> ok).
-spec(cancel_timer(maybe(reference())) -> ok).
cancel_timer(Timer) when is_reference(Timer) ->
case erlang:cancel_timer(Timer) of
false ->
@ -82,57 +96,10 @@ proc_stats(Pid) ->
[{mailbox_len, Len}|Stats]
end.
-define(DISABLED, 0).
init_proc_mng_policy(undefined) -> ok;
init_proc_mng_policy(Zone) ->
#{max_heap_size := MaxHeapSizeInBytes}
= ShutdownPolicy
= emqx_zone:get_env(Zone, force_shutdown_policy),
MaxHeapSize = MaxHeapSizeInBytes div erlang:system_info(wordsize),
_ = erlang:process_flag(max_heap_size, MaxHeapSize), % zero is discarded
erlang:put(force_shutdown_policy, ShutdownPolicy),
ok.
%% @doc Check self() process status against connection/session process management policy,
%% return `continue | hibernate | {shutdown, Reason}' accordingly.
%% `continue': There is nothing out of the ordinary.
%% `hibernate': Nothing to process in my mailbox, and since this check is triggered
%% by a timer, we assume it is a fat chance to continue idel, hence hibernate.
%% `shutdown': Some numbers (message queue length hit the limit),
%% hence shutdown for greater good (system stability).
-spec(conn_proc_mng_policy(#{message_queue_len => integer()} | false) ->
continue | hibernate | {shutdown, _}).
conn_proc_mng_policy(#{message_queue_len := MaxMsgQueueLen}) ->
Qlength = proc_info(message_queue_len),
Checks =
[{fun() -> is_message_queue_too_long(Qlength, MaxMsgQueueLen) end,
{shutdown, message_queue_too_long}},
{fun() -> Qlength > 0 end, continue},
{fun() -> true end, hibernate}
],
check(Checks);
conn_proc_mng_policy(_) ->
%% disable by default
conn_proc_mng_policy(#{message_queue_len => 0}).
check([{Pred, Result} | Rest]) ->
case Pred() of
true -> Result;
false -> check(Rest)
end.
is_message_queue_too_long(Qlength, Max) ->
is_enabled(Max) andalso Qlength > Max.
is_enabled(Max) ->
is_integer(Max) andalso Max > ?DISABLED.
proc_info(Key) ->
{Key, Value} = erlang:process_info(self(), Key),
Value.
%% @doc Drain delivers from the channel's mailbox.
drain_deliver() ->
drain_deliver([]).
drain_deliver(Acc) ->
receive
Deliver = {deliver, _Topic, _Msg} ->

View File

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

View File

@ -40,7 +40,7 @@
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
start_child(ChildSpec) when is_tuple(ChildSpec) ->
start_child(ChildSpec) when is_map(ChildSpec) ->
supervisor:start_child(?MODULE, ChildSpec).
start_child(Mod, Type) when is_atom(Mod) andalso is_atom(Type) ->

View File

@ -79,10 +79,6 @@ do_check_pub(#{qos := QoS}, #{max_qos_allowed := MaxQoS})
{error, ?RC_QOS_NOT_SUPPORTED};
do_check_pub(#{retain := true}, #{retain_available := false}) ->
{error, ?RC_RETAIN_NOT_SUPPORTED};
do_check_pub(#{topic_alias := TopicAlias},
#{max_topic_alias := MaxTopicAlias})
when 0 == TopicAlias; TopicAlias >= MaxTopicAlias ->
{error, ?RC_TOPIC_ALIAS_INVALID};
do_check_pub(_Flags, _Caps) -> ok.
-spec(check_sub(emqx_types:zone(),

View File

@ -25,6 +25,12 @@
, validate/1
]).
%% For tests
-export([all/0]).
-type(prop_name() :: atom()).
-type(prop_id() :: pos_integer()).
-define(PROPS_TABLE,
#{16#01 => {'Payload-Format-Indicator', 'Byte', [?PUBLISH]},
16#02 => {'Message-Expiry-Interval', 'Four-Byte-Integer', [?PUBLISH]},
@ -42,7 +48,9 @@
16#19 => {'Request-Response-Information', 'Byte', [?CONNECT]},
16#1A => {'Response-Information', 'UTF8-Encoded-String', [?CONNACK]},
16#1C => {'Server-Reference', 'UTF8-Encoded-String', [?CONNACK, ?DISCONNECT]},
16#1F => {'Reason-String', 'UTF8-Encoded-String', 'ALL'},
16#1F => {'Reason-String', 'UTF8-Encoded-String', [?CONNACK, ?DISCONNECT, ?PUBACK,
?PUBREC, ?PUBREL, ?PUBCOMP,
?SUBACK, ?UNSUBACK, ?AUTH]},
16#21 => {'Receive-Maximum', 'Two-Byte-Integer', [?CONNECT, ?CONNACK]},
16#22 => {'Topic-Alias-Maximum', 'Two-Byte-Integer', [?CONNECT, ?CONNACK]},
16#23 => {'Topic-Alias', 'Two-Byte-Integer', [?PUBLISH]},
@ -52,36 +60,10 @@
16#27 => {'Maximum-Packet-Size', 'Four-Byte-Integer', [?CONNECT, ?CONNACK]},
16#28 => {'Wildcard-Subscription-Available', 'Byte', [?CONNACK]},
16#29 => {'Subscription-Identifier-Available', 'Byte', [?CONNACK]},
16#2A => {'Shared-Subscription-Available', 'Byte', [?CONNACK]}}).
name(16#01) -> 'Payload-Format-Indicator';
name(16#02) -> 'Message-Expiry-Interval';
name(16#03) -> 'Content-Type';
name(16#08) -> 'Response-Topic';
name(16#09) -> 'Correlation-Data';
name(16#0B) -> 'Subscription-Identifier';
name(16#11) -> 'Session-Expiry-Interval';
name(16#12) -> 'Assigned-Client-Identifier';
name(16#13) -> 'Server-Keep-Alive';
name(16#15) -> 'Authentication-Method';
name(16#16) -> 'Authentication-Data';
name(16#17) -> 'Request-Problem-Information';
name(16#18) -> 'Will-Delay-Interval';
name(16#19) -> 'Request-Response-Information';
name(16#1A) -> 'Response-Information';
name(16#1C) -> 'Server-Reference';
name(16#1F) -> 'Reason-String';
name(16#21) -> 'Receive-Maximum';
name(16#22) -> 'Topic-Alias-Maximum';
name(16#23) -> 'Topic-Alias';
name(16#24) -> 'Maximum-QoS';
name(16#25) -> 'Retain-Available';
name(16#26) -> 'User-Property';
name(16#27) -> 'Maximum-Packet-Size';
name(16#28) -> 'Wildcard-Subscription-Available';
name(16#29) -> 'Subscription-Identifier-Available';
name(16#2A) -> 'Shared-Subscription-Available'.
16#2A => {'Shared-Subscription-Available', 'Byte', [?CONNACK]}
}).
-spec(id(prop_name()) -> prop_id()).
id('Payload-Format-Indicator') -> 16#01;
id('Message-Expiry-Interval') -> 16#02;
id('Content-Type') -> 16#03;
@ -108,12 +90,47 @@ id('User-Property') -> 16#26;
id('Maximum-Packet-Size') -> 16#27;
id('Wildcard-Subscription-Available') -> 16#28;
id('Subscription-Identifier-Available') -> 16#29;
id('Shared-Subscription-Available') -> 16#2A.
id('Shared-Subscription-Available') -> 16#2A;
id(Name) -> error({bad_property, Name}).
-spec(name(prop_id()) -> prop_name()).
name(16#01) -> 'Payload-Format-Indicator';
name(16#02) -> 'Message-Expiry-Interval';
name(16#03) -> 'Content-Type';
name(16#08) -> 'Response-Topic';
name(16#09) -> 'Correlation-Data';
name(16#0B) -> 'Subscription-Identifier';
name(16#11) -> 'Session-Expiry-Interval';
name(16#12) -> 'Assigned-Client-Identifier';
name(16#13) -> 'Server-Keep-Alive';
name(16#15) -> 'Authentication-Method';
name(16#16) -> 'Authentication-Data';
name(16#17) -> 'Request-Problem-Information';
name(16#18) -> 'Will-Delay-Interval';
name(16#19) -> 'Request-Response-Information';
name(16#1A) -> 'Response-Information';
name(16#1C) -> 'Server-Reference';
name(16#1F) -> 'Reason-String';
name(16#21) -> 'Receive-Maximum';
name(16#22) -> 'Topic-Alias-Maximum';
name(16#23) -> 'Topic-Alias';
name(16#24) -> 'Maximum-QoS';
name(16#25) -> 'Retain-Available';
name(16#26) -> 'User-Property';
name(16#27) -> 'Maximum-Packet-Size';
name(16#28) -> 'Wildcard-Subscription-Available';
name(16#29) -> 'Subscription-Identifier-Available';
name(16#2A) -> 'Shared-Subscription-Available';
name(Id) -> error({unsupported_property, Id}).
-spec(filter(emqx_types:packet_type(), emqx_types:properties()|list())
-> emqx_types:properties()).
filter(PacketType, Props) when is_map(Props) ->
maps:from_list(filter(PacketType, maps:to_list(Props)));
filter(PacketType, Props) when ?CONNECT =< PacketType, PacketType =< ?AUTH, is_list(Props) ->
filter(PacketType, Props) when ?CONNECT =< PacketType,
PacketType =< ?AUTH,
is_list(Props) ->
Filter = fun(Name) ->
case maps:find(id(Name), ?PROPS_TABLE) of
{ok, {Name, _Type, 'ALL'}} ->
@ -125,6 +142,7 @@ filter(PacketType, Props) when ?CONNECT =< PacketType, PacketType =< ?AUTH, is_l
end,
[Prop || Prop = {Name, _} <- Props, Filter(Name)].
-spec(validate(emqx_types:properties()) -> ok).
validate(Props) when is_map(Props) ->
lists:foreach(fun validate_prop/1, maps:to_list(Props)).
@ -132,23 +150,32 @@ validate_prop(Prop = {Name, Val}) ->
case maps:find(id(Name), ?PROPS_TABLE) of
{ok, {Name, Type, _}} ->
validate_value(Type, Val)
orelse error(bad_property, Prop);
orelse error({bad_property_value, Prop});
error ->
error({bad_property, Prop})
error({bad_property, Name})
end.
validate_value('Byte', Val) ->
is_integer(Val);
is_integer(Val) andalso Val =< 16#FF;
validate_value('Two-Byte-Integer', Val) ->
is_integer(Val);
is_integer(Val) andalso 0 =< Val andalso Val =< 16#FFFF;
validate_value('Four-Byte-Integer', Val) ->
is_integer(Val);
is_integer(Val) andalso 0 =< Val andalso Val =< 16#FFFFFFFF;
validate_value('Variable-Byte-Integer', Val) ->
is_integer(Val);
is_integer(Val) andalso 0 =< Val andalso Val =< 16#7FFFFFFF;
validate_value('UTF8-String-Pair', {Name, Val}) ->
validate_value('UTF8-Encoded-String', Name)
andalso validate_value('UTF8-Encoded-String', Val);
validate_value('UTF8-String-Pair', Pairs) when is_list(Pairs) ->
lists:foldl(fun(Pair, OK) ->
OK andalso validate_value('UTF8-String-Pair', Pair)
end, true, Pairs);
validate_value('UTF8-Encoded-String', Val) ->
is_binary(Val);
validate_value('Binary-Data', Val) ->
is_binary(Val);
validate_value('UTF8-String-Pair', Val) ->
is_tuple(Val) orelse is_list(Val).
validate_value(_Type, _Val) -> false.
-spec(all() -> map()).
all() -> ?PROPS_TABLE.

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -22,6 +22,7 @@
-export([ name/1
, name/2
, text/1
, text/2
, connack_error/1
, puback/1
]).
@ -30,7 +31,7 @@
name(I, Ver) when Ver >= ?MQTT_PROTO_V5 ->
name(I);
name(0, _Ver) -> connection_acceptd;
name(0, _Ver) -> connection_accepted;
name(1, _Ver) -> unacceptable_protocol_version;
name(2, _Ver) -> client_identifier_not_valid;
name(3, _Ver) -> server_unavaliable;
@ -83,6 +84,16 @@ name(16#A1) -> subscription_identifiers_not_supported;
name(16#A2) -> wildcard_subscriptions_not_supported;
name(_Code) -> unknown_error.
text(I, Ver) when Ver >= ?MQTT_PROTO_V5 ->
text(I);
text(0, _Ver) -> <<"Connection accepted">>;
text(1, _Ver) -> <<"unacceptable_protocol_version">>;
text(2, _Ver) -> <<"client_identifier_not_valid">>;
text(3, _Ver) -> <<"server_unavaliable">>;
text(4, _Ver) -> <<"malformed_username_or_password">>;
text(5, _Ver) -> <<"unauthorized_client">>;
text(_, _Ver) -> <<"unknown_error">>.
text(16#00) -> <<"Success">>;
text(16#01) -> <<"Granted QoS 1">>;
text(16#02) -> <<"Granted QoS 2">>;
@ -150,7 +161,8 @@ compat(connack, 16#9F) -> ?CONNACK_SERVER;
compat(suback, Code) when Code =< ?QOS_2 -> Code;
compat(suback, Code) when Code >= 16#80 -> 16#80;
compat(unsuback, _Code) -> undefined.
compat(unsuback, _Code) -> undefined;
compat(_Other, _Code) -> undefined.
connack_error(client_identifier_not_valid) -> ?RC_CLIENT_IDENTIFIER_NOT_VALID;
connack_error(bad_username_or_password) -> ?RC_BAD_USER_NAME_OR_PASSWORD;
@ -167,4 +179,3 @@ connack_error(_) -> ?RC_NOT_AUTHORIZED.
%%TODO: This function should be removed.
puback([]) -> ?RC_NO_MATCHING_SUBSCRIBERS;
puback(L) when is_list(L) -> ?RC_SUCCESS.

View File

@ -29,6 +29,8 @@
-define(RPC, gen_rpc).
-define(DefaultClientNum, 1).
call(Node, Mod, Fun, Args) ->
filter_result(?RPC:call(rpc_node(Node), Mod, Fun, Args)).
@ -39,7 +41,7 @@ cast(Node, Mod, Fun, Args) ->
filter_result(?RPC:cast(rpc_node(Node), Mod, Fun, Args)).
rpc_node(Node) ->
{ok, ClientNum} = application:get_env(gen_rpc, tcp_client_num),
ClientNum = application:get_env(gen_rpc, tcp_client_num, ?DefaultClientNum),
{Node, rand:uniform(ClientNum)}.
rpc_nodes(Nodes) ->
@ -55,4 +57,3 @@ filter_result({Error, Reason})
{badrpc, Reason};
filter_result(Delivery) ->
Delivery.

View File

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

View File

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

View File

@ -57,6 +57,10 @@ start_link(Opts) ->
init([Opts]) ->
erlang:system_monitor(self(), parse_opt(Opts)),
emqx_logger:set_proc_metadata(#{sysmon => true}),
%% Monitor cluster partition event
ekka:monitor(partition, fun handle_partition_event/1),
{ok, start_timer(#{timer => undefined, events => []})}.
start_timer(State) ->
@ -156,6 +160,15 @@ terminate(_Reason, #{timer := TRef}) ->
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%--------------------------------------------------------------------
%% Internal Functions
%%--------------------------------------------------------------------
handle_partition_event({partition, {occurred, Node}}) ->
alarm_handler:set_alarm({partitioned, Node});
handle_partition_event({partition, {healed, Node}}) ->
alarm_handler:clear_alarm(partitioned).
suppress(Key, SuccFun, State = #{events := Events}) ->
case lists:member(Key, Events) of
true -> {noreply, State};

View File

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

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

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

View File

@ -19,6 +19,13 @@
-compile(export_all).
-compile(nowarn_export_all).
-import(emqx_channel,
[ handle_in/2
, handle_out/2
]).
-include("emqx.hrl").
-include("emqx_mqtt.hrl").
-include_lib("eunit/include/eunit.hrl").
all() -> emqx_ct:all(?MODULE).
@ -30,28 +37,259 @@ init_per_suite(Config) ->
end_per_suite(_Config) ->
emqx_ct_helpers:stop_apps([]).
t_basic(_) ->
Topic = <<"TopicA">>,
{ok, C} = emqtt:start_link([{port, 1883}, {client_id, <<"hello">>}]),
{ok, _} = emqtt:connect(C),
{ok, _, [1]} = emqtt:subscribe(C, Topic, qos1),
{ok, _, [2]} = emqtt:subscribe(C, Topic, qos2),
{ok, _} = emqtt:publish(C, Topic, <<"qos 2">>, 2),
{ok, _} = emqtt:publish(C, Topic, <<"qos 2">>, 2),
{ok, _} = emqtt:publish(C, Topic, <<"qos 2">>, 2),
?assertEqual(3, length(recv_msgs(3))),
ok = emqtt:disconnect(C).
%%--------------------------------------------------------------------
%% Test cases for handle_in
%%--------------------------------------------------------------------
recv_msgs(Count) ->
recv_msgs(Count, []).
t_handle_connect(_) ->
ConnPkt = #mqtt_packet_connect{
proto_name = <<"MQTT">>,
proto_ver = ?MQTT_PROTO_V4,
is_bridge = false,
clean_start = true,
keepalive = 30,
properties = #{},
client_id = <<"clientid">>,
username = <<"username">>,
password = <<"passwd">>
},
with_channel(
fun(Channel) ->
{ok, ?CONNACK_PACKET(?RC_SUCCESS), Channel1}
= handle_in(?CONNECT_PACKET(ConnPkt), Channel),
#{client_id := ClientId, username := Username}
= emqx_channel:info(client, Channel1),
?assertEqual(<<"clientid">>, ClientId),
?assertEqual(<<"username">>, Username)
end).
recv_msgs(0, Msgs) ->
Msgs;
recv_msgs(Count, Msgs) ->
receive
{publish, Msg} ->
recv_msgs(Count-1, [Msg|Msgs])
after 100 ->
Msgs
end.
t_handle_publish_qos0(_) ->
with_channel(
fun(Channel) ->
Publish = ?PUBLISH_PACKET(?QOS_0, <<"topic">>, undefined, <<"payload">>),
{ok, Channel} = handle_in(Publish, Channel)
end).
t_handle_publish_qos1(_) ->
with_channel(
fun(Channel) ->
Publish = ?PUBLISH_PACKET(?QOS_1, <<"topic">>, 1, <<"payload">>),
{ok, ?PUBACK_PACKET(1, RC), _} = handle_in(Publish, Channel),
?assert((RC == ?RC_SUCCESS) orelse (RC == ?RC_NO_MATCHING_SUBSCRIBERS))
end).
t_handle_publish_qos2(_) ->
with_channel(
fun(Channel) ->
Publish1 = ?PUBLISH_PACKET(?QOS_2, <<"topic">>, 1, <<"payload">>),
{ok, ?PUBREC_PACKET(1, RC), Channel1} = handle_in(Publish1, Channel),
Publish2 = ?PUBLISH_PACKET(?QOS_2, <<"topic">>, 2, <<"payload">>),
{ok, ?PUBREC_PACKET(2, RC), Channel2} = handle_in(Publish2, Channel1),
?assert((RC == ?RC_SUCCESS) orelse (RC == ?RC_NO_MATCHING_SUBSCRIBERS)),
#{awaiting_rel := AwaitingRel} = emqx_channel:info(session, Channel2),
?assertEqual(2, AwaitingRel)
end).
t_handle_puback(_) ->
with_channel(
fun(Channel) ->
{ok, Channel} = handle_in(?PUBACK_PACKET(1, ?RC_SUCCESS), Channel)
end).
t_handle_pubrec(_) ->
with_channel(
fun(Channel) ->
{ok, ?PUBREL_PACKET(1, ?RC_PACKET_IDENTIFIER_NOT_FOUND), Channel}
= handle_in(?PUBREC_PACKET(1, ?RC_SUCCESS), Channel)
end).
t_handle_pubrel(_) ->
with_channel(
fun(Channel) ->
{ok, ?PUBCOMP_PACKET(1, ?RC_PACKET_IDENTIFIER_NOT_FOUND), Channel}
= handle_in(?PUBREL_PACKET(1, ?RC_SUCCESS), Channel)
end).
t_handle_pubcomp(_) ->
with_channel(
fun(Channel) ->
{ok, Channel} = handle_in(?PUBCOMP_PACKET(1, ?RC_SUCCESS), Channel)
end).
t_handle_subscribe(_) ->
with_channel(
fun(Channel) ->
TopicFilters = [{<<"+">>, ?DEFAULT_SUBOPTS}],
{ok, ?SUBACK_PACKET(10, [?QOS_0]), Channel1}
= handle_in(?SUBSCRIBE_PACKET(10, #{}, TopicFilters), Channel),
#{subscriptions := Subscriptions}
= emqx_channel:info(session, Channel1),
?assertEqual(maps:from_list(TopicFilters), Subscriptions)
end).
t_handle_unsubscribe(_) ->
with_channel(
fun(Channel) ->
{ok, ?UNSUBACK_PACKET(11), Channel}
= handle_in(?UNSUBSCRIBE_PACKET(11, #{}, [<<"+">>]), Channel)
end).
t_handle_pingreq(_) ->
with_channel(
fun(Channel) ->
{ok, ?PACKET(?PINGRESP), Channel} = handle_in(?PACKET(?PINGREQ), Channel)
end).
t_handle_disconnect(_) ->
with_channel(
fun(Channel) ->
{stop, {shutdown, normal}, Channel1} = handle_in(?DISCONNECT_PACKET(?RC_SUCCESS), Channel),
?assertMatch(#{will_msg := undefined}, emqx_channel:info(protocol, Channel1))
end).
t_handle_auth(_) ->
with_channel(
fun(Channel) ->
{ok, Channel} = handle_in(?AUTH_PACKET(), Channel)
end).
%%--------------------------------------------------------------------
%% Test cases for handle_deliver
%%--------------------------------------------------------------------
t_handle_deliver(_) ->
with_channel(
fun(Channel) ->
TopicFilters = [{<<"+">>, ?DEFAULT_SUBOPTS#{qos => ?QOS_2}}],
{ok, ?SUBACK_PACKET(1, [?QOS_2]), Channel1}
= handle_in(?SUBSCRIBE_PACKET(1, #{}, TopicFilters), Channel),
Msg0 = emqx_message:make(<<"clientx">>, ?QOS_0, <<"t0">>, <<"qos0">>),
Msg1 = emqx_message:make(<<"clientx">>, ?QOS_1, <<"t1">>, <<"qos1">>),
Delivers = [{deliver, <<"+">>, Msg0}, {deliver, <<"+">>, Msg1}],
{ok, _Ch} = emqx_channel:handle_out({deliver, Delivers}, Channel1)
end).
%%--------------------------------------------------------------------
%% Test cases for handle_out
%%--------------------------------------------------------------------
t_handle_connack(_) ->
with_channel(
fun(Channel) ->
{ok, ?CONNACK_PACKET(?RC_SUCCESS, SP, _), _}
= handle_out({connack, ?RC_SUCCESS, 0}, Channel),
{stop, {shutdown, unauthorized_client}, ?CONNACK_PACKET(5), _}
= handle_out({connack, ?RC_NOT_AUTHORIZED}, Channel)
end).
t_handle_out_publish(_) ->
with_channel(
fun(Channel) ->
Pub0 = {publish, undefined, emqx_message:make(<<"t">>, <<"qos0">>)},
Pub1 = {publish, 1, emqx_message:make(<<"c">>, ?QOS_1, <<"t">>, <<"qos1">>)},
{ok, ?PUBLISH_PACKET(?QOS_0), Channel} = handle_out(Pub0, Channel),
{ok, ?PUBLISH_PACKET(?QOS_1), Channel} = handle_out(Pub1, Channel),
{ok, Packets, Channel} = handle_out({publish, [Pub0, Pub1]}, Channel),
?assertEqual(2, length(Packets))
end).
t_handle_out_puback(_) ->
with_channel(
fun(Channel) ->
{ok, Channel} = handle_out({puberr, ?RC_NOT_AUTHORIZED}, Channel),
{ok, ?PUBACK_PACKET(1, ?RC_SUCCESS), Channel}
= handle_out({puback, 1, ?RC_SUCCESS}, Channel)
end).
t_handle_out_pubrec(_) ->
with_channel(
fun(Channel) ->
{ok, ?PUBREC_PACKET(4, ?RC_SUCCESS), Channel}
= handle_out({pubrec, 4, ?RC_SUCCESS}, Channel)
end).
t_handle_out_pubrel(_) ->
with_channel(
fun(Channel) ->
{ok, ?PUBREL_PACKET(2), Channel}
= handle_out({pubrel, 2, ?RC_SUCCESS}, Channel),
{ok, ?PUBREL_PACKET(3, ?RC_SUCCESS), Channel}
= handle_out({pubrel, 3, ?RC_SUCCESS}, Channel)
end).
t_handle_out_pubcomp(_) ->
with_channel(
fun(Channel) ->
{ok, ?PUBCOMP_PACKET(5, ?RC_SUCCESS), Channel}
= handle_out({pubcomp, 5, ?RC_SUCCESS}, Channel)
end).
t_handle_out_suback(_) ->
with_channel(
fun(Channel) ->
{ok, ?SUBACK_PACKET(1, [?QOS_2]), Channel}
= handle_out({suback, 1, [?QOS_2]}, Channel)
end).
t_handle_out_unsuback(_) ->
with_channel(
fun(Channel) ->
{ok, ?UNSUBACK_PACKET(1), Channel}
= handle_out({unsuback, 1, [?RC_SUCCESS]}, Channel)
end).
t_handle_out_disconnect(_) ->
with_channel(
fun(Channel) ->
handle_out({disconnect, ?RC_SUCCESS}, Channel)
end).
%%--------------------------------------------------------------------
%% Test cases for handle_timeout
%%--------------------------------------------------------------------
t_handle_timeout(_) ->
with_channel(
fun(Channel) ->
'TODO'
end).
%%--------------------------------------------------------------------
%% Test cases for terminate
%%--------------------------------------------------------------------
t_terminate(_) ->
with_channel(
fun(Channel) ->
'TODO'
end).
%%--------------------------------------------------------------------
%% Helper functions
%%--------------------------------------------------------------------
with_channel(Fun) ->
ConnInfo = #{peername => {{127,0,0,1}, 3456},
sockname => {{127,0,0,1}, 1883},
client_id => <<"clientid">>,
username => <<"username">>
},
Options = [{zone, testing}],
Channel = emqx_channel:init(ConnInfo, Options),
ConnPkt = #mqtt_packet_connect{
proto_name = <<"MQTT">>,
proto_ver = ?MQTT_PROTO_V4,
clean_start = true,
keepalive = 30,
properties = #{},
client_id = <<"clientid">>,
username = <<"username">>
},
Protocol = emqx_protocol:init(ConnPkt),
Session = emqx_session:init(#{zone => testing},
#{max_inflight => 100,
expiry_interval => 0
}),
Fun(emqx_channel:set(protocol, Protocol,
emqx_channel:set(session, Session, Channel))).

View File

@ -54,6 +54,8 @@ groups() ->
]},
{mqttv4, [non_parallel_tests],
[t_basic_v4,
t_cm,
t_cm_registry,
%% t_will_message,
%% t_offline_message_queueing,
t_overlapping_subscriptions,
@ -88,56 +90,79 @@ t_basic_v3(_) ->
t_basic_v4(_Config) ->
t_basic([{proto_ver, v4}]).
t_cm(_) ->
IdleTimeout = emqx_zone:get_env(external, idle_timeout, 30000),
emqx_zone:set_env(external, idle_timeout, 1000),
ClientId = <<"myclient">>,
{ok, C} = emqtt:start_link([{client_id, ClientId}]),
{ok, _} = emqtt:connect(C),
#{client := #{client_id := ClientId}} = emqx_cm:get_chan_attrs(ClientId),
emqtt:subscribe(C, <<"mytopic">>, 0),
ct:sleep(1200),
Stats = emqx_cm:get_chan_stats(ClientId),
?assertEqual(1, proplists:get_value(subscriptions, Stats)),
emqx_zone:set_env(external, idle_timeout, IdleTimeout).
t_cm_registry(_) ->
Info = supervisor:which_children(emqx_cm_sup),
{_, Pid, _, _} = lists:keyfind(registry, 1, Info),
ignored = gen_server:call(Pid, <<"Unexpected call">>),
gen_server:cast(Pid, <<"Unexpected cast">>),
Pid ! <<"Unexpected info">>,
ok = application:stop(mnesia),
emqx_ct_helpers:stop_apps([]),
emqx_ct_helpers:start_apps([]).
t_will_message(_Config) ->
{ok, C1} = emqx_client:start_link([{clean_start, true},
{ok, C1} = emqtt:start_link([{clean_start, true},
{will_topic, nth(3, ?TOPICS)},
{will_payload, <<"client disconnected">>},
{keepalive, 1}]),
{ok, _} = emqx_client:connect(C1),
{ok, _} = emqtt:connect(C1),
{ok, C2} = emqx_client:start_link(),
{ok, _} = emqx_client:connect(C2),
{ok, C2} = emqtt:start_link(),
{ok, _} = emqtt:connect(C2),
{ok, _, [2]} = emqx_client:subscribe(C2, nth(3, ?TOPICS), 2),
{ok, _, [2]} = emqtt:subscribe(C2, nth(3, ?TOPICS), 2),
timer:sleep(5),
ok = emqx_client:stop(C1),
ok = emqtt:stop(C1),
timer:sleep(5),
?assertEqual(1, length(recv_msgs(1))),
ok = emqx_client:disconnect(C2),
ok = emqtt:disconnect(C2),
ct:pal("Will message test succeeded").
t_offline_message_queueing(_) ->
{ok, C1} = emqx_client:start_link([{clean_start, false},
{ok, C1} = emqtt:start_link([{clean_start, false},
{client_id, <<"c1">>}]),
{ok, _} = emqx_client:connect(C1),
{ok, _} = emqtt:connect(C1),
{ok, _, [2]} = emqx_client:subscribe(C1, nth(6, ?WILD_TOPICS), 2),
ok = emqx_client:disconnect(C1),
{ok, C2} = emqx_client:start_link([{clean_start, true},
{ok, _, [2]} = emqtt:subscribe(C1, nth(6, ?WILD_TOPICS), 2),
ok = emqtt:disconnect(C1),
{ok, C2} = emqtt:start_link([{clean_start, true},
{client_id, <<"c2">>}]),
{ok, _} = emqx_client:connect(C2),
{ok, _} = emqtt:connect(C2),
ok = emqx_client:publish(C2, nth(2, ?TOPICS), <<"qos 0">>, 0),
{ok, _} = emqx_client:publish(C2, nth(3, ?TOPICS), <<"qos 1">>, 1),
{ok, _} = emqx_client:publish(C2, nth(4, ?TOPICS), <<"qos 2">>, 2),
ok = emqtt:publish(C2, nth(2, ?TOPICS), <<"qos 0">>, 0),
{ok, _} = emqtt:publish(C2, nth(3, ?TOPICS), <<"qos 1">>, 1),
{ok, _} = emqtt:publish(C2, nth(4, ?TOPICS), <<"qos 2">>, 2),
timer:sleep(10),
emqx_client:disconnect(C2),
{ok, C3} = emqx_client:start_link([{clean_start, false},
emqtt:disconnect(C2),
{ok, C3} = emqtt:start_link([{clean_start, false},
{client_id, <<"c1">>}]),
{ok, _} = emqx_client:connect(C3),
{ok, _} = emqtt:connect(C3),
timer:sleep(10),
emqx_client:disconnect(C3),
emqtt:disconnect(C3),
?assertEqual(3, length(recv_msgs(3))).
t_overlapping_subscriptions(_) ->
{ok, C} = emqx_client:start_link([]),
{ok, _} = emqx_client:connect(C),
{ok, C} = emqtt:start_link([]),
{ok, _} = emqtt:connect(C),
{ok, _, [2, 1]} = emqx_client:subscribe(C, [{nth(7, ?WILD_TOPICS), 2},
{ok, _, [2, 1]} = emqtt:subscribe(C, [{nth(7, ?WILD_TOPICS), 2},
{nth(1, ?WILD_TOPICS), 1}]),
timer:sleep(10),
{ok, _} = emqx_client:publish(C, nth(4, ?TOPICS), <<"overlapping topic filters">>, 2),
{ok, _} = emqtt:publish(C, nth(4, ?TOPICS), <<"overlapping topic filters">>, 2),
timer:sleep(10),
Num = length(recv_msgs(2)),
@ -151,67 +176,67 @@ t_overlapping_subscriptions(_) ->
matching overlapping subscription.");
true -> ok
end,
emqx_client:disconnect(C).
emqtt:disconnect(C).
%% t_keepalive_test(_) ->
%% ct:print("Keepalive test starting"),
%% {ok, C1, _} = emqx_client:start_link([{clean_start, true},
%% {ok, C1, _} = emqtt:start_link([{clean_start, true},
%% {keepalive, 5},
%% {will_flag, true},
%% {will_topic, nth(5, ?TOPICS)},
%% %% {will_qos, 2},
%% {will_payload, <<"keepalive expiry">>}]),
%% ok = emqx_client:pause(C1),
%% {ok, C2, _} = emqx_client:start_link([{clean_start, true},
%% ok = emqtt:pause(C1),
%% {ok, C2, _} = emqtt:start_link([{clean_start, true},
%% {keepalive, 0}]),
%% {ok, _, [2]} = emqx_client:subscribe(C2, nth(5, ?TOPICS), 2),
%% ok = emqx_client:disconnect(C2),
%% {ok, _, [2]} = emqtt:subscribe(C2, nth(5, ?TOPICS), 2),
%% ok = emqtt:disconnect(C2),
%% ?assertEqual(1, length(recv_msgs(1))),
%% ct:print("Keepalive test succeeded").
t_redelivery_on_reconnect(_) ->
ct:pal("Redelivery on reconnect test starting"),
{ok, C1} = emqx_client:start_link([{clean_start, false},
{ok, C1} = emqtt:start_link([{clean_start, false},
{client_id, <<"c">>}]),
{ok, _} = emqx_client:connect(C1),
{ok, _} = emqtt:connect(C1),
{ok, _, [2]} = emqx_client:subscribe(C1, nth(7, ?WILD_TOPICS), 2),
{ok, _, [2]} = emqtt:subscribe(C1, nth(7, ?WILD_TOPICS), 2),
timer:sleep(10),
ok = emqx_client:pause(C1),
{ok, _} = emqx_client:publish(C1, nth(2, ?TOPICS), <<>>,
ok = emqtt:pause(C1),
{ok, _} = emqtt:publish(C1, nth(2, ?TOPICS), <<>>,
[{qos, 1}, {retain, false}]),
{ok, _} = emqx_client:publish(C1, nth(4, ?TOPICS), <<>>,
{ok, _} = emqtt:publish(C1, nth(4, ?TOPICS), <<>>,
[{qos, 2}, {retain, false}]),
timer:sleep(10),
ok = emqx_client:disconnect(C1),
ok = emqtt:disconnect(C1),
?assertEqual(0, length(recv_msgs(2))),
{ok, C2} = emqx_client:start_link([{clean_start, false},
{ok, C2} = emqtt:start_link([{clean_start, false},
{client_id, <<"c">>}]),
{ok, _} = emqx_client:connect(C2),
{ok, _} = emqtt:connect(C2),
timer:sleep(10),
ok = emqx_client:disconnect(C2),
ok = emqtt:disconnect(C2),
?assertEqual(2, length(recv_msgs(2))).
%% t_subscribe_sys_topics(_) ->
%% ct:print("Subscribe failure test starting"),
%% {ok, C, _} = emqx_client:start_link([]),
%% {ok, _, [2]} = emqx_client:subscribe(C, <<"$SYS/#">>, 2),
%% {ok, C, _} = emqtt:start_link([]),
%% {ok, _, [2]} = emqtt:subscribe(C, <<"$SYS/#">>, 2),
%% timer:sleep(10),
%% ct:print("Subscribe failure test succeeded").
t_dollar_topics(_) ->
ct:pal("$ topics test starting"),
{ok, C} = emqx_client:start_link([{clean_start, true},
{ok, C} = emqtt:start_link([{clean_start, true},
{keepalive, 0}]),
{ok, _} = emqx_client:connect(C),
{ok, _} = emqtt:connect(C),
{ok, _, [1]} = emqx_client:subscribe(C, nth(6, ?WILD_TOPICS), 1),
{ok, _} = emqx_client:publish(C, << <<"$">>/binary, (nth(2, ?TOPICS))/binary>>,
{ok, _, [1]} = emqtt:subscribe(C, nth(6, ?WILD_TOPICS), 1),
{ok, _} = emqtt:publish(C, << <<"$">>/binary, (nth(2, ?TOPICS))/binary>>,
<<"test">>, [{qos, 1}, {retain, false}]),
timer:sleep(10),
?assertEqual(0, length(recv_msgs(1))),
ok = emqx_client:disconnect(C),
ok = emqtt:disconnect(C),
ct:pal("$ topics test succeeded").
%%--------------------------------------------------------------------
@ -229,15 +254,15 @@ t_basic_with_props_v5(_) ->
t_basic(Opts) ->
Topic = nth(1, ?TOPICS),
{ok, C} = emqx_client:start_link([{proto_ver, v4}]),
{ok, _} = emqx_client:connect(C),
{ok, _, [1]} = emqx_client:subscribe(C, Topic, qos1),
{ok, _, [2]} = emqx_client:subscribe(C, Topic, qos2),
{ok, _} = emqx_client:publish(C, Topic, <<"qos 2">>, 2),
{ok, _} = emqx_client:publish(C, Topic, <<"qos 2">>, 2),
{ok, _} = emqx_client:publish(C, Topic, <<"qos 2">>, 2),
{ok, C} = emqtt:start_link([{proto_ver, v4}]),
{ok, _} = emqtt:connect(C),
{ok, _, [1]} = emqtt:subscribe(C, Topic, qos1),
{ok, _, [2]} = emqtt:subscribe(C, Topic, qos2),
{ok, _} = emqtt:publish(C, Topic, <<"qos 2">>, 2),
{ok, _} = emqtt:publish(C, Topic, <<"qos 2">>, 2),
{ok, _} = emqtt:publish(C, Topic, <<"qos 2">>, 2),
?assertEqual(3, length(recv_msgs(3))),
ok = emqx_client:disconnect(C).
ok = emqtt:disconnect(C).
%%--------------------------------------------------------------------
%% Helper functions

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

View File

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

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
end.
t_shutdown_disabled() ->
ok = drain(),
self() ! foo,
?assertEqual(continue, emqx_misc:conn_proc_mng_policy(0)),
receive foo -> ok end,
?assertEqual(hibernate, emqx_misc:conn_proc_mng_policy(0)).
t_message_queue_too_long() ->
ok = drain(),
self() ! foo,
self() ! bar,
?assertEqual({shutdown, message_queue_too_long},
emqx_misc:conn_proc_mng_policy(1)),
receive foo -> ok end,
?assertEqual(continue, emqx_misc:conn_proc_mng_policy(1)),
receive bar -> ok end.
t_conn_proc_mng_policy(L) ->
emqx_misc:conn_proc_mng_policy(#{message_queue_len => L}).
t_proc_name(_) ->
'TODO'.

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.
%%--------------------------------------------------------------------
-module(emqx_net_SUITE).
-module(emqx_mod_sup_SUITE).
%% CT
-compile(export_all).
-compile(nowarn_export_all).
all() -> [{group, keepalive}].
-include_lib("eunit/include/eunit.hrl").
groups() -> [{keepalive, [], [t_keepalive]}].
all() -> emqx_ct:all(?MODULE).
%%--------------------------------------------------------------------
%% Keepalive
%% Test cases
%%--------------------------------------------------------------------
t_keepalive(_) ->
{ok, KA} = emqx_keepalive:start(fun() -> {ok, 1} end, 1, {keepalive, timeout}),
[resumed, timeout] = lists:reverse(keepalive_recv(KA, [])).
t_start(_) ->
{ok, _} = emqx_mod_sup:start_link(),
?assertEqual([], supervisor:which_children(emqx_mod_sup)).
keepalive_recv(KA, Acc) ->
receive
{keepalive, timeout} ->
case emqx_keepalive:check(KA) of
{ok, KA1} -> keepalive_recv(KA1, [resumed | Acc]);
{error, timeout} -> [timeout | Acc]
end
after 4000 ->
Acc
end.
t_start_child(_) ->
%% Set the emqx_mod_sup child with emqx_hooks for test
Mod = emqx_hooks,
Spec = #{id => Mod,
start => {Mod, start_link, []},
restart => permanent,
shutdown => 5000,
type => worker,
modules => [Mod]},
{ok, _} = emqx_mod_sup:start_link(),
{ok, _} = emqx_mod_sup:start_child(Mod, worker),
{error, {already_started, _}} = emqx_mod_sup:start_child(Spec),
ok = emqx_mod_sup:stop_child(Mod),
{error, not_found} = emqx_mod_sup:stop_child(Mod),
ok.

View File

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

View File

@ -20,23 +20,61 @@
-compile(nowarn_export_all).
-include("emqx_mqtt.hrl").
-include_lib("eunit/include/eunit.hrl").
-include_lib("emqx_ct_helpers/include/emqx_ct.hrl").
all() -> emqx_ct:all(?MODULE).
t_id(_) ->
'TODO'.
foreach_prop(
fun({Id, Prop}) ->
?assertEqual(Id, emqx_mqtt_props:id(element(1, Prop)))
end),
?catch_error({bad_property, 'Bad-Property'}, emqx_mqtt_props:id('Bad-Property')).
t_name(_) ->
'TODO'.
foreach_prop(
fun({Id, Prop}) ->
?assertEqual(emqx_mqtt_props:name(Id), element(1, Prop))
end),
?catch_error({unsupported_property, 16#FF}, emqx_mqtt_props:name(16#FF)).
t_filter(_) ->
'TODO'.
ConnProps = #{'Session-Expiry-Interval' => 1,
'Maximum-Packet-Size' => 255
},
?assertEqual(ConnProps,
emqx_mqtt_props:filter(?CONNECT, ConnProps)),
PubProps = #{'Payload-Format-Indicator' => 6,
'Message-Expiry-Interval' => 300,
'Session-Expiry-Interval' => 300
},
?assertEqual(#{'Payload-Format-Indicator' => 6,
'Message-Expiry-Interval' => 300
},
emqx_mqtt_props:filter(?PUBLISH, PubProps)).
t_validate(_) ->
'TODO'.
ConnProps = #{'Session-Expiry-Interval' => 1,
'Maximum-Packet-Size' => 255
},
ok = emqx_mqtt_props:validate(ConnProps),
BadProps = #{'Unknown-Property' => 10},
?catch_error({bad_property,'Unknown-Property'},
emqx_mqtt_props:validate(BadProps)).
deprecated_mqtt_properties_all(_) ->
Props = emqx_mqtt_props:filter(?CONNECT, #{'Session-Expiry-Interval' => 1, 'Maximum-Packet-Size' => 255}),
ok = emqx_mqtt_props:validate(Props),
#{} = emqx_mqtt_props:filter(?CONNECT, #{'Maximum-QoS' => ?QOS_2}).
t_validate_value(_) ->
ok = emqx_mqtt_props:validate(#{'Correlation-Data' => <<"correlation-id">>}),
ok = emqx_mqtt_props:validate(#{'Reason-String' => <<"Unknown Reason">>}),
ok = emqx_mqtt_props:validate(#{'User-Property' => {<<"Prop">>, <<"Val">>}}),
ok = emqx_mqtt_props:validate(#{'User-Property' => [{<<"Prop">>, <<"Val">>}]}),
?catch_error({bad_property_value, {'Payload-Format-Indicator', 16#FFFF}},
emqx_mqtt_props:validate(#{'Payload-Format-Indicator' => 16#FFFF})),
?catch_error({bad_property_value, {'Server-Keep-Alive', 16#FFFFFF}},
emqx_mqtt_props:validate(#{'Server-Keep-Alive' => 16#FFFFFF})),
?catch_error({bad_property_value, {'Will-Delay-Interval', -16#FF}},
emqx_mqtt_props:validate(#{'Will-Delay-Interval' => -16#FF})).
foreach_prop(Fun) ->
lists:foreach(Fun, maps:to_list(emqx_mqtt_props:all())).

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).
t_proto_name(_) ->
?assertEqual(<<"MQIsdp">>, emqx_packet:protocol_name(3)),
?assertEqual(<<"MQTT">>, emqx_packet:protocol_name(4)),
?assertEqual(<<"MQTT">>, emqx_packet:protocol_name(5)).
?assertEqual(<<"MQIsdp">>, emqx_packet:proto_name(3)),
?assertEqual(<<"MQTT">>, emqx_packet:proto_name(4)),
?assertEqual(<<"MQTT">>, emqx_packet:proto_name(5)).
t_type_name(_) ->
?assertEqual('CONNECT', emqx_packet:type_name(?CONNECT)),

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(nowarn_export_all).
-import(emqx_protocol,
[ handle_in/2
, handle_out/2
]).
-include("emqx.hrl").
-include("emqx_mqtt.hrl").
-include_lib("eunit/include/eunit.hrl").
all() -> emqx_ct:all(?MODULE).
init_per_suite(Config) ->
emqx_ct_helpers:start_apps([]),
Config.
end_per_suite(_Config) ->
emqx_ct_helpers:stop_apps([]).
%%--------------------------------------------------------------------
%% Test cases for handle_in
%%--------------------------------------------------------------------
t_handle_connect(_) ->
t_init_and_info(_) ->
ConnPkt = #mqtt_packet_connect{
proto_name = <<"MQTT">>,
proto_ver = ?MQTT_PROTO_V4,
@ -54,234 +36,14 @@ t_handle_connect(_) ->
username = <<"username">>,
password = <<"passwd">>
},
with_proto(
fun(PState) ->
{ok, ?CONNACK_PACKET(?RC_SUCCESS), PState1}
= handle_in(?CONNECT_PACKET(ConnPkt), PState),
Client = emqx_protocol:info(client, PState1),
?assertEqual(<<"clientid">>, maps:get(client_id, Client)),
?assertEqual(<<"username">>, maps:get(username, Client))
end).
Proto = emqx_protocol:init(ConnPkt),
?assertEqual(<<"MQTT">>, emqx_protocol:info(proto_name, Proto)),
?assertEqual(?MQTT_PROTO_V4, emqx_protocol:info(proto_ver, Proto)),
?assertEqual(true, emqx_protocol:info(clean_start, Proto)),
?assertEqual(<<"clientid">>, emqx_protocol:info(client_id, Proto)),
?assertEqual(<<"username">>, emqx_protocol:info(username, Proto)),
?assertEqual(undefined, emqx_protocol:info(will_msg, Proto)),
?assertEqual(#{}, emqx_protocol:info(conn_props, Proto)).
t_handle_publish_qos0(_) ->
with_proto(
fun(PState) ->
Publish = ?PUBLISH_PACKET(?QOS_0, <<"topic">>, undefined, <<"payload">>),
{ok, PState} = handle_in(Publish, PState)
end).
t_handle_publish_qos1(_) ->
with_proto(
fun(PState) ->
Publish = ?PUBLISH_PACKET(?QOS_1, <<"topic">>, 1, <<"payload">>),
{ok, ?PUBACK_PACKET(1, RC), _} = handle_in(Publish, PState),
?assert((RC == ?RC_SUCCESS) orelse (RC == ?RC_NO_MATCHING_SUBSCRIBERS))
end).
t_handle_publish_qos2(_) ->
with_proto(
fun(PState) ->
Publish1 = ?PUBLISH_PACKET(?QOS_2, <<"topic">>, 1, <<"payload">>),
{ok, ?PUBREC_PACKET(1, RC), PState1} = handle_in(Publish1, PState),
Publish2 = ?PUBLISH_PACKET(?QOS_2, <<"topic">>, 2, <<"payload">>),
{ok, ?PUBREC_PACKET(2, RC), PState2} = handle_in(Publish2, PState1),
?assert((RC == ?RC_SUCCESS) orelse (RC == ?RC_NO_MATCHING_SUBSCRIBERS)),
Session = emqx_protocol:info(session, PState2),
?assertEqual(2, emqx_session:info(awaiting_rel, Session))
end).
t_handle_puback(_) ->
with_proto(
fun(PState) ->
{ok, PState} = handle_in(?PUBACK_PACKET(1, ?RC_SUCCESS), PState)
end).
t_handle_pubrec(_) ->
with_proto(
fun(PState) ->
{ok, ?PUBREL_PACKET(1, ?RC_PACKET_IDENTIFIER_NOT_FOUND), PState}
= handle_in(?PUBREC_PACKET(1, ?RC_SUCCESS), PState)
end).
t_handle_pubrel(_) ->
with_proto(
fun(PState) ->
{ok, ?PUBCOMP_PACKET(1, ?RC_PACKET_IDENTIFIER_NOT_FOUND), PState}
= handle_in(?PUBREL_PACKET(1, ?RC_SUCCESS), PState)
end).
t_handle_pubcomp(_) ->
with_proto(
fun(PState) ->
{ok, PState} = handle_in(?PUBCOMP_PACKET(1, ?RC_SUCCESS), PState)
end).
t_handle_subscribe(_) ->
with_proto(
fun(PState) ->
TopicFilters = [{<<"+">>, ?DEFAULT_SUBOPTS}],
{ok, ?SUBACK_PACKET(10, [?QOS_0]), PState1}
= handle_in(?SUBSCRIBE_PACKET(10, #{}, TopicFilters), PState),
Session = emqx_protocol:info(session, PState1),
?assertEqual(maps:from_list(TopicFilters),
emqx_session:info(subscriptions, Session))
end).
t_handle_unsubscribe(_) ->
with_proto(
fun(PState) ->
{ok, ?UNSUBACK_PACKET(11), PState}
= handle_in(?UNSUBSCRIBE_PACKET(11, #{}, [<<"+">>]), PState)
end).
t_handle_pingreq(_) ->
with_proto(
fun(PState) ->
{ok, ?PACKET(?PINGRESP), PState} = handle_in(?PACKET(?PINGREQ), PState)
end).
t_handle_disconnect(_) ->
with_proto(
fun(PState) ->
{stop, normal, PState1} = handle_in(?DISCONNECT_PACKET(?RC_SUCCESS), PState),
?assertEqual(undefined, emqx_protocol:info(will_msg, PState1))
end).
t_handle_auth(_) ->
with_proto(
fun(PState) ->
{ok, PState} = handle_in(?AUTH_PACKET(), PState)
end).
%%--------------------------------------------------------------------
%% Test cases for handle_deliver
%%--------------------------------------------------------------------
t_handle_deliver(_) ->
with_proto(
fun(PState) ->
TopicFilters = [{<<"+">>, ?DEFAULT_SUBOPTS#{qos => ?QOS_2}}],
{ok, ?SUBACK_PACKET(1, [?QOS_2]), PState1}
= handle_in(?SUBSCRIBE_PACKET(1, #{}, TopicFilters), PState),
Msg0 = emqx_message:make(<<"clientx">>, ?QOS_0, <<"t0">>, <<"qos0">>),
Msg1 = emqx_message:make(<<"clientx">>, ?QOS_1, <<"t1">>, <<"qos1">>),
Delivers = [{deliver, <<"+">>, Msg0},
{deliver, <<"+">>, Msg1}],
{ok, Packets, _PState2} = emqx_protocol:handle_deliver(Delivers, PState1),
?assertMatch([?PUBLISH_PACKET(?QOS_0, <<"t0">>, undefined, <<"qos0">>),
?PUBLISH_PACKET(?QOS_1, <<"t1">>, 1, <<"qos1">>)
], Packets)
end).
%%--------------------------------------------------------------------
%% Test cases for handle_out
%%--------------------------------------------------------------------
t_handle_conack(_) ->
with_proto(
fun(PState) ->
{ok, ?CONNACK_PACKET(?RC_SUCCESS, SP, _), _}
= handle_out({connack, ?RC_SUCCESS, 0}, PState),
{error, unauthorized_client, ?CONNACK_PACKET(5), _}
= handle_out({connack, ?RC_NOT_AUTHORIZED}, PState)
end).
t_handle_out_publish(_) ->
with_proto(
fun(PState) ->
Pub0 = {publish, undefined, emqx_message:make(<<"t">>, <<"qos0">>)},
Pub1 = {publish, 1, emqx_message:make(<<"c">>, ?QOS_1, <<"t">>, <<"qos1">>)},
{ok, ?PUBLISH_PACKET(?QOS_0), PState} = handle_out(Pub0, PState),
{ok, ?PUBLISH_PACKET(?QOS_1), PState} = handle_out(Pub1, PState),
{ok, Packets, PState} = handle_out({publish, [Pub0, Pub1]}, PState),
?assertEqual(2, length(Packets))
end).
t_handle_out_puback(_) ->
with_proto(
fun(PState) ->
{ok, PState} = handle_out({puberr, ?RC_NOT_AUTHORIZED}, PState),
{ok, ?PUBACK_PACKET(1, ?RC_SUCCESS), PState}
= handle_out({puback, 1, ?RC_SUCCESS}, PState)
end).
t_handle_out_pubrec(_) ->
with_proto(
fun(PState) ->
{ok, ?PUBREC_PACKET(4, ?RC_SUCCESS), PState}
= handle_out({pubrec, 4, ?RC_SUCCESS}, PState)
end).
t_handle_out_pubrel(_) ->
with_proto(
fun(PState) ->
{ok, ?PUBREL_PACKET(2), PState} = handle_out({pubrel, 2}, PState),
{ok, ?PUBREL_PACKET(3, ?RC_SUCCESS), PState}
= handle_out({pubrel, 3, ?RC_SUCCESS}, PState)
end).
t_handle_out_pubcomp(_) ->
with_proto(
fun(PState) ->
{ok, ?PUBCOMP_PACKET(5, ?RC_SUCCESS), PState}
= handle_out({pubcomp, 5, ?RC_SUCCESS}, PState)
end).
t_handle_out_suback(_) ->
with_proto(
fun(PState) ->
{ok, ?SUBACK_PACKET(1, [?QOS_2]), PState}
= handle_out({suback, 1, [?QOS_2]}, PState)
end).
t_handle_out_unsuback(_) ->
with_proto(
fun(PState) ->
{ok, ?UNSUBACK_PACKET(1), PState} = handle_out({unsuback, 1, [?RC_SUCCESS]}, PState)
end).
t_handle_out_disconnect(_) ->
with_proto(
fun(PState) ->
handle_out({disconnect, 0}, PState)
end).
%%--------------------------------------------------------------------
%% Test cases for handle_timeout
%%--------------------------------------------------------------------
t_handle_timeout(_) ->
with_proto(
fun(PState) ->
'TODO'
end).
%%--------------------------------------------------------------------
%% Test cases for terminate
%%--------------------------------------------------------------------
t_terminate(_) ->
with_proto(
fun(PState) ->
'TODO'
end).
%%--------------------------------------------------------------------
%% Helper functions
%%--------------------------------------------------------------------
with_proto(Fun) ->
ConnInfo = #{peername => {{127,0,0,1}, 3456},
sockname => {{127,0,0,1}, 1883},
client_id => <<"clientid">>,
username => <<"username">>
},
Options = [{zone, testing}],
PState = emqx_protocol:init(ConnInfo, Options),
Session = emqx_session:init(false, #{zone => testing},
#{max_inflight => 100,
expiry_interval => 0
}),
Fun(emqx_protocol:set(session, Session, PState)).

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).
-include("emqx_mqtt.hrl").
-include_lib("proper/include/proper.hrl").
-include_lib("eunit/include/eunit.hrl").
-import(lists, [seq/2, zip/2, foreach/2]).
-define(MQTTV4_CODE_NAMES, [connection_acceptd,
unacceptable_protocol_version,
client_identifier_not_valid,
server_unavaliable,
malformed_username_or_password,
unauthorized_client,
unknown_error]).
-define(MQTTV5_CODE_NAMES, [success, granted_qos1, granted_qos2, disconnect_with_will_message,
no_matching_subscribers, no_subscription_existed, continue_authentication,
re_authenticate, unspecified_error, malformed_Packet, protocol_error,
implementation_specific_error, unsupported_protocol_version,
client_identifier_not_valid, bad_username_or_password, not_authorized,
server_unavailable, server_busy, banned,server_shutting_down,
bad_authentication_method, keepalive_timeout, session_taken_over,
topic_filter_invalid, topic_name_invalid, packet_identifier_inuse,
packet_identifier_not_found, receive_maximum_exceeded, topic_alias_invalid,
packet_too_large, message_rate_too_high, quota_exceeded,
administrative_action, payload_format_invalid, retain_not_supported,
qos_not_supported, use_another_server, server_moved,
shared_subscriptions_not_supported, connection_rate_exceeded,
maximum_connect_time, subscription_identifiers_not_supported,
wildcard_subscriptions_not_supported, unknown_error]).
-define(MQTTV5_CODES, [16#00, 16#01, 16#02, 16#04, 16#10, 16#11, 16#18, 16#19, 16#80, 16#81, 16#82,
16#83, 16#84, 16#85, 16#86, 16#87, 16#88, 16#89, 16#8A, 16#8B, 16#8C, 16#8D,
16#8E, 16#8F, 16#90, 16#91, 16#92, 16#93, 16#94, 16#95, 16#96, 16#97, 16#98,
16#99, 16#9A, 16#9B, 16#9C, 16#9D, 16#9E, 16#9F, 16#A0, 16#A1, 16#A2, code]).
-define(MQTTV5_TXT, [<<"Success">>, <<"Granted QoS 1">>, <<"Granted QoS 2">>,
<<"Disconnect with Will Message">>, <<"No matching subscribers">>,
<<"No subscription existed">>, <<"Continue authentication">>,
<<"Re-authenticate">>, <<"Unspecified error">>, <<"Malformed Packet">>,
<<"Protocol Error">>, <<"Implementation specific error">>,
<<"Unsupported Protocol Version">>, <<"Client Identifier not valid">>,
<<"Bad User Name or Password">>, <<"Not authorized">>,
<<"Server unavailable">>, <<"Server busy">>, <<"Banned">>,
<<"Server shutting down">>, <<"Bad authentication method">>,
<<"Keep Alive timeout">>, <<"Session taken over">>,
<<"Topic Filter invalid">>, <<"Topic Name invalid">>,
<<"Packet Identifier in use">>, <<"Packet Identifier not found">>,
<<"Receive Maximum exceeded">>, <<"Topic Alias invalid">>,
<<"Packet too large">>, <<"Message rate too high">>, <<"Quota exceeded">>,
<<"Administrative action">>, <<"Payload format invalid">>,
<<"Retain not supported">>, <<"QoS not supported">>,
<<"Use another server">>, <<"Server moved">>,
<<"Shared Subscriptions not supported">>, <<"Connection rate exceeded">>,
<<"Maximum connect time">>, <<"Subscription Identifiers not supported">>,
<<"Wildcard Subscriptions not supported">>, <<"Unknown error">>]).
-define(COMPAT_CODES_V5, [16#80, 16#81, 16#82, 16#83, 16#84, 16#85, 16#86, 16#87,
16#88, 16#89, 16#8A, 16#8B, 16#8C, 16#97, 16#9C, 16#9D,
16#9F]).
-define(COMPAT_CODES_V4, [?CONNACK_PROTO_VER, ?CONNACK_PROTO_VER, ?CONNACK_PROTO_VER,
?CONNACK_PROTO_VER, ?CONNACK_PROTO_VER,
?CONNACK_INVALID_ID,
?CONNACK_CREDENTIALS,
?CONNACK_AUTH,
?CONNACK_SERVER,
?CONNACK_SERVER,
?CONNACK_AUTH,
?CONNACK_SERVER,
?CONNACK_AUTH,
?CONNACK_SERVER, ?CONNACK_SERVER, ?CONNACK_SERVER, ?CONNACK_SERVER]).
all() -> emqx_ct:all(?MODULE).
t_mqttv4_name(_) ->
(((codes_test(?MQTT_PROTO_V4))
(seq(0,6)))
(?MQTTV4_CODE_NAMES))
(fun emqx_reason_codes:name/2).
t_prop_name_text(_) ->
?assert(proper:quickcheck(prop_name_text(), prop_name_text(opts))).
t_mqttv5_name(_) ->
(((codes_test(?MQTT_PROTO_V5))
(?MQTTV5_CODES))
(?MQTTV5_CODE_NAMES))
(fun emqx_reason_codes:name/2).
t_prop_compat(_) ->
?assert(proper:quickcheck(prop_compat(), prop_compat(opts))).
t_text(_) ->
(((codes_test(?MQTT_PROTO_V5))
(?MQTTV5_CODES))
(?MQTTV5_TXT))
(fun emqx_reason_codes:text/1).
t_prop_connack_error(_) ->
?assert(proper:quickcheck(prop_connack_error(), default_opts([]))).
t_compat(_) ->
(((codes_test(connack))
(?COMPAT_CODES_V5))
(?COMPAT_CODES_V4))
(fun emqx_reason_codes:compat/2),
(((codes_test(suback))
([0,1,2, 16#80]))
([0,1,2, 16#80]))
(fun emqx_reason_codes:compat/2),
(((codes_test(unsuback))
([0, 1, 2]))
([undefined, undefined, undefined]))
(fun emqx_reason_codes:compat/2).
prop_name_text(opts) ->
default_opts([{numtests, 1000}]).
codes_test(AsistVar) ->
fun(CODES) ->
fun(NAMES) ->
fun(Procedure) ->
foreach(fun({Code, Result}) ->
?assertEqual(Result, case erlang:fun_info(Procedure, name) of
{name, text} -> Procedure(Code);
{name, name} -> Procedure(Code, AsistVar);
{name, compat} -> Procedure(AsistVar, Code)
end)
end, zip(CODES, NAMES))
end
end
end.
prop_name_text() ->
?FORALL(UnionArgs, union_args(),
is_atom(apply_fun(name, UnionArgs)) andalso
is_binary(apply_fun(text, UnionArgs))).
prop_compat(opts) ->
default_opts([{numtests, 512}]).
prop_compat() ->
?FORALL(CompatArgs, compat_args(),
begin
Result = apply_fun(compat, CompatArgs),
is_number(Result) orelse Result =:= undefined
end).
prop_connack_error() ->
?FORALL(CONNACK_ERROR_ARGS, connack_error_args(),
is_integer(apply_fun(connack_error, CONNACK_ERROR_ARGS))).
%%--------------------------------------------------------------------
%% Helper
%%--------------------------------------------------------------------
default_opts() ->
default_opts([]).
default_opts(AdditionalOpts) ->
[{to_file, user} | AdditionalOpts].
apply_fun(Fun, Args) ->
apply(emqx_reason_codes, Fun, Args).
%%--------------------------------------------------------------------
%% Generator
%%--------------------------------------------------------------------
union_args() ->
frequency([{6, [real_mqttv3_rc(), mqttv3_version()]},
{43, [real_mqttv5_rc(), mqttv5_version()]}]).
compat_args() ->
frequency([{18, [connack, compat_rc()]},
{2, [suback, compat_rc()]},
{1, [unsuback, compat_rc()]}]).
connack_error_args() ->
[frequency([{10, connack_error()},
{1, unexpected_connack_error()}])].
connack_error() ->
oneof([client_identifier_not_valid,
bad_username_or_password,
bad_clientid_or_password,
username_or_password_undefined,
password_error,
not_authorized,
server_unavailable,
server_busy,
banned,
bad_authentication_method]).
unexpected_connack_error() ->
oneof([who_knows]).
real_mqttv3_rc() ->
frequency([{6, mqttv3_rc()},
{1, unexpected_rc()}]).
real_mqttv5_rc() ->
frequency([{43, mqttv5_rc()},
{2, unexpected_rc()}]).
compat_rc() ->
frequency([{95, ?SUCHTHAT(RC , mqttv5_rc(), RC >= 16#80 orelse RC =< 2)},
{5, unexpected_rc()}]).
mqttv3_rc() ->
oneof(mqttv3_rcs()).
mqttv5_rc() ->
oneof(mqttv5_rcs()).
unexpected_rc() ->
oneof(unexpected_rcs()).
mqttv3_rcs() ->
[0, 1, 2, 3, 4, 5].
mqttv5_rcs() ->
[16#00, 16#01, 16#02, 16#04, 16#10, 16#11, 16#18, 16#19,
16#80, 16#81, 16#82, 16#83, 16#84, 16#85, 16#86, 16#87,
16#88, 16#89, 16#8A, 16#8B, 16#8C, 16#8D, 16#8E, 16#8F,
16#90, 16#91, 16#92, 16#93, 16#94, 16#95, 16#96, 16#97,
16#98, 16#99, 16#9A, 16#9B, 16#9C, 16#9D, 16#9E, 16#9F,
16#A0, 16#A1, 16#A2].
unexpected_rcs() ->
ReasonCodes = mqttv3_rcs() ++ mqttv5_rcs(),
Unexpected = lists:seq(0, 16#FF) -- ReasonCodes,
lists:sublist(Unexpected, 5).
mqttv5_version() ->
?MQTT_PROTO_V5.
mqttv3_version() ->
oneof([?MQTT_PROTO_V3, ?MQTT_PROTO_V4]).

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(nowarn_export_all).
-include("emqx_mqtt.hrl").
-include_lib("proper/include/proper.hrl").
-include_lib("eunit/include/eunit.hrl").
-define(mock_modules,
[ emqx_metrics
, emqx_broker
, emqx_misc
, emqx_message
, emqx_hooks
, emqx_zone
, emqx_pd
]).
all() -> emqx_ct:all(?MODULE).
init_per_suite(Config) ->
emqx_ct_helpers:start_apps([]),
Config.
t_proper_session(_) ->
Opts = [{numtests, 1000}, {to_file, user}],
ok = emqx_logger:set_log_level(emergency),
ok = before_proper(),
?assert(proper:quickcheck(prop_session(), Opts)),
ok = after_proper().
end_per_suite(_Config) ->
emqx_ct_helpers:stop_apps([]).
before_proper() ->
load(?mock_modules).
t_info(_) ->
'TODO'.
after_proper() ->
unload(?mock_modules),
emqx_logger:set_log_level(error).
t_attrs(_) ->
'TODO'.
prop_session() ->
?FORALL({Session, OpList}, {session(), session_op_list()},
begin
try
apply_ops(Session, OpList),
true
after
true
end
end).
t_stats(_) ->
'TODO'.
%%%%%%%%%%%%%%%
%%% Helpers %%%
%%%%%%%%%%%%%%%
t_subscribe(_) ->
'TODO'.
apply_ops(Session, []) ->
?assertEqual(session, element(1, Session));
apply_ops(Session, [Op | Rest]) ->
NSession = apply_op(Session, Op),
apply_ops(NSession, Rest).
t_unsubscribe(_) ->
'TODO'.
apply_op(Session, info) ->
Info = emqx_session:info(Session),
?assert(is_map(Info)),
?assertEqual(15, maps:size(Info)),
Session;
apply_op(Session, attrs) ->
Attrs = emqx_session:attrs(Session),
?assert(is_map(Attrs)),
?assertEqual(2, maps:size(Attrs)),
Session;
apply_op(Session, stats) ->
Stats = emqx_session:stats(Session),
?assert(is_list(Stats)),
?assertEqual(9, length(Stats)),
Session;
apply_op(Session, {info, InfoArg}) ->
_Ret = emqx_session:info(InfoArg, Session),
Session;
apply_op(Session, {subscribe, {Client, TopicFilter, SubOpts}}) ->
case emqx_session:subscribe(Client, TopicFilter, SubOpts, Session) of
{ok, NSession} ->
NSession;
{error, ?RC_QUOTA_EXCEEDED} ->
Session
end;
apply_op(Session, {unsubscribe, {Client, TopicFilter}}) ->
case emqx_session:unsubscribe(Client, TopicFilter, Session) of
{ok, NSession} ->
NSession;
{error, ?RC_NO_SUBSCRIPTION_EXISTED} ->
Session
end;
apply_op(Session, {publish, {PacketId, Msg}}) ->
case emqx_session:publish(PacketId, Msg, Session) of
{ok, _Msg} ->
Session;
{ok, _Deliver, NSession} ->
NSession;
{error, _ErrorCode} ->
Session
end;
apply_op(Session, {puback, PacketId}) ->
case emqx_session:puback(PacketId, Session) of
{ok, _Msg} ->
Session;
{ok, _Deliver, NSession} ->
NSession;
{error, _ErrorCode} ->
Session
end;
apply_op(Session, {pubrec, PacketId}) ->
case emqx_session:pubrec(PacketId, Session) of
{ok, NSession} ->
NSession;
{error, _ErrorCode} ->
Session
end;
apply_op(Session, {pubrel, PacketId}) ->
case emqx_session:pubrel(PacketId, Session) of
{ok, NSession} ->
NSession;
{error, _ErrorCode} ->
Session
end;
apply_op(Session, {pubcomp, PacketId}) ->
case emqx_session:pubcomp(PacketId, Session) of
{ok, _Msgs} ->
Session;
{ok, _Msgs, NSession} ->
NSession;
{error, _ErrorCode} ->
Session
end;
apply_op(Session, {deliver, Delivers}) ->
{ok, _Msgs, NSession} = emqx_session:deliver(Delivers, Session),
NSession.
t_publish(_) ->
'TODO'.
%%%%%%%%%%%%%%%%%%
%%% Generators %%%
%%%%%%%%%%%%%%%%%%
session_op_list() ->
Union = [info,
attrs,
stats,
{info, info_args()},
{subscribe, sub_args()},
{unsubscribe, unsub_args()},
{publish, publish_args()},
{puback, puback_args()},
{pubrec, pubrec_args()},
{pubrel, pubrel_args()},
{pubcomp, pubcomp_args()},
{deliver, deliver_args()}
],
list(?LAZY(oneof(Union))).
t_puback(_) ->
'TODO'.
deliver_args() ->
list({deliver, topic(), message()}).
t_pubrec(_) ->
'TODO'.
info_args() ->
oneof([subscriptions,
max_subscriptions,
upgrade_qos,
inflight,
max_inflight,
retry_interval,
mqueue_len,
max_mqueue,
mqueue_dropped,
next_pkt_id,
awaiting_rel,
max_awaiting_rel,
await_rel_timeout,
expiry_interval,
created_at
]).
t_pubrel(_) ->
'TODO'.
sub_args() ->
?LET({ClientId, TopicFilter, SubOpts},
{clientid(), topic(), sub_opts()},
{#{client_id => ClientId}, TopicFilter, SubOpts}).
t_pubcomp(_) ->
'TODO'.
unsub_args() ->
?LET({ClientId, TopicFilter},
{clientid(), topic()},
{#{client_id => ClientId}, TopicFilter}).
t_deliver(_) ->
'TODO'.
publish_args() ->
?LET({PacketId, Message},
{packetid(), message()},
{PacketId, Message}).
t_timeout(_) ->
'TODO'.
puback_args() ->
packetid().
ignore_loop(_Config) ->
emqx_zone:set_env(external, ignore_loop_deliver, true),
{ok, Client} = emqx_client:start_link(),
{ok, _} = emqx_client:connect(Client),
TestTopic = <<"Self">>,
{ok, _, [2]} = emqx_client:subscribe(Client, TestTopic, qos2),
ok = emqx_client:publish(Client, TestTopic, <<"testmsg">>, 0),
{ok, _} = emqx_client:publish(Client, TestTopic, <<"testmsg">>, 1),
{ok, _} = emqx_client:publish(Client, TestTopic, <<"testmsg">>, 2),
?assertEqual(0, length(emqx_client_SUITE:receive_messages(3))),
ok = emqx_client:disconnect(Client),
emqx_zone:set_env(external, ignore_loop_deliver, false).
pubrec_args() ->
packetid().
session_all(_) ->
emqx_zone:set_env(internal, idle_timeout, 1000),
ClientId = <<"ClientId">>,
{ok, ConnPid} = emqx_mock_client:start_link(ClientId),
{ok, SPid} = emqx_mock_client:open_session(ConnPid, ClientId, internal),
Message1 = emqx_message:make(<<"ClientId">>, 2, <<"topic">>, <<"hello">>),
emqx_session:subscribe(SPid, [{<<"topic">>, #{qos => 2}}]),
emqx_session:subscribe(SPid, [{<<"topic">>, #{qos => 1}}]),
timer:sleep(200),
[{<<"topic">>, _}] = emqx:subscriptions(SPid),
emqx_session:publish(SPid, 1, Message1),
timer:sleep(200),
[{publish, 1, _}] = emqx_mock_client:get_last_message(ConnPid),
Attrs = emqx_session:attrs(SPid),
Info = emqx_session:info(SPid),
Stats = emqx_session:stats(SPid),
ClientId = proplists:get_value(client_id, Attrs),
ClientId = proplists:get_value(client_id, Info),
1 = proplists:get_value(subscriptions_count, Stats),
emqx_session:unsubscribe(SPid, [<<"topic">>]),
timer:sleep(200),
[] = emqx:subscriptions(SPid),
emqx_mock_client:close_session(ConnPid).
pubrel_args() ->
packetid().
pubcomp_args() ->
packetid().
sub_opts() ->
?LET({RH, RAP, NL, QOS, SHARE, SUBID},
{rh(), rap(), nl(), qos(), share(), subid()}
, make_subopts(RH, RAP, NL, QOS, SHARE, SUBID)).
message() ->
?LET({QoS, Topic, Payload},
{qos(), topic(), payload()},
emqx_message:make(proper, QoS, Topic, Payload)).
subid() -> integer().
rh() -> oneof([0, 1, 2]).
rap() -> oneof([0, 1]).
nl() -> oneof([0, 1]).
qos() -> oneof([0, 1, 2]).
share() -> binary().
clientid() -> binary().
topic() -> ?LET(No, choose(1, 10),
begin
NoBin = integer_to_binary(No),
<<"topic/", NoBin/binary>>
end).
payload() -> binary().
packetid() -> choose(1, 30).
zone() ->
?LET(Zone, [{max_subscriptions, max_subscription()},
{upgrade_qos, upgrade_qos()},
{retry_interval, retry_interval()},
{max_awaiting_rel, max_awaiting_rel()},
{await_rel_timeout, await_rel_timeout()}]
, maps:from_list(Zone)).
max_subscription() ->
frequency([{33, 0},
{33, 1},
{34, choose(0,10)}]).
upgrade_qos() -> bool().
retry_interval() -> ?LET(Interval, choose(0, 20), Interval*1000).
max_awaiting_rel() -> choose(0, 10).
await_rel_timeout() -> ?LET(Interval, choose(0, 150), Interval*1000).
max_inflight() -> choose(0, 10).
expiry_interval() -> ?LET(EI, choose(1, 10), EI * 3600).
option() ->
?LET(Option, [{max_inflight, max_inflight()},
{expiry_interval, expiry_interval()}],
maps:from_list(Option)).
session() ->
?LET({Zone, Options},
{zone(), option()},
begin
Session = emqx_session:init(#{zone => Zone}, Options),
emqx_session:set_pkt_id(Session, 16#ffff)
end).
%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% Internal functions %%%
%%%%%%%%%%%%%%%%%%%%%%%%%%
make_subopts(RH, RAP, NL, QOS, SHARE, SubId) ->
#{rh => RH,
rap => RAP,
nl => NL,
qos => QOS,
share => SHARE,
subid => SubId}.
load(Modules) ->
[mock(Module) || Module <- Modules],
ok.
unload(Modules) ->
lists:foreach(fun(Module) ->
ok = meck:unload(Module)
end, Modules).
mock(Module) ->
ok = meck:new(Module, [passthrough, no_history]),
do_mock(Module).
do_mock(emqx_metrics) ->
meck:expect(emqx_metrics, inc, fun(_Anything) -> ok end);
do_mock(emqx_broker) ->
meck:expect(emqx_broker, subscribe, fun(_, _, _) -> ok end),
meck:expect(emqx_broker, set_subopts, fun(_, _) -> ok end),
meck:expect(emqx_broker, unsubscribe, fun(_) -> ok end),
meck:expect(emqx_broker, publish, fun(_) -> ok end);
do_mock(emqx_misc) ->
meck:expect(emqx_misc, start_timer, fun(_, _) -> tref end);
do_mock(emqx_message) ->
meck:expect(emqx_message, set_header, fun(_Hdr, _Val, Msg) -> Msg end),
meck:expect(emqx_message, is_expired, fun(_Msg) -> (rand:uniform(16) > 8) end);
do_mock(emqx_hooks) ->
meck:expect(emqx_hooks, run, fun(_Hook, _Args) -> ok end);
do_mock(emqx_zone) ->
meck:expect(emqx_zone, get_env, fun(Env, Key, Default) -> maps:get(Key, Env, Default) end);
do_mock(emqx_pd) ->
meck:expect(emqx_pd, update_counter, fun(_stats, _num) -> ok end).

View File

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

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

View File

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

View File

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

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.