Merge pull request #2839 from emqx/master
Auto-pull-request-by-2019-08-24
This commit is contained in:
commit
e4a5121e86
|
@ -11,7 +11,6 @@ script:
|
|||
- make xref
|
||||
- make eunit
|
||||
- make ct
|
||||
- make proper
|
||||
- make cover
|
||||
|
||||
after_success:
|
||||
|
|
8
Makefile
8
Makefile
|
@ -15,11 +15,7 @@ RUN_NODE_NAME = emqxdebug@127.0.0.1
|
|||
all: compile
|
||||
|
||||
.PHONY: tests
|
||||
tests: eunit ct proper
|
||||
|
||||
.PHONY: proper
|
||||
proper:
|
||||
@rebar3 proper
|
||||
tests: eunit ct
|
||||
|
||||
.PHONY: run
|
||||
run: run_setup unlock
|
||||
|
@ -99,7 +95,7 @@ ct: ct_setup
|
|||
## e.g. make ct-one-suite suite=emqx_bridge
|
||||
.PHONY: $(SUITES:%=ct-%)
|
||||
$(CT_SUITES:%=ct-%): ct_setup
|
||||
@rebar3 ct -v --readable=false --name $(CT_NODE_NAME) --suite=$(@:ct-%=%)_SUITE
|
||||
@rebar3 ct -v --readable=false --name $(CT_NODE_NAME) --suite=$(@:ct-%=%)_SUITE --cover
|
||||
|
||||
.PHONY: app.config
|
||||
app.config: $(CUTTLEFISH_SCRIPT) etc/gen.emqx.conf
|
||||
|
|
18
README-CN.md
18
README-CN.md
|
@ -1,10 +1,10 @@
|
|||
# EMQ X Broker
|
||||
|
||||
[](https://github.com/emqx/emqx/releases)
|
||||
[](https://travis-ci.org/emqx/emqx)
|
||||
[](https://coveralls.io/github/emqx/emqx)
|
||||
[](https://hub.docker.com/r/emqx/emqx)
|
||||
[](https://emqx.slack.com)
|
||||
[](https://github.com/emqx/emqx/releases)
|
||||
[](https://travis-ci.org/emqx/emqx)
|
||||
[](https://coveralls.io/github/emqx/emqx)
|
||||
[](https://hub.docker.com/r/emqx/emqx)
|
||||
[](https://slack-invite.emqx.io)
|
||||
[](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)。
|
||||
|
|
18
README.md
18
README.md
|
@ -1,10 +1,10 @@
|
|||
# EMQ X Broker
|
||||
|
||||
[](https://github.com/emqx/emqx/releases)
|
||||
[](https://travis-ci.org/emqx/emqx)
|
||||
[](https://coveralls.io/github/emqx/emqx)
|
||||
[](https://hub.docker.com/r/emqx/emqx)
|
||||
[](https://emqx.slack.com)
|
||||
[](https://github.com/emqx/emqx/releases)
|
||||
[](https://travis-ci.org/emqx/emqx)
|
||||
[](https://coveralls.io/github/emqx/emqx)
|
||||
[](https://hub.docker.com/r/emqx/emqx)
|
||||
[](https://slack-invite.emqx.io)
|
||||
[](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).
|
||||
|
|
|
@ -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
|
||||
%%--------------------------------------------------------------------
|
||||
|
|
|
@ -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"]}.
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
1715
src/emqx_channel.erl
1715
src/emqx_channel.erl
File diff suppressed because it is too large
Load Diff
1258
src/emqx_client.erl
1258
src/emqx_client.erl
File diff suppressed because it is too large
Load Diff
|
@ -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).
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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}.
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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).
|
||||
|
||||
|
|
|
@ -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}.
|
||||
|
|
|
@ -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}).
|
||||
|
||||
|
|
|
@ -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} ->
|
||||
|
|
|
@ -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} ->
|
||||
|
|
|
@ -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) ->
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
@ -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;
|
||||
|
|
|
@ -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
|
@ -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.
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -50,7 +50,7 @@
|
|||
|
||||
-logger_header("[Session]").
|
||||
|
||||
-export([init/3]).
|
||||
-export([init/2]).
|
||||
|
||||
-export([ info/1
|
||||
, info/2
|
||||
|
@ -58,6 +58,8 @@
|
|||
, stats/1
|
||||
]).
|
||||
|
||||
-export([update_expiry_interval/2]).
|
||||
|
||||
-export([ subscribe/4
|
||||
, unsubscribe/3
|
||||
]).
|
||||
|
@ -69,71 +71,53 @@
|
|||
, pubcomp/2
|
||||
]).
|
||||
|
||||
-export([deliver/2]).
|
||||
-export([ deliver/2
|
||||
, enqueue/2
|
||||
, retry/1
|
||||
]).
|
||||
|
||||
-export([timeout/3]).
|
||||
-export([ takeover/1
|
||||
, resume/2
|
||||
, redeliver/1
|
||||
]).
|
||||
|
||||
-export([expire/2]).
|
||||
|
||||
-export_type([session/0]).
|
||||
|
||||
-import(emqx_zone,
|
||||
[ get_env/2
|
||||
, get_env/3
|
||||
]).
|
||||
|
||||
%% For test case
|
||||
-export([set_pkt_id/2]).
|
||||
|
||||
-record(session, {
|
||||
%% Clean Start Flag
|
||||
clean_start :: boolean(),
|
||||
-import(emqx_zone, [get_env/3]).
|
||||
|
||||
-record(session, {
|
||||
%% Client’s Subscriptions.
|
||||
subscriptions :: map(),
|
||||
|
||||
%% Max subscriptions allowed
|
||||
max_subscriptions :: non_neg_integer(),
|
||||
|
||||
%% Upgrade QoS?
|
||||
upgrade_qos :: boolean(),
|
||||
|
||||
%% Client <- Broker:
|
||||
%% Inflight QoS1, QoS2 messages sent to the client but unacked.
|
||||
inflight :: emqx_inflight:inflight(),
|
||||
|
||||
%% All QoS1, QoS2 messages published to when client is disconnected.
|
||||
%% QoS 1 and QoS 2 messages pending transmission to the Client.
|
||||
%%
|
||||
%% Optionally, QoS 0 messages pending transmission to the Client.
|
||||
mqueue :: emqx_mqueue:mqueue(),
|
||||
|
||||
%% Next packet id of the session
|
||||
next_pkt_id = 1 :: emqx_types:packet_id(),
|
||||
|
||||
%% Retry interval for redelivering QoS1/2 messages
|
||||
retry_interval :: timeout(),
|
||||
|
||||
%% Retry delivery timer
|
||||
retry_timer :: maybe(reference()),
|
||||
|
||||
%% Client -> Broker:
|
||||
%% Inflight QoS2 messages received from client and waiting for pubrel.
|
||||
awaiting_rel :: map(),
|
||||
|
||||
%% Max Packets Awaiting PUBREL
|
||||
max_awaiting_rel :: non_neg_integer(),
|
||||
|
||||
%% Awaiting PUBREL Timer
|
||||
await_rel_timer :: maybe(reference()),
|
||||
|
||||
%% Awaiting PUBREL Timeout
|
||||
await_rel_timeout :: timeout(),
|
||||
|
||||
%% Session Expiry Interval
|
||||
expiry_interval :: timeout(),
|
||||
|
||||
%% Expired Timer
|
||||
expiry_timer :: maybe(reference()),
|
||||
|
||||
%% Created at
|
||||
created_at :: erlang:timestamp()
|
||||
}).
|
||||
|
@ -149,11 +133,10 @@
|
|||
%%--------------------------------------------------------------------
|
||||
|
||||
%% @doc Init a session.
|
||||
-spec(init(boolean(), emqx_types:client(), Options :: map()) -> session()).
|
||||
init(CleanStart, #{zone := Zone}, #{max_inflight := MaxInflight,
|
||||
expiry_interval := ExpiryInterval}) ->
|
||||
#session{clean_start = CleanStart,
|
||||
max_subscriptions = get_env(Zone, max_subscriptions, 0),
|
||||
-spec(init(emqx_types:client(), Options :: map()) -> session()).
|
||||
init(#{zone := Zone}, #{max_inflight := MaxInflight,
|
||||
expiry_interval := ExpiryInterval}) ->
|
||||
#session{max_subscriptions = get_env(Zone, max_subscriptions, 0),
|
||||
subscriptions = #{},
|
||||
upgrade_qos = get_env(Zone, upgrade_qos, false),
|
||||
inflight = emqx_inflight:new(MaxInflight),
|
||||
|
@ -179,8 +162,7 @@ init_mqueue(Zone) ->
|
|||
%%--------------------------------------------------------------------
|
||||
|
||||
-spec(info(session()) -> emqx_types:infos()).
|
||||
info(#session{clean_start = CleanStart,
|
||||
max_subscriptions = MaxSubscriptions,
|
||||
info(#session{max_subscriptions = MaxSubscriptions,
|
||||
subscriptions = Subscriptions,
|
||||
upgrade_qos = UpgradeQoS,
|
||||
inflight = Inflight,
|
||||
|
@ -192,8 +174,7 @@ info(#session{clean_start = CleanStart,
|
|||
await_rel_timeout = AwaitRelTimeout,
|
||||
expiry_interval = ExpiryInterval,
|
||||
created_at = CreatedAt}) ->
|
||||
#{clean_start => CleanStart,
|
||||
subscriptions => Subscriptions,
|
||||
#{subscriptions => Subscriptions,
|
||||
max_subscriptions => MaxSubscriptions,
|
||||
upgrade_qos => UpgradeQoS,
|
||||
inflight => emqx_inflight:size(Inflight),
|
||||
|
@ -206,12 +187,10 @@ info(#session{clean_start = CleanStart,
|
|||
awaiting_rel => maps:size(AwaitingRel),
|
||||
max_awaiting_rel => MaxAwaitingRel,
|
||||
await_rel_timeout => AwaitRelTimeout,
|
||||
expiry_interval => ExpiryInterval div 1000,
|
||||
expiry_interval => ExpiryInterval,
|
||||
created_at => CreatedAt
|
||||
}.
|
||||
|
||||
info(clean_start, #session{clean_start = CleanStart}) ->
|
||||
CleanStart;
|
||||
info(subscriptions, #session{subscriptions = Subs}) ->
|
||||
Subs;
|
||||
info(max_subscriptions, #session{max_subscriptions = MaxSubs}) ->
|
||||
|
@ -239,20 +218,23 @@ info(max_awaiting_rel, #session{max_awaiting_rel = MaxAwaitingRel}) ->
|
|||
info(await_rel_timeout, #session{await_rel_timeout = Timeout}) ->
|
||||
Timeout;
|
||||
info(expiry_interval, #session{expiry_interval = Interval}) ->
|
||||
Interval div 1000;
|
||||
Interval;
|
||||
info(created_at, #session{created_at = CreatedAt}) ->
|
||||
CreatedAt.
|
||||
|
||||
update_expiry_interval(ExpiryInterval, Session) ->
|
||||
Session#session{expiry_interval = ExpiryInterval}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Attrs of the session
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-spec(attrs(session()) -> emqx_types:attrs()).
|
||||
attrs(#session{clean_start = CleanStart,
|
||||
expiry_interval = ExpiryInterval,
|
||||
attrs(undefined) ->
|
||||
#{};
|
||||
attrs(#session{expiry_interval = ExpiryInterval,
|
||||
created_at = CreatedAt}) ->
|
||||
#{clean_start => CleanStart,
|
||||
expiry_interval => ExpiryInterval,
|
||||
#{expiry_interval => ExpiryInterval,
|
||||
created_at => CreatedAt
|
||||
}.
|
||||
|
||||
|
@ -278,6 +260,37 @@ stats(#session{subscriptions = Subscriptions,
|
|||
{awaiting_rel, maps:size(AwaitingRel)},
|
||||
{max_awaiting_rel, MaxAwaitingRel}].
|
||||
|
||||
-spec(takeover(session()) -> ok).
|
||||
takeover(#session{subscriptions = Subs}) ->
|
||||
lists:foreach(fun({TopicFilter, _SubOpts}) ->
|
||||
ok = emqx_broker:unsubscribe(TopicFilter)
|
||||
end, maps:to_list(Subs)).
|
||||
|
||||
-spec(resume(emqx_types:client_id(), session()) -> ok).
|
||||
resume(ClientId, #session{subscriptions = Subs}) ->
|
||||
?LOG(info, "Session is resumed."),
|
||||
%% 1. Subscribe again.
|
||||
lists:foreach(fun({TopicFilter, SubOpts}) ->
|
||||
ok = emqx_broker:subscribe(TopicFilter, ClientId, SubOpts)
|
||||
end, maps:to_list(Subs)).
|
||||
%% 2. Run hooks.
|
||||
%% ok = emqx_hooks:run('session.resumed', [#{client_id => ClientId}, attrs(Session)]),
|
||||
%% TODO: 3. Redeliver: Replay delivery and Dequeue pending messages
|
||||
%%Session.
|
||||
|
||||
redeliver(Session = #session{inflight = Inflight}) ->
|
||||
Publishes = lists:map(fun({PacketId, {pubrel, _Ts}}) ->
|
||||
{pubrel, PacketId, ?RC_SUCCESS};
|
||||
({PacketId, {Msg, _Ts}}) ->
|
||||
{publish, PacketId, Msg}
|
||||
end, emqx_inflight:to_list(Inflight)),
|
||||
case dequeue(Session) of
|
||||
{ok, NSession} ->
|
||||
{ok, Publishes, NSession};
|
||||
{ok, More, NSession} ->
|
||||
{ok, lists:append(Publishes, More), NSession}
|
||||
end.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Client -> Broker: SUBSCRIBE
|
||||
%%--------------------------------------------------------------------
|
||||
|
@ -363,7 +376,7 @@ do_publish(PacketId, Msg = #message{timestamp = Ts},
|
|||
DeliverResults = emqx_broker:publish(Msg),
|
||||
AwaitingRel1 = maps:put(PacketId, Ts, AwaitingRel),
|
||||
Session1 = Session#session{awaiting_rel = AwaitingRel1},
|
||||
{ok, DeliverResults, ensure_await_rel_timer(Session1)};
|
||||
{ok, DeliverResults, Session1};
|
||||
true ->
|
||||
{error, ?RC_PACKET_IDENTIFIER_IN_USE}
|
||||
end.
|
||||
|
@ -502,7 +515,13 @@ deliver([Msg = #message{qos = QoS}|More], Acc,
|
|||
deliver(More, [Publish|Acc], next_pkt_id(Session1))
|
||||
end.
|
||||
|
||||
enqueue(Msg, Session = #session{mqueue = Q}) ->
|
||||
enqueue(Delivers, Session = #session{subscriptions = Subs})
|
||||
when is_list(Delivers) ->
|
||||
Msgs = [enrich(get_subopts(Topic, Subs), Msg, Session)
|
||||
|| {deliver, Topic, Msg} <- Delivers],
|
||||
lists:foldl(fun enqueue/2, Session, Msgs);
|
||||
|
||||
enqueue(Msg, Session = #session{mqueue = Q}) when is_record(Msg, message) ->
|
||||
emqx_pd:update_counter(enqueue_stats, 1),
|
||||
{Dropped, NewQ} = emqx_mqueue:in(Msg, Q),
|
||||
if
|
||||
|
@ -519,9 +538,8 @@ enqueue(Msg, Session = #session{mqueue = Q}) ->
|
|||
%%--------------------------------------------------------------------
|
||||
|
||||
await(PacketId, Msg, Session = #session{inflight = Inflight}) ->
|
||||
Inflight1 = emqx_inflight:insert(
|
||||
PacketId, {Msg, os:timestamp()}, Inflight),
|
||||
ensure_retry_timer(Session#session{inflight = Inflight1}).
|
||||
Inflight1 = emqx_inflight:insert(PacketId, {Msg, os:timestamp()}, Inflight),
|
||||
Session#session{inflight = Inflight1}.
|
||||
|
||||
get_subopts(Topic, SubMap) ->
|
||||
case maps:find(Topic, SubMap) of
|
||||
|
@ -542,53 +560,23 @@ enrich([{qos, SubQoS}|Opts], Msg = #message{qos = PubQoS}, Session = #session{up
|
|||
enrich(Opts, Msg#message{qos = max(SubQoS, PubQoS)}, Session);
|
||||
enrich([{qos, SubQoS}|Opts], Msg = #message{qos = PubQoS}, Session = #session{upgrade_qos= false}) ->
|
||||
enrich(Opts, Msg#message{qos = min(SubQoS, PubQoS)}, Session);
|
||||
enrich([{rap, _Rap}|Opts], Msg = #message{flags = Flags, headers = #{retained := true}}, Session = #session{}) ->
|
||||
enrich(Opts, Msg#message{flags = maps:put(retain, true, Flags)}, Session);
|
||||
enrich([{rap, 0}|Opts], Msg = #message{flags = Flags}, Session) ->
|
||||
enrich([{rap, 0}|Opts], Msg = #message{flags = Flags, headers = #{proto_ver := ?MQTT_PROTO_V5}}, Session) ->
|
||||
enrich(Opts, Msg#message{flags = maps:put(retain, false, Flags)}, Session);
|
||||
enrich([{rap, _}|Opts], Msg, Session) ->
|
||||
enrich([{rap, _}|Opts], Msg = #message{headers = #{proto_ver := ?MQTT_PROTO_V5}}, Session) ->
|
||||
enrich(Opts, Msg, Session);
|
||||
enrich([{rap, _}|Opts], Msg = #message{headers = #{retained := true}}, Session = #session{}) ->
|
||||
enrich(Opts, Msg, Session);
|
||||
enrich([{rap, _}|Opts], Msg = #message{flags = Flags}, Session) ->
|
||||
enrich(Opts, Msg#message{flags = maps:put(retain, false, Flags)}, Session);
|
||||
enrich([{subid, SubId}|Opts], Msg, Session) ->
|
||||
enrich(Opts, emqx_message:set_header('Subscription-Identifier', SubId, Msg), Session).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Handle timeout
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-spec(timeout(reference(), atom(), session())
|
||||
-> {ok, session()} | {ok, list(), session()}).
|
||||
timeout(TRef, retry_delivery, Session = #session{retry_timer = TRef}) ->
|
||||
retry_delivery(Session#session{retry_timer = undefined});
|
||||
|
||||
timeout(TRef, check_awaiting_rel, Session = #session{await_rel_timer = TRef}) ->
|
||||
expire_awaiting_rel(Session);
|
||||
|
||||
timeout(TRef, Msg, Session) ->
|
||||
?LOG(error, "unexpected timeout - ~p: ~p", [TRef, Msg]),
|
||||
{ok, Session}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Ensure retry timer
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
ensure_retry_timer(Session = #session{retry_interval = Interval,
|
||||
retry_timer = undefined}) ->
|
||||
ensure_retry_timer(Interval, Session);
|
||||
ensure_retry_timer(Session) ->
|
||||
Session.
|
||||
|
||||
ensure_retry_timer(Interval, Session = #session{retry_timer = undefined}) ->
|
||||
TRef = emqx_misc:start_timer(Interval, retry_delivery),
|
||||
Session#session{retry_timer = TRef};
|
||||
ensure_retry_timer(_Interval, Session) ->
|
||||
Session.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Retry Delivery
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
%% Redeliver at once if force is true
|
||||
retry_delivery(Session = #session{inflight = Inflight}) ->
|
||||
retry(Session = #session{inflight = Inflight}) ->
|
||||
case emqx_inflight:is_empty(Inflight) of
|
||||
true -> {ok, Session};
|
||||
false ->
|
||||
|
@ -599,10 +587,11 @@ retry_delivery(Session = #session{inflight = Inflight}) ->
|
|||
|
||||
retry_delivery([], _Now, Acc, Session) ->
|
||||
%% Retry again...
|
||||
{ok, lists:reverse(Acc), ensure_retry_timer(Session)};
|
||||
{ok, lists:reverse(Acc), Session};
|
||||
|
||||
retry_delivery([{PacketId, {Val, Ts}}|More], Now, Acc,
|
||||
Session = #session{retry_interval = Interval, inflight = Inflight}) ->
|
||||
Session = #session{retry_interval = Interval,
|
||||
inflight = Inflight}) ->
|
||||
%% Microseconds -> MilliSeconds
|
||||
Age = timer:now_diff(Now, Ts) div 1000,
|
||||
if
|
||||
|
@ -610,7 +599,7 @@ retry_delivery([{PacketId, {Val, Ts}}|More], Now, Acc,
|
|||
{Acc1, Inflight1} = retry_delivery(PacketId, Val, Now, Acc, Inflight),
|
||||
retry_delivery(More, Now, Acc1, Session#session{inflight = Inflight1});
|
||||
true ->
|
||||
{ok, lists:reverse(Acc), ensure_retry_timer(Interval - max(0, Age), Session)}
|
||||
{ok, lists:reverse(Acc), Interval - max(0, Age), Session}
|
||||
end.
|
||||
|
||||
retry_delivery(PacketId, Msg, Now, Acc, Inflight) when is_record(Msg, message) ->
|
||||
|
@ -627,34 +616,20 @@ retry_delivery(PacketId, pubrel, Now, Acc, Inflight) ->
|
|||
Inflight1 = emqx_inflight:update(PacketId, {pubrel, Now}, Inflight),
|
||||
{[{pubrel, PacketId}|Acc], Inflight1}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Ensure await_rel timer
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
ensure_await_rel_timer(Session = #session{await_rel_timeout = Timeout,
|
||||
await_rel_timer = undefined}) ->
|
||||
ensure_await_rel_timer(Timeout, Session);
|
||||
ensure_await_rel_timer(Session) ->
|
||||
Session.
|
||||
|
||||
ensure_await_rel_timer(Timeout, Session = #session{await_rel_timer = undefined}) ->
|
||||
TRef = emqx_misc:start_timer(Timeout, check_awaiting_rel),
|
||||
Session#session{await_rel_timer = TRef};
|
||||
ensure_await_rel_timer(_Timeout, Session) ->
|
||||
Session.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Expire Awaiting Rel
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
expire_awaiting_rel(Session = #session{awaiting_rel = AwaitingRel}) ->
|
||||
expire(awaiting_rel, Session = #session{awaiting_rel = AwaitingRel}) ->
|
||||
case maps:size(AwaitingRel) of
|
||||
0 -> {ok, Session};
|
||||
_ -> expire_awaiting_rel(lists:keysort(2, maps:to_list(AwaitingRel)), os:timestamp(), Session)
|
||||
_ ->
|
||||
AwaitingRel1 = lists:keysort(2, maps:to_list(AwaitingRel)),
|
||||
expire_awaiting_rel(AwaitingRel1, os:timestamp(), Session)
|
||||
end.
|
||||
|
||||
expire_awaiting_rel([], _Now, Session) ->
|
||||
{ok, Session#session{await_rel_timer = undefined}};
|
||||
{ok, Session};
|
||||
|
||||
expire_awaiting_rel([{PacketId, Ts} | More], Now,
|
||||
Session = #session{awaiting_rel = AwaitingRel,
|
||||
|
@ -666,7 +641,7 @@ expire_awaiting_rel([{PacketId, Ts} | More], Now,
|
|||
Session1 = Session#session{awaiting_rel = maps:remove(PacketId, AwaitingRel)},
|
||||
expire_awaiting_rel(More, Now, Session1);
|
||||
Age ->
|
||||
{ok, ensure_await_rel_timer(Timeout - max(0, Age), Session)}
|
||||
{ok, Timeout - max(0, Age), Session}
|
||||
end.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
|
@ -683,6 +658,6 @@ next_pkt_id(Session = #session{next_pkt_id = Id}) ->
|
|||
%% For Test case
|
||||
%%---------------------------------------------------------------------
|
||||
|
||||
|
||||
set_pkt_id(Session, PktId) ->
|
||||
Session#session{next_pkt_id = PktId}.
|
||||
|
||||
|
|
|
@ -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)))).
|
||||
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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}.
|
||||
|
|
@ -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}.
|
|
@ -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})).
|
||||
|
||||
|
|
|
@ -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 %%%
|
|
@ -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))).
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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).
|
|
@ -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.
|
||||
|
|
@ -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").
|
||||
|
||||
|
||||
|
|
@ -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).
|
|
@ -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),
|
||||
|
|
|
@ -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)).
|
||||
|
||||
|
|
|
@ -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.
|
|
@ -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'.
|
||||
|
||||
|
|
|
@ -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([]).
|
|
@ -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.
|
||||
|
|
@ -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(_) ->
|
||||
|
|
|
@ -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())).
|
||||
|
||||
|
|
|
@ -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)).
|
||||
|
|
@ -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)),
|
||||
|
|
|
@ -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).
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
mini.name = test
|
|
@ -0,0 +1,5 @@
|
|||
%%-*- mode: erlang -*-
|
||||
|
||||
{mapping, "mini.name", "emqx_mini_plugin.name", [
|
||||
{datatype, string}
|
||||
]}.
|
|
@ -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"}}}
|
||||
]}
|
||||
]}
|
||||
]}.
|
|
@ -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, []}
|
||||
]}.
|
|
@ -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}}.
|
||||
|
|
@ -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)).
|
||||
|
||||
|
|
|
@ -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().
|
|
@ -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]).
|
||||
|
|
|
@ -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.
|
|
@ -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).
|
|
@ -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.
|
|
@ -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".
|
|
@ -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).
|
||||
|
|
|
@ -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) ->
|
||||
|
|
|
@ -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.
|
|
@ -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]),
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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, []).
|
|
@ -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.
|
Loading…
Reference in New Issue