diff --git a/.travis.yml b/.travis.yml index abe6f7a6a..c0b7bf5b9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,7 @@ script: - make xref - make eunit - make ct + - make proper - make cover after_success: diff --git a/Makefile b/Makefile index 791aed2f2..b0a1dce16 100644 --- a/Makefile +++ b/Makefile @@ -14,10 +14,17 @@ RUN_NODE_NAME = emqxdebug@127.0.0.1 .PHONY: all all: compile +.PHONY: tests +tests: eunit ct proper + +.PHONY: proper +proper: + @rebar3 proper + .PHONY: run run: run_setup unlock @rebar3 as test get-deps - @rebar3 as test auto --name $(RUN_NODE_NAME) --script test/run_emqx.escript + @rebar3 as test auto --name $(RUN_NODE_NAME) --script scripts/run_emqx.escript .PHONY: run_setup run_setup: diff --git a/README-CN.md b/README-CN.md new file mode 100644 index 000000000..d0ad2f556 --- /dev/null +++ b/README-CN.md @@ -0,0 +1,87 @@ +# EMQ X Broker + +[![GitHub Release](https://img.shields.io/github/release/emqx/emqx?color=brightgreen)](https://github.com/emqx/emqx/releases) +[![Build Status](https://travis-ci.org/emqx/emqx.svg)](https://travis-ci.org/emqx/emqx) +[![Coverage Status](https://coveralls.io/repos/github/emqx/emqx/badge.svg)](https://coveralls.io/github/emqx/emqx) +[![Docker Pulls](https://img.shields.io/docker/pulls/emqx/emqx)](https://hub.docker.com/r/emqx/emqx) +[![Slack](https://img.shields.io/badge/Slack-EMQ%20X-brightgreen?logo=slack&style=flat&color=7E4798)](https://emqx.slack.com) +[![Twitter](https://img.shields.io/badge/Twiiter-EMQ%20X-1DA1F2?logo=twitter)](https://twitter.com/emqtt) + +[English](./README.md) | 简体中文 + +*EMQ X* 是一款完全开源,高度可伸缩,高可用的分布式 MQTT 消息服务器,适用于 IoT、M2M 和移动应用程序,可处理千万级别的并发客户端。 + +从 3.0 版本开始,*EMQ X* 完整支持 MQTT V5.0 协议规范,向下兼容 MQTT V3.1 和 V3.1.1,并支持 MQTT-SN、CoAP、LwM2M、WebSocket 和 STOMP 等通信协议。EMQ X 3.0 单集群可支持千万级别的 MQTT 并发连接。 + +- 新功能的完整列表,请参阅 [EMQ X Release Notes](https://github.com/emqx/emqx/releases)。 +- 获取更多信息,请访问 [EMQ X](https://emqx.io)。 + +## 安装 + +*EMQ X* 是跨平台的,支持 Linux、Unix、Mac OS 以及 Windows。这意味着 *EMQ X* 可以部署在 x86_64 架构的服务器上,也可以部署在 Raspberry Pi 这样的 ARM 设备上。 + +获取适合你的二进制软件包,[点此下载](https://emqx.io/downloads)。 + +- [单节点安装](https://developer.emqx.io/docs/emq/v3/en/install.html) +- [集群安装](https://developer.emqx.io/docs/emq/v3/en/cluster.html) + +## 从源码构建 + +3.0 版本开始,构建 *EMQ X* 需要 Erlang/OTP R21+。 + +``` +git clone https://github.com/emqx/emqx-rel.git + +cd emqx-rel && make + +cd _rel/emqx && ./bin/emqx console +``` + +## 快速入门 + +``` +# Start emqx +./bin/emqx start + +# Check Status +./bin/emqx_ctl status + +# Stop emqx +./bin/emqx stop +``` + +*EMQ X* 启动,可以使用浏览器访问 http://localhost:18083 来查看 Dashboard。 + +## FAQ + +访问 [FAQ](https://developer.emqx.io/docs/tutorial/zh/faq/faq.html) 以获取常见问题的帮助。 + +## 产品路线 + +通过 [EMQ X Roadmap uses Github milestones](https://github.com/emqx/emqx/milestones) 参与跟踪项目进度。 + +## 社区、讨论、贡献和支持 + +你可通过以下途径与 EMQ 社区及开发者联系: + +- [EMQX Slack](http://emqx.slack.com) +- [Mailing Lists]() +- [Twitter](https://twitter.com/emqtt) +- [Forum](https://groups.google.com/d/forum/emqtt) +- [Blog](https://medium.com/@emqtt) + +欢迎你将任何 bug、问题和功能请求提交到 [emqx/emqx](https://github.com/emqx/emqx/issues)。 + +## MQTT 规范 + +你可以通过以下链接了解与查阅 MQTT 协议: + +[MQTT Version 3.1.1](https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html) + +[MQTT Version 5.0](https://docs.oasis-open.org/mqtt/mqtt/v5.0/cs02/mqtt-v5.0-cs02.html) + +[MQTT SN](http://mqtt.org/new/wp-content/uploads/2009/06/MQTT-SN_spec_v1.2.pdf) + +## 开源许可 + +Apache License 2.0, 详见 [LICENSE](./LICENSE)。 \ No newline at end of file diff --git a/README.md b/README.md index a9c6e710f..c6dc53fe7 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,26 @@ # EMQ X Broker +[![GitHub Release](https://img.shields.io/github/release/emqx/emqx?color=brightgreen)](https://github.com/emqx/emqx/releases) +[![Build Status](https://travis-ci.org/emqx/emqx.svg)](https://travis-ci.org/emqx/emqx) +[![Coverage Status](https://coveralls.io/repos/github/emqx/emqx/badge.svg)](https://coveralls.io/github/emqx/emqx) +[![Docker Pulls](https://img.shields.io/docker/pulls/emqx/emqx)](https://hub.docker.com/r/emqx/emqx) +[![Slack](https://img.shields.io/badge/Slack-EMQ%20X-brightgreen?logo=slack&style=flat&color=7E4798)](https://emqx.slack.com) +[![Twitter](https://img.shields.io/badge/Twiiter-EMQ%20X-1DA1F2?logo=twitter)](https://twitter.com/emqtt) + +English | [简体中文](./README-CN.md) + *EMQ X* broker is a fully open source, highly scalable, highly available distributed MQTT messaging broker for IoT, M2M and Mobile applications that can handle tens of millions of concurrent clients. Starting from 3.0 release, *EMQ X* broker fully supports MQTT V5.0 protocol specifications and backward compatible with MQTT V3.1 and V3.1.1, as well as other communication protocols such as MQTT-SN, CoAP, LwM2M, WebSocket and STOMP. The 3.0 release of the *EMQ X* broker can scaled to 10+ million concurrent MQTT connections on one cluster. - -- For full list of new features, please read *EMQ X* broker 3.0 [release notes](https://github.com/emqx/emqx/releases/). -- For more information, please visit [EMQ X homepage](http://emqx.io). - +- For full list of new features, please read [EMQ X Release Notes](https://github.com/emqx/emqx/releases). +- For more information, please visit [EMQ X homepage](https://emqx.io). ## Installation -The *EMQ X* broker is cross-platform, which can be deployed on Linux, Unix, Mac, Windows and even Raspberry Pi. +The *EMQ X* broker is cross-platform, which supports Linux, Unix, Mac OS and Windows. It means *EMQ X* can be deployed on x86_64 architecture servers and ARM devices like Raspberry Pi. -Download the binary package for your platform from [here](http://emqx.io/downloads). +Download the binary package for your platform from [here](https://emqx.io/downloads). - [Single Node Install](https://developer.emqx.io/docs/emq/v3/en/install.html) - [Multi Node Install](https://developer.emqx.io/docs/emq/v3/en/cluster.html) @@ -34,17 +41,22 @@ cd _build/emqx/rel/emqx && ./bin/emqx console ## Quick Start - # Start emqx - ./bin/emqx start +``` +# Start emqx +./bin/emqx start - # Check Status - ./bin/emqx_ctl status +# Check Status +./bin/emqx_ctl status - # Stop emqx - ./bin/emqx stop +# Stop emqx +./bin/emqx stop +``` - To view the dashboard after running, use your browser to open: http://localhost:18083 +To view the dashboard after running, use your browser to open: http://localhost:18083 +## FAQ + +Visiting [FAQ](https://developer.emqx.io/docs/tutorial/en/faq/faq.html) to get help of common problems. ## Roadmap @@ -54,8 +66,6 @@ The [EMQ X Roadmap uses Github milestones](https://github.com/emqx/emqx/mileston You can reach the EMQ community and developers via the following channels: - [EMQX Slack](http://emqx.slack.com) - -[#emqx-users](https://emqx.slack.com/messages/CBUF2TTB8/) - -[#emqx-devs](https://emqx.slack.com/messages/CBSL57DUH/) - [Mailing Lists]() - [Twitter](https://twitter.com/emqtt) - [Forum](https://groups.google.com/d/forum/emqtt) @@ -75,11 +85,4 @@ You can read the mqtt protocol via the following links: ## License -Copyright (c) 2013-2019 [EMQ Technologies Co., Ltd](http://emqx.io). 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](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. +Apache License 2.0, see [LICENSE](./LICENSE). \ No newline at end of file diff --git a/etc/emqx.conf b/etc/emqx.conf index df3be2a9a..34c64b099 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -11,6 +11,16 @@ ## Value: String cluster.name = emqxcl +## Specify the erlang distributed protocol. +## +## Value: Enum +## - inet_tcp: the default; handles TCP streams with IPv4 addressing. +## - inet6_tcp: handles TCP with IPv6 addressing. +## - inet_tls: using TLS for Erlang Distribution. +## +## vm.args: -proto_dist inet_tcp +cluster.proto_dist = inet_tcp + ## Cluster auto-discovery strategy. ## ## Value: Enum @@ -251,15 +261,7 @@ node.fullsweep_after = 1000 ## Value: Log file node.crash_dump = {{ platform_log_dir }}/crash.dump -## Specify the erlang distributed protocol. -## -## Value: Enum -## - inet_tcp: the default; handles TCP streams with IPv4 addressing. -## - inet6_tcp: handles TCP with IPv6 addressing. -## - inet_tls: using TLS for Erlang Distribution. -## -## vm.args: -proto_dist inet_tcp -node.proto_dist = inet_tcp + ## Specify SSL Options in the file if using SSL for Erlang Distribution. ## @@ -292,6 +294,18 @@ node.dist_listen_max = 6369 ##-------------------------------------------------------------------- ## RPC ##-------------------------------------------------------------------- +## RPC Mode. +## +## Value: sync | async +rpc.mode = async + +## Max batch size of async RPC requests. +## +## Value: Integer +## Zero or negative value disables rpc batching. +## +## NOTE: RPC batch won't work when rpc.mode = sync +rpc.async_batch_size = 256 ## TCP server port for RPC. ## @@ -303,6 +317,11 @@ rpc.tcp_server_port = 5369 ## Value: Port [1024-65535] rpc.tcp_client_port = 5369 +## Number of utgoing RPC connections. +## +## Value: Interger [1-256] +rpc.tcp_client_num = 32 + ## RCP Client connect timeout. ## ## Value: Seconds @@ -338,6 +357,21 @@ rpc.socket_keepalive_interval = 75s ## Value: Integer rpc.socket_keepalive_count = 9 +## Size of TCP send buffer. +## +## Value: Bytes +rpc.socket_sndbuf = 1MB + +## Size of TCP receive buffer. +## +## Value: Seconds +rpc.socket_recbuf = 1MB + +## Size of user-level software socket buffer. +## +## Value: Seconds +rpc.socket_buffer = 1MB + ##-------------------------------------------------------------------- ## Log ##-------------------------------------------------------------------- diff --git a/include/emqx.hrl b/include/emqx.hrl index 7afe009a2..d676be9cf 100644 --- a/include/emqx.hrl +++ b/include/emqx.hrl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,17 +12,20 @@ %% 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. +%%-------------------------------------------------------------------- -ifndef(EMQ_X_HRL). -define(EMQ_X_HRL, true). %%-------------------------------------------------------------------- -%% Banner +%% Common %%-------------------------------------------------------------------- --define(COPYRIGHT, "Copyright (c) 2013-2019 EMQ Technologies Co., Ltd"). +-define(Otherwise, true). --define(LICENSE_MESSAGE, "Licensed under the Apache License, Version 2.0"). +%%-------------------------------------------------------------------- +%% Banner +%%-------------------------------------------------------------------- -define(PROTOCOL_VERSION, "MQTT/5.0"). @@ -47,8 +51,6 @@ %% Message and Delivery %%-------------------------------------------------------------------- --record(session, {sid, pid}). - -record(subscription, {topic, subid, subopts}). %% See 'Application Message' in MQTT Version 5.0 @@ -73,8 +75,7 @@ -record(delivery, { sender :: pid(), %% Sender of the delivery - message :: #message{}, %% The message delivered - results :: list() %% Dispatches of the message + message :: #message{} %% The message delivered }). %%-------------------------------------------------------------------- @@ -167,12 +168,3 @@ -endif. -%%-------------------------------------------------------------------- -%% Compatible with Windows -%%-------------------------------------------------------------------- - --define(compat_windows(Expression, Default), - case os:type() of - {win32, nt} -> Default; - _Unix -> Expression - end). diff --git a/include/emqx_client.hrl b/include/emqx_client.hrl deleted file mode 100644 index bf2f49283..000000000 --- a/include/emqx_client.hrl +++ /dev/null @@ -1,21 +0,0 @@ -%% 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. - - --ifndef(EMQX_CLIENT_HRL). --define(EMQX_CLIENT_HRL, true). --include("emqx_mqtt.hrl"). --record(mqtt_msg, {qos = ?QOS_0, retain = false, dup = false, - packet_id, topic, props, payload}). --endif. diff --git a/include/emqx_mqtt.hrl b/include/emqx_mqtt.hrl index 655b8e755..854399999 100644 --- a/include/emqx_mqtt.hrl +++ b/include/emqx_mqtt.hrl @@ -204,8 +204,7 @@ -define(DEFAULT_SUBOPTS, #{rh => 0, %% Retain Handling rap => 0, %% Retain as Publish nl => 0, %% No Local - qos => 0, %% QoS - rc => 0 %% Reason Code + qos => 0 %% QoS }). -record(mqtt_packet_connect, { @@ -351,6 +350,13 @@ variable = #mqtt_packet_publish{packet_id = PacketId} }). +-define(PUBLISH_PACKET(QoS, Topic, PacketId), + #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, + qos = QoS}, + variable = #mqtt_packet_publish{topic_name = Topic, + packet_id = PacketId} + }). + -define(PUBLISH_PACKET(QoS, Topic, PacketId, Payload), #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, qos = QoS}, diff --git a/include/logger.hrl b/include/logger.hrl index 1cf5facc6..6f046e3c2 100644 --- a/include/logger.hrl +++ b/include/logger.hrl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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. +%%-------------------------------------------------------------------- %% debug | info | notice | warning | error | critical | alert | emergency @@ -43,3 +45,4 @@ begin (logger:log(Level,#{},#{report_cb => fun(_) -> {'$logger_header'()++(Format), (Args)} end})) end). + diff --git a/include/types.hrl b/include/types.hrl index 85a9aadf0..41b7ad50e 100644 --- a/include/types.hrl +++ b/include/types.hrl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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. +%%-------------------------------------------------------------------- -type(maybe(T) :: undefined | T). @@ -19,3 +21,4 @@ -type(ok_or_error(Reason) :: ok | {error, Reason}). -type(ok_or_error(Value, Reason) :: {ok, Value} | {error, Reason}). + diff --git a/priv/emqx.schema b/priv/emqx.schema index 1d308476d..48b85ad6e 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -85,6 +85,13 @@ {datatype, string} ]}. +%% @doc The erlang distributed protocol +{mapping, "cluster.proto_dist", "ekka.proto_dist", [ + {default, "inet_tcp"}, + {datatype, {enum, [inet_tcp, inet6_tcp, inet_tls]}}, + hidden +]}. + {mapping, "cluster.dns.app", "ekka.cluster_discovery", [ {datatype, string} ]}. @@ -198,12 +205,7 @@ end}. {default, "emqx@127.0.0.1"} ]}. -%% @doc The erlang distributed protocol -{mapping, "node.proto_dist", "vm_args.-proto_dist", [ - %{default, "inet_tcp"}, - {datatype, {enum, [inet_tcp, inet6_tcp, inet_tls]}}, - hidden -]}. + %% @doc Specify SSL Options in the file if using SSL for erlang distribution {mapping, "node.ssl_dist_optfile", "vm_args.-ssl_dist_optfile", [ @@ -338,6 +340,17 @@ end}. %% RPC %%-------------------------------------------------------------------- +%% RPC Mode. +{mapping, "rpc.mode", "emqx.rpc_mode", [ + {default, async}, + {datatype, {enum, [sync, async]}} +]}. + +{mapping, "rpc.async_batch_size", "gen_rpc.max_batch_size", [ + {default, 256}, + {datatype, integer} +]}. + %% RPC server port. {mapping, "rpc.tcp_server_port", "gen_rpc.tcp_server_port", [ {default, 5369}, @@ -350,6 +363,13 @@ end}. {datatype, integer} ]}. +%% Default TCP port for outgoing connections +{mapping, "rpc.tcp_client_num", "gen_rpc.tcp_client_num", [ + {default, 32}, + {datatype, integer}, + {validators, ["range:gt_0_lt_256"]} +]}. + %% Client connect timeout {mapping, "rpc.connect_timeout", "gen_rpc.connect_timeout", [ {default, "5s"}, @@ -392,6 +412,28 @@ end}. {datatype, integer} ]}. +%% Size of TCP send buffer +{mapping, "rpc.socket_sndbuf", "gen_rpc.socket_sndbuf", [ + {default, "1MB"}, + {datatype, bytesize} +]}. + +%% Size of TCP receive buffer +{mapping, "rpc.socket_recbuf", "gen_rpc.socket_recbuf", [ + {default, "1MB"}, + {datatype, bytesize} +]}. + +%% Size of TCP receive buffer +{mapping, "rpc.socket_buffer", "gen_rpc.socket_buffer", [ + {default, "1MB"}, + {datatype, bytesize} +]}. + +{validator, "range:gt_0_lt_256", "must greater than 0 and less than 256", + fun(X) -> X > 0 andalso X < 256 end +}. + %%-------------------------------------------------------------------- %% Log %%-------------------------------------------------------------------- @@ -634,19 +676,19 @@ end}. ]}. %% @doc Whether the server supports MQTT retained messages. -{mapping, "mqtt.retain_available", "emqx.mqtt_retain_available", [ +{mapping, "mqtt.retain_available", "emqx.retain_available", [ {default, true}, {datatype, {enum, [true, false]}} ]}. %% @doc Whether the Server supports MQTT Wildcard Subscriptions. -{mapping, "mqtt.wildcard_subscription", "emqx.mqtt_wildcard_subscription", [ +{mapping, "mqtt.wildcard_subscription", "emqx.wildcard_subscription", [ {default, true}, {datatype, {enum, [true, false]}} ]}. %% @doc Whether the Server supports MQTT Shared Subscriptions. -{mapping, "mqtt.shared_subscription", "emqx.mqtt_shared_subscription", [ +{mapping, "mqtt.shared_subscription", "emqx.shared_subscription", [ {default, true}, {datatype, {enum, [true, false]}} ]}. @@ -876,7 +918,7 @@ end}. {translation, "emqx.zones", fun(Conf) -> Mapping = fun("retain_available", Val) -> - {mqtt_retain_available, Val}; + {retain_available, Val}; ("flapping_threshold", Val) -> [Limit, Duration] = string:tokens(Val, ", "), FlappingThreshold = case cuttlefish_duration:parse(Duration, s) of @@ -887,9 +929,9 @@ end}. end, {flapping_threshold, FlappingThreshold}; ("wildcard_subscription", Val) -> - {mqtt_wildcard_subscription, Val}; + {wildcard_subscription, Val}; ("shared_subscription", Val) -> - {mqtt_shared_subscription, Val}; + {shared_subscription, Val}; ("publish_limit", Val) -> [Limit, Duration] = string:tokens(Val, ", "), PubLimit = case cuttlefish_duration:parse(Duration, s) of diff --git a/rebar.config b/rebar.config index c28a43276..0f49084bc 100644 --- a/rebar.config +++ b/rebar.config @@ -2,10 +2,9 @@ [{jsx, "2.9.0"}, % hex {cowboy, "2.6.1"}, % hex {gproc, "0.8.0"}, % hex - {replayq, "0.1.1"}, %hex {esockd, "5.5.0"}, %hex - {ekka, {git, "https://github.com/emqx/ekka", {tag, "v0.5.8"}}}, - {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.4.0"}}}, + {ekka, "0.6.0"}, %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"}}} ]}. @@ -22,16 +21,17 @@ {cover_opts, [verbose]}. {cover_export_enabled, true}. -{plugins, [coveralls]}. +{plugins, [coveralls, + rebar3_proper]}. {erl_first_files, ["src/emqx_logger.erl"]}. {profiles, [{test, [{deps, - [{meck, "0.8.13"}, % hex - {bbmustache, "1.7.0"}, % hex - {emqx_ct_helpers, "1.1.3"} % hex + [{bbmustache, "1.7.0"}, % hex + {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "v1.0.1"}}}, + {emqx_ct_helpers, {git, "https://github.com/emqx/emqx-ct-helpers", {branch, "develop"}}} ]} ]} ]}. diff --git a/test/run_emqx.escript b/scripts/run_emqx.escript similarity index 60% rename from test/run_emqx.escript rename to scripts/run_emqx.escript index e8d1e2aae..10767820b 100644 --- a/test/run_emqx.escript +++ b/scripts/run_emqx.escript @@ -4,6 +4,10 @@ main(_) -> start(). start() -> + ok = application:load(mnesia), + MnesiaName = lists:concat(["Mnesia.", atom_to_list(node())]), + MnesiaDir = filename:join(["_build", "data", MnesiaName]), + ok = application:set_env(mnesia, dir, MnesiaDir), SpecEmqxConfig = fun(_) -> ok end, start(SpecEmqxConfig). diff --git a/src/emqx.app.src b/src/emqx.app.src index c88be22c7..0d58e4dd3 100644 --- a/src/emqx.app.src +++ b/src/emqx.app.src @@ -1,12 +1,14 @@ -{application,emqx, - [{description,"EMQ X Broker"}, - {vsn,"git"}, - {modules,[]}, - {registered,[emqx_sup]}, - {applications,[kernel,stdlib,jsx,gproc,gen_rpc,esockd,cowboy, - replayq,sasl,os_mon]}, - {env,[]}, - {mod,{emqx_app,[]}}, - {maintainers,["Feng Lee "]}, - {licenses,["Apache-2.0"]}, - {links,[{"Github","https://github.com/emqx/emqx"}]}]}. +{application, emqx, [ + {id, "emqx"}, + {vsn, "git"}, + {description, "EMQ X Broker"}, + {modules, []}, + {registered, []}, + {applications, [kernel,stdlib,jsx,gproc,gen_rpc,esockd,cowboy, + sasl,os_mon]}, + {env, []}, + {mod, {emqx_app,[]}}, + {maintainers, ["Feng Lee "]}, + {licenses, ["Apache-2.0"]}, + {links, [{"Github", "https://github.com/emqx/emqx"}]} +]}. diff --git a/src/emqx.erl b/src/emqx.erl index 3c7622887..79356d38b 100644 --- a/src/emqx.erl +++ b/src/emqx.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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). @@ -61,9 +63,13 @@ -define(APP, ?MODULE). -%%------------------------------------------------------------------------------ +-define(COPYRIGHT, "Copyright (c) 2019 EMQ Technologies Co., Ltd"). + +-define(LICENSE_MESSAGE, "Licensed under the Apache License, Version 2.0"). + +%%-------------------------------------------------------------------- %% Bootstrap, is_running... -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% @doc Start emqx application -spec(start() -> {ok, list(atom())} | {error, term()}). @@ -95,9 +101,9 @@ is_running(Node) -> Pid when is_pid(Pid) -> true end. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% PubSub API -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- -spec(subscribe(emqx_topic:topic() | string()) -> ok). subscribe(Topic) -> @@ -114,7 +120,7 @@ subscribe(Topic, SubOpts) when is_map(SubOpts) -> subscribe(Topic, SubId, SubOpts) when (is_atom(SubId) orelse is_binary(SubId)), is_map(SubOpts) -> emqx_broker:subscribe(iolist_to_binary(Topic), SubId, SubOpts). --spec(publish(emqx_types:message()) -> emqx_types:deliver_results()). +-spec(publish(emqx_types:message()) -> emqx_types:publish_result()). publish(Msg) -> emqx_broker:publish(Msg). @@ -122,9 +128,9 @@ publish(Msg) -> unsubscribe(Topic) -> emqx_broker:unsubscribe(iolist_to_binary(Topic)). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% PubSub management API -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- -spec(topics() -> list(emqx_topic:topic())). topics() -> emqx_router:topics(). @@ -143,9 +149,9 @@ subscribed(SubPid, Topic) when is_pid(SubPid) -> subscribed(SubId, Topic) when is_atom(SubId); is_binary(SubId) -> emqx_broker:subscribed(SubId, iolist_to_binary(Topic)). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Hooks API -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- -spec(hook(emqx_hooks:hookpoint(), emqx_hooks:action()) -> ok | {error, already_exists}). hook(HookPoint, Action) -> @@ -177,9 +183,9 @@ run_hook(HookPoint, Args) -> run_fold_hook(HookPoint, Args, Acc) -> emqx_hooks:run_fold(HookPoint, Args, Acc). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Shutdown and reboot -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- shutdown() -> shutdown(normal). @@ -193,12 +199,13 @@ shutdown(Reason) -> reboot() -> lists:foreach(fun application:start/1, [gproc, esockd, ranch, cowboy, ekka, emqx]). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Internal functions -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- reload_config(ConfFile) -> {ok, [Conf]} = file:consult(ConfFile), lists:foreach(fun({App, Vals}) -> [application:set_env(App, Par, Val) || {Par, Val} <- Vals] end, Conf). + diff --git a/src/emqx_access_control.erl b/src/emqx_access_control.erl index 7d849f493..06e8f5ed8 100644 --- a/src/emqx_access_control.erl +++ b/src/emqx_access_control.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_access_control). @@ -22,55 +24,61 @@ , reload_acl/0 ]). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% APIs -%%------------------------------------------------------------------------------ --spec(authenticate(emqx_types:credentials()) - -> {ok, emqx_types:credentials()} | {error, term()}). -authenticate(Credentials) -> - case emqx_hooks:run_fold('client.authenticate', [], init_auth_result(Credentials)) of - #{auth_result := success, anonymous := true} = NewCredentials -> - emqx_metrics:inc('auth.mqtt.anonymous'), - {ok, NewCredentials}; - #{auth_result := success} = NewCredentials -> - {ok, NewCredentials}; - NewCredentials -> - {error, maps:get(auth_result, NewCredentials, unknown_error)} - end. +%%-------------------------------------------------------------------- -%% @doc Check ACL --spec(check_acl(emqx_types:credentials(), emqx_types:pubsub(), emqx_types:topic()) -> allow | deny). -check_acl(Credentials, PubSub, Topic) -> - case emqx_acl_cache:is_enabled() of - false -> - do_check_acl(Credentials, PubSub, Topic); - true -> - case emqx_acl_cache:get_acl_cache(PubSub, Topic) of - not_found -> - AclResult = do_check_acl(Credentials, PubSub, Topic), - emqx_acl_cache:put_acl_cache(PubSub, Topic, AclResult), - AclResult; - AclResult -> - AclResult - end +-spec(authenticate(emqx_types:client()) + -> {ok, #{auth_result := emqx_types:auth_result(), + anonymous := boolean}} | {error, term()}). +authenticate(Client) -> + case emqx_hooks:run_fold('client.authenticate', + [Client], default_auth_result(maps:get(zone, Client, undefined))) of + Result = #{auth_result := success, anonymous := true} -> + emqx_metrics:inc('auth.mqtt.anonymous'), + {ok, Result}; + Result = #{auth_result := success} -> + {ok, Result}; + Result -> + {error, maps:get(auth_result, Result, unknown_error)} end. -do_check_acl(#{zone := Zone} = Credentials, PubSub, Topic) -> - case emqx_hooks:run_fold('client.check_acl', [Credentials, PubSub, Topic], - emqx_zone:get_env(Zone, acl_nomatch, deny)) of - allow -> allow; - _ -> deny +%% @doc Check ACL +-spec(check_acl(emqx_types:cient(), emqx_types:pubsub(), emqx_types:topic()) + -> allow | deny). +check_acl(Client, PubSub, Topic) -> + case emqx_acl_cache:is_enabled() of + true -> + check_acl_cache(Client, PubSub, Topic); + false -> + do_check_acl(Client, PubSub, Topic) + end. + +check_acl_cache(Client, PubSub, Topic) -> + case emqx_acl_cache:get_acl_cache(PubSub, Topic) of + not_found -> + AclResult = do_check_acl(Client, PubSub, Topic), + emqx_acl_cache:put_acl_cache(PubSub, Topic, AclResult), + AclResult; + AclResult -> AclResult + end. + +do_check_acl(#{zone := Zone} = Client, PubSub, Topic) -> + Default = emqx_zone:get_env(Zone, acl_nomatch, deny), + case emqx_hooks:run_fold('client.check_acl', [Client, PubSub, Topic], Default) of + allow -> allow; + _Other -> deny end. -spec(reload_acl() -> ok | {error, term()}). reload_acl() -> - emqx_acl_cache:is_enabled() andalso - emqx_acl_cache:empty_acl_cache(), + emqx_acl_cache:is_enabled() + andalso emqx_acl_cache:empty_acl_cache(), emqx_mod_acl_internal:reload_acl(). -init_auth_result(Credentials) -> - case emqx_zone:get_env(maps:get(zone, Credentials, undefined), allow_anonymous, false) of - true -> Credentials#{auth_result => success, anonymous => true}; - false -> Credentials#{auth_result => not_authorized, anonymous => false} +default_auth_result(Zone) -> + case emqx_zone:get_env(Zone, allow_anonymous, false) of + true -> #{auth_result => success, anonymous => true}; + false -> #{auth_result => not_authorized, anonymous => false} end. diff --git a/src/emqx_access_rule.erl b/src/emqx_access_rule.erl index 5d40341cc..dbde32984 100644 --- a/src/emqx_access_rule.erl +++ b/src/emqx_access_rule.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_access_rule). @@ -21,6 +23,8 @@ , compile/1 ]). +-export_type([rule/0]). + -type(acl_result() :: allow | deny). -type(who() :: all | binary() | @@ -33,15 +37,9 @@ -type(rule() :: {acl_result(), all} | {acl_result(), who(), access(), list(emqx_topic:topic())}). --export_type([rule/0]). - -define(ALLOW_DENY(A), ((A =:= allow) orelse (A =:= deny))). -define(PUBSUB(A), ((A =:= subscribe) orelse (A =:= publish) orelse (A =:= pubsub))). -%%------------------------------------------------------------------------------ -%% APIs -%%------------------------------------------------------------------------------ - %% @doc Compile Access Rule. compile({A, all}) when ?ALLOW_DENY(A) -> {A, all}; @@ -88,23 +86,23 @@ bin(B) when is_binary(B) -> B. %% @doc Match access rule --spec(match(emqx_types:credentials(), emqx_types:topic(), rule()) +-spec(match(emqx_types:client(), emqx_types:topic(), rule()) -> {matched, allow} | {matched, deny} | nomatch). -match(_Credentials, _Topic, {AllowDeny, all}) when ?ALLOW_DENY(AllowDeny) -> +match(_Client, _Topic, {AllowDeny, all}) when ?ALLOW_DENY(AllowDeny) -> {matched, AllowDeny}; -match(Credentials, Topic, {AllowDeny, Who, _PubSub, TopicFilters}) +match(Client, Topic, {AllowDeny, Who, _PubSub, TopicFilters}) when ?ALLOW_DENY(AllowDeny) -> - case match_who(Credentials, Who) - andalso match_topics(Credentials, Topic, TopicFilters) of + case match_who(Client, Who) + andalso match_topics(Client, Topic, TopicFilters) of true -> {matched, AllowDeny}; false -> nomatch end. -match_who(_Credentials, all) -> +match_who(_Client, all) -> true; -match_who(_Credentials, {user, all}) -> +match_who(_Client, {user, all}) -> true; -match_who(_Credentials, {client, all}) -> +match_who(_Client, {client, all}) -> true; match_who(#{client_id := ClientId}, {client, ClientId}) -> true; @@ -114,44 +112,44 @@ match_who(#{peername := undefined}, {ipaddr, _Tup}) -> false; match_who(#{peername := {IP, _}}, {ipaddr, CIDR}) -> esockd_cidr:match(IP, CIDR); -match_who(Credentials, {'and', Conds}) when is_list(Conds) -> +match_who(Client, {'and', Conds}) when is_list(Conds) -> lists:foldl(fun(Who, Allow) -> - match_who(Credentials, Who) andalso Allow + match_who(Client, Who) andalso Allow end, true, Conds); -match_who(Credentials, {'or', Conds}) when is_list(Conds) -> +match_who(Client, {'or', Conds}) when is_list(Conds) -> lists:foldl(fun(Who, Allow) -> - match_who(Credentials, Who) orelse Allow + match_who(Client, Who) orelse Allow end, false, Conds); -match_who(_Credentials, _Who) -> +match_who(_Client, _Who) -> false. -match_topics(_Credentials, _Topic, []) -> +match_topics(_Client, _Topic, []) -> false; -match_topics(Credentials, Topic, [{pattern, PatternFilter}|Filters]) -> - TopicFilter = feed_var(Credentials, PatternFilter), +match_topics(Client, Topic, [{pattern, PatternFilter}|Filters]) -> + TopicFilter = feed_var(Client, PatternFilter), match_topic(emqx_topic:words(Topic), TopicFilter) - orelse match_topics(Credentials, Topic, Filters); -match_topics(Credentials, Topic, [TopicFilter|Filters]) -> + orelse match_topics(Client, Topic, Filters); +match_topics(Client, Topic, [TopicFilter|Filters]) -> match_topic(emqx_topic:words(Topic), TopicFilter) - orelse match_topics(Credentials, Topic, Filters). + orelse match_topics(Client, Topic, Filters). match_topic(Topic, {eq, TopicFilter}) -> Topic == TopicFilter; match_topic(Topic, TopicFilter) -> emqx_topic:match(Topic, TopicFilter). -feed_var(Credentials, Pattern) -> - feed_var(Credentials, Pattern, []). -feed_var(_Credentials, [], Acc) -> +feed_var(Client, Pattern) -> + feed_var(Client, Pattern, []). +feed_var(_Client, [], Acc) -> lists:reverse(Acc); -feed_var(Credentials = #{client_id := undefined}, [<<"%c">>|Words], Acc) -> - feed_var(Credentials, Words, [<<"%c">>|Acc]); -feed_var(Credentials = #{client_id := ClientId}, [<<"%c">>|Words], Acc) -> - feed_var(Credentials, Words, [ClientId |Acc]); -feed_var(Credentials = #{username := undefined}, [<<"%u">>|Words], Acc) -> - feed_var(Credentials, Words, [<<"%u">>|Acc]); -feed_var(Credentials = #{username := Username}, [<<"%u">>|Words], Acc) -> - feed_var(Credentials, Words, [Username|Acc]); -feed_var(Credentials, [W|Words], Acc) -> - feed_var(Credentials, Words, [W|Acc]). +feed_var(Client = #{client_id := undefined}, [<<"%c">>|Words], Acc) -> + feed_var(Client, Words, [<<"%c">>|Acc]); +feed_var(Client = #{client_id := ClientId}, [<<"%c">>|Words], Acc) -> + feed_var(Client, Words, [ClientId |Acc]); +feed_var(Client = #{username := undefined}, [<<"%u">>|Words], Acc) -> + feed_var(Client, Words, [<<"%u">>|Acc]); +feed_var(Client = #{username := Username}, [<<"%u">>|Words], Acc) -> + feed_var(Client, Words, [Username|Acc]); +feed_var(Client, [W|Words], Acc) -> + feed_var(Client, Words, [W|Acc]). diff --git a/src/emqx_acl_cache.erl b/src/emqx_acl_cache.erl index 92d4d8328..b417f16fa 100644 --- a/src/emqx_acl_cache.erl +++ b/src/emqx_acl_cache.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_acl_cache). @@ -124,6 +126,7 @@ map_acl_cache(Fun) -> %%-------------------------------------------------------------------- %% Internal functions %%-------------------------------------------------------------------- + add_acl(PubSub, Topic, AclResult) -> K = cache_k(PubSub, Topic), V = cache_v(AclResult), diff --git a/src/emqx_alarm_handler.erl b/src/emqx_alarm_handler.erl index cf5f79ee5..c01003c0d 100644 --- a/src/emqx_alarm_handler.erl +++ b/src/emqx_alarm_handler.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_alarm_handler). @@ -47,9 +49,9 @@ -define(ALARM_TAB, emqx_alarm). -define(ALARM_HISTORY_TAB, emqx_alarm_history). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Mnesia bootstrap -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- mnesia(boot) -> ok = ekka_mnesia:create_table(?ALARM_TAB, [ @@ -64,13 +66,14 @@ mnesia(boot) -> {local_content, true}, {record_name, alarm_history}, {attributes, record_info(fields, alarm_history)}]); + mnesia(copy) -> ok = ekka_mnesia:copy_table(?ALARM_TAB), ok = ekka_mnesia:copy_table(?ALARM_HISTORY_TAB). -%%---------------------------------------------------------------------- +%%-------------------------------------------------------------------- %% API -%%---------------------------------------------------------------------- +%%-------------------------------------------------------------------- load() -> gen_event:swap_handler(alarm_handler, {alarm_handler, swap}, {?MODULE, []}). @@ -89,13 +92,14 @@ get_alarms(history) -> Alarms = ets:tab2list(?ALARM_HISTORY_TAB), [{Id, Desc, ClearAt} || #alarm_history{id = Id, desc = Desc, clear_at = ClearAt} <- Alarms]. -%%---------------------------------------------------------------------- +%%-------------------------------------------------------------------- %% gen_event callbacks -%%---------------------------------------------------------------------- +%%-------------------------------------------------------------------- init({_Args, {alarm_handler, ExistingAlarms}}) -> init_tables(ExistingAlarms), {ok, []}; + init(_) -> init_tables([]), {ok, []}. @@ -188,7 +192,7 @@ clear_alarm_(Id) -> end. set_alarm_history(Id, Desc) -> - mnesia:dirty_write(?ALARM_HISTORY_TAB, #alarm_history{id = Id, - desc = Desc, - clear_at = os:timestamp()}). - + His = #alarm_history{id = Id, + desc = Desc, + clear_at = os:timestamp()}, + mnesia:dirty_write(?ALARM_HISTORY_TAB, His). diff --git a/src/emqx_app.erl b/src/emqx_app.erl index f2c6b6dec..29b8d9b49 100644 --- a/src/emqx_app.erl +++ b/src/emqx_app.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_app). @@ -30,10 +32,10 @@ start(_Type, _Args) -> print_banner(), ekka:start(), {ok, Sup} = emqx_sup:start_link(), - emqx_modules:load(), - emqx_plugins:init(), + ok = emqx_modules:load(), + ok = emqx_plugins:init(), emqx_plugins:load(), - emqx_listeners:start(), + ok = emqx_listeners:start(), start_autocluster(), register(emqx, self()), diff --git a/src/emqx_banned.erl b/src/emqx_banned.erl index 11378461c..ba3743385 100644 --- a/src/emqx_banned.erl +++ b/src/emqx_banned.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_banned). @@ -30,9 +32,10 @@ -export([start_link/0]). --export([ add/1 +-export([ check/1 + , add/1 , delete/1 - , check/1 + , info/1 ]). %% gen_server callbacks @@ -44,14 +47,14 @@ , code_change/3 ]). --define(TAB, ?MODULE). +-define(BANNED_TAB, ?MODULE). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Mnesia bootstrap -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- mnesia(boot) -> - ok = ekka_mnesia:create_table(?TAB, [ + ok = ekka_mnesia:create_table(?BANNED_TAB, [ {type, set}, {disc_copies, [node()]}, {record_name, banned}, @@ -59,32 +62,35 @@ mnesia(boot) -> {storage_properties, [{ets, [{read_concurrency, true}]}]}]); mnesia(copy) -> - ok = ekka_mnesia:copy_table(?TAB). + ok = ekka_mnesia:copy_table(?BANNED_TAB). %% @doc Start the banned server. -spec(start_link() -> startlink_ret()). start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). --spec(check(emqx_types:credentials()) -> boolean()). +-spec(check(emqx_types:client()) -> boolean()). check(#{client_id := ClientId, username := Username, peername := {IPAddr, _}}) -> - ets:member(?TAB, {client_id, ClientId}) - orelse ets:member(?TAB, {username, Username}) - orelse ets:member(?TAB, {ipaddr, IPAddr}). + ets:member(?BANNED_TAB, {client_id, ClientId}) + orelse ets:member(?BANNED_TAB, {username, Username}) + orelse ets:member(?BANNED_TAB, {ipaddr, IPAddr}). -spec(add(emqx_types:banned()) -> ok). add(Banned) when is_record(Banned, banned) -> - mnesia:dirty_write(?TAB, Banned). + mnesia:dirty_write(?BANNED_TAB, Banned). --spec(delete({client_id, emqx_types:client_id()} - | {username, emqx_types:username()} - | {peername, emqx_types:peername()}) -> ok). +-spec(delete({client_id, emqx_types:client_id()} | + {username, emqx_types:username()} | + {peername, emqx_types:peername()}) -> ok). delete(Key) -> - mnesia:dirty_delete(?TAB, Key). + mnesia:dirty_delete(?BANNED_TAB, Key). -%%------------------------------------------------------------------------------ +info(InfoKey) -> + mnesia:table_info(?BANNED_TAB, InfoKey). + +%%-------------------------------------------------------------------- %% gen_server callbacks -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- init([]) -> {ok, ensure_expiry_timer(#{expiry_timer => undefined})}. @@ -98,7 +104,8 @@ handle_cast(Msg, State) -> {noreply, State}. handle_info({timeout, TRef, expire}, State = #{expiry_timer := TRef}) -> - mnesia:async_dirty(fun expire_banned_items/1, [erlang:system_time(second)]), + mnesia:async_dirty(fun expire_banned_items/1, + [erlang:system_time(second)]), {noreply, ensure_expiry_timer(State), hibernate}; handle_info(Info, State) -> @@ -111,9 +118,9 @@ terminate(_Reason, #{expiry_timer := TRef}) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Internal functions -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- -ifdef(TEST). ensure_expiry_timer(State) -> @@ -126,6 +133,7 @@ ensure_expiry_timer(State) -> expire_banned_items(Now) -> mnesia:foldl( fun(B = #banned{until = Until}, _Acc) when Until < Now -> - mnesia:delete_object(?TAB, B, sticky_write); + mnesia:delete_object(?BANNED_TAB, B, sticky_write); (_, _Acc) -> ok - end, ok, ?TAB). + end, ok, ?BANNED_TAB). + diff --git a/src/emqx_base62.erl b/src/emqx_base62.erl index 77d04fc4d..979821d06 100644 --- a/src/emqx_base62.erl +++ b/src/emqx_base62.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,26 +12,25 @@ %% 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). %% APIs -export([ encode/1 - , encode/2 , decode/1 - , decode/2 ]). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% APIs -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% @doc Encode any data to base62 binary -spec encode(string() | integer() | binary()) -> binary(). encode(I) when is_integer(I) -> encode(integer_to_binary(I)); encode(S) when is_list(S)-> - encode(list_to_binary(S)); + encode(unicode:characters_to_binary(S)); encode(B) when is_binary(B) -> encode(B, <<>>). @@ -38,17 +38,13 @@ encode(B) when is_binary(B) -> %% binary_to_list(encode(D)). %% @doc Decode base62 binary to origin data binary -decode(L) when is_list(L) -> - decode(list_to_binary(L)); decode(B) when is_binary(B) -> decode(B, <<>>). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Interval Functions -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- -encode(D, string) -> - binary_to_list(encode(D)); encode(<>, Acc) -> CharList = [encode_char(Index1), encode_char(Index2), encode_char(Index3), encode_char(Index4)], NewAcc = <>, @@ -64,10 +60,6 @@ encode(<>, Acc) -> encode(<<>>, Acc) -> Acc. -decode(D, integer) -> - binary_to_integer(decode(D)); -decode(D, string) -> - binary_to_list(decode(D)); decode(<>, Acc) when bit_size(Rest) >= 8-> case Head == $9 of @@ -112,4 +104,3 @@ decode_char(I) when I >= $A andalso I =< $Z-> decode_char(9, I) -> I + 61 - $A. - diff --git a/src/emqx_batch.erl b/src/emqx_batch.erl index b7e341367..c5ea3c598 100644 --- a/src/emqx_batch.erl +++ b/src/emqx_batch.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_batch). @@ -22,46 +24,49 @@ , items/1 ]). --record(batch, - { batch_size :: non_neg_integer() - , batch_q :: list(any()) - , linger_ms :: pos_integer() - , linger_timer :: reference() | undefined - , commit_fun :: function() - }). +-export_type([options/0, batch/0]). --type(options() :: - #{ batch_size => non_neg_integer() - , linger_ms => pos_integer() - , commit_fun := function() +-record(batch, { + batch_size :: non_neg_integer(), + batch_q :: list(any()), + linger_ms :: pos_integer(), + linger_timer :: reference() | undefined, + commit_fun :: function() + }). + +-type(options() :: #{ + batch_size => non_neg_integer(), + linger_ms => pos_integer(), + commit_fun := function() }). -opaque(batch() :: #batch{}). --export_type([options/0]). - --export_type([batch/0]). - -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% APIs -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- -spec(init(options()) -> batch()). init(Opts) when is_map(Opts) -> #batch{batch_size = maps:get(batch_size, Opts, 1000), batch_q = [], - linger_ms = maps:get(linger_ms, Opts, 1000), + linger_ms = maps:get(linger_ms, Opts, 1000), commit_fun = maps:get(commit_fun, Opts)}. -spec(push(any(), batch()) -> batch()). -push(El, Batch = #batch{batch_q = Q, linger_ms = Ms, linger_timer = undefined}) when length(Q) == 0 -> - Batch#batch{batch_q = [El], linger_timer = erlang:send_after(Ms, self(), batch_linger_expired)}; +push(El, Batch = #batch{batch_q = Q, + linger_ms = Ms, + linger_timer = undefined}) + when length(Q) == 0 -> + TRef = erlang:send_after(Ms, self(), batch_linger_expired), + Batch#batch{batch_q = [El], linger_timer = TRef}; %% no limit. push(El, Batch = #batch{batch_size = 0, batch_q = Q}) -> Batch#batch{batch_q = [El|Q]}; -push(El, Batch = #batch{batch_size = MaxSize, batch_q = Q}) when length(Q) >= MaxSize -> +push(El, Batch = #batch{batch_size = MaxSize, batch_q = Q}) + when length(Q) >= MaxSize -> commit(Batch#batch{batch_q = [El|Q]}); push(El, Batch = #batch{batch_q = Q}) -> diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index b9a4eb1bd..3992a0b35 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_broker). @@ -191,7 +193,7 @@ do_unsubscribe(Group, Topic, SubPid, _SubOpts) -> %% Publish %%------------------------------------------------------------------------------ --spec(publish(emqx_types:message()) -> emqx_types:deliver_results()). +-spec(publish(emqx_types:message()) -> emqx_types:publish_result()). publish(Msg) when is_record(Msg, message) -> _ = emqx_tracer:trace(publish, Msg), Headers = Msg#message.headers, @@ -200,8 +202,7 @@ publish(Msg) when is_record(Msg, message) -> ?LOG(notice, "Publishing interrupted: ~s", [emqx_message:format(Msg)]), []; #message{topic = Topic} = Msg1 -> - Delivery = route(aggre(emqx_router:match_routes(Topic)), delivery(Msg1)), - Delivery#delivery.results + route(aggre(emqx_router:match_routes(Topic)), delivery(Msg1)) end. %% Called internally @@ -217,27 +218,27 @@ safe_publish(Msg) when is_record(Msg, message) -> end. delivery(Msg) -> - #delivery{sender = self(), message = Msg, results = []}. + #delivery{sender = self(), message = Msg}. %%------------------------------------------------------------------------------ %% Route %%------------------------------------------------------------------------------ - -route([], Delivery = #delivery{message = Msg}) -> +-spec(route([emqx_types:route_entry()], emqx_types:delivery()) -> emqx_types:publish_result()). +route([], #delivery{message = Msg}) -> emqx_hooks:run('message.dropped', [#{node => node()}, Msg]), - inc_dropped_cnt(Msg#message.topic), Delivery; - -route([{To, Node}], Delivery) when Node =:= node() -> - dispatch(To, Delivery); - -route([{To, Node}], Delivery = #delivery{results = Results}) when is_atom(Node) -> - forward(Node, To, Delivery#delivery{results = [{route, Node, To}|Results]}); - -route([{To, Group}], Delivery) when is_tuple(Group); is_binary(Group) -> - emqx_shared_sub:dispatch(Group, To, Delivery); - + inc_dropped_cnt(Msg#message.topic), + []; route(Routes, Delivery) -> - lists:foldl(fun(Route, Acc) -> route([Route], Acc) end, Delivery, Routes). + lists:foldl(fun(Route, Acc) -> + [do_route(Route, Delivery) | Acc] + end, [], Routes). + +do_route({To, Node}, Delivery) when Node =:= node() -> + {Node, To, dispatch(To, Delivery)}; +do_route({To, Node}, Delivery) when is_atom(Node) -> + {Node, To, forward(Node, To, Delivery, emqx_config:get_env(rpc_mode, async))}; +do_route({To, Group}, Delivery) when is_tuple(Group); is_binary(Group) -> + {share, To, emqx_shared_sub:dispatch(Group, To, Delivery)}. aggre([]) -> []; @@ -254,45 +255,58 @@ aggre(Routes) -> end, [], Routes). %% @doc Forward message to another node. -forward(Node, To, Delivery) -> - %% rpc:call to ensure the delivery, but the latency:( +-spec(forward(node(), emqx_types:topic(), emqx_types:delivery(), RPCMode::sync|async) + -> emqx_types:deliver_result()). +forward(Node, To, Delivery, async) -> + case emqx_rpc:cast(Node, ?BROKER, dispatch, [To, Delivery]) of + true -> ok; + {badrpc, Reason} -> + ?LOG(error, "Ansync forward msg to ~s failed: ~p", [Node, Reason]), + {error, badrpc} + end; + +forward(Node, To, Delivery, sync) -> case emqx_rpc:call(Node, ?BROKER, dispatch, [To, Delivery]) of {badrpc, Reason} -> - ?LOG(error, "Failed to forward msg to ~s: ~p", [Node, Reason]), - Delivery; - Delivery1 -> Delivery1 + ?LOG(error, "Sync forward msg to ~s failed: ~p", [Node, Reason]), + {error, badrpc}; + Result -> Result end. --spec(dispatch(emqx_topic:topic(), emqx_types:delivery()) -> emqx_types:delivery()). -dispatch(Topic, Delivery = #delivery{message = Msg, results = Results}) -> +-spec(dispatch(emqx_topic:topic(), emqx_types:delivery()) -> emqx_types:deliver_result()). +dispatch(Topic, #delivery{message = Msg}) -> case subscribers(Topic) of [] -> emqx_hooks:run('message.dropped', [#{node => node()}, Msg]), inc_dropped_cnt(Topic), - Delivery; + {error, no_subscribers}; [Sub] -> %% optimize? - Cnt = dispatch(Sub, Topic, Msg), - Delivery#delivery{results = [{dispatch, Topic, Cnt}|Results]}; + dispatch(Sub, Topic, Msg); Subs -> - Cnt = lists:foldl( - fun(Sub, Acc) -> - dispatch(Sub, Topic, Msg) + Acc - end, 0, Subs), - Delivery#delivery{results = [{dispatch, Topic, Cnt}|Results]} + lists:foldl( + fun(Sub, Res) -> + case dispatch(Sub, Topic, Msg) of + ok -> Res; + Err -> Err + end + end, ok, Subs) end. dispatch(SubPid, Topic, Msg) when is_pid(SubPid) -> case erlang:is_process_alive(SubPid) of true -> - SubPid ! {dispatch, Topic, Msg}, - 1; - false -> 0 + SubPid ! {deliver, Topic, Msg}, + ok; + false -> {error, subscriber_die} end; dispatch({shard, I}, Topic, Msg) -> lists:foldl( - fun(SubPid, Cnt) -> - dispatch(SubPid, Topic, Msg) + Cnt - end, 0, subscribers({shard, Topic, I})). + fun(SubPid, Res) -> + case dispatch(SubPid, Topic, Msg) of + ok -> Res; + Err -> Err + end + end, ok, subscribers({shard, Topic, I})). inc_dropped_cnt(<<"$SYS/", _/binary>>) -> ok; @@ -374,9 +388,9 @@ set_subopts(Topic, NewOpts) when is_binary(Topic), is_map(NewOpts) -> topics() -> emqx_router:topics(). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Stats fun -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- stats_fun() -> safe_update_stats(?SUBSCRIBER, 'subscribers.count', 'subscribers.max'), diff --git a/src/emqx_broker_helper.erl b/src/emqx_broker_helper.erl index 9e9b337cf..34e2de817 100644 --- a/src/emqx_broker_helper.erl +++ b/src/emqx_broker_helper.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_broker_helper). @@ -92,9 +94,9 @@ create_seq(Topic) -> reclaim_seq(Topic) -> emqx_sequence:reclaim(?SUBSEQ, Topic). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% gen_server callbacks -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- init([]) -> %% Helper table @@ -142,9 +144,9 @@ terminate(_Reason, _State) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Internal functions -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- clean_down(SubPid) -> case ets:lookup(?SUBMON, SubPid) of diff --git a/src/emqx_broker_sup.erl b/src/emqx_broker_sup.erl index 05154f393..325019ef6 100644 --- a/src/emqx_broker_sup.erl +++ b/src/emqx_broker_sup.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_broker_sup). @@ -23,9 +25,9 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Supervisor callbacks -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- init([]) -> %% Broker pool @@ -34,20 +36,20 @@ init([]) -> {emqx_broker, start_link, []}]), %% Shared subscription - SharedSub = #{id => shared_sub, - start => {emqx_shared_sub, start_link, []}, - restart => permanent, + SharedSub = #{id => shared_sub, + start => {emqx_shared_sub, start_link, []}, + restart => permanent, shutdown => 2000, - type => worker, - modules => [emqx_shared_sub]}, + type => worker, + modules => [emqx_shared_sub]}, %% Broker helper - Helper = #{id => helper, - start => {emqx_broker_helper, start_link, []}, - restart => permanent, + Helper = #{id => helper, + start => {emqx_broker_helper, start_link, []}, + restart => permanent, shutdown => 2000, - type => worker, - modules => [emqx_broker_helper]}, + type => worker, + modules => [emqx_broker_helper]}, {ok, {{one_for_all, 0, 1}, [BrokerPool, SharedSub, Helper]}}. diff --git a/src/emqx_channel.erl b/src/emqx_channel.erl index 5aaa62b30..630c4de6a 100644 --- a/src/emqx_channel.erl +++ b/src/emqx_channel.erl @@ -14,6 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- +%% MQTT TCP/SSL Channel -module(emqx_channel). -behaviour(gen_statem). @@ -21,6 +22,7 @@ -include("emqx.hrl"). -include("emqx_mqtt.hrl"). -include("logger.hrl"). +-include("types.hrl"). -logger_header("[Channel]"). @@ -32,15 +34,16 @@ , stats/1 ]). --export([kick/1]). +%% for Debug +-export([state/1]). --export([session/1]). - -%% gen_statem callbacks +%% state callbacks -export([ idle/3 , connected/3 + , disconnected/3 ]). +%% gen_statem callbacks -export([ init/1 , callback_mode/0 , code_change/4 @@ -48,28 +51,35 @@ ]). -record(state, { - transport, - socket, - peername, - sockname, - conn_state, - active_n, - proto_state, - parse_state, - gc_state, - keepalive, - rate_limit, - pub_limit, - limit_timer, - enable_stats, - stats_timer, - idle_timeout - }). + transport :: esockd:transport(), + socket :: esockd:socket(), + peername :: emqx_types:peername(), + sockname :: emqx_types:peername(), + conn_state :: running | blocked, + active_n :: pos_integer(), + rate_limit :: maybe(esockd_rate_limit:bucket()), + pub_limit :: maybe(esockd_rate_limit:bucket()), + limit_timer :: maybe(reference()), + serialize :: fun((emqx_types:packet()) -> iodata()), + parse_state :: emqx_frame:parse_state(), + proto_state :: emqx_protocol:proto_state(), + gc_state :: emqx_gc:gc_state(), + keepalive :: maybe(emqx_keepalive:keepalive()), + stats_timer :: disabled | maybe(reference()), + idle_timeout :: timeout(), + connected :: boolean(), + connected_at :: erlang:timestamp() + }). + +-type(state() :: #state{}). -define(ACTIVE_N, 100). -define(HANDLE(T, C, D), handle((T), (C), (D))). +-define(CHAN_STATS, [recv_pkt, recv_msg, send_pkt, send_msg]). -define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt, send_pend]). +-spec(start_link(esockd:transport(), esockd:socket(), proplists:proplist()) + -> {ok, pid()}). start_link(Transport, Socket, Options) -> {ok, proc_lib:spawn_link(?MODULE, init, [{Transport, Socket, Options}])}. @@ -77,11 +87,10 @@ start_link(Transport, Socket, Options) -> %% API %%-------------------------------------------------------------------- -%% For debug --spec(info(pid() | #state{}) -> map()). +%% @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, @@ -90,39 +99,57 @@ info(#state{transport = Transport, active_n = ActiveN, rate_limit = RateLimit, pub_limit = PubLimit, - proto_state = ProtoState}) -> - ConnInfo = #{socktype => Transport:type(Socket), + proto_state = ProtoState, + gc_state = GCState, + stats_timer = StatsTimer, + idle_timeout = IdleTimeout, + connected = Connected, + connected_at = ConnectedAt}) -> + ChanInfo = #{socktype => Transport:type(Socket), peername => Peername, sockname => Sockname, conn_state => ConnState, active_n => ActiveN, - rate_limit => rate_limit_info(RateLimit), - pub_limit => rate_limit_info(PubLimit) + rate_limit => limit_info(RateLimit), + pub_limit => limit_info(PubLimit), + gc_state => emqx_gc:info(GCState), + enable_stats => case StatsTimer of + disabled -> false; + _Otherwise -> true + end, + idle_timeout => IdleTimeout, + connected => Connected, + connected_at => ConnectedAt }, - ProtoInfo = emqx_protocol:info(ProtoState), - maps:merge(ConnInfo, ProtoInfo). + maps:merge(ChanInfo, emqx_protocol:info(ProtoState)). -rate_limit_info(undefined) -> - #{}; -rate_limit_info(Limit) -> +limit_info(undefined) -> + undefined; +limit_info(Limit) -> esockd_rate_limit:info(Limit). -%% For dashboard +%% @doc Get attrs of the channel. +-spec(attrs(pid() | state()) -> emqx_types:attrs()). attrs(CPid) when is_pid(CPid) -> call(CPid, attrs); - -attrs(#state{peername = Peername, +attrs(#state{transport = Transport, + socket = Socket, + peername = Peername, sockname = Sockname, - proto_state = ProtoState}) -> - SockAttrs = #{peername => Peername, - sockname => Sockname}, - ProtoAttrs = emqx_protocol:attrs(ProtoState), - maps:merge(SockAttrs, ProtoAttrs). + proto_state = ProtoState, + connected = Connected, + connected_at = ConnectedAt}) -> + ConnAttrs = #{socktype => Transport:type(Socket), + peername => Peername, + sockname => Sockname, + connected => Connected, + connected_at => ConnectedAt}, + maps:merge(ConnAttrs, emqx_protocol:attrs(ProtoState)). -%% Conn stats +%% @doc Get stats of the channel. +-spec(stats(pid() | state()) -> emqx_types:stats()). stats(CPid) when is_pid(CPid) -> call(CPid, stats); - stats(#state{transport = Transport, socket = Socket, proto_state = ProtoState}) -> @@ -130,16 +157,13 @@ stats(#state{transport = Transport, {ok, Ss} -> Ss; {error, _} -> [] end, - lists:append([SockStats, - emqx_misc:proc_stats(), - emqx_protocol:stats(ProtoState)]). + ChanStats = [{Name, emqx_pd:get_counter(Name)} || Name <- ?CHAN_STATS], + SessStats = emqx_session:stats(emqx_protocol:info(session, ProtoState)), + lists:append([SockStats, ChanStats, SessStats, emqx_misc:proc_stats()]). -kick(CPid) -> - call(CPid, kick). - -session(CPid) -> - call(CPid, session). +state(CPid) -> call(CPid, get_state). +%% @private call(CPid, Req) -> gen_statem:call(CPid, Req, infinity). @@ -148,7 +172,6 @@ call(CPid, Req) -> %%-------------------------------------------------------------------- init({Transport, RawSocket, Options}) -> - process_flag(trap_exit, true), {ok, Socket} = Transport:wait(RawSocket), {ok, Peername} = Transport:ensure_ok_or_exit(peername, [Socket]), {ok, Sockname} = Transport:ensure_ok_or_exit(sockname, [Socket]), @@ -158,39 +181,33 @@ init({Transport, RawSocket, 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), - SendFun = fun(Packet, Opts) -> - Data = emqx_frame:serialize(Packet, Opts), - case Transport:async_send(Socket, Data) of - ok -> {ok, Data}; - {error, Reason} -> - {error, Reason} - end - end, + MaxSize = emqx_zone:get_env(Zone, max_packet_size, ?MAX_PACKET_SIZE), + ParseState = emqx_frame:initial_parse_state(#{max_size => MaxSize}), ProtoState = emqx_protocol:init(#{peername => Peername, sockname => Sockname, peercert => Peercert, - sendfun => SendFun, conn_mod => ?MODULE}, Options), - 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), + ok = emqx_misc:init_proc_mng_policy(Zone), State = #state{transport = Transport, socket = Socket, peername = Peername, + sockname = Sockname, conn_state = running, active_n = ActiveN, rate_limit = RateLimit, pub_limit = PubLimit, - proto_state = ProtoState, parse_state = ParseState, + proto_state = ProtoState, gc_state = GcState, - enable_stats = EnableStats, - idle_timeout = IdleTimout + stats_timer = StatsTimer, + idle_timeout = IdleTimout, + connected = false }, - ok = emqx_misc:init_proc_mng_policy(Zone), gen_statem:enter_loop(?MODULE, [{hibernate_after, 2 * IdleTimout}], idle, State, self(), [IdleTimout]). @@ -206,16 +223,27 @@ callback_mode() -> %% Idle State idle(enter, _, State) -> - ok = activate_socket(State), - keep_state_and_data; + case activate_socket(State) of + ok -> keep_state_and_data; + {error, Reason} -> + shutdown(Reason, State) + end; idle(timeout, _Timeout, State) -> - {stop, {shutdown, idle_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) -> - handle_incoming(Packet, fun(NState) -> - {next_state, connected, NState} - end, State); + ?LOG(warning, "Unexpected incoming: ~p", [Packet]), + shutdown(unexpected_incoming_packet, State); idle(EventType, Content, State) -> ?HANDLE(EventType, Content, State). @@ -223,56 +251,76 @@ idle(EventType, Content, State) -> %%-------------------------------------------------------------------- %% Connected State -connected(enter, _, _State) -> - %% What to do? - keep_state_and_data; - -%% Handle Input -connected(cast, {incoming, Packet = ?PACKET(Type)}, State) -> - ok = emqx_metrics:inc_recv(Packet), - (Type == ?PUBLISH) andalso emqx_pd:update_counter(incoming_pubs, 1), - handle_incoming(Packet, fun(NState) -> {keep_state, NState} end, State); - -%% Handle Output -connected(info, {deliver, PubOrAck}, State = #state{proto_state = ProtoState}) -> - case emqx_protocol:deliver(PubOrAck, ProtoState) of - {ok, NProtoState} -> - NState = State#state{proto_state = NProtoState}, - {keep_state, maybe_gc(PubOrAck, NState)}; - {error, Reason} -> - shutdown(Reason, State) - end; - -%% Start Keepalive -connected(info, {keepalive, start, Interval}, - State = #state{transport = Transport, socket = Socket}) -> - StatFun = fun() -> - case Transport:getstat(Socket, [recv_oct]) of - {ok, [{recv_oct, RecvOct}]} -> {ok, RecvOct}; - Error -> Error - end - end, - case emqx_keepalive:start(StatFun, Interval, {keepalive, check}) of +connected(enter, _PrevSt, State = #state{proto_state = ProtoState}) -> + NState = State#state{connected = true, + connected_at = os:timestamp()}, + ClientId = emqx_protocol:info(client_id, ProtoState), + ok = emqx_cm:register_channel(ClientId), + ok = emqx_cm:set_chan_attrs(ClientId, info(NState)), + %% Ensure keepalive after connected successfully. + Interval = emqx_protocol:info(keepalive, ProtoState), + case ensure_keepalive(Interval, NState) of + ignore -> keep_state(NState); {ok, KeepAlive} -> - {keep_state, State#state{keepalive = KeepAlive}}; - {error, Error} -> - shutdown(Error, State) + keep_state(NState#state{keepalive = KeepAlive}); + {error, Reason} -> + shutdown(Reason, NState) end; +connected(cast, {incoming, Packet = ?PACKET(?CONNECT)}, State) -> + ?LOG(warning, "Unexpected connect: ~p", [Packet]), + shutdown(unexpected_incoming_connect, State); + +connected(cast, {incoming, Packet}, State) when is_record(Packet, mqtt_packet) -> + handle_incoming(Packet, fun keep_state/1, State); + +connected(info, Deliver = {deliver, _Topic, _Msg}, + State = #state{proto_state = ProtoState}) -> + Delivers = emqx_misc:drain_deliver([Deliver]), + case emqx_protocol:handle_deliver(Delivers, ProtoState) of + {ok, NProtoState} -> + keep_state(State#state{proto_state = NProtoState}); + {ok, Packets, NProtoState} -> + NState = State#state{proto_state = NProtoState}, + handle_outgoing(Packets, fun keep_state/1, NState); + {error, Reason} -> + shutdown(Reason, State); + {error, Reason, NProtoState} -> + shutdown(Reason, State#state{proto_state = NProtoState}) + end; + +%% TODO: Improve later. +connected(info, {subscribe, TopicFilters}, State) -> + handle_request({subscribe, TopicFilters}, State); + +connected(info, {unsubscribe, TopicFilters}, State) -> + handle_request({unsubscribe, TopicFilters}, State); + %% Keepalive timer connected(info, {keepalive, check}, State = #state{keepalive = KeepAlive}) -> case emqx_keepalive:check(KeepAlive) of {ok, KeepAlive1} -> - {keep_state, State#state{keepalive = KeepAlive1}}; + keep_state(State#state{keepalive = KeepAlive1}); {error, timeout} -> shutdown(keepalive_timeout, State); - {error, Error} -> - shutdown(Error, State) + {error, Reason} -> + shutdown(Reason, State) end; connected(EventType, Content, State) -> ?HANDLE(EventType, Content, State). +%%-------------------------------------------------------------------- +%% Disconnected State + +disconnected(enter, _, _State) -> + %% TODO: What to do? + %% CleanStart is true + keep_state_and_data; + +disconnected(EventType, Content, State) -> + ?HANDLE(EventType, Content, State). + %% Handle call handle({call, From}, info, State) -> reply(From, info(State), State); @@ -283,12 +331,16 @@ handle({call, From}, attrs, State) -> handle({call, From}, stats, State) -> reply(From, stats(State), State); -handle({call, From}, kick, State) -> - ok = gen_statem:reply(From, ok), - shutdown(kicked, State); +handle({call, From}, get_state, State) -> + reply(From, State, State); -handle({call, From}, session, State = #state{proto_state = ProtoState}) -> - reply(From, emqx_protocol:session(ProtoState), State); +%%handle({call, From}, kick, State) -> +%% ok = gen_statem:reply(From, ok), +%% shutdown(kicked, State); + +%%handle({call, From}, discard, State) -> +%% ok = gen_statem:reply(From, ok), +%% shutdown(discard, State); handle({call, From}, Req, State) -> ?LOG(error, "Unexpected call: ~p", [Req]), @@ -297,40 +349,49 @@ handle({call, From}, Req, State) -> %% Handle cast handle(cast, Msg, State) -> ?LOG(error, "Unexpected cast: ~p", [Msg]), - {keep_state, State}; + keep_state(State); -%% Handle Incoming -handle(info, {Inet, _Sock, Data}, State) when Inet == tcp; Inet == ssl -> +%% Handle incoming data +handle(info, {Inet, _Sock, Data}, State) when Inet == tcp; + Inet == ssl -> Oct = iolist_size(Data), ?LOG(debug, "RECV ~p", [Data]), emqx_pd:update_counter(incoming_bytes, Oct), ok = emqx_metrics:inc('bytes.received', Oct), - NState = ensure_stats_timer(maybe_gc({1, Oct}, State)), - process_incoming(Data, [], NState); + NState = maybe_gc(1, Oct, State), + process_incoming(Data, ensure_stats_timer(NState)); -handle(info, {Error, _Sock, Reason}, State) - when Error == tcp_error; Error == ssl_error -> +handle(info, {Error, _Sock, Reason}, State) when Error == tcp_error; + Error == ssl_error -> shutdown(Reason, State); -handle(info, {Closed, _Sock}, State) - when Closed == tcp_closed; Closed == ssl_closed -> +handle(info, {Closed, _Sock}, State) when Closed == tcp_closed; + Closed == ssl_closed -> shutdown(closed, State); handle(info, {Passive, _Sock}, State) when Passive == tcp_passive; Passive == ssl_passive -> %% Rate limit here:) NState = ensure_rate_limit(State), - ok = activate_socket(NState), - {keep_state, NState}; + case activate_socket(NState) of + ok -> keep_state(NState); + {error, Reason} -> + shutdown(Reason, NState) + end; handle(info, activate_socket, State) -> %% Rate limit timer expired. - ok = activate_socket(State#state{conn_state = running}), - {keep_state, State#state{conn_state = running, limit_timer = undefined}}; + NState = State#state{conn_state = running}, + case activate_socket(NState) of + ok -> + keep_state(NState#state{limit_timer = undefined}); + {error, Reason} -> + shutdown(Reason, NState) + end; handle(info, {inet_reply, _Sock, ok}, State) -> %% something sent - {keep_state, ensure_stats_timer(State)}; + keep_state(ensure_stats_timer(State)); handle(info, {inet_reply, _Sock, {error, Reason}}, State) -> shutdown(Reason, State); @@ -338,14 +399,14 @@ handle(info, {inet_reply, _Sock, {error, Reason}}, State) -> handle(info, {timeout, Timer, emit_stats}, State = #state{stats_timer = Timer, proto_state = ProtoState, - gc_state = GcState}) -> - ClientId = emqx_protocol:client_id(ProtoState), - emqx_cm:set_conn_stats(ClientId, stats(State)), + gc_state = GcState}) -> + ClientId = emqx_protocol:info(client_id, ProtoState), + ok = emqx_cm:set_chan_stats(ClientId, stats(State)), NState = State#state{stats_timer = undefined}, Limits = erlang:get(force_shutdown_policy), case emqx_misc:conn_proc_mng_policy(Limits) of continue -> - {keep_state, NState}; + keep_state(NState); hibernate -> %% going to hibernate, reset gc stats GcState1 = emqx_gc:reset(GcState), @@ -355,6 +416,20 @@ handle(info, {timeout, Timer, emit_stats}, shutdown(Reason, NState) end; +handle(info, {timeout, Timer, Msg}, + State = #state{proto_state = ProtoState}) -> + case emqx_protocol:handle_timeout(Timer, Msg, ProtoState) of + {ok, NProtoState} -> + keep_state(State#state{proto_state = NProtoState}); + {ok, Packets, NProtoState} -> + handle_outgoing(Packets, fun keep_state/1, + State#state{proto_state = NProtoState}); + {error, Reason} -> + shutdown(Reason, State); + {error, Reason, NProtoState} -> + shutdown(Reason, State#state{proto_state = NProtoState}) + end; + handle(info, {shutdown, discard, {ClientId, ByPid}}, State) -> ?LOG(error, "Discarded by ~s:~p", [ClientId, ByPid]), shutdown(discard, State); @@ -366,49 +441,48 @@ handle(info, {shutdown, conflict, {ClientId, NewPid}}, State) -> handle(info, {shutdown, Reason}, State) -> shutdown(Reason, State); -handle(info, Info = {'EXIT', SessionPid, Reason}, State = #state{proto_state = ProtoState}) -> - case emqx_protocol:session(ProtoState) of - undefined -> - ?LOG(error, "Unexpected EXIT: ~p", [Info]), - {keep_state, State}; - SessionPid -> - ?LOG(error, "Session ~p termiated: ~p", [SessionPid, Reason]), - shutdown(Reason, State) - end; - handle(info, Info, State) -> ?LOG(error, "Unexpected info: ~p", [Info]), - {keep_state, State}. + keep_state(State). code_change(_Vsn, State, Data, _Extra) -> {ok, State, Data}. -terminate(Reason, _StateName, #state{transport = Transport, - socket = Socket, - keepalive = KeepAlive, +terminate(Reason, _StateName, #state{transport = Transport, + socket = Socket, + keepalive = KeepAlive, proto_state = ProtoState}) -> ?LOG(debug, "Terminated for ~p", [Reason]), - Transport:fast_close(Socket), - emqx_keepalive:cancel(KeepAlive), - case {ProtoState, Reason} of - {undefined, _} -> ok; - {_, {shutdown, Error}} -> - emqx_protocol:terminate(Error, ProtoState); - {_, Reason} -> - emqx_protocol:terminate(Reason, ProtoState) + ok = Transport:fast_close(Socket), + ok = emqx_keepalive:cancel(KeepAlive), + emqx_protocol:terminate(Reason, ProtoState). + +%%-------------------------------------------------------------------- +%% Handle internal request + +handle_request(Req, State = #state{proto_state = ProtoState}) -> + case emqx_protocol:handle_req(Req, ProtoState) of + {ok, _Result, NProtoState} -> %% TODO:: how to handle the result? + keep_state(State#state{proto_state = NProtoState}); + {error, Reason, NProtoState} -> + shutdown(Reason, State#state{proto_state = NProtoState}) end. %%-------------------------------------------------------------------- %% Process incoming data +-compile({inline, [process_incoming/2]}). +process_incoming(Data, State) -> + process_incoming(Data, [], State). + process_incoming(<<>>, Packets, State) -> - {keep_state, State, next_events(Packets)}; + {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_events(Packets)}; + {keep_state, NState, next_incoming_events(Packets)}; {ok, Packet, Rest, NParseState} -> NState = State#state{parse_state = NParseState}, process_incoming(Rest, [Packet|Packets], NState); @@ -421,26 +495,85 @@ process_incoming(Data, Packets, State = #state{parse_state = ParseState}) -> shutdown(parse_error, State) end. -next_events(Packets) when is_list(Packets) -> - [next_events(Packet) || Packet <- lists:reverse(Packets)]; -next_events(Packet) -> - {next_event, cast, {incoming, Packet}}. +next_incoming_events(Packets) when is_list(Packets) -> + [next_event(cast, {incoming, Packet}) + || Packet <- lists:reverse(Packets)]. %%-------------------------------------------------------------------- %% Handle incoming packet -handle_incoming(Packet, SuccFun, State = #state{proto_state = ProtoState}) -> - case emqx_protocol:received(Packet, ProtoState) of +handle_incoming(Packet = ?PACKET(Type), SuccFun, + State = #state{proto_state = ProtoState}) -> + _ = inc_incoming_stats(Type), + ok = emqx_metrics:inc_recv(Packet), + ?LOG(debug, "RECV ~s", [emqx_packet:format(Packet)]), + case emqx_protocol:handle_in(Packet, ProtoState) of {ok, NProtoState} -> SuccFun(State#state{proto_state = NProtoState}); - {error, Reason} -> - shutdown(Reason, State); + {ok, OutPackets, NProtoState} -> + handle_outgoing(OutPackets, SuccFun, + State#state{proto_state = NProtoState}); {error, Reason, NProtoState} -> shutdown(Reason, State#state{proto_state = NProtoState}); + {error, Reason, OutPacket, NProtoState} -> + Shutdown = fun(NewSt) -> shutdown(Reason, NewSt) end, + handle_outgoing(OutPacket, Shutdown, State#state{proto_state = NProtoState}); {stop, Error, NProtoState} -> stop(Error, State#state{proto_state = NProtoState}) end. +%%-------------------------------------------------------------------- +%% Handle outgoing packets + +handle_outgoing(Packets, SuccFun, State = #state{serialize = Serialize}) + when is_list(Packets) -> + send(lists:map(Serialize, Packets), SuccFun, State); + +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(maybe_gc(1, Oct, State)); + {error, Reason} -> + shutdown(Reason, State) + end. + +%%-------------------------------------------------------------------- +%% Ensure keepalive + +ensure_keepalive(0, _State) -> + ignore; +ensure_keepalive(Interval, #state{transport = Transport, + socket = Socket, + proto_state = ProtoState}) -> + StatFun = fun() -> + case Transport:getstat(Socket, [recv_oct]) of + {ok, [{recv_oct, RecvOct}]} -> + {ok, RecvOct}; + Error -> Error + end + end, + Backoff = emqx_zone:get_env(emqx_protocol:info(zone, ProtoState), + keepalive_backoff, 0.75), + emqx_keepalive:start(StatFun, round(Interval * Backoff), {keepalive, check}). + %%-------------------------------------------------------------------- %% Ensure rate limit @@ -454,64 +587,88 @@ ensure_rate_limit([], 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. + 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 activate_socket(#state{conn_state = blocked}) -> ok; +activate_socket(#state{transport = Transport, + socket = Socket, + active_n = N}) -> + Transport:setopts(Socket, [{active, N}]). -activate_socket(#state{transport = Transport, socket = Socket, active_n = N}) -> - case Transport:setopts(Socket, [{active, N}]) of - ok -> ok; - {error, Reason} -> - self() ! {shutdown, Reason}, - ok +%%-------------------------------------------------------------------- +%% 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). + %%-------------------------------------------------------------------- %% Ensure stats timer -ensure_stats_timer(State = #state{enable_stats = true, - stats_timer = undefined, +ensure_stats_timer(State = #state{stats_timer = undefined, idle_timeout = IdleTimeout}) -> - State#state{stats_timer = emqx_misc:start_timer(IdleTimeout, emit_stats)}; + TRef = emqx_misc:start_timer(IdleTimeout, emit_stats), + State#state{stats_timer = TRef}; +%% disabled or timer existed ensure_stats_timer(State) -> State. %%-------------------------------------------------------------------- %% Maybe GC -maybe_gc(_, State = #state{gc_state = undefined}) -> +maybe_gc(_Cnt, _Oct, State = #state{gc_state = undefined}) -> State; -maybe_gc({publish, _, #message{payload = Payload}}, State) -> - Oct = iolist_size(Payload), - maybe_gc({1, Oct}, State); -maybe_gc(Packets, State) when is_list(Packets) -> - {Cnt, Oct} = - lists:unzip([{1, iolist_size(Payload)} - || {publish, _, #message{payload = Payload}} <- Packets]), - maybe_gc({lists:sum(Cnt), lists:sum(Oct)}, State); -maybe_gc({Cnt, Oct}, State = #state{gc_state = GCSt}) -> - {_, GCSt1} = emqx_gc:run(Cnt, Oct, GCSt), - State#state{gc_state = GCSt1}; -maybe_gc(_, State) -> 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}. %%-------------------------------------------------------------------- %% 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}]}. -shutdown(Reason = {shutdown, _}, State) -> - stop(Reason, State); +keep_state(State) -> + {keep_state, State}. + +next_event(Type, Content) -> + {next_event, Type, Content}. + shutdown(Reason, State) -> stop({shutdown, Reason}, State). diff --git a/src/emqx_cli.erl b/src/emqx_cli.erl index e681936c3..3deb50b07 100644 --- a/src/emqx_cli.erl +++ b/src/emqx_cli.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_cli). @@ -35,3 +37,4 @@ usage(CmdList) -> usage(Format, Args) -> usage([{Format, Args}]). + diff --git a/src/emqx_client.erl b/src/emqx_client.erl index 41ff2f72e..511b36b6c 100644 --- a/src/emqx_client.erl +++ b/src/emqx_client.erl @@ -20,7 +20,7 @@ -include("logger.hrl"). -include("types.hrl"). --include("emqx_client.hrl"). +-include("emqx_mqtt.hrl"). -logger_header("[Client]"). @@ -144,7 +144,17 @@ | {force_ping, boolean()} | {properties, properties()}). --type(mqtt_msg() :: #mqtt_msg{}). +-record(mqtt_msg, { + qos = ?QOS_0, + retain = false, + dup = false, + packet_id, + topic, + props, + payload + }). + +-opaque(mqtt_msg() :: #mqtt_msg{}). -record(state, {name :: atom(), owner :: pid(), @@ -160,7 +170,7 @@ clean_start :: boolean(), username :: maybe(binary()), password :: maybe(binary()), - proto_ver :: emqx_mqtt_types:version(), + proto_ver :: emqx_types:mqtt_ver(), proto_name :: iodata(), keepalive :: non_neg_integer(), keepalive_timer :: maybe(reference()), @@ -192,11 +202,11 @@ -type(payload() :: iodata()). --type(packet_id() :: emqx_mqtt_types:packet_id()). +-type(packet_id() :: emqx_types:packet_id()). --type(properties() :: emqx_mqtt_types:properties()). +-type(properties() :: emqx_types:properties()). --type(qos() :: emqx_mqtt_types:qos_name() | emqx_mqtt_types:qos()). +-type(qos() :: emqx_types:qos_name() | emqx_types:qos()). -type(pubopt() :: {retain, boolean()} | {qos, qos()} | {timeout, timeout()}). @@ -205,7 +215,7 @@ | {nl, boolean()} | {qos, qos()}). --type(reason_code() :: emqx_mqtt_types:reason_code()). +-type(reason_code() :: emqx_types:reason_code()). -type(subscribe_ret() :: {ok, properties(), [reason_code()]} | {error, term()}). @@ -1201,7 +1211,7 @@ send(Msg, State) when is_record(Msg, mqtt_msg) -> send(Packet, State = #state{socket = Sock, proto_ver = Ver}) when is_record(Packet, mqtt_packet) -> - Data = emqx_frame:serialize(Packet, #{version => Ver}), + Data = emqx_frame:serialize(Packet, Ver), ?LOG(debug, "SEND Data: ~1000p", [Packet]), case emqx_client_sock:send(Sock, Data) of ok -> {ok, bump_last_packet_id(State)}; @@ -1246,4 +1256,3 @@ bump_last_packet_id(State = #state{last_packet_id = Id}) -> -spec next_packet_id(packet_id()) -> packet_id(). next_packet_id(?MAX_PACKET_ID) -> 1; next_packet_id(Id) -> Id + 1. - diff --git a/src/emqx_client_sock.erl b/src/emqx_client_sock.erl index ae9b03305..eb938910c 100644 --- a/src/emqx_client_sock.erl +++ b/src/emqx_client_sock.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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). @@ -24,6 +26,8 @@ , getstat/2 ]). +-export_type([socket/0, option/0]). + -record(ssl_socket, {tcp, ssl}). -type(socket() :: inet:socket() | #ssl_socket{}). @@ -32,8 +36,6 @@ -type(option() :: gen_tcp:connect_option() | {ssl_opts, [ssl:ssl_option()]}). --export_type([socket/0, option/0]). - -define(DEFAULT_TCP_OPTIONS, [binary, {packet, raw}, {active, false}, {nodelay, true}, {reuseaddr, true}]). @@ -105,3 +107,4 @@ default_ciphers(TlsVersions) -> fun(TlsVer, Ciphers) -> Ciphers ++ ssl:cipher_suites(all, TlsVer) end, [], TlsVersions). + diff --git a/src/emqx_cm.erl b/src/emqx_cm.erl index 852338854..a4800e839 100644 --- a/src/emqx_cm.erl +++ b/src/emqx_cm.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,7 +12,9 @@ %% 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. +%%-------------------------------------------------------------------- +%% Channel Manager -module(emqx_cm). -behaviour(gen_server). @@ -24,25 +27,29 @@ -export([start_link/0]). --export([ register_connection/1 - , register_connection/2 - , unregister_connection/1 - , unregister_connection/2 +-export([ register_channel/1 + , unregister_channel/1 + , unregister_channel/2 ]). --export([ get_conn_attrs/1 - , get_conn_attrs/2 - , set_conn_attrs/2 - , set_conn_attrs/3 +-export([ get_chan_attrs/1 + , get_chan_attrs/2 + , set_chan_attrs/2 ]). --export([ get_conn_stats/1 - , get_conn_stats/2 - , set_conn_stats/2 - , set_conn_stats/3 +-export([ get_chan_stats/1 + , get_chan_stats/2 + , set_chan_stats/2 ]). --export([lookup_conn_pid/1]). +-export([ open_session/3 + , discard_session/1 + , resume_session/1 + ]). + +-export([ lookup_channels/1 + , lookup_channels/2 + ]). %% gen_server callbacks -export([ init/1 @@ -53,159 +60,269 @@ , code_change/3 ]). -%% internal export +%% Internal export -export([stats_fun/0]). --define(CM, ?MODULE). +-type(chan_pid() :: pid()). -%% ETS tables for connection management. --define(CONN_TAB, emqx_conn). --define(CONN_ATTRS_TAB, emqx_conn_attrs). --define(CONN_STATS_TAB, emqx_conn_stats). +%% Tables for channel management. +-define(CHAN_TAB, emqx_channel). +-define(CHAN_P_TAB, emqx_channel_p). +-define(CHAN_ATTRS_TAB, emqx_channel_attrs). +-define(CHAN_STATS_TAB, emqx_channel_stats). +-define(CHAN_STATS, + [{?CHAN_TAB, 'channels.count', 'channels.max'}, + {?CHAN_TAB, 'connections.count', 'connections.max'}, + {?CHAN_TAB, 'sessions.count', 'sessions.max'}, + {?CHAN_P_TAB, 'sessions.persistent.count', 'sessions.persistent.max'} + ]). + +%% Batch drain -define(BATCH_SIZE, 100000). -%% @doc Start the connection manager. +%% Server name +-define(CM, ?MODULE). + +%% @doc Start the channel manager. -spec(start_link() -> startlink_ret()). start_link() -> gen_server:start_link({local, ?CM}, ?MODULE, [], []). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% API -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- -%% @doc Register a connection. --spec(register_connection(emqx_types:client_id()) -> ok). -register_connection(ClientId) when is_binary(ClientId) -> - register_connection(ClientId, self()). +%% @doc Register a channel. +-spec(register_channel(emqx_types:client_id()) -> ok). +register_channel(ClientId) when is_binary(ClientId) -> + register_channel(ClientId, self()). --spec(register_connection(emqx_types:client_id(), pid()) -> ok). -register_connection(ClientId, ConnPid) when is_binary(ClientId), is_pid(ConnPid) -> - true = ets:insert(?CONN_TAB, {ClientId, ConnPid}), - notify({registered, ClientId, ConnPid}). +%% @doc Register a channel with pid. +-spec(register_channel(emqx_types:client_id(), chan_pid()) -> ok). +register_channel(ClientId, ChanPid) -> + Chan = {ClientId, ChanPid}, + true = ets:insert(?CHAN_TAB, Chan), + ok = emqx_cm_registry:register_channel(Chan), + cast({registered, Chan}). -%% @doc Unregister a connection. --spec(unregister_connection(emqx_types:client_id()) -> ok). -unregister_connection(ClientId) when is_binary(ClientId) -> - unregister_connection(ClientId, self()). +%% @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_connection(emqx_types:client_id(), pid()) -> ok). -unregister_connection(ClientId, ConnPid) when is_binary(ClientId), is_pid(ConnPid) -> - true = do_unregister_connection({ClientId, ConnPid}), - notify({unregistered, ConnPid}). +-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}). -do_unregister_connection(Conn) -> - true = ets:delete(?CONN_STATS_TAB, Conn), - true = ets:delete(?CONN_ATTRS_TAB, Conn), - true = ets:delete_object(?CONN_TAB, Conn). +%% @private +do_unregister_channel(Chan) -> + ok = emqx_cm_registry:unregister_channel(Chan), + true = ets:delete_object(?CHAN_P_TAB, Chan), + true = ets:delete(?CHAN_ATTRS_TAB, Chan), + true = ets:delete(?CHAN_STATS_TAB, Chan), + ets:delete_object(?CHAN_TAB, Chan). -%% @doc Get conn attrs --spec(get_conn_attrs(emqx_types:client_id()) -> list()). -get_conn_attrs(ClientId) when is_binary(ClientId) -> - ConnPid = lookup_conn_pid(ClientId), - get_conn_attrs(ClientId, ConnPid). +%% @doc Get attrs of a channel. +-spec(get_chan_attrs(emqx_types:client_id()) -> maybe(emqx_types:attrs())). +get_chan_attrs(ClientId) -> + with_channel(ClientId, fun(ChanPid) -> get_chan_attrs(ClientId, ChanPid) end). --spec(get_conn_attrs(emqx_types:client_id(), pid()) -> list()). -get_conn_attrs(ClientId, ConnPid) when is_binary(ClientId) -> - emqx_tables:lookup_value(?CONN_ATTRS_TAB, {ClientId, ConnPid}, []). +-spec(get_chan_attrs(emqx_types:client_id(), chan_pid()) -> maybe(emqx_types:attrs())). +get_chan_attrs(ClientId, ChanPid) when node(ChanPid) == node() -> + Chan = {ClientId, ChanPid}, + emqx_tables:lookup_value(?CHAN_ATTRS_TAB, Chan); +get_chan_attrs(ClientId, ChanPid) -> + rpc_call(node(ChanPid), get_chan_attrs, [ClientId, ChanPid]). -%% @doc Set conn attrs --spec(set_conn_attrs(emqx_types:client_id(), list()) -> true). -set_conn_attrs(ClientId, Attrs) when is_binary(ClientId) -> - set_conn_attrs(ClientId, self(), Attrs). +%% @doc Set attrs of a channel. +-spec(set_chan_attrs(emqx_types:client_id(), emqx_types:attrs()) -> ok). +set_chan_attrs(ClientId, Attrs) when is_binary(ClientId) -> + Chan = {ClientId, self()}, + true = ets:insert(?CHAN_ATTRS_TAB, {Chan, Attrs}), + ok. --spec(set_conn_attrs(emqx_types:client_id(), pid(), list()) -> true). -set_conn_attrs(ClientId, ConnPid, Attrs) when is_binary(ClientId), is_pid(ConnPid) -> - Conn = {ClientId, ConnPid}, - ets:insert(?CONN_ATTRS_TAB, {Conn, Attrs}). +%% @doc Get channel's stats. +-spec(get_chan_stats(emqx_types:client_id()) -> maybe(emqx_types:stats())). +get_chan_stats(ClientId) -> + with_channel(ClientId, fun(ChanPid) -> get_chan_stats(ClientId, ChanPid) end). -%% @doc Get conn stats --spec(get_conn_stats(emqx_types:client_id()) -> list(emqx_stats:stats())). -get_conn_stats(ClientId) when is_binary(ClientId) -> - ConnPid = lookup_conn_pid(ClientId), - get_conn_stats(ClientId, ConnPid). +-spec(get_chan_stats(emqx_types:client_id(), chan_pid()) -> maybe(emqx_types:stats())). +get_chan_stats(ClientId, ChanPid) when node(ChanPid) == node() -> + Chan = {ClientId, ChanPid}, + emqx_tables:lookup_value(?CHAN_STATS_TAB, Chan); +get_chan_stats(ClientId, ChanPid) -> + rpc_call(node(ChanPid), get_chan_stats, [ClientId, ChanPid]). --spec(get_conn_stats(emqx_types:client_id(), pid()) -> list(emqx_stats:stats())). -get_conn_stats(ClientId, ConnPid) when is_binary(ClientId) -> - Conn = {ClientId, ConnPid}, - emqx_tables:lookup_value(?CONN_STATS_TAB, Conn, []). +%% @doc Set channel's stats. +-spec(set_chan_stats(emqx_types:client_id(), emqx_types:stats()) -> ok). +set_chan_stats(ClientId, Stats) when is_binary(ClientId) -> + set_chan_stats(ClientId, self(), Stats). -%% @doc Set conn stats. --spec(set_conn_stats(emqx_types:client_id(), list(emqx_stats:stats())) -> true). -set_conn_stats(ClientId, Stats) when is_binary(ClientId) -> - set_conn_stats(ClientId, self(), Stats). +-spec(set_chan_stats(emqx_types:client_id(), chan_pid(), emqx_types:stats()) -> ok). +set_chan_stats(ClientId, ChanPid, Stats) -> + Chan = {ClientId, ChanPid}, + true = ets:insert(?CHAN_STATS_TAB, {Chan, Stats}), + ok. --spec(set_conn_stats(emqx_types:client_id(), pid(), list(emqx_stats:stats())) -> true). -set_conn_stats(ClientId, ConnPid, Stats) when is_binary(ClientId), is_pid(ConnPid) -> - Conn = {ClientId, ConnPid}, - ets:insert(?CONN_STATS_TAB, {Conn, Stats}). +%% @doc Open a session. +-spec(open_session(boolean(), emqx_types:client(), map()) + -> {ok, emqx_session:session()} | {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} + end, + emqx_cm_locker:trans(ClientId, CleanStart); -%% @doc Lookup connection pid. --spec(lookup_conn_pid(emqx_types:client_id()) -> maybe(pid())). -lookup_conn_pid(ClientId) when is_binary(ClientId) -> - emqx_tables:lookup_value(?CONN_TAB, ClientId). +open_session(false, Client = #{client_id := ClientId}, Options) -> + ResumeStart = fun(_) -> + case resume_session(ClientId) of + {ok, Session} -> + {ok, Session, true}; + {error, not_found} -> + {ok, emqx_session:init(false, Client, Options), false} + end + end, + emqx_cm_locker:trans(ClientId, ResumeStart). -notify(Msg) -> - gen_server:cast(?CM, {notify, Msg}). +%% @doc Try to resume a session. +-spec(resume_session(emqx_types:client_id()) + -> {ok, emqx_session:session()} | {error, Reason :: term()}). +resume_session(ClientId) -> + case lookup_channels(ClientId) of + [] -> {error, not_found}; + [_ChanPid] -> + ok; + % emqx_channel:resume(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 + end, StalePids), + % emqx_channel:resume(ChanPid) + ok + end. -%%----------------------------------------------------------------------------- +%% @doc Discard all the sessions identified by the ClientId. +-spec(discard_session(emqx_types:client_id()) -> ok). +discard_session(ClientId) when is_binary(ClientId) -> + case lookup_channels(ClientId) of + [] -> ok; + ChanPids -> + lists:foreach( + fun(ChanPid) -> + try ok + % emqx_channel:discard(ChanPid) + catch + _:Error:_Stk -> + ?LOG(warning, "[SM] Failed to discard ~p: ~p", [ChanPid, Error]) + end + end, ChanPids) + end. + +%% @doc Is clean start? +% is_clean_start(#{clean_start := false}) -> false; +% is_clean_start(_Attrs) -> true. + +with_channel(ClientId, Fun) -> + case lookup_channels(ClientId) of + [] -> undefined; + [Pid] -> Fun(Pid); + Pids -> Fun(lists:last(Pids)) + end. + +%% @doc Lookup channels. +-spec(lookup_channels(emqx_types:client_id()) -> list(chan_pid())). +lookup_channels(ClientId) -> + lookup_channels(global, ClientId). + +%% @doc Lookup local or global channels. +-spec(lookup_channels(local | global, emqx_types:client_id()) -> list(chan_pid())). +lookup_channels(global, ClientId) -> + case emqx_cm_registry:is_enabled() of + true -> + emqx_cm_registry:lookup_channels(ClientId); + false -> + lookup_channels(local, ClientId) + end; + +lookup_channels(local, ClientId) -> + [ChanPid || {_, ChanPid} <- ets:lookup(?CHAN_TAB, ClientId)]. + +%% @private +rpc_call(Node, Fun, Args) -> + case rpc:call(Node, ?MODULE, Fun, Args) of + {badrpc, Reason} -> error(Reason); + Res -> Res + end. + +%% @private +cast(Msg) -> gen_server:cast(?CM, Msg). + +%%-------------------------------------------------------------------- %% gen_server callbacks -%%----------------------------------------------------------------------------- +%%-------------------------------------------------------------------- init([]) -> - TabOpts = [public, set, {write_concurrency, true}], - ok = emqx_tables:new(?CONN_TAB, [{read_concurrency, true} | TabOpts]), - ok = emqx_tables:new(?CONN_ATTRS_TAB, TabOpts), - ok = emqx_tables:new(?CONN_STATS_TAB, TabOpts), - ok = emqx_stats:update_interval(conn_stats, fun ?MODULE:stats_fun/0), - {ok, #{conn_pmon => emqx_pmon:new()}}. + TabOpts = [public, {write_concurrency, true}], + ok = emqx_tables:new(?CHAN_TAB, [bag, {read_concurrency, true} | TabOpts]), + ok = emqx_tables:new(?CHAN_P_TAB, [bag | TabOpts]), + ok = emqx_tables:new(?CHAN_ATTRS_TAB, [set, compressed | TabOpts]), + ok = emqx_tables:new(?CHAN_STATS_TAB, [set | TabOpts]), + ok = emqx_stats:update_interval(chan_stats, fun ?MODULE:stats_fun/0), + {ok, #{chan_pmon => emqx_pmon:new()}}. handle_call(Req, _From, State) -> ?LOG(error, "Unexpected call: ~p", [Req]), {reply, ignored, State}. -handle_cast({notify, {registered, ClientId, ConnPid}}, State = #{conn_pmon := PMon}) -> - {noreply, State#{conn_pmon := emqx_pmon:monitor(ConnPid, ClientId, PMon)}}; +handle_cast({registered, {ClientId, ChanPid}}, State = #{chan_pmon := PMon}) -> + PMon1 = emqx_pmon:monitor(ChanPid, ClientId, PMon), + {noreply, State#{chan_pmon := PMon1}}; -handle_cast({notify, {unregistered, ConnPid}}, State = #{conn_pmon := PMon}) -> - {noreply, State#{conn_pmon := emqx_pmon:demonitor(ConnPid, PMon)}}; +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}. -handle_info({'DOWN', _MRef, process, Pid, _Reason}, State = #{conn_pmon := PMon}) -> - ConnPids = [Pid | emqx_misc:drain_down(?BATCH_SIZE)], - {Items, PMon1} = emqx_pmon:erase_all(ConnPids, PMon), - ok = emqx_pool:async_submit( - fun lists:foreach/2, [fun clean_down/1, Items]), - {noreply, State#{conn_pmon := PMon1}}; +handle_info({'DOWN', _MRef, process, Pid, _Reason}, State = #{chan_pmon := PMon}) -> + ChanPids = [Pid | emqx_misc:drain_down(?BATCH_SIZE)], + {Items, PMon1} = emqx_pmon:erase_all(ChanPids, PMon), + ok = emqx_pool:async_submit(fun lists:foreach/2, [fun clean_down/1, Items]), + {noreply, State#{chan_pmon := PMon1}}; handle_info(Info, State) -> ?LOG(error, "Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, _State) -> - emqx_stats:cancel_update(conn_stats). + emqx_stats:cancel_update(chan_stats). code_change(_OldVsn, State, _Extra) -> {ok, State}. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Internal functions -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- -clean_down({Pid, ClientId}) -> - Conn = {ClientId, Pid}, - case ets:member(?CONN_TAB, ClientId) - orelse ets:member(?CONN_ATTRS_TAB, Conn) of - true -> - do_unregister_connection(Conn); - false -> false - end. +clean_down({ChanPid, ClientId}) -> + Chan = {ClientId, ChanPid}, + do_unregister_channel(Chan). stats_fun() -> - case ets:info(?CONN_TAB, size) of + lists:foreach(fun update_stats/1, ?CHAN_STATS). + +update_stats({Tab, Stat, MaxStat}) -> + case ets:info(Tab, size) of undefined -> ok; - Size -> emqx_stats:setstat('connections.count', 'connections.max', Size) + Size -> emqx_stats:setstat(Stat, MaxStat, Size) end. + diff --git a/src/emqx_sm_locker.erl b/src/emqx_cm_locker.erl similarity index 93% rename from src/emqx_sm_locker.erl rename to src/emqx_cm_locker.erl index 854b95653..c5cd9c2f6 100644 --- a/src/emqx_sm_locker.erl +++ b/src/emqx_cm_locker.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,8 +12,9 @@ %% 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_sm_locker). +-module(emqx_cm_locker). -include("emqx.hrl"). -include("types.hrl"). @@ -26,10 +28,6 @@ , unlock/1 ]). -%%------------------------------------------------------------------------------ -%% APIs -%%------------------------------------------------------------------------------ - -spec(start_link() -> startlink_ret()). start_link() -> ekka_locker:start_link(?MODULE). diff --git a/src/emqx_sm_registry.erl b/src/emqx_cm_registry.erl similarity index 56% rename from src/emqx_sm_registry.erl rename to src/emqx_cm_registry.erl index 5e8d4663d..ce843a427 100644 --- a/src/emqx_sm_registry.erl +++ b/src/emqx_cm_registry.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,8 +12,10 @@ %% 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_sm_registry). +%% Global Channel Registry +-module(emqx_cm_registry). -behaviour(gen_server). @@ -24,12 +27,14 @@ -export([start_link/0]). --export([ is_enabled/0 - , register_session/1 - , lookup_session/1 - , unregister_session/1 +-export([is_enabled/0]). + +-export([ register_channel/1 + , unregister_channel/1 ]). +-export([lookup_channels/1]). + %% gen_server callbacks -export([ init/1 , handle_call/3 @@ -40,57 +45,67 @@ ]). -define(REGISTRY, ?MODULE). --define(TAB, emqx_session_registry). --define(LOCK, {?MODULE, cleanup_sessions}). +-define(TAB, emqx_channel_registry). +-define(LOCK, {?MODULE, cleanup_down}). --record(global_session, {sid, pid}). +-record(channel, {chid, pid}). --type(session_pid() :: pid()). - -%%------------------------------------------------------------------------------ -%% APIs -%%------------------------------------------------------------------------------ - -%% @doc Start the global session manager. +%% @doc Start the global channel registry. -spec(start_link() -> startlink_ret()). start_link() -> gen_server:start_link({local, ?REGISTRY}, ?MODULE, [], []). +%%-------------------------------------------------------------------- +%% APIs +%%-------------------------------------------------------------------- + +%% @doc Is the global registry enabled? -spec(is_enabled() -> boolean()). is_enabled() -> - emqx_config:get_env(enable_session_registry, true). + emqx_config:get_env(enable_channel_registry, true). --spec(lookup_session(emqx_types:client_id()) -> list(session_pid())). -lookup_session(ClientId) -> - [SessPid || #global_session{pid = SessPid} <- mnesia:dirty_read(?TAB, ClientId)]. +%% @doc Register a global channel. +-spec(register_channel(emqx_types:client_id() + | {emqx_types:client_id(), pid()}) -> ok). +register_channel(ClientId) when is_binary(ClientId) -> + register_channel({ClientId, self()}); --spec(register_session({emqx_types:client_id(), session_pid()}) -> ok). -register_session({ClientId, SessPid}) when is_binary(ClientId), is_pid(SessPid) -> +register_channel({ClientId, ChanPid}) when is_binary(ClientId), is_pid(ChanPid) -> case is_enabled() of - true -> mnesia:dirty_write(?TAB, record(ClientId, SessPid)); + true -> mnesia:dirty_write(?TAB, record(ClientId, ChanPid)); false -> ok end. --spec(unregister_session({emqx_types:client_id(), session_pid()}) -> ok). -unregister_session({ClientId, SessPid}) when is_binary(ClientId), is_pid(SessPid) -> +%% @doc Unregister a global channel. +-spec(unregister_channel(emqx_types:client_id() + | {emqx_types:client_id(), pid()}) -> ok). +unregister_channel(ClientId) when is_binary(ClientId) -> + unregister_channel({ClientId, self()}); + +unregister_channel({ClientId, ChanPid}) when is_binary(ClientId), is_pid(ChanPid) -> case is_enabled() of - true -> mnesia:dirty_delete_object(?TAB, record(ClientId, SessPid)); + true -> mnesia:dirty_delete_object(?TAB, record(ClientId, ChanPid)); false -> ok end. -record(ClientId, SessPid) -> - #global_session{sid = ClientId, pid = SessPid}. +%% @doc Lookup the global channels. +-spec(lookup_channels(emqx_types:client_id()) -> list(pid())). +lookup_channels(ClientId) -> + [ChanPid || #channel{pid = ChanPid} <- mnesia:dirty_read(?TAB, ClientId)]. -%%------------------------------------------------------------------------------ +record(ClientId, ChanPid) -> + #channel{chid = ClientId, pid = ChanPid}. + +%%-------------------------------------------------------------------- %% gen_server callbacks -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- init([]) -> ok = ekka_mnesia:create_table(?TAB, [ {type, bag}, {ram_copies, [node()]}, - {record_name, global_session}, - {attributes, record_info(fields, global_session)}, + {record_name, channel}, + {attributes, record_info(fields, channel)}, {storage_properties, [{ets, [{read_concurrency, true}, {write_concurrency, true}]}]}]), ok = ekka_mnesia:copy_table(?TAB), @@ -108,7 +123,7 @@ handle_cast(Msg, State) -> handle_info({membership, {mnesia, down, Node}}, State) -> global:trans({?LOCK, self()}, fun() -> - mnesia:transaction(fun cleanup_sessions/1, [Node]) + mnesia:transaction(fun cleanup_channels/1, [Node]) end), {noreply, State}; @@ -125,14 +140,14 @@ terminate(_Reason, _State) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Internal functions -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- -cleanup_sessions(Node) -> - Pat = [{#global_session{pid = '$1', _ = '_'}, [{'==', {node, '$1'}, Node}], ['$_']}], - lists:foreach(fun delete_session/1, mnesia:select(?TAB, Pat, write)). +cleanup_channels(Node) -> + Pat = [{#channel{pid = '$1', _ = '_'}, [{'==', {node, '$1'}, Node}], ['$_']}], + lists:foreach(fun delete_channel/1, mnesia:select(?TAB, Pat, write)). -delete_session(Session) -> - mnesia:delete_object(?TAB, Session, write). +delete_channel(Chan) -> + mnesia:delete_object(?TAB, Chan, write). diff --git a/src/emqx_cm_sup.erl b/src/emqx_cm_sup.erl index e054c53e7..6cbf8d432 100644 --- a/src/emqx_cm_sup.erl +++ b/src/emqx_cm_sup.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_cm_sup). @@ -36,13 +38,33 @@ init([]) -> shutdown => 1000, type => worker, modules => [emqx_flapping]}, + %% Channel locker + Locker = #{id => locker, + start => {emqx_cm_locker, start_link, []}, + restart => permanent, + shutdown => 5000, + type => worker, + modules => [emqx_cm_locker] + }, + %% Channel registry + Registry = #{id => registry, + start => {emqx_cm_registry, start_link, []}, + restart => permanent, + shutdown => 5000, + type => worker, + modules => [emqx_cm_registry] + }, + %% Channel Manager Manager = #{id => manager, start => {emqx_cm, start_link, []}, restart => permanent, - shutdown => 2000, + shutdown => 5000, type => worker, - modules => [emqx_cm]}, + modules => [emqx_cm] + }, SupFlags = #{strategy => one_for_one, intensity => 100, - period => 10}, - {ok, {SupFlags, [Banned, Manager, Flapping]}}. + period => 10 + }, + {ok, {SupFlags, [Banned, Flapping, Locker, Registry, Manager]}}. + diff --git a/src/emqx_config.erl b/src/emqx_config.erl index fd80de0c2..3d4c33369 100644 --- a/src/emqx_config.erl +++ b/src/emqx_config.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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 Hot Configuration %% diff --git a/src/emqx_ctl.erl b/src/emqx_ctl.erl index 09bab3e58..08b9a56fd 100644 --- a/src/emqx_ctl.erl +++ b/src/emqx_ctl.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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). diff --git a/src/emqx_flapping.erl b/src/emqx_flapping.erl index 2a5e66be1..ba3f444b7 100644 --- a/src/emqx_flapping.erl +++ b/src/emqx_flapping.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,9 @@ %% 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 This module is used to garbage clean the flapping records. -module(emqx_flapping). @@ -21,14 +25,12 @@ -export([start_link/0]). -%% This module is used to garbage clean the flapping records - %% gen_statem callbacks --export([ terminate/3 - , code_change/4 - , init/1 +-export([ init/1 , initialized/3 , callback_mode/0 + , terminate/3 + , code_change/4 ]). -define(FLAPPING_TAB, ?MODULE). @@ -37,15 +39,15 @@ -export([check/3]). --record(flapping, - { client_id :: binary() - , check_count :: integer() - , timestamp :: integer() - }). +-record(flapping, { + client_id :: binary(), + check_count :: integer(), + timestamp :: integer() + }). -type(flapping_record() :: #flapping{}). --type(flapping_state() :: flapping | ok). +-type(flapping_state() :: flapping | ok). %% @doc This function is used to initialize flapping records %% the expiry time unit is minutes. @@ -103,14 +105,14 @@ start_link() -> gen_statem:start_link({local, ?MODULE}, ?MODULE, [], []). init([]) -> - TimerInterval = emqx_config:get_env(flapping_clean_interval, ?default_flapping_clean_interval), + Interval = emqx_config:get_env(flapping_clean_interval, ?default_flapping_clean_interval), TabOpts = [ public , set , {keypos, 2} , {write_concurrency, true} , {read_concurrency, true}], ok = emqx_tables:new(?FLAPPING_TAB, TabOpts), - {ok, initialized, #{timer_interval => TimerInterval}}. + {ok, initialized, #{timer_interval => Interval}}. callback_mode() -> [state_functions, state_enter]. @@ -137,3 +139,4 @@ clean_expired_records() -> NowTime = emqx_time:now_secs(), MatchSpec = [{{'$1', '$2', '$3'},[{'<', '$3', NowTime}], [true]}], ets:select_delete(?FLAPPING_TAB, MatchSpec). + diff --git a/src/emqx_frame.erl b/src/emqx_frame.erl index a4ef34840..c0115cea8 100644 --- a/src/emqx_frame.erl +++ b/src/emqx_frame.erl @@ -29,22 +29,22 @@ , serialize/2 ]). +-export_type([ options/0 + , parse_state/0 + , parse_result/0 + ]). + -type(options() :: #{max_size => 1..?MAX_PACKET_SIZE, - version => emqx_mqtt_types:version() + version => emqx_types:version() }). -opaque(parse_state() :: {none, options()} | {more, cont_fun()}). -opaque(parse_result() :: {ok, parse_state()} - | {ok, emqx_mqtt_types:packet(), binary(), parse_state()}). + | {ok, emqx_types:packet(), binary(), parse_state()}). -type(cont_fun() :: fun((binary()) -> parse_result())). --export_type([ options/0 - , parse_state/0 - , parse_result/0 - ]). - -define(none(Opts), {none, Opts}). -define(more(Cont), {more, Cont}). -define(DEFAULT_OPTIONS, @@ -385,15 +385,15 @@ parse_binary_data(<>) -> %% Serialize MQTT Packet %%-------------------------------------------------------------------- --spec(serialize(emqx_mqtt_types:packet()) -> iodata()). +-spec(serialize(emqx_types:packet()) -> iodata()). serialize(Packet) -> - serialize(Packet, ?DEFAULT_OPTIONS). + serialize(Packet, ?MQTT_PROTO_V4). --spec(serialize(emqx_mqtt_types:packet(), options()) -> iodata()). +-spec(serialize(emqx_types:packet(), emqx_types:version()) -> iodata()). serialize(#mqtt_packet{header = Header, variable = Variable, - payload = Payload}, Options) when is_map(Options) -> - serialize(Header, serialize_variable(Variable, merge_opts(Options)), serialize_payload(Payload)). + payload = Payload}, Ver) -> + serialize(Header, serialize_variable(Variable, Ver), serialize_payload(Payload)). serialize(#mqtt_packet_header{type = Type, dup = Dup, @@ -420,7 +420,7 @@ serialize_variable(#mqtt_packet_connect{ will_topic = WillTopic, will_payload = WillPayload, username = Username, - password = Password}, _Options) -> + password = Password}, _Ver) -> [serialize_binary_data(ProtoName), <<(case IsBridge of true -> 16#80 + ProtoVer; @@ -447,14 +447,12 @@ serialize_variable(#mqtt_packet_connect{ serialize_variable(#mqtt_packet_connack{ack_flags = AckFlags, reason_code = ReasonCode, - properties = Properties}, - #{version := Ver}) -> + properties = Properties}, Ver) -> [AckFlags, ReasonCode, serialize_properties(Properties, Ver)]; serialize_variable(#mqtt_packet_publish{topic_name = TopicName, packet_id = PacketId, - properties = Properties}, - #{version := Ver}) -> + properties = Properties}, Ver) -> [serialize_utf8_string(TopicName), if PacketId =:= undefined -> <<>>; @@ -462,59 +460,54 @@ serialize_variable(#mqtt_packet_publish{topic_name = TopicName, end, serialize_properties(Properties, Ver)]; -serialize_variable(#mqtt_packet_puback{packet_id = PacketId}, - #{version := Ver}) +serialize_variable(#mqtt_packet_puback{packet_id = PacketId}, Ver) when Ver == ?MQTT_PROTO_V3; Ver == ?MQTT_PROTO_V4 -> <>; serialize_variable(#mqtt_packet_puback{packet_id = PacketId, reason_code = ReasonCode, properties = Properties}, - #{version := ?MQTT_PROTO_V5}) -> + ?MQTT_PROTO_V5) -> [<>, ReasonCode, serialize_properties(Properties, ?MQTT_PROTO_V5)]; serialize_variable(#mqtt_packet_subscribe{packet_id = PacketId, properties = Properties, - topic_filters = TopicFilters}, - #{version := Ver}) -> + topic_filters = TopicFilters}, Ver) -> [<>, serialize_properties(Properties, Ver), serialize_topic_filters(subscribe, TopicFilters, Ver)]; serialize_variable(#mqtt_packet_suback{packet_id = PacketId, properties = Properties, - reason_codes = ReasonCodes}, - #{version := Ver}) -> + reason_codes = ReasonCodes}, Ver) -> [<>, serialize_properties(Properties, Ver), serialize_reason_codes(ReasonCodes)]; serialize_variable(#mqtt_packet_unsubscribe{packet_id = PacketId, properties = Properties, - topic_filters = TopicFilters}, - #{version := Ver}) -> + topic_filters = TopicFilters}, Ver) -> [<>, serialize_properties(Properties, Ver), serialize_topic_filters(unsubscribe, TopicFilters, Ver)]; serialize_variable(#mqtt_packet_unsuback{packet_id = PacketId, properties = Properties, - reason_codes = ReasonCodes}, - #{version := Ver}) -> + reason_codes = ReasonCodes}, Ver) -> [<>, serialize_properties(Properties, Ver), serialize_reason_codes(ReasonCodes)]; -serialize_variable(#mqtt_packet_disconnect{}, #{version := Ver}) +serialize_variable(#mqtt_packet_disconnect{}, Ver) when Ver == ?MQTT_PROTO_V3; Ver == ?MQTT_PROTO_V4 -> <<>>; serialize_variable(#mqtt_packet_disconnect{reason_code = ReasonCode, properties = Properties}, - #{version := Ver = ?MQTT_PROTO_V5}) -> + Ver = ?MQTT_PROTO_V5) -> [ReasonCode, serialize_properties(Properties, Ver)]; serialize_variable(#mqtt_packet_disconnect{}, _Ver) -> <<>>; serialize_variable(#mqtt_packet_auth{reason_code = ReasonCode, properties = Properties}, - #{version := Ver = ?MQTT_PROTO_V5}) -> + Ver = ?MQTT_PROTO_V5) -> [ReasonCode, serialize_properties(Properties, Ver)]; serialize_variable(PacketId, ?MQTT_PROTO_V3) when is_integer(PacketId) -> diff --git a/src/emqx_gc.erl b/src/emqx_gc.erl index fa0d3d6ed..974d5b48e 100644 --- a/src/emqx_gc.erl +++ b/src/emqx_gc.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,13 +12,17 @@ %% 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 This module manages an opaque collection of statistics data used to +%%-------------------------------------------------------------------- +%% @doc +%% This module manages an opaque collection of statistics data used to %% force garbage collection on `self()' process when hitting thresholds. %% Namely: %% (1) Total number of messages passed through %% (2) Total data volume passed through %% @end +%%-------------------------------------------------------------------- -module(emqx_gc). @@ -29,17 +34,17 @@ , reset/1 ]). +-export_type([gc_state/0]). + -type(opts() :: #{count => integer(), bytes => integer()}). -type(st() :: #{cnt => {integer(), integer()}, oct => {integer(), integer()}}). --opaque(gc_state() :: {?MODULE, st()}). +-opaque(gc_state() :: {gc_state, st()}). --export_type([gc_state/0]). - --define(GCS(St), {?MODULE, St}). +-define(GCS(St), {gc_state, St}). -define(disabled, disabled). -define(ENABLED(X), (is_integer(X) andalso X > 0)). @@ -85,9 +90,9 @@ reset(?GCS(St)) -> reset(undefined) -> undefined. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Internal functions -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- -spec(dec(cnt | oct, pos_integer(), st()) -> {boolean(), st()}). dec(Key, Num, St) -> diff --git a/src/emqx_gen_mod.erl b/src/emqx_gen_mod.erl index dc9304f98..9ae4d5b6f 100644 --- a/src/emqx_gen_mod.erl +++ b/src/emqx_gen_mod.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_gen_mod). diff --git a/src/emqx_guid.erl b/src/emqx_guid.erl index 2e54e9aaa..c9368ca1c 100644 --- a/src/emqx_guid.erl +++ b/src/emqx_guid.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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 Generate global unique id for mqtt message. %% @@ -136,6 +138,5 @@ to_base62(<>) -> emqx_base62:encode(I). from_base62(S) -> - I = emqx_base62:decode(S, integer), + I = binary_to_integer( emqx_base62:decode(S)), <>. - diff --git a/src/emqx_hooks.erl b/src/emqx_hooks.erl index 594aacbec..39c692b4a 100644 --- a/src/emqx_hooks.erl +++ b/src/emqx_hooks.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_hooks). @@ -21,7 +23,9 @@ -logger_header("[Hooks]"). --export([start_link/0, stop/0]). +-export([ start_link/0 + , stop/0 + ]). %% Hooks API -export([ add/2 @@ -42,6 +46,11 @@ , code_change/3 ]). +-export_type([ hookpoint/0 + , action/0 + , filter/0 + ]). + %% Multiple callbacks can be registered on a hookpoint. %% The execution order depends on the priority value: %% - Callbacks with greater priority values will be run before @@ -54,28 +63,32 @@ -type(action() :: function() | mfa()). -type(filter() :: function() | mfa()). --record(callback, {action :: action(), - filter :: filter(), - priority :: integer()}). +-record(callback, { + action :: action(), + filter :: filter(), + priority :: integer() + }). --record(hook, {name :: hookpoint(), callbacks :: list(#callback{})}). - --export_type([hookpoint/0, action/0, filter/0]). +-record(hook, { + name :: hookpoint(), + callbacks :: list(#callback{}) + }). -define(TAB, ?MODULE). -define(SERVER, ?MODULE). -spec(start_link() -> startlink_ret()). start_link() -> - gen_server:start_link({local, ?SERVER}, ?MODULE, [], [{hibernate_after, 1000}]). + gen_server:start_link({local, ?SERVER}, + ?MODULE, [], [{hibernate_after, 1000}]). -spec(stop() -> ok). stop() -> gen_server:stop(?SERVER, normal, infinity). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Hooks API -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% @doc Register a callback -spec(add(hookpoint(), action() | #callback{}) -> ok_or_error(already_exists)). @@ -113,7 +126,6 @@ run(HookPoint, Args) -> run_fold(HookPoint, Args, Acc) -> do_run_fold(lookup(HookPoint), Args, Acc). - do_run([#callback{action = Action, filter = Filter} | Callbacks], Args) -> case filter_passed(Filter, Args) andalso execute(Action, Args) of %% stop the hook chain and return @@ -165,12 +177,12 @@ lookup(HookPoint) -> [] -> [] end. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% gen_server callbacks -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- init([]) -> - ok = emqx_tables:new(?TAB, [{keypos, #hook.name}, {read_concurrency, true}, protected]), + ok = emqx_tables:new(?TAB, [{keypos, #hook.name}, {read_concurrency, true}]), {ok, #{}}. handle_call({add, HookPoint, Callback = #callback{action = Action}}, _From, State) -> diff --git a/src/emqx_inflight.erl b/src/emqx_inflight.erl index 7a41e4ec7..a51abdc68 100644 --- a/src/emqx_inflight.erl +++ b/src/emqx_inflight.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,16 +12,20 @@ %% 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_inflight). +-compile(inline). + %% APIs --export([ new/1 +-export([ new/0 + , new/1 , contain/2 , lookup/2 , insert/3 , update/3 - , update_size/2 + , resize/2 , delete/2 , values/1 , to_list/1 @@ -31,24 +36,24 @@ , window/1 ]). +-export_type([inflight/0]). + -type(key() :: term()). -type(max_size() :: pos_integer()). --opaque(inflight() :: {?MODULE, max_size(), gb_trees:tree()}). +-opaque(inflight() :: {inflight, max_size(), gb_trees:tree()}). --define(Inflight(Tree), {?MODULE, _MaxSize, Tree}). --define(Inflight(MaxSize, Tree), {?MODULE, MaxSize, (Tree)}). +-define(Inflight(Tree), {inflight, _MaxSize, Tree}). --export_type([inflight/0]). +-define(Inflight(MaxSize, Tree), {inflight, MaxSize, (Tree)}). -%%------------------------------------------------------------------------------ -%% APIs -%%------------------------------------------------------------------------------ +-spec(new() -> inflight()). +new() -> new(0). -spec(new(non_neg_integer()) -> inflight()). new(MaxSize) when MaxSize >= 0 -> - {?MODULE, MaxSize, gb_trees:empty()}. + ?Inflight(MaxSize, gb_trees:empty()). -spec(contain(key(), inflight()) -> boolean()). contain(Key, ?Inflight(Tree)) -> @@ -70,8 +75,8 @@ delete(Key, ?Inflight(MaxSize, Tree)) -> update(Key, Val, ?Inflight(MaxSize, Tree)) -> ?Inflight(MaxSize, gb_trees:update(Key, Val, Tree)). --spec(update_size(integer(), inflight()) -> inflight()). -update_size(MaxSize, ?Inflight(Tree)) -> +-spec(resize(integer(), inflight()) -> inflight()). +resize(MaxSize, ?Inflight(Tree)) -> ?Inflight(MaxSize, Tree). -spec(is_full(inflight()) -> boolean()). diff --git a/src/emqx_json.erl b/src/emqx_json.erl index 07a2e9a23..665e7ad57 100644 --- a/src/emqx_json.erl +++ b/src/emqx_json.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,9 +12,12 @@ %% 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_json). +-compile(inline). + -export([ encode/1 , encode/2 , safe_encode/1 @@ -30,17 +34,18 @@ encode(Term) -> jsx:encode(Term). --spec(encode(jsx:json_term(), jsx_to_json:config()) -> jsx:json_text()). +-spec(encode(jsx:json_term(), jsx_to_json:config()) + -> jsx:json_text()). encode(Term, Opts) -> jsx:encode(Term, Opts). -spec(safe_encode(jsx:json_term()) - -> {ok, jsx:json_text()} | {error, term()}). + -> {ok, jsx:json_text()} | {error, Reason :: term()}). safe_encode(Term) -> safe_encode(Term, []). -spec(safe_encode(jsx:json_term(), jsx_to_json:config()) - -> {ok, jsx:json_text()} | {error, term()}). + -> {ok, jsx:json_text()} | {error, Reason :: term()}). safe_encode(Term, Opts) -> try encode(Term, Opts) of Json -> {ok, Json} @@ -53,17 +58,18 @@ safe_encode(Term, Opts) -> decode(Json) -> jsx:decode(Json). --spec(decode(jsx:json_text(), jsx_to_json:config()) -> jsx:json_term()). +-spec(decode(jsx:json_text(), jsx_to_json:config()) + -> jsx:json_term()). decode(Json, Opts) -> jsx:decode(Json, Opts). -spec(safe_decode(jsx:json_text()) - -> {ok, jsx:json_term()} | {error, term()}). + -> {ok, jsx:json_term()} | {error, Reason :: term()}). safe_decode(Json) -> safe_decode(Json, []). -spec(safe_decode(jsx:json_text(), jsx_to_json:config()) - -> {ok, jsx:json_term()} | {error, term()}). + -> {ok, jsx:json_term()} | {error, Reason :: term()}). safe_decode(Json, Opts) -> try decode(Json, Opts) of Term -> {ok, Term} diff --git a/src/emqx_keepalive.erl b/src/emqx_keepalive.erl index ebd5e18d4..25170067d 100644 --- a/src/emqx_keepalive.erl +++ b/src/emqx_keepalive.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_keepalive). @@ -20,15 +22,22 @@ , cancel/1 ]). --record(keepalive, {statfun, statval, tsec, tmsg, tref, repeat = 0}). +-export_type([keepalive/0]). + +-record(keepalive, { + statfun, + statval, + tsec, + tmsg, + tref, + repeat = 0 + }). -opaque(keepalive() :: #keepalive{}). --export_type([keepalive/0]). - -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% APIs -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% @doc Start a keepalive -spec(start(fun(), integer(), any()) -> {ok, keepalive()} | {error, term()}). @@ -79,3 +88,4 @@ cancel(_) -> timer(Secs, Msg) -> erlang:send_after(timer:seconds(Secs), self(), Msg). + diff --git a/src/emqx_kernel_sup.erl b/src/emqx_kernel_sup.erl index cfed226cd..fdca99f89 100644 --- a/src/emqx_kernel_sup.erl +++ b/src/emqx_kernel_sup.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_kernel_sup). @@ -30,8 +32,7 @@ init([]) -> child_spec(emqx_stats, worker), child_spec(emqx_metrics, worker), child_spec(emqx_ctl, worker), - child_spec(emqx_zone, worker), - child_spec(emqx_tracer, worker)]}}. + child_spec(emqx_zone, worker)]}}. child_spec(M, worker) -> #{id => M, diff --git a/src/emqx_listeners.erl b/src/emqx_listeners.erl index 745daf000..b39873879 100644 --- a/src/emqx_listeners.erl +++ b/src/emqx_listeners.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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 Start/Stop MQTT listeners. -module(emqx_listeners). @@ -33,9 +35,9 @@ -type(listener() :: {esockd:proto(), esockd:listen_on(), [esockd:option()]}). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% APIs -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% @doc Start all listeners. -spec(start() -> ok). @@ -44,13 +46,15 @@ start() -> -spec(start_listener(listener()) -> {ok, pid()} | {error, term()}). start_listener({Proto, ListenOn, Options}) -> - case start_listener(Proto, ListenOn, Options) of - {ok, _} -> - io:format("Start mqtt:~s listener on ~s successfully.~n", [Proto, format(ListenOn)]); + StartRet = start_listener(Proto, ListenOn, Options), + case StartRet of + {ok, _} -> io:format("Start mqtt:~s listener on ~s successfully.~n", + [Proto, format(ListenOn)]); {error, Reason} -> io:format(standard_error, "Failed to start mqtt:~s listener on ~s - ~p~n!", [Proto, format(ListenOn), Reason]) - end. + end, + StartRet. %% Start MQTT/TCP listener -spec(start_listener(esockd:proto(), esockd:listen_on(), [esockd:option()]) @@ -64,11 +68,13 @@ start_listener(Proto, ListenOn, Options) when Proto == ssl; Proto == tls -> %% Start MQTT/WS listener start_listener(Proto, ListenOn, Options) when Proto == http; Proto == ws -> - start_http_listener(fun cowboy:start_clear/3, 'mqtt:ws', ListenOn, ranch_opts(Options), ws_opts(Options)); + start_http_listener(fun cowboy:start_clear/3, 'mqtt:ws', ListenOn, + ranch_opts(Options), ws_opts(Options)); %% Start MQTT/WSS listener start_listener(Proto, ListenOn, Options) when Proto == https; Proto == wss -> - start_http_listener(fun cowboy:start_tls/3, 'mqtt:wss', ListenOn, ranch_opts(Options), ws_opts(Options)). + start_http_listener(fun cowboy:start_tls/3, 'mqtt:wss', ListenOn, + ranch_opts(Options), ws_opts(Options)). start_mqtt_listener(Name, ListenOn, Options) -> SockOpts = esockd:parse_opt(Options), @@ -82,8 +88,10 @@ mqtt_path(Options) -> proplists:get_value(mqtt_path, Options, "/mqtt"). ws_opts(Options) -> - Dispatch = cowboy_router:compile([{'_', [{mqtt_path(Options), emqx_ws_channel, Options}]}]), - #{env => #{dispatch => Dispatch}, proxy_header => proplists:get_value(proxy_protocol, Options, false)}. + WsPaths = [{mqtt_path(Options), emqx_ws_channel, Options}], + Dispatch = cowboy_router:compile([{'_', WsPaths}]), + ProxyProto = proplists:get_value(proxy_protocol, Options, false), + #{env => #{dispatch => Dispatch}, proxy_header => ProxyProto}. ranch_opts(Options) -> NumAcceptors = proplists:get_value(acceptors, Options, 4), @@ -132,13 +140,15 @@ stop() -> -spec(stop_listener(listener()) -> ok | {error, term()}). stop_listener({Proto, ListenOn, Opts}) -> - case stop_listener(Proto, ListenOn, Opts) of - ok -> - io:format("Stop mqtt:~s listener on ~s successfully.~n", [Proto, format(ListenOn)]); + StopRet = stop_listener(Proto, ListenOn, Opts), + case StopRet of + ok -> io:format("Stop mqtt:~s listener on ~s successfully.~n", + [Proto, format(ListenOn)]); {error, Reason} -> io:format(standard_error, "Failed to stop mqtt:~s listener on ~s - ~p~n.", [Proto, format(ListenOn), Reason]) - end. + end, + StopRet. -spec(stop_listener(esockd:proto(), esockd:listen_on(), [esockd:option()]) -> ok | {error, term()}). @@ -167,3 +177,4 @@ format({Addr, Port}) when is_list(Addr) -> io_lib:format("~s:~w", [Addr, Port]); format({Addr, Port}) when is_tuple(Addr) -> io_lib:format("~s:~w", [esockd_net:ntoab(Addr), Port]). + diff --git a/src/emqx_logger.erl b/src/emqx_logger.erl index 7f45843e3..2f65d1222 100644 --- a/src/emqx_logger.erl +++ b/src/emqx_logger.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,10 +12,11 @@ %% 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). --compile({no_auto_import,[error/1]}). +-compile({no_auto_import, [error/1]}). %% Logs -export([ debug/1 @@ -50,84 +52,127 @@ -export([parse_transform/2]). -%%------------------------------------------------------------------------------ -%% APIs -%%------------------------------------------------------------------------------ +-type(peername_str() :: list()). +-type(logger_dst() :: file:filename() | console | unknown). +-type(logger_handler_info() :: {logger:handler_id(), logger:level(), logger_dst()}). +%%-------------------------------------------------------------------- +%% APIs +%%-------------------------------------------------------------------- +-spec(debug(unicode:chardata()) -> ok). debug(Msg) -> logger:debug(Msg). + +-spec(debug(io:format(), [term()]) -> ok). debug(Format, Args) -> logger:debug(Format, Args). + +-spec(debug(logger:metadata(), io:format(), [term()]) -> ok). debug(Metadata, Format, Args) when is_map(Metadata) -> logger:debug(Format, Args, Metadata). + +-spec(info(unicode:chardata()) -> ok). info(Msg) -> logger:info(Msg). + +-spec(info(io:format(), [term()]) -> ok). info(Format, Args) -> logger:info(Format, Args). + +-spec(info(logger:metadata(), io:format(), [term()]) -> ok). info(Metadata, Format, Args) when is_map(Metadata) -> logger:info(Format, Args, Metadata). + +-spec(warning(unicode:chardata()) -> ok). warning(Msg) -> logger:warning(Msg). + +-spec(warning(io:format(), [term()]) -> ok). warning(Format, Args) -> logger:warning(Format, Args). + +-spec(warning(logger:metadata(), io:format(), [term()]) -> ok). warning(Metadata, Format, Args) when is_map(Metadata) -> logger:warning(Format, Args, Metadata). + +-spec(error(unicode:chardata()) -> ok). error(Msg) -> logger:error(Msg). +-spec(error(io:format(), [term()]) -> ok). error(Format, Args) -> logger:error(Format, Args). +-spec(error(logger:metadata(), io:format(), [term()]) -> ok). error(Metadata, Format, Args) when is_map(Metadata) -> logger:error(Format, Args, Metadata). + +-spec(critical(unicode:chardata()) -> ok). critical(Msg) -> logger:critical(Msg). + +-spec(critical(io:format(), [term()]) -> ok). critical(Format, Args) -> logger:critical(Format, Args). + +-spec(critical(logger:metadata(), io:format(), [term()]) -> ok). 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(ClientId) -> set_proc_metadata(#{client_id => ClientId}). +-spec(set_metadata_peername(peername_str()) -> ok). set_metadata_peername(Peername) -> set_proc_metadata(#{peername => Peername}). +-spec(set_proc_metadata(logger:metadata()) -> ok). set_proc_metadata(Meta) -> logger:update_process_metadata(Meta). +-spec(get_primary_log_level() -> logger:level()). get_primary_log_level() -> #{level := Level} = logger:get_primary_config(), Level. +-spec(set_primary_log_level(logger:level()) -> ok | {error, term()}). set_primary_log_level(Level) -> logger:set_primary_config(level, Level). +-spec(get_log_handlers() -> [logger_handler_info()]). get_log_handlers() -> lists:map(fun log_hanlder_info/1, logger:get_handler_config()). +-spec(get_log_handler(logger:handler_id()) -> logger_handler_info()). get_log_handler(HandlerId) -> {ok, Conf} = logger:get_handler_config(HandlerId), log_hanlder_info(Conf). +-spec(set_log_handler_level(logger:handler_id(), logger:level()) -> ok | {error, term()}). set_log_handler_level(HandlerId, Level) -> logger:set_handler_config(HandlerId, level, Level). -%% Set both the primary and all handlers level in one command +%% @doc Set both the primary and all handlers level in one command +-spec(set_log_level(logger:handler_id()) -> ok | {error, term()}). set_log_level(Level) -> case set_primary_log_level(Level) of ok -> set_all_log_handlers_level(Level); {error, Error} -> {error, {primary_logger_level, Error}} end. +%% @doc The parse transform for prefixing a module-specific logger header to the logs. +%% The logger header can be specified by "-logger_header(Header)", where Header +%% must be a string (list). +%% @end parse_transform(AST, _Opts) -> trans(AST, "", []). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Internal Functions -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- log_hanlder_info(#{id := Id, level := Level, module := logger_std_h, config := #{type := Type}}) when Type =:= standard_io; @@ -165,7 +210,10 @@ rollback([{ID, Level} | List]) -> rollback(List); rollback([]) -> ok. -%% Generate a function '$logger_header'/0 into the source code +%% @doc The following parse-transforms stripped off the module attribute named +%% `-logger_header(Header)` (if there's one) from the source code, and then +%% generate a function named '$logger_header'/0, which returns the logger header. +%% @end trans([], LogHeader, ResAST) -> lists:reverse([header_fun(LogHeader) | ResAST]); trans([{eof, L} | AST], LogHeader, ResAST) -> diff --git a/src/emqx_logger_formatter.erl b/src/emqx_logger_formatter.erl index 92c194883..6cfe1ca58 100644 --- a/src/emqx_logger_formatter.erl +++ b/src/emqx_logger_formatter.erl @@ -34,18 +34,22 @@ -define(IS_STRING(String), (is_list(String) orelse is_binary(String))). -%%%----------------------------------------------------------------- -%%% Types --type config() :: #{chars_limit => pos_integer() | unlimited, - depth => pos_integer() | unlimited, - max_size => pos_integer() | unlimited, - report_cb => logger:report_cb(), - quit => template()}. --type template() :: [metakey() | {metakey(),template(),template()} | string()]. --type metakey() :: atom() | [atom()]. +%%-------------------------------------------------------------------- +%% Types + +-type(config() :: #{chars_limit => pos_integer() | unlimited, + depth => pos_integer() | unlimited, + max_size => pos_integer() | unlimited, + report_cb => logger:report_cb(), + quit => template()}). + +-type(template() :: [metakey() | {metakey(),template(),template()} | string()]). + +-type(metakey() :: atom() | [atom()]). + +%%-------------------------------------------------------------------- +%% API -%%%----------------------------------------------------------------- -%%% API -spec format(LogEvent,Config) -> unicode:chardata() when LogEvent :: logger:log_event(), Config :: config(). diff --git a/src/emqx_message.erl b/src/emqx_message.erl index e2394a5df..d0059cde0 100644 --- a/src/emqx_message.erl +++ b/src/emqx_message.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_message). @@ -74,7 +76,7 @@ make(From, Topic, Payload) -> make(From, ?QOS_0, Topic, Payload). -spec(make(atom() | emqx_types:client_id(), - emqx_mqtt_types:qos(), + emqx_types:qos(), emqx_topic:topic(), emqx_types:payload()) -> emqx_types:message()). make(From, QoS, Topic, Payload) when ?QOS_0 =< QoS, QoS =< ?QOS_2 -> @@ -89,7 +91,7 @@ make(From, QoS, Topic, Payload) when ?QOS_0 =< QoS, QoS =< ?QOS_2 -> -spec(id(emqx_types:message()) -> maybe(binary())). id(#message{id = Id}) -> Id. --spec(qos(emqx_types:message()) -> emqx_mqtt_types:qos()). +-spec(qos(emqx_types:message()) -> emqx_types:qos()). qos(#message{qos = QoS}) -> QoS. -spec(from(emqx_types:message()) -> atom() | binary()). diff --git a/src/emqx_metrics.erl b/src/emqx_metrics.erl index fc04ab597..c7f347dc5 100644 --- a/src/emqx_metrics.erl +++ b/src/emqx_metrics.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_metrics). @@ -58,12 +60,12 @@ , code_change/3 ]). +-export_type([metric_idx/0]). + -opaque(metric_idx() :: 1..1024). -type(metric_name() :: atom() | string() | binary()). --export_type([metric_idx/0]). - -define(MAX_SIZE, 1024). -define(RESERVED_IDX, 256). -define(TAB, ?MODULE). @@ -92,6 +94,7 @@ {counter, 'packets.puback.missed'}, % PUBACK packets missed {counter, 'packets.pubrec.received'}, % PUBREC packets received {counter, 'packets.pubrec.sent'}, % PUBREC packets sent + {counter, 'packets.pubrec.inuse'}, % PUBREC packet_id inuse {counter, 'packets.pubrec.missed'}, % PUBREC packets missed {counter, 'packets.pubrel.received'}, % PUBREL packets received {counter, 'packets.pubrel.sent'}, % PUBREL packets sent @@ -132,10 +135,15 @@ {counter, 'messages.forward'} % Messages forward ]). +-define(CHAN_METRICS, [ + {counter, 'channel.gc.cnt'} +]). + -define(MQTT_METRICS, [ {counter, 'auth.mqtt.anonymous'} ]). + -record(state, {next_idx = 1}). -record(metric, {name, type, idx}). @@ -149,9 +157,9 @@ start_link() -> stop() -> gen_server:stop(?SERVER). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Metrics API -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- -spec(new(metric_name()) -> ok). new(Name) -> @@ -255,12 +263,12 @@ update_counter(Name, Value) -> end, counters:add(CRef, CIdx, Value). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Inc Received/Sent metrics -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% @doc Inc packets received. --spec(inc_recv(emqx_mqtt_types:packet()) -> ok). +-spec(inc_recv(emqx_types:packet()) -> ok). inc_recv(Packet) -> inc('packets.received'), do_inc_recv(Packet). @@ -297,7 +305,7 @@ do_inc_recv(_Packet) -> ignore. %% @doc Inc packets sent. Will not count $SYS PUBLISH. --spec(inc_sent(emqx_mqtt_types:packet()) -> ok | ignore). +-spec(inc_sent(emqx_types:packet()) -> ok | ignore). inc_sent(?PUBLISH_PACKET(_QoS, <<"$SYS/", _/binary>>, _, _)) -> ignore; inc_sent(Packet) -> @@ -343,9 +351,9 @@ do_inc_sent(?PACKET(?AUTH)) -> do_inc_sent(_Packet) -> ignore. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% gen_server callbacks -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- init([]) -> % Create counters array @@ -395,9 +403,9 @@ terminate(_Reason, _State) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Internal functions -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- reserved_idx('bytes.received') -> 01; reserved_idx('bytes.sent') -> 02; @@ -451,4 +459,7 @@ reserved_idx('messages.dropped') -> 49; reserved_idx('messages.expired') -> 50; reserved_idx('messages.forward') -> 51; reserved_idx('auth.mqtt.anonymous') -> 52; +reserved_idx('channel.gc.cnt') -> 53; +reserved_idx('packets.pubrec.inuse') -> 54; reserved_idx(_) -> undefined. + diff --git a/src/emqx_misc.erl b/src/emqx_misc.erl index e236b560f..42e88d850 100644 --- a/src/emqx_misc.erl +++ b/src/emqx_misc.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_misc). @@ -27,7 +29,14 @@ , conn_proc_mng_policy/1 ]). --export([drain_down/1]). +-export([ drain_deliver/1 + , drain_down/1 + ]). + +-compile({inline, + [ start_timer/2 + , start_timer/3 + ]}). %% @doc Merge options -spec(merge_opts(list(), list()) -> list()). @@ -66,9 +75,12 @@ proc_stats() -> -spec(proc_stats(pid()) -> list()). proc_stats(Pid) -> - Stats = process_info(Pid, [message_queue_len, heap_size, reductions]), - {value, {_, V}, Stats1} = lists:keytake(message_queue_len, 1, Stats), - [{mailbox_len, V} | Stats1]. + case process_info(Pid, [message_queue_len, heap_size, + total_heap_size, reductions, memory]) of + undefined -> []; + [{message_queue_len, Len}|Stats] -> + [{mailbox_len, Len}|Stats] + end. -define(DISABLED, 0). @@ -113,24 +125,34 @@ check([{Pred, Result} | Rest]) -> is_message_queue_too_long(Qlength, Max) -> is_enabled(Max) andalso Qlength > Max. -is_enabled(Max) -> is_integer(Max) andalso Max > ?DISABLED. +is_enabled(Max) -> + is_integer(Max) andalso Max > ?DISABLED. proc_info(Key) -> {Key, Value} = erlang:process_info(self(), Key), Value. +%% @doc Drain delivers from the channel's mailbox. +drain_deliver(Acc) -> + receive + Deliver = {deliver, _Topic, _Msg} -> + drain_deliver([Deliver|Acc]) + after 0 -> + lists:reverse(Acc) + end. + +%% @doc Drain process down events. -spec(drain_down(pos_integer()) -> list(pid())). drain_down(Cnt) when Cnt > 0 -> drain_down(Cnt, []). drain_down(0, Acc) -> lists:reverse(Acc); - drain_down(Cnt, Acc) -> receive {'DOWN', _MRef, process, Pid, _Reason} -> drain_down(Cnt - 1, [Pid|Acc]) after 0 -> - lists:reverse(Acc) + drain_down(0, Acc) end. diff --git a/src/emqx_mod_acl_internal.erl b/src/emqx_mod_acl_internal.erl index 9c88c9ea2..2629ed60a 100644 --- a/src/emqx_mod_acl_internal.erl +++ b/src/emqx_mod_acl_internal.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_acl_internal). @@ -37,9 +39,9 @@ -type(acl_rules() :: #{publish => [emqx_access_rule:rule()], subscribe => [emqx_access_rule:rule()]}). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% API -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- load(_Env) -> Rules = rules_from_file(acl_file()), @@ -54,16 +56,16 @@ unload(_Env) -> all_rules() -> rules_from_file(acl_file()). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% ACL callbacks -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% @doc Check ACL --spec(check_acl(emqx_types:credentials(), emqx_types:pubsub(), emqx_topic:topic(), +-spec(check_acl(emqx_types:client(), emqx_types:pubsub(), emqx_topic:topic(), emqx_access_rule:acl_result(), acl_rules()) -> {ok, allow} | {ok, deny} | ok). -check_acl(Credentials, PubSub, Topic, _AclResult, Rules) -> - case match(Credentials, Topic, lookup(PubSub, Rules)) of +check_acl(Client, PubSub, Topic, _AclResult, Rules) -> + case match(Client, Topic, lookup(PubSub, Rules)) of {matched, allow} -> {ok, allow}; {matched, deny} -> {ok, deny}; nomatch -> ok @@ -73,9 +75,9 @@ check_acl(Credentials, PubSub, Topic, _AclResult, Rules) -> reload_acl() -> unload([]), load([]). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Internal Functions -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- acl_file() -> emqx_config:get_env(acl_file). @@ -83,12 +85,12 @@ acl_file() -> lookup(PubSub, Rules) -> maps:get(PubSub, Rules, []). -match(_Credentials, _Topic, []) -> +match(_Client, _Topic, []) -> nomatch; -match(Credentials, Topic, [Rule|Rules]) -> - case emqx_access_rule:match(Credentials, Topic, Rule) of +match(Client, Topic, [Rule|Rules]) -> + case emqx_access_rule:match(Client, Topic, Rule) of nomatch -> - match(Credentials, Topic, Rules); + match(Client, Topic, Rules); {matched, AllowDeny} -> {matched, AllowDeny} end. diff --git a/src/emqx_mod_presence.erl b/src/emqx_mod_presence.erl index ef164102d..97f7a9929 100644 --- a/src/emqx_mod_presence.erl +++ b/src/emqx_mod_presence.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_presence). @@ -31,39 +33,50 @@ , unload/1 ]). --define(ATTR_KEYS, [clean_start, proto_ver, proto_name, keepalive]). - -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% APIs -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- load(Env) -> - emqx_hooks:add('client.connected', fun ?MODULE:on_client_connected/4, [Env]), - emqx_hooks:add('client.disconnected', fun ?MODULE:on_client_disconnected/3, [Env]). + emqx_hooks:add('client.connected', {?MODULE, on_client_connected, [Env]}), + emqx_hooks:add('client.disconnected', {?MODULE, on_client_disconnected, [Env]}). on_client_connected(#{client_id := ClientId, username := Username, - peername := {IpAddr, _}}, ConnAck, ConnAttrs, Env) -> - Attrs = maps:filter(fun(K, _) -> - lists:member(K, ?ATTR_KEYS) - end, ConnAttrs), - case emqx_json:safe_encode(Attrs#{clientid => ClientId, - username => Username, - ipaddress => iolist_to_binary(esockd_net:ntoa(IpAddr)), - connack => ConnAck, - ts => erlang:system_time(millisecond) - }) of + peername := {IpAddr, _} + }, ConnAck, + #{session := #{clean_start := CleanStart, + expiry_interval := Interval + }, + 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 {ok, Payload} -> emqx:publish(message(qos(Env), topic(connected, ClientId), Payload)); {error, Reason} -> ?LOG(error, "Encoding connected event error: ~p", [Reason]) end. -on_client_disconnected(#{client_id := ClientId, username := Username}, Reason, Env) -> - case emqx_json:safe_encode([{clientid, ClientId}, - {username, Username}, - {reason, reason(Reason)}, - {ts, erlang:system_time(millisecond)}]) of +on_client_disconnected(#{client_id := ClientId, + username := Username}, Reason, Env) -> + case emqx_json:safe_encode(#{clientid => ClientId, + username => Username, + reason => reason(Reason), + ts => erlang:system_time(millisecond) + }) of {ok, Payload} -> emqx_broker:publish(message(qos(Env), topic(disconnected, ClientId), Payload)); {error, Reason} -> @@ -71,8 +84,8 @@ on_client_disconnected(#{client_id := ClientId, username := Username}, Reason, E end. unload(_Env) -> - emqx_hooks:del('client.connected', fun ?MODULE:on_client_connected/4), - emqx_hooks:del('client.disconnected', fun ?MODULE:on_client_disconnected/3). + emqx_hooks:del('client.connected', {?MODULE, on_client_connected}), + emqx_hooks:del('client.disconnected', {?MODULE, on_client_disconnected}). message(QoS, Topic, Payload) -> emqx_message:set_flag( @@ -89,3 +102,4 @@ qos(Env) -> proplists:get_value(qos, Env, 0). reason(Reason) when is_atom(Reason) -> Reason; reason({Error, _}) when is_atom(Error) -> Error; reason(_) -> internal_error. + diff --git a/src/emqx_mod_rewrite.erl b/src/emqx_mod_rewrite.erl index b06428e9f..5cd5af323 100644 --- a/src/emqx_mod_rewrite.erl +++ b/src/emqx_mod_rewrite.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_rewrite). @@ -20,8 +22,8 @@ -include_lib("emqx_mqtt.hrl"). %% APIs --export([ rewrite_subscribe/3 - , rewrite_unsubscribe/3 +-export([ rewrite_subscribe/4 + , rewrite_unsubscribe/4 , rewrite_publish/2 ]). @@ -30,33 +32,33 @@ , unload/1 ]). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Load/Unload -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- load(RawRules) -> Rules = compile(RawRules), - emqx_hooks:add('client.subscribe', fun ?MODULE:rewrite_subscribe/3, [Rules]), - emqx_hooks:add('client.unsubscribe', fun ?MODULE:rewrite_unsubscribe/3, [Rules]), - emqx_hooks:add('message.publish', fun ?MODULE:rewrite_publish/2, [Rules]). + emqx_hooks:add('client.subscribe', {?MODULE, rewrite_subscribe, [Rules]}), + emqx_hooks:add('client.unsubscribe', {?MODULE, rewrite_unsubscribe, [Rules]}), + emqx_hooks:add('message.publish', {?MODULE, rewrite_publish, [Rules]}). -rewrite_subscribe(_Credentials, TopicTable, Rules) -> - {ok, [{match_rule(Topic, Rules), Opts} || {Topic, Opts} <- TopicTable]}. +rewrite_subscribe(_Client, _Properties, TopicFilters, Rules) -> + {ok, [{match_rule(Topic, Rules), Opts} || {Topic, Opts} <- TopicFilters]}. -rewrite_unsubscribe(_Credentials, TopicTable, Rules) -> - {ok, [{match_rule(Topic, Rules), Opts} || {Topic, Opts} <- TopicTable]}. +rewrite_unsubscribe(_Client, _Properties, TopicFilters, Rules) -> + {ok, [{match_rule(Topic, Rules), Opts} || {Topic, Opts} <- TopicFilters]}. rewrite_publish(Message = #message{topic = Topic}, Rules) -> {ok, Message#message{topic = match_rule(Topic, Rules)}}. unload(_) -> - emqx_hooks:del('client.subscribe', fun ?MODULE:rewrite_subscribe/3), - emqx_hooks:del('client.unsubscribe', fun ?MODULE:rewrite_unsubscribe/3), - emqx_hooks:del('message.publish', fun ?MODULE:rewrite_publish/2). + emqx_hooks:del('client.subscribe', {?MODULE, rewrite_subscribe}), + emqx_hooks:del('client.unsubscribe', {?MODULE, rewrite_unsubscribe}), + emqx_hooks:del('message.publish', {?MODULE, rewrite_publish}). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Internal functions -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- match_rule(Topic, []) -> Topic; @@ -84,3 +86,4 @@ compile(Rules) -> {ok, MP} = re:compile(Re), {rewrite, Topic, MP, Dest} end, Rules). + diff --git a/src/emqx_mod_subscription.erl b/src/emqx_mod_subscription.erl index c674adf97..f46ef0f0b 100644 --- a/src/emqx_mod_subscription.erl +++ b/src/emqx_mod_subscription.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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). @@ -20,28 +22,30 @@ -include_lib("emqx_mqtt.hrl"). %% APIs --export([on_session_created/3]). +-export([on_client_connected/4]). %% emqx_gen_mod callbacks -export([ load/1 , unload/1 ]). -%%------------------------------------------------------------------------------ + +%%-------------------------------------------------------------------- %% Load/Unload Hook -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- load(Topics) -> - emqx_hooks:add('session.created', fun ?MODULE:on_session_created/3, [Topics]). + emqx_hooks:add('client.connected', {?MODULE, on_client_connected, [Topics]}). -on_session_created(#{client_id := ClientId}, SessAttrs, Topics) -> - Username = proplists:get_value(username, SessAttrs), +on_client_connected(#{client_id := ClientId, + username := Username}, ?RC_SUCCESS, _ConnAttrs, Topics) -> Replace = fun(Topic) -> rep(<<"%u">>, Username, rep(<<"%c">>, ClientId, Topic)) end, - emqx_session:subscribe(self(), [{Replace(Topic), #{qos => QoS}} || {Topic, QoS} <- Topics]). + TopicFilters = [{Replace(Topic), #{qos => QoS}} || {Topic, QoS} <- Topics], + self() ! {subscribe, TopicFilters}. unload(_) -> - emqx_hooks:del('session.created', fun ?MODULE:on_session_created/3). + emqx_hooks:del('client.connected', {?MODULE, on_client_connected}). %%------------------------------------------------------------------------------ %% Internal functions diff --git a/src/emqx_mod_sup.erl b/src/emqx_mod_sup.erl index 54a77feda..cb6e86130 100644 --- a/src/emqx_mod_sup.erl +++ b/src/emqx_mod_sup.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,11 +12,14 @@ %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and %% limitations under the License. +%%-------------------------------------------------------------------- -module(emqx_mod_sup). -behaviour(supervisor). +-include("types.hrl"). + -export([ start_link/0 , start_child/1 , start_child/2 @@ -25,8 +29,14 @@ -export([init/1]). %% Helper macro for declaring children of supervisor --define(CHILD(Mod, Type), {Mod, {Mod, start_link, []}, permanent, 5000, Type, [Mod]}). +-define(CHILD(Mod, Type), #{id => Mod, + start => {Mod, start_link, []}, + restart => permanent, + shutdown => 5000, + type => Type, + modules => [Mod]}). +-spec(start_link() -> startlink_ret()). start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). @@ -39,13 +49,14 @@ start_child(Mod, Type) when is_atom(Mod) andalso is_atom(Type) -> -spec(stop_child(any()) -> ok | {error, term()}). stop_child(ChildId) -> case supervisor:terminate_child(?MODULE, ChildId) of - ok -> supervisor:delete_child(?MODULE, ChildId); + ok -> supervisor:delete_child(?MODULE, ChildId); Error -> Error end. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Supervisor callbacks -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- init([]) -> {ok, {{one_for_one, 10, 100}, []}}. + diff --git a/src/emqx_modules.erl b/src/emqx_modules.erl index 79e2c02b4..c96cd6c15 100644 --- a/src/emqx_modules.erl +++ b/src/emqx_modules.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_modules). @@ -22,20 +24,25 @@ , unload/0 ]). +%% @doc Load all the extended modules. -spec(load() -> ok). load() -> ok = emqx_mod_acl_internal:load([]), - lists:foreach( - fun({Mod, Env}) -> - ok = Mod:load(Env), - ?LOG(info, "Load ~s module successfully.", [Mod]) - end, emqx_config:get_env(modules, [])). + lists:foreach(fun load/1, modules()). +load({Mod, Env}) -> + ok = Mod:load(Env), + ?LOG(info, "Load ~s module successfully.", [Mod]). + +modules() -> + emqx_config:get_env(modules, []). + +%% @doc Unload all the extended modules. -spec(unload() -> ok). unload() -> ok = emqx_mod_acl_internal:unload([]), - lists:foreach( - fun({Mod, Env}) -> - Mod:unload(Env) end, - emqx_config:get_env(modules, [])). + lists:foreach(fun unload/1, modules()). + +unload({Mod, Env}) -> + Mod:unload(Env). diff --git a/src/emqx_mountpoint.erl b/src/emqx_mountpoint.erl index 66b8da057..986bbd048 100644 --- a/src/emqx_mountpoint.erl +++ b/src/emqx_mountpoint.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,13 +12,12 @@ %% 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_mountpoint). -include("emqx.hrl"). --include("logger.hrl"). - --logger_header("[Mountpoint]"). +-include("types.hrl"). -export([ mount/2 , unmount/2 @@ -25,37 +25,50 @@ -export([replvar/2]). --type(mountpoint() :: binary()). - -export_type([mountpoint/0]). -%%------------------------------------------------------------------------------ -%% APIs -%%------------------------------------------------------------------------------ +-type(mountpoint() :: binary()). +-spec(mount(maybe(mountpoint()), Any) -> Any + when Any :: emqx_types:topic() + | emqx_types:message() + | emqx_types:topic_filters()). mount(undefined, Any) -> Any; +mount(MountPoint, Topic) when is_binary(Topic) -> + prefix(MountPoint, Topic); mount(MountPoint, Msg = #message{topic = Topic}) -> - Msg#message{topic = <>}; - + Msg#message{topic = prefix(MountPoint, Topic)}; mount(MountPoint, TopicFilters) when is_list(TopicFilters) -> - [{<>, SubOpts} || {Topic, SubOpts} <- TopicFilters]. + [{prefix(MountPoint, Topic), SubOpts} || {Topic, SubOpts} <- TopicFilters]. -unmount(undefined, Msg) -> - Msg; +%% @private +-compile({inline, [prefix/2]}). +prefix(MountPoint, Topic) -> + <>. + +-spec(unmount(maybe(mountpoint()), Any) -> Any + when Any :: emqx_types:topic() + | emqx_types:message()). +unmount(undefined, Any) -> + Any; +unmount(MountPoint, Topic) when is_binary(Topic) -> + case string:prefix(Topic, MountPoint) of + nomatch -> Topic; + Topic1 -> Topic1 + end; unmount(MountPoint, Msg = #message{topic = Topic}) -> - try split_binary(Topic, byte_size(MountPoint)) of - {MountPoint, Topic1} -> Msg#message{topic = Topic1} - catch - _Error:Reason -> - ?LOG(error, "Unmount error : ~p", [Reason]), - Msg + case string:prefix(Topic, MountPoint) of + nomatch -> Msg; + Topic1 -> Msg#message{topic = Topic1} end. +-spec(replvar(maybe(mountpoint()), map()) -> maybe(mountpoint())). replvar(undefined, _Vars) -> undefined; replvar(MountPoint, #{client_id := ClientId, username := Username}) -> - lists:foldl(fun feed_var/2, MountPoint, [{<<"%c">>, ClientId}, {<<"%u">>, Username}]). + lists:foldl(fun feed_var/2, MountPoint, + [{<<"%c">>, ClientId}, {<<"%u">>, Username}]). feed_var({<<"%c">>, ClientId}, MountPoint) -> emqx_topic:feed_var(<<"%c">>, ClientId, MountPoint); diff --git a/src/emqx_mqtt_caps.erl b/src/emqx_mqtt_caps.erl index bde3e3eb5..0aa3a2a23 100644 --- a/src/emqx_mqtt_caps.erl +++ b/src/emqx_mqtt_caps.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,146 +12,139 @@ %% 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 MQTTv5 capabilities +%% @doc MQTTv5 Capabilities -module(emqx_mqtt_caps). --include("emqx.hrl"). -include("emqx_mqtt.hrl"). +-include("types.hrl"). -export([ check_pub/2 - , check_sub/2 - , get_caps/1 + , check_sub/3 + ]). + +-export([ get_caps/1 , get_caps/2 ]). --export([default_caps/0]). - --type(caps() :: #{max_packet_size => integer(), - max_clientid_len => integer(), - max_topic_alias => integer(), - max_topic_levels => integer(), - max_qos_allowed => emqx_mqtt_types:qos(), - mqtt_retain_available => boolean(), - mqtt_shared_subscription => boolean(), - mqtt_wildcard_subscription => boolean()}). +-export([default/0]). -export_type([caps/0]). +-type(caps() :: #{max_packet_size => integer(), + max_clientid_len => integer(), + max_topic_alias => integer(), + max_topic_levels => integer(), + max_qos_allowed => emqx_types:qos(), + retain_available => boolean(), + wildcard_subscription => boolean(), + subscription_identifiers => boolean(), + shared_subscription => boolean() + }). + -define(UNLIMITED, 0). --define(DEFAULT_CAPS, [{max_packet_size, ?MAX_PACKET_SIZE}, - {max_clientid_len, ?MAX_CLIENTID_LEN}, - {max_topic_alias, ?UNLIMITED}, - {max_topic_levels, ?UNLIMITED}, - {max_qos_allowed, ?QOS_2}, - {mqtt_retain_available, true}, - {mqtt_shared_subscription, true}, - {mqtt_wildcard_subscription, true}]). - --define(PUBCAP_KEYS, [max_qos_allowed, - mqtt_retain_available, - max_topic_alias +-define(PUBCAP_KEYS, [max_topic_alias, + max_qos_allowed, + retain_available ]). --define(SUBCAP_KEYS, [max_qos_allowed, - max_topic_levels, - mqtt_shared_subscription, - mqtt_wildcard_subscription]). --spec(check_pub(emqx_types:zone(), map()) -> ok | {error, emqx_mqtt_types:reason_code()}). -check_pub(Zone, Props) when is_map(Props) -> - do_check_pub(Props, maps:to_list(get_caps(Zone, publish))). +-define(SUBCAP_KEYS, [max_topic_levels, + max_qos_allowed, + wildcard_subscription, + shared_subscription + ]). -do_check_pub(_Props, []) -> - ok; -do_check_pub(Props = #{qos := QoS}, [{max_qos_allowed, MaxQoS}|Caps]) -> - case QoS > MaxQoS of - true -> {error, ?RC_QOS_NOT_SUPPORTED}; - false -> do_check_pub(Props, Caps) - end; -do_check_pub(Props = #{ topic_alias := TopicAlias}, [{max_topic_alias, MaxTopicAlias}| Caps]) -> - case TopicAlias =< MaxTopicAlias andalso TopicAlias > 0 of - false -> {error, ?RC_TOPIC_ALIAS_INVALID}; - true -> do_check_pub(Props, Caps) - end; -do_check_pub(#{retain := true}, [{mqtt_retain_available, false}|_Caps]) -> +-define(DEFAULT_CAPS, #{max_packet_size => ?MAX_PACKET_SIZE, + max_clientid_len => ?MAX_CLIENTID_LEN, + max_topic_alias => ?UNLIMITED, + max_topic_levels => ?UNLIMITED, + max_qos_allowed => ?QOS_2, + retain_available => true, + wildcard_subscription => true, + subscription_identifiers => true, + shared_subscription => true + }). + +-spec(check_pub(emqx_types:zone(), + #{qos => emqx_types:qos(), + retain => boolean()}) + -> ok_or_error(emqx_types:reason_code())). +check_pub(Zone, Flags) when is_map(Flags) -> + do_check_pub(Flags, get_caps(Zone, publish)). + +do_check_pub(#{qos := QoS}, #{max_qos_allowed := MaxQoS}) + when QoS > MaxQoS -> + {error, ?RC_QOS_NOT_SUPPORTED}; +do_check_pub(#{retain := true}, #{retain_available := false}) -> {error, ?RC_RETAIN_NOT_SUPPORTED}; -do_check_pub(Props, [{max_topic_alias, _} | Caps]) -> - do_check_pub(Props, Caps); -do_check_pub(Props, [{mqtt_retain_available, _}|Caps]) -> - do_check_pub(Props, Caps). +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(), emqx_mqtt_types:topic_filters()) - -> {ok | error, emqx_mqtt_types:topic_filters()}). -check_sub(Zone, TopicFilters) -> - Caps = maps:to_list(get_caps(Zone, subscribe)), - lists:foldr(fun({Topic, Opts}, {Ok, Result}) -> - case check_sub(Topic, Opts, Caps) of - {ok, Opts1} -> - {Ok, [{Topic, Opts1}|Result]}; - {error, Opts1} -> - {error, [{Topic, Opts1}|Result]} - end - end, {ok, []}, TopicFilters). +-spec(check_sub(emqx_types:zone(), + emqx_types:topic(), + emqx_types:subopts()) + -> ok_or_error(emqx_types:reason_code())). +check_sub(Zone, Topic, SubOpts) -> + Caps = get_caps(Zone, subscribe), + Flags = lists:foldl( + fun(max_topic_levels, Map) -> + Map#{topic_levels => emqx_topic:levels(Topic)}; + (wildcard_subscription, Map) -> + Map#{is_wildcard => emqx_topic:wildcard(Topic)}; + (shared_subscription, Map) -> + Map#{is_shared => maps:is_key(share, SubOpts)}; + (_Key, Map) -> Map %% Ignore + end, #{}, maps:keys(Caps)), + do_check_sub(Flags, Caps). -check_sub(_Topic, Opts, []) -> - {ok, Opts}; -check_sub(Topic, Opts = #{qos := QoS}, [{max_qos_allowed, MaxQoS}|Caps]) -> - check_sub(Topic, Opts#{qos := min(QoS, MaxQoS)}, Caps); -check_sub(Topic, Opts, [{mqtt_shared_subscription, true}|Caps]) -> - check_sub(Topic, Opts, Caps); -check_sub(Topic, Opts, [{mqtt_shared_subscription, false}|Caps]) -> - case maps:is_key(share, Opts) of - true -> - {error, Opts#{rc := ?RC_SHARED_SUBSCRIPTIONS_NOT_SUPPORTED}}; - false -> check_sub(Topic, Opts, Caps) - end; -check_sub(Topic, Opts, [{mqtt_wildcard_subscription, true}|Caps]) -> - check_sub(Topic, Opts, Caps); -check_sub(Topic, Opts, [{mqtt_wildcard_subscription, false}|Caps]) -> - case emqx_topic:wildcard(Topic) of - true -> - {error, Opts#{rc := ?RC_WILDCARD_SUBSCRIPTIONS_NOT_SUPPORTED}}; - false -> check_sub(Topic, Opts, Caps) - end; -check_sub(Topic, Opts, [{max_topic_levels, ?UNLIMITED}|Caps]) -> - check_sub(Topic, Opts, Caps); -check_sub(Topic, Opts, [{max_topic_levels, Limit}|Caps]) -> - case emqx_topic:levels(Topic) of - Levels when Levels > Limit -> - {error, Opts#{rc := ?RC_TOPIC_FILTER_INVALID}}; - _ -> check_sub(Topic, Opts, Caps) - end. +do_check_sub(#{topic_levels := Levels}, #{max_topic_levels := Limit}) + when Limit > 0, Levels > Limit -> + {error, ?RC_TOPIC_FILTER_INVALID}; +do_check_sub(#{is_wildcard := true}, #{wildcard_subscription := false}) -> + {error, ?RC_WILDCARD_SUBSCRIPTIONS_NOT_SUPPORTED}; +do_check_sub(#{is_shared := true}, #{shared_subscription := false}) -> + {error, ?RC_SHARED_SUBSCRIPTIONS_NOT_SUPPORTED}; +do_check_sub(_Flags, _Caps) -> ok. -default_caps() -> - ?DEFAULT_CAPS. +-spec(get_caps(emqx_zone:zone()) -> caps()). +get_caps(Zone) -> + with_env(Zone, '$mqtt_caps', fun all_caps/1). +-spec(get_caps(emqx_zone:zone(), publish|subscribe) -> caps()). get_caps(Zone, publish) -> - with_env(Zone, '$mqtt_pub_caps', - fun() -> - filter_caps(?PUBCAP_KEYS, get_caps(Zone)) - end); + with_env(Zone, '$mqtt_pub_caps', fun pub_caps/1); get_caps(Zone, subscribe) -> - with_env(Zone, '$mqtt_sub_caps', - fun() -> - filter_caps(?SUBCAP_KEYS, get_caps(Zone)) - end). + with_env(Zone, '$mqtt_sub_caps', fun sub_caps/1). -get_caps(Zone) -> - with_env(Zone, '$mqtt_caps', - fun() -> - maps:from_list([{Cap, emqx_zone:get_env(Zone, Cap, Def)} - || {Cap, Def} <- ?DEFAULT_CAPS]) - end). +pub_caps(Zone) -> + filter_caps(?PUBCAP_KEYS, get_caps(Zone)). + +sub_caps(Zone) -> + filter_caps(?SUBCAP_KEYS, get_caps(Zone)). + +all_caps(Zone) -> + maps:map(fun(Cap, Def) -> + emqx_zone:get_env(Zone, Cap, Def) + end, ?DEFAULT_CAPS). filter_caps(Keys, Caps) -> maps:filter(fun(Key, _Val) -> lists:member(Key, Keys) end, Caps). with_env(Zone, Key, InitFun) -> case emqx_zone:get_env(Zone, Key) of - undefined -> Caps = InitFun(), - ok = emqx_zone:set_env(Zone, Key, Caps), - Caps; - ZoneCaps -> ZoneCaps + undefined -> + Caps = InitFun(Zone), + ok = emqx_zone:set_env(Zone, Key, Caps), + Caps; + Caps -> Caps end. + +-spec(default() -> caps()). +default() -> ?DEFAULT_CAPS. + diff --git a/src/emqx_mqtt_props.erl b/src/emqx_mqtt_props.erl index 686bb5bc8..47a368714 100644 --- a/src/emqx_mqtt_props.erl +++ b/src/emqx_mqtt_props.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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 MQTT5 Properties -module(emqx_mqtt_props). diff --git a/src/emqx_mqtt_types.erl b/src/emqx_mqtt_types.erl deleted file mode 100644 index 0274b6ac3..000000000 --- a/src/emqx_mqtt_types.erl +++ /dev/null @@ -1,43 +0,0 @@ -%% 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_mqtt_types). - --include("emqx_mqtt.hrl"). - --export_type([version/0, qos/0, qos_name/0]). --export_type([connack/0, reason_code/0]). --export_type([properties/0, subopts/0]). --export_type([topic_filters/0]). --export_type([packet_id/0, packet_type/0, packet/0]). - --type(qos() :: ?QOS_0 | ?QOS_1 | ?QOS_2). --type(version() :: ?MQTT_PROTO_V3 | ?MQTT_PROTO_V4 | ?MQTT_PROTO_V5). --type(qos_name() :: qos0 | at_most_once | - qos1 | at_least_once | - qos2 | exactly_once). --type(packet_type() :: ?RESERVED..?AUTH). --type(connack() :: ?CONNACK_ACCEPT..?CONNACK_AUTH). --type(reason_code() :: 0..16#FF). --type(packet_id() :: 1..16#FFFF). --type(properties() :: #{atom() => term()}). --type(subopts() :: #{rh := 0 | 1 | 2, - rap := 0 | 1, - nl := 0 | 1, - qos := qos(), - rc => reason_code() - }). --type(topic_filters() :: [{emqx_topic:topic(), subopts()}]). --type(packet() :: #mqtt_packet{}). - diff --git a/src/emqx_mqueue.erl b/src/emqx_mqueue.erl index f13ccdd34..12154c184 100644 --- a/src/emqx_mqueue.erl +++ b/src/emqx_mqueue.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,7 +12,9 @@ %% 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 A Simple in-memory message queue. %% %% Notice that MQTT is not a (on-disk) persistent messaging queue. @@ -42,6 +45,7 @@ %% unless `max_len' is set to `0' which implies (`infinity'). %% %% @end +%%-------------------------------------------------------------------- -module(emqx_mqueue). diff --git a/src/emqx_os_mon.erl b/src/emqx_os_mon.erl index 967f94ea2..8ce16700a 100644 --- a/src/emqx_os_mon.erl +++ b/src/emqx_os_mon.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_os_mon). @@ -18,19 +20,10 @@ -include("logger.hrl"). --logger_header("[OS Monitor]"). +-logger_header("[OS_MON]"). -export([start_link/1]). -%% gen_server callbacks --export([ init/1 - , handle_call/3 - , handle_cast/2 - , handle_info/2 - , terminate/2 - , code_change/3 - ]). - -export([ get_cpu_check_interval/0 , set_cpu_check_interval/1 , get_cpu_high_watermark/0 @@ -45,17 +38,26 @@ , set_procmem_high_watermark/1 ]). --define(OS_MON, ?MODULE). +%% gen_server callbacks +-export([ init/1 + , handle_call/3 + , handle_cast/2 + , handle_info/2 + , terminate/2 + , code_change/3 + ]). -include("emqx.hrl"). -%%------------------------------------------------------------------------------ -%% API -%%------------------------------------------------------------------------------ +-define(OS_MON, ?MODULE). start_link(Opts) -> gen_server:start_link({local, ?OS_MON}, ?MODULE, [Opts], []). +%%-------------------------------------------------------------------- +%% API +%%-------------------------------------------------------------------- + get_cpu_check_interval() -> call(get_cpu_check_interval). @@ -92,12 +94,14 @@ get_procmem_high_watermark() -> set_procmem_high_watermark(Float) -> memsup:set_procmem_high_watermark(Float). -%%------------------------------------------------------------------------------ +call(Req) -> + gen_server:call(?OS_MON, Req, infinity). + +%%-------------------------------------------------------------------- %% gen_server callbacks -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- init([Opts]) -> - _ = ?compat_windows(cpu_sup:util(), windows), set_mem_check_interval(proplists:get_value(mem_check_interval, Opts, 60)), set_sysmem_high_watermark(proplists:get_value(sysmem_high_watermark, Opts, 0.70)), set_procmem_high_watermark(proplists:get_value(procmem_high_watermark, Opts, 0.05)), @@ -109,49 +113,56 @@ init([Opts]) -> handle_call(get_cpu_check_interval, _From, State) -> {reply, maps:get(cpu_check_interval, State, undefined), State}; + handle_call({set_cpu_check_interval, Seconds}, _From, State) -> {reply, ok, State#{cpu_check_interval := Seconds}}; handle_call(get_cpu_high_watermark, _From, State) -> {reply, maps:get(cpu_high_watermark, State, undefined), State}; + handle_call({set_cpu_high_watermark, Float}, _From, State) -> {reply, ok, State#{cpu_high_watermark := Float}}; handle_call(get_cpu_low_watermark, _From, State) -> {reply, maps:get(cpu_low_watermark, State, undefined), State}; + handle_call({set_cpu_low_watermark, Float}, _From, State) -> {reply, ok, State#{cpu_low_watermark := Float}}; -handle_call(_Request, _From, State) -> - {reply, ok, State}. +handle_call(Req, _From, State) -> + ?LOG(error, "Unexpected call: ~p", [Req]), + {reply, ignored, State}. -handle_cast(_Request, State) -> +handle_cast(Msg, State) -> + ?LOG(error, "Unexpected cast: ~p", [Msg]), {noreply, State}. handle_info({timeout, Timer, check}, State = #{timer := Timer, cpu_high_watermark := CPUHighWatermark, cpu_low_watermark := CPULowWatermark, is_cpu_alarm_set := IsCPUAlarmSet}) -> - case ?compat_windows(cpu_sup:util(), windows) of - 0 -> - {noreply, State#{timer := undefined}}; + NState = + case emqx_vm:cpu_util() of %% TODO: should be improved? + 0 -> State#{timer := undefined}; {error, Reason} -> ?LOG(error, "Failed to get cpu utilization: ~p", [Reason]), - {noreply, ensure_check_timer(State)}; - windows -> - {noreply, State}; + ensure_check_timer(State); Busy when Busy / 100 >= CPUHighWatermark -> alarm_handler:set_alarm({cpu_high_watermark, Busy}), - {noreply, ensure_check_timer(State#{is_cpu_alarm_set := true})}; + ensure_check_timer(State#{is_cpu_alarm_set := true}); Busy when Busy / 100 < CPULowWatermark -> case IsCPUAlarmSet of - true -> alarm_handler:clear_alarm(cpu_high_watermark); + true -> alarm_handler:clear_alarm(cpu_high_watermark); false -> ok end, - {noreply, ensure_check_timer(State#{is_cpu_alarm_set := false})}; - _Busy -> - {noreply, ensure_check_timer(State)} - end. + ensure_check_timer(State#{is_cpu_alarm_set := false}); + _Busy -> ensure_check_timer(State) + end, + {noreply, NState}; + +handle_info(Info, State) -> + ?LOG(error, "unexpected info: ~p", [Info]), + {noreply, State}. terminate(_Reason, #{timer := Timer}) -> emqx_misc:cancel_timer(Timer). @@ -159,11 +170,12 @@ terminate(_Reason, #{timer := Timer}) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Internal functions -%%------------------------------------------------------------------------------ -call(Req) -> - gen_server:call(?OS_MON, Req, infinity). +%%-------------------------------------------------------------------- ensure_check_timer(State = #{cpu_check_interval := Interval}) -> - State#{timer := emqx_misc:start_timer(timer:seconds(Interval), check)}. + case erlang:system_info(system_architecture) of + "x86_64-pc-linux-musl" -> State; + _ -> State#{timer := emqx_misc:start_timer(timer:seconds(Interval), check)} + end. diff --git a/src/emqx_packet.erl b/src/emqx_packet.erl index ba28bddac..e0351b8a9 100644 --- a/src/emqx_packet.erl +++ b/src/emqx_packet.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_packet). @@ -27,7 +29,7 @@ ]). %% @doc Protocol name of version --spec(protocol_name(emqx_mqtt_types:version()) -> binary()). +-spec(protocol_name(emqx_types:version()) -> binary()). protocol_name(?MQTT_PROTO_V3) -> <<"MQIsdp">>; protocol_name(?MQTT_PROTO_V4) -> @@ -36,14 +38,15 @@ protocol_name(?MQTT_PROTO_V5) -> <<"MQTT">>. %% @doc Name of MQTT packet type --spec(type_name(emqx_mqtt_types:packet_type()) -> atom()). +-spec(type_name(emqx_types:packet_type()) -> atom()). type_name(Type) when Type > ?RESERVED andalso Type =< ?AUTH -> lists:nth(Type, ?TYPE_NAMES). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Validate MQTT Packet -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- +-spec(validate(emqx_types:packet()) -> true). validate(?SUBSCRIBE_PACKET(_PacketId, _Properties, [])) -> error(topic_filters_invalid); validate(?SUBSCRIBE_PACKET(PacketId, Properties, TopicFilters)) -> @@ -110,7 +113,8 @@ validate_qos(QoS) when ?QOS_0 =< QoS, QoS =< ?QOS_2 -> validate_qos(_) -> error(bad_qos). %% @doc From message to packet --spec(from_message(emqx_mqtt_types:packet_id(), emqx_types:message()) -> emqx_mqtt_types:packet()). +-spec(from_message(emqx_types:packet_id(), emqx_types:message()) + -> emqx_types:packet()). from_message(PacketId, #message{qos = QoS, flags = Flags, headers = Headers, topic = Topic, payload = Payload}) -> Flags1 = if Flags =:= undefined -> @@ -138,7 +142,7 @@ publish_props(Headers) -> 'Message-Expiry-Interval'], Headers). %% @doc Message from Packet --spec(to_message(emqx_types:credentials(), emqx_mqtt_types:packet()) +-spec(to_message(emqx_types:client(), emqx_ypes:packet()) -> emqx_types:message()). to_message(#{client_id := ClientId, username := Username, peername := Peername}, #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, @@ -173,7 +177,7 @@ merge_props(Headers, Props) -> maps:merge(Headers, Props). %% @doc Format packet --spec(format(emqx_mqtt_types:packet()) -> iolist()). +-spec(format(emqx_types:packet()) -> iolist()). format(#mqtt_packet{header = Header, variable = Variable, payload = Payload}) -> format_header(Header, format_variable(Variable, Payload)). @@ -233,11 +237,11 @@ format_variable(#mqtt_packet_puback{packet_id = PacketId}) -> format_variable(#mqtt_packet_subscribe{packet_id = PacketId, topic_filters = TopicFilters}) -> - io_lib:format("PacketId=~p, TopicFilters=~p", [PacketId, TopicFilters]); + io_lib:format("PacketId=~p, TopicFilters=~0p", [PacketId, TopicFilters]); format_variable(#mqtt_packet_unsubscribe{packet_id = PacketId, topic_filters = Topics}) -> - io_lib:format("PacketId=~p, TopicFilters=~p", [PacketId, Topics]); + io_lib:format("PacketId=~p, TopicFilters=~0p", [PacketId, Topics]); format_variable(#mqtt_packet_suback{packet_id = PacketId, reason_codes = ReasonCodes}) -> diff --git a/src/emqx_pd.erl b/src/emqx_pd.erl index 04b4d6075..9800d9d57 100644 --- a/src/emqx_pd.erl +++ b/src/emqx_pd.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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 The utility functions for erlang process dictionary. -module(emqx_pd). @@ -22,6 +24,12 @@ , reset_counter/1 ]). +-compile({inline, + [ update_counter/2 + , get_counter/1 + , reset_counter/1 + ]}). + -type(key() :: term()). -spec(update_counter(key(), number()) -> maybe(number())). diff --git a/src/emqx_plugins.erl b/src/emqx_plugins.erl index cedb5c5b0..7b7b1b895 100644 --- a/src/emqx_plugins.erl +++ b/src/emqx_plugins.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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). @@ -30,9 +32,9 @@ , load_expand_plugin/1 ]). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% APIs -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% @doc Init plugins' config -spec(init() -> ok). @@ -47,8 +49,8 @@ init() -> init_config(CfgFile) -> {ok, [AppsEnv]} = file:consult(CfgFile), - lists:foreach(fun({AppName, Envs}) -> - [application:set_env(AppName, Par, Val) || {Par, Val} <- Envs] + lists:foreach(fun({App, Envs}) -> + [application:set_env(App, Par, Val) || {Par, Val} <- Envs] end, AppsEnv). %% @doc Load all plugins when the broker started. diff --git a/src/emqx_pmon.erl b/src/emqx_pmon.erl index 6aa880f90..eb74918cf 100644 --- a/src/emqx_pmon.erl +++ b/src/emqx_pmon.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_pmon). @@ -30,50 +32,53 @@ -export([count/1]). --type(pmon() :: {?MODULE, map()}). -export_type([pmon/0]). -%%------------------------------------------------------------------------------ +-opaque(pmon() :: {?MODULE, map()}). + +-define(PMON(Map), {?MODULE, Map}). + +%%-------------------------------------------------------------------- %% APIs -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- -spec(new() -> pmon()). -new() -> - {?MODULE, maps:new()}. +new() -> ?PMON(maps:new()). -spec(monitor(pid(), pmon()) -> pmon()). -monitor(Pid, PM) -> - ?MODULE:monitor(Pid, undefined, PM). +monitor(Pid, PMon) -> + ?MODULE:monitor(Pid, undefined, PMon). -spec(monitor(pid(), term(), pmon()) -> pmon()). -monitor(Pid, Val, {?MODULE, PM}) -> - {?MODULE, case maps:is_key(Pid, PM) of - true -> PM; - false -> Ref = erlang:monitor(process, Pid), - maps:put(Pid, {Ref, Val}, PM) - end}. +monitor(Pid, Val, PMon = ?PMON(Map)) -> + case maps:is_key(Pid, Map) of + true -> PMon; + false -> + Ref = erlang:monitor(process, Pid), + ?PMON(maps:put(Pid, {Ref, Val}, Map)) + end. -spec(demonitor(pid(), pmon()) -> pmon()). -demonitor(Pid, {?MODULE, PM}) -> - {?MODULE, case maps:find(Pid, PM) of - {ok, {Ref, _Val}} -> - %% flush - _ = erlang:demonitor(Ref, [flush]), - maps:remove(Pid, PM); - error -> PM - end}. +demonitor(Pid, PMon = ?PMON(Map)) -> + case maps:find(Pid, Map) of + {ok, {Ref, _Val}} -> + %% flush + _ = erlang:demonitor(Ref, [flush]), + ?PMON(maps:remove(Pid, Map)); + error -> PMon + end. -spec(find(pid(), pmon()) -> error | {ok, term()}). -find(Pid, {?MODULE, PM}) -> - case maps:find(Pid, PM) of +find(Pid, ?PMON(Map)) -> + case maps:find(Pid, Map) of {ok, {_Ref, Val}} -> {ok, Val}; error -> error end. -spec(erase(pid(), pmon()) -> pmon()). -erase(Pid, {?MODULE, PM}) -> - {?MODULE, maps:remove(Pid, PM)}. +erase(Pid, ?PMON(Map)) -> + ?PMON(maps:remove(Pid, Map)). -spec(erase_all([pid()], pmon()) -> {[{pid(), term()}], pmon()}). erase_all(Pids, PMon0) -> @@ -87,6 +92,5 @@ erase_all(Pids, PMon0) -> end, {[], PMon0}, Pids). -spec(count(pmon()) -> non_neg_integer()). -count({?MODULE, PM}) -> - maps:size(PM). +count(?PMON(Map)) -> maps:size(Map). diff --git a/src/emqx_pool.erl b/src/emqx_pool.erl index 5a6231ee0..9db0c23c4 100644 --- a/src/emqx_pool.erl +++ b/src/emqx_pool.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_pool). @@ -47,9 +49,9 @@ -type(task() :: fun() | mfa() | {fun(), Args :: list(any())}). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% APIs -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% @doc Start pool. -spec(start_link(atom(), pos_integer()) -> startlink_ret()). @@ -87,9 +89,9 @@ cast(Msg) -> worker() -> gproc_pool:pick_worker(?POOL). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% gen_server callbacks -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- init([Pool, Id]) -> true = gproc_pool:connect_worker(Pool, {Pool, Id}), @@ -123,9 +125,9 @@ terminate(_Reason, #{pool := Pool, id := Id}) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Internal functions -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- run({M, F, A}) -> erlang:apply(M, F, A); diff --git a/src/emqx_pool_sup.erl b/src/emqx_pool_sup.erl index 1e884bafe..77278807d 100644 --- a/src/emqx_pool_sup.erl +++ b/src/emqx_pool_sup.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_pool_sup). @@ -44,7 +46,8 @@ spec(ChildId, Args) -> start_link() -> start_link(?POOL, random, {?POOL, start_link, []}). --spec(start_link(atom() | tuple(), atom(), mfa()) -> {ok, pid()} | {error, term()}). +-spec(start_link(atom() | tuple(), atom(), mfa()) + -> {ok, pid()} | {error, term()}). start_link(Pool, Type, MFA) -> start_link(Pool, Type, emqx_vm:schedulers(), MFA). @@ -54,11 +57,16 @@ start_link(Pool, Type, Size, MFA) -> supervisor:start_link(?MODULE, [Pool, Type, Size, MFA]). init([Pool, Type, Size, {M, F, Args}]) -> - ensure_pool(Pool, Type, [{size, Size}]), + ok = ensure_pool(Pool, Type, [{size, Size}]), {ok, {{one_for_one, 10, 3600}, [ begin ensure_pool_worker(Pool, {Pool, I}, I), - {{M, I}, {M, F, [Pool, I | Args]}, transient, 5000, worker, [M]} + #{id => {M, I}, + start => {M, F, [Pool, I | Args]}, + restart => transient, + shutdown => 5000, + type => worker, + modules => [M]} end || I <- lists:seq(1, Size)]}}. ensure_pool(Pool, Type, Opts) -> diff --git a/src/emqx_pqueue.erl b/src/emqx_pqueue.erl index 06ea192fb..85c89866d 100644 --- a/src/emqx_pqueue.erl +++ b/src/emqx_pqueue.erl @@ -57,6 +57,8 @@ , highest/1 ]). +-export_type([q/0]). + %%---------------------------------------------------------------------------- -type(priority() :: integer() | 'infinity'). @@ -64,8 +66,6 @@ -type(pqueue() :: squeue() | {pqueue, [{priority(), squeue()}]}). -type(q() :: pqueue()). --export_type([q/0]). - %%---------------------------------------------------------------------------- -spec(new() -> pqueue()). diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index b88c82df6..3fa92ac70 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -14,608 +14,321 @@ %% limitations under the License. %%-------------------------------------------------------------------- +%% MQTT Protocol -module(emqx_protocol). -include("emqx.hrl"). -include("emqx_mqtt.hrl"). -include("logger.hrl"). +-include("types.hrl"). -logger_header("[Protocol]"). -export([ info/1 + , info/2 , attrs/1 - , attr/2 , caps/1 - , caps/2 - , stats/1 - , client_id/1 - , credentials/1 - , session/1 ]). +%% for tests +-export([set/3]). + -export([ init/2 - , received/2 - , process/2 - , deliver/2 - , send/2 + , handle_in/2 + , handle_req/2 + , handle_deliver/2 + , handle_out/2 + , handle_timeout/3 , terminate/2 ]). --record(pstate, { - zone, - sendfun, - sockname, - peername, - peercert, - proto_ver, - proto_name, - client_id, - is_assigned, - conn_pid, - conn_props, - ack_props, - username, - session, - clean_start, - topic_aliases, - will_topic, - will_msg, - keepalive, - is_bridge, - recv_stats, - send_stats, - connected, - connected_at, - topic_alias_maximum, - conn_mod, - credentials, - ws_cookie - }). +-import(emqx_access_control, + [ authenticate/1 + , check_acl/3 + ]). --opaque(state() :: #pstate{}). +-export_type([proto_state/0]). --export_type([state/0]). +-record(protocol, { + client :: emqx_types:client(), + session :: emqx_session:session(), + proto_name :: binary(), + proto_ver :: emqx_types:ver(), + keepalive :: non_neg_integer(), + will_msg :: emqx_types:message(), + topic_aliases :: maybe(map()), + alias_maximum :: maybe(map()), + ack_props :: maybe(emqx_types:properties()) %% Tmp props + }). --ifdef(TEST). --compile(export_all). --compile(nowarn_export_all). --endif. +-opaque(proto_state() :: #protocol{}). -define(NO_PROPS, undefined). -%%------------------------------------------------------------------------------ -%% Init -%%------------------------------------------------------------------------------ - --spec(init(map(), list()) -> state()). -init(SocketOpts = #{ sockname := Sockname - , peername := Peername - , peercert := Peercert - , sendfun := SendFun}, Options) -> - Zone = proplists:get_value(zone, Options), - #pstate{zone = Zone, - sendfun = SendFun, - sockname = Sockname, - peername = Peername, - peercert = Peercert, - proto_ver = ?MQTT_PROTO_V4, - proto_name = <<"MQTT">>, - client_id = <<>>, - is_assigned = false, - conn_pid = self(), - username = init_username(Peercert, Options), - clean_start = false, - topic_aliases = #{}, - is_bridge = false, - recv_stats = #{msg => 0, pkt => 0}, - send_stats = #{msg => 0, pkt => 0}, - connected = false, - topic_alias_maximum = #{to_client => 0, from_client => 0}, - conn_mod = maps:get(conn_mod, SocketOpts, undefined), - credentials = #{}, - ws_cookie = maps:get(ws_cookie, SocketOpts, undefined)}. - -init_username(Peercert, Options) -> - case proplists:get_value(peer_cert_as_username, Options) of - cn -> esockd_peercert:common_name(Peercert); - dn -> esockd_peercert:subject(Peercert); - crt -> Peercert; - _ -> undefined - end. - -set_username(Username, PState = #pstate{username = undefined}) -> - PState#pstate{username = Username}; -set_username(_Username, PState) -> - PState. - -%%------------------------------------------------------------------------------ -%% API -%%------------------------------------------------------------------------------ - -info(PState = #pstate{zone = Zone, - conn_props = ConnProps, - ack_props = AckProps, - session = Session, - topic_aliases = Aliases, - will_msg = WillMsg}) -> - maps:merge(attrs(PState), #{conn_props => ConnProps, - ack_props => AckProps, - session => Session, - topic_aliases => Aliases, - will_msg => WillMsg, - enable_acl => emqx_zone:get_env(Zone, enable_acl, false) - }). - -attrs(#pstate{zone = Zone, - client_id = ClientId, - username = Username, - peername = Peername, - peercert = Peercert, - clean_start = CleanStart, - proto_ver = ProtoVer, - proto_name = ProtoName, - keepalive = Keepalive, - is_bridge = IsBridge, - connected_at = ConnectedAt, - conn_mod = ConnMod, - credentials = Credentials}) -> - #{ zone => Zone - , client_id => ClientId - , username => Username - , peername => Peername - , peercert => Peercert - , proto_ver => ProtoVer - , proto_name => ProtoName - , clean_start => CleanStart - , keepalive => Keepalive - , is_bridge => IsBridge - , connected_at => ConnectedAt - , conn_mod => ConnMod - , credentials => Credentials +-spec(info(proto_state()) -> emqx_types:infos()). +info(#protocol{client = Client, + session = Session, + proto_name = ProtoName, + proto_ver = ProtoVer, + keepalive = Keepalive, + will_msg = WillMsg, + topic_aliases = Aliases}) -> + #{client => Client, + session => session_info(Session), + proto_name => ProtoName, + proto_ver => ProtoVer, + keepalive => Keepalive, + will_msg => WillMsg, + topic_aliases => Aliases }. -attr(proto_ver, #pstate{proto_ver = ProtoVer}) -> +-spec(info(atom(), proto_state()) -> term()). +info(client, #protocol{client = Client}) -> + Client; +info(zone, #protocol{client = #{zone := Zone}}) -> + Zone; +info(client_id, #protocol{client = #{client_id := ClientId}}) -> + ClientId; +info(session, #protocol{session = Session}) -> + Session; +info(proto_name, #protocol{proto_name = ProtoName}) -> + ProtoName; +info(proto_ver, #protocol{proto_ver = ProtoVer}) -> ProtoVer; -attr(max_inflight, #pstate{proto_ver = ?MQTT_PROTO_V5, conn_props = ConnProps}) -> - get_property('Receive-Maximum', ConnProps, 65535); -attr(max_inflight, #pstate{zone = Zone}) -> - emqx_zone:get_env(Zone, max_inflight, 65535); -attr(expiry_interval, #pstate{proto_ver = ?MQTT_PROTO_V5, conn_props = ConnProps}) -> - get_property('Session-Expiry-Interval', ConnProps, 0); -attr(expiry_interval, #pstate{zone = Zone, clean_start = CleanStart}) -> - case CleanStart of - true -> 0; - false -> emqx_zone:get_env(Zone, session_expiry_interval, 16#ffffffff) - end; -attr(topic_alias_maximum, #pstate{proto_ver = ?MQTT_PROTO_V5, conn_props = ConnProps}) -> - get_property('Topic-Alias-Maximum', ConnProps, 0); -attr(topic_alias_maximum, #pstate{zone = Zone}) -> - emqx_zone:get_env(Zone, max_topic_alias, 0); -attr(Name, PState) -> - Attrs = lists:zip(record_info(fields, pstate), tl(tuple_to_list(PState))), - case lists:keyfind(Name, 1, Attrs) of - {_, Value} -> Value; - false -> undefined - end. +info(keepalive, #protocol{keepalive = Keepalive}) -> + Keepalive; +info(will_msg, #protocol{will_msg = WillMsg}) -> + WillMsg; +info(topic_aliases, #protocol{topic_aliases = Aliases}) -> + Aliases. -caps(Name, PState) -> - maps:get(Name, caps(PState)). +%% For tests +set(client, Client, PState) -> + PState#protocol{client = Client}; +set(session, Session, PState) -> + PState#protocol{session = Session}. -caps(#pstate{zone = Zone}) -> +attrs(#protocol{client = Client, + session = Session, + proto_name = ProtoName, + proto_ver = ProtoVer, + keepalive = Keepalive}) -> + #{client => Client, + session => emqx_session:attrs(Session), + proto_name => ProtoName, + proto_ver => ProtoVer, + keepalive => Keepalive + }. + +caps(#protocol{client = #{zone := Zone}}) -> emqx_mqtt_caps:get_caps(Zone). -client_id(#pstate{client_id = ClientId}) -> - ClientId. -credentials(#pstate{zone = Zone, - client_id = ClientId, - username = Username, - sockname = Sockname, - peername = Peername, - peercert = Peercert, - ws_cookie = WsCookie}) -> - with_cert(#{zone => Zone, - client_id => ClientId, - sockname => Sockname, - username => Username, - peername => Peername, - ws_cookie => WsCookie, - mountpoint => emqx_zone:get_env(Zone, mountpoint)}, Peercert). +-spec(init(emqx_types:conn(), proplists:proplist()) -> proto_state()). +init(ConnInfo, Options) -> + Zone = proplists:get_value(zone, Options), + Peercert = maps:get(peercert, ConnInfo, undefined), + Username = case peer_cert_as_username(Options) of + cn -> esockd_peercert:common_name(Peercert); + dn -> esockd_peercert:subject(Peercert); + crt -> Peercert; + _ -> undefined + end, + MountPoint = emqx_zone:get_env(Zone, mountpoint), + Client = maps:merge(#{zone => Zone, + username => Username, + mountpoint => MountPoint, + is_bridge => false, + is_superuser => false + }, ConnInfo), + #protocol{client = Client, + proto_name = <<"MQTT">>, + proto_ver = ?MQTT_PROTO_V4 + }. -with_cert(Credentials, undefined) -> Credentials; -with_cert(Credentials, Peercert) -> - Credentials#{dn => esockd_peercert:subject(Peercert), - cn => esockd_peercert:common_name(Peercert)}. +peer_cert_as_username(Options) -> + proplists:get_value(peer_cert_as_username, Options). -keepsafety(Credentials) -> - maps:filter(fun(password, _) -> false; - (dn, _) -> false; - (cn, _) -> false; - (_, _) -> true end, Credentials). +%%-------------------------------------------------------------------- +%% Handle incoming packet +%%-------------------------------------------------------------------- -stats(#pstate{recv_stats = #{pkt := RecvPkt, msg := RecvMsg}, - send_stats = #{pkt := SendPkt, msg := SendMsg}}) -> - [{recv_pkt, RecvPkt}, - {recv_msg, RecvMsg}, - {send_pkt, SendPkt}, - {send_msg, SendMsg}]. +-spec(handle_in(emqx_types:packet(), proto_state()) + -> {ok, proto_state()} + | {ok, emqx_types:packet(), proto_state()} + | {ok, list(emqx_types:packet()), proto_state()} + | {error, Reason :: term(), proto_state()} + | {stop, Error :: atom(), proto_state()}). +handle_in(?CONNECT_PACKET( + #mqtt_packet_connect{proto_name = ProtoName, + proto_ver = ProtoVer, + keepalive = Keepalive, + client_id = ClientId + } = ConnPkt), PState) -> + PState1 = PState#protocol{proto_name = ProtoName, + proto_ver = ProtoVer, + keepalive = Keepalive + }, + ok = emqx_logger:set_metadata_client_id(ClientId), + case pipeline([fun validate_in/2, + fun process_props/2, + fun check_connect/2, + fun enrich_client/2, + fun auth_connect/2], ConnPkt, PState1) of + {ok, NConnPkt, NPState} -> + process_connect(NConnPkt, maybe_assign_clientid(NPState)); + {error, ReasonCode, NPState} -> + handle_out({disconnect, ReasonCode}, NPState) + end; -session(#pstate{session = SPid}) -> - SPid. +handle_in(Packet = ?PUBLISH_PACKET(QoS, Topic, PacketId), PState) -> + case pipeline([fun validate_in/2, + fun process_alias/2, + fun check_publish/2], Packet, PState) of + {ok, NPacket, NPState} -> + process_publish(NPacket, NPState); + {error, ReasonCode, NPState} -> + ?LOG(warning, "Cannot publish message to ~s due to ~s", + [Topic, emqx_reason_codes:text(ReasonCode)]), + puback(QoS, PacketId, ReasonCode, NPState) + end; -%%------------------------------------------------------------------------------ -%% Packet Received -%%------------------------------------------------------------------------------ +handle_in(?PUBACK_PACKET(PacketId, _ReasonCode), PState = #protocol{session = Session}) -> + case emqx_session:puback(PacketId, Session) of + {ok, Publishes, NSession} -> + handle_out({publish, Publishes}, PState#protocol{session = NSession}); + {ok, NSession} -> + {ok, PState#protocol{session = NSession}}; + {error, _NotFound} -> + {ok, PState} + end; -set_protover(?CONNECT_PACKET(#mqtt_packet_connect{proto_ver = ProtoVer}), PState) -> - PState#pstate{proto_ver = ProtoVer}; -set_protover(_Packet, PState) -> - PState. +handle_in(?PUBREC_PACKET(PacketId, _ReasonCode), PState = #protocol{session = Session}) -> + case emqx_session:pubrec(PacketId, Session) of + {ok, NSession} -> + handle_out({pubrel, PacketId}, PState#protocol{session = NSession}); + {error, ReasonCode1} -> + handle_out({pubrel, PacketId, ReasonCode1}, PState) + end; --spec(received(emqx_mqtt_types:packet(), state()) - -> {ok, state()} - | {error, term()} - | {error, term(), state()} - | {stop, term(), state()}). -received(?PACKET(Type), PState = #pstate{connected = false}) when Type =/= ?CONNECT -> - {error, proto_not_connected, PState}; +handle_in(?PUBREL_PACKET(PacketId, _ReasonCode), PState = #protocol{session = Session}) -> + case emqx_session:pubrel(PacketId, Session) of + {ok, NSession} -> + handle_out({pubcomp, PacketId}, PState#protocol{session = NSession}); + {error, ReasonCode1} -> + handle_out({pubcomp, PacketId, ReasonCode1}, PState) + end; -received(?PACKET(?CONNECT), PState = #pstate{connected = true}) -> - {error, proto_unexpected_connect, PState}; +handle_in(?PUBCOMP_PACKET(PacketId, _ReasonCode), PState = #protocol{session = Session}) -> + case emqx_session:pubcomp(PacketId, Session) of + {ok, Publishes, NSession} -> + handle_out({publish, Publishes}, PState#protocol{session = NSession}); + {ok, NSession} -> + {ok, PState#protocol{session = NSession}}; + {error, _NotFound} -> + {ok, PState} + end; -received(Packet = ?PACKET(Type), PState) -> - trace(recv, Packet), - PState1 = set_protover(Packet, PState), - try emqx_packet:validate(Packet) of - true -> - case preprocess_properties(Packet, PState1) of - {ok, Packet1, PState2} -> - process(Packet1, inc_stats(recv, Type, PState2)); - {error, ReasonCode} -> - {error, ReasonCode, PState1} - end - catch - error:protocol_error -> - deliver({disconnect, ?RC_PROTOCOL_ERROR}, PState1), - {error, protocol_error, PState}; - error:subscription_identifier_invalid -> - deliver({disconnect, ?RC_SUBSCRIPTION_IDENTIFIERS_NOT_SUPPORTED}, PState1), - {error, subscription_identifier_invalid, PState1}; - error:topic_alias_invalid -> - deliver({disconnect, ?RC_TOPIC_ALIAS_INVALID}, PState1), - {error, topic_alias_invalid, PState1}; - error:topic_filters_invalid -> - deliver({disconnect, ?RC_TOPIC_FILTER_INVALID}, PState1), - {error, topic_filters_invalid, PState1}; - error:topic_name_invalid -> - deliver({disconnect, ?RC_TOPIC_FILTER_INVALID}, PState1), - {error, topic_filters_invalid, PState1}; - error:Reason -> - deliver({disconnect, ?RC_MALFORMED_PACKET}, PState1), - {error, Reason, PState1} +handle_in(Packet = ?SUBSCRIBE_PACKET(PacketId, Properties, TopicFilters), + PState = #protocol{client = Client}) -> + case validate_in(Packet, PState) of + ok -> TopicFilters1 = [emqx_topic:parse(TopicFilter, SubOpts) + || {TopicFilter, SubOpts} <- TopicFilters], + TopicFilters2 = emqx_hooks:run_fold('client.subscribe', + [Client, Properties], + TopicFilters1), + TopicFilters3 = enrich_subid(Properties, TopicFilters2), + {ReasonCodes, NPState} = process_subscribe(TopicFilters3, PState), + handle_out({suback, PacketId, ReasonCodes}, NPState); + {error, ReasonCode} -> + handle_out({disconnect, ReasonCode}, PState) + end; + +handle_in(Packet = ?UNSUBSCRIBE_PACKET(PacketId, Properties, TopicFilters), + PState = #protocol{client = Client}) -> + case validate_in(Packet, PState) of + ok -> TopicFilters1 = lists:map(fun emqx_topic:parse/1, TopicFilters), + TopicFilters2 = emqx_hooks:run_fold('client.unsubscribe', + [Client, Properties], + TopicFilters1), + {ReasonCodes, NPState} = process_unsubscribe(TopicFilters2, PState), + handle_out({unsuback, PacketId, ReasonCodes}, NPState); + {error, ReasonCode} -> + handle_out({disconnect, ReasonCode}, PState) + end; + +handle_in(?PACKET(?PINGREQ), PState) -> + {ok, ?PACKET(?PINGRESP), PState}; + +handle_in(?DISCONNECT_PACKET(?RC_SUCCESS), PState) -> + %% Clear will msg + {stop, normal, PState#protocol{will_msg = undefined}}; + +handle_in(?DISCONNECT_PACKET(RC), PState = #protocol{proto_ver = Ver}) -> + {stop, {shutdown, emqx_reason_codes:name(RC, Ver)}, PState}; + +handle_in(?AUTH_PACKET(), PState) -> + %%TODO: implement later. + {ok, PState}; + +handle_in(Packet, PState) -> + io:format("In: ~p~n", [Packet]), + {ok, PState}. + +%%-------------------------------------------------------------------- +%% Handle internal request +%%-------------------------------------------------------------------- + +-spec(handle_req(Req:: term(), proto_state()) + -> {ok, Result :: term(), proto_state()} | + {error, Reason :: term(), proto_state()}). +handle_req({subscribe, TopicFilters}, PState = #protocol{client = Client}) -> + TopicFilters1 = emqx_hooks:run_fold('client.subscribe', + [Client, #{'Internal' => true}], + parse(subscribe, TopicFilters)), + {ReasonCodes, NPState} = process_subscribe(TopicFilters1, PState), + {ok, ReasonCodes, NPState}; + +handle_req({unsubscribe, TopicFilters}, PState = #protocol{client = Client}) -> + TopicFilters1 = emqx_hooks:run_fold('client.unsubscribe', + [Client, #{'Internal' => true}], + parse(unsubscribe, TopicFilters)), + {ReasonCodes, NPState} = process_unsubscribe(TopicFilters1, PState), + {ok, ReasonCodes, NPState}; + +handle_req(Req, PState) -> + ?LOG(error, "Unexpected request: ~p~n", [Req]), + {ok, ignored, PState}. + +%%-------------------------------------------------------------------- +%% Handle delivers +%%-------------------------------------------------------------------- + +handle_deliver(Delivers, PState = #protocol{session = Session}) + when is_list(Delivers) -> + case emqx_session:deliver(Delivers, Session) of + {ok, Publishes, NSession} -> + handle_out({publish, Publishes}, PState#protocol{session = NSession}); + {ok, NSession} -> + {ok, PState#protocol{session = NSession}} end. -%%------------------------------------------------------------------------------ -%% Preprocess MQTT Properties -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- +%% Handle outgoing packet +%%-------------------------------------------------------------------- -preprocess_properties(Packet = #mqtt_packet{ - variable = #mqtt_packet_connect{ - properties = #{'Topic-Alias-Maximum' := ToClient} - } - }, - PState = #pstate{topic_alias_maximum = TopicAliasMaximum}) -> - {ok, Packet, PState#pstate{topic_alias_maximum = TopicAliasMaximum#{to_client => ToClient}}}; - -%% Subscription Identifier -preprocess_properties(Packet = #mqtt_packet{ - variable = Subscribe = #mqtt_packet_subscribe{ - properties = #{'Subscription-Identifier' := SubId}, - topic_filters = TopicFilters - } - }, - PState = #pstate{proto_ver = ?MQTT_PROTO_V5}) -> - TopicFilters1 = [{Topic, SubOpts#{subid => SubId}} || {Topic, SubOpts} <- TopicFilters], - {ok, Packet#mqtt_packet{variable = Subscribe#mqtt_packet_subscribe{topic_filters = TopicFilters1}}, PState}; - -%% Topic Alias Mapping -preprocess_properties(#mqtt_packet{ - variable = #mqtt_packet_publish{ - properties = #{'Topic-Alias' := 0}} - }, - PState) -> - deliver({disconnect, ?RC_TOPIC_ALIAS_INVALID}, PState), - {error, ?RC_TOPIC_ALIAS_INVALID}; - -preprocess_properties(Packet = #mqtt_packet{ - variable = Publish = #mqtt_packet_publish{ - topic_name = <<>>, - properties = #{'Topic-Alias' := AliasId}} - }, - PState = #pstate{proto_ver = ?MQTT_PROTO_V5, - topic_aliases = Aliases, - topic_alias_maximum = #{from_client := TopicAliasMaximum}}) -> - case AliasId =< TopicAliasMaximum of - true -> - {ok, Packet#mqtt_packet{variable = Publish#mqtt_packet_publish{ - topic_name = maps:get(AliasId, Aliases, <<>>)}}, PState}; - false -> - deliver({disconnect, ?RC_TOPIC_ALIAS_INVALID}, PState), - {error, ?RC_TOPIC_ALIAS_INVALID} - end; - -preprocess_properties(Packet = #mqtt_packet{ - variable = #mqtt_packet_publish{ - topic_name = Topic, - properties = #{'Topic-Alias' := AliasId}} - }, - PState = #pstate{proto_ver = ?MQTT_PROTO_V5, - topic_aliases = Aliases, - topic_alias_maximum = #{from_client := TopicAliasMaximum}}) -> - case AliasId =< TopicAliasMaximum of - true -> - {ok, Packet, PState#pstate{topic_aliases = maps:put(AliasId, Topic, Aliases)}}; - false -> - deliver({disconnect, ?RC_TOPIC_ALIAS_INVALID}, PState), - {error, ?RC_TOPIC_ALIAS_INVALID} - end; - -preprocess_properties(Packet, PState) -> - {ok, Packet, PState}. - -%%------------------------------------------------------------------------------ -%% Process MQTT Packet -%%------------------------------------------------------------------------------ -process(?CONNECT_PACKET( - #mqtt_packet_connect{proto_name = ProtoName, - proto_ver = ProtoVer, - is_bridge = IsBridge, - clean_start = CleanStart, - keepalive = Keepalive, - properties = ConnProps, - client_id = ClientId, - username = Username, - password = Password} = ConnPkt), PState) -> - - %% TODO: Mountpoint... - %% Msg -> emqx_mountpoint:mount(MountPoint, Msg) - PState0 = maybe_use_username_as_clientid(ClientId, - set_username(Username, - PState#pstate{proto_ver = ProtoVer, - proto_name = ProtoName, - clean_start = CleanStart, - keepalive = Keepalive, - conn_props = ConnProps, - is_bridge = IsBridge, - connected_at = os:timestamp()})), - - NewClientId = PState0#pstate.client_id, - - emqx_logger:set_metadata_client_id(NewClientId), - - Credentials = credentials(PState0), - PState1 = PState0#pstate{credentials = Credentials}, - connack( - case check_connect(ConnPkt, PState1) of - ok -> - case emqx_access_control:authenticate(Credentials#{password => Password}) of - {ok, Credentials0} -> - PState3 = maybe_assign_client_id(PState1), - emqx_logger:set_metadata_client_id(PState3#pstate.client_id), - %% Open session - SessAttrs = #{will_msg => make_will_msg(ConnPkt)}, - case try_open_session(SessAttrs, PState3) of - {ok, SPid, SP} -> - PState4 = PState3#pstate{session = SPid, connected = true, - credentials = keepsafety(Credentials0)}, - ok = emqx_cm:register_connection(client_id(PState4)), - true = emqx_cm:set_conn_attrs(client_id(PState4), attrs(PState4)), - %% Start keepalive - start_keepalive(Keepalive, PState4), - %% Success - {?RC_SUCCESS, SP, PState4}; - {error, Error} -> - ?LOG(error, "Failed to open session: ~p", [Error]), - {?RC_UNSPECIFIED_ERROR, PState1#pstate{credentials = Credentials0}} - end; - {error, Reason} -> - ?LOG(warning, "Client ~s (Username: '~s') login failed for ~p", [NewClientId, Username, Reason]), - {emqx_reason_codes:connack_error(Reason), PState1#pstate{credentials = Credentials}} - end; - {error, ReasonCode} -> - {ReasonCode, PState1} - end); - -process(Packet = ?PUBLISH_PACKET(?QOS_0, Topic, _PacketId, _Payload), PState = #pstate{zone = Zone}) -> - case check_publish(Packet, PState) of - ok -> - do_publish(Packet, PState); - {error, ReasonCode} -> - ?LOG(warning, "Cannot publish qos0 message to ~s for ~s", - [Topic, emqx_reason_codes:text(ReasonCode)]), - AclDenyAction = emqx_zone:get_env(Zone, acl_deny_action, ignore), - do_acl_deny_action(AclDenyAction, Packet, ReasonCode, PState) - end; - -process(Packet = ?PUBLISH_PACKET(?QOS_1, Topic, PacketId, _Payload), PState = #pstate{zone = Zone}) -> - case check_publish(Packet, PState) of - ok -> - do_publish(Packet, PState); - {error, ReasonCode} -> - ?LOG(warning, "Cannot publish qos1 message to ~s for ~s", [Topic, emqx_reason_codes:text(ReasonCode)]), - case deliver({puback, PacketId, ReasonCode}, PState) of - {ok, PState1} -> - AclDenyAction = emqx_zone:get_env(Zone, acl_deny_action, ignore), - do_acl_deny_action(AclDenyAction, Packet, ReasonCode, PState1); - Error -> Error - end - end; - -process(Packet = ?PUBLISH_PACKET(?QOS_2, Topic, PacketId, _Payload), PState = #pstate{zone = Zone}) -> - case check_publish(Packet, PState) of - ok -> - do_publish(Packet, PState); - {error, ReasonCode} -> - ?LOG(warning, "Cannot publish qos2 message to ~s for ~s", - [Topic, emqx_reason_codes:text(ReasonCode)]), - case deliver({pubrec, PacketId, ReasonCode}, PState) of - {ok, PState1} -> - AclDenyAction = emqx_zone:get_env(Zone, acl_deny_action, ignore), - do_acl_deny_action(AclDenyAction, Packet, ReasonCode, PState1); - Error -> Error - end - end; - -process(?PUBACK_PACKET(PacketId, ReasonCode), PState = #pstate{session = SPid}) -> - {ok = emqx_session:puback(SPid, PacketId, ReasonCode), PState}; - -process(?PUBREC_PACKET(PacketId, ReasonCode), PState = #pstate{session = SPid}) -> - case emqx_session:pubrec(SPid, PacketId, ReasonCode) of - ok -> - send(?PUBREL_PACKET(PacketId), PState); - {error, NotFound} -> - send(?PUBREL_PACKET(PacketId, NotFound), PState) - end; - -process(?PUBREL_PACKET(PacketId, ReasonCode), PState = #pstate{session = SPid}) -> - case emqx_session:pubrel(SPid, PacketId, ReasonCode) of - ok -> - send(?PUBCOMP_PACKET(PacketId), PState); - {error, NotFound} -> - send(?PUBCOMP_PACKET(PacketId, NotFound), PState) - end; - -process(?PUBCOMP_PACKET(PacketId, ReasonCode), PState = #pstate{session = SPid}) -> - {ok = emqx_session:pubcomp(SPid, PacketId, ReasonCode), PState}; - -process(Packet = ?SUBSCRIBE_PACKET(PacketId, Properties, RawTopicFilters), - PState = #pstate{zone = Zone, session = SPid, credentials = Credentials}) -> - case check_subscribe(parse_topic_filters(?SUBSCRIBE, raw_topic_filters(PState, RawTopicFilters)), PState) of - {ok, TopicFilters} -> - TopicFilters0 = emqx_hooks:run_fold('client.subscribe', [Credentials], TopicFilters), - TopicFilters1 = emqx_mountpoint:mount(mountpoint(Credentials), TopicFilters0), - ok = emqx_session:subscribe(SPid, PacketId, Properties, TopicFilters1), - {ok, PState}; - {error, TopicFilters} -> - {SubTopics, ReasonCodes} = - lists:foldr(fun({Topic, #{rc := ?RC_SUCCESS}}, {Topics, Codes}) -> - {[Topic|Topics], [?RC_IMPLEMENTATION_SPECIFIC_ERROR | Codes]}; - ({Topic, #{rc := Code}}, {Topics, Codes}) -> - {[Topic|Topics], [Code|Codes]} - end, {[], []}, TopicFilters), - ?LOG(warning, "Cannot subscribe ~p for ~p", - [SubTopics, [emqx_reason_codes:text(R) || R <- ReasonCodes]]), - case deliver({suback, PacketId, ReasonCodes}, PState) of - {ok, PState1} -> - AclDenyAction = emqx_zone:get_env(Zone, acl_deny_action, ignore), - do_acl_deny_action(AclDenyAction, Packet, ReasonCodes, PState1); - Error -> - Error - end - end; - -process(?UNSUBSCRIBE_PACKET(PacketId, Properties, RawTopicFilters), - PState = #pstate{session = SPid, credentials = Credentials}) -> - TopicFilters = emqx_hooks:run_fold('client.unsubscribe', [Credentials], - parse_topic_filters(?UNSUBSCRIBE, RawTopicFilters)), - ok = emqx_session:unsubscribe(SPid, PacketId, Properties, - emqx_mountpoint:mount(mountpoint(Credentials), TopicFilters)), - {ok, PState}; - -process(?PACKET(?PINGREQ), PState) -> - send(?PACKET(?PINGRESP), PState); - -process(?DISCONNECT_PACKET(?RC_SUCCESS, #{'Session-Expiry-Interval' := Interval}), - PState = #pstate{session = SPid, conn_props = #{'Session-Expiry-Interval' := OldInterval}}) -> - case Interval =/= 0 andalso OldInterval =:= 0 of - true -> - deliver({disconnect, ?RC_PROTOCOL_ERROR}, PState), - {error, protocol_error, PState#pstate{will_msg = undefined}}; - false -> - emqx_session:update_expiry_interval(SPid, Interval), - %% Clean willmsg - {stop, normal, PState#pstate{will_msg = undefined}} - end; - -process(?DISCONNECT_PACKET(?RC_SUCCESS), PState) -> - {stop, normal, PState#pstate{will_msg = undefined}}; - -process(?DISCONNECT_PACKET(_), PState) -> - {stop, {shutdown, abnormal_disconnet}, PState}. - -%%------------------------------------------------------------------------------ -%% ConnAck --> Client -%%------------------------------------------------------------------------------ - -connack({?RC_SUCCESS, SP, PState = #pstate{credentials = Credentials}}) -> - ok = emqx_hooks:run('client.connected', [Credentials, ?RC_SUCCESS, attrs(PState)]), - deliver({connack, ?RC_SUCCESS, sp(SP)}, PState); - -connack({ReasonCode, PState = #pstate{proto_ver = ProtoVer, credentials = Credentials}}) -> - ok = emqx_hooks:run('client.connected', [Credentials, ReasonCode, attrs(PState)]), - [ReasonCode1] = reason_codes_compat(connack, [ReasonCode], ProtoVer), - _ = deliver({connack, ReasonCode1}, PState), - {error, emqx_reason_codes:name(ReasonCode1, ProtoVer), PState}. - -%%------------------------------------------------------------------------------ -%% Publish Message -> Broker -%%------------------------------------------------------------------------------ - -do_publish(Packet = ?PUBLISH_PACKET(QoS, PacketId), - PState = #pstate{session = SPid, credentials = Credentials}) -> - Msg = emqx_mountpoint:mount(mountpoint(Credentials), - emqx_packet:to_message(Credentials, Packet)), - puback(QoS, PacketId, emqx_session:publish(SPid, PacketId, emqx_message:set_flag(dup, false, Msg)), PState). - -%%------------------------------------------------------------------------------ -%% Puback -> Client -%%------------------------------------------------------------------------------ - -puback(?QOS_0, _PacketId, _Result, PState) -> - {ok, PState}; -puback(?QOS_1, PacketId, {ok, []}, PState) -> - deliver({puback, PacketId, ?RC_NO_MATCHING_SUBSCRIBERS}, PState); -%%TODO: calc the deliver count? -puback(?QOS_1, PacketId, {ok, _Result}, PState) -> - deliver({puback, PacketId, ?RC_SUCCESS}, PState); -puback(?QOS_1, PacketId, {error, ReasonCode}, PState) -> - deliver({puback, PacketId, ReasonCode}, PState); -puback(?QOS_2, PacketId, {ok, []}, PState) -> - deliver({pubrec, PacketId, ?RC_NO_MATCHING_SUBSCRIBERS}, PState); -puback(?QOS_2, PacketId, {ok, _Result}, PState) -> - deliver({pubrec, PacketId, ?RC_SUCCESS}, PState); -puback(?QOS_2, PacketId, {error, ReasonCode}, PState) -> - deliver({pubrec, PacketId, ReasonCode}, PState). - -%%------------------------------------------------------------------------------ -%% Deliver Packet -> Client -%%------------------------------------------------------------------------------ - --spec(deliver(list(tuple()) | tuple(), state()) -> {ok, state()} | {error, term()}). -deliver([], PState) -> - {ok, PState}; -deliver([Pub|More], PState) -> - case deliver(Pub, PState) of - {ok, PState1} -> - deliver(More, PState1); - {error, _} = Error -> - Error - end; - -deliver({connack, ReasonCode}, PState) -> - send(?CONNACK_PACKET(ReasonCode), PState); - -deliver({connack, ?RC_SUCCESS, SP}, PState = #pstate{zone = Zone, - proto_ver = ?MQTT_PROTO_V5, - client_id = ClientId, - is_assigned = IsAssigned, - topic_alias_maximum = TopicAliasMaximum}) -> +handle_out({connack, ?RC_SUCCESS, SP}, + PState = #protocol{client = Client = #{zone := Zone}, + ack_props = AckProps, + alias_maximum = AliasMaximum}) -> + ok = emqx_hooks:run('client.connected', [Client, ?RC_SUCCESS, attrs(PState)]), #{max_packet_size := MaxPktSize, max_qos_allowed := MaxQoS, - mqtt_retain_available := Retain, + retain_available := Retain, max_topic_alias := MaxAlias, - mqtt_shared_subscription := Shared, - mqtt_wildcard_subscription := Wildcard} = caps(PState), + shared_subscription := Shared, + wildcard_subscription := Wildcard + } = caps(PState), %% Response-Information is so far not set by broker. %% i.e. It's a Client-to-Client contract for the request-response topic naming scheme. %% According to MQTT 5.0 spec: @@ -627,160 +340,170 @@ deliver({connack, ?RC_SUCCESS, SP}, PState = #pstate{zone = Zone, %% to allow prefixing the response topic based on different ACL config. %% e.g. prefix by username or client-id, so that unauthorized clients can not %% subscribe requests or responses that are not intended for them. - Props = #{'Retain-Available' => flag(Retain), - 'Maximum-Packet-Size' => MaxPktSize, - 'Topic-Alias-Maximum' => MaxAlias, - 'Wildcard-Subscription-Available' => flag(Wildcard), - 'Subscription-Identifier-Available' => 1, - %'Response-Information' => - 'Shared-Subscription-Available' => flag(Shared)}, + AckProps1 = if AckProps == undefined -> #{}; true -> AckProps end, + AckProps2 = AckProps1#{'Retain-Available' => flag(Retain), + 'Maximum-Packet-Size' => MaxPktSize, + 'Topic-Alias-Maximum' => MaxAlias, + 'Wildcard-Subscription-Available' => flag(Wildcard), + 'Subscription-Identifier-Available' => 1, + %'Response-Information' => + 'Shared-Subscription-Available' => flag(Shared), + 'Maximum-QoS' => MaxQoS + }, + AckProps3 = case emqx_zone:get_env(Zone, server_keepalive) of + undefined -> AckProps2; + Keepalive -> AckProps2#{'Server-Keep-Alive' => Keepalive} + end, + AliasMaximum1 = set_property(inbound, MaxAlias, AliasMaximum), + PState1 = PState#protocol{alias_maximum = AliasMaximum1, + ack_props = undefined + }, + {ok, ?CONNACK_PACKET(?RC_SUCCESS, SP, AckProps3), PState1}; - Props1 = if - MaxQoS =:= ?QOS_2 -> - Props; - true -> - maps:put('Maximum-QoS', MaxQoS, Props) - end, +handle_out({connack, ReasonCode}, PState = #protocol{client = Client, + proto_ver = ProtoVer}) -> + ok = emqx_hooks:run('client.connected', [Client, ReasonCode, attrs(PState)]), + ReasonCode1 = if + ProtoVer == ?MQTT_PROTO_V5 -> ReasonCode; + true -> emqx_reason_codes:compat(connack, ReasonCode) + end, + Reason = emqx_reason_codes:name(ReasonCode1, ProtoVer), + {error, Reason, ?CONNACK_PACKET(ReasonCode1), PState}; - Props2 = if IsAssigned -> - Props1#{'Assigned-Client-Identifier' => ClientId}; - true -> Props1 +handle_out({publish, Publishes}, PState) -> + Packets = [element(2, handle_out(Publish, PState)) || Publish <- Publishes], + {ok, Packets, PState}; - end, +handle_out({publish, PacketId, Msg}, PState = #protocol{client = Client}) -> + Msg1 = emqx_hooks:run_fold('message.deliver', [Client], + emqx_message:update_expiry(Msg)), + Packet = emqx_packet:from_message(PacketId, unmount(Client, Msg1)), + {ok, Packet, PState}; - Props3 = case emqx_zone:get_env(Zone, server_keepalive) of - undefined -> Props2; - Keepalive -> Props2#{'Server-Keep-Alive' => Keepalive} - end, +%% TODO: How to handle the err? +handle_out({puberr, _ReasonCode}, PState) -> + {ok, PState}; - PState1 = PState#pstate{topic_alias_maximum = TopicAliasMaximum#{from_client => MaxAlias}}, +handle_out({puback, PacketId, ReasonCode}, PState) -> + {ok, ?PUBACK_PACKET(PacketId, ReasonCode), PState}; - send(?CONNACK_PACKET(?RC_SUCCESS, SP, Props3), PState1); +handle_out({pubrel, PacketId}, PState) -> + {ok, ?PUBREL_PACKET(PacketId), PState}; +handle_out({pubrel, PacketId, ReasonCode}, PState) -> + {ok, ?PUBREL_PACKET(PacketId, ReasonCode), PState}; -deliver({connack, ReasonCode, SP}, PState) -> - send(?CONNACK_PACKET(ReasonCode, SP), PState); +handle_out({pubrec, PacketId, ReasonCode}, PState) -> + {ok, ?PUBREC_PACKET(PacketId, ReasonCode), PState}; -deliver({publish, PacketId, Msg}, PState = #pstate{credentials = Credentials}) -> - Msg0 = emqx_hooks:run_fold('message.deliver', [Credentials], Msg), - Msg1 = emqx_message:update_expiry(Msg0), - Msg2 = emqx_mountpoint:unmount(mountpoint(Credentials), Msg1), - send(emqx_packet:from_message(PacketId, Msg2), PState); +handle_out({pubcomp, PacketId}, PState) -> + {ok, ?PUBCOMP_PACKET(PacketId), PState}; +handle_out({pubcomp, PacketId, ReasonCode}, PState) -> + {ok, ?PUBCOMP_PACKET(PacketId, ReasonCode), PState}; -deliver({puback, PacketId, ReasonCode}, PState) -> - send(?PUBACK_PACKET(PacketId, ReasonCode), PState); +handle_out({suback, PacketId, ReasonCodes}, PState = #protocol{proto_ver = ?MQTT_PROTO_V5}) -> + %% TODO: ACL Deny + {ok, ?SUBACK_PACKET(PacketId, ReasonCodes), PState}; +handle_out({suback, PacketId, ReasonCodes}, PState) -> + %% TODO: ACL Deny + ReasonCodes1 = [emqx_reason_codes:compat(suback, RC) || RC <- ReasonCodes], + {ok, ?SUBACK_PACKET(PacketId, ReasonCodes1), PState}; -deliver({pubrel, PacketId}, PState) -> - send(?PUBREL_PACKET(PacketId), PState); +handle_out({unsuback, PacketId, ReasonCodes}, PState = #protocol{proto_ver = ?MQTT_PROTO_V5}) -> + {ok, ?UNSUBACK_PACKET(PacketId, ReasonCodes), PState}; +%% Ignore reason codes if not MQTT5 +handle_out({unsuback, PacketId, _ReasonCodes}, PState) -> + {ok, ?UNSUBACK_PACKET(PacketId), PState}; -deliver({pubrec, PacketId, ReasonCode}, PState) -> - send(?PUBREC_PACKET(PacketId, ReasonCode), PState); +handle_out({disconnect, ReasonCode}, PState = #protocol{proto_ver = ?MQTT_PROTO_V5}) -> + Reason = emqx_reason_codes:name(ReasonCode), + {error, Reason, ?DISCONNECT_PACKET(ReasonCode), PState}; -deliver({suback, PacketId, ReasonCodes}, PState = #pstate{proto_ver = ProtoVer}) -> - send(?SUBACK_PACKET(PacketId, reason_codes_compat(suback, ReasonCodes, ProtoVer)), PState); +handle_out({disconnect, ReasonCode}, PState = #protocol{proto_ver = ProtoVer}) -> + {error, emqx_reason_codes:name(ReasonCode, ProtoVer), PState}; -deliver({unsuback, PacketId, ReasonCodes}, PState = #pstate{proto_ver = ProtoVer}) -> - send(?UNSUBACK_PACKET(PacketId, reason_codes_compat(unsuback, ReasonCodes, ProtoVer)), PState); - -%% Deliver a disconnect for mqtt 5.0 -deliver({disconnect, ReasonCode}, PState = #pstate{proto_ver = ?MQTT_PROTO_V5}) -> - send(?DISCONNECT_PACKET(ReasonCode), PState); - -deliver({disconnect, _ReasonCode}, PState) -> +handle_out(Packet, PState) -> + ?LOG(error, "Unexpected out:~p", [Packet]), {ok, PState}. -%%------------------------------------------------------------------------------ -%% Send Packet to Client +%%-------------------------------------------------------------------- +%% Handle timeout +%%-------------------------------------------------------------------- --spec(send(emqx_mqtt_types:packet(), state()) -> {ok, state()} | {error, term()}). -send(Packet = ?PACKET(Type), PState = #pstate{proto_ver = Ver, sendfun = Send}) -> - case Send(Packet, #{version => Ver}) of - ok -> - trace(send, Packet), - {ok, PState}; - {ok, Data} -> - trace(send, Packet), - emqx_metrics:inc_sent(Packet), - ok = emqx_metrics:inc('bytes.sent', iolist_size(Data)), - {ok, inc_stats(send, Type, PState)}; - {error, Reason} -> - {error, Reason} +handle_timeout(TRef, Msg, PState = #protocol{session = Session}) -> + case emqx_session:timeout(TRef, Msg, Session) of + {ok, NSession} -> + {ok, PState#protocol{session = NSession}}; + {ok, Publishes, NSession} -> + handle_out({publish, Publishes}, PState#protocol{session = NSession}) end. -%%------------------------------------------------------------------------------ -%% Maybe use username replace client id +terminate(normal, #protocol{client = Client}) -> + ok = emqx_hooks:run('client.disconnected', [Client, normal]); +terminate(Reason, #protocol{client = Client, will_msg = WillMsg}) -> + ok = emqx_hooks:run('client.disconnected', [Client, Reason]), + publish_will_msg(WillMsg). -maybe_use_username_as_clientid(ClientId, PState = #pstate{username = undefined}) -> - PState#pstate{client_id = ClientId}; -maybe_use_username_as_clientid(ClientId, PState = #pstate{username = Username, zone = Zone}) -> - case emqx_zone:get_env(Zone, use_username_as_clientid, false) of - true -> PState#pstate{client_id = Username}; - false -> PState#pstate{client_id = ClientId} +publish_will_msg(undefined) -> + ok; +publish_will_msg(Msg) -> + emqx_broker:publish(Msg). + +%%-------------------------------------------------------------------- +%% Validate incoming packet +%%-------------------------------------------------------------------- + +-spec(validate_in(emqx_types:packet(), proto_state()) + -> ok | {error, emqx_types:reason_code()}). +validate_in(Packet, _PState) -> + try emqx_packet:validate(Packet) of + true -> ok + catch + error:protocol_error -> + {error, ?RC_PROTOCOL_ERROR}; + error:subscription_identifier_invalid -> + {error, ?RC_SUBSCRIPTION_IDENTIFIERS_NOT_SUPPORTED}; + error:topic_alias_invalid -> + {error, ?RC_TOPIC_ALIAS_INVALID}; + error:topic_filters_invalid -> + {error, ?RC_TOPIC_FILTER_INVALID}; + error:topic_name_invalid -> + {error, ?RC_TOPIC_FILTER_INVALID}; + error:_Reason -> + {error, ?RC_MALFORMED_PACKET} end. -%%------------------------------------------------------------------------------ -%% Assign a clientId +%%-------------------------------------------------------------------- +%% Preprocess properties +%%-------------------------------------------------------------------- -maybe_assign_client_id(PState = #pstate{client_id = <<>>, ack_props = AckProps}) -> - ClientId = emqx_guid:to_base62(emqx_guid:gen()), - AckProps1 = set_property('Assigned-Client-Identifier', ClientId, AckProps), - PState#pstate{client_id = ClientId, is_assigned = true, ack_props = AckProps1}; -maybe_assign_client_id(PState) -> - PState. +process_props(#mqtt_packet_connect{ + properties = #{'Topic-Alias-Maximum' := Max} + }, + PState = #protocol{alias_maximum = AliasMaximum}) -> + NAliasMaximum = if AliasMaximum == undefined -> + #{outbound => Max}; + true -> AliasMaximum#{outbound => Max} + end, + {ok, PState#protocol{alias_maximum = NAliasMaximum}}; -try_open_session(SessAttrs, PState = #pstate{zone = Zone, - client_id = ClientId, - conn_pid = ConnPid, - username = Username, - clean_start = CleanStart}) -> - case emqx_sm:open_session( - maps:merge(#{zone => Zone, - client_id => ClientId, - conn_pid => ConnPid, - username => Username, - clean_start => CleanStart, - max_inflight => attr(max_inflight, PState), - expiry_interval => attr(expiry_interval, PState), - topic_alias_maximum => attr(topic_alias_maximum, PState)}, - SessAttrs)) of - {ok, SPid} -> - {ok, SPid, false}; - Other -> Other +process_props(Packet, PState) -> + {ok, Packet, PState}. + +%%-------------------------------------------------------------------- +%% Check Connect Packet +%%-------------------------------------------------------------------- + +check_connect(ConnPkt, PState) -> + case pipeline([fun check_proto_ver/2, + fun check_client_id/2, + %%fun check_flapping/2, + fun check_banned/2, + fun check_will_topic/2, + fun check_will_retain/2], ConnPkt, PState) of + ok -> {ok, PState}; + Error -> Error end. -set_property(Name, Value, ?NO_PROPS) -> - #{Name => Value}; -set_property(Name, Value, Props) -> - Props#{Name => Value}. - -get_property(_Name, undefined, Default) -> - Default; -get_property(Name, Props, Default) -> - maps:get(Name, Props, Default). - -make_will_msg(#mqtt_packet_connect{proto_ver = ProtoVer, - will_props = WillProps} = ConnPkt) -> - emqx_packet:will_msg( - case ProtoVer of - ?MQTT_PROTO_V5 -> - WillDelayInterval = get_property('Will-Delay-Interval', WillProps, 0), - ConnPkt#mqtt_packet_connect{ - will_props = set_property('Will-Delay-Interval', WillDelayInterval, WillProps)}; - _ -> - ConnPkt - end). - -%%------------------------------------------------------------------------------ -%% Check Packet -%%------------------------------------------------------------------------------ - -check_connect(Packet, PState) -> - run_check_steps([fun check_proto_ver/2, - fun check_client_id/2, - fun check_flapping/2, - fun check_banned/2, - fun check_will_topic/2, - fun check_will_retain/2], Packet, PState). - check_proto_ver(#mqtt_packet_connect{proto_ver = Ver, proto_name = Name}, _PState) -> case lists:member({Ver, Name}, ?PROTOCOL_NAMES) of @@ -790,7 +513,8 @@ check_proto_ver(#mqtt_packet_connect{proto_ver = Ver, %% MQTT3.1 does not allow null clientId check_client_id(#mqtt_packet_connect{proto_ver = ?MQTT_PROTO_V3, - client_id = <<>>}, _PState) -> + client_id = <<>> + }, _PState) -> {error, ?RC_CLIENT_IDENTIFIER_NOT_VALID}; %% Issue#599: Null clientId and clean_start = false @@ -802,7 +526,8 @@ check_client_id(#mqtt_packet_connect{client_id = <<>>, clean_start = true}, _PState) -> ok; -check_client_id(#mqtt_packet_connect{client_id = ClientId}, #pstate{zone = Zone}) -> +check_client_id(#mqtt_packet_connect{client_id = ClientId}, + #protocol{client = #{zone := Zone}}) -> Len = byte_size(ClientId), MaxLen = emqx_zone:get_env(Zone, max_clientid_len), case (1 =< Len) andalso (Len =< MaxLen) of @@ -810,241 +535,390 @@ check_client_id(#mqtt_packet_connect{client_id = ClientId}, #pstate{zone = Zone} false -> {error, ?RC_CLIENT_IDENTIFIER_NOT_VALID} end. -check_flapping(#mqtt_packet_connect{}, PState) -> - do_flapping_detect(connect, PState). - -check_banned(#mqtt_packet_connect{client_id = ClientId, username = Username}, - #pstate{zone = Zone, peername = Peername}) -> - Credentials = #{client_id => ClientId, - username => Username, - peername => Peername}, - EnableBan = emqx_zone:get_env(Zone, enable_ban, false), - do_check_banned(EnableBan, Credentials). +%%TODO: check banned... +check_banned(#mqtt_packet_connect{client_id = ClientId, + username = Username}, + #protocol{client = Client = #{zone := Zone}}) -> + case emqx_zone:get_env(Zone, enable_ban, false) of + true -> + case emqx_banned:check(Client#{client_id => ClientId, + username => Username}) of + true -> {error, ?RC_BANNED}; + false -> ok + end; + false -> ok + end. check_will_topic(#mqtt_packet_connect{will_flag = false}, _PState) -> ok; -check_will_topic(#mqtt_packet_connect{will_topic = WillTopic} = ConnPkt, PState) -> +check_will_topic(#mqtt_packet_connect{will_topic = WillTopic}, _PState) -> try emqx_topic:validate(WillTopic) of - true -> check_will_acl(ConnPkt, PState) - catch error : _Error -> - {error, ?RC_TOPIC_NAME_INVALID} + true -> ok + catch error:_Error -> + {error, ?RC_TOPIC_NAME_INVALID} end. -check_will_retain(#mqtt_packet_connect{will_retain = false, proto_ver = ?MQTT_PROTO_V5}, _PState) -> +check_will_retain(#mqtt_packet_connect{will_retain = false}, _PState) -> ok; -check_will_retain(#mqtt_packet_connect{will_retain = true, proto_ver = ?MQTT_PROTO_V5}, #pstate{zone = Zone}) -> +check_will_retain(#mqtt_packet_connect{will_retain = true}, + #protocol{client = #{zone := Zone}}) -> case emqx_zone:get_env(Zone, mqtt_retain_available, true) of - true -> {error, ?RC_RETAIN_NOT_SUPPORTED}; - false -> ok - end; -check_will_retain(_Packet, _PState) -> - ok. - -check_will_acl(#mqtt_packet_connect{will_topic = WillTopic}, - #pstate{zone = Zone, credentials = Credentials}) -> - EnableAcl = emqx_zone:get_env(Zone, enable_acl, false), - case do_acl_check(EnableAcl, publish, Credentials, WillTopic) of - ok -> ok; - Other -> - ?LOG(warning, "Cannot publish will message to ~p for acl denied", [WillTopic]), - Other + true -> ok; + false -> {error, ?RC_RETAIN_NOT_SUPPORTED} end. -check_publish(Packet, PState) -> - run_check_steps([fun check_pub_caps/2, - fun check_pub_acl/2], Packet, PState). +%%-------------------------------------------------------------------- +%% Enrich client +%%-------------------------------------------------------------------- -check_pub_caps(#mqtt_packet{header = #mqtt_packet_header{qos = QoS, retain = Retain}, - variable = #mqtt_packet_publish{properties = _Properties}}, - #pstate{zone = Zone}) -> +enrich_client(#mqtt_packet_connect{client_id = ClientId, + username = Username, + is_bridge = IsBridge + }, + PState = #protocol{client = Client}) -> + Client1 = set_username(Username, Client#{client_id => ClientId, + is_bridge => IsBridge + }), + {ok, PState#protocol{client = maybe_username_as_clientid(Client1)}}. + +%% Username maybe not undefined if peer_cert_as_username +set_username(Username, Client = #{username := undefined}) -> + Client#{username => Username}; +set_username(_Username, Client) -> Client. + +maybe_username_as_clientid(Client = #{username := undefined}) -> + Client; +maybe_username_as_clientid(Client = #{zone := Zone, + username := Username}) -> + case emqx_zone:get_env(Zone, use_username_as_clientid, false) of + true -> Client#{client_id => Username}; + false -> Client + end. + +%%-------------------------------------------------------------------- +%% Auth Connect +%%-------------------------------------------------------------------- + +auth_connect(#mqtt_packet_connect{client_id = ClientId, + username = Username, + password = Password}, + PState = #protocol{client = Client}) -> + case authenticate(Client#{password => Password}) of + {ok, AuthResult} -> + {ok, PState#protocol{client = maps:merge(Client, AuthResult)}}; + {error, Reason} -> + ?LOG(warning, "Client ~s (Username: '~s') login failed for ~0p", + [ClientId, Username, Reason]), + {error, emqx_reason_codes:connack_error(Reason)} + end. + +%%-------------------------------------------------------------------- +%% Assign a random clientId +%%-------------------------------------------------------------------- + +maybe_assign_clientid(PState = #protocol{client = Client = #{client_id := <<>>}, + ack_props = AckProps}) -> + ClientId = emqx_guid:to_base62(emqx_guid:gen()), + Client1 = Client#{client_id => ClientId}, + AckProps1 = set_property('Assigned-Client-Identifier', ClientId, AckProps), + PState#protocol{client = Client1, ack_props = AckProps1}; +maybe_assign_clientid(PState) -> PState. + +%%-------------------------------------------------------------------- +%% Process Connect +%%-------------------------------------------------------------------- + +process_connect(ConnPkt, PState) -> + case open_session(ConnPkt, PState) of + {ok, Session, SP} -> + WillMsg = emqx_packet:will_msg(ConnPkt), + NPState = PState#protocol{session = Session, + will_msg = WillMsg + }, + handle_out({connack, ?RC_SUCCESS, sp(SP)}, NPState); + {error, Reason} -> + %% TODO: Unknown error? + ?LOG(error, "Failed to open session: ~p", [Reason]), + handle_out({connack, ?RC_UNSPECIFIED_ERROR}, PState) + end. + +%%-------------------------------------------------------------------- +%% Open session +%%-------------------------------------------------------------------- + +open_session(#mqtt_packet_connect{clean_start = CleanStart, + properties = ConnProps}, + #protocol{client = Client = #{zone := Zone}}) -> + MaxInflight = get_property('Receive-Maximum', ConnProps, + emqx_zone:get_env(Zone, max_inflight, 65535)), + Interval = get_property('Session-Expiry-Interval', ConnProps, + emqx_zone:get_env(Zone, session_expiry_interval, 0)), + emqx_cm:open_session(CleanStart, Client, #{max_inflight => MaxInflight, + expiry_interval => Interval + }). + +%%-------------------------------------------------------------------- +%% Process publish message: Client -> Broker +%%-------------------------------------------------------------------- + +process_alias(Packet = #mqtt_packet{ + variable = #mqtt_packet_publish{topic_name = <<>>, + properties = #{'Topic-Alias' := AliasId} + } = Publish + }, PState = #protocol{topic_aliases = Aliases}) -> + case find_alias(AliasId, Aliases) of + {ok, Topic} -> + {ok, Packet#mqtt_packet{ + variable = Publish#mqtt_packet_publish{ + topic_name = Topic}}, PState}; + false -> {error, ?RC_TOPIC_ALIAS_INVALID} + end; + +process_alias(#mqtt_packet{ + variable = #mqtt_packet_publish{topic_name = Topic, + properties = #{'Topic-Alias' := AliasId} + } + }, PState = #protocol{topic_aliases = Aliases}) -> + {ok, PState#protocol{topic_aliases = save_alias(AliasId, Topic, Aliases)}}; + +process_alias(_Packet, PState) -> + {ok, PState}. + +find_alias(_AliasId, undefined) -> + false; +find_alias(AliasId, Aliases) -> + maps:find(AliasId, Aliases). + +save_alias(AliasId, Topic, undefined) -> + #{AliasId => Topic}; +save_alias(AliasId, Topic, Aliases) -> + maps:put(AliasId, Topic, Aliases). + +%% Check Publish +check_publish(Packet, PState) -> + pipeline([fun check_pub_acl/2, + fun check_pub_alias/2, + fun check_pub_caps/2], Packet, PState). + +%% Check Pub ACL +check_pub_acl(#mqtt_packet{variable = #mqtt_packet_publish{topic_name = Topic}}, + #protocol{client = Client}) -> + case is_acl_enabled(Client) andalso check_acl(Client, publish, Topic) of + false -> ok; + allow -> ok; + deny -> {error, ?RC_NOT_AUTHORIZED} + end. + +%% Check Pub Alias +check_pub_alias(#mqtt_packet{ + variable = #mqtt_packet_publish{ + properties = #{'Topic-Alias' := AliasId} + } + }, + #protocol{alias_maximum = Limits}) -> + case (Limits == undefined) + orelse (Max = maps:get(inbound, Limits, 0)) == 0 + orelse (AliasId > Max) of + false -> ok; + true -> {error, ?RC_TOPIC_ALIAS_INVALID} + end; +check_pub_alias(_Packet, _PState) -> ok. + +%% Check Pub Caps +check_pub_caps(#mqtt_packet{header = #mqtt_packet_header{qos = QoS, + retain = Retain + } + }, + #protocol{client = #{zone := Zone}}) -> emqx_mqtt_caps:check_pub(Zone, #{qos => QoS, retain => Retain}). -check_pub_acl(_Packet, #pstate{credentials = #{is_superuser := IsSuper}}) - when IsSuper -> - ok; -check_pub_acl(#mqtt_packet{variable = #mqtt_packet_publish{topic_name = Topic}}, - #pstate{zone = Zone, credentials = Credentials}) -> - EnableAcl = emqx_zone:get_env(Zone, enable_acl, false), - do_acl_check(EnableAcl, publish, Credentials, Topic). +%% Process Publish +process_publish(Packet = ?PUBLISH_PACKET(_QoS, _Topic, PacketId), + PState = #protocol{client = Client}) -> + Msg = emqx_packet:to_message(Client, Packet), + %%TODO: Improve later. + Msg1 = emqx_message:set_flag(dup, false, Msg), + process_publish(PacketId, mount(Client, Msg1), PState). -run_check_steps([], _Packet, _PState) -> - ok; -run_check_steps([Check|Steps], Packet, PState) -> - case Check(Packet, PState) of - ok -> - run_check_steps(Steps, Packet, PState); - Error = {error, _RC} -> - Error +process_publish(_PacketId, Msg = #message{qos = ?QOS_0}, PState) -> + _ = emqx_broker:publish(Msg), + {ok, PState}; + +process_publish(PacketId, Msg = #message{qos = ?QOS_1}, PState) -> + Deliveries = emqx_broker:publish(Msg), + ReasonCode = emqx_reason_codes:puback(Deliveries), + handle_out({puback, PacketId, ReasonCode}, PState); + +process_publish(PacketId, Msg = #message{qos = ?QOS_2}, + PState = #protocol{session = Session}) -> + case emqx_session:publish(PacketId, Msg, Session) of + {ok, Deliveries, NSession} -> + ReasonCode = emqx_reason_codes:puback(Deliveries), + handle_out({pubrec, PacketId, ReasonCode}, + PState#protocol{session = NSession}); + {error, ReasonCode} -> + handle_out({pubrec, PacketId, ReasonCode}, PState) end. -check_subscribe(TopicFilters, PState = #pstate{zone = Zone}) -> - case emqx_mqtt_caps:check_sub(Zone, TopicFilters) of - {ok, TopicFilter1} -> - check_sub_acl(TopicFilter1, PState); - {error, TopicFilter1} -> - {error, TopicFilter1} +%%-------------------------------------------------------------------- +%% Puback +%%-------------------------------------------------------------------- + +puback(?QOS_0, _PacketId, ReasonCode, PState) -> + handle_out({puberr, ReasonCode}, PState); +puback(?QOS_1, PacketId, ReasonCode, PState) -> + handle_out({puback, PacketId, ReasonCode}, PState); +puback(?QOS_2, PacketId, ReasonCode, PState) -> + handle_out({pubrec, PacketId, ReasonCode}, PState). + +%%-------------------------------------------------------------------- +%% Process subscribe request +%%-------------------------------------------------------------------- + +process_subscribe(TopicFilters, PState) -> + process_subscribe(TopicFilters, [], PState). + +process_subscribe([], Acc, PState) -> + {lists:reverse(Acc), PState}; + +process_subscribe([{TopicFilter, SubOpts}|More], Acc, PState) -> + {RC, NPState} = do_subscribe(TopicFilter, SubOpts, PState), + process_subscribe(More, [RC|Acc], NPState). + +do_subscribe(TopicFilter, SubOpts = #{qos := QoS}, + PState = #protocol{client = Client, session = Session}) -> + case check_subscribe(TopicFilter, SubOpts, PState) of + ok -> TopicFilter1 = mount(Client, TopicFilter), + SubOpts1 = enrich_subopts(maps:merge(?DEFAULT_SUBOPTS, SubOpts), PState), + case emqx_session:subscribe(Client, TopicFilter1, SubOpts1, Session) of + {ok, NSession} -> + {QoS, PState#protocol{session = NSession}}; + {error, RC} -> {RC, PState} + end; + {error, RC} -> {RC, PState} end. -check_sub_acl(TopicFilters, #pstate{credentials = #{is_superuser := IsSuper}}) - when IsSuper -> - {ok, TopicFilters}; -check_sub_acl(TopicFilters, #pstate{zone = Zone, credentials = Credentials}) -> - EnableAcl = emqx_zone:get_env(Zone, enable_acl, false), - lists:foldr( - fun({Topic, SubOpts}, {ok, Acc}) when EnableAcl -> - AllowTerm = {ok, [{Topic, SubOpts}|Acc]}, - DenyTerm = {error, [{Topic, SubOpts#{rc := ?RC_NOT_AUTHORIZED}}|Acc]}, - do_acl_check(subscribe, Credentials, Topic, AllowTerm, DenyTerm); - (TopicFilter, {ok, Acc}) -> - {ok, [TopicFilter | Acc]} - end, {ok, []}, TopicFilters). +enrich_subid(#{'Subscription-Identifier' := SubId}, TopicFilters) -> + [{Topic, SubOpts#{subid => SubId}} || {Topic, SubOpts} <- TopicFilters]; +enrich_subid(_Properties, TopicFilters) -> + TopicFilters. -trace(recv, Packet) -> - ?LOG(debug, "RECV ~s", [emqx_packet:format(Packet)]); -trace(send, Packet) -> - ?LOG(debug, "SEND ~s", [emqx_packet:format(Packet)]). +enrich_subopts(SubOpts, #protocol{proto_ver = ?MQTT_PROTO_V5}) -> + SubOpts; +enrich_subopts(SubOpts, #protocol{client = #{zone := Zone, is_bridge := IsBridge}}) -> + Rap = flag(IsBridge), + Nl = flag(emqx_zone:get_env(Zone, ignore_loop_deliver, false)), + SubOpts#{rap => Rap, nl => Nl}. -inc_stats(recv, Type, PState = #pstate{recv_stats = Stats}) -> - PState#pstate{recv_stats = inc_stats(Type, Stats)}; +%% Check Sub +check_subscribe(TopicFilter, SubOpts, PState) -> + case check_sub_acl(TopicFilter, PState) of + allow -> check_sub_caps(TopicFilter, SubOpts, PState); + deny -> {error, ?RC_NOT_AUTHORIZED} + end. -inc_stats(send, Type, PState = #pstate{send_stats = Stats}) -> - PState#pstate{send_stats = inc_stats(Type, Stats)}. +%% Check Sub ACL +check_sub_acl(TopicFilter, #protocol{client = Client}) -> + case is_acl_enabled(Client) andalso + check_acl(Client, subscribe, TopicFilter) of + false -> allow; + Result -> Result + end. -inc_stats(Type, Stats = #{pkt := PktCnt, msg := MsgCnt}) -> - Stats#{pkt := PktCnt + 1, msg := case Type =:= ?PUBLISH of - true -> MsgCnt + 1; - false -> MsgCnt - end}. +%% Check Sub Caps +check_sub_caps(TopicFilter, SubOpts, #protocol{client = #{zone := Zone}}) -> + emqx_mqtt_caps:check_sub(Zone, TopicFilter, SubOpts). -terminate(_Reason, #pstate{client_id = undefined}) -> - ok; -terminate(_Reason, PState = #pstate{connected = false}) -> - do_flapping_detect(disconnect, PState), - ok; -terminate(Reason, PState) when Reason =:= conflict; - Reason =:= discard -> - do_flapping_detect(disconnect, PState), - ok; +%%-------------------------------------------------------------------- +%% Process unsubscribe request +%%-------------------------------------------------------------------- -terminate(Reason, PState = #pstate{credentials = Credentials}) -> - do_flapping_detect(disconnect, PState), - ?LOG(info, "Shutdown for ~p", [Reason]), - ok = emqx_hooks:run('client.disconnected', [Credentials, Reason]). +process_unsubscribe(TopicFilters, PState) -> + process_unsubscribe(TopicFilters, [], PState). -start_keepalive(0, _PState) -> - ignore; -start_keepalive(Secs, #pstate{zone = Zone}) when Secs > 0 -> - Backoff = emqx_zone:get_env(Zone, keepalive_backoff, 0.75), - self() ! {keepalive, start, round(Secs * Backoff)}. +process_unsubscribe([], Acc, PState) -> + {lists:reverse(Acc), PState}; -%%----------------------------------------------------------------------------- +process_unsubscribe([{TopicFilter, SubOpts}|More], Acc, PState) -> + {RC, PState1} = do_unsubscribe(TopicFilter, SubOpts, PState), + process_unsubscribe(More, [RC|Acc], PState1). + +do_unsubscribe(TopicFilter, _SubOpts, PState = #protocol{client = Client, + session = Session}) -> + case emqx_session:unsubscribe(Client, mount(Client, TopicFilter), Session) of + {ok, NSession} -> + {?RC_SUCCESS, PState#protocol{session = NSession}}; + {error, RC} -> {RC, PState} + end. + +%%-------------------------------------------------------------------- +%% Is ACL enabled? +%%-------------------------------------------------------------------- + +is_acl_enabled(#{zone := Zone, is_superuser := IsSuperuser}) -> + (not IsSuperuser) andalso emqx_zone:get_env(Zone, enable_acl, true). + +%%-------------------------------------------------------------------- %% Parse topic filters -%%----------------------------------------------------------------------------- +%%-------------------------------------------------------------------- -parse_topic_filters(?SUBSCRIBE, RawTopicFilters) -> - [emqx_topic:parse(RawTopic, SubOpts) || {RawTopic, SubOpts} <- RawTopicFilters]; +parse(subscribe, TopicFilters) -> + [emqx_topic:parse(TopicFilter, SubOpts) || {TopicFilter, SubOpts} <- TopicFilters]; -parse_topic_filters(?UNSUBSCRIBE, RawTopicFilters) -> - lists:map(fun emqx_topic:parse/1, RawTopicFilters). +parse(unsubscribe, TopicFilters) -> + lists:map(fun emqx_topic:parse/1, TopicFilters). + +%%-------------------------------------------------------------------- +%% Mount/Unmount +%%-------------------------------------------------------------------- + +mount(Client = #{mountpoint := MountPoint}, TopicOrMsg) -> + emqx_mountpoint:mount(emqx_mountpoint:replvar(MountPoint, Client), TopicOrMsg). + +unmount(Client = #{mountpoint := MountPoint}, TopicOrMsg) -> + emqx_mountpoint:unmount(emqx_mountpoint:replvar(MountPoint, Client), TopicOrMsg). + +%%-------------------------------------------------------------------- +%% Pipeline +%%-------------------------------------------------------------------- + +pipeline([], Packet, PState) -> + {ok, Packet, PState}; + +pipeline([Fun|More], Packet, PState) -> + case Fun(Packet, PState) of + ok -> pipeline(More, Packet, PState); + {ok, NPState} -> + pipeline(More, Packet, NPState); + {ok, NPacket, NPState} -> + pipeline(More, NPacket, NPState); + {error, ReasonCode} -> + {error, ReasonCode, PState}; + {error, ReasonCode, NPState} -> + {error, ReasonCode, NPState} + end. + +%%-------------------------------------------------------------------- +%% Helper functions +%%-------------------------------------------------------------------- + +set_property(Name, Value, ?NO_PROPS) -> + #{Name => Value}; +set_property(Name, Value, Props) -> + Props#{Name => Value}. + +get_property(_Name, undefined, Default) -> + Default; +get_property(Name, Props, Default) -> + maps:get(Name, Props, Default). sp(true) -> 1; sp(false) -> 0. -flag(false) -> 0; -flag(true) -> 1. +flag(true) -> 1; +flag(false) -> 0. -%%------------------------------------------------------------------------------ -%% Execute actions in case acl deny - -do_flapping_detect(Action, #pstate{zone = Zone, - client_id = ClientId}) -> - ok = case emqx_zone:get_env(Zone, enable_flapping_detect, false) of - true -> - Threshold = emqx_zone:get_env(Zone, flapping_threshold, {10, 60}), - case emqx_flapping:check(Action, ClientId, Threshold) of - flapping -> - BanExpiryInterval = emqx_zone:get_env(Zone, flapping_banned_expiry_interval, 3600000), - Until = erlang:system_time(second) + BanExpiryInterval, - emqx_banned:add(#banned{who = {client_id, ClientId}, - reason = <<"flapping">>, - by = <<"flapping_checker">>, - until = Until}), - ok; - _Other -> - ok - end; - _EnableFlappingDetect -> ok - end. - -do_acl_deny_action(disconnect, ?PUBLISH_PACKET(?QOS_0, _Topic, _PacketId, _Payload), - ?RC_NOT_AUTHORIZED, PState = #pstate{proto_ver = ProtoVer}) -> - {error, emqx_reason_codes:name(?RC_NOT_AUTHORIZED, ProtoVer), PState}; - -do_acl_deny_action(disconnect, ?PUBLISH_PACKET(QoS, _Topic, _PacketId, _Payload), - ?RC_NOT_AUTHORIZED, PState = #pstate{proto_ver = ProtoVer}) - when QoS =:= ?QOS_1; QoS =:= ?QOS_2 -> - deliver({disconnect, ?RC_NOT_AUTHORIZED}, PState), - {error, emqx_reason_codes:name(?RC_NOT_AUTHORIZED, ProtoVer), PState}; - -do_acl_deny_action(Action, ?SUBSCRIBE_PACKET(_PacketId, _Properties, _RawTopicFilters), ReasonCodes, PState) - when is_list(ReasonCodes) -> - traverse_reason_codes(ReasonCodes, Action, PState); -do_acl_deny_action(_OtherAction, _PubSubPacket, ?RC_NOT_AUTHORIZED, PState) -> - {ok, PState}; -do_acl_deny_action(_OtherAction, _PubSubPacket, ReasonCode, PState = #pstate{proto_ver = ProtoVer}) -> - {error, emqx_reason_codes:name(ReasonCode, ProtoVer), PState}. - -traverse_reason_codes([], _Action, PState) -> - {ok, PState}; -traverse_reason_codes([?RC_SUCCESS | LeftReasonCodes], Action, PState) -> - traverse_reason_codes(LeftReasonCodes, Action, PState); -traverse_reason_codes([?RC_NOT_AUTHORIZED | _LeftReasonCodes], disconnect, PState = #pstate{proto_ver = ProtoVer}) -> - {error, emqx_reason_codes:name(?RC_NOT_AUTHORIZED, ProtoVer), PState}; -traverse_reason_codes([?RC_NOT_AUTHORIZED | LeftReasonCodes], Action, PState) -> - traverse_reason_codes(LeftReasonCodes, Action, PState); -traverse_reason_codes([OtherCode | _LeftReasonCodes], _Action, PState = #pstate{proto_ver = ProtoVer}) -> - {error, emqx_reason_codes:name(OtherCode, ProtoVer), PState}. - -%% Reason code compat -reason_codes_compat(_PktType, ReasonCodes, ?MQTT_PROTO_V5) -> - ReasonCodes; -reason_codes_compat(unsuback, _ReasonCodes, _ProtoVer) -> +session_info(undefined) -> undefined; -reason_codes_compat(PktType, ReasonCodes, _ProtoVer) -> - [emqx_reason_codes:compat(PktType, RC) || RC <- ReasonCodes]. - -raw_topic_filters(#pstate{zone = Zone, proto_ver = ProtoVer, is_bridge = IsBridge}, RawTopicFilters) -> - IgnoreLoop = emqx_zone:get_env(Zone, ignore_loop_deliver, false), - case ProtoVer < ?MQTT_PROTO_V5 of - true -> - IfIgnoreLoop = case IgnoreLoop of true -> 1; false -> 0 end, - case IsBridge of - true -> [{RawTopic, SubOpts#{rap => 1, nl => IfIgnoreLoop}} || {RawTopic, SubOpts} <- RawTopicFilters]; - false -> [{RawTopic, SubOpts#{rap => 0, nl => IfIgnoreLoop}} || {RawTopic, SubOpts} <- RawTopicFilters] - end; - false -> - RawTopicFilters - end. - -mountpoint(Credentials) -> - maps:get(mountpoint, Credentials, undefined). - -do_check_banned(_EnableBan = true, Credentials) -> - case emqx_banned:check(Credentials) of - true -> {error, ?RC_BANNED}; - false -> ok - end; -do_check_banned(_EnableBan, _Credentials) -> ok. - -do_acl_check(_EnableAcl = true, Action, Credentials, Topic) -> - AllowTerm = ok, - DenyTerm = {error, ?RC_NOT_AUTHORIZED}, - do_acl_check(Action, Credentials, Topic, AllowTerm, DenyTerm); -do_acl_check(_EnableAcl, _Action, _Credentials, _Topic) -> - ok. - -do_acl_check(Action, Credentials, Topic, AllowTerm, DenyTerm) -> - case emqx_access_control:check_acl(Credentials, Action, Topic) of - allow -> AllowTerm; - deny -> DenyTerm - end. +session_info(Session) -> + emqx_session:info(Session). diff --git a/src/emqx_psk.erl b/src/emqx_psk.erl index cb8835e78..699aa2b36 100644 --- a/src/emqx_psk.erl +++ b/src/emqx_psk.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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). @@ -35,4 +37,4 @@ lookup(psk, ClientPSKID, _UserState) -> Except:Error:Stacktrace -> ?LOG(error, "Lookup PSK failed, ~p: ~p", [{Except,Error}, Stacktrace]), error - end. \ No newline at end of file + end. diff --git a/src/emqx_reason_codes.erl b/src/emqx_reason_codes.erl index b8a9b9e4b..ef4c0cedd 100644 --- a/src/emqx_reason_codes.erl +++ b/src/emqx_reason_codes.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,15 +12,18 @@ %% 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 MQTT5 reason codes -module(emqx_reason_codes). -include("emqx_mqtt.hrl"). --export([ name/2 +-export([ name/1 + , name/2 , text/1 , connack_error/1 + , puback/1 ]). -export([compat/2]). @@ -159,3 +163,8 @@ connack_error(server_busy) -> ?RC_SERVER_BUSY; connack_error(banned) -> ?RC_BANNED; connack_error(bad_authentication_method) -> ?RC_BAD_AUTHENTICATION_METHOD; connack_error(_) -> ?RC_NOT_AUTHORIZED. + +%%TODO: This function should be removed. +puback([]) -> ?RC_NO_MATCHING_SUBSCRIBERS; +puback(L) when is_list(L) -> ?RC_SUCCESS. + diff --git a/src/emqx_router.erl b/src/emqx_router.erl index aefe61667..d0e5bf188 100644 --- a/src/emqx_router.erl +++ b/src/emqx_router.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_router). @@ -64,16 +66,16 @@ -type(group() :: binary()). --type(destination() :: node() | {group(), node()}). +-type(dest() :: node() | {group(), node()}). --define(ROUTE, emqx_route). +-define(ROUTE_TAB, emqx_route). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Mnesia bootstrap -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- mnesia(boot) -> - ok = ekka_mnesia:create_table(?ROUTE, [ + ok = ekka_mnesia:create_table(?ROUTE_TAB, [ {type, bag}, {ram_copies, [node()]}, {record_name, route}, @@ -81,26 +83,26 @@ mnesia(boot) -> {storage_properties, [{ets, [{read_concurrency, true}, {write_concurrency, true}]}]}]); mnesia(copy) -> - ok = ekka_mnesia:copy_table(?ROUTE). + ok = ekka_mnesia:copy_table(?ROUTE_TAB). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Start a router -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- -spec(start_link(atom(), pos_integer()) -> startlink_ret()). start_link(Pool, Id) -> gen_server:start_link({local, emqx_misc:proc_name(?MODULE, Id)}, ?MODULE, [Pool, Id], [{hibernate_after, 1000}]). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Route APIs -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- -spec(add_route(emqx_topic:topic()) -> ok | {error, term()}). add_route(Topic) when is_binary(Topic) -> add_route(Topic, node()). --spec(add_route(emqx_topic:topic(), destination()) -> ok | {error, term()}). +-spec(add_route(emqx_topic:topic(), dest()) -> ok | {error, term()}). add_route(Topic, Dest) when is_binary(Topic) -> call(pick(Topic), {add_route, Topic, Dest}). @@ -108,7 +110,7 @@ add_route(Topic, Dest) when is_binary(Topic) -> do_add_route(Topic) when is_binary(Topic) -> do_add_route(Topic, node()). --spec(do_add_route(emqx_topic:topic(), destination()) -> ok | {error, term()}). +-spec(do_add_route(emqx_topic:topic(), dest()) -> ok | {error, term()}). do_add_route(Topic, Dest) when is_binary(Topic) -> Route = #route{topic = Topic, dest = Dest}, case lists:member(Route, lookup_routes(Topic)) of @@ -140,17 +142,17 @@ match_trie(Topic) -> -spec(lookup_routes(emqx_topic:topic()) -> [emqx_types:route()]). lookup_routes(Topic) -> - ets:lookup(?ROUTE, Topic). + ets:lookup(?ROUTE_TAB, Topic). -spec(has_routes(emqx_topic:topic()) -> boolean()). has_routes(Topic) when is_binary(Topic) -> - ets:member(?ROUTE, Topic). + ets:member(?ROUTE_TAB, Topic). -spec(delete_route(emqx_topic:topic()) -> ok | {error, term()}). delete_route(Topic) when is_binary(Topic) -> delete_route(Topic, node()). --spec(delete_route(emqx_topic:topic(), destination()) -> ok | {error, term()}). +-spec(delete_route(emqx_topic:topic(), dest()) -> ok | {error, term()}). delete_route(Topic, Dest) when is_binary(Topic) -> call(pick(Topic), {delete_route, Topic, Dest}). @@ -158,7 +160,7 @@ delete_route(Topic, Dest) when is_binary(Topic) -> do_delete_route(Topic) when is_binary(Topic) -> do_delete_route(Topic, node()). --spec(do_delete_route(emqx_topic:topic(), destination()) -> ok | {error, term()}). +-spec(do_delete_route(emqx_topic:topic(), dest()) -> ok | {error, term()}). do_delete_route(Topic, Dest) -> Route = #route{topic = Topic, dest = Dest}, case emqx_topic:wildcard(Topic) of @@ -168,7 +170,7 @@ do_delete_route(Topic, Dest) -> -spec(topics() -> list(emqx_topic:topic())). topics() -> - mnesia:dirty_all_keys(?ROUTE). + mnesia:dirty_all_keys(?ROUTE_TAB). %% @doc Print routes to a topic -spec(print_routes(emqx_topic:topic()) -> ok). @@ -183,9 +185,9 @@ call(Router, Msg) -> pick(Topic) -> gproc_pool:pick_worker(router_pool, Topic). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% gen_server callbacks -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- init([Pool, Id]) -> true = gproc_pool:connect_worker(Pool, {Pool, Id}), @@ -217,30 +219,30 @@ terminate(_Reason, #{pool := Pool, id := Id}) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Internal functions -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- insert_direct_route(Route) -> - mnesia:async_dirty(fun mnesia:write/3, [?ROUTE, Route, sticky_write]). + mnesia:async_dirty(fun mnesia:write/3, [?ROUTE_TAB, Route, sticky_write]). insert_trie_route(Route = #route{topic = Topic}) -> - case mnesia:wread({?ROUTE, Topic}) of + case mnesia:wread({?ROUTE_TAB, Topic}) of [] -> emqx_trie:insert(Topic); _ -> ok end, - mnesia:write(?ROUTE, Route, sticky_write). + mnesia:write(?ROUTE_TAB, Route, sticky_write). delete_direct_route(Route) -> - mnesia:async_dirty(fun mnesia:delete_object/3, [?ROUTE, Route, sticky_write]). + mnesia:async_dirty(fun mnesia:delete_object/3, [?ROUTE_TAB, Route, sticky_write]). delete_trie_route(Route = #route{topic = Topic}) -> - case mnesia:wread({?ROUTE, Topic}) of + case mnesia:wread({?ROUTE_TAB, Topic}) of [Route] -> %% Remove route and trie - ok = mnesia:delete_object(?ROUTE, Route, sticky_write), + ok = mnesia:delete_object(?ROUTE_TAB, Route, sticky_write), emqx_trie:delete(Topic); [_|_] -> %% Remove route only - mnesia:delete_object(?ROUTE, Route, sticky_write); + mnesia:delete_object(?ROUTE_TAB, Route, sticky_write); [] -> ok end. diff --git a/src/emqx_router_helper.erl b/src/emqx_router_helper.erl index aac228f66..301958dc3 100644 --- a/src/emqx_router_helper.erl +++ b/src/emqx_router_helper.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_router_helper). @@ -51,9 +53,9 @@ -define(ROUTING_NODE, emqx_routing_node). -define(LOCK, {?MODULE, cleanup_routes}). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Mnesia bootstrap -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- mnesia(boot) -> ok = ekka_mnesia:create_table(?ROUTING_NODE, [ @@ -66,9 +68,9 @@ mnesia(boot) -> mnesia(copy) -> ok = ekka_mnesia:copy_table(?ROUTING_NODE). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% API -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% @doc Starts the router helper -spec(start_link() -> startlink_ret()). @@ -86,9 +88,9 @@ monitor(Node) when is_atom(Node) -> false -> mnesia:dirty_write(?ROUTING_NODE, #routing_node{name = Node}) end. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% gen_server callbacks -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- init([]) -> ok = ekka:monitor(membership), @@ -154,9 +156,9 @@ terminate(_Reason, _State) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Internal functions -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- stats_fun() -> case ets:info(?ROUTE, size) of diff --git a/src/emqx_router_sup.erl b/src/emqx_router_sup.erl index 486d44b69..c4944e3c1 100644 --- a/src/emqx_router_sup.erl +++ b/src/emqx_router_sup.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_router_sup). diff --git a/src/emqx_rpc.erl b/src/emqx_rpc.erl index 96adf6605..c66d938e2 100644 --- a/src/emqx_rpc.erl +++ b/src/emqx_rpc.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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 wrap gen_rpc? -module(emqx_rpc). @@ -20,6 +22,11 @@ , multicall/4 ]). +-compile({inline, + [ rpc_node/1 + , rpc_nodes/1 + ]}). + -define(RPC, gen_rpc). call(Node, Mod, Fun, Args) -> @@ -32,7 +39,8 @@ cast(Node, Mod, Fun, Args) -> filter_result(?RPC:cast(rpc_node(Node), Mod, Fun, Args)). rpc_node(Node) -> - {Node, erlang:system_info(scheduler_id)}. + {ok, ClientNum} = application:get_env(gen_rpc, tcp_client_num), + {Node, rand:uniform(ClientNum)}. rpc_nodes(Nodes) -> rpc_nodes(Nodes, []). @@ -42,9 +50,9 @@ rpc_nodes([], Acc) -> rpc_nodes([Node | Nodes], Acc) -> rpc_nodes(Nodes, [rpc_node(Node) | Acc]). - filter_result({Error, Reason}) when Error =:= badrpc; Error =:= badtcp -> {badrpc, Reason}; filter_result(Delivery) -> Delivery. + diff --git a/src/emqx_sequence.erl b/src/emqx_sequence.erl index f02165ea6..8aa763b6f 100644 --- a/src/emqx_sequence.erl +++ b/src/emqx_sequence.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_sequence). @@ -21,17 +23,17 @@ , delete/1 ]). +-export_type([seqid/0]). + -type(key() :: term()). -type(name() :: atom()). -type(seqid() :: non_neg_integer()). --export_type([seqid/0]). - -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% APIs -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% @doc Create a sequence. -spec(create(name()) -> ok). diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 7d23c9e62..2530f3a42 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -14,6 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- +%%-------------------------------------------------------------------- %% @doc %% A stateful interaction between a Client and a Server. Some Sessions %% last only as long as the Network Connection, others can span multiple @@ -37,11 +38,11 @@ %% If the Session is currently not connected, the time at which the Session %% will end and Session State will be discarded. %% @end +%%-------------------------------------------------------------------- +%% MQTT Session -module(emqx_session). --behaviour(gen_server). - -include("emqx.hrl"). -include("emqx_mqtt.hrl"). -include("logger.hrl"). @@ -49,761 +50,423 @@ -logger_header("[Session]"). --export([start_link/1]). +-export([init/3]). -export([ info/1 + , info/2 , attrs/1 , stats/1 ]). --export([ resume/2 - , discard/2 - , update_expiry_interval/2 +-export([ subscribe/4 + , unsubscribe/3 ]). --export([ subscribe/2 - , subscribe/4 - , unsubscribe/2 - , unsubscribe/4 - , publish/3 +-export([ publish/3 , puback/2 - , puback/3 , pubrec/2 - , pubrec/3 - , pubrel/3 - , pubcomp/3 + , pubrel/2 + , pubcomp/2 ]). --export([close/1]). +-export([deliver/2]). -%% gen_server callbacks --export([ init/1 - , handle_call/3 - , handle_cast/2 - , handle_info/2 - , terminate/2 - , code_change/3 +-export([timeout/3]). + +-export_type([session/0]). + +-import(emqx_zone, + [ get_env/2 + , get_env/3 ]). --import(emqx_zone, [get_env/2, get_env/3]). - --record(state, { - %% zone - zone :: atom(), - - %% Idle timeout - idle_timeout :: pos_integer(), +%% For test case +-export([set_pkt_id/2]). +-record(session, { %% Clean Start Flag - clean_start = false :: boolean(), - - %% Conn Binding: local | remote - %% binding = local :: local | remote, - - %% Deliver fun - deliver_fun :: function(), - - %% ClientId: Identifier of Session - client_id :: binary(), - - %% Username - username :: maybe(binary()), - - %% Connection pid binding with session - conn_pid :: pid(), - - %% Old Connection Pid that has been kickout - old_conn_pid :: pid(), - - %% Next packet id of the session - next_pkt_id = 1 :: emqx_mqtt_types:packet_id(), + clean_start :: boolean(), %% Client’s Subscriptions. subscriptions :: map(), - %% Client <- Broker: Inflight QoS1, QoS2 messages sent to the client but unacked. + %% 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(), - %% Max Inflight Size. DEPRECATED: Get from inflight - %% max_inflight = 32 :: non_neg_integer(), - - %% Retry Timer - retry_timer :: maybe(reference()), - %% 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(), - %% Client -> Broker: Inflight QoS2 messages received from client and waiting for pubrel. + %% 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 = 7200 :: timeout(), + expiry_interval :: timeout(), %% Expired Timer expiry_timer :: maybe(reference()), - %% Stats timer - stats_timer :: maybe(reference()), - - %% GC State - gc_state, - %% Created at - created_at :: erlang:timestamp(), - - will_msg :: emqx:message(), - - will_delay_timer :: maybe(reference()) - + created_at :: erlang:timestamp() }). --type(spid() :: pid()). --type(attr() :: {atom(), term()}). +-opaque(session() :: #session{}). --export_type([attr/0]). +-type(publish() :: {publish, emqx_types:packet_id(), emqx_types:message()}). -define(DEFAULT_BATCH_N, 1000). -%% @doc Start a session proc. --spec(start_link(SessAttrs :: map()) -> {ok, pid()}). -start_link(SessAttrs) -> - proc_lib:start_link(?MODULE, init, [[self(), SessAttrs]]). +%%-------------------------------------------------------------------- +%% Init a session +%%-------------------------------------------------------------------- -%% @doc Get session info --spec(info(spid() | #state{}) -> list({atom(), term()})). -info(SPid) when is_pid(SPid) -> - gen_server:call(SPid, info, infinity); - -info(State = #state{zone = Zone, - conn_pid = ConnPid, - next_pkt_id = PktId, - subscriptions = Subscriptions, - inflight = Inflight, - mqueue = MQueue, - awaiting_rel = AwaitingRel}) -> - attrs(State) ++ [{conn_pid, ConnPid}, - {next_pkt_id, PktId}, - {max_subscriptions, get_env(Zone, max_subscriptions, 0)}, - {subscriptions, Subscriptions}, - {upgrade_qos, get_env(Zone, upgrade_qos, false)}, - {inflight, Inflight}, - {retry_interval, get_env(Zone, retry_interval, 0)}, - {mqueue_len, emqx_mqueue:len(MQueue)}, - {awaiting_rel, AwaitingRel}, - {max_awaiting_rel, get_env(Zone, max_awaiting_rel)}, - {await_rel_timeout, get_env(Zone, await_rel_timeout)}]. - -%% @doc Get session attrs --spec(attrs(spid() | #state{}) -> list({atom(), term()})). -attrs(SPid) when is_pid(SPid) -> - gen_server:call(SPid, attrs, infinity); - -attrs(#state{clean_start = CleanStart, - client_id = ClientId, - conn_pid = ConnPid, - username = Username, - expiry_interval = ExpiryInterval, - created_at = CreatedAt}) -> - [{clean_start, CleanStart}, - {binding, binding(ConnPid)}, - {client_id, ClientId}, - {username, Username}, - {expiry_interval, ExpiryInterval}, - {created_at, CreatedAt}]. - --spec(stats(spid() | #state{}) -> list({atom(), non_neg_integer()})). -stats(SPid) when is_pid(SPid) -> - gen_server:call(SPid, stats, infinity); - -stats(#state{zone = Zone, - subscriptions = Subscriptions, - inflight = Inflight, - mqueue = MQueue, - awaiting_rel = AwaitingRel}) -> - lists:append(emqx_misc:proc_stats(), - [{max_subscriptions, get_env(Zone, max_subscriptions, 0)}, - {subscriptions_count, maps:size(Subscriptions)}, - {max_inflight, emqx_inflight:max_size(Inflight)}, - {inflight_len, emqx_inflight:size(Inflight)}, - {max_mqueue, emqx_mqueue:max_len(MQueue)}, - {mqueue_len, emqx_mqueue:len(MQueue)}, - {mqueue_dropped, emqx_mqueue:dropped(MQueue)}, - {max_awaiting_rel, get_env(Zone, max_awaiting_rel)}, - {awaiting_rel_len, maps:size(AwaitingRel)}, - {deliver_msg, emqx_pd:get_counter(deliver_stats)}, - {enqueue_msg, emqx_pd:get_counter(enqueue_stats)}]). - -%%------------------------------------------------------------------------------ -%% PubSub API -%%------------------------------------------------------------------------------ - --spec(subscribe(spid(), list({emqx_topic:topic(), emqx_types:subopts()})) -> ok). -subscribe(SPid, RawTopicFilters) when is_list(RawTopicFilters) -> - TopicFilters = [emqx_topic:parse(RawTopic, maps:merge(?DEFAULT_SUBOPTS, SubOpts)) - || {RawTopic, SubOpts} <- RawTopicFilters], - subscribe(SPid, undefined, #{}, TopicFilters). - --spec(subscribe(spid(), emqx_mqtt_types:packet_id(), - emqx_mqtt_types:properties(), emqx_mqtt_types:topic_filters()) -> ok). -subscribe(SPid, PacketId, Properties, TopicFilters) -> - SubReq = {PacketId, Properties, TopicFilters}, - gen_server:cast(SPid, {subscribe, self(), SubReq}). - -%% @doc Called by connection processes when publishing messages --spec(publish(spid(), emqx_mqtt_types:packet_id(), emqx_types:message()) - -> {ok, emqx_types:deliver_results()} | {error, term()}). -publish(_SPid, _PacketId, Msg = #message{qos = ?QOS_0}) -> - %% Publish QoS0 message directly - {ok, emqx_broker:publish(Msg)}; - -publish(_SPid, _PacketId, Msg = #message{qos = ?QOS_1}) -> - %% Publish QoS1 message directly - {ok, emqx_broker:publish(Msg)}; - -publish(SPid, PacketId, Msg = #message{qos = ?QOS_2, timestamp = Ts}) -> - %% Register QoS2 message packet ID (and timestamp) to session, then publish - case gen_server:call(SPid, {register_publish_packet_id, PacketId, Ts}, infinity) of - ok -> {ok, emqx_broker:publish(Msg)}; - {error, Reason} -> {error, Reason} - end. - --spec(puback(spid(), emqx_mqtt_types:packet_id()) -> ok). -puback(SPid, PacketId) -> - gen_server:cast(SPid, {puback, PacketId, ?RC_SUCCESS}). - --spec(puback(spid(), emqx_mqtt_types:packet_id(), emqx_mqtt_types:reason_code()) -> ok). -puback(SPid, PacketId, ReasonCode) -> - gen_server:cast(SPid, {puback, PacketId, ReasonCode}). - --spec(pubrec(spid(), emqx_mqtt_types:packet_id()) -> ok | {error, emqx_mqtt_types:reason_code()}). -pubrec(SPid, PacketId) -> - pubrec(SPid, PacketId, ?RC_SUCCESS). - --spec(pubrec(spid(), emqx_mqtt_types:packet_id(), emqx_mqtt_types:reason_code()) - -> ok | {error, emqx_mqtt_types:reason_code()}). -pubrec(SPid, PacketId, ReasonCode) -> - gen_server:call(SPid, {pubrec, PacketId, ReasonCode}, infinity). - --spec(pubrel(spid(), emqx_mqtt_types:packet_id(), emqx_mqtt_types:reason_code()) - -> ok | {error, emqx_mqtt_types:reason_code()}). -pubrel(SPid, PacketId, ReasonCode) -> - gen_server:call(SPid, {pubrel, PacketId, ReasonCode}, infinity). - --spec(pubcomp(spid(), emqx_mqtt_types:packet_id(), emqx_mqtt_types:reason_code()) -> ok). -pubcomp(SPid, PacketId, ReasonCode) -> - gen_server:cast(SPid, {pubcomp, PacketId, ReasonCode}). - --spec(unsubscribe(spid(), emqx_types:topic_table()) -> ok). -unsubscribe(SPid, RawTopicFilters) when is_list(RawTopicFilters) -> - TopicFilters = lists:map(fun({RawTopic, Opts}) -> - emqx_topic:parse(RawTopic, Opts); - (RawTopic) when is_binary(RawTopic) -> - emqx_topic:parse(RawTopic) - end, RawTopicFilters), - unsubscribe(SPid, undefined, #{}, TopicFilters). - --spec(unsubscribe(spid(), emqx_mqtt_types:packet_id(), - emqx_mqtt_types:properties(), emqx_mqtt_types:topic_filters()) -> ok). -unsubscribe(SPid, PacketId, Properties, TopicFilters) -> - UnsubReq = {PacketId, Properties, TopicFilters}, - gen_server:cast(SPid, {unsubscribe, self(), UnsubReq}). - --spec(resume(spid(), map()) -> ok). -resume(SPid, SessAttrs) -> - gen_server:cast(SPid, {resume, SessAttrs}). - -%% @doc Discard the session --spec(discard(spid(), ByPid :: pid()) -> ok). -discard(SPid, ByPid) -> - gen_server:call(SPid, {discard, ByPid}). - --spec(update_expiry_interval(spid(), timeout()) -> ok). -update_expiry_interval(SPid, Interval) -> - gen_server:cast(SPid, {update_expiry_interval, Interval}). - --spec(close(spid()) -> ok). -close(SPid) -> - gen_server:call(SPid, close). - -%%------------------------------------------------------------------------------ -%% gen_server callbacks -%%------------------------------------------------------------------------------ - -init([Parent, #{zone := Zone, - client_id := ClientId, - username := Username, - conn_pid := ConnPid, - clean_start := CleanStart, - expiry_interval := ExpiryInterval, - max_inflight := MaxInflight, - will_msg := WillMsg}]) -> - process_flag(trap_exit, true), - true = link(ConnPid), - emqx_logger:set_metadata_client_id(ClientId), - GcPolicy = emqx_zone:get_env(Zone, force_gc_policy, false), - IdleTimout = get_env(Zone, idle_timeout, 30000), - State = #state{zone = Zone, - idle_timeout = IdleTimout, - clean_start = CleanStart, - deliver_fun = deliver_fun(ConnPid), - client_id = ClientId, - username = Username, - conn_pid = ConnPid, - subscriptions = #{}, - inflight = emqx_inflight:new(MaxInflight), - mqueue = init_mqueue(Zone), - awaiting_rel = #{}, - expiry_interval = ExpiryInterval, - gc_state = emqx_gc:init(GcPolicy), - created_at = os:timestamp(), - will_msg = WillMsg - }, - ok = emqx_sm:register_session(ClientId, self()), - true = emqx_sm:set_session_attrs(ClientId, attrs(State)), - true = emqx_sm:set_session_stats(ClientId, stats(State)), - ok = emqx_hooks:run('session.created', [#{client_id => ClientId}, info(State)]), - ok = emqx_misc:init_proc_mng_policy(Zone), - ok = proc_lib:init_ack(Parent, {ok, self()}), - gen_server:enter_loop(?MODULE, [{hibernate_after, IdleTimout}], State). +%% @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), + subscriptions = #{}, + upgrade_qos = get_env(Zone, upgrade_qos, false), + inflight = emqx_inflight:new(MaxInflight), + mqueue = init_mqueue(Zone), + next_pkt_id = 1, + retry_interval = get_env(Zone, retry_interval, 0), + awaiting_rel = #{}, + max_awaiting_rel = get_env(Zone, max_awaiting_rel, 100), + await_rel_timeout = get_env(Zone, await_rel_timeout, 3600*1000), + expiry_interval = ExpiryInterval, + created_at = os:timestamp() + }. init_mqueue(Zone) -> emqx_mqueue:init(#{max_len => get_env(Zone, max_mqueue_len, 1000), store_qos0 => get_env(Zone, mqueue_store_qos0, true), - priorities => get_env(Zone, mqueue_priorities), - default_priority => get_env(Zone, mqueue_default_priority) + priorities => get_env(Zone, mqueue_priorities, none), + default_priority => get_env(Zone, mqueue_default_priority, lowest) }). -binding(undefined) -> undefined; -binding(ConnPid) -> - case node(ConnPid) =:= node() of true -> local; false -> remote end. +%%-------------------------------------------------------------------- +%% Infos of the session +%%-------------------------------------------------------------------- -deliver_fun(ConnPid) when node(ConnPid) == node() -> - fun(Packet) -> ConnPid ! {deliver, Packet}, ok end; -deliver_fun(ConnPid) -> - Node = node(ConnPid), - fun(Packet) -> - true = emqx_rpc:cast(Node, erlang, send, [ConnPid, {deliver, Packet}]), ok +-spec(info(session()) -> emqx_types:infos()). +info(#session{clean_start = CleanStart, + max_subscriptions = MaxSubscriptions, + subscriptions = Subscriptions, + upgrade_qos = UpgradeQoS, + inflight = Inflight, + retry_interval = RetryInterval, + mqueue = MQueue, + next_pkt_id = PacketId, + max_awaiting_rel = MaxAwaitingRel, + awaiting_rel = AwaitingRel, + await_rel_timeout = AwaitRelTimeout, + expiry_interval = ExpiryInterval, + created_at = CreatedAt}) -> + #{clean_start => CleanStart, + subscriptions => Subscriptions, + max_subscriptions => MaxSubscriptions, + upgrade_qos => UpgradeQoS, + inflight => emqx_inflight:size(Inflight), + max_inflight => emqx_inflight:max_size(Inflight), + retry_interval => RetryInterval, + mqueue_len => emqx_mqueue:len(MQueue), + max_mqueue => emqx_mqueue:max_len(MQueue), + mqueue_dropped => emqx_mqueue:dropped(MQueue), + next_pkt_id => PacketId, + awaiting_rel => maps:size(AwaitingRel), + max_awaiting_rel => MaxAwaitingRel, + await_rel_timeout => AwaitRelTimeout, + expiry_interval => ExpiryInterval div 1000, + created_at => CreatedAt + }. + +info(clean_start, #session{clean_start = CleanStart}) -> + CleanStart; +info(subscriptions, #session{subscriptions = Subs}) -> + Subs; +info(max_subscriptions, #session{max_subscriptions = MaxSubs}) -> + MaxSubs; +info(upgrade_qos, #session{upgrade_qos = UpgradeQoS}) -> + UpgradeQoS; +info(inflight, #session{inflight = Inflight}) -> + emqx_inflight:size(Inflight); +info(max_inflight, #session{inflight = Inflight}) -> + emqx_inflight:max_size(Inflight); +info(retry_interval, #session{retry_interval = Interval}) -> + Interval; +info(mqueue_len, #session{mqueue = MQueue}) -> + emqx_mqueue:len(MQueue); +info(max_mqueue, #session{mqueue = MQueue}) -> + emqx_mqueue:max_len(MQueue); +info(mqueue_dropped, #session{mqueue = MQueue}) -> + emqx_mqueue:dropped(MQueue); +info(next_pkt_id, #session{next_pkt_id = PacketId}) -> + PacketId; +info(awaiting_rel, #session{awaiting_rel = AwaitingRel}) -> + maps:size(AwaitingRel); +info(max_awaiting_rel, #session{max_awaiting_rel = MaxAwaitingRel}) -> + MaxAwaitingRel; +info(await_rel_timeout, #session{await_rel_timeout = Timeout}) -> + Timeout; +info(expiry_interval, #session{expiry_interval = Interval}) -> + Interval div 1000; +info(created_at, #session{created_at = CreatedAt}) -> + CreatedAt. + +%%-------------------------------------------------------------------- +%% Attrs of the session +%%-------------------------------------------------------------------- + +-spec(attrs(session()) -> emqx_types:attrs()). +attrs(#session{clean_start = CleanStart, + expiry_interval = ExpiryInterval, + created_at = CreatedAt}) -> + #{clean_start => CleanStart, + expiry_interval => ExpiryInterval, + created_at => CreatedAt + }. + +%%-------------------------------------------------------------------- +%% Stats of the session +%%-------------------------------------------------------------------- + +%% @doc Get stats of the session. +-spec(stats(session()) -> emqx_types:stats()). +stats(#session{subscriptions = Subscriptions, + max_subscriptions = MaxSubscriptions, + inflight = Inflight, + mqueue = MQueue, + awaiting_rel = AwaitingRel, + max_awaiting_rel = MaxAwaitingRel}) -> + [{subscriptions, maps:size(Subscriptions)}, + {max_subscriptions, MaxSubscriptions}, + {inflight, emqx_inflight:size(Inflight)}, + {max_inflight, emqx_inflight:max_size(Inflight)}, + {mqueue_len, emqx_mqueue:len(MQueue)}, + {max_mqueue, emqx_mqueue:max_len(MQueue)}, + {mqueue_dropped, emqx_mqueue:dropped(MQueue)}, + {awaiting_rel, maps:size(AwaitingRel)}, + {max_awaiting_rel, MaxAwaitingRel}]. + +%%-------------------------------------------------------------------- +%% Client -> Broker: SUBSCRIBE +%%-------------------------------------------------------------------- + +-spec(subscribe(emqx_types:client(), emqx_types:topic(), emqx_types:subopts(), + session()) -> {ok, session()} | {error, emqx_types:reason_code()}). +subscribe(Client, TopicFilter, SubOpts, Session = #session{subscriptions = Subs}) -> + case is_subscriptions_full(Session) + andalso (not maps:is_key(TopicFilter, Subs)) of + true -> {error, ?RC_QUOTA_EXCEEDED}; + false -> + do_subscribe(Client, TopicFilter, SubOpts, Session) end. -handle_call(info, _From, State) -> - reply(info(State), State); +is_subscriptions_full(#session{max_subscriptions = 0}) -> + false; +is_subscriptions_full(#session{max_subscriptions = MaxLimit, + subscriptions = Subs}) -> + maps:size(Subs) >= MaxLimit. -handle_call(attrs, _From, State) -> - reply(attrs(State), State); - -handle_call(stats, _From, State) -> - reply(stats(State), State); - -handle_call({discard, ByPid}, _From, State = #state{conn_pid = undefined}) -> - ?LOG(warning, "Discarded by ~p", [ByPid]), - {stop, {shutdown, discarded}, ok, State}; - -handle_call({discard, ByPid}, _From, State = #state{client_id = ClientId, conn_pid = ConnPid}) -> - ?LOG(warning, "Conn ~p is discarded by ~p", [ConnPid, ByPid]), - ConnPid ! {shutdown, discard, {ClientId, ByPid}}, - {stop, {shutdown, discarded}, ok, State}; - -%% PUBLISH: This is only to register packetId to session state. -%% The actual message dispatching should be done by the caller (e.g. connection) process. -handle_call({register_publish_packet_id, PacketId, Ts}, _From, - State = #state{zone = Zone, awaiting_rel = AwaitingRel}) -> - MaxAwaitingRel = get_env(Zone, max_awaiting_rel), - reply( - case is_awaiting_full(MaxAwaitingRel, AwaitingRel) of - false -> - case maps:is_key(PacketId, AwaitingRel) of - true -> - {{error, ?RC_PACKET_IDENTIFIER_IN_USE}, State}; - false -> - State1 = State#state{awaiting_rel = maps:put(PacketId, Ts, AwaitingRel)}, - {ok, ensure_stats_timer(ensure_await_rel_timer(State1))} - end; - true -> - ?LOG(warning, "Dropped qos2 packet ~w for too many awaiting_rel", [PacketId]), - ok = emqx_metrics:inc('messages.qos2.dropped'), - {{error, ?RC_RECEIVE_MAXIMUM_EXCEEDED}, State} - end); - -%% PUBREC: -handle_call({pubrec, PacketId, _ReasonCode}, _From, State = #state{inflight = Inflight}) -> - reply( - case emqx_inflight:contain(PacketId, Inflight) of - true -> - {ok, ensure_stats_timer(acked(pubrec, PacketId, State))}; - false -> - ?LOG(warning, "The PUBREC PacketId ~w is not found.", [PacketId]), - ok = emqx_metrics:inc('packets.pubrec.missed'), - {{error, ?RC_PACKET_IDENTIFIER_NOT_FOUND}, State} - end); - -%% PUBREL: -handle_call({pubrel, PacketId, _ReasonCode}, _From, State = #state{awaiting_rel = AwaitingRel}) -> - reply( - case maps:take(PacketId, AwaitingRel) of - {_Ts, AwaitingRel1} -> - {ok, ensure_stats_timer(State#state{awaiting_rel = AwaitingRel1})}; - error -> - ?LOG(warning, "The PUBREL PacketId ~w is not found", [PacketId]), - ok = emqx_metrics:inc('packets.pubrel.missed'), - {{error, ?RC_PACKET_IDENTIFIER_NOT_FOUND}, State} - end); - -handle_call(close, _From, State) -> - {stop, normal, ok, State}; - -handle_call(Req, _From, State) -> - ?LOG(error, "Unexpected call: ~p", [Req]), - {reply, ignored, State}. - -%% SUBSCRIBE: -handle_cast({subscribe, FromPid, {PacketId, _Properties, TopicFilters}}, - State = #state{client_id = ClientId, username = Username, subscriptions = Subscriptions}) -> - {ReasonCodes, Subscriptions1} = - lists:foldr( - fun ({Topic, SubOpts = #{qos := QoS, rc := RC}}, {RcAcc, SubMap}) when - RC == ?QOS_0; RC == ?QOS_1; RC == ?QOS_2 -> - {[QoS|RcAcc], do_subscribe(ClientId, Username, Topic, SubOpts, SubMap)}; - ({_Topic, #{rc := RC}}, {RcAcc, SubMap}) -> - {[RC|RcAcc], SubMap} - end, {[], Subscriptions}, TopicFilters), - suback(FromPid, PacketId, ReasonCodes), - noreply(ensure_stats_timer(State#state{subscriptions = Subscriptions1})); - -%% UNSUBSCRIBE: -handle_cast({unsubscribe, From, {PacketId, _Properties, TopicFilters}}, - State = #state{client_id = ClientId, username = Username, subscriptions = Subscriptions}) -> - {ReasonCodes, Subscriptions1} = - lists:foldr(fun({Topic, _SubOpts}, {Acc, SubMap}) -> - case maps:find(Topic, SubMap) of - {ok, SubOpts} -> - ok = emqx_broker:unsubscribe(Topic), - ok = emqx_hooks:run('session.unsubscribed', [#{client_id => ClientId, username => Username}, Topic, SubOpts]), - {[?RC_SUCCESS|Acc], maps:remove(Topic, SubMap)}; - error -> - {[?RC_NO_SUBSCRIPTION_EXISTED|Acc], SubMap} - end - end, {[], Subscriptions}, TopicFilters), - unsuback(From, PacketId, ReasonCodes), - noreply(ensure_stats_timer(State#state{subscriptions = Subscriptions1})); - -%% PUBACK: -handle_cast({puback, PacketId, _ReasonCode}, State = #state{inflight = Inflight}) -> - noreply( - case emqx_inflight:contain(PacketId, Inflight) of - true -> - ensure_stats_timer(dequeue(acked(puback, PacketId, State))); - false -> - ?LOG(warning, "The PUBACK PacketId ~w is not found", [PacketId]), - ok = emqx_metrics:inc('packets.puback.missed'), - State - end); - -%% PUBCOMP: -handle_cast({pubcomp, PacketId, _ReasonCode}, State = #state{inflight = Inflight}) -> - noreply( - case emqx_inflight:contain(PacketId, Inflight) of - true -> - ensure_stats_timer(dequeue(acked(pubcomp, PacketId, State))); - false -> - ?LOG(warning, "The PUBCOMP PacketId ~w is not found", [PacketId]), - ok = emqx_metrics:inc('packets.pubcomp.missed'), - State - end); - -%% RESUME: -handle_cast({resume, #{conn_pid := ConnPid, - will_msg := WillMsg, - expiry_interval := ExpiryInterval, - max_inflight := MaxInflight}}, - State = #state{client_id = ClientId, - conn_pid = OldConnPid, - clean_start = CleanStart, - retry_timer = RetryTimer, - await_rel_timer = AwaitTimer, - expiry_timer = ExpireTimer, - will_delay_timer = WillDelayTimer}) -> - - ?LOG(info, "Resumed by connection ~p ", [ConnPid]), - - %% Cancel Timers - lists:foreach(fun emqx_misc:cancel_timer/1, - [RetryTimer, AwaitTimer, ExpireTimer, WillDelayTimer]), - - case kick(ClientId, OldConnPid, ConnPid) of - ok -> ?LOG(warning, "Connection ~p kickout ~p", [ConnPid, OldConnPid]); - ignore -> ok +do_subscribe(Client = #{client_id := ClientId}, + TopicFilter, SubOpts, Session = #session{subscriptions = Subs}) -> + case IsNew = (not maps:is_key(TopicFilter, Subs)) of + true -> + ok = emqx_broker:subscribe(TopicFilter, ClientId, SubOpts); + false -> + _ = emqx_broker:set_subopts(TopicFilter, SubOpts) end, + ok = emqx_hooks:run('session.subscribed', + [Client, TopicFilter, SubOpts#{new => IsNew}]), + Subs1 = maps:put(TopicFilter, SubOpts, Subs), + {ok, Session#session{subscriptions = Subs1}}. - true = link(ConnPid), +%%-------------------------------------------------------------------- +%% Client -> Broker: UNSUBSCRIBE +%%-------------------------------------------------------------------- - State1 = State#state{conn_pid = ConnPid, - deliver_fun = deliver_fun(ConnPid), - old_conn_pid = OldConnPid, - clean_start = false, - retry_timer = undefined, - awaiting_rel = #{}, - await_rel_timer = undefined, - expiry_timer = undefined, - expiry_interval = ExpiryInterval, - inflight = emqx_inflight:update_size(MaxInflight, State#state.inflight), - will_delay_timer = undefined, - will_msg = WillMsg}, +-spec(unsubscribe(emqx_types:client(), emqx_types:topic(), session()) + -> {ok, session()} | {error, emqx_types:reason_code()}). +unsubscribe(Client, TopicFilter, Session = #session{subscriptions = Subs}) -> + case maps:find(TopicFilter, Subs) of + {ok, SubOpts} -> + ok = emqx_broker:unsubscribe(TopicFilter), + ok = emqx_hooks:run('session.unsubscribed', [Client, TopicFilter, SubOpts]), + {ok, Session#session{subscriptions = maps:remove(TopicFilter, Subs)}}; + error -> + {error, ?RC_NO_SUBSCRIPTION_EXISTED} + end. - %% Clean Session: true -> false??? - CleanStart andalso emqx_sm:set_session_attrs(ClientId, attrs(State1)), +%%-------------------------------------------------------------------- +%% Client -> Broker: PUBLISH +%%-------------------------------------------------------------------- - ok = emqx_hooks:run('session.resumed', [#{client_id => ClientId}, attrs(State)]), - - %% Replay delivery and Dequeue pending messages - noreply(ensure_stats_timer(dequeue(retry_delivery(true, State1)))); - -handle_cast({update_expiry_interval, Interval}, State) -> - {noreply, State#state{expiry_interval = Interval}}; - -handle_cast(Msg, State) -> - ?LOG(error, "Unexpected cast: ~p", [Msg]), - {noreply, State}. - -handle_info({dispatch, Topic, Msg}, State) when is_record(Msg, message) -> - handle_dispatch([{Topic, Msg}], State); - -handle_info({dispatch, Topic, Msgs}, State) when is_list(Msgs) -> - handle_dispatch([{Topic, Msg} || Msg <- Msgs], State); - -%% Do nothing if the client has been disconnected. -handle_info({timeout, Timer, retry_delivery}, State = #state{conn_pid = undefined, retry_timer = Timer}) -> - noreply(State#state{retry_timer = undefined}); - -handle_info({timeout, Timer, retry_delivery}, State = #state{retry_timer = Timer}) -> - noreply(retry_delivery(false, State#state{retry_timer = undefined})); - -handle_info({timeout, Timer, check_awaiting_rel}, State = #state{await_rel_timer = Timer}) -> - State1 = State#state{await_rel_timer = undefined}, - noreply(ensure_stats_timer(expire_awaiting_rel(State1))); - -handle_info({timeout, Timer, emit_stats}, - State = #state{client_id = ClientId, - stats_timer = Timer, - gc_state = GcState}) -> - _ = emqx_sm:set_session_stats(ClientId, stats(State)), - NewState = State#state{stats_timer = undefined}, - Limits = erlang:get(force_shutdown_policy), - case emqx_misc:conn_proc_mng_policy(Limits) of - continue -> - {noreply, NewState}; - hibernate -> - %% going to hibernate, reset gc stats - GcState1 = emqx_gc:reset(GcState), - {noreply, NewState#state{gc_state = GcState1}, hibernate}; - {shutdown, Reason} -> - ?LOG(warning, "Shutdown exceptionally due to ~p", [Reason]), - shutdown(Reason, NewState) +-spec(publish(emqx_types:packet_id(), emqx_types:message(), session()) + -> {ok, emqx_types:publish_result()} | + {ok, emqx_types:publish_result(), session()} | + {error, emqx_types:reason_code()}). +publish(PacketId, Msg = #message{qos = ?QOS_2}, Session) -> + case is_awaiting_full(Session) of + false -> + do_publish(PacketId, Msg, Session); + true -> + ?LOG(warning, "Dropped qos2 packet ~w for too many awaiting_rel", [PacketId]), + ok = emqx_metrics:inc('messages.qos2.dropped'), + {error, ?RC_RECEIVE_MAXIMUM_EXCEEDED} end; -handle_info({timeout, Timer, expired}, State = #state{expiry_timer = Timer}) -> - ?LOG(info, "Expired, shutdown now.", []), - shutdown(expired, State); +%% Publish QoS0/1 directly +publish(_PacketId, Msg, _Session) -> + {ok, emqx_broker:publish(Msg)}. -handle_info({timeout, Timer, will_delay}, State = #state{will_msg = WillMsg, will_delay_timer = Timer}) -> - send_willmsg(WillMsg), - {noreply, State#state{will_msg = undefined}}; - -%% ConnPid is shutting down by the supervisor. -handle_info({'EXIT', ConnPid, Reason}, #state{conn_pid = ConnPid}) - when Reason =:= killed; Reason =:= shutdown -> - exit(Reason); - -handle_info({'EXIT', ConnPid, Reason}, State = #state{will_msg = WillMsg, expiry_interval = 0, conn_pid = ConnPid}) -> - case Reason of - normal -> - ignore; - _ -> - send_willmsg(WillMsg) - end, - shutdown(Reason, State#state{will_msg = undefined, conn_pid = undefined}); - -handle_info({'EXIT', ConnPid, Reason}, State = #state{conn_pid = ConnPid}) -> - State1 = case Reason of - normal -> - State#state{will_msg = undefined}; - _ -> - ensure_will_delay_timer(State) - end, - {noreply, ensure_expire_timer(State1#state{conn_pid = undefined})}; - -handle_info({'EXIT', OldPid, _Reason}, State = #state{old_conn_pid = OldPid}) -> - %% ignore - {noreply, State#state{old_conn_pid = undefined}}; - -handle_info({'EXIT', Pid, Reason}, State = #state{conn_pid = ConnPid}) -> - ?LOG(error, "Unexpected EXIT: conn_pid=~p, exit_pid=~p, reason=~p", - [ConnPid, Pid, Reason]), - {noreply, State}; - -handle_info(Info, State) -> - ?LOG(error, "Unexpected info: ~p", [Info]), - {noreply, State}. - -terminate(Reason, #state{will_msg = WillMsg, - client_id = ClientId, - username = Username}) -> - send_willmsg(WillMsg), - ok = emqx_hooks:run('session.terminated', [#{client_id => ClientId, username => Username}, Reason]). - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -%%------------------------------------------------------------------------------ -%% Internal functions -%%------------------------------------------------------------------------------ - -is_connection_alive(#state{conn_pid = Pid}) -> - is_pid(Pid) andalso is_process_alive(Pid). - -%%------------------------------------------------------------------------------ -%% Suback and unsuback - -suback(_From, undefined, _ReasonCodes) -> - ignore; -suback(From, PacketId, ReasonCodes) -> - From ! {deliver, {suback, PacketId, ReasonCodes}}. - -unsuback(_From, undefined, _ReasonCodes) -> - ignore; -unsuback(From, PacketId, ReasonCodes) -> - From ! {deliver, {unsuback, PacketId, ReasonCodes}}. - -%%------------------------------------------------------------------------------ -%% Kickout old connection - -kick(_ClientId, undefined, _ConnPid) -> - ignore; -kick(_ClientId, ConnPid, ConnPid) -> - ignore; -kick(ClientId, OldConnPid, ConnPid) -> - unlink(OldConnPid), - OldConnPid ! {shutdown, conflict, {ClientId, ConnPid}}, - %% Clean noproc - receive {'EXIT', OldConnPid, _} -> ok after 1 -> ok end. - -%%------------------------------------------------------------------------------ -%% Replay or Retry Delivery - -%% Redeliver at once if force is true -retry_delivery(Force, State = #state{inflight = Inflight}) -> - case emqx_inflight:is_empty(Inflight) of - true -> State; - false -> - SortFun = fun({_, _, Ts1}, {_, _, Ts2}) -> Ts1 < Ts2 end, - Msgs = lists:sort(SortFun, emqx_inflight:values(Inflight)), - retry_delivery(Force, Msgs, os:timestamp(), State) - end. - -retry_delivery(_Force, [], _Now, State) -> - %% Retry again... - ensure_retry_timer(State); - -retry_delivery(Force, [{Type, Msg0, Ts} | Msgs], Now, - State = #state{zone = Zone, inflight = Inflight}) -> - Interval = get_env(Zone, retry_interval, 0), - %% Microseconds -> MilliSeconds - Age = timer:now_diff(Now, Ts) div 1000, - if - Force orelse (Age >= Interval) -> - Inflight1 = case {Type, Msg0} of - {publish, {PacketId, Msg}} -> - case emqx_message:is_expired(Msg) of - true -> - ok = emqx_metrics:inc('messages.expired'), - emqx_inflight:delete(PacketId, Inflight); - false -> - redeliver({PacketId, Msg}, State), - emqx_inflight:update(PacketId, {publish, {PacketId, Msg}, Now}, Inflight) - end; - {pubrel, PacketId} -> - redeliver({pubrel, PacketId}, State), - emqx_inflight:update(PacketId, {pubrel, PacketId, Now}, Inflight) - end, - retry_delivery(Force, Msgs, Now, State#state{inflight = Inflight1}); - true -> - ensure_retry_timer(Interval - max(0, Age), State) - end. - -%%------------------------------------------------------------------------------ -%% Send Will Message -%%------------------------------------------------------------------------------ - -send_willmsg(undefined) -> - ignore; -send_willmsg(WillMsg) -> - emqx_broker:publish(WillMsg). - -%%------------------------------------------------------------------------------ -%% Expire Awaiting Rel -%%------------------------------------------------------------------------------ - -expire_awaiting_rel(State = #state{awaiting_rel = AwaitingRel}) -> - case maps:size(AwaitingRel) of - 0 -> State; - _ -> expire_awaiting_rel(lists:keysort(2, maps:to_list(AwaitingRel)), os:timestamp(), State) - end. - -expire_awaiting_rel([], _Now, State) -> - State#state{await_rel_timer = undefined}; - -expire_awaiting_rel([{PacketId, Ts} | More], Now, - State = #state{zone = Zone, awaiting_rel = AwaitingRel}) -> - Timeout = get_env(Zone, await_rel_timeout), - case (timer:now_diff(Now, Ts) div 1000) of - Age when Age >= Timeout -> - ok = emqx_metrics:inc('messages.qos2.expired'), - ?LOG(warning, "Dropped qos2 packet ~s for await_rel_timeout", [PacketId]), - expire_awaiting_rel(More, Now, State#state{awaiting_rel = maps:remove(PacketId, AwaitingRel)}); - Age -> - ensure_await_rel_timer(Timeout - max(0, Age), State) - end. - -%%------------------------------------------------------------------------------ -%% Check awaiting rel -%%------------------------------------------------------------------------------ - -is_awaiting_full(_MaxAwaitingRel = 0, _AwaitingRel) -> +is_awaiting_full(#session{max_awaiting_rel = 0}) -> false; -is_awaiting_full(MaxAwaitingRel, AwaitingRel) -> - maps:size(AwaitingRel) >= MaxAwaitingRel. +is_awaiting_full(#session{awaiting_rel = AwaitingRel, + max_awaiting_rel = MaxLimit}) -> + maps:size(AwaitingRel) >= MaxLimit. -%%------------------------------------------------------------------------------ -%% Dispatch messages -%%------------------------------------------------------------------------------ +-compile({inline, [do_publish/3]}). +do_publish(PacketId, Msg = #message{timestamp = Ts}, + Session = #session{awaiting_rel = AwaitingRel}) -> + case maps:is_key(PacketId, AwaitingRel) of + false -> + DeliverResults = emqx_broker:publish(Msg), + AwaitingRel1 = maps:put(PacketId, Ts, AwaitingRel), + Session1 = Session#session{awaiting_rel = AwaitingRel1}, + {ok, DeliverResults, ensure_await_rel_timer(Session1)}; + true -> + {error, ?RC_PACKET_IDENTIFIER_IN_USE} + end. -handle_dispatch(Msgs, State = #state{inflight = Inflight, - client_id = ClientId, - username = Username, - subscriptions = SubMap}) -> - SessProps = #{client_id => ClientId, username => Username}, - %% Drain the mailbox and batch deliver - Msgs1 = Msgs ++ drain_m(batch_n(Inflight)), - %% Ack the messages for shared subscription - Msgs2 = maybe_ack_shared(Msgs1, State), - %% Process suboptions - Msgs3 = lists:foldr( - fun({Topic, Msg}, Acc) -> - SubOpts = find_subopts(Topic, SubMap), - case process_subopts(SubOpts, Msg, State) of - {ok, Msg1} -> [Msg1|Acc]; - ignore -> - emqx_hooks:run('message.dropped', [SessProps, Msg]), - Acc - end - end, [], Msgs2), - NState = batch_process(Msgs3, State), - noreply(ensure_stats_timer(NState)). +%%-------------------------------------------------------------------- +%% Client -> Broker: PUBACK +%%-------------------------------------------------------------------- + +-spec(puback(emqx_types:packet_id(), session()) + -> {ok, session()} | {ok, list(publish()), session()} | + {error, emqx_types:reason_code()}). +puback(PacketId, Session = #session{inflight = Inflight}) -> + case emqx_inflight:lookup(PacketId, Inflight) of + {value, {Msg, _Ts}} when is_record(Msg, message) -> + ok = emqx_hooks:run('message.acked', [Msg]), + Inflight1 = emqx_inflight:delete(PacketId, Inflight), + dequeue(Session#session{inflight = Inflight1}); + {value, {_OtherPub, _Ts}} -> + ?LOG(warning, "The PacketId has been used, PacketId: ~p", [PacketId]), + {error, ?RC_PACKET_IDENTIFIER_IN_USE}; + none -> + ?LOG(warning, "The PUBACK PacketId ~w is not found", [PacketId]), + ok = emqx_metrics:inc('packets.puback.missed'), + {error, ?RC_PACKET_IDENTIFIER_NOT_FOUND} + end. + +%%-------------------------------------------------------------------- +%% Client -> Broker: PUBREC +%%-------------------------------------------------------------------- + +-spec(pubrec(emqx_types:packet_id(), session()) + -> {ok, session()} | {error, emqx_types:reason_code()}). +pubrec(PacketId, Session = #session{inflight = Inflight}) -> + case emqx_inflight:lookup(PacketId, Inflight) of + {value, {Msg, _Ts}} when is_record(Msg, message) -> + ok = emqx_hooks:run('message.acked', [Msg]), + Inflight1 = emqx_inflight:update(PacketId, {pubrel, os:timestamp()}, Inflight), + {ok, Session#session{inflight = Inflight1}}; + {value, {pubrel, _Ts}} -> + ?LOG(warning, "The PUBREC ~w is duplicated", [PacketId]), + ok = emqx_metrics:inc('packets.pubrec.inuse'), + {error, ?RC_PACKET_IDENTIFIER_IN_USE}; + none -> + ?LOG(warning, "The PUBREC ~w is not found.", [PacketId]), + ok = emqx_metrics:inc('packets.pubrec.missed'), + {error, ?RC_PACKET_IDENTIFIER_NOT_FOUND} + end. + +%%-------------------------------------------------------------------- +%% Client -> Broker: PUBREL +%%-------------------------------------------------------------------- + +-spec(pubrel(emqx_types:packet_id(), session()) + -> {ok, session()} | {error, emqx_types:reason_code()}). +pubrel(PacketId, Session = #session{awaiting_rel = AwaitingRel}) -> + case maps:take(PacketId, AwaitingRel) of + {_Ts, AwaitingRel1} -> + {ok, Session#session{awaiting_rel = AwaitingRel1}}; + error -> + ?LOG(warning, "The PUBREL PacketId ~w is not found", [PacketId]), + ok = emqx_metrics:inc('packets.pubrel.missed'), + {error, ?RC_PACKET_IDENTIFIER_NOT_FOUND} + end. + +%%-------------------------------------------------------------------- +%% Client -> Broker: PUBCOMP +%%-------------------------------------------------------------------- + +-spec(pubcomp(emqx_types:packet_id(), session()) + -> {ok, session()} | {ok, list(publish()), session()} | + {error, emqx_types:reason_code()}). +pubcomp(PacketId, Session = #session{inflight = Inflight}) -> + case emqx_inflight:contain(PacketId, Inflight) of + true -> + Inflight1 = emqx_inflight:delete(PacketId, Inflight), + dequeue(Session#session{inflight = Inflight1}); + false -> + ?LOG(warning, "The PUBCOMP PacketId ~w is not found", [PacketId]), + ok = emqx_metrics:inc('packets.pubcomp.missed'), + {error, ?RC_PACKET_IDENTIFIER_NOT_FOUND} + end. + +%%-------------------------------------------------------------------- +%% Dequeue Msgs +%%-------------------------------------------------------------------- + +dequeue(Session = #session{inflight = Inflight, mqueue = Q}) -> + case emqx_mqueue:is_empty(Q) of + true -> {ok, Session}; + false -> + {Msgs, Q1} = dequeue(batch_n(Inflight), [], Q), + deliver(lists:reverse(Msgs), [], Session#session{mqueue = Q1}) + end. + +dequeue(Cnt, Msgs, Q) when Cnt =< 0 -> + {Msgs, Q}; + +dequeue(Cnt, Msgs, Q) -> + case emqx_mqueue:out(Q) of + {empty, _Q} -> {Msgs, Q}; + {{value, Msg}, Q1} -> + dequeue(Cnt-1, [Msg|Msgs], Q1) + end. batch_n(Inflight) -> case emqx_inflight:max_size(Inflight) of @@ -811,82 +474,56 @@ batch_n(Inflight) -> Sz -> Sz - emqx_inflight:size(Inflight) end. -drain_m(Cnt) -> - drain_m(Cnt, []). +%%-------------------------------------------------------------------- +%% Broker -> Client: Publish | Msg +%%-------------------------------------------------------------------- -drain_m(Cnt, Msgs) when Cnt =< 0 -> - lists:reverse(Msgs); -drain_m(Cnt, Msgs) -> - receive - {dispatch, Topic, Msg} when is_record(Msg, message)-> - drain_m(Cnt-1, [{Topic, Msg} | Msgs]); - {dispatch, Topic, InMsgs} when is_list(InMsgs) -> - Msgs1 = lists:foldl( - fun(Msg, Acc) -> - [{Topic, Msg} | Acc] - end, Msgs, InMsgs), - drain_m(Cnt-length(InMsgs), Msgs1) - after 0 -> - lists:reverse(Msgs) +deliver(Delivers, Session = #session{subscriptions = Subs}) + when is_list(Delivers) -> + Msgs = [enrich(get_subopts(Topic, Subs), Msg, Session) + || {deliver, Topic, Msg} <- Delivers], + deliver(Msgs, [], Session). + +deliver([], Publishes, Session) -> + {ok, lists:reverse(Publishes), Session}; + +deliver([Msg = #message{qos = ?QOS_0}|More], Acc, Session) -> + deliver(More, [{publish, undefined, Msg}|Acc], Session); + +deliver([Msg = #message{qos = QoS}|More], Acc, + Session = #session{next_pkt_id = PacketId, inflight = Inflight}) + when QoS =:= ?QOS_1 orelse QoS =:= ?QOS_2 -> + case emqx_inflight:is_full(Inflight) of + true -> + deliver(More, Acc, enqueue(Msg, Session)); + false -> + Publish = {publish, PacketId, Msg}, + Session1 = await(PacketId, Msg, Session), + deliver(More, [Publish|Acc], next_pkt_id(Session1)) end. -%% Ack or nack the messages of shared subscription? -maybe_ack_shared(Msgs, State) when is_list(Msgs) -> - lists:foldr( - fun({Topic, Msg}, Acc) -> - case maybe_ack_shared(Msg, State) of - ok -> Acc; - Msg1 -> [{Topic, Msg1}|Acc] - end - end, [], Msgs); +enqueue(Msg, Session = #session{mqueue = Q}) -> + emqx_pd:update_counter(enqueue_stats, 1), + {Dropped, NewQ} = emqx_mqueue:in(Msg, Q), + if + Dropped =/= undefined -> + %% TODO:... + %% SessProps = #{client_id => ClientId, username => Username}, + ok; %% = emqx_hooks:run('message.dropped', [SessProps, Dropped]); + true -> ok + end, + Session#session{mqueue = NewQ}. -maybe_ack_shared(Msg, State) -> - case emqx_shared_sub:is_ack_required(Msg) of - true -> do_ack_shared(Msg, State); - false -> Msg - end. +%%-------------------------------------------------------------------- +%% Awaiting ACK for QoS1/QoS2 Messages +%%-------------------------------------------------------------------- -do_ack_shared(Msg, State = #state{inflight = Inflight}) -> - case {is_connection_alive(State), - emqx_inflight:is_full(Inflight)} of - {false, _} -> - %% Require ack, but we do not have connection - %% negative ack the message so it can try the next subscriber in the group - emqx_shared_sub:nack_no_connection(Msg); - {_, true} -> - emqx_shared_sub:maybe_nack_dropped(Msg); - _ -> - %% Ack QoS1/QoS2 messages when message is delivered to connection. - %% NOTE: NOT to wait for PUBACK because: - %% The sender is monitoring this session process, - %% if the message is delivered to client but connection or session crashes, - %% sender will try to dispatch the message to the next shared subscriber. - %% This violates spec as QoS2 messages are not allowed to be sent to more - %% than one member in the group. - emqx_shared_sub:maybe_ack(Msg) - end. +await(PacketId, Msg, Session = #session{inflight = Inflight}) -> + Inflight1 = emqx_inflight:insert( + PacketId, {Msg, os:timestamp()}, Inflight), + ensure_retry_timer(Session#session{inflight = Inflight1}). -process_subopts([], Msg, _State) -> - {ok, Msg}; -process_subopts([{nl, 1}|_Opts], #message{from = ClientId}, #state{client_id = ClientId}) -> - ignore; -process_subopts([{nl, _}|Opts], Msg, State) -> - process_subopts(Opts, Msg, State); -process_subopts([{qos, SubQoS}|Opts], Msg = #message{qos = PubQoS}, State = #state{zone = Zone}) -> - case get_env(Zone, upgrade_qos, false) of - true -> process_subopts(Opts, Msg#message{qos = max(SubQoS, PubQoS)}, State); - false -> process_subopts(Opts, Msg#message{qos = min(SubQoS, PubQoS)}, State) - end; -process_subopts([{rap, _Rap}|Opts], Msg = #message{flags = Flags, headers = #{retained := true}}, State = #state{}) -> - process_subopts(Opts, Msg#message{flags = maps:put(retain, true, Flags)}, State); -process_subopts([{rap, 0}|Opts], Msg = #message{flags = Flags}, State = #state{}) -> - process_subopts(Opts, Msg#message{flags = maps:put(retain, false, Flags)}, State); -process_subopts([{rap, _}|Opts], Msg, State) -> - process_subopts(Opts, Msg, State); -process_subopts([{subid, SubId}|Opts], Msg, State) -> - process_subopts(Opts, emqx_message:set_header('Subscription-Identifier', SubId, Msg), State). - -find_subopts(Topic, SubMap) -> +get_subopts(Topic, SubMap) -> case maps:find(Topic, SubMap) of {ok, #{nl := Nl, qos := QoS, rap := Rap, subid := SubId}} -> [{nl, Nl}, {qos, QoS}, {rap, Rap}, {subid, SubId}]; @@ -895,238 +532,157 @@ find_subopts(Topic, SubMap) -> error -> [] end. -batch_process(Msgs, State) -> - {ok, Publishes, NState} = process_msgs(Msgs, [], State), - ok = batch_deliver(Publishes, NState), - maybe_gc(msg_cnt(Msgs), NState). +enrich([], Msg, _Session) -> + Msg; +%%enrich([{nl, 1}|_Opts], #message{from = ClientId}, #session{client_id = ClientId}) -> +%% ignore; +enrich([{nl, _}|Opts], Msg, Session) -> + enrich(Opts, Msg, Session); +enrich([{qos, SubQoS}|Opts], Msg = #message{qos = PubQoS}, Session = #session{upgrade_qos= true}) -> + 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(Opts, Msg#message{flags = maps:put(retain, false, Flags)}, Session); +enrich([{rap, _}|Opts], Msg, Session) -> + enrich(Opts, Msg, Session); +enrich([{subid, SubId}|Opts], Msg, Session) -> + enrich(Opts, emqx_message:set_header('Subscription-Identifier', SubId, Msg), Session). -process_msgs([], Publishes, State) -> - {ok, lists:reverse(Publishes), State}; +%%-------------------------------------------------------------------- +%% Handle timeout +%%-------------------------------------------------------------------- -process_msgs([Msg|Msgs], Publishes, State) -> - case process_msg(Msg, State) of - {ok, Publish, NState} -> - process_msgs(Msgs, [Publish|Publishes], NState); - {ignore, NState} -> - process_msgs(Msgs, Publishes, NState) - end. +-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}); -%% Enqueue message if the client has been disconnected -process_msg(Msg, State = #state{conn_pid = undefined}) -> - {ignore, enqueue_msg(Msg, State)}; +timeout(TRef, check_awaiting_rel, Session = #session{await_rel_timer = TRef}) -> + expire_awaiting_rel(Session); -%% Prepare the qos0 message delivery -process_msg(Msg = #message{qos = ?QOS_0}, State) -> - {ok, {publish, undefined, Msg}, State}; +timeout(TRef, Msg, Session) -> + ?LOG(error, "unexpected timeout - ~p: ~p", [TRef, Msg]), + {ok, Session}. -process_msg(Msg = #message{qos = QoS}, - State = #state{next_pkt_id = PacketId, inflight = Inflight}) - when QoS =:= ?QOS_1 orelse QoS =:= ?QOS_2 -> - case emqx_inflight:is_full(Inflight) of - true -> - {ignore, enqueue_msg(Msg, State)}; +%%-------------------------------------------------------------------- +%% 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}) -> + case emqx_inflight:is_empty(Inflight) of + true -> {ok, Session}; false -> - Publish = {publish, PacketId, Msg}, - NState = await(PacketId, Msg, State), - {ok, Publish, next_pkt_id(NState)} + SortFun = fun({_, {_, Ts1}}, {_, {_, Ts2}}) -> Ts1 < Ts2 end, + Msgs = lists:sort(SortFun, emqx_inflight:to_list(Inflight)), + retry_delivery(Msgs, os:timestamp(), [], Session) end. -enqueue_msg(Msg, State = #state{mqueue = Q, client_id = ClientId, username = Username}) -> - emqx_pd:update_counter(enqueue_stats, 1), - {Dropped, NewQ} = emqx_mqueue:in(Msg, Q), +retry_delivery([], _Now, Acc, Session) -> + %% Retry again... + {ok, lists:reverse(Acc), ensure_retry_timer(Session)}; + +retry_delivery([{PacketId, {Val, Ts}}|More], Now, Acc, + Session = #session{retry_interval = Interval, inflight = Inflight}) -> + %% Microseconds -> MilliSeconds + Age = timer:now_diff(Now, Ts) div 1000, if - Dropped =/= undefined -> - SessProps = #{client_id => ClientId, username => Username}, - ok = emqx_hooks:run('message.dropped', [SessProps, Dropped]); - true -> ok - end, - State#state{mqueue = NewQ}. + Age >= Interval -> + {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)} + end. -%%------------------------------------------------------------------------------ -%% Deliver -%%------------------------------------------------------------------------------ - -redeliver({PacketId, Msg = #message{qos = QoS}}, State) when QoS =/= ?QOS_0 -> - Msg1 = emqx_message:set_flag(dup, Msg), - do_deliver(PacketId, Msg1, State); - -redeliver({pubrel, PacketId}, #state{deliver_fun = DeliverFun}) -> - DeliverFun({pubrel, PacketId}). - -do_deliver(PacketId, Msg, #state{deliver_fun = DeliverFun}) -> - emqx_pd:update_counter(deliver_stats, 1), - DeliverFun({publish, PacketId, Msg}). - -batch_deliver(Publishes, #state{deliver_fun = DeliverFun}) -> - emqx_pd:update_counter(deliver_stats, length(Publishes)), - DeliverFun(Publishes). - -%%------------------------------------------------------------------------------ -%% Awaiting ACK for QoS1/QoS2 Messages -%%------------------------------------------------------------------------------ - -await(PacketId, Msg, State = #state{inflight = Inflight}) -> - Inflight1 = emqx_inflight:insert( - PacketId, {publish, {PacketId, Msg}, os:timestamp()}, Inflight), - ensure_retry_timer(State#state{inflight = Inflight1}). - -acked(puback, PacketId, State = #state{client_id = ClientId, username = Username, inflight = Inflight}) -> - case emqx_inflight:lookup(PacketId, Inflight) of - {value, {publish, {_, Msg}, _Ts}} -> - ok = emqx_hooks:run('message.acked', [#{client_id => ClientId, username => Username}, Msg]), - State#state{inflight = emqx_inflight:delete(PacketId, Inflight)}; - none -> - ?LOG(warning, "Duplicated PUBACK PacketId ~w", [PacketId]), - State - end; - -acked(pubrec, PacketId, State = #state{client_id = ClientId, username = Username, inflight = Inflight}) -> - case emqx_inflight:lookup(PacketId, Inflight) of - {value, {publish, {_, Msg}, _Ts}} -> - ok = emqx_hooks:run('message.acked', [#{client_id => ClientId, username => Username}, Msg]), - State#state{inflight = emqx_inflight:update(PacketId, {pubrel, PacketId, os:timestamp()}, Inflight)}; - {value, {pubrel, PacketId, _Ts}} -> - ?LOG(warning, "Duplicated PUBREC PacketId ~w", [PacketId]), - State; - none -> - ?LOG(warning, "Unexpected PUBREC PacketId ~w", [PacketId]), - State - end; - -acked(pubcomp, PacketId, State = #state{inflight = Inflight}) -> - State#state{inflight = emqx_inflight:delete(PacketId, Inflight)}. - -%%------------------------------------------------------------------------------ -%% Dequeue -%%------------------------------------------------------------------------------ - -%% Do nothing if client is disconnected -dequeue(State = #state{conn_pid = undefined}) -> - State; - -dequeue(State = #state{inflight = Inflight, mqueue = Q}) -> - case emqx_mqueue:is_empty(Q) - orelse emqx_inflight:is_full(Inflight) of - true -> State; +retry_delivery(PacketId, Msg, Now, Acc, Inflight) when is_record(Msg, message) -> + case emqx_message:is_expired(Msg) of + true -> + ok = emqx_metrics:inc('messages.expired'), + {Acc, emqx_inflight:delete(PacketId, Inflight)}; false -> - {Msgs, Q1} = drain_q(batch_n(Inflight), [], Q), - batch_process(lists:reverse(Msgs), State#state{mqueue = Q1}) - end. - -drain_q(Cnt, Msgs, Q) when Cnt =< 0 -> - {Msgs, Q}; - -drain_q(Cnt, Msgs, Q) -> - case emqx_mqueue:out(Q) of - {empty, _Q} -> {Msgs, Q}; - {{value, Msg}, Q1} -> - drain_q(Cnt-1, [Msg|Msgs], Q1) - end. - -%%------------------------------------------------------------------------------ -%% Ensure timers - -ensure_await_rel_timer(State = #state{zone = Zone, - await_rel_timer = undefined}) -> - Timeout = get_env(Zone, await_rel_timeout), - ensure_await_rel_timer(Timeout, State); -ensure_await_rel_timer(State) -> - State. - -ensure_await_rel_timer(Timeout, State = #state{await_rel_timer = undefined}) -> - State#state{await_rel_timer = emqx_misc:start_timer(Timeout, check_awaiting_rel)}; -ensure_await_rel_timer(_Timeout, State) -> - State. - -ensure_retry_timer(State = #state{zone = Zone, retry_timer = undefined}) -> - Interval = get_env(Zone, retry_interval, 0), - ensure_retry_timer(Interval, State); -ensure_retry_timer(State) -> - State. - -ensure_retry_timer(Interval, State = #state{retry_timer = undefined}) -> - State#state{retry_timer = emqx_misc:start_timer(Interval, retry_delivery)}; -ensure_retry_timer(_Timeout, State) -> - State. - -ensure_expire_timer(State = #state{expiry_interval = Interval}) - when Interval > 0 andalso Interval =/= 16#ffffffff -> - State#state{expiry_timer = emqx_misc:start_timer(Interval * 1000, expired)}; -ensure_expire_timer(State) -> - State. - -ensure_will_delay_timer(State = #state{will_msg = #message{headers = #{'Will-Delay-Interval' := WillDelayInterval}}}) -> - State#state{will_delay_timer = emqx_misc:start_timer(WillDelayInterval * 1000, will_delay)}; -ensure_will_delay_timer(State = #state{will_msg = WillMsg}) -> - send_willmsg(WillMsg), - State#state{will_msg = undefined}. - -ensure_stats_timer(State = #state{zone = Zone, - stats_timer = undefined, - idle_timeout = IdleTimeout}) -> - case get_env(Zone, enable_stats, true) of - true -> State#state{stats_timer = emqx_misc:start_timer(IdleTimeout, emit_stats)}; - _Other -> State + {[{publish, PacketId, Msg}|Acc], + emqx_inflight:update(PacketId, {Msg, Now}, Inflight)} end; -ensure_stats_timer(State) -> - State. -%%------------------------------------------------------------------------------ -%% Next Packet Id +retry_delivery(PacketId, pubrel, Now, Acc, Inflight) -> + Inflight1 = emqx_inflight:update(PacketId, {pubrel, Now}, Inflight), + {[{pubrel, PacketId}|Acc], Inflight1}. -next_pkt_id(State = #state{next_pkt_id = 16#FFFF}) -> - State#state{next_pkt_id = 1}; +%%-------------------------------------------------------------------- +%% Ensure await_rel timer +%%-------------------------------------------------------------------- -next_pkt_id(State = #state{next_pkt_id = Id}) -> - State#state{next_pkt_id = Id + 1}. +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. -%%------------------------------------------------------------------------------ -%% Maybe GC +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. -msg_cnt(Msgs) -> - lists:foldl(fun(Msg, {Cnt, Oct}) -> - {Cnt+1, Oct+msg_size(Msg)} - end, {0, 0}, Msgs). +%%-------------------------------------------------------------------- +%% Expire Awaiting Rel +%%-------------------------------------------------------------------- -%% Take only the payload size into account, add other fields if necessary -msg_size(#message{payload = Payload}) -> payload_size(Payload). - -%% Payload should be binary(), but not 100% sure. Need dialyzer! -payload_size(Payload) -> erlang:iolist_size(Payload). - -maybe_gc(_, State = #state{gc_state = undefined}) -> - State; -maybe_gc({Cnt, Oct}, State = #state{gc_state = GCSt}) -> - {_, GCSt1} = emqx_gc:run(Cnt, Oct, GCSt), - State#state{gc_state = GCSt1}. - -%%------------------------------------------------------------------------------ -%% Helper functions - -reply({Reply, State}) -> - reply(Reply, State). - -reply(Reply, State) -> - {reply, Reply, State}. - -noreply(State) -> - {noreply, State}. - -shutdown(Reason, State) -> - {stop, {shutdown, Reason}, State}. - -do_subscribe(ClientId, Username, Topic, SubOpts, SubMap) -> - case maps:find(Topic, SubMap) of - {ok, SubOpts} -> - ok = emqx_hooks:run('session.subscribed', [#{client_id => ClientId, username => Username}, Topic, SubOpts#{first => false}]), - SubMap; - {ok, _SubOpts} -> - emqx_broker:set_subopts(Topic, SubOpts), - %% Why??? - ok = emqx_hooks:run('session.subscribed', [#{client_id => ClientId, username => Username}, Topic, SubOpts#{first => false}]), - maps:put(Topic, SubOpts, SubMap); - error -> - emqx_broker:subscribe(Topic, ClientId, SubOpts), - ok = emqx_hooks:run('session.subscribed', [#{client_id => ClientId, username => Username}, Topic, SubOpts#{first => true}]), - maps:put(Topic, SubOpts, SubMap) +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) end. + +expire_awaiting_rel([], _Now, Session) -> + {ok, Session#session{await_rel_timer = undefined}}; + +expire_awaiting_rel([{PacketId, Ts} | More], Now, + Session = #session{awaiting_rel = AwaitingRel, + await_rel_timeout = Timeout}) -> + case (timer:now_diff(Now, Ts) div 1000) of + Age when Age >= Timeout -> + ok = emqx_metrics:inc('messages.qos2.expired'), + ?LOG(warning, "Dropped qos2 packet ~s for await_rel_timeout", [PacketId]), + 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)} + end. + +%%-------------------------------------------------------------------- +%% Next Packet Id +%%-------------------------------------------------------------------- + +next_pkt_id(Session = #session{next_pkt_id = 16#FFFF}) -> + Session#session{next_pkt_id = 1}; + +next_pkt_id(Session = #session{next_pkt_id = Id}) -> + Session#session{next_pkt_id = Id + 1}. + +%%--------------------------------------------------------------------- +%% For Test case +%%--------------------------------------------------------------------- + + +set_pkt_id(Session, PktId) -> + Session#session{next_pkt_id = PktId}. diff --git a/src/emqx_session_sup.erl b/src/emqx_session_sup.erl deleted file mode 100644 index 8c176edb2..000000000 --- a/src/emqx_session_sup.erl +++ /dev/null @@ -1,267 +0,0 @@ -%% 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_session_sup). - --behaviour(gen_server). - --include("logger.hrl"). --include("types.hrl"). - --logger_header("[Session Supervisor]"). - --export([start_link/1]). - --export([ start_session/1 - , count_sessions/0 - ]). - -%% gen_server callbacks --export([ init/1 - , handle_call/3 - , handle_cast/2 - , handle_info/2 - , terminate/2 - , code_change/3 - ]). - --type(shutdown() :: brutal_kill | infinity | pos_integer()). - --record(state, - { sessions :: #{pid() => emqx_types:client_id()} - , mfargs :: mfa() - , shutdown :: shutdown() - , clean_down :: fun() - }). - --define(SUP, ?MODULE). --define(BATCH_EXIT, 100000). - -%% @doc Start session supervisor. --spec(start_link(map()) -> startlink_ret()). -start_link(SessSpec) when is_map(SessSpec) -> - gen_server:start_link({local, ?SUP}, ?MODULE, [SessSpec], []). - -%%------------------------------------------------------------------------------ -%% API -%%------------------------------------------------------------------------------ - -%% @doc Start a session. --spec(start_session(map()) -> startlink_ret()). -start_session(SessAttrs) -> - gen_server:call(?SUP, {start_session, SessAttrs}, infinity). - -%% @doc Count sessions. --spec(count_sessions() -> non_neg_integer()). -count_sessions() -> - gen_server:call(?SUP, count_sessions, infinity). - -%%------------------------------------------------------------------------------ -%% gen_server callbacks -%%------------------------------------------------------------------------------ - -init([Spec]) -> - process_flag(trap_exit, true), - MFA = maps:get(start, Spec), - Shutdown = maps:get(shutdown, Spec, brutal_kill), - CleanDown = maps:get(clean_down, Spec, undefined), - State = #state{sessions = #{}, - mfargs = MFA, - shutdown = Shutdown, - clean_down = CleanDown - }, - {ok, State}. - -handle_call({start_session, SessAttrs = #{client_id := ClientId}}, _From, - State = #state{sessions = SessMap, mfargs = {M, F, Args}}) -> - try erlang:apply(M, F, [SessAttrs | Args]) of - {ok, Pid} -> - reply({ok, Pid}, State#state{sessions = maps:put(Pid, ClientId, SessMap)}); - ignore -> - reply(ignore, State); - {error, Reason} -> - reply({error, Reason}, State) - catch - _:Error:Stk -> - ?LOG(error, "Failed to start session ~p: ~p, stacktrace:~n~p", - [ClientId, Error, Stk]), - reply({error, Error}, State) - end; - -handle_call(count_sessions, _From, State = #state{sessions = SessMap}) -> - {reply, maps:size(SessMap), State}; - -handle_call(Req, _From, State) -> - ?LOG(error, "Unexpected call: ~p", [Req]), - {reply, ignored, State}. - -handle_cast(Msg, State) -> - ?LOG(error, "Unexpected cast: ~p", [Msg]), - {noreply, State}. - -handle_info({'EXIT', Pid, _Reason}, State = #state{sessions = SessMap, clean_down = CleanDown}) -> - SessPids = [Pid | drain_exit(?BATCH_EXIT, [])], - {SessItems, SessMap1} = erase_all(SessPids, SessMap), - (CleanDown =:= undefined) - orelse emqx_pool:async_submit( - fun lists:foreach/2, [CleanDown, SessItems]), - {noreply, State#state{sessions = SessMap1}}; - -handle_info(Info, State) -> - ?LOG(error, "Unexpected info: ~p", [Info]), - {noreply, State}. - -terminate(_Reason, State) -> - terminate_children(State). - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -%%------------------------------------------------------------------------------ -%% Internal functions -%%------------------------------------------------------------------------------ - -drain_exit(0, Acc) -> - lists:reverse(Acc); -drain_exit(Cnt, Acc) -> - receive - {'EXIT', Pid, _Reason} -> - drain_exit(Cnt - 1, [Pid|Acc]) - after 0 -> - lists:reverse(Acc) - end. - -erase_all(Pids, Map) -> - lists:foldl( - fun(Pid, {Acc, M}) -> - case maps:take(Pid, M) of - {Val, M1} -> - {[{Val, Pid}|Acc], M1}; - error -> - {Acc, M} - end - end, {[], Map}, Pids). - -terminate_children(State = #state{sessions = SessMap, shutdown = Shutdown}) -> - {Pids, EStack0} = monitor_children(SessMap), - Sz = sets:size(Pids), - EStack = - case Shutdown of - brutal_kill -> - sets:fold(fun(P, _) -> exit(P, kill) end, ok, Pids), - wait_children(Shutdown, Pids, Sz, undefined, EStack0); - infinity -> - sets:fold(fun(P, _) -> exit(P, shutdown) end, ok, Pids), - wait_children(Shutdown, Pids, Sz, undefined, EStack0); - Time when is_integer(Time) -> - sets:fold(fun(P, _) -> exit(P, shutdown) end, ok, Pids), - TRef = erlang:start_timer(Time, self(), kill), - wait_children(Shutdown, Pids, Sz, TRef, EStack0) - end, - %% Unroll stacked errors and report them - dict:fold(fun(Reason, Pid, _) -> - report_error(connection_shutdown_error, Reason, Pid, State) - end, ok, EStack). - -monitor_children(SessMap) -> - lists:foldl( - fun(Pid, {Pids, EStack}) -> - case monitor_child(Pid) of - ok -> - {sets:add_element(Pid, Pids), EStack}; - {error, normal} -> - {Pids, EStack}; - {error, Reason} -> - {Pids, dict:append(Reason, Pid, EStack)} - end - end, {sets:new(), dict:new()}, maps:keys(SessMap)). - -%% Help function to shutdown/2 switches from link to monitor approach -monitor_child(Pid) -> - %% Do the monitor operation first so that if the child dies - %% before the monitoring is done causing a 'DOWN'-message with - %% reason noproc, we will get the real reason in the 'EXIT'-message - %% unless a naughty child has already done unlink... - erlang:monitor(process, Pid), - unlink(Pid), - - receive - %% If the child dies before the unlik we must empty - %% the mail-box of the 'EXIT'-message and the 'DOWN'-message. - {'EXIT', Pid, Reason} -> - receive - {'DOWN', _, process, Pid, _} -> - {error, Reason} - end - after 0 -> - %% If a naughty child did unlink and the child dies before - %% monitor the result will be that shutdown/2 receives a - %% 'DOWN'-message with reason noproc. - %% If the child should die after the unlink there - %% will be a 'DOWN'-message with a correct reason - %% that will be handled in shutdown/2. - ok - end. - -wait_children(_Shutdown, _Pids, 0, undefined, EStack) -> - EStack; -wait_children(_Shutdown, _Pids, 0, TRef, EStack) -> - %% If the timer has expired before its cancellation, we must empty the - %% mail-box of the 'timeout'-message. - erlang:cancel_timer(TRef), - receive - {timeout, TRef, kill} -> - EStack - after 0 -> - EStack - end; - -%%TODO: Copied from supervisor.erl, rewrite it later. -wait_children(brutal_kill, Pids, Sz, TRef, EStack) -> - receive - {'DOWN', _MRef, process, Pid, killed} -> - wait_children(brutal_kill, sets:del_element(Pid, Pids), Sz-1, TRef, EStack); - - {'DOWN', _MRef, process, Pid, Reason} -> - wait_children(brutal_kill, sets:del_element(Pid, Pids), - Sz-1, TRef, dict:append(Reason, Pid, EStack)) - end; - -wait_children(Shutdown, Pids, Sz, TRef, EStack) -> - receive - {'DOWN', _MRef, process, Pid, shutdown} -> - wait_children(Shutdown, sets:del_element(Pid, Pids), Sz-1, TRef, EStack); - {'DOWN', _MRef, process, Pid, normal} -> - wait_children(Shutdown, sets:del_element(Pid, Pids), Sz-1, TRef, EStack); - {'DOWN', _MRef, process, Pid, Reason} -> - wait_children(Shutdown, sets:del_element(Pid, Pids), Sz-1, - TRef, dict:append(Reason, Pid, EStack)); - {timeout, TRef, kill} -> - sets:fold(fun(P, _) -> exit(P, kill) end, ok, Pids), - wait_children(Shutdown, Pids, Sz-1, undefined, EStack) - end. - -report_error(Error, Reason, Pid, #state{mfargs = MFA}) -> - SupName = list_to_atom("esockd_connection_sup - " ++ pid_to_list(self())), - ErrorMsg = [{supervisor, SupName}, - {errorContext, Error}, - {reason, Reason}, - {offender, [{pid, Pid}, - {name, connection}, - {mfargs, MFA}]}], - error_logger:error_report(supervisor_report, ErrorMsg). - -reply(Repy, State) -> - {reply, Repy, State}. - diff --git a/src/emqx_shared_sub.erl b/src/emqx_shared_sub.erl index 3b0d48189..66be8ee81 100644 --- a/src/emqx_shared_sub.erl +++ b/src/emqx_shared_sub.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_shared_sub). @@ -67,11 +69,12 @@ -define(no_ack, no_ack). -record(state, {pmon}). + -record(emqx_shared_subscription, {group, topic, subpid}). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Mnesia bootstrap -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- mnesia(boot) -> ok = ekka_mnesia:create_table(?TAB, [ @@ -83,9 +86,9 @@ mnesia(boot) -> mnesia(copy) -> ok = ekka_mnesia:copy_table(?TAB). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% API -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- -spec(start_link() -> startlink_ret()). start_link() -> @@ -103,19 +106,19 @@ record(Group, Topic, SubPid) -> #emqx_shared_subscription{group = Group, topic = Topic, subpid = SubPid}. -spec(dispatch(emqx_topic:group(), emqx_topic:topic(), emqx_types:delivery()) - -> emqx_types:delivery()). + -> emqx_types:deliver_result()). dispatch(Group, Topic, Delivery) -> dispatch(Group, Topic, Delivery, _FailedSubs = []). -dispatch(Group, Topic, Delivery = #delivery{message = Msg, results = Results}, FailedSubs) -> +dispatch(Group, Topic, Delivery = #delivery{message = Msg}, FailedSubs) -> #message{from = ClientId} = Msg, case pick(strategy(), ClientId, Group, Topic, FailedSubs) of false -> - Delivery; + {error, no_subscribers}; {Type, SubPid} -> case do_dispatch(SubPid, Topic, Msg, Type) of ok -> - Delivery#delivery{results = [{dispatch, {Group, Topic}, 1} | Results]}; + ok; {error, _Reason} -> %% Failed to dispatch to this sub, try next. dispatch(Group, Topic, Delivery, [SubPid | FailedSubs]) @@ -132,7 +135,7 @@ ack_enabled() -> do_dispatch(SubPid, Topic, Msg, _Type) when SubPid =:= self() -> %% Deadlock otherwise - _ = erlang:send(SubPid, {dispatch, Topic, Msg}), + _ = erlang:send(SubPid, {deliver, Topic, Msg}), ok; do_dispatch(SubPid, Topic, Msg, Type) -> dispatch_per_qos(SubPid, Topic, Msg, Type). @@ -140,18 +143,18 @@ do_dispatch(SubPid, Topic, Msg, Type) -> %% return either 'ok' (when everything is fine) or 'error' dispatch_per_qos(SubPid, Topic, #message{qos = ?QOS_0} = Msg, _Type) -> %% For QoS 0 message, send it as regular dispatch - _ = erlang:send(SubPid, {dispatch, Topic, Msg}), + _ = erlang:send(SubPid, {deliver, Topic, Msg}), ok; dispatch_per_qos(SubPid, Topic, Msg, retry) -> %% Retry implies all subscribers nack:ed, send again without ack - _ = erlang:send(SubPid, {dispatch, Topic, Msg}), + _ = erlang:send(SubPid, {deliver, Topic, Msg}), ok; dispatch_per_qos(SubPid, Topic, Msg, fresh) -> case ack_enabled() of true -> dispatch_with_ack(SubPid, Topic, Msg); false -> - _ = erlang:send(SubPid, {dispatch, Topic, Msg}), + _ = erlang:send(SubPid, {deliver, Topic, Msg}), ok end. @@ -159,7 +162,7 @@ dispatch_with_ack(SubPid, Topic, Msg) -> %% For QoS 1/2 message, expect an ack Ref = erlang:monitor(process, SubPid), Sender = self(), - _ = erlang:send(SubPid, {dispatch, Topic, with_ack_ref(Msg, {Sender, Ref})}), + _ = erlang:send(SubPid, {deliver, Topic, with_ack_ref(Msg, {Sender, Ref})}), Timeout = case Msg#message.qos of ?QOS_1 -> timer:seconds(?SHARED_SUB_QOS1_DISPATCH_TIMEOUT_SECONDS); ?QOS_2 -> infinity @@ -275,12 +278,12 @@ do_pick_subscriber(Group, Topic, round_robin, _ClientId, Count) -> subscribers(Group, Topic) -> ets:select(?TAB, [{{emqx_shared_subscription, Group, Topic, '$1'}, [], ['$1']}]). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% gen_server callbacks -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- init([]) -> - mnesia:subscribe({table, ?TAB, simple}), + {ok, _} = mnesia:subscribe({table, ?TAB, simple}), {atomic, PMon} = mnesia:transaction(fun init_monitors/0), ok = emqx_tables:new(?SHARED_SUBS, [protected, bag]), ok = emqx_tables:new(?ALIVE_SUBS, [protected, set, {read_concurrency, true}]), @@ -345,9 +348,9 @@ terminate(_Reason, _State) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Internal functions -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% keep track of alive remote pids maybe_insert_alive_tab(Pid) when ?IS_LOCAL_PID(Pid) -> ok; diff --git a/src/emqx_sm.erl b/src/emqx_sm.erl deleted file mode 100644 index f6f2fbf47..000000000 --- a/src/emqx_sm.erl +++ /dev/null @@ -1,298 +0,0 @@ -%% 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_sm). - --behaviour(gen_server). - --include("emqx.hrl"). --include("logger.hrl"). --include("types.hrl"). - --logger_header("[SM]"). - -%% APIs --export([start_link/0]). - --export([ open_session/1 - , close_session/1 - , resume_session/2 - , discard_session/1 - , discard_session/2 - , register_session/1 - , register_session/2 - , unregister_session/1 - , unregister_session/2 - ]). - --export([ get_session_attrs/1 - , get_session_attrs/2 - , set_session_attrs/2 - , set_session_attrs/3 - , get_session_stats/1 - , get_session_stats/2 - , set_session_stats/2 - , set_session_stats/3 - ]). - --export([lookup_session_pids/1]). - -%% Internal functions for rpc --export([dispatch/3]). - -%% Internal function for stats --export([stats_fun/0]). - -%% Internal function for emqx_session_sup --export([clean_down/1]). - -%% gen_server callbacks --export([ init/1 - , handle_call/3 - , handle_cast/2 - , handle_info/2 - , terminate/2 - , code_change/3 - ]). - --define(SM, ?MODULE). - -%% ETS Tables for session management. --define(SESSION_TAB, emqx_session). --define(SESSION_P_TAB, emqx_session_p). --define(SESSION_ATTRS_TAB, emqx_session_attrs). --define(SESSION_STATS_TAB, emqx_session_stats). - --define(BATCH_SIZE, 100000). - -%%------------------------------------------------------------------------------ -%% APIs -%%------------------------------------------------------------------------------ - --spec(start_link() -> startlink_ret()). -start_link() -> - gen_server:start_link({local, ?SM}, ?MODULE, [], []). - -%% @doc Open a session. --spec(open_session(map()) -> {ok, pid()} | {ok, pid(), boolean()} | {error, term()}). -open_session(SessAttrs = #{clean_start := true, client_id := ClientId, conn_pid := ConnPid}) -> - CleanStart = fun(_) -> - ok = discard_session(ClientId, ConnPid), - emqx_session_sup:start_session(SessAttrs) - end, - emqx_sm_locker:trans(ClientId, CleanStart); - -open_session(SessAttrs = #{clean_start := false, client_id := ClientId}) -> - ResumeStart = fun(_) -> - case resume_session(ClientId, SessAttrs) of - {ok, SessPid} -> - {ok, SessPid, true}; - {error, not_found} -> - emqx_session_sup:start_session(SessAttrs) - end - end, - emqx_sm_locker:trans(ClientId, ResumeStart). - -%% @doc Discard all the sessions identified by the ClientId. --spec(discard_session(emqx_types:client_id()) -> ok). -discard_session(ClientId) when is_binary(ClientId) -> - discard_session(ClientId, self()). - --spec(discard_session(emqx_types:client_id(), pid()) -> ok). -discard_session(ClientId, ConnPid) when is_binary(ClientId) -> - lists:foreach( - fun(SessPid) -> - try emqx_session:discard(SessPid, ConnPid) - catch - _:Error:_Stk -> - unregister_session(ClientId, SessPid), - ?LOG(warning, "Failed to discard ~p: ~p", [SessPid, Error]) - end - end, lookup_session_pids(ClientId)). - -%% @doc Try to resume a session. --spec(resume_session(emqx_types:client_id(), map()) -> {ok, pid()} | {error, term()}). -resume_session(ClientId, SessAttrs = #{conn_pid := ConnPid}) -> - case lookup_session_pids(ClientId) of - [] -> {error, not_found}; - [SessPid] -> - ok = emqx_session:resume(SessPid, SessAttrs), - {ok, SessPid}; - SessPids -> - [SessPid|StalePids] = lists:reverse(SessPids), - ?LOG(error, "More than one session found: ~p", [SessPids]), - lists:foreach(fun(StalePid) -> - catch emqx_session:discard(StalePid, ConnPid) - end, StalePids), - ok = emqx_session:resume(SessPid, SessAttrs), - {ok, SessPid} - end. - -%% @doc Close a session. --spec(close_session(emqx_types:client_id() | pid()) -> ok). -close_session(ClientId) when is_binary(ClientId) -> - case lookup_session_pids(ClientId) of - [] -> ok; - [SessPid] -> close_session(SessPid); - SessPids -> lists:foreach(fun close_session/1, SessPids) - end; - -close_session(SessPid) when is_pid(SessPid) -> - emqx_session:close(SessPid). - -%% @doc Register a session. --spec(register_session(emqx_types:client_id()) -> ok). -register_session(ClientId) when is_binary(ClientId) -> - register_session(ClientId, self()). - --spec(register_session(emqx_types:client_id(), pid()) -> ok). -register_session(ClientId, SessPid) when is_binary(ClientId), is_pid(SessPid) -> - Session = {ClientId, SessPid}, - true = ets:insert(?SESSION_TAB, Session), - emqx_sm_registry:register_session(Session). - -%% @doc Unregister a session --spec(unregister_session(emqx_types:client_id()) -> ok). -unregister_session(ClientId) when is_binary(ClientId) -> - unregister_session(ClientId, self()). - --spec(unregister_session(emqx_types:client_id(), pid()) -> ok). -unregister_session(ClientId, SessPid) when is_binary(ClientId), is_pid(SessPid) -> - Session = {ClientId, SessPid}, - true = ets:delete(?SESSION_STATS_TAB, Session), - true = ets:delete(?SESSION_ATTRS_TAB, Session), - true = ets:delete_object(?SESSION_P_TAB, Session), - true = ets:delete_object(?SESSION_TAB, Session), - emqx_sm_registry:unregister_session(Session). - -%% @doc Get session attrs --spec(get_session_attrs(emqx_types:client_id()) -> list(emqx_session:attr())). -get_session_attrs(ClientId) when is_binary(ClientId) -> - case lookup_session_pids(ClientId) of - [] -> []; - [SessPid|_] -> get_session_attrs(ClientId, SessPid) - end. - --spec(get_session_attrs(emqx_types:client_id(), pid()) -> list(emqx_session:attr())). -get_session_attrs(ClientId, SessPid) when is_binary(ClientId), is_pid(SessPid) -> - emqx_tables:lookup_value(?SESSION_ATTRS_TAB, {ClientId, SessPid}, []). - -%% @doc Set session attrs --spec(set_session_attrs(emqx_types:client_id(), list(emqx_session:attr())) -> true). -set_session_attrs(ClientId, SessAttrs) when is_binary(ClientId) -> - set_session_attrs(ClientId, self(), SessAttrs). - --spec(set_session_attrs(emqx_types:client_id(), pid(), list(emqx_session:attr())) -> true). -set_session_attrs(ClientId, SessPid, SessAttrs) when is_binary(ClientId), is_pid(SessPid) -> - Session = {ClientId, SessPid}, - true = ets:insert(?SESSION_ATTRS_TAB, {Session, SessAttrs}), - proplists:get_value(clean_start, SessAttrs, true) orelse ets:insert(?SESSION_P_TAB, Session). - -%% @doc Get session stats --spec(get_session_stats(emqx_types:client_id()) -> list(emqx_stats:stats())). -get_session_stats(ClientId) when is_binary(ClientId) -> - case lookup_session_pids(ClientId) of - [] -> []; - [SessPid|_] -> - get_session_stats(ClientId, SessPid) - end. - --spec(get_session_stats(emqx_types:client_id(), pid()) -> list(emqx_stats:stats())). -get_session_stats(ClientId, SessPid) when is_binary(ClientId) -> - emqx_tables:lookup_value(?SESSION_STATS_TAB, {ClientId, SessPid}, []). - -%% @doc Set session stats --spec(set_session_stats(emqx_types:client_id(), emqx_stats:stats()) -> true). -set_session_stats(ClientId, Stats) when is_binary(ClientId) -> - set_session_stats(ClientId, self(), Stats). - --spec(set_session_stats(emqx_types:client_id(), pid(), emqx_stats:stats()) -> true). -set_session_stats(ClientId, SessPid, Stats) when is_binary(ClientId), is_pid(SessPid) -> - ets:insert(?SESSION_STATS_TAB, {{ClientId, SessPid}, Stats}). - -%% @doc Lookup session pid. --spec(lookup_session_pids(emqx_types:client_id()) -> list(pid())). -lookup_session_pids(ClientId) -> - case emqx_sm_registry:is_enabled() of - true -> emqx_sm_registry:lookup_session(ClientId); - false -> - case emqx_tables:lookup_value(?SESSION_TAB, ClientId) of - undefined -> []; - SessPid when is_pid(SessPid) -> [SessPid] - end - end. - -%% @doc Dispatch a message to the session. --spec(dispatch(emqx_types:client_id(), emqx_topic:topic(), emqx_types:message()) -> any()). -dispatch(ClientId, Topic, Msg) -> - case lookup_session_pids(ClientId) of - [SessPid|_] when is_pid(SessPid) -> - SessPid ! {dispatch, Topic, Msg}; - [] -> - emqx_hooks:run('message.dropped', [#{client_id => ClientId}, Msg]) - end. - -%%------------------------------------------------------------------------------ -%% gen_server callbacks -%%------------------------------------------------------------------------------ - -init([]) -> - TabOpts = [public, set, {write_concurrency, true}], - ok = emqx_tables:new(?SESSION_TAB, [{read_concurrency, true} | TabOpts]), - ok = emqx_tables:new(?SESSION_P_TAB, TabOpts), - ok = emqx_tables:new(?SESSION_ATTRS_TAB, TabOpts), - ok = emqx_tables:new(?SESSION_STATS_TAB, TabOpts), - ok = emqx_stats:update_interval(sess_stats, fun ?MODULE:stats_fun/0), - {ok, #{}}. - -handle_call(Req, _From, State) -> - ?LOG(error, "Unexpected call: ~p", [Req]), - {reply, ignored, State}. - -handle_cast(Msg, State) -> - ?LOG(error, "Unexpected cast: ~p", [Msg]), - {noreply, State}. - -handle_info(Info, State) -> - ?LOG(error, "Unexpected info: ~p", [Info]), - {noreply, State}. - -terminate(_Reason, _State) -> - emqx_stats:cancel_update(sess_stats). - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -%%------------------------------------------------------------------------------ -%% Internal functions -%%------------------------------------------------------------------------------ - -clean_down(Session = {ClientId, SessPid}) -> - case ets:member(?SESSION_TAB, ClientId) - orelse ets:member(?SESSION_ATTRS_TAB, Session) of - true -> - unregister_session(ClientId, SessPid); - false -> ok - end. - -stats_fun() -> - safe_update_stats(?SESSION_TAB, 'sessions.count', 'sessions.max'), - safe_update_stats(?SESSION_P_TAB, 'sessions.persistent.count', 'sessions.persistent.max'). - -safe_update_stats(Tab, Stat, MaxStat) -> - case ets:info(Tab, size) of - undefined -> ok; - Size -> emqx_stats:setstat(Stat, MaxStat, Size) - end. - diff --git a/src/emqx_sm_sup.erl b/src/emqx_sm_sup.erl deleted file mode 100644 index 65702b26f..000000000 --- a/src/emqx_sm_sup.erl +++ /dev/null @@ -1,64 +0,0 @@ -%% 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_sm_sup). - --behaviour(supervisor). - --export([start_link/0]). - --export([init/1]). - -start_link() -> - supervisor:start_link({local, ?MODULE}, ?MODULE, []). - -init([]) -> - %% Session locker - Locker = #{id => locker, - start => {emqx_sm_locker, start_link, []}, - restart => permanent, - shutdown => 5000, - type => worker, - modules => [emqx_sm_locker] - }, - %% Session registry - Registry = #{id => registry, - start => {emqx_sm_registry, start_link, []}, - restart => permanent, - shutdown => 5000, - type => worker, - modules => [emqx_sm_registry] - }, - %% Session Manager - Manager = #{id => manager, - start => {emqx_sm, start_link, []}, - restart => permanent, - shutdown => 5000, - type => worker, - modules => [emqx_sm] - }, - %% Session Sup - SessSpec = #{start => {emqx_session, start_link, []}, - shutdown => brutal_kill, - clean_down => fun emqx_sm:clean_down/1 - }, - SessionSup = #{id => session_sup, - start => {emqx_session_sup, start_link, [SessSpec ]}, - restart => transient, - shutdown => infinity, - type => supervisor, - modules => [emqx_session_sup] - }, - {ok, {{rest_for_one, 10, 3600}, [Locker, Registry, Manager, SessionSup]}}. - diff --git a/src/emqx_stats.erl b/src/emqx_stats.erl index a095df8d8..d3454c6f2 100644 --- a/src/emqx_stats.erl +++ b/src/emqx_stats.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_stats). @@ -49,14 +51,18 @@ , code_change/3 ]). +-export_type([stats/0]). + -record(update, {name, countdown, interval, func}). --record(state, {timer, updates :: [#update{}], tick_ms :: timeout()}). +-record(state, { + timer :: reference(), + updates :: [#update{}], + tick_ms :: timeout() + }). -type(stats() :: list({atom(), non_neg_integer()})). --export_type([stats/0]). - %% Connection stats -define(CONNECTION_STATS, [ 'connections.count', % current connections @@ -168,9 +174,9 @@ rec(Name, Secs, UpFun) -> cast(Msg) -> gen_server:cast(?SERVER, Msg). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% gen_server callbacks -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- init(#{tick_ms := TickMs}) -> ok = emqx_tables:new(?TAB, [public, set, {write_concurrency, true}]), @@ -201,7 +207,8 @@ handle_cast({setstat, Stat, MaxStat, Val}, State) -> safe_update_element(Stat, Val), {noreply, State}; -handle_cast({update_interval, Update = #update{name = Name}}, State = #state{updates = Updates}) -> +handle_cast({update_interval, Update = #update{name = Name}}, + State = #state{updates = Updates}) -> case lists:keyfind(Name, #update.name, Updates) of #update{} -> ?LOG(warning, "Duplicated update: ~s", [Name]), @@ -242,9 +249,9 @@ terminate(_Reason, #state{timer = TRef}) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Internal functions -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- safe_update_element(Key, Val) -> try ets:update_element(?TAB, Key, {2, Val}) of @@ -256,3 +263,4 @@ safe_update_element(Key, Val) -> error:badarg -> ?LOG(warning, "Update ~p to ~p failed", [Key, Val]) end. + diff --git a/src/emqx_sup.erl b/src/emqx_sup.erl index b517e4f07..d3a81fc7f 100644 --- a/src/emqx_sup.erl +++ b/src/emqx_sup.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,11 +12,14 @@ %% 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_sup). -behaviour(supervisor). +-include("types.hrl"). + -export([ start_link/0 , start_child/1 , start_child/2 @@ -28,29 +32,28 @@ | {ok, supervisor:child(), term()} | {error, term()}). --define(SUPERVISOR, ?MODULE). +-define(SUP, ?MODULE). %%-------------------------------------------------------------------- %% API %%-------------------------------------------------------------------- +-spec(start_link() -> startlink_ret()). start_link() -> - supervisor:start_link({local, ?SUPERVISOR}, ?MODULE, []). + supervisor:start_link({local, ?SUP}, ?MODULE, []). -spec(start_child(supervisor:child_spec()) -> startchild_ret()). start_child(ChildSpec) when is_tuple(ChildSpec) -> - supervisor:start_child(?SUPERVISOR, ChildSpec). + supervisor:start_child(?SUP, ChildSpec). -spec(start_child(module(), worker | supervisor) -> startchild_ret()). -start_child(Mod, worker) -> - start_child(worker_spec(Mod)); -start_child(Mod, supervisor) -> - start_child(supervisor_spec(Mod)). +start_child(Mod, Type) -> + start_child(child_spec(Mod, Type)). -spec(stop_child(supervisor:child_id()) -> ok | {error, term()}). stop_child(ChildId) -> - case supervisor:terminate_child(?SUPERVISOR, ChildId) of - ok -> supervisor:delete_child(?SUPERVISOR, ChildId); + case supervisor:terminate_child(?SUP, ChildId) of + ok -> supervisor:delete_child(?SUP, ChildId); Error -> Error end. @@ -60,30 +63,37 @@ stop_child(ChildId) -> init([]) -> %% Kernel Sup - KernelSup = supervisor_spec(emqx_kernel_sup), + KernelSup = child_spec(emqx_kernel_sup, supervisor), %% Router Sup - RouterSup = supervisor_spec(emqx_router_sup), + RouterSup = child_spec(emqx_router_sup, supervisor), %% Broker Sup - BrokerSup = supervisor_spec(emqx_broker_sup), - %% Session Manager - SMSup = supervisor_spec(emqx_sm_sup), - %% Connection Manager - CMSup = supervisor_spec(emqx_cm_sup), + BrokerSup = child_spec(emqx_broker_sup, supervisor), + %% CM Sup + CMSup = child_spec(emqx_cm_sup, supervisor), %% Sys Sup - SysSup = supervisor_spec(emqx_sys_sup), + SysSup = child_spec(emqx_sys_sup, supervisor), {ok, {{one_for_all, 0, 1}, - [KernelSup, - RouterSup, - BrokerSup, - SMSup, - CMSup, - SysSup]}}. + [KernelSup, RouterSup, BrokerSup, CMSup, SysSup]}}. %%-------------------------------------------------------------------- %% Internal functions %%-------------------------------------------------------------------- -worker_spec(M) -> - {M, {M, start_link, []}, permanent, 30000, worker, [M]}. -supervisor_spec(M) -> - {M, {M, start_link, []}, permanent, infinity, supervisor, [M]}. +child_spec(Mod, supervisor) -> + #{id => Mod, + start => {Mod, start_link, []}, + restart => permanent, + shutdown => infinity, + type => supervisor, + modules => [Mod] + }; + +child_spec(Mod, worker) -> + #{id => Mod, + start => {Mod, start_link, []}, + restart => permanent, + shutdown => 15000, + type => worker, + modules => [Mod] + }. + diff --git a/src/emqx_sys.erl b/src/emqx_sys.erl index 8a9374e2d..f024d2504 100644 --- a/src/emqx_sys.erl +++ b/src/emqx_sys.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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). diff --git a/src/emqx_sys_mon.erl b/src/emqx_sys_mon.erl index f66c2a0c5..7d7bdae21 100644 --- a/src/emqx_sys_mon.erl +++ b/src/emqx_sys_mon.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_mon). @@ -43,18 +45,14 @@ -define(SYSMON, ?MODULE). -%%------------------------------------------------------------------------------ -%% APIs -%%------------------------------------------------------------------------------ - -%% @doc Start system monitor +%% @doc Start the system monitor. -spec(start_link(list(option())) -> startlink_ret()). start_link(Opts) -> gen_server:start_link({local, ?SYSMON}, ?MODULE, [Opts], []). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% gen_server callbacks -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- init([Opts]) -> erlang:system_monitor(self(), parse_opt(Opts)), diff --git a/src/emqx_sys_sup.erl b/src/emqx_sys_sup.erl index 9341b8528..cbc07650e 100644 --- a/src/emqx_sys_sup.erl +++ b/src/emqx_sys_sup.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_sup). @@ -24,23 +26,28 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). init([]) -> - {ok, {{one_for_one, 10, 100}, [child_spec(emqx_sys, worker), - child_spec(emqx_sys_mon, worker, [emqx_config:get_env(sysmon, [])]), - child_spec(emqx_os_mon, worker, [emqx_config:get_env(os_mon, [])]), - child_spec(emqx_vm_mon, worker, [emqx_config:get_env(vm_mon, [])])]}}. + Childs = [child_spec(emqx_sys), + child_spec(emqx_sys_mon, [config(sysmon)]), + child_spec(emqx_os_mon, [config(os_mon)]), + child_spec(emqx_vm_mon, [config(vm_mon)])], + {ok, {{one_for_one, 10, 100}, Childs}}. %%-------------------------------------------------------------------- %% Internal functions %%-------------------------------------------------------------------- -child_spec(M, worker) -> - child_spec(M, worker, []). +child_spec(Mod) -> + child_spec(Mod, []). -child_spec(M, worker, A) -> - #{id => M, - start => {M, start_link, A}, - restart => permanent, +child_spec(Mod, Args) -> + #{id => Mod, + start => {Mod, start_link, Args}, + restart => permanent, shutdown => 5000, - type => worker, - modules => [M]}. + type => worker, + modules => [Mod] + }. + +config(Name) -> + emqx_config:get_env(Name, []). diff --git a/src/emqx_tables.erl b/src/emqx_tables.erl index 16812036a..fec1864e0 100644 --- a/src/emqx_tables.erl +++ b/src/emqx_tables.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,15 +12,25 @@ %% 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_tables). --export([new/2, delete/1]). +-export([ new/1 + , new/2 + ]). -export([ lookup_value/2 , lookup_value/3 ]). +-export([delete/1]). + +%% Create an ets table. +-spec(new(atom()) -> ok). +new(Tab) -> + new(Tab, []). + %% Create a named_table ets. -spec(new(atom(), list()) -> ok). new(Tab, Opts) -> @@ -30,25 +41,25 @@ new(Tab, Opts) -> Tab -> ok end. --spec(delete(atom()) -> ok). +%% KV lookup +-spec(lookup_value(ets:tab(), term()) -> any()). +lookup_value(Tab, Key) -> + lookup_value(Tab, Key, undefined). + +-spec(lookup_value(ets:tab(), term(), any()) -> any()). +lookup_value(Tab, Key, Def) -> + try ets:lookup_element(Tab, Key, 2) + catch + error:badarg -> Def + end. + +%% Delete the ets table. +-spec(delete(ets:tab()) -> ok). delete(Tab) -> case ets:info(Tab, name) of - undefined -> - ok; + undefined -> ok; Tab -> ets:delete(Tab), ok end. -%% KV lookup --spec(lookup_value(atom(), term()) -> any()). -lookup_value(Tab, Key) -> - lookup_value(Tab, Key, undefined). - --spec(lookup_value(atom(), term(), any()) -> any()). -lookup_value(Tab, Key, Def) -> - try - ets:lookup_element(Tab, Key, 2) - catch - error:badarg -> Def - end. diff --git a/src/emqx_time.erl b/src/emqx_time.erl index e0ef8e5fb..16508cdda 100644 --- a/src/emqx_time.erl +++ b/src/emqx_time.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_time). @@ -19,9 +21,16 @@ , now_secs/1 , now_ms/0 , now_ms/1 - , ts_from_ms/1 ]). +-compile({inline, + [ seed/0 + , now_secs/0 + , now_secs/1 + , now_ms/0 + , now_ms/1 + ]}). + seed() -> rand:seed(exsplus, erlang:timestamp()). @@ -37,5 +46,3 @@ now_ms() -> now_ms({MegaSecs, Secs, MicroSecs}) -> (MegaSecs * 1000000 + Secs) * 1000 + round(MicroSecs/1000). -ts_from_ms(Ms) -> - {Ms div 1000000, Ms rem 1000000, 0}. diff --git a/src/emqx_topic.erl b/src/emqx_topic.erl index f1d15d8e2..680cb299e 100644 --- a/src/emqx_topic.erl +++ b/src/emqx_topic.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,11 +12,10 @@ %% 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_topic). --include("emqx_mqtt.hrl"). - %% APIs -export([ match/2 , validate/1 @@ -33,19 +33,23 @@ , parse/2 ]). +-export_type([ group/0 + , topic/0 + , word/0 + , triple/0 + ]). + -type(group() :: binary()). -type(topic() :: binary()). -type(word() :: '' | '+' | '#' | binary()). -type(words() :: list(word())). -opaque(triple() :: {root | binary(), word(), binary()}). --export_type([group/0, topic/0, word/0, triple/0]). - -define(MAX_TOPIC_LEN, 4096). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% APIs -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% @doc Is wildcard topic? -spec(wildcard(topic() | words()) -> true | false). @@ -60,7 +64,7 @@ wildcard(['+'|_]) -> wildcard([_H|T]) -> wildcard(T). -%% @doc Match Topic name with filter +%% @doc Match Topic name with filter. -spec(match(Name, Filter) -> boolean() when Name :: topic() | words(), Filter :: topic() | words()). @@ -68,7 +72,7 @@ match(<<$$, _/binary>>, <<$+, _/binary>>) -> false; match(<<$$, _/binary>>, <<$#, _/binary>>) -> false; -match(Name, Filter) when is_binary(Name) and is_binary(Filter) -> +match(Name, Filter) when is_binary(Name), is_binary(Filter) -> match(words(Name), words(Filter)); match([], []) -> true; @@ -95,13 +99,15 @@ validate({Type, Topic}) when Type =:= name; Type =:= filter -> -spec(validate(name | filter, topic()) -> true). validate(_, <<>>) -> error(empty_topic); -validate(_, Topic) when is_binary(Topic) and (size(Topic) > ?MAX_TOPIC_LEN) -> +validate(_, Topic) when is_binary(Topic) andalso (size(Topic) > ?MAX_TOPIC_LEN) -> error(topic_too_long); validate(filter, Topic) when is_binary(Topic) -> validate2(words(Topic)); validate(name, Topic) when is_binary(Topic) -> Words = words(Topic), - validate2(Words) and (not wildcard(Words)). + validate2(Words) + andalso (not wildcard(Words)) + orelse error(topic_name_error). validate2([]) -> true; @@ -123,7 +129,7 @@ validate3(<>) when C == $#; C == $+; C == 0 -> validate3(<<_/utf8, Rest/binary>>) -> validate3(Rest). -%% @doc Topic to triples +%% @doc Topic to triples. -spec(triples(topic()) -> list(triple())). triples(Topic) when is_binary(Topic) -> triples(words(Topic), root, []). @@ -206,27 +212,28 @@ join(Words) -> end, {true, <<>>}, [bin(W) || W <- Words]), Bin. --spec(parse(topic()) -> {topic(), #{}}). -parse(Topic) when is_binary(Topic) -> - parse(Topic, #{}). +-spec(parse(topic() | {topic(), map()}) -> {topic(), #{share => binary()}}). +parse(TopicFilter) when is_binary(TopicFilter) -> + parse(TopicFilter, #{}); +parse({TopicFilter, Options}) when is_binary(TopicFilter) -> + parse(TopicFilter, Options). -parse(Topic = <<"$queue/", _/binary>>, #{share := _Group}) -> - error({invalid_topic, Topic}); -parse(Topic = <>, #{share := _Group}) -> - error({invalid_topic, Topic}); -parse(<<"$queue/", Topic1/binary>>, Options) -> - parse(Topic1, maps:put(share, <<"$queue">>, Options)); -parse(Topic = <>, Options) -> - case binary:split(Topic1, <<"/">>) of - [<<>>] -> error({invalid_topic, Topic}); - [_] -> error({invalid_topic, Topic}); - [Group, Topic2] -> - case binary:match(Group, [<<"/">>, <<"+">>, <<"#">>]) of - nomatch -> {Topic2, maps:put(share, Group, Options)}; - _ -> error({invalid_topic, Topic}) +-spec(parse(topic(), map()) -> {topic(), map()}). +parse(TopicFilter = <<"$queue/", _/binary>>, #{share := _Group}) -> + error({invalid_topic_filter, TopicFilter}); +parse(TopicFilter = <<"$share/", _/binary>>, #{share := _Group}) -> + error({invalid_topic_filter, TopicFilter}); +parse(<<"$queue/", TopicFilter/binary>>, Options) -> + parse(TopicFilter, Options#{share => <<"$queue">>}); +parse(TopicFilter = <<"$share/", Rest/binary>>, Options) -> + case binary:split(Rest, <<"/">>) of + [_Any] -> error({invalid_topic_filter, TopicFilter}); + [ShareName, Filter] -> + case binary:match(ShareName, [<<"+">>, <<"#">>]) of + nomatch -> parse(Filter, Options#{share => ShareName}); + _ -> error({invalid_topic_filter, TopicFilter}) end end; -parse(Topic, Options = #{qos := QoS}) -> - {Topic, Options#{rc => QoS}}; -parse(Topic, Options) -> - {Topic, Options}. +parse(TopicFilter, Options) -> + {TopicFilter, Options}. + diff --git a/src/emqx_tracer.erl b/src/emqx_tracer.erl index 21562930b..f9273d3e3 100644 --- a/src/emqx_tracer.erl +++ b/src/emqx_tracer.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,37 +12,23 @@ %% 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_tracer). --behaviour(gen_server). - -include("emqx.hrl"). -include("logger.hrl"). -logger_header("[Tracer]"). %% APIs --export([start_link/0]). - -export([ trace/2 , start_trace/3 , lookup_traces/0 , stop_trace/1 ]). -%% gen_server callbacks --export([ init/1 - , handle_call/3 - , handle_cast/2 - , handle_info/2 - , terminate/2 - , code_change/3 - ]). - --record(state, {traces}). - --type(trace_who() :: {client_id | topic, binary()}). +-type(trace_who() :: {client_id | topic, binary() | list()}). -define(TRACER, ?MODULE). -define(FORMAT, {emqx_logger_formatter, @@ -55,120 +42,100 @@ [peername," "], []}]}, msg,"\n"]}}). +-define(TOPIC_TRACE_ID(T), "trace_topic_"++T). +-define(CLIENT_TRACE_ID(C), "trace_clientid_"++C). +-define(TOPIC_TRACE(T), {topic,T}). +-define(CLIENT_TRACE(C), {client_id,C}). + +-define(is_log_level(L), + L =:= emergency orelse + L =:= alert orelse + L =:= critical orelse + L =:= error orelse + L =:= warning orelse + L =:= notice orelse + L =:= info orelse + L =:= debug). %%------------------------------------------------------------------------------ %% APIs %%------------------------------------------------------------------------------ - --spec(start_link() -> {ok, pid()} | ignore | {error, term()}). -start_link() -> - gen_server:start_link({local, ?TRACER}, ?MODULE, [], []). - trace(publish, #message{topic = <<"$SYS/", _/binary>>}) -> - %% Dont' trace '$SYS' publish + %% Do not trace '$SYS' publish ignore; trace(publish, #message{from = From, topic = Topic, payload = Payload}) when is_binary(From); is_atom(From) -> emqx_logger:info(#{topic => Topic}, "PUBLISH to ~s: ~p", [Topic, Payload]). -%%------------------------------------------------------------------------------ -%% Start/Stop trace -%%------------------------------------------------------------------------------ - %% @doc Start to trace client_id or topic. -spec(start_trace(trace_who(), logger:level(), string()) -> ok | {error, term()}). -start_trace({client_id, ClientId}, Level, LogFile) -> - do_start_trace({client_id, ClientId}, Level, LogFile); -start_trace({topic, Topic}, Level, LogFile) -> - do_start_trace({topic, Topic}, Level, LogFile). - -do_start_trace(Who, Level, LogFile) -> - #{level := PrimaryLevel} = logger:get_primary_config(), - try logger:compare_levels(log_level(Level), PrimaryLevel) of - lt -> - {error, io_lib:format("Cannot trace at a log level (~s) lower than the primary log level (~s)", [Level, PrimaryLevel])}; - _GtOrEq -> - gen_server:call(?MODULE, {start_trace, Who, Level, LogFile}, 5000) - catch - _:Error -> - {error, Error} +start_trace(Who, all, LogFile) -> + start_trace(Who, debug, LogFile); +start_trace(Who, Level, LogFile) -> + case ?is_log_level(Level) of + true -> + #{level := PrimaryLevel} = logger:get_primary_config(), + try logger:compare_levels(Level, PrimaryLevel) of + lt -> + {error, io_lib:format("Cannot trace at a log level (~s) lower than the primary log level (~s)", [Level, PrimaryLevel])}; + _GtOrEq -> + install_trace_handler(Who, Level, LogFile) + catch + _:Error -> + {error, Error} + end; + false -> {error, {invalid_log_level, Level}} end. %% @doc Stop tracing client_id or topic. -spec(stop_trace(trace_who()) -> ok | {error, term()}). -stop_trace({client_id, ClientId}) -> - gen_server:call(?MODULE, {stop_trace, {client_id, ClientId}}); -stop_trace({topic, Topic}) -> - gen_server:call(?MODULE, {stop_trace, {topic, Topic}}). +stop_trace(Who) -> + uninstall_trance_handler(Who). %% @doc Lookup all traces -spec(lookup_traces() -> [{Who :: trace_who(), LogFile :: string()}]). lookup_traces() -> - gen_server:call(?TRACER, lookup_traces). + lists:foldl(fun filter_traces/2, [], emqx_logger:get_log_handlers()). -%%------------------------------------------------------------------------------ -%% gen_server callbacks -%%------------------------------------------------------------------------------ - -init([]) -> - {ok, #state{traces = #{}}}. - -handle_call({start_trace, Who, Level, LogFile}, _From, State = #state{traces = Traces}) -> +install_trace_handler(Who, Level, LogFile) -> case logger:add_handler(handler_id(Who), logger_disk_log_h, - #{level => Level, - formatter => ?FORMAT, - filesync_repeat_interval => no_repeat, - config => #{type => halt, file => LogFile}, - filter_default => stop, - filters => [{meta_key_filter, - {fun filter_by_meta_key/2, Who} }]}) of + #{level => Level, + formatter => ?FORMAT, + filesync_repeat_interval => no_repeat, + config => #{type => halt, file => LogFile}, + filter_default => stop, + filters => [{meta_key_filter, + {fun filter_by_meta_key/2, Who}}]}) + of ok -> - ?LOG(info, "Start trace for ~p", [Who]), - {reply, ok, State#state{traces = maps:put(Who, {Level, LogFile}, Traces)}}; + ?LOG(info, "Start trace for ~p", [Who]); {error, Reason} -> ?LOG(error, "Start trace for ~p failed, error: ~p", [Who, Reason]), - {reply, {error, Reason}, State} - end; + {error, Reason} + end. -handle_call({stop_trace, Who}, _From, State = #state{traces = Traces}) -> - case maps:find(Who, Traces) of - {ok, _LogFile} -> - case logger:remove_handler(handler_id(Who)) of - ok -> - ?LOG(info, "Stop trace for ~p", [Who]); - {error, Reason} -> - ?LOG(error, "Stop trace for ~p failed, error: ~p", [Who, Reason]) - end, - {reply, ok, State#state{traces = maps:remove(Who, Traces)}}; - error -> - {reply, {error, not_found}, State} - end; +uninstall_trance_handler(Who) -> + case logger:remove_handler(handler_id(Who)) of + ok -> + ?LOG(info, "Stop trace for ~p", [Who]); + {error, Reason} -> + ?LOG(error, "Stop trace for ~p failed, error: ~p", [Who, Reason]), + {error, Reason} + end. -handle_call(lookup_traces, _From, State = #state{traces = Traces}) -> - {reply, [{Who, LogFile} || {Who, LogFile} <- maps:to_list(Traces)], State}; +filter_traces({Id, Level, Dst}, Acc) -> + case atom_to_list(Id) of + ?TOPIC_TRACE_ID(T)-> + [{?TOPIC_TRACE(T), {Level,Dst}} | Acc]; + ?CLIENT_TRACE_ID(C) -> + [{?CLIENT_TRACE(C), {Level,Dst}} | Acc]; + _ -> Acc + end. -handle_call(Req, _From, State) -> - ?LOG(error, "Unexpected call: ~p", [Req]), - {reply, ignored, State}. - -handle_cast(Msg, State) -> - ?LOG(error, "Unexpected cast: ~p", [Msg]), - {noreply, State}. - -handle_info(Info, State) -> - ?LOG(error, "Unexpected info: ~p", [Info]), - {noreply, State}. - -terminate(_Reason, _State) -> - ok. - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -handler_id({topic, Topic}) -> - list_to_atom("topic_" ++ binary_to_list(Topic)); -handler_id({client_id, ClientId}) -> - list_to_atom("clientid_" ++ binary_to_list(ClientId)). +handler_id(?TOPIC_TRACE(Topic)) -> + list_to_atom(?TOPIC_TRACE_ID(str(Topic))); +handler_id(?CLIENT_TRACE(ClientId)) -> + list_to_atom(?CLIENT_TRACE_ID(str(ClientId))). filter_by_meta_key(#{meta:=Meta}=LogEvent, {MetaKey, MetaValue}) -> case maps:find(MetaKey, Meta) of @@ -181,13 +148,6 @@ filter_by_meta_key(#{meta:=Meta}=LogEvent, {MetaKey, MetaValue}) -> _ -> ignore end. -log_level(emergency) -> emergency; -log_level(alert) -> alert; -log_level(critical) -> critical; -log_level(error) -> error; -log_level(warning) -> warning; -log_level(notice) -> notice; -log_level(info) -> info; -log_level(debug) -> debug; -log_level(all) -> debug; -log_level(_) -> throw(invalid_log_level). +str(Bin) when is_binary(Bin) -> binary_to_list(Bin); +str(Atom) when is_atom(Atom) -> atom_to_list(Atom); +str(Str) when is_list(Str) -> Str. diff --git a/src/emqx_trie.erl b/src/emqx_trie.erl index 1fb5e0ead..c0c037a7c 100644 --- a/src/emqx_trie.erl +++ b/src/emqx_trie.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_trie). @@ -32,12 +34,12 @@ -export([empty/0]). %% Mnesia tables --define(TRIE, emqx_trie). --define(TRIE_NODE, emqx_trie_node). +-define(TRIE_TAB, emqx_trie). +-define(TRIE_NODE_TAB, emqx_trie_node). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Mnesia bootstrap -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% @doc Create or replicate trie tables. -spec(mnesia(boot | copy) -> ok). @@ -46,13 +48,13 @@ mnesia(boot) -> StoreProps = [{ets, [{read_concurrency, true}, {write_concurrency, true}]}], %% Trie table - ok = ekka_mnesia:create_table(?TRIE, [ + ok = ekka_mnesia:create_table(?TRIE_TAB, [ {ram_copies, [node()]}, {record_name, trie}, {attributes, record_info(fields, trie)}, {storage_properties, StoreProps}]), %% Trie node table - ok = ekka_mnesia:create_table(?TRIE_NODE, [ + ok = ekka_mnesia:create_table(?TRIE_NODE_TAB, [ {ram_copies, [node()]}, {record_name, trie_node}, {attributes, record_info(fields, trie_node)}, @@ -60,18 +62,18 @@ mnesia(boot) -> mnesia(copy) -> %% Copy trie table - ok = ekka_mnesia:copy_table(?TRIE), + ok = ekka_mnesia:copy_table(?TRIE_TAB), %% Copy trie_node table - ok = ekka_mnesia:copy_table(?TRIE_NODE). + ok = ekka_mnesia:copy_table(?TRIE_NODE_TAB). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Trie APIs -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% @doc Insert a topic filter into the trie. -spec(insert(emqx_topic:topic()) -> ok). insert(Topic) when is_binary(Topic) -> - case mnesia:wread({?TRIE_NODE, Topic}) of + case mnesia:wread({?TRIE_NODE_TAB, Topic}) of [#trie_node{topic = Topic}] -> ok; [TrieNode = #trie_node{topic = undefined}] -> @@ -92,14 +94,14 @@ match(Topic) when is_binary(Topic) -> %% @doc Lookup a trie node. -spec(lookup(NodeId :: binary()) -> [#trie_node{}]). lookup(NodeId) -> - mnesia:read(?TRIE_NODE, NodeId). + mnesia:read(?TRIE_NODE_TAB, NodeId). %% @doc Delete a topic filter from the trie. -spec(delete(emqx_topic:topic()) -> ok). delete(Topic) when is_binary(Topic) -> - case mnesia:wread({?TRIE_NODE, Topic}) of + case mnesia:wread({?TRIE_NODE_TAB, Topic}) of [#trie_node{edge_count = 0}] -> - ok = mnesia:delete({?TRIE_NODE, Topic}), + ok = mnesia:delete({?TRIE_NODE_TAB, Topic}), delete_path(lists:reverse(emqx_topic:triples(Topic))); [TrieNode] -> write_trie_node(TrieNode#trie_node{topic = undefined}); @@ -109,19 +111,19 @@ delete(Topic) when is_binary(Topic) -> %% @doc Is the trie empty? -spec(empty() -> boolean()). empty() -> - ets:info(?TRIE, size) == 0. + ets:info(?TRIE_TAB, size) == 0. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Internal functions -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% @private %% @doc Add a path to the trie. add_path({Node, Word, Child}) -> Edge = #trie_edge{node_id = Node, word = Word}, - case mnesia:wread({?TRIE_NODE, Node}) of + case mnesia:wread({?TRIE_NODE_TAB, Node}) of [TrieNode = #trie_node{edge_count = Count}] -> - case mnesia:wread({?TRIE, Edge}) of + case mnesia:wread({?TRIE_TAB, Edge}) of [] -> ok = write_trie_node(TrieNode#trie_node{edge_count = Count + 1}), write_trie(#trie{edge = Edge, node_id = Child}); @@ -141,11 +143,11 @@ match_node(NodeId, Words) -> match_node(NodeId, Words, []). match_node(NodeId, [], ResAcc) -> - mnesia:read(?TRIE_NODE, NodeId) ++ 'match_#'(NodeId, ResAcc); + mnesia:read(?TRIE_NODE_TAB, NodeId) ++ 'match_#'(NodeId, ResAcc); match_node(NodeId, [W|Words], ResAcc) -> lists:foldl(fun(WArg, Acc) -> - case mnesia:read(?TRIE, #trie_edge{node_id = NodeId, word = WArg}) of + case mnesia:read(?TRIE_TAB, #trie_edge{node_id = NodeId, word = WArg}) of [#trie{node_id = ChildId}] -> match_node(ChildId, Words, Acc); [] -> Acc end @@ -154,9 +156,9 @@ match_node(NodeId, [W|Words], ResAcc) -> %% @private %% @doc Match node with '#'. 'match_#'(NodeId, ResAcc) -> - case mnesia:read(?TRIE, #trie_edge{node_id = NodeId, word = '#'}) of + case mnesia:read(?TRIE_TAB, #trie_edge{node_id = NodeId, word = '#'}) of [#trie{node_id = ChildId}] -> - mnesia:read(?TRIE_NODE, ChildId) ++ ResAcc; + mnesia:read(?TRIE_NODE_TAB, ChildId) ++ ResAcc; [] -> ResAcc end. @@ -165,10 +167,10 @@ match_node(NodeId, [W|Words], ResAcc) -> delete_path([]) -> ok; delete_path([{NodeId, Word, _} | RestPath]) -> - ok = mnesia:delete({?TRIE, #trie_edge{node_id = NodeId, word = Word}}), - case mnesia:wread({?TRIE_NODE, NodeId}) of + ok = mnesia:delete({?TRIE_TAB, #trie_edge{node_id = NodeId, word = Word}}), + case mnesia:wread({?TRIE_NODE_TAB, NodeId}) of [#trie_node{edge_count = 1, topic = undefined}] -> - ok = mnesia:delete({?TRIE_NODE, NodeId}), + ok = mnesia:delete({?TRIE_NODE_TAB, NodeId}), delete_path(RestPath); [TrieNode = #trie_node{edge_count = 1, topic = _}] -> write_trie_node(TrieNode#trie_node{edge_count = 0}); @@ -180,9 +182,9 @@ delete_path([{NodeId, Word, _} | RestPath]) -> %% @private write_trie(Trie) -> - mnesia:write(?TRIE, Trie, write). + mnesia:write(?TRIE_TAB, Trie, write). %% @private write_trie_node(TrieNode) -> - mnesia:write(?TRIE_NODE, TrieNode, write). + mnesia:write(?TRIE_NODE_TAB, TrieNode, write). diff --git a/src/emqx_types.erl b/src/emqx_types.erl index 50e7f78c0..e7f8af692 100644 --- a/src/emqx_types.erl +++ b/src/emqx_types.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,34 +12,48 @@ %% 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_types). -include("emqx.hrl"). +-include("emqx_mqtt.hrl"). -include("types.hrl"). --export_type([zone/0]). - --export_type([ pubsub/0 - , topic/0 - , subid/0 - , subopts/0 +-export_type([ ver/0 + , qos/0 + , qos_name/0 ]). --export_type([ client_id/0 +-export_type([ zone/0 + , pubsub/0 + , topic/0 + , subid/0 + ]). + +-export_type([ conn/0 + , client/0 + , client_id/0 , username/0 , password/0 , peername/0 , protocol/0 ]). --export_type([ credentials/0 - , session/0 +-export_type([ connack/0 + , subopts/0 + , reason_code/0 + , properties/0 + ]). + +-export_type([ packet_id/0 + , packet_type/0 + , packet/0 ]). -export_type([ subscription/0 , subscriber/0 - , topic_table/0 + , topic_filters/0 ]). -export_type([ payload/0 @@ -46,10 +61,13 @@ ]). -export_type([ delivery/0 - , deliver_results/0 + , publish_result/0 + , deliver_result/0 ]). --export_type([route/0]). +-export_type([ route/0 + , route_entry/0 + ]). -export_type([ alarm/0 , plugin/0 @@ -57,19 +75,52 @@ , command/0 ]). --type(zone() :: atom()). +-export_type([ caps/0 + , infos/0 + , attrs/0 + , stats/0 + ]). + +-type(ver() :: ?MQTT_PROTO_V3 + | ?MQTT_PROTO_V4 + | ?MQTT_PROTO_V5). +-type(qos() :: ?QOS_0 | ?QOS_1 | ?QOS_2). +-type(qos_name() :: qos0 | at_most_once | + qos1 | at_least_once | + qos2 | exactly_once). + +-type(zone() :: emqx_zone:zone()). -type(pubsub() :: publish | subscribe). --type(topic() :: binary()). +-type(topic() :: emqx_topic:topic()). -type(subid() :: binary() | atom()). --type(subopts() :: #{qos := emqx_mqtt_types:qos(), - share => binary(), - atom() => term() - }). --type(session() :: #session{}). + +-type(conn() :: #{peername := peername(), + sockname := peername(), + peercert := esockd_peercert:peercert(), + conn_mod := module(), + atom() => term() + }). +-type(client() :: #{zone := zone(), + conn_mod := maybe(module()), + peername := peername(), + sockname := peername(), + client_id := client_id(), + username := username(), + peercert := esockd_peercert:peercert(), + is_bridge := boolean(), + is_superuser := boolean(), + mountpoint := maybe(binary()), + ws_cookie := maybe(list()), + password => maybe(binary()), + auth_result => auth_result(), + anonymous => boolean(), + atom() => term() + }). -type(client_id() :: binary() | atom()). -type(username() :: maybe(binary())). -type(password() :: maybe(binary())). -type(peername() :: {inet:ip_address(), inet:port_number()}). +-type(protocol() :: mqtt | 'mqtt-sn' | coap | stomp | none | atom()). -type(auth_result() :: success | client_identifier_not_valid | bad_username_or_password @@ -79,29 +130,40 @@ | server_busy | banned | bad_authentication_method). --type(protocol() :: mqtt | 'mqtt-sn' | coap | stomp | none | atom()). --type(credentials() :: #{zone := zone(), - client_id := client_id(), - username := username(), - sockname := peername(), - peername := peername(), - ws_cookie := undefined | list(), - mountpoint := binary(), - password => binary(), - auth_result => auth_result(), - anonymous => boolean(), - atom() => term() - }). + +-type(packet_type() :: ?RESERVED..?AUTH). +-type(connack() :: ?CONNACK_ACCEPT..?CONNACK_AUTH). +-type(subopts() :: #{rh := 0 | 1 | 2, + rap := 0 | 1, + nl := 0 | 1, + qos := qos(), + share => binary(), + atom() => term() + }). +-type(reason_code() :: 0..16#FF). +-type(packet_id() :: 1..16#FFFF). +-type(properties() :: #{atom() => term()}). +-type(topic_filters() :: list({topic(), subopts()})). +-type(packet() :: #mqtt_packet{}). + -type(subscription() :: #subscription{}). -type(subscriber() :: {pid(), subid()}). --type(topic_table() :: [{topic(), subopts()}]). -type(payload() :: binary() | iodata()). -type(message() :: #message{}). -type(banned() :: #banned{}). -type(delivery() :: #delivery{}). --type(deliver_results() :: [{route, node(), topic()} | - {dispatch, topic(), pos_integer()}]). +-type(deliver_result() :: ok | {error, term()}). +-type(publish_result() :: [ {node(), topic(), deliver_result()} + | {share, topic(), deliver_result()}]). -type(route() :: #route{}). +-type(sub_group() :: tuple() | binary()). +-type(route_entry() :: {topic(), node()} | {topic, sub_group()}). -type(alarm() :: #alarm{}). -type(plugin() :: #plugin{}). -type(command() :: #command{}). + +-type(caps() :: emqx_mqtt_caps:caps()). +-type(infos() :: #{atom() => term()}). +-type(attrs() :: #{atom() => term()}). +-type(stats() :: list({atom(), non_neg_integer()})). + diff --git a/src/emqx_vm.erl b/src/emqx_vm.erl index 46c038732..ba07eee4c 100644 --- a/src/emqx_vm.erl +++ b/src/emqx_vm.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_vm). @@ -45,6 +47,8 @@ , get_port_info/1 ]). +-export([cpu_util/0]). + -define(UTIL_ALLOCATORS, [temp_alloc, eheap_alloc, binary_alloc, @@ -159,8 +163,6 @@ sndbuf, tos]). --include("emqx.hrl"). - schedulers() -> erlang:system_info(schedulers). @@ -169,9 +171,9 @@ microsecs() -> (Mega * 1000000 + Sec) * 1000000 + Micro. loads() -> - [{load1, ftos(?compat_windows(cpu_sup:avg1()/256, 0.0))}, - {load5, ftos(?compat_windows(cpu_sup:avg5()/256, 0.0))}, - {load15, ftos(?compat_windows(cpu_sup:avg15()/256, 0.0))}]. + [{load1, ftos(avg1()/256)}, + {load5, ftos(avg5()/256)}, + {load15, ftos(avg15()/256)}]. get_system_info() -> [{Key, format_system_info(Key, get_system_info(Key))} || Key <- ?SYSTEM_INFO]. @@ -447,3 +449,26 @@ mapping([{owner, V}|Entries], Acc) when is_pid(V) -> mapping(Entries, [{owner, Owner}|Acc]); mapping([{Key, Value}|Entries], Acc) -> mapping(Entries, [{Key, Value}|Acc]). + +avg1() -> + compat_windows(fun cpu_sup:avg1/0). + +avg5() -> + compat_windows(fun cpu_sup:avg5/0). + +avg15() -> + compat_windows(fun cpu_sup:avg15/0). + +cpu_util() -> + compat_windows(fun cpu_sup:util/0). + +compat_windows(Fun) -> + case os:type() of + {win32, nt} -> 0; + _Other -> handle_error(Fun()) + end. + +handle_error(Value) when is_number(Value) -> + Value; +handle_error({error, _Reason}) -> + 0. diff --git a/src/emqx_vm_mon.erl b/src/emqx_vm_mon.erl index 10be9d71b..e42ceb2ee 100644 --- a/src/emqx_vm_mon.erl +++ b/src/emqx_vm_mon.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,11 +12,14 @@ %% 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_vm_mon). -behaviour(gen_server). +-include("logger.hrl"). + %% APIs -export([start_link/1]). @@ -38,13 +42,13 @@ -define(VM_MON, ?MODULE). -%%---------------------------------------------------------------------- -%% API -%%---------------------------------------------------------------------- - start_link(Opts) -> gen_server:start_link({local, ?VM_MON}, ?MODULE, [Opts], []). +%%-------------------------------------------------------------------- +%% API +%%-------------------------------------------------------------------- + get_check_interval() -> call(get_check_interval). @@ -63,9 +67,12 @@ get_process_low_watermark() -> set_process_low_watermark(Float) -> call({set_process_low_watermark, Float}). -%%---------------------------------------------------------------------- +call(Req) -> + gen_server:call(?VM_MON, Req, infinity). + +%%-------------------------------------------------------------------- %% gen_server callbacks -%%---------------------------------------------------------------------- +%%-------------------------------------------------------------------- init([Opts]) -> {ok, ensure_check_timer(#{check_interval => proplists:get_value(check_interval, Opts, 30), @@ -76,43 +83,53 @@ init([Opts]) -> handle_call(get_check_interval, _From, State) -> {reply, maps:get(check_interval, State, undefined), State}; + handle_call({set_check_interval, Seconds}, _From, State) -> {reply, ok, State#{check_interval := Seconds}}; handle_call(get_process_high_watermark, _From, State) -> {reply, maps:get(process_high_watermark, State, undefined), State}; + handle_call({set_process_high_watermark, Float}, _From, State) -> {reply, ok, State#{process_high_watermark := Float}}; handle_call(get_process_low_watermark, _From, State) -> {reply, maps:get(process_low_watermark, State, undefined), State}; + handle_call({set_process_low_watermark, Float}, _From, State) -> {reply, ok, State#{process_low_watermark := Float}}; -handle_call(_Request, _From, State) -> - {reply, ok, State}. +handle_call(Req, _From, State) -> + ?LOG(error, "[VM_MON] Unexpected call: ~p", [Req]), + {reply, ignored, State}. -handle_cast(_Request, State) -> +handle_cast(Msg, State) -> + ?LOG(error, "[VM_MON] Unexpected cast: ~p", [Msg]), {noreply, State}. -handle_info({timeout, Timer, check}, State = #{timer := Timer, - process_high_watermark := ProcHighWatermark, - process_low_watermark := ProcLowWatermark, - is_process_alarm_set := IsProcessAlarmSet}) -> +handle_info({timeout, Timer, check}, + State = #{timer := Timer, + process_high_watermark := ProcHighWatermark, + process_low_watermark := ProcLowWatermark, + is_process_alarm_set := IsProcessAlarmSet}) -> ProcessCount = erlang:system_info(process_count), - case ProcessCount / erlang:system_info(process_limit) of - Percent when Percent >= ProcHighWatermark -> - alarm_handler:set_alarm({too_many_processes, ProcessCount}), - {noreply, ensure_check_timer(State#{is_process_alarm_set := true})}; - Percent when Percent < ProcLowWatermark -> - case IsProcessAlarmSet of - true -> alarm_handler:clear_alarm(too_many_processes); - false -> ok - end, - {noreply, ensure_check_timer(State#{is_process_alarm_set := false})}; - _Precent -> - {noreply, ensure_check_timer(State)} - end. + NState = case ProcessCount / erlang:system_info(process_limit) of + Percent when Percent >= ProcHighWatermark -> + alarm_handler:set_alarm({too_many_processes, ProcessCount}), + State#{is_process_alarm_set := true}; + Percent when Percent < ProcLowWatermark -> + case IsProcessAlarmSet of + true -> alarm_handler:clear_alarm(too_many_processes); + false -> ok + end, + State#{is_process_alarm_set := false}; + _Precent -> State + end, + {noreply, ensure_check_timer(NState)}; + +handle_info(Info, State) -> + ?LOG(error, "[VM_MON] Unexpected info: ~p", [Info]), + {noreply, State}. terminate(_Reason, #{timer := Timer}) -> emqx_misc:cancel_timer(Timer). @@ -120,11 +137,10 @@ terminate(_Reason, #{timer := Timer}) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -%%---------------------------------------------------------------------- +%%-------------------------------------------------------------------- %% Internal functions -%%---------------------------------------------------------------------- -call(Req) -> - gen_server:call(?VM_MON, Req, infinity). +%%-------------------------------------------------------------------- ensure_check_timer(State = #{check_interval := Interval}) -> State#{timer := emqx_misc:start_timer(timer:seconds(Interval), check)}. + diff --git a/src/emqx_ws_channel.erl b/src/emqx_ws_channel.erl index e8c86202a..3bc067525 100644 --- a/src/emqx_ws_channel.erl +++ b/src/emqx_ws_channel.erl @@ -14,22 +14,22 @@ %% limitations under the License. %%-------------------------------------------------------------------- +%% MQTT WebSocket Channel -module(emqx_ws_channel). -include("emqx.hrl"). -include("emqx_mqtt.hrl"). -include("logger.hrl"). +-include("types.hrl"). --logger_header("[WS Channel]"). +-logger_header("[WsChannel]"). -export([ info/1 , attrs/1 , stats/1 - , kick/1 - , session/1 ]). -%% websocket callbacks +%% WebSocket callbacks -export([ init/2 , websocket_init/1 , websocket_handle/2 @@ -38,66 +38,82 @@ ]). -record(state, { - request, - options, - peername, - sockname, - proto_state, - parse_state, - keepalive, - enable_stats, - stats_timer, - idle_timeout, - shutdown - }). + 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() + }). + +-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]). %%-------------------------------------------------------------------- %% API %%-------------------------------------------------------------------- -%% for debug +-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}) -> - ProtoInfo = emqx_protocol:info(ProtoState), - ConnInfo = #{socktype => websocket, - conn_state => running, +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}, - maps:merge(ProtoInfo, ConnInfo). + sockname => Sockname, + conn_state => running, + gc_state => emqx_gc:info(GCState), + enable_stats => enable_stats(StatsTimer), + idle_timeout => IdleTimeout, + connected => Connected, + connected_at => ConnectedAt + }, + maps:merge(ChanInfo, emqx_protocol:info(ProtoState)). -%% for dashboard +enable_stats(disabled) -> false; +enable_stats(_MaybeRef) -> true. + +-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}) -> + ConnAttrs = #{socktype => websocket, + peername => Peername, + sockname => Sockname, + connected => Connected, + connected_at => ConnectedAt + }, + maps:merge(ConnAttrs, emqx_protocol:attrs(ProtoState)). -attrs(#state{peername = Peername, - sockname = Sockname, - proto_state = ProtoState}) -> - SockAttrs = #{peername => Peername, - sockname => Sockname}, - ProtoAttrs = emqx_protocol:attrs(ProtoState), - maps:merge(SockAttrs, ProtoAttrs). - +-spec(stats(pid() | state()) -> emqx_types:stats()). stats(WSPid) when is_pid(WSPid) -> call(WSPid, stats); - stats(#state{proto_state = ProtoState}) -> - lists:append([wsock_stats(), - emqx_misc:proc_stats(), - emqx_protocol:stats(ProtoState) - ]). - -kick(WSPid) when is_pid(WSPid) -> - call(WSPid, kick). - -session(WSPid) when is_pid(WSPid) -> - call(WSPid, session). + ProcStats = emqx_misc:proc_stats(), + SessStats = emqx_session:stats(emqx_protocol:info(session, ProtoState)), + lists:append([ProcStats, SessStats, chan_stats(), wsock_stats()]). +%% @private call(WSPid, Req) when is_pid(WSPid) -> Mref = erlang:monitor(process, WSPid), WSPid ! {call, {self(), Mref}, Req}, @@ -121,24 +137,27 @@ init(Req, Opts) -> DeflateOptions = maps:from_list(proplists:get_value(deflate_options, Opts, [])), MaxFrameSize = case proplists:get_value(max_frame_size, Opts, 0) of 0 -> infinity; - MFS -> MFS + I -> I end, Compress = proplists:get_value(compress, Opts, false), - Options = #{compress => Compress, - deflate_opts => DeflateOptions, - max_frame_size => MaxFrameSize, - idle_timeout => IdleTimeout}, + WsOpts = #{compress => Compress, + deflate_opts => DeflateOptions, + max_frame_size => MaxFrameSize, + idle_timeout => IdleTimeout + }, case cowboy_req:parse_header(<<"sec-websocket-protocol">>, Req) of undefined -> - {cowboy_websocket, Req, #state{}, Options}; + %% TODO: why not reply 500??? + {cowboy_websocket, Req, [Req, Opts], WsOpts}; [<<"mqtt", Vsn/binary>>] -> - Resp = cowboy_req:set_resp_header(<<"sec-websocket-protocol">>, <<"mqtt", Vsn/binary>>, Req), - {cowboy_websocket, Resp, #state{request = Req, options = Opts}, Options}; + Resp = cowboy_req:set_resp_header( + <<"sec-websocket-protocol">>, <<"mqtt", Vsn/binary>>, Req), + {cowboy_websocket, Resp, [Req, Opts], WsOpts}; _ -> {ok, cowboy_req:reply(400, Req), #state{}} end. -websocket_init(#state{request = Req, options = Options}) -> +websocket_init([Req, Opts]) -> Peername = cowboy_req:peer(Req), Sockname = cowboy_req:sock(Req), Peercert = cowboy_req:cert(Req), @@ -148,84 +167,64 @@ websocket_init(#state{request = Req, options = Options}) -> ?LOG(error, "Illegal cookie"), undefined; Error:Reason -> - ?LOG(error, - "Cookie is parsed failed, Error: ~p, Reason ~p", + ?LOG(error, "Cookie is parsed failed, Error: ~p, Reason ~p", [Error, Reason]), undefined end, ProtoState = emqx_protocol:init(#{peername => Peername, sockname => Sockname, peercert => Peercert, - sendfun => send_fun(self()), ws_cookie => WsCookie, - conn_mod => ?MODULE}, Options), - Zone = proplists:get_value(zone, Options), + 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, - enable_stats = EnableStats, - idle_timeout = IdleTimout}}. - -send_fun(WsPid) -> - fun(Packet, Options) -> - Data = emqx_frame:serialize(Packet, Options), - BinSize = iolist_size(Data), - emqx_pd:update_counter(send_cnt, 1), - emqx_pd:update_counter(send_oct, BinSize), - WsPid ! {binary, iolist_to_binary(Data)}, - {ok, Data} - end. + gc_state = GcState, + pendings = [], + stats_timer = StatsTimer, + idle_timeout = IdleTimout, + connected = false + }}. stat_fun() -> fun() -> {ok, emqx_pd:get_counter(recv_oct)} end. -websocket_handle({binary, <<>>}, State) -> - {ok, ensure_stats_timer(State)}; -websocket_handle({binary, [<<>>]}, State) -> - {ok, ensure_stats_timer(State)}; -websocket_handle({binary, Data}, State = #state{parse_state = ParseState}) -> +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) -> ?LOG(debug, "RECV ~p", [Data]), - BinSize = iolist_size(Data), - emqx_pd:update_counter(recv_oct, BinSize), - ok = emqx_metrics:inc('bytes.received', BinSize), - try emqx_frame:parse(iolist_to_binary(Data), ParseState) of - {ok, NParseState} -> - {ok, State#state{parse_state = NParseState}}; - {ok, Packet, Rest, NParseState} -> - ok = emqx_metrics:inc_recv(Packet), - emqx_pd:update_counter(recv_cnt, 1), - handle_incoming(Packet, fun(NState) -> - websocket_handle({binary, Rest}, NState) - end, - State#state{parse_state = NParseState}); - {error, Reason} -> - ?LOG(error, "Frame error: ~p", [Reason]), - shutdown(Reason, State) - catch - error:Reason:Stk -> - ?LOG(error, "Parse failed for ~p~n\ - Stacktrace:~p~nFrame data: ~p", [Reason, Stk, Data]), - shutdown(parse_error, State) - end; + 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)); + %% Pings should be replied with pongs, cowboy does it automatically %% Pongs can be safely ignored. Clause here simply prevents crash. websocket_handle(Frame, State) when Frame =:= ping; Frame =:= pong -> - {ok, ensure_stats_timer(State)}; + {ok, State}; websocket_handle({FrameType, _}, State) when FrameType =:= ping; FrameType =:= pong -> - {ok, ensure_stats_timer(State)}; + {ok, State}; %% According to mqtt spec[https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901285] -websocket_handle({_OtherFrameType, _}, State) -> - ?LOG(error, "Frame error: Other type of data frame"), - shutdown(other_frame_type, State). +websocket_handle({FrameType, _}, State) -> + ?LOG(error, "Frame error: unexpected frame - ~p", [FrameType]), + stop(unexpected_ws_frame, State). websocket_info({call, From, info}, State) -> gen_server:reply(From, info(State)), @@ -241,33 +240,41 @@ websocket_info({call, From, stats}, State) -> websocket_info({call, From, kick}, State) -> gen_server:reply(From, ok), - shutdown(kick, State); + stop(kick, State); -websocket_info({call, From, session}, State = #state{proto_state = ProtoState}) -> - gen_server:reply(From, emqx_protocol:session(ProtoState)), - {ok, State}; +websocket_info({incoming, Packet = ?CONNECT_PACKET( + #mqtt_packet_connect{ + proto_ver = ProtoVer} + )}, + State = #state{fsm_state = idle}) -> + handle_incoming(Packet, fun connected/1, + State#state{serialize = serialize_fun(ProtoVer)}); -websocket_info({deliver, PubOrAck}, State = #state{proto_state = ProtoState}) -> - case emqx_protocol:deliver(PubOrAck, ProtoState) of - {ok, ProtoState1} -> - {ok, ensure_stats_timer(State#state{proto_state = ProtoState1})}; +websocket_info({incoming, Packet}, State = #state{fsm_state = idle}) -> + ?LOG(warning, "Unexpected incoming: ~p", [Packet]), + stop(unexpected_incoming_packet, State); + +websocket_info({incoming, Packet = ?PACKET(?CONNECT)}, + State = #state{fsm_state = connected}) -> + ?LOG(warning, "Unexpected connect: ~p", [Packet]), + stop(unexpected_incoming_connect, State); + +websocket_info({incoming, Packet}, State = #state{fsm_state = connected}) + when is_record(Packet, mqtt_packet) -> + handle_incoming(Packet, fun reply/1, State); + +websocket_info(Deliver = {deliver, _Topic, _Msg}, + State = #state{proto_state = ProtoState}) -> + Delivers = emqx_misc:drain_deliver([Deliver]), + case emqx_protocol:handle_deliver(Delivers, ProtoState) of + {ok, NProtoState} -> + reply(State#state{proto_state = NProtoState}); + {ok, Packets, NProtoState} -> + reply(enqueue(Packets, State#state{proto_state = NProtoState})); {error, Reason} -> - shutdown(Reason, State) - end; - -websocket_info({timeout, Timer, emit_stats}, - State = #state{stats_timer = Timer, proto_state = ProtoState}) -> - emqx_cm:set_conn_stats(emqx_protocol:client_id(ProtoState), stats(State)), - {ok, State#state{stats_timer = undefined}, hibernate}; - -websocket_info({keepalive, start, Interval}, State) -> - ?LOG(debug, "Keepalive at the interval of ~p", [Interval]), - case emqx_keepalive:start(stat_fun(), Interval, {keepalive, check}) of - {ok, KeepAlive} -> - {ok, State#state{keepalive = KeepAlive}}; - {error, Error} -> - ?LOG(warning, "Keepalive error: ~p", [Error]), - shutdown(Error, State) + stop(Reason, State); + {error, Reason, NProtoState} -> + stop(Reason, State#state{proto_state = NProtoState}) end; websocket_info({keepalive, check}, State = #state{keepalive = KeepAlive}) -> @@ -275,100 +282,242 @@ websocket_info({keepalive, check}, State = #state{keepalive = KeepAlive}) -> {ok, KeepAlive1} -> {ok, State#state{keepalive = KeepAlive1}}; {error, timeout} -> - ?LOG(debug, "Keepalive Timeout!"), - shutdown(keepalive_timeout, State); + stop(keepalive_timeout, State); {error, Error} -> ?LOG(error, "Keepalive error: ~p", [Error]), - shutdown(keepalive_error, State) + stop(keepalive_error, State) end; +websocket_info({timeout, Timer, emit_stats}, + State = #state{stats_timer = Timer, + proto_state = ProtoState, + gc_state = GcState}) -> + ClientId = emqx_protocol:info(client_id, ProtoState), + ok = emqx_cm:set_chan_stats(ClientId, stats(State)), + NState = State#state{stats_timer = undefined}, + Limits = erlang:get(force_shutdown_policy), + case emqx_misc:conn_proc_mng_policy(Limits) of + continue -> + {ok, NState}; + hibernate -> + %% going to hibernate, reset gc stats + GcState1 = emqx_gc:reset(GcState), + {ok, NState#state{gc_state = GcState1}, hibernate}; + {shutdown, Reason} -> + ?LOG(error, "Shutdown exceptionally due to ~p", [Reason]), + stop(Reason, NState) + end; + +websocket_info({timeout, Timer, Msg}, + State = #state{proto_state = ProtoState}) -> + case emqx_protocol:handle_timeout(Timer, Msg, ProtoState) of + {ok, NProtoState} -> + {ok, State#state{proto_state = NProtoState}}; + {ok, Packets, NProtoState} -> + reply(enqueue(Packets, State#state{proto_state = NProtoState})); + {error, Reason} -> + stop(Reason, State); + {error, Reason, NProtoState} -> + stop(Reason, State#state{proto_state = NProtoState}) + end; + +websocket_info({subscribe, TopicFilters}, State) -> + handle_request({subscribe, TopicFilters}, State); + +websocket_info({unsubscribe, TopicFilters}, State) -> + handle_request({unsubscribe, TopicFilters}, State); + websocket_info({shutdown, discard, {ClientId, ByPid}}, State) -> ?LOG(warning, "Discarded by ~s:~p", [ClientId, ByPid]), - shutdown(discard, State); + stop(discard, State); websocket_info({shutdown, conflict, {ClientId, NewPid}}, State) -> ?LOG(warning, "Clientid '~s' conflict with ~p", [ClientId, NewPid]), - shutdown(conflict, State); + stop(conflict, State); -websocket_info({binary, Data}, State) -> - {reply, {binary, Data}, State}; +%% websocket_info({binary, Data}, State) -> +%% {reply, {binary, Data}, State}; websocket_info({shutdown, Reason}, State) -> - shutdown(Reason, State); + stop(Reason, State); -websocket_info(stop, State) -> - {stop, State}; - -websocket_info(Info = {'EXIT', SessionPid, Reason}, State = #state{proto_state = ProtoState}) -> - case emqx_protocol:session(ProtoState) of - undefined -> - ?LOG(error, "Unexpected EXIT: ~p", [Info]), - {ok, State}; - SessionPid -> - ?LOG(error, "Session ~p termiated: ~p", [SessionPid, Reason]), - shutdown(Reason, State) - end; +websocket_info({stop, Reason}, State) -> + stop(Reason, State); websocket_info(Info, State) -> ?LOG(error, "Unexpected info: ~p", [Info]), {ok, State}. -terminate(WsReason, _Req, #state{keepalive = Keepalive, - proto_state = ProtoState, - shutdown = Shutdown}) -> - ?LOG(debug, "Terminated for ~p, websocket reason: ~p", - [Shutdown, WsReason]), +terminate(SockError, _Req, #state{keepalive = Keepalive, + proto_state = ProtoState, + reason = Reason}) -> + ?LOG(debug, "Terminated for ~p, sockerror: ~p", + [Reason, SockError]), emqx_keepalive:cancel(Keepalive), - case {ProtoState, Shutdown} of - {undefined, _} -> ok; - {_, {shutdown, Reason}} -> - terminate_session(Reason, ProtoState); - {_, _Error} -> - ?LOG(info, "Terminate for unexpected error: ~p", [WsReason]), - terminate_session(unknown, ProtoState) + emqx_protocol:terminate(Reason, ProtoState). + +%%-------------------------------------------------------------------- +%% 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), + 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. %%-------------------------------------------------------------------- -%% Internal functions -%%-------------------------------------------------------------------- +%% Ensure keepalive -terminate_session(Reason, ProtoState) -> - emqx_protocol:terminate(Reason, ProtoState), - case emqx_protocol:session(ProtoState) of - undefined -> - ok; - SessionPid -> - unlink(SessionPid), - SessionPid ! {'EXIT', self(), Reason} +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}) end. -handle_incoming(Packet, SuccFun, State = #state{proto_state = ProtoState}) -> - case emqx_protocol:received(Packet, ProtoState) of +%%-------------------------------------------------------------------- +%% Process incoming data + +process_incoming(<<>>, State) -> + {ok, State}; + +process_incoming(Data, State = #state{parse_state = ParseState}) -> + try emqx_frame:parse(Data, ParseState) of + {ok, NParseState} -> + {ok, State#state{parse_state = NParseState}}; + {ok, Packet, Rest, NParseState} -> + self() ! {incoming, Packet}, + process_incoming(Rest, State#state{parse_state = NParseState}); + {error, Reason} -> + ?LOG(error, "Frame error: ~p", [Reason]), + stop(Reason, State) + catch + error:Reason:Stk -> + ?LOG(error, "Parse failed for ~p~n\ + Stacktrace:~p~nFrame data: ~p", [Reason, Stk, Data]), + stop(parse_error, State) + end. + +%%-------------------------------------------------------------------- +%% Handle incoming packets + +handle_incoming(Packet = ?PACKET(Type), SuccFun, + State = #state{proto_state = ProtoState}) -> + _ = inc_incoming_stats(Type), + ok = emqx_metrics:inc_recv(Packet), + ?LOG(debug, "RECV ~s", [emqx_packet:format(Packet)]), + case emqx_protocol:handle_in(Packet, ProtoState) of {ok, NProtoState} -> SuccFun(State#state{proto_state = NProtoState}); - {error, Reason} -> - ?LOG(error, "Protocol error: ~p", [Reason]), - shutdown(Reason, State); + {ok, OutPackets, NProtoState} -> + SuccFun(enqueue(OutPackets, State#state{proto_state = NProtoState})); {error, Reason, NProtoState} -> - shutdown(Reason, State#state{proto_state = NProtoState}); + stop(Reason, State#state{proto_state = NProtoState}); + {error, Reason, OutPacket, NProtoState} -> + stop(Reason, enqueue(OutPacket, State#state{proto_state = NProtoState})); {stop, Error, NProtoState} -> - shutdown(Error, State#state{proto_state = NProtoState}) + stop(Error, State#state{proto_state = NProtoState}) end. -ensure_stats_timer(State = #state{enable_stats = true, - stats_timer = undefined, - idle_timeout = IdleTimeout}) -> - State#state{stats_timer = emqx_misc:start_timer(IdleTimeout, emit_stats)}; -ensure_stats_timer(State) -> - State. +%%-------------------------------------------------------------------- +%% Handle outgoing packets -shutdown(Reason = {shutdown, _}, State) -> - self() ! stop, - {ok, State#state{shutdown = Reason}}; -shutdown(Reason, State) -> - %% Fix the issue#2591(https://github.com/emqx/emqx/issues/2591#issuecomment-500278696) - self() ! stop, - {ok, State#state{shutdown = {shutdown, Reason}}}. +handle_outgoing(Packets, #state{serialize = Serialize}) -> + Data = lists:map(Serialize, Packets), + emqx_pd:update_counter(send_oct, iolist_size(Data)), + {binary, Data}. + +%%-------------------------------------------------------------------- +%% 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. + +%%-------------------------------------------------------------------- +%% Inc incoming/outgoing stats + +inc_incoming_stats(Type) -> + emqx_pd:update_counter(recv_pkt, 1), + (Type == ?PUBLISH) + andalso emqx_pd:update_counter(recv_msg, 1). + +inc_outgoing_stats(Type) -> + emqx_pd:update_counter(send_cnt, 1), + emqx_pd:update_counter(send_pkt, 1), + (Type == ?PUBLISH) + andalso emqx_pd:update_counter(send_msg, 1). + +%%-------------------------------------------------------------------- +%% Reply or Stop + +reply(State = #state{pendings = []}) -> + {ok, State}; +reply(State = #state{pendings = Pendings}) -> + Reply = handle_outgoing(Pendings, State), + {reply, Reply, State#state{pendings = []}}. + +stop(Reason, State = #state{pendings = []}) -> + {stop, State#state{reason = Reason}}; +stop(Reason, State = #state{pendings = Pendings}) -> + Reply = handle_outgoing(Pendings, State), + {reply, [Reply, close], + State#state{pendings = [], reason = Reason}}. + +enqueue(Packet, State) when is_record(Packet, mqtt_packet) -> + enqueue([Packet], State); +enqueue(Packets, State = #state{pendings = Pendings}) -> + State#state{pendings = lists:append(Pendings, Packets)}. + +%%-------------------------------------------------------------------- +%% Ensure stats timer + +ensure_stats_timer(State = #state{stats_timer = undefined, + idle_timeout = IdleTimeout}) -> + TRef = emqx_misc:start_timer(IdleTimeout, emit_stats), + State#state{stats_timer = TRef}; +%% disabled or timer existed +ensure_stats_timer(State) -> State. wsock_stats() -> [{Key, emqx_pd:get_counter(Key)} || Key <- ?SOCK_STATS]. + +chan_stats() -> + [{Name, emqx_pd:get_counter(Name)} || Name <- ?CHAN_STATS]. + +%%-------------------------------------------------------------------- +%% Maybe GC + +maybe_gc(_Cnt, _Oct, State = #state{gc_state = undefined}) -> + State; +maybe_gc(Cnt, Oct, State = #state{gc_state = GCSt}) -> + {Ok, GCSt1} = emqx_gc:run(Cnt, Oct, GCSt), + Ok andalso emqx_metrics:inc('channel.gc.cnt'), + State#state{gc_state = GCSt1}. + diff --git a/src/emqx_zone.erl b/src/emqx_zone.erl index d654487c5..bac32b849 100644 --- a/src/emqx_zone.erl +++ b/src/emqx_zone.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_zone). @@ -28,6 +30,7 @@ -export([ get_env/2 , get_env/3 , set_env/3 + , unset_env/2 , force_reload/0 ]). @@ -43,28 +46,32 @@ , code_change/3 ]). +-export_type([zone/0]). + %% dummy state -record(state, {}). +-type(zone() :: atom()). + -define(TAB, ?MODULE). -define(SERVER, ?MODULE). -define(KEY(Zone, Key), {?MODULE, Zone, Key}). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% APIs -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- -spec(start_link() -> startlink_ret()). start_link() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). --spec(get_env(maybe(emqx_types:zone()), atom()) -> maybe(term())). +-spec(get_env(maybe(zone()), atom()) -> maybe(term())). get_env(undefined, Key) -> emqx_config:get_env(Key); get_env(Zone, Key) -> get_env(Zone, Key, undefined). --spec(get_env(maybe(emqx_types:zone()), atom(), term()) -> maybe(term())). +-spec(get_env(maybe(zone()), atom(), term()) -> maybe(term())). get_env(undefined, Key, Def) -> emqx_config:get_env(Key, Def); get_env(Zone, Key, Def) -> @@ -73,9 +80,13 @@ get_env(Zone, Key, Def) -> emqx_config:get_env(Key, Def) end. --spec(set_env(emqx_types:zone(), atom(), term()) -> ok). +-spec(set_env(zone(), atom(), term()) -> ok). set_env(Zone, Key, Val) -> - gen_server:cast(?SERVER, {set_env, Zone, Key, Val}). + persistent_term:put(?KEY(Zone, Key), Val). + +-spec(unset_env(zone(), atom()) -> boolean()). +unset_env(Zone, Key) -> + persistent_term:erase(?KEY(Zone, Key)). -spec(force_reload() -> ok). force_reload() -> @@ -85,9 +96,9 @@ force_reload() -> stop() -> gen_server:stop(?SERVER, normal, infinity). -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% gen_server callbacks -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- init([]) -> _ = do_reload(), @@ -101,10 +112,6 @@ handle_call(Req, _From, State) -> ?LOG(error, "Unexpected call: ~p", [Req]), {reply, ignored, State}. -handle_cast({set_env, Zone, Key, Val}, State) -> - ok = persistent_term:put(?KEY(Zone, Key), Val), - {noreply, State}; - handle_cast(Msg, State) -> ?LOG(error, "Unexpected cast: ~p", [Msg]), {noreply, State}. @@ -119,11 +126,11 @@ terminate(_Reason, _State) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% Internal functions -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- do_reload() -> - [ persistent_term:put(?KEY(Zone, Key), Val) - || {Zone, Opts} <- emqx_config:get_env(zones, []), {Key, Val} <- Opts ]. + [persistent_term:put(?KEY(Zone, Key), Val) + || {Zone, Opts} <- emqx_config:get_env(zones, []), {Key, Val} <- Opts]. diff --git a/test/emqx_SUITE.erl b/test/emqx_SUITE.erl deleted file mode 100644 index a74e16552..000000000 --- a/test/emqx_SUITE.erl +++ /dev/null @@ -1,206 +0,0 @@ -%% 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_SUITE). - --compile(export_all). --compile(nowarn_export_all). - --define(APP, emqx). - --include_lib("eunit/include/eunit.hrl"). - --include_lib("common_test/include/ct.hrl"). - --include("emqx_mqtt.hrl"). - --record(ssl_socket, {tcp, ssl}). - - --define(CLIENT, ?CONNECT_PACKET(#mqtt_packet_connect{ - client_id = <<"mqtt_client">>, - username = <<"admin">>, - password = <<"public">>})). - --define(CLIENT2, ?CONNECT_PACKET(#mqtt_packet_connect{ - username = <<"admin">>, - clean_start = false, - password = <<"public">>})). - --define(CLIENT3, ?CONNECT_PACKET(#mqtt_packet_connect{ - username = <<"admin">>, - proto_ver = ?MQTT_PROTO_V5, - clean_start = false, - password = <<"public">>, - will_props = #{'Will-Delay-Interval' => 2}})). - --define(SUBCODE, [0]). - --define(PACKETID, 1). - --define(PUBQOS, 1). - --define(SUBPACKET, ?SUBSCRIBE_PACKET(?PACKETID, [{<<"sub/topic">>, ?DEFAULT_SUBOPTS}])). - --define(PUBPACKET, ?PUBLISH_PACKET(?PUBQOS, <<"sub/topic">>, ?PACKETID, <<"publish">>)). - --define(PAYLOAD, [{type,"dsmSimulationData"}, - {id, 9999}, - {status, "running"}, - {soc, 1536702170}, - {fracsec, 451000}, - {data, lists:seq(1, 20480)}]). - --define(BIG_PUBPACKET, ?PUBLISH_PACKET(?PUBQOS, <<"sub/topic">>, ?PACKETID, emqx_json:encode(?PAYLOAD))). - -all() -> - [{group, connect}, - {group, publish}]. - -groups() -> - [{connect, [non_parallel_tests], - [mqtt_connect, - mqtt_connect_with_tcp, - mqtt_connect_with_will_props, - mqtt_connect_with_ssl_oneway, - mqtt_connect_with_ssl_twoway, - mqtt_connect_with_ws]}, - {publish, [non_parallel_tests], - [packet_size]}]. - -init_per_suite(Config) -> - emqx_ct_helpers:start_apps([]), - Config. - -end_per_suite(_Config) -> - emqx_ct_helpers:stop_apps([]). - -%%-------------------------------------------------------------------- -%% Protocol Test -%%-------------------------------------------------------------------- -mqtt_connect(_) -> - %% Issue #599 - %% Empty clientId and clean_session = false - ?assertEqual(<<32,2,0,2>>, connect_broker_(<<16,12,0,4,77,81,84,84,4,0,0,90,0,0>>, 4)), - %% Empty clientId and clean_session = true - ?assertEqual(<<32,2,0,0>>, connect_broker_(<<16,12,0,4,77,81,84,84,4,2,0,90,0,0>>, 4)). - -connect_broker_(Packet, RecvSize) -> - {ok, Sock} = emqx_client_sock:connect({127,0,0,1}, 1883, [binary, {packet, raw}, {active, false}], 3000), - emqx_client_sock:send(Sock, Packet), - {ok, Data} = gen_tcp:recv(Sock, RecvSize, 3000), - emqx_client_sock:close(Sock), - Data. - -mqtt_connect_with_tcp(_) -> - %% Issue #599 - %% Empty clientId and clean_session = false - {ok, Sock} = emqx_client_sock:connect({127,0,0,1}, 1883, [binary, {packet, raw}, {active, false}], 3000), - Packet = raw_send_serialize(?CLIENT2), - emqx_client_sock:send(Sock, Packet), - {ok, Data} = gen_tcp:recv(Sock, 0), - {ok, ?CONNACK_PACKET(?CONNACK_INVALID_ID), <<>>, _} = raw_recv_pase(Data), - emqx_client_sock:close(Sock). - -mqtt_connect_with_will_props(_) -> - %% Issue #599 - %% Empty clientId and clean_session = false - {ok, Sock} = emqx_client_sock:connect({127,0,0,1}, 1883, [binary, {packet, raw}, {active, false}], 3000), - Packet = raw_send_serialize(?CLIENT3), - emqx_client_sock:send(Sock, Packet), - emqx_client_sock:close(Sock). - -mqtt_connect_with_ssl_oneway(_) -> - emqx:shutdown(), - emqx_ct_helpers:change_emqx_opts(ssl_oneway), - emqx:start(), - ClientSsl = emqx_ct_helpers:client_ssl(), - {ok, #ssl_socket{tcp = _Sock1, ssl = SslSock} = Sock} - = emqx_client_sock:connect("127.0.0.1", 8883, [{ssl_opts, ClientSsl}], 3000), - Packet = raw_send_serialize(?CLIENT), - emqx_client_sock:setopts(Sock, [{active, once}]), - emqx_client_sock:send(Sock, Packet), - ?assert( - receive {ssl, _, ConAck}-> - {ok, ?CONNACK_PACKET(?CONNACK_ACCEPT), <<>>, _} = raw_recv_pase(ConAck), true - after 1000 -> - false - end), - ssl:close(SslSock). - -mqtt_connect_with_ssl_twoway(_Config) -> - emqx:shutdown(), - emqx_ct_helpers:change_emqx_opts(ssl_twoway), - emqx:start(), - ClientSsl = emqx_ct_helpers:client_ssl_twoway(), - {ok, #ssl_socket{tcp = _Sock1, ssl = SslSock} = Sock} - = emqx_client_sock:connect("127.0.0.1", 8883, [{ssl_opts, ClientSsl}], 3000), - Packet = raw_send_serialize(?CLIENT), - emqx_client_sock:setopts(Sock, [{active, once}]), - emqx_client_sock:send(Sock, Packet), - timer:sleep(500), - ?assert( - receive {ssl, _, Data}-> - {ok, ?CONNACK_PACKET(?CONNACK_ACCEPT), <<>>, _} = raw_recv_pase(Data), true - after 1000 -> - false - end), - ssl:close(SslSock), - emqx_client_sock:close(Sock). - -mqtt_connect_with_ws(_Config) -> - WS = rfc6455_client:new("ws://127.0.0.1:8083" ++ "/mqtt", self()), - {ok, _} = rfc6455_client:open(WS), - - %% Connect Packet - Packet = raw_send_serialize(?CLIENT), - ok = rfc6455_client:send_binary(WS, Packet), - {binary, CONACK} = rfc6455_client:recv(WS), - {ok, ?CONNACK_PACKET(?CONNACK_ACCEPT), <<>>, _} = raw_recv_pase(CONACK), - - %% Sub Packet - SubPacket = raw_send_serialize(?SUBPACKET), - rfc6455_client:send_binary(WS, SubPacket), - {binary, SubAck} = rfc6455_client:recv(WS), - {ok, ?SUBACK_PACKET(?PACKETID, ?SUBCODE), <<>>, _} = raw_recv_pase(SubAck), - - %% Pub Packet QoS 1 - PubPacket = raw_send_serialize(?PUBPACKET), - rfc6455_client:send_binary(WS, PubPacket), - {binary, PubAck} = rfc6455_client:recv(WS), - {ok, ?PUBACK_PACKET(?PACKETID), <<>>, _} = raw_recv_pase(PubAck), - {close, _} = rfc6455_client:close(WS), - ok. - -%%issue 1811 -packet_size(_Config) -> - {ok, Sock} = emqx_client_sock:connect({127,0,0,1}, 1883, [binary, {packet, raw}, {active, false}], 3000), - Packet = raw_send_serialize(?CLIENT), - emqx_client_sock:send(Sock, Packet), - {ok, Data} = gen_tcp:recv(Sock, 0), - {ok, ?CONNACK_PACKET(?CONNACK_ACCEPT), <<>>, _} = raw_recv_pase(Data), - - %% Pub Packet QoS 1 - PubPacket = raw_send_serialize(?BIG_PUBPACKET), - emqx_client_sock:send(Sock, PubPacket), - {ok, Data1} = gen_tcp:recv(Sock, 0), - {ok, ?PUBACK_PACKET(?PACKETID), <<>>, _} = raw_recv_pase(Data1), - emqx_client_sock:close(Sock). - -raw_send_serialize(Packet) -> - emqx_frame:serialize(Packet). - -raw_recv_pase(Bin) -> - emqx_frame:parse(Bin). - diff --git a/test/emqx_access_SUITE.erl b/test/emqx_access_SUITE.erl index 9be887ce3..c98973d22 100644 --- a/test/emqx_access_SUITE.erl +++ b/test/emqx_access_SUITE.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_access_SUITE). @@ -19,41 +21,46 @@ -include("emqx.hrl"). --include_lib("common_test/include/ct.hrl"). -include_lib("eunit/include/eunit.hrl"). +-include_lib("common_test/include/ct.hrl"). -define(AC, emqx_access_control). -define(CACHE, emqx_acl_cache). --import(emqx_access_rule, [compile/1, match/3]). +-import(emqx_access_rule, + [ compile/1 + , match/3 + ]). all() -> [{group, access_control}, {group, acl_cache}, {group, access_control_cache_mode}, - {group, access_rule}]. + {group, access_rule} + ]. groups() -> [{access_control, [sequence], - [reload_acl, - check_acl_1, - check_acl_2]}, - {access_control_cache_mode, [], - [acl_cache_basic, - acl_cache_expiry, - acl_cache_cleanup, - acl_cache_full]}, - {acl_cache, [], - [put_get_del_cache, - cache_update, - cache_expiry, - cache_replacement, - cache_cleanup, - cache_auto_emtpy, - cache_auto_cleanup]}, - {access_rule, [], - [compile_rule, - match_rule]}]. + [t_reload_acl, + t_check_acl_1, + t_check_acl_2]}, + {access_control_cache_mode, [sequence], + [t_acl_cache_basic, + t_acl_cache_expiry, + t_acl_cache_cleanup, + t_acl_cache_full]}, + {acl_cache, [sequence], + [t_put_get_del_cache, + t_cache_update, + t_cache_expiry, + t_cache_replacement, + t_cache_cleanup, + t_cache_auto_emtpy, + t_cache_auto_cleanup]}, + {access_rule, [parallel], + [t_compile_rule, + t_match_rule] + }]. init_per_suite(Config) -> emqx_ct_helpers:start_apps([]), @@ -100,62 +107,76 @@ end_per_group(_Group, Config) -> %% emqx_access_control %%-------------------------------------------------------------------- -reload_acl(_) -> +t_reload_acl(_) -> ok = ?AC:reload_acl(). -check_acl_1(_) -> - SelfUser = #{client_id => <<"client1">>, username => <<"testuser">>, zone => external}, - allow = ?AC:check_acl(SelfUser, subscribe, <<"users/testuser/1">>), - allow = ?AC:check_acl(SelfUser, subscribe, <<"clients/client1">>), - deny = ?AC:check_acl(SelfUser, subscribe, <<"clients/client1/x/y">>), - allow = ?AC:check_acl(SelfUser, publish, <<"users/testuser/1">>), - allow = ?AC:check_acl(SelfUser, subscribe, <<"a/b/c">>). -check_acl_2(_) -> - SelfUser = #{client_id => <<"client2">>, username => <<"xyz">>, zone => external}, - deny = ?AC:check_acl(SelfUser, subscribe, <<"a/b/c">>). +t_check_acl_1(_) -> + Client = #{zone => external, + client_id => <<"client1">>, + username => <<"testuser">> + }, + allow = ?AC:check_acl(Client, subscribe, <<"users/testuser/1">>), + allow = ?AC:check_acl(Client, subscribe, <<"clients/client1">>), + deny = ?AC:check_acl(Client, subscribe, <<"clients/client1/x/y">>), + allow = ?AC:check_acl(Client, publish, <<"users/testuser/1">>), + allow = ?AC:check_acl(Client, subscribe, <<"a/b/c">>). -acl_cache_basic(_) -> - SelfUser = #{client_id => <<"client1">>, username => <<"testuser">>, zone => external}, +t_check_acl_2(_) -> + Client = #{zone => external, + client_id => <<"client2">>, + username => <<"xyz">> + }, + deny = ?AC:check_acl(Client, subscribe, <<"a/b/c">>). + +t_acl_cache_basic(_) -> + Client = #{zone => external, + client_id => <<"client1">>, + username => <<"testuser">> + }, not_found = ?CACHE:get_acl_cache(subscribe, <<"users/testuser/1">>), not_found = ?CACHE:get_acl_cache(subscribe, <<"clients/client1">>), - allow = ?AC:check_acl(SelfUser, subscribe, <<"users/testuser/1">>), - allow = ?AC:check_acl(SelfUser, subscribe, <<"clients/client1">>), + allow = ?AC:check_acl(Client, subscribe, <<"users/testuser/1">>), + allow = ?AC:check_acl(Client, subscribe, <<"clients/client1">>), allow = ?CACHE:get_acl_cache(subscribe, <<"users/testuser/1">>), - allow = ?CACHE:get_acl_cache(subscribe, <<"clients/client1">>), - ok. + allow = ?CACHE:get_acl_cache(subscribe, <<"clients/client1">>). -acl_cache_expiry(_) -> +t_acl_cache_expiry(_) -> application:set_env(emqx, acl_cache_ttl, 100), - SelfUser = #{client_id => <<"client1">>, username => <<"testuser">>, zone => external}, - allow = ?AC:check_acl(SelfUser, subscribe, <<"clients/client1">>), + Client = #{zone => external, + client_id => <<"client1">>, + username => <<"testuser">> + }, + allow = ?AC:check_acl(Client, subscribe, <<"clients/client1">>), allow = ?CACHE:get_acl_cache(subscribe, <<"clients/client1">>), ct:sleep(150), - not_found = ?CACHE:get_acl_cache(subscribe, <<"clients/client1">>), - ok. + not_found = ?CACHE:get_acl_cache(subscribe, <<"clients/client1">>). -acl_cache_full(_) -> +t_acl_cache_full(_) -> application:set_env(emqx, acl_cache_max_size, 1), - - SelfUser = #{client_id => <<"client1">>, username => <<"testuser">>, zone => external}, - allow = ?AC:check_acl(SelfUser, subscribe, <<"users/testuser/1">>), - allow = ?AC:check_acl(SelfUser, subscribe, <<"clients/client1">>), + Client = #{zone => external, + client_id => <<"client1">>, + username => <<"testuser">> + }, + allow = ?AC:check_acl(Client, subscribe, <<"users/testuser/1">>), + allow = ?AC:check_acl(Client, subscribe, <<"clients/client1">>), %% the older ones (the <<"users/testuser/1">>) will be evicted first not_found = ?CACHE:get_acl_cache(subscribe, <<"users/testuser/1">>), - allow = ?CACHE:get_acl_cache(subscribe, <<"clients/client1">>), - ok. + allow = ?CACHE:get_acl_cache(subscribe, <<"clients/client1">>). -acl_cache_cleanup(_) -> +t_acl_cache_cleanup(_) -> %% The acl cache will try to evict memory, if the size is full and the newest %% cache entry is expired application:set_env(emqx, acl_cache_ttl, 100), application:set_env(emqx, acl_cache_max_size, 2), - - SelfUser = #{client_id => <<"client1">>, username => <<"testuser">>, zone => external}, - allow = ?AC:check_acl(SelfUser, subscribe, <<"users/testuser/1">>), - allow = ?AC:check_acl(SelfUser, subscribe, <<"clients/client1">>), + Client = #{zone => external, + client_id => <<"client1">>, + username => <<"testuser">> + }, + allow = ?AC:check_acl(Client, subscribe, <<"users/testuser/1">>), + allow = ?AC:check_acl(Client, subscribe, <<"clients/client1">>), allow = ?CACHE:get_acl_cache(subscribe, <<"users/testuser/1">>), allow = ?CACHE:get_acl_cache(subscribe, <<"clients/client1">>), @@ -164,14 +185,13 @@ acl_cache_cleanup(_) -> %% now the cache is full and the newest one - "clients/client1" %% should be expired, so we'll empty the cache before putting %% the next cache entry - deny = ?AC:check_acl(SelfUser, subscribe, <<"#">>), + deny = ?AC:check_acl(Client, subscribe, <<"#">>), not_found = ?CACHE:get_acl_cache(subscribe, <<"users/testuser/1">>), not_found = ?CACHE:get_acl_cache(subscribe, <<"clients/client1">>), - deny = ?CACHE:get_acl_cache(subscribe, <<"#">>), - ok. + deny = ?CACHE:get_acl_cache(subscribe, <<"#">>). -put_get_del_cache(_) -> +t_put_get_del_cache(_) -> application:set_env(emqx, acl_cache_ttl, 300000), application:set_env(emqx, acl_cache_max_size, 30), @@ -186,7 +206,7 @@ put_get_del_cache(_) -> 2 = ?CACHE:get_cache_size(), ?assertEqual(?CACHE:cache_k(subscribe, <<"b">>), ?CACHE:get_newest_key()). -cache_expiry(_) -> +t_cache_expiry(_) -> application:set_env(emqx, acl_cache_ttl, 100), application:set_env(emqx, acl_cache_max_size, 30), ok = ?CACHE:put_acl_cache(subscribe, <<"a">>, allow), @@ -201,7 +221,7 @@ cache_expiry(_) -> ct:sleep(150), not_found = ?CACHE:get_acl_cache(subscribe, <<"a">>). -cache_update(_) -> +t_cache_update(_) -> application:set_env(emqx, acl_cache_ttl, 300000), application:set_env(emqx, acl_cache_max_size, 30), [] = ?CACHE:dump_acl_cache(), @@ -220,7 +240,7 @@ cache_update(_) -> ?assertEqual(?CACHE:cache_k(publish, <<"b">>), ?CACHE:get_newest_key()), ?assertEqual(?CACHE:cache_k(subscribe, <<"a">>), ?CACHE:get_oldest_key()). -cache_replacement(_) -> +t_cache_replacement(_) -> application:set_env(emqx, acl_cache_ttl, 300000), application:set_env(emqx, acl_cache_max_size, 3), ok = ?CACHE:put_acl_cache(subscribe, <<"a">>, allow), @@ -246,7 +266,7 @@ cache_replacement(_) -> not_found = ?CACHE:get_acl_cache(publish, <<"b">>), allow = ?CACHE:get_acl_cache(publish, <<"c">>). -cache_cleanup(_) -> +t_cache_cleanup(_) -> application:set_env(emqx, acl_cache_ttl, 100), application:set_env(emqx, acl_cache_max_size, 30), ok = ?CACHE:put_acl_cache(subscribe, <<"a">>, allow), @@ -259,7 +279,7 @@ cache_cleanup(_) -> ?assertEqual(?CACHE:cache_k(publish, <<"c">>), ?CACHE:get_oldest_key()), 1 = ?CACHE:get_cache_size(). -cache_auto_emtpy(_) -> +t_cache_auto_emtpy(_) -> %% verify cache is emptied when cache full and even the newest %% one is expired. application:set_env(emqx, acl_cache_ttl, 100), @@ -273,7 +293,7 @@ cache_auto_emtpy(_) -> ok = ?CACHE:put_acl_cache(subscribe, <<"d">>, deny), 1 = ?CACHE:get_cache_size(). -cache_auto_cleanup(_) -> +t_cache_auto_cleanup(_) -> %% verify we'll cleanup expired entries when we got a exipired acl %% from cache. application:set_env(emqx, acl_cache_ttl, 100), @@ -297,7 +317,7 @@ cache_auto_cleanup(_) -> %% emqx_access_rule %%-------------------------------------------------------------------- -compile_rule(_) -> +t_compile_rule(_) -> {allow, {'and', [{ipaddr, {{127,0,0,1}, {127,0,0,1}, 32}}, {user, <<"user">>}]}, subscribe, [ [<<"$SYS">>, '#'], ['#'] ]} = compile({allow, {'and', [{ipaddr, "127.0.0.1"}, {user, <<"user">>}]}, subscribe, ["$SYS/#", "#"]}), @@ -322,23 +342,36 @@ compile_rule(_) -> {allow, all} = compile({allow, all}), {deny, all} = compile({deny, all}). -match_rule(_) -> - User = #{client_id => <<"testClient">>, username => <<"TestUser">>, peername => {{127,0,0,1}, 2948}, zone => external}, - User2 = #{client_id => <<"testClient">>, username => <<"TestUser">>, peername => {{192,168,0,10}, 3028}, zone => external}, - - {matched, allow} = match(User, <<"Test/Topic">>, {allow, all}), - {matched, deny} = match(User, <<"Test/Topic">>, {deny, all}), - {matched, allow} = match(User, <<"Test/Topic">>, compile({allow, {ipaddr, "127.0.0.1"}, subscribe, ["$SYS/#", "#"]})), - {matched, allow} = match(User2, <<"Test/Topic">>, compile({allow, {ipaddr, "192.168.0.1/24"}, subscribe, ["$SYS/#", "#"]})), - {matched, allow} = match(User, <<"d/e/f/x">>, compile({allow, {user, "TestUser"}, subscribe, ["a/b/c", "d/e/f/#"]})), - nomatch = match(User, <<"d/e/f/x">>, compile({allow, {user, "admin"}, pubsub, ["d/e/f/#"]})), - {matched, allow} = match(User, <<"testTopics/testClient">>, compile({allow, {client, "testClient"}, publish, ["testTopics/testClient"]})), - {matched, allow} = match(User, <<"clients/testClient">>, compile({allow, all, pubsub, ["clients/%c"]})), - {matched, allow} = match(#{username => <<"user2">>}, <<"users/user2/abc/def">>, compile({allow, all, subscribe, ["users/%u/#"]})), - {matched, deny} = match(User, <<"d/e/f">>, compile({deny, all, subscribe, ["$SYS/#", "#"]})), +t_match_rule(_) -> + Client1 = #{zone => external, + client_id => <<"testClient">>, + username => <<"TestUser">>, + peername => {{127,0,0,1}, 2948} + }, + Client2 = #{zone => external, + client_id => <<"testClient">>, + username => <<"TestUser">>, + peername => {{192,168,0,10}, 3028} + }, + {matched, allow} = match(Client1, <<"Test/Topic">>, {allow, all}), + {matched, deny} = match(Client1, <<"Test/Topic">>, {deny, all}), + {matched, allow} = match(Client1, <<"Test/Topic">>, + compile({allow, {ipaddr, "127.0.0.1"}, subscribe, ["$SYS/#", "#"]})), + {matched, allow} = match(Client2, <<"Test/Topic">>, + compile({allow, {ipaddr, "192.168.0.1/24"}, subscribe, ["$SYS/#", "#"]})), + {matched, allow} = match(Client1, <<"d/e/f/x">>, + compile({allow, {user, "TestUser"}, subscribe, ["a/b/c", "d/e/f/#"]})), + nomatch = match(Client1, <<"d/e/f/x">>, compile({allow, {user, "admin"}, pubsub, ["d/e/f/#"]})), + {matched, allow} = match(Client1, <<"testTopics/testClient">>, + compile({allow, {client, "testClient"}, publish, ["testTopics/testClient"]})), + {matched, allow} = match(Client1, <<"clients/testClient">>, compile({allow, all, pubsub, ["clients/%c"]})), + {matched, allow} = match(#{username => <<"user2">>}, <<"users/user2/abc/def">>, + compile({allow, all, subscribe, ["users/%u/#"]})), + {matched, deny} = match(Client1, <<"d/e/f">>, compile({deny, all, subscribe, ["$SYS/#", "#"]})), Rule = compile({allow, {'and', [{ipaddr, "127.0.0.1"}, {user, <<"WrongUser">>}]}, publish, <<"Topic">>}), - nomatch = match(User, <<"Topic">>, Rule), + nomatch = match(Client1, <<"Topic">>, Rule), AndRule = compile({allow, {'and', [{ipaddr, "127.0.0.1"}, {user, <<"TestUser">>}]}, publish, <<"Topic">>}), - {matched, allow} = match(User, <<"Topic">>, AndRule), + {matched, allow} = match(Client1, <<"Topic">>, AndRule), OrRule = compile({allow, {'or', [{ipaddr, "127.0.0.1"}, {user, <<"WrongUser">>}]}, publish, ["Topic"]}), - {matched, allow} = match(User, <<"Topic">>, OrRule). + {matched, allow} = match(Client1, <<"Topic">>, OrRule). + diff --git a/test/emqx_acl_test_mod.erl b/test/emqx_acl_test_mod.erl index 343202a96..75386453f 100644 --- a/test/emqx_acl_test_mod.erl +++ b/test/emqx_acl_test_mod.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,11 +12,16 @@ %% 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_acl_test_mod). %% ACL callbacks --export([init/1, check_acl/2, reload_acl/1, description/0]). +-export([ init/1 + , check_acl/2 + , reload_acl/1 + , description/0 + ]). init(AclOpts) -> {ok, AclOpts}. diff --git a/test/emqx_alarm_handler_SUITE.erl b/test/emqx_alarm_handler_SUITE.erl index f6aba8a1f..91cde5c11 100644 --- a/test/emqx_alarm_handler_SUITE.erl +++ b/test/emqx_alarm_handler_SUITE.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,20 +12,18 @@ %% 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_alarm_handler_SUITE). -compile(export_all). -compile(nowarn_export_all). +-include("emqx.hrl"). +-include("emqx_mqtt.hrl"). -include_lib("eunit/include/eunit.hrl"). --include_lib("common_test/include/ct.hrl"). - --include("emqx_mqtt.hrl"). --include("emqx.hrl"). - -all() -> [t_alarm_handler]. +all() -> emqx_ct:all(?MODULE). init_per_suite(Config) -> emqx_ct_helpers:start_apps([], fun set_special_configs/1), @@ -34,9 +33,57 @@ end_per_suite(_Config) -> emqx_ct_helpers:stop_apps([]). set_special_configs(emqx) -> - application:set_env(emqx, acl_file, emqx_ct_helpers:deps_path(emqx, "test/emqx_access_SUITE_data/acl_deny_action.conf")); -set_special_configs(_App) -> - ok. + AclFile = emqx_ct_helpers:deps_path(emqx, "test/emqx_access_SUITE_data/acl_deny_action.conf"), + application:set_env(emqx, acl_file, AclFile); +set_special_configs(_App) -> ok. + +t_alarm_handler(_) -> + with_connection( + fun(Sock) -> + emqx_client_sock:send(Sock, + raw_send_serialize( + ?CONNECT_PACKET( + #mqtt_packet_connect{ + proto_ver = ?MQTT_PROTO_V5}) + )), + {ok, Data} = gen_tcp:recv(Sock, 0), + {ok, ?CONNACK_PACKET(?RC_SUCCESS), <<>>, _} = raw_recv_parse(Data), + + 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, + raw_send_serialize( + ?SUBSCRIBE_PACKET( + 1, + [{Topic1, SubOpts}, + {Topic2, SubOpts}]) + )), + + {ok, Data2} = gen_tcp:recv(Sock, 0), + {ok, ?SUBACK_PACKET(1, #{}, [2, 2]), <<>>, _} = raw_recv_parse(Data2), + + alarm_handler:set_alarm({alarm_for_test, #alarm{id = alarm_for_test, + severity = error, + title="alarm title", + summary="alarm summary" + }}), + + {ok, Data3} = gen_tcp:recv(Sock, 0), + + {ok, ?PUBLISH_PACKET(?QOS_0, Topic1, _, _), <<>>, _} = raw_recv_parse(Data3), + + ?assertEqual(true, lists:keymember(alarm_for_test, 1, emqx_alarm_handler:get_alarms())), + + alarm_handler:clear_alarm(alarm_for_test), + + {ok, Data4} = gen_tcp:recv(Sock, 0), + + {ok, ?PUBLISH_PACKET(?QOS_0, Topic2, _, _), <<>>, _} = raw_recv_parse(Data4), + + ?assertEqual(false, lists:keymember(alarm_for_test, 1, emqx_alarm_handler:get_alarms())) + + end). with_connection(DoFun) -> {ok, Sock} = emqx_client_sock:connect({127, 0, 0, 1}, 1883, @@ -48,61 +95,9 @@ with_connection(DoFun) -> emqx_client_sock:close(Sock) end. -t_alarm_handler(_) -> - with_connection( - fun(Sock) -> - emqx_client_sock:send(Sock, - raw_send_serialize( - ?CONNECT_PACKET( - #mqtt_packet_connect{ - proto_ver = ?MQTT_PROTO_V5}), - #{version => ?MQTT_PROTO_V5} - )), - {ok, Data} = gen_tcp:recv(Sock, 0), - {ok, ?CONNACK_PACKET(?RC_SUCCESS), <<>>, _} = raw_recv_parse(Data, ?MQTT_PROTO_V5), - - 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, - raw_send_serialize( - ?SUBSCRIBE_PACKET( - 1, - [{Topic1, SubOpts}, - {Topic2, SubOpts}]), - #{version => ?MQTT_PROTO_V5})), - - {ok, Data2} = gen_tcp:recv(Sock, 0), - {ok, ?SUBACK_PACKET(1, #{}, [2, 2]), <<>>, _} = raw_recv_parse(Data2, ?MQTT_PROTO_V5), - - alarm_handler:set_alarm({alarm_for_test, #alarm{id = alarm_for_test, - severity = error, - title="alarm title", - summary="alarm summary"}}), - - {ok, Data3} = gen_tcp:recv(Sock, 0), - - {ok, ?PUBLISH_PACKET(?QOS_0, Topic1, _, _), <<>>, _} = raw_recv_parse(Data3, ?MQTT_PROTO_V5), - - ?assertEqual(true, lists:keymember(alarm_for_test, 1, emqx_alarm_handler:get_alarms())), - - alarm_handler:clear_alarm(alarm_for_test), - - {ok, Data4} = gen_tcp:recv(Sock, 0), - - {ok, ?PUBLISH_PACKET(?QOS_0, Topic2, _, _), <<>>, _} = raw_recv_parse(Data4, ?MQTT_PROTO_V5), - - ?assertEqual(false, lists:keymember(alarm_for_test, 1, emqx_alarm_handler:get_alarms())) - - end). - raw_send_serialize(Packet) -> - emqx_frame:serialize(Packet). + emqx_frame:serialize(Packet, ?MQTT_PROTO_V5). -raw_send_serialize(Packet, Opts) -> - emqx_frame:serialize(Packet, Opts). - -raw_recv_parse(Bin, ProtoVer) -> - emqx_frame:parse(Bin, {none, #{max_size => ?MAX_PACKET_SIZE, - version => ProtoVer}}). +raw_recv_parse(Bin) -> + emqx_frame:parse(Bin, emqx_frame:initial_parse_state(#{version => ?MQTT_PROTO_V5})). diff --git a/test/emqx_auth_anonymous_test_mod.erl b/test/emqx_auth_anonymous_test_mod.erl deleted file mode 100644 index 60ee4ad5f..000000000 --- a/test/emqx_auth_anonymous_test_mod.erl +++ /dev/null @@ -1,27 +0,0 @@ -%% 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_auth_anonymous_test_mod). - -%% ACL callbacks --export([init/1, check/3, description/0]). - -init(AclOpts) -> - {ok, AclOpts}. - -check(_Client, _Password, _Opts) -> - allow. - -description() -> - "Test emqx_auth_anonymous Mod". diff --git a/test/emqx_auth_dashboard.erl b/test/emqx_auth_dashboard.erl deleted file mode 100644 index 00c8bdec5..000000000 --- a/test/emqx_auth_dashboard.erl +++ /dev/null @@ -1,28 +0,0 @@ -%% 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_auth_dashboard). - -%% Auth callbacks --export([init/1, check/3, description/0]). - -init(Opts) -> - {ok, Opts}. - -check(_Client, _Password, _Opts) -> - allow. - -description() -> - "Test Auth Mod". - diff --git a/test/emqx_banned_SUITE.erl b/test/emqx_banned_SUITE.erl index 02b057ebf..1dc8e0dcb 100644 --- a/test/emqx_banned_SUITE.erl +++ b/test/emqx_banned_SUITE.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_banned_SUITE). @@ -18,35 +20,63 @@ -compile(nowarn_export_all). -include("emqx.hrl"). --include("emqx_mqtt.hrl"). -include_lib("eunit/include/eunit.hrl"). -all() -> [t_banned_all]. +all() -> emqx_ct:all(?MODULE). -t_banned_all(_) -> - emqx_ct_helpers:start_apps([]), - emqx_banned:start_link(), - TimeNow = erlang:system_time(second), +init_per_suite(Config) -> + application:load(emqx), + ok = ekka:start(), + Config. + +end_per_suite(_Config) -> + ekka:stop(), + ekka_mnesia:ensure_stopped(), + ekka_mnesia:delete_schema(). + +t_add_delete(_) -> Banned = #banned{who = {client_id, <<"TestClient">>}, reason = <<"test">>, by = <<"banned suite">>, desc = <<"test">>, - until = TimeNow + 1}, + until = erlang:system_time(second) + 1000 + }, ok = emqx_banned:add(Banned), - % here is not expire banned test because its check interval is greater than 5 mins, but its effect has been confirmed - ?assert(emqx_banned:check(#{client_id => <<"TestClient">>, - username => undefined, - peername => {undefined, undefined}})), - timer:sleep(2500), - ?assertNot(emqx_banned:check(#{client_id => <<"TestClient">>, - username => undefined, - peername => {undefined, undefined}})), - ok = emqx_banned:add(Banned), - ?assert(emqx_banned:check(#{client_id => <<"TestClient">>, - username => undefined, - peername => {undefined, undefined}})), - emqx_banned:delete({client_id, <<"TestClient">>}), - ?assertNot(emqx_banned:check(#{client_id => <<"TestClient">>, - username => undefined, - peername => {undefined, undefined}})), - emqx_ct_helpers:stop_apps([]). + ?assertEqual(1, emqx_banned:info(size)), + ok = emqx_banned:delete({client_id, <<"TestClient">>}), + ?assertEqual(0, emqx_banned:info(size)). + +t_check(_) -> + ok = emqx_banned:add(#banned{who = {client_id, <<"BannedClient">>}}), + ok = emqx_banned:add(#banned{who = {username, <<"BannedUser">>}}), + ok = emqx_banned:add(#banned{who = {ipaddr, {192,168,0,1}}}), + ?assertEqual(3, emqx_banned:info(size)), + Client1 = #{client_id => <<"BannedClient">>, + username => <<"user">>, + peername => {{127,0,0,1}, 5000} + }, + Client2 = #{client_id => <<"client">>, + username => <<"BannedUser">>, + peername => {{127,0,0,1}, 5000} + }, + Client3 = #{client_id => <<"client">>, + username => <<"user">>, + peername => {{192,168,0,1}, 5000} + }, + Client4 = #{client_id => <<"client">>, + username => <<"user">>, + peername => {{127,0,0,1}, 5000} + }, + ?assert(emqx_banned:check(Client1)), + ?assert(emqx_banned:check(Client2)), + ?assert(emqx_banned:check(Client3)), + ?assertNot(emqx_banned:check(Client4)), + ok = emqx_banned:delete({client_id, <<"BannedClient">>}), + ok = emqx_banned:delete({username, <<"BannedUser">>}), + ok = emqx_banned:delete({ipaddr, {192,168,0,1}}), + ?assertNot(emqx_banned:check(Client1)), + ?assertNot(emqx_banned:check(Client2)), + ?assertNot(emqx_banned:check(Client3)), + ?assertNot(emqx_banned:check(Client4)), + ?assertEqual(0, emqx_banned:info(size)). + diff --git a/test/emqx_batch_SUITE.erl b/test/emqx_batch_SUITE.erl index 277459cfe..e45e89c83 100644 --- a/test/emqx_batch_SUITE.erl +++ b/test/emqx_batch_SUITE.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_batch_SUITE). @@ -19,11 +21,13 @@ -include_lib("eunit/include/eunit.hrl"). -all() -> - [batch_full_commit, batch_linger_commit]. +all() -> emqx_ct:all(?MODULE). -batch_full_commit(_) -> - B0 = emqx_batch:init(#{batch_size => 3, linger_ms => 2000, commit_fun => fun(_) -> ok end}), +t_batch_full_commit(_) -> + B0 = emqx_batch:init(#{batch_size => 3, + linger_ms => 2000, + commit_fun => fun(_) -> ok end + }), B3 = lists:foldl(fun(E, B) -> emqx_batch:push(E, B) end, B0, [a, b, c]), ?assertEqual(3, emqx_batch:size(B3)), ?assertEqual([a, b, c], emqx_batch:items(B3)), @@ -32,9 +36,12 @@ batch_full_commit(_) -> ?assertEqual(0, emqx_batch:size(B4)), ?assertEqual([], emqx_batch:items(B4)). -batch_linger_commit(_) -> +t_batch_linger_commit(_) -> CommitFun = fun(Q) -> ?assertEqual(3, length(Q)) end, - B0 = emqx_batch:init(#{batch_size => 3, linger_ms => 500, commit_fun => CommitFun}), + B0 = emqx_batch:init(#{batch_size => 3, + linger_ms => 500, + commit_fun => CommitFun + }), B3 = lists:foldl(fun(E, B) -> emqx_batch:push(E, B) end, B0, [a, b, c]), ?assertEqual(3, emqx_batch:size(B3)), ?assertEqual([a, b, c], emqx_batch:items(B3)), diff --git a/test/emqx_broker_SUITE.erl b/test/emqx_broker_SUITE.erl index 14cd9e4ca..89832e2de 100644 --- a/test/emqx_broker_SUITE.erl +++ b/test/emqx_broker_SUITE.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_broker_SUITE). @@ -27,19 +29,24 @@ all() -> [{group, pubsub}, - {group, session}, {group, metrics}, {group, stats}]. groups() -> - [{pubsub, [sequence], [subscribe_unsubscribe, - publish, pubsub, - t_shared_subscribe, - dispatch_with_no_sub, - 'pubsub#', 'pubsub+']}, - {session, [sequence], [start_session]}, - {metrics, [sequence], [inc_dec_metric]}, - {stats, [sequence], [set_get_stat]}]. + [{pubsub, [sequence], + [t_sub_unsub, + t_publish, + t_pubsub, + t_shared_subscribe, + t_dispatch_with_no_sub, + 't_pubsub#', + 't_pubsub+' + ]}, + {metrics, [sequence], + [inc_dec_metric]}, + {stats, [sequence], + [set_get_stat] + }]. init_per_suite(Config) -> emqx_ct_helpers:start_apps([]), @@ -52,47 +59,49 @@ end_per_suite(_Config) -> %% PubSub Test %%-------------------------------------------------------------------- -subscribe_unsubscribe(_) -> - ok = emqx:subscribe(<<"topic">>, <<"clientId">>), - ok = emqx:subscribe(<<"topic/1">>, <<"clientId">>, #{ qos => 1 }), - ok = emqx:subscribe(<<"topic/2">>, <<"clientId">>, #{ qos => 2 }), - true = emqx:subscribed(<<"clientId">>, <<"topic">>), - Topics = emqx:topics(), +t_sub_unsub(_) -> + ok = emqx_broker:subscribe(<<"topic">>, <<"clientId">>), + ok = emqx_broker:subscribe(<<"topic/1">>, <<"clientId">>, #{qos => 1}), + ok = emqx_broker:subscribe(<<"topic/2">>, <<"clientId">>, #{qos => 2}), + true = emqx_broker:subscribed(<<"clientId">>, <<"topic">>), + Topics = emqx_broker:topics(), lists:foreach(fun(Topic) -> ?assert(lists:member(Topic, Topics)) end, Topics), - ok = emqx:unsubscribe(<<"topic">>), - ok = emqx:unsubscribe(<<"topic/1">>), - ok = emqx:unsubscribe(<<"topic/2">>). + ok = emqx_broker:unsubscribe(<<"topic">>), + ok = emqx_broker:unsubscribe(<<"topic/1">>), + ok = emqx_broker:unsubscribe(<<"topic/2">>). -publish(_) -> +t_publish(_) -> Msg = emqx_message:make(ct, <<"test/pubsub">>, <<"hello">>), - ok = emqx:subscribe(<<"test/+">>), + ok = emqx_broker:subscribe(<<"test/+">>), timer:sleep(10), - emqx:publish(Msg), - ?assert(receive {dispatch, <<"test/+">>, #message{payload = <<"hello">>}} -> true after 100 -> false end). + emqx_broker:publish(Msg), + ?assert(receive {deliver, <<"test/+">>, #message{payload = <<"hello">>}} -> true after 100 -> false end). -dispatch_with_no_sub(_) -> +t_dispatch_with_no_sub(_) -> Msg = emqx_message:make(ct, <<"no_subscribers">>, <<"hello">>), - Delivery = #delivery{sender = self(), message = Msg, results = []}, - ?assertEqual(Delivery, emqx_broker:route([{<<"no_subscribers">>, node()}], Delivery)). + Delivery = #delivery{sender = self(), message = Msg}, + ?assertEqual([{node(),<<"no_subscribers">>,{error,no_subscribers}}], + emqx_broker:route([{<<"no_subscribers">>, node()}], Delivery)). -pubsub(_) -> +t_pubsub(_) -> true = emqx:is_running(node()), Self = self(), Subscriber = <<"clientId">>, - ok = emqx:subscribe(<<"a/b/c">>, Subscriber, #{ qos => 1 }), + ok = emqx_broker:subscribe(<<"a/b/c">>, Subscriber, #{ qos => 1 }), #{qos := 1} = ets:lookup_element(emqx_suboption, {Self, <<"a/b/c">>}, 2), #{qos := 1} = emqx_broker:get_subopts(Subscriber, <<"a/b/c">>), true = emqx_broker:set_subopts(<<"a/b/c">>, #{qos => 0}), #{qos := 0} = emqx_broker:get_subopts(Subscriber, <<"a/b/c">>), - ok = emqx:subscribe(<<"a/b/c">>, Subscriber, #{ qos => 2 }), + ok = emqx_broker:subscribe(<<"a/b/c">>, Subscriber, #{ qos => 2 }), %% ct:log("Emq Sub: ~p.~n", [ets:lookup(emqx_suboption, {<<"a/b/c">>, Subscriber})]), timer:sleep(10), [Self] = emqx_broker:subscribers(<<"a/b/c">>), - emqx:publish(emqx_message:make(ct, <<"a/b/c">>, <<"hello">>)), + emqx_broker:publish( + emqx_message:make(ct, <<"a/b/c">>, <<"hello">>)), ?assert( - receive {dispatch, <<"a/b/c">>, _ } -> + receive {deliver, <<"a/b/c">>, _ } -> true; P -> ct:log("Receive Message: ~p~n",[P]) @@ -100,62 +109,43 @@ pubsub(_) -> false end), spawn(fun() -> - emqx:subscribe(<<"a/b/c">>), - emqx:subscribe(<<"c/d/e">>), + emqx_broker:subscribe(<<"a/b/c">>), + emqx_broker:subscribe(<<"c/d/e">>), timer:sleep(10), - emqx:unsubscribe(<<"a/b/c">>) + emqx_broker:unsubscribe(<<"a/b/c">>) end), timer:sleep(20), - emqx:unsubscribe(<<"a/b/c">>). + emqx_broker:unsubscribe(<<"a/b/c">>). t_shared_subscribe(_) -> - emqx:subscribe("$share/group2/topic2"), - emqx:subscribe("$queue/topic3"), + emqx_broker:subscribe(<<"$share/group2/topic2">>), + emqx_broker:subscribe(<<"$queue/topic3">>), timer:sleep(10), - ct:log("share subscriptions: ~p~n", [emqx:subscriptions(self())]), - ?assertEqual(2, length(emqx:subscriptions(self()))), - emqx:unsubscribe("$share/group2/topic2"), - emqx:unsubscribe("$queue/topic3"), - ?assertEqual(0, length(emqx:subscriptions(self()))). + ct:pal("Share subscriptions: ~p", + [emqx_broker:subscriptions(self())]), + ?assertEqual(2, length(emqx_broker:subscriptions(self()))), + emqx_broker:unsubscribe(<<"$share/group2/topic2">>), + emqx_broker:unsubscribe(<<"$queue/topic3">>), + ?assertEqual(0, length(emqx_broker:subscriptions(self()))). -'pubsub#'(_) -> - emqx:subscribe(<<"a/#">>), +'t_pubsub#'(_) -> + emqx_broker:subscribe(<<"a/#">>), timer:sleep(10), - emqx:publish(emqx_message:make(ct, <<"a/b/c">>, <<"hello">>)), - ?assert(receive {dispatch, <<"a/#">>, _} -> true after 100 -> false end), - emqx:unsubscribe(<<"a/#">>). + emqx_broker:publish(emqx_message:make(ct, <<"a/b/c">>, <<"hello">>)), + ?assert(receive {deliver, <<"a/#">>, _} -> true after 100 -> false end), + emqx_broker:unsubscribe(<<"a/#">>). -'pubsub+'(_) -> - emqx:subscribe(<<"a/+/+">>), - timer:sleep(10), - emqx:publish(emqx_message:make(ct, <<"a/b/c">>, <<"hello">>)), - ?assert(receive {dispatch, <<"a/+/+">>, _} -> true after 100 -> false end), - emqx:unsubscribe(<<"a/+/+">>). - -%%-------------------------------------------------------------------- -%% Session Group -%%-------------------------------------------------------------------- -start_session(_) -> - ClientId = <<"clientId">>, - {ok, ClientPid} = emqx_mock_client:start_link(ClientId), - {ok, SessPid} = emqx_mock_client:open_session(ClientPid, ClientId, internal), - Message1 = emqx_message:make(<<"clientId">>, 2, <<"topic">>, <<"hello">>), - emqx_session:publish(SessPid, 1, Message1), - emqx_session:pubrel(SessPid, 2, reasoncode), - emqx_session:subscribe(SessPid, [{<<"topic/session">>, #{qos => 2}}]), - Message2 = emqx_message:make(<<"clientId">>, 1, <<"topic/session">>, <<"test">>), - emqx_session:publish(SessPid, 3, Message2), - emqx_session:unsubscribe(SessPid, [{<<"topic/session">>, []}]), - %% emqx_mock_client:stop(ClientPid). - emqx_mock_client:close_session(ClientPid). - -%%-------------------------------------------------------------------- -%% Broker Group -%%-------------------------------------------------------------------- +'t_pubsub+'(_) -> + emqx_broker:subscribe(<<"a/+/+">>), + timer:sleep(10), %% TODO: why sleep? + emqx_broker:publish(emqx_message:make(ct, <<"a/b/c">>, <<"hello">>)), + ?assert(receive {deliver, <<"a/+/+">>, _} -> true after 100 -> false end), + emqx_broker:unsubscribe(<<"a/+/+">>). %%-------------------------------------------------------------------- %% Metric Group %%-------------------------------------------------------------------- + inc_dec_metric(_) -> emqx_metrics:inc('messages.retained', 10), emqx_metrics:dec('messages.retained', 10). @@ -166,4 +156,5 @@ inc_dec_metric(_) -> set_get_stat(_) -> emqx_stats:setstat('retained.max', 99), - 99 = emqx_stats:getstat('retained.max'). + ?assertEqual(99, emqx_stats:getstat('retained.max')). + diff --git a/test/emqx_channel_SUITE.erl b/test/emqx_channel_SUITE.erl index 7d8b216f2..580b7a0fa 100644 --- a/test/emqx_channel_SUITE.erl +++ b/test/emqx_channel_SUITE.erl @@ -19,14 +19,9 @@ -compile(export_all). -compile(nowarn_export_all). --include("emqx_mqtt.hrl"). - -include_lib("eunit/include/eunit.hrl"). --include_lib("common_test/include/ct.hrl"). - -all() -> - [t_connect_api]. +all() -> emqx_ct:all(?MODULE). init_per_suite(Config) -> emqx_ct_helpers:start_apps([]), @@ -35,40 +30,28 @@ init_per_suite(Config) -> end_per_suite(_Config) -> emqx_ct_helpers:stop_apps([]). -t_connect_api(_Config) -> - {ok, T1} = emqx_client:start_link([{host, "localhost"}, - {client_id, <<"client1">>}, - {username, <<"testuser1">>}, - {password, <<"pass1">>}]), - {ok, _} = emqx_client:connect(T1), - CPid = emqx_cm:lookup_conn_pid(<<"client1">>), - ConnStats = emqx_channel:stats(CPid), - ok = t_stats(ConnStats), - ConnAttrs = emqx_channel:attrs(CPid), - ok = t_attrs(ConnAttrs), - ConnInfo = emqx_channel:info(CPid), - ok = t_info(ConnInfo), - SessionPid = emqx_channel:session(CPid), - true = is_pid(SessionPid), - emqx_client:disconnect(T1). +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). -t_info(ConnInfo) -> - ?assertEqual(tcp, maps:get(socktype, ConnInfo)), - ?assertEqual(running, maps:get(conn_state, ConnInfo)), - ?assertEqual(<<"client1">>, maps:get(client_id, ConnInfo)), - ?assertEqual(<<"testuser1">>, maps:get(username, ConnInfo)), - ?assertEqual(<<"MQTT">>, maps:get(proto_name, ConnInfo)). +recv_msgs(Count) -> + recv_msgs(Count, []). -t_attrs(AttrsData) -> - ?assertEqual(<<"client1">>, maps:get(client_id, AttrsData)), - ?assertEqual(emqx_channel, maps:get(conn_mod, AttrsData)), - ?assertEqual(<<"testuser1">>, maps:get(username, AttrsData)). +recv_msgs(0, Msgs) -> + Msgs; +recv_msgs(Count, Msgs) -> + receive + {publish, Msg} -> + recv_msgs(Count-1, [Msg|Msgs]) + after 100 -> + Msgs + end. -t_stats(StatsData) -> - ?assertEqual(true, proplists:get_value(recv_oct, StatsData) >= 0), - ?assertEqual(true, proplists:get_value(mailbox_len, StatsData) >= 0), - ?assertEqual(true, proplists:get_value(heap_size, StatsData) >= 0), - ?assertEqual(true, proplists:get_value(reductions, StatsData) >=0), - ?assertEqual(true, proplists:get_value(recv_pkt, StatsData) =:=1), - ?assertEqual(true, proplists:get_value(recv_msg, StatsData) >=0), - ?assertEqual(true, proplists:get_value(send_pkt, StatsData) =:=1). diff --git a/test/emqx_client_SUITE.erl b/test/emqx_client_SUITE.erl index b9f0daf10..3ed8a5dff 100644 --- a/test/emqx_client_SUITE.erl +++ b/test/emqx_client_SUITE.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_SUITE). @@ -20,30 +22,50 @@ -import(lists, [nth/2]). -include("emqx_mqtt.hrl"). - -include_lib("eunit/include/eunit.hrl"). - -include_lib("common_test/include/ct.hrl"). --define(TOPICS, [<<"TopicA">>, <<"TopicA/B">>, <<"Topic/C">>, <<"TopicA/C">>, - <<"/TopicA">>]). +-define(TOPICS, [<<"TopicA">>, + <<"TopicA/B">>, + <<"Topic/C">>, + <<"TopicA/C">>, + <<"/TopicA">> + ]). + +-define(WILD_TOPICS, [<<"TopicA/+">>, + <<"+/C">>, + <<"#">>, + <<"/#">>, + <<"/+">>, + <<"+/+">>, + <<"TopicA/#">> + ]). --define(WILD_TOPICS, [<<"TopicA/+">>, <<"+/C">>, <<"#">>, <<"/#">>, <<"/+">>, - <<"+/+">>, <<"TopicA/#">>]). all() -> - [{group, mqttv4}]. + [{group, mqttv3}, + {group, mqttv4}, + {group, mqttv5} + ]. groups() -> - [{mqttv4, [non_parallel_tests], - [basic_test, - will_message_test, - offline_message_queueing_test, - overlapping_subscriptions_test, - %% keepalive_test, - redelivery_on_reconnect_test, + [{mqttv3, [non_parallel_tests], + [t_basic_v3 + ]}, + {mqttv4, [non_parallel_tests], + [t_basic_v4, + %% t_will_message, + %% t_offline_message_queueing, + t_overlapping_subscriptions, + %% t_keepalive, + %% t_redelivery_on_reconnect, %% subscribe_failure_test, - dollar_topics_test]}]. + t_dollar_topics + ]}, + {mqttv5, [non_parallel_tests], + [t_basic_with_props_v5 + ]} + ]. init_per_suite(Config) -> emqx_ct_helpers:start_apps([]), @@ -52,53 +74,39 @@ init_per_suite(Config) -> end_per_suite(_Config) -> emqx_ct_helpers:stop_apps([]). -receive_messages(Count) -> - receive_messages(Count, []). +%%-------------------------------------------------------------------- +%% Test cases for MQTT v3 +%%-------------------------------------------------------------------- -receive_messages(0, Msgs) -> - Msgs; -receive_messages(Count, Msgs) -> - receive - {publish, Msg} -> - receive_messages(Count-1, [Msg|Msgs]); - _Other -> - receive_messages(Count, Msgs) - after 100 -> - Msgs - end. +t_basic_v3(_) -> + t_basic([{proto_ver, v3}]). -basic_test(_Config) -> - Topic = nth(1, ?TOPICS), - ct:print("Basic test starting"), - {ok, C} = emqx_client:start_link(), - {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), - ?assertEqual(3, length(receive_messages(3))), - ok = emqx_client:disconnect(C). +%%-------------------------------------------------------------------- +%% Test cases for MQTT v4 +%%-------------------------------------------------------------------- -will_message_test(_Config) -> +t_basic_v4(_Config) -> + t_basic([{proto_ver, v4}]). + +t_will_message(_Config) -> {ok, C1} = emqx_client:start_link([{clean_start, true}, - {will_topic, nth(3, ?TOPICS)}, - {will_payload, <<"client disconnected">>}, - {keepalive, 2}]), + {will_topic, nth(3, ?TOPICS)}, + {will_payload, <<"client disconnected">>}, + {keepalive, 1}]), {ok, _} = emqx_client:connect(C1), {ok, C2} = emqx_client:start_link(), {ok, _} = emqx_client:connect(C2), {ok, _, [2]} = emqx_client:subscribe(C2, nth(3, ?TOPICS), 2), - timer:sleep(10), + timer:sleep(5), ok = emqx_client:stop(C1), timer:sleep(5), - ?assertEqual(1, length(receive_messages(1))), + ?assertEqual(1, length(recv_msgs(1))), ok = emqx_client:disconnect(C2), - ct:print("Will message test succeeded"). + ct:pal("Will message test succeeded"). -offline_message_queueing_test(_) -> +t_offline_message_queueing(_) -> {ok, C1} = emqx_client:start_link([{clean_start, false}, {client_id, <<"c1">>}]), {ok, _} = emqx_client:connect(C1), @@ -115,14 +123,14 @@ offline_message_queueing_test(_) -> timer:sleep(10), emqx_client:disconnect(C2), {ok, C3} = emqx_client:start_link([{clean_start, false}, - {client_id, <<"c1">>}]), + {client_id, <<"c1">>}]), {ok, _} = emqx_client:connect(C3), timer:sleep(10), emqx_client:disconnect(C3), - ?assertEqual(3, length(receive_messages(3))). + ?assertEqual(3, length(recv_msgs(3))). -overlapping_subscriptions_test(_) -> +t_overlapping_subscriptions(_) -> {ok, C} = emqx_client:start_link([]), {ok, _} = emqx_client:connect(C), @@ -132,20 +140,20 @@ overlapping_subscriptions_test(_) -> {ok, _} = emqx_client:publish(C, nth(4, ?TOPICS), <<"overlapping topic filters">>, 2), timer:sleep(10), - Num = length(receive_messages(2)), + Num = length(recv_msgs(2)), ?assert(lists:member(Num, [1, 2])), if Num == 1 -> - ct:print("This server is publishing one message for all - matching overlapping subscriptions, not one for each."); + ct:pal("This server is publishing one message for all + matching overlapping subscriptions, not one for each."); Num == 2 -> - ct:print("This server is publishing one message per each - matching overlapping subscription."); + ct:pal("This server is publishing one message per each + matching overlapping subscription."); true -> ok end, emqx_client:disconnect(C). -%% keepalive_test(_) -> +%% t_keepalive_test(_) -> %% ct:print("Keepalive test starting"), %% {ok, C1, _} = emqx_client:start_link([{clean_start, true}, %% {keepalive, 5}, @@ -158,11 +166,11 @@ overlapping_subscriptions_test(_) -> %% {keepalive, 0}]), %% {ok, _, [2]} = emqx_client:subscribe(C2, nth(5, ?TOPICS), 2), %% ok = emqx_client:disconnect(C2), -%% ?assertEqual(1, length(receive_messages(1))), +%% ?assertEqual(1, length(recv_msgs(1))), %% ct:print("Keepalive test succeeded"). -redelivery_on_reconnect_test(_) -> - ct:print("Redelivery on reconnect test starting"), +t_redelivery_on_reconnect(_) -> + ct:pal("Redelivery on reconnect test starting"), {ok, C1} = emqx_client:start_link([{clean_start, false}, {client_id, <<"c">>}]), {ok, _} = emqx_client:connect(C1), @@ -176,24 +184,24 @@ redelivery_on_reconnect_test(_) -> [{qos, 2}, {retain, false}]), timer:sleep(10), ok = emqx_client:disconnect(C1), - ?assertEqual(0, length(receive_messages(2))), + ?assertEqual(0, length(recv_msgs(2))), {ok, C2} = emqx_client:start_link([{clean_start, false}, - {client_id, <<"c">>}]), + {client_id, <<"c">>}]), {ok, _} = emqx_client:connect(C2), timer:sleep(10), ok = emqx_client:disconnect(C2), - ?assertEqual(2, length(receive_messages(2))). + ?assertEqual(2, length(recv_msgs(2))). -%% subscribe_failure_test(_) -> +%% t_subscribe_sys_topics(_) -> %% ct:print("Subscribe failure test starting"), %% {ok, C, _} = emqx_client:start_link([]), %% {ok, _, [2]} = emqx_client:subscribe(C, <<"$SYS/#">>, 2), %% timer:sleep(10), %% ct:print("Subscribe failure test succeeded"). -dollar_topics_test(_) -> - ct:print("$ topics test starting"), +t_dollar_topics(_) -> + ct:pal("$ topics test starting"), {ok, C} = emqx_client:start_link([{clean_start, true}, {keepalive, 0}]), {ok, _} = emqx_client:connect(C), @@ -202,6 +210,49 @@ dollar_topics_test(_) -> {ok, _} = emqx_client:publish(C, << <<"$">>/binary, (nth(2, ?TOPICS))/binary>>, <<"test">>, [{qos, 1}, {retain, false}]), timer:sleep(10), - ?assertEqual(0, length(receive_messages(1))), + ?assertEqual(0, length(recv_msgs(1))), ok = emqx_client:disconnect(C), - ct:print("$ topics test succeeded"). + ct:pal("$ topics test succeeded"). + +%%-------------------------------------------------------------------- +%% Test cases for MQTT v5 +%%-------------------------------------------------------------------- + +t_basic_with_props_v5(_) -> + t_basic([{proto_ver, v5}, + {properties, #{'Receive-Maximum' => 4}} + ]). + +%%-------------------------------------------------------------------- +%% General test cases. +%%-------------------------------------------------------------------- + +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), + ?assertEqual(3, length(recv_msgs(3))), + ok = emqx_client:disconnect(C). + +%%-------------------------------------------------------------------- +%% Helper functions +%%-------------------------------------------------------------------- + +recv_msgs(Count) -> + recv_msgs(Count, []). + +recv_msgs(0, Msgs) -> + Msgs; +recv_msgs(Count, Msgs) -> + receive + {publish, Msg} -> + recv_msgs(Count-1, [Msg|Msgs]); + _Other -> recv_msgs(Count, Msgs) %%TODO:: remove the branch? + after 100 -> + Msgs + end. diff --git a/test/emqx_cm_SUITE.erl b/test/emqx_cm_SUITE.erl deleted file mode 100644 index 05e8f3514..000000000 --- a/test/emqx_cm_SUITE.erl +++ /dev/null @@ -1,70 +0,0 @@ - -%% 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_cm_SUITE). - --compile(export_all). --compile(nowarn_export_all). - --include("emqx.hrl"). --include("emqx_mqtt.hrl"). --include_lib("eunit/include/eunit.hrl"). - -all() -> [{group, cm}]. - -groups() -> - [{cm, [non_parallel_tests], - [t_get_set_conn_attrs, - t_get_set_conn_stats, - t_lookup_conn_pid]}]. - -init_per_suite(Config) -> - emqx_ct_helpers:start_apps([]), - Config. - -end_per_suite(_Config) -> - emqx_ct_helpers:stop_apps([]). - -init_per_testcase(_TestCase, Config) -> - register_connection(), - Config. - -end_per_testcase(_TestCase, _Config) -> - unregister_connection(), - ok. - -t_get_set_conn_attrs(_) -> - ?assert(emqx_cm:set_conn_attrs(<<"conn1">>, [{port, 8080}, {ip, "192.168.0.1"}])), - ?assert(emqx_cm:set_conn_attrs(<<"conn2">>, self(), [{port, 8080}, {ip, "192.168.0.2"}])), - ?assertEqual([{port, 8080}, {ip, "192.168.0.1"}], emqx_cm:get_conn_attrs(<<"conn1">>)), - ?assertEqual([{port, 8080}, {ip, "192.168.0.2"}], emqx_cm:get_conn_attrs(<<"conn2">>, self())). - -t_get_set_conn_stats(_) -> - ?assert(emqx_cm:set_conn_stats(<<"conn1">>, [{count, 1}, {max, 2}])), - ?assert(emqx_cm:set_conn_stats(<<"conn2">>, self(), [{count, 1}, {max, 2}])), - ?assertEqual([{count, 1}, {max, 2}], emqx_cm:get_conn_stats(<<"conn1">>)), - ?assertEqual([{count, 1}, {max, 2}], emqx_cm:get_conn_stats(<<"conn2">>, self())). - -t_lookup_conn_pid(_) -> - ?assertEqual(ok, emqx_cm:register_connection(<<"conn1">>, self())), - ?assertEqual(self(), emqx_cm:lookup_conn_pid(<<"conn1">>)). - -register_connection() -> - ?assertEqual(ok, emqx_cm:register_connection(<<"conn1">>)), - ?assertEqual(ok, emqx_cm:register_connection(<<"conn2">>, self())). - -unregister_connection() -> - ?assertEqual(ok, emqx_cm:unregister_connection(<<"conn1">>)), - ?assertEqual(ok, emqx_cm:unregister_connection(<<"conn2">>, self())). diff --git a/test/emqx_flapping_SUITE.erl b/test/emqx_flapping_SUITE.erl index 407bf6f6a..0d4bbc1e6 100644 --- a/test/emqx_flapping_SUITE.erl +++ b/test/emqx_flapping_SUITE.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,19 +12,14 @@ %% 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_flapping_SUITE). -compile(export_all). -compile(nowarn_export_all). --include("emqx.hrl"). - --include_lib("common_test/include/ct.hrl"). --include_lib("eunit/include/eunit.hrl"). - -all() -> - [t_flapping]. +all() -> emqx_ct:all(?MODULE). init_per_suite(Config) -> emqx_ct_helpers:start_apps([]), @@ -33,28 +29,27 @@ init_per_suite(Config) -> end_per_suite(_Config) -> emqx_ct_helpers:stop_apps([]). -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), - receive - {'EXIT', Client, _Reason} -> - ct:log("receive exit signal, Client: ~p", [Client]) - after 1000 -> - ct:log("timeout") - end. - +%% 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), +%% receive +%% {'EXIT', Client, _Reason} -> +%% ct:log("receive exit signal, Client: ~p", [Client]) +%% after 1000 -> +%% ct:log("timeout") +%% end. flapping_connect(Times) -> - [flapping_connect() || _ <- lists:seq(1, Times)]. + lists:foreach(fun do_connect/1, lists:seq(1, Times)). -flapping_connect() -> +do_connect(_I) -> {ok, C} = emqx_client:start_link([{client_id, <<"Client">>}]), {ok, _} = emqx_client:connect(C), ok = emqx_client:disconnect(C). prepare_for_test() -> - emqx_zone:set_env(external, enable_flapping_detect, true), - emqx_zone:set_env(external, flapping_threshold, {10, 60}), - emqx_zone:set_env(external, flapping_expiry_interval, 3600). + ok = emqx_zone:set_env(external, enable_flapping_detect, true), + ok = emqx_zone:set_env(external, flapping_threshold, {10, 60}), + ok = emqx_zone:set_env(external, flapping_expiry_interval, 3600). diff --git a/test/emqx_frame_SUITE.erl b/test/emqx_frame_SUITE.erl index 8de05e734..cde76bfb3 100644 --- a/test/emqx_frame_SUITE.erl +++ b/test/emqx_frame_SUITE.erl @@ -37,60 +37,60 @@ all() -> groups() -> [{connect, [parallel], - [serialize_parse_connect, - serialize_parse_v3_connect, - serialize_parse_v4_connect, - serialize_parse_v5_connect, - serialize_parse_connect_without_clientid, - serialize_parse_connect_with_will, - serialize_parse_bridge_connect + [t_serialize_parse_connect, + t_serialize_parse_v3_connect, + t_serialize_parse_v4_connect, + t_serialize_parse_v5_connect, + t_serialize_parse_connect_without_clientid, + t_serialize_parse_connect_with_will, + t_serialize_parse_bridge_connect ]}, {connack, [parallel], - [serialize_parse_connack, - serialize_parse_connack_v5 + [t_serialize_parse_connack, + t_serialize_parse_connack_v5 ]}, {publish, [parallel], - [serialize_parse_qos0_publish, - serialize_parse_qos1_publish, - serialize_parse_qos2_publish, - serialize_parse_publish_v5 + [t_serialize_parse_qos0_publish, + t_serialize_parse_qos1_publish, + t_serialize_parse_qos2_publish, + t_serialize_parse_publish_v5 ]}, {puback, [parallel], - [serialize_parse_puback, - serialize_parse_puback_v5, - serialize_parse_pubrec, - serialize_parse_pubrec_v5, - serialize_parse_pubrel, - serialize_parse_pubrel_v5, - serialize_parse_pubcomp, - serialize_parse_pubcomp_v5 + [t_serialize_parse_puback, + t_serialize_parse_puback_v5, + t_serialize_parse_pubrec, + t_serialize_parse_pubrec_v5, + t_serialize_parse_pubrel, + t_serialize_parse_pubrel_v5, + t_serialize_parse_pubcomp, + t_serialize_parse_pubcomp_v5 ]}, {subscribe, [parallel], - [serialize_parse_subscribe, - serialize_parse_subscribe_v5 + [t_serialize_parse_subscribe, + t_serialize_parse_subscribe_v5 ]}, {suback, [parallel], - [serialize_parse_suback, - serialize_parse_suback_v5 + [t_serialize_parse_suback, + t_serialize_parse_suback_v5 ]}, {unsubscribe, [parallel], - [serialize_parse_unsubscribe, - serialize_parse_unsubscribe_v5 + [t_serialize_parse_unsubscribe, + t_serialize_parse_unsubscribe_v5 ]}, {unsuback, [parallel], - [serialize_parse_unsuback, - serialize_parse_unsuback_v5 + [t_serialize_parse_unsuback, + t_serialize_parse_unsuback_v5 ]}, {ping, [parallel], - [serialize_parse_pingreq, - serialize_parse_pingresp + [t_serialize_parse_pingreq, + t_serialize_parse_pingresp ]}, {disconnect, [parallel], - [serialize_parse_disconnect, - serialize_parse_disconnect_v5 + [t_serialize_parse_disconnect, + t_serialize_parse_disconnect_v5 ]}, {auth, [parallel], - [serialize_parse_auth_v5] + [t_serialize_parse_auth_v5] }]. init_per_suite(Config) -> @@ -105,7 +105,7 @@ init_per_group(_Group, Config) -> end_per_group(_Group, _Config) -> ok. -serialize_parse_connect(_) -> +t_serialize_parse_connect(_) -> Packet1 = ?CONNECT_PACKET(#mqtt_packet_connect{}), ?assertEqual(Packet1, parse_serialize(Packet1)), Packet2 = ?CONNECT_PACKET(#mqtt_packet_connect{ @@ -119,7 +119,7 @@ serialize_parse_connect(_) -> }), ?assertEqual(Packet2, parse_serialize(Packet2)). -serialize_parse_v3_connect(_) -> +t_serialize_parse_v3_connect(_) -> Bin = <<16,37,0,6,77,81,73,115,100,112,3,2,0,60,0,23,109,111,115, 113,112,117, 98,47,49,48,52,53,49,45,105,77,97,99,46,108, 111,99,97>>, @@ -132,7 +132,7 @@ serialize_parse_v3_connect(_) -> }), ?assertMatch({ok, Packet, <<>>, _}, emqx_frame:parse(Bin)). -serialize_parse_v4_connect(_) -> +t_serialize_parse_v4_connect(_) -> Bin = <<16,35,0,4,77,81,84,84,4,2,0,60,0,23,109,111,115,113,112,117, 98,47,49,48,52,53,49,45,105,77,97,99,46,108,111,99,97>>, Packet = ?CONNECT_PACKET(#mqtt_packet_connect{proto_ver = 4, @@ -143,7 +143,7 @@ serialize_parse_v4_connect(_) -> ?assertEqual(Bin, serialize_to_binary(Packet)), ?assertMatch({ok, Packet, <<>>, _}, emqx_frame:parse(Bin)). -serialize_parse_v5_connect(_) -> +t_serialize_parse_v5_connect(_) -> Props = #{'Session-Expiry-Interval' => 60, 'Receive-Maximum' => 100, 'Maximum-QoS' => ?QOS_2, @@ -183,7 +183,7 @@ serialize_parse_v5_connect(_) -> }), ?assertEqual(Packet, parse_serialize(Packet)). -serialize_parse_connect_without_clientid(_) -> +t_serialize_parse_connect_without_clientid(_) -> Bin = <<16,12,0,4,77,81,84,84,4,2,0,60,0,0>>, Packet = ?CONNECT_PACKET( #mqtt_packet_connect{proto_ver = 4, @@ -195,7 +195,7 @@ serialize_parse_connect_without_clientid(_) -> ?assertEqual(Bin, serialize_to_binary(Packet)), ?assertMatch({ok, Packet, <<>>, _}, emqx_frame:parse(Bin)). -serialize_parse_connect_with_will(_) -> +t_serialize_parse_connect_with_will(_) -> Bin = <<16,67,0,6,77,81,73,115,100,112,3,206,0,60,0,23,109,111,115,113,112, 117,98,47,49,48,52,53,50,45,105,77,97,99,46,108,111,99,97,0,5,47,119, 105,108,108,0,7,119,105,108,108,109,115,103,0,4,116,101,115,116,0,6, @@ -217,7 +217,7 @@ serialize_parse_connect_with_will(_) -> ?assertEqual(Bin, serialize_to_binary(Packet)), ?assertMatch({ok, Packet, <<>>, _}, emqx_frame:parse(Bin)). -serialize_parse_bridge_connect(_) -> +t_serialize_parse_bridge_connect(_) -> Bin = <<16,86,0,6,77,81,73,115,100,112,131,44,0,60,0,19,67,95,48,48,58,48,67, 58,50,57,58,50,66,58,55,55,58,53,50,0,48,36,83,89,83,47,98,114,111,107, 101,114,47,99,111,110,110,101,99,116,105,111,110,47,67,95,48,48,58,48, @@ -239,12 +239,12 @@ serialize_parse_bridge_connect(_) -> ?assertEqual(Bin, serialize_to_binary(Packet)), ?assertMatch({ok, Packet, <<>>, _}, emqx_frame:parse(Bin)). -serialize_parse_connack(_) -> +t_serialize_parse_connack(_) -> Packet = ?CONNACK_PACKET(?RC_SUCCESS), ?assertEqual(<<32,2,0,0>>, serialize_to_binary(Packet)), ?assertEqual(Packet, parse_serialize(Packet)). -serialize_parse_connack_v5(_) -> +t_serialize_parse_connack_v5(_) -> Props = #{'Session-Expiry-Interval' => 60, 'Receive-Maximum' => 100, 'Maximum-QoS' => ?QOS_2, @@ -265,7 +265,7 @@ serialize_parse_connack_v5(_) -> Packet = ?CONNACK_PACKET(?RC_SUCCESS, 0, Props), ?assertEqual(Packet, parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). -serialize_parse_qos0_publish(_) -> +t_serialize_parse_qos0_publish(_) -> Bin = <<48,14,0,7,120,120,120,47,121,121,121,104,101,108,108,111>>, Packet = #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, dup = false, @@ -277,7 +277,7 @@ serialize_parse_qos0_publish(_) -> ?assertEqual(Bin, serialize_to_binary(Packet)), ?assertMatch({ok, Packet, <<>>, _}, emqx_frame:parse(Bin)). -serialize_parse_qos1_publish(_) -> +t_serialize_parse_qos1_publish(_) -> Bin = <<50,13,0,5,97,47,98,47,99,0,1,104,97,104,97>>, Packet = #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, dup = false, @@ -289,11 +289,11 @@ serialize_parse_qos1_publish(_) -> ?assertEqual(Bin, serialize_to_binary(Packet)), ?assertMatch({ok, Packet, <<>>, _}, emqx_frame:parse(Bin)). -serialize_parse_qos2_publish(_) -> +t_serialize_parse_qos2_publish(_) -> Packet = ?PUBLISH_PACKET(?QOS_2, <<"Topic">>, 1, payload()), ?assertEqual(Packet, parse_serialize(Packet)). -serialize_parse_publish_v5(_) -> +t_serialize_parse_publish_v5(_) -> Props = #{'Payload-Format-Indicator' => 1, 'Message-Expiry-Interval' => 60, 'Topic-Alias' => 16#AB, @@ -304,45 +304,45 @@ serialize_parse_publish_v5(_) -> Packet = ?PUBLISH_PACKET(?QOS_1, <<"$share/group/topic">>, 1, Props, <<"payload">>), ?assertEqual(Packet, parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). -serialize_parse_puback(_) -> +t_serialize_parse_puback(_) -> Packet = ?PUBACK_PACKET(1), ?assertEqual(<<64,2,0,1>>, serialize_to_binary(Packet)), ?assertEqual(Packet, parse_serialize(Packet)). -serialize_parse_puback_v5(_) -> +t_serialize_parse_puback_v5(_) -> Packet = ?PUBACK_PACKET(16, ?RC_SUCCESS, #{'Reason-String' => <<"success">>}), ?assertEqual(Packet, parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). -serialize_parse_pubrec(_) -> +t_serialize_parse_pubrec(_) -> Packet = ?PUBREC_PACKET(1), ?assertEqual(<<5:4,0:4,2,0,1>>, serialize_to_binary(Packet)), ?assertEqual(Packet, parse_serialize(Packet)). -serialize_parse_pubrec_v5(_) -> +t_serialize_parse_pubrec_v5(_) -> Packet = ?PUBREC_PACKET(16, ?RC_SUCCESS, #{'Reason-String' => <<"success">>}), ?assertEqual(Packet, parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). -serialize_parse_pubrel(_) -> +t_serialize_parse_pubrel(_) -> Packet = ?PUBREL_PACKET(1), Bin = serialize_to_binary(Packet), ?assertEqual(<<6:4,2:4,2,0,1>>, Bin), ?assertEqual(Packet, parse_serialize(Packet)). -serialize_parse_pubrel_v5(_) -> +t_serialize_parse_pubrel_v5(_) -> Packet = ?PUBREL_PACKET(16, ?RC_SUCCESS, #{'Reason-String' => <<"success">>}), ?assertEqual(Packet, parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). -serialize_parse_pubcomp(_) -> +t_serialize_parse_pubcomp(_) -> Packet = ?PUBCOMP_PACKET(1), Bin = serialize_to_binary(Packet), ?assertEqual(<<7:4,0:4,2,0,1>>, Bin), ?assertEqual(Packet, parse_serialize(Packet)). -serialize_parse_pubcomp_v5(_) -> +t_serialize_parse_pubcomp_v5(_) -> Packet = ?PUBCOMP_PACKET(16, ?RC_SUCCESS, #{'Reason-String' => <<"success">>}), ?assertEqual(Packet, parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). -serialize_parse_subscribe(_) -> +t_serialize_parse_subscribe(_) -> %% SUBSCRIBE(Q1, R0, D0, PacketId=2, TopicTable=[{<<"TopicA">>,2}]) Bin = <<130,11,0,2,0,6,84,111,112,105,99,65,2>>, TopicOpts = #{nl => 0 , rap => 0, rc => 0, rh => 0, qos => 2}, @@ -352,61 +352,61 @@ serialize_parse_subscribe(_) -> %%ct:log("Bin: ~p, Packet: ~p ~n", [Packet, parse(Bin)]), ?assertMatch({ok, Packet, <<>>, _}, emqx_frame:parse(Bin)). -serialize_parse_subscribe_v5(_) -> +t_serialize_parse_subscribe_v5(_) -> TopicFilters = [{<<"TopicQos0">>, #{rh => 1, qos => ?QOS_2, rap => 0, nl => 0, rc => 0}}, {<<"TopicQos1">>, #{rh => 1, qos => ?QOS_2, rap => 0, nl => 0, rc => 0}}], Packet = ?SUBSCRIBE_PACKET(3, #{'Subscription-Identifier' => 16#FFFFFFF}, TopicFilters), ?assertEqual(Packet, parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). -serialize_parse_suback(_) -> +t_serialize_parse_suback(_) -> Packet = ?SUBACK_PACKET(10, [?QOS_0, ?QOS_1, 128]), ?assertEqual(Packet, parse_serialize(Packet)). -serialize_parse_suback_v5(_) -> +t_serialize_parse_suback_v5(_) -> Packet = ?SUBACK_PACKET(1, #{'Reason-String' => <<"success">>, 'User-Property' => [{<<"key">>, <<"value">>}]}, [?QOS_0, ?QOS_1, 128]), ?assertEqual(Packet, parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). -serialize_parse_unsubscribe(_) -> +t_serialize_parse_unsubscribe(_) -> %% UNSUBSCRIBE(Q1, R0, D0, PacketId=2, TopicTable=[<<"TopicA">>]) Packet = ?UNSUBSCRIBE_PACKET(2, [<<"TopicA">>]), Bin = <<162,10,0,2,0,6,84,111,112,105,99,65>>, ?assertEqual(Bin, serialize_to_binary(Packet)), ?assertMatch({ok, Packet, <<>>, _}, emqx_frame:parse(Bin)). -serialize_parse_unsubscribe_v5(_) -> +t_serialize_parse_unsubscribe_v5(_) -> Props = #{'User-Property' => [{<<"key">>, <<"val">>}]}, Packet = ?UNSUBSCRIBE_PACKET(10, Props, [<<"Topic1">>, <<"Topic2">>]), ?assertEqual(Packet, parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). -serialize_parse_unsuback(_) -> +t_serialize_parse_unsuback(_) -> Packet = ?UNSUBACK_PACKET(10), ?assertEqual(Packet, parse_serialize(Packet)). -serialize_parse_unsuback_v5(_) -> +t_serialize_parse_unsuback_v5(_) -> Packet = ?UNSUBACK_PACKET(10, #{'Reason-String' => <<"Not authorized">>, 'User-Property' => [{<<"key">>, <<"val">>}]}, [16#87, 16#87, 16#87]), ?assertEqual(Packet, parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). -serialize_parse_pingreq(_) -> +t_serialize_parse_pingreq(_) -> PingReq = ?PACKET(?PINGREQ), ?assertEqual(PingReq, parse_serialize(PingReq)). -serialize_parse_pingresp(_) -> +t_serialize_parse_pingresp(_) -> PingResp = ?PACKET(?PINGRESP), ?assertEqual(PingResp, parse_serialize(PingResp)). -parse_disconnect(_) -> +t_parse_disconnect(_) -> Packet = ?DISCONNECT_PACKET(?RC_SUCCESS), ?assertMatch({ok, Packet, <<>>, _}, emqx_frame:parse(<<224, 0>>)). -serialize_parse_disconnect(_) -> +t_serialize_parse_disconnect(_) -> Packet = ?DISCONNECT_PACKET(?RC_SUCCESS), ?assertEqual(Packet, parse_serialize(Packet)). -serialize_parse_disconnect_v5(_) -> +t_serialize_parse_disconnect_v5(_) -> Packet = ?DISCONNECT_PACKET(?RC_SUCCESS, #{'Session-Expiry-Interval' => 60, 'Reason-String' => <<"server_moved">>, @@ -414,7 +414,7 @@ serialize_parse_disconnect_v5(_) -> }), ?assertEqual(Packet, parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). -serialize_parse_auth_v5(_) -> +t_serialize_parse_auth_v5(_) -> Packet = ?AUTH_PACKET(?RC_SUCCESS, #{'Authentication-Method' => <<"oauth2">>, 'Authentication-Data' => <<"3zekkd">>, @@ -427,7 +427,8 @@ parse_serialize(Packet) -> parse_serialize(Packet, #{}). parse_serialize(Packet, Opts) when is_map(Opts) -> - Bin = iolist_to_binary(emqx_frame:serialize(Packet, Opts)), + Ver = maps:get(version, Opts, ?MQTT_PROTO_V4), + Bin = iolist_to_binary(emqx_frame:serialize(Packet, Ver)), ParseState = emqx_frame:initial_parse_state(Opts), {ok, NPacket, <<>>, _} = emqx_frame:parse(Bin, ParseState), NPacket. diff --git a/test/emqx_gc_SUITE.erl b/test/emqx_gc_SUITE.erl index e75cb2320..1cdecebba 100644 --- a/test/emqx_gc_SUITE.erl +++ b/test/emqx_gc_SUITE.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_gc_SUITE). @@ -19,8 +21,7 @@ -include_lib("eunit/include/eunit.hrl"). -all() -> - [t_init, t_run, t_info, t_reset]. +all() -> emqx_ct:all(?MODULE). t_init(_) -> ?assertEqual(undefined, emqx_gc:init(false)), @@ -32,6 +33,9 @@ t_init(_) -> ?assertEqual(#{cnt => {10, 10}, oct => {10, 10}}, emqx_gc:info(GC3)). t_run(_) -> + Undefined = emqx_gc:init(false), + ?assertEqual(undefined, Undefined), + ?assertEqual({false, undefined}, emqx_gc:run(1, 1, Undefined)), GC = emqx_gc:init(#{count => 10, bytes => 10}), ?assertEqual({true, GC}, emqx_gc:run(1, 1000, GC)), ?assertEqual({true, GC}, emqx_gc:run(1000, 1, GC)), @@ -41,7 +45,10 @@ t_run(_) -> ?assertEqual(#{cnt => {10, 7}, oct => {10, 7}}, emqx_gc:info(GC2)), {false, GC3} = emqx_gc:run(3, 3, GC2), ?assertEqual(#{cnt => {10, 4}, oct => {10, 4}}, emqx_gc:info(GC3)), - ?assertEqual({true, GC}, emqx_gc:run(4, 4, GC3)). + ?assertEqual({true, GC}, emqx_gc:run(4, 4, GC3)), + %% Disabled? + DisabledGC = emqx_gc:init(#{count => 0, bytes => 0}), + ?assertEqual({false, DisabledGC}, emqx_gc:run(1, 1, DisabledGC)). t_info(_) -> ?assertEqual(undefined, emqx_gc:info(undefined)), @@ -53,5 +60,7 @@ t_reset(_) -> GC = emqx_gc:init(#{count => 10, bytes => 10}), {false, GC1} = emqx_gc:run(5, 5, GC), ?assertEqual(#{cnt => {10, 5}, oct => {10, 5}}, emqx_gc:info(GC1)), - ?assertEqual(GC, emqx_gc:reset(GC1)). + ?assertEqual(GC, emqx_gc:reset(GC1)), + DisabledGC = emqx_gc:init(#{count => 0, bytes => 0}), + ?assertEqual(DisabledGC, emqx_gc:reset(DisabledGC)). diff --git a/test/emqx_guid_SUITE.erl b/test/emqx_guid_SUITE.erl index f412138ad..004661b72 100644 --- a/test/emqx_guid_SUITE.erl +++ b/test/emqx_guid_SUITE.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,24 +12,25 @@ %% 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_guid_SUITE). --include_lib("eunit/include/eunit.hrl"). - -compile(export_all). -compile(nowarn_export_all). -all() -> [t_guid_gen, t_guid_hexstr, t_guid_base62]. +-include_lib("eunit/include/eunit.hrl"). + +all() -> emqx_ct:all(?MODULE). t_guid_gen(_) -> Guid1 = emqx_guid:gen(), Guid2 = emqx_guid:gen(), <<_:128>> = Guid1, - true = (Guid2 >= Guid1), + ?assert((Guid2 >= Guid1)), {Ts1, _, 0} = emqx_guid:new(), Ts2 = emqx_guid:timestamp(emqx_guid:gen()), - true = Ts2 > Ts1. + ?assert(Ts2 > Ts1). t_guid_hexstr(_) -> Guid = emqx_guid:gen(), diff --git a/test/emqx_hooks_SUITE.erl b/test/emqx_hooks_SUITE.erl index b79559122..102a66161 100644 --- a/test/emqx_hooks_SUITE.erl +++ b/test/emqx_hooks_SUITE.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_hooks_SUITE). @@ -18,12 +20,10 @@ -compile(nowarn_export_all). -include_lib("eunit/include/eunit.hrl"). --include_lib("common_test/include/ct.hrl"). -all() -> - [add_delete_hook, run_hook]. +all() -> emqx_ct:all(?MODULE). -add_delete_hook(_) -> +t_add_del_hook(_) -> {ok, _} = emqx_hooks:start_link(), ok = emqx:hook(test_hook, fun ?MODULE:hook_fun1/1, []), ok = emqx:hook(test_hook, fun ?MODULE:hook_fun2/1, []), @@ -54,7 +54,7 @@ add_delete_hook(_) -> ?assertEqual([], emqx_hooks:lookup(emqx_hook)), ok = emqx_hooks:stop(). -run_hook(_) -> +t_run_hooks(_) -> {ok, _} = emqx_hooks:start_link(), ok = emqx:hook(foldl_hook, fun ?MODULE:hook_fun3/4, [init]), ok = emqx:hook(foldl_hook, {?MODULE, hook_fun3, [init]}), @@ -84,6 +84,10 @@ run_hook(_) -> ok = emqx_hooks:stop(). +%%-------------------------------------------------------------------- +%% Hook fun +%%-------------------------------------------------------------------- + hook_fun1(arg) -> ok; hook_fun1(_) -> error. @@ -102,7 +106,7 @@ hook_fun7(arg, initArg) -> ok. hook_fun8(arg, initArg) -> ok. hook_fun9(arg, Acc) -> {stop, [r9 | Acc]}. -hook_fun10(arg, Acc) -> {stop, [r10 | Acc]}. +hook_fun10(arg, Acc) -> {stop, [r10 | Acc]}. hook_filter1(arg) -> true; hook_filter1(_) -> false. @@ -110,6 +114,7 @@ hook_filter1(_) -> false. hook_filter2(arg, _Acc, init_arg) -> true; hook_filter2(_, _Acc, _IntArg) -> false. -hook_filter2_1(arg, _Acc, init_arg) -> true; +hook_filter2_1(arg, _Acc, init_arg) -> true; hook_filter2_1(arg1, _Acc, init_arg) -> true; -hook_filter2_1(_, _Acc, _IntArg) -> false. +hook_filter2_1(_, _Acc, _IntArg) -> false. + diff --git a/test/emqx_inflight_SUITE.erl b/test/emqx_inflight_SUITE.erl index d0373e11b..7a015a443 100644 --- a/test/emqx_inflight_SUITE.erl +++ b/test/emqx_inflight_SUITE.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,31 +12,80 @@ %% 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_inflight_SUITE). -compile(export_all). -compile(nowarn_export_all). -all() -> [t_inflight_all]. +-include_lib("eunit/include/eunit.hrl"). +-include_lib("emqx_ct_helpers/include/emqx_ct.hrl"). + +all() -> emqx_ct:all(?MODULE). + +t_contain(_) -> + Inflight = emqx_inflight:insert(k, v, emqx_inflight:new()), + ?assert(emqx_inflight:contain(k, Inflight)), + ?assertNot(emqx_inflight:contain(badkey, Inflight)). + +t_lookup(_) -> + Inflight = emqx_inflight:insert(k, v, emqx_inflight:new()), + ?assertEqual({value, v}, emqx_inflight:lookup(k, Inflight)), + ?assertEqual(none, emqx_inflight:lookup(badkey, Inflight)). + +t_insert(_) -> + Inflight = emqx_inflight:insert( + b, 2, emqx_inflight:insert( + a, 1, emqx_inflight:new())), + ?assertEqual(2, emqx_inflight:size(Inflight)), + ?assertEqual({value, 1}, emqx_inflight:lookup(a, Inflight)), + ?assertEqual({value, 2}, emqx_inflight:lookup(b, Inflight)), + ?catch_error({key_exists, a}, emqx_inflight:insert(a, 1, Inflight)). + +t_update(_) -> + Inflight = emqx_inflight:insert(k, v, emqx_inflight:new()), + ?assertEqual(Inflight, emqx_inflight:update(k, v, Inflight)), + ?catch_error(function_clause, emqx_inflight:update(badkey, v, Inflight)). + +t_resize(_) -> + Inflight = emqx_inflight:insert(k, v, emqx_inflight:new(2)), + ?assertEqual(1, emqx_inflight:size(Inflight)), + ?assertEqual(2, emqx_inflight:max_size(Inflight)), + Inflight1 = emqx_inflight:resize(4, Inflight), + ?assertEqual(4, emqx_inflight:max_size(Inflight1)), + ?assertEqual(1, emqx_inflight:size(Inflight)). + +t_delete(_) -> + Inflight = emqx_inflight:insert(k, v, emqx_inflight:new(2)), + Inflight1 = emqx_inflight:delete(k, Inflight), + ?assert(emqx_inflight:is_empty(Inflight1)), + ?assertNot(emqx_inflight:contain(k, Inflight1)). + +t_values(_) -> + Inflight = emqx_inflight:insert( + b, 2, emqx_inflight:insert( + a, 1, emqx_inflight:new())), + ?assertEqual([1,2], emqx_inflight:values(Inflight)), + ?assertEqual([{a,1},{b,2}], emqx_inflight:to_list(Inflight)). + +t_is_full(_) -> + Inflight = emqx_inflight:insert(k, v, emqx_inflight:new()), + ?assertNot(emqx_inflight:is_full(Inflight)), + Inflight1 = emqx_inflight:insert( + b, 2, emqx_inflight:insert( + a, 1, emqx_inflight:new(2))), + ?assert(emqx_inflight:is_full(Inflight1)). + +t_is_empty(_) -> + Inflight = emqx_inflight:insert(a, 1, emqx_inflight:new(2)), + ?assertNot(emqx_inflight:is_empty(Inflight)), + Inflight1 = emqx_inflight:delete(a, Inflight), + ?assert(emqx_inflight:is_empty(Inflight1)). + +t_window(_) -> + Inflight = emqx_inflight:insert( + b, 2, emqx_inflight:insert( + a, 1, emqx_inflight:new(2))), + [a, b] = emqx_inflight:window(Inflight). -t_inflight_all(_) -> - Empty = emqx_inflight:new(2), - true = emqx_inflight:is_empty(Empty), - 2 = emqx_inflight:max_size(Empty), - false = emqx_inflight:contain(a, Empty), - none = emqx_inflight:lookup(a, Empty), - try emqx_inflight:update(a, 1, Empty) catch - error:Reason -> io:format("Reason: ~w~n", [Reason]) - end, - 0 = emqx_inflight:size(Empty), - Inflight1 = emqx_inflight:insert(a, 1, Empty), - Inflight2 = emqx_inflight:insert(b, 2, Inflight1), - 2 = emqx_inflight:size(Inflight2), - true = emqx_inflight:is_full(Inflight2), - {value, 1} = emqx_inflight:lookup(a, Inflight1), - {value, 2} = emqx_inflight:lookup(a, emqx_inflight:update(a, 2, Inflight1)), - false = emqx_inflight:contain(a, emqx_inflight:delete(a, Inflight1)), - [1, 2] = emqx_inflight:values(Inflight2), - [{a, 1}, {b ,2}] = emqx_inflight:to_list(Inflight2), - [a, b] = emqx_inflight:window(Inflight2). diff --git a/test/emqx_json_SUITE.erl b/test/emqx_json_SUITE.erl index ccdce1cd2..122296a93 100644 --- a/test/emqx_json_SUITE.erl +++ b/test/emqx_json_SUITE.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,27 +12,33 @@ %% 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_json_SUITE). -compile(export_all). -compile(nowarn_export_all). -all() -> [t_decode_encode, t_safe_decode_encode]. +-include_lib("eunit/include/eunit.hrl"). + +-define(DEC_OPTS, [{labels, atom}, return_maps]). + +all() -> emqx_ct:all(?MODULE). t_decode_encode(_) -> JsonText = <<"{\"library\": \"jsx\", \"awesome\": true}">>, JsonTerm = emqx_json:decode(JsonText), JsonMaps = #{library => <<"jsx">>, awesome => true}, - JsonMaps = emqx_json:decode(JsonText, [{labels, atom}, return_maps]), - JsonText = emqx_json:encode(JsonTerm, [{space, 1}]). + ?assertEqual(JsonText, emqx_json:encode(JsonTerm, [{space, 1}])), + ?assertEqual(JsonMaps, emqx_json:decode(JsonText, ?DEC_OPTS)). t_safe_decode_encode(_) -> JsonText = <<"{\"library\": \"jsx\", \"awesome\": true}">>, {ok, JsonTerm} = emqx_json:safe_decode(JsonText), JsonMaps = #{library => <<"jsx">>, awesome => true}, - {ok, JsonMaps} = emqx_json:safe_decode(JsonText, [{labels, atom}, return_maps]), - {ok, JsonText} = emqx_json:safe_encode(JsonTerm, [{space, 1}]), + ?assertEqual({ok, JsonText}, emqx_json:safe_encode(JsonTerm, [{space, 1}])), + ?assertEqual({ok, JsonMaps}, emqx_json:safe_decode(JsonText, ?DEC_OPTS)), BadJsonText = <<"{\"library\", \"awesome\": true}">>, - {error, _} = emqx_json:safe_decode(BadJsonText), - {error, _} = emqx_json:safe_encode({a, {b ,1}}). + ?assertEqual({error, badarg}, emqx_json:safe_decode(BadJsonText)), + {error, badarg} = emqx_json:safe_encode({a, {b ,1}}). + diff --git a/test/emqx_keepalive_SUITE.erl b/test/emqx_keepalive_SUITE.erl index a60383b01..abfd8ee2b 100644 --- a/test/emqx_keepalive_SUITE.erl +++ b/test/emqx_keepalive_SUITE.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,19 +12,14 @@ %% 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_keepalive_SUITE). -compile(export_all). -compile(nowarn_export_all). -all() -> [{group, keepalive}]. - -groups() -> [{keepalive, [], [t_keepalive]}]. - -%%-------------------------------------------------------------------- -%% Keepalive -%%-------------------------------------------------------------------- +all() -> emqx_ct:all(?MODULE). t_keepalive(_) -> {ok, KA} = emqx_keepalive:start(fun() -> {ok, 1} end, 1, {keepalive, timeout}), @@ -36,7 +32,6 @@ keepalive_recv(KA, Acc) -> {ok, KA1} -> keepalive_recv(KA1, [resumed | Acc]); {error, timeout} -> [timeout | Acc] end - after 4000 -> - Acc + after 4000 -> Acc end. diff --git a/test/emqx_lib_SUITE.erl b/test/emqx_lib_SUITE.erl deleted file mode 100644 index 1d7e1f31f..000000000 --- a/test/emqx_lib_SUITE.erl +++ /dev/null @@ -1,173 +0,0 @@ -%% 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_lib_SUITE). - --include_lib("eunit/include/eunit.hrl"). - --compile(export_all). --compile(nowarn_export_all). - --define(SOCKOPTS, [ - binary, - {packet, raw}, - {reuseaddr, true}, - {backlog, 512}, - {nodelay, true} -]). - --define(PQ, emqx_pqueue). - --define(BASE62, emqx_base62). - -all() -> [{group, guid}, {group, opts}, - {group, ?PQ}, {group, time}, - {group, node}, {group, base62}]. - -groups() -> - [{guid, [], [guid_gen, guid_hexstr, guid_base62]}, - {opts, [], [opts_merge]}, - {?PQ, [], [priority_queue_plen, - priority_queue_out2]}, - {time, [], [time_now_to_]}, - {node, [], [node_is_aliving, node_parse_name]}, - {base62, [], [base62_encode]}]. - -%%-------------------------------------------------------------------- -%% emqx_guid -%%-------------------------------------------------------------------- - -guid_gen(_) -> - Guid1 = emqx_guid:gen(), - Guid2 = emqx_guid:gen(), - <<_:128>> = Guid1, - true = (Guid2 >= Guid1), - {Ts1, _, 0} = emqx_guid:new(), - Ts2 = emqx_guid:timestamp(emqx_guid:gen()), - true = Ts2 > Ts1. - -guid_hexstr(_) -> - Guid = emqx_guid:gen(), - ?assertEqual(Guid, emqx_guid:from_hexstr(emqx_guid:to_hexstr(Guid))). - -guid_base62(_) -> - Guid = emqx_guid:gen(), - ?assertEqual(Guid, emqx_guid:from_base62(emqx_guid:to_base62(Guid))). - -%%-------------------------------------------------------------------- -%% emqx_opts -%%-------------------------------------------------------------------- - -opts_merge(_) -> - Opts = emqx_misc:merge_opts(?SOCKOPTS, [raw, - binary, - {backlog, 1024}, - {nodelay, false}, - {max_clients, 1024}, - {acceptors, 16}]), - 1024 = proplists:get_value(backlog, Opts), - 1024 = proplists:get_value(max_clients, Opts), - [binary, raw, - {acceptors, 16}, - {backlog, 1024}, - {max_clients, 1024}, - {nodelay, false}, - {packet, raw}, - {reuseaddr, true}] = lists:sort(Opts). - -%%-------------------------------------------------------------------- -%% priority_queue -%%-------------------------------------------------------------------- - -priority_queue_plen(_) -> - Q = ?PQ:new(), - 0 = ?PQ:plen(0, Q), - Q0 = ?PQ:in(z, Q), - 1 = ?PQ:plen(0, Q0), - Q1 = ?PQ:in(x, 1, Q0), - 1 = ?PQ:plen(1, Q1), - Q2 = ?PQ:in(y, 2, Q1), - 1 = ?PQ:plen(2, Q2), - Q3 = ?PQ:in(z, 2, Q2), - 2 = ?PQ:plen(2, Q3), - {_, Q4} = ?PQ:out(1, Q3), - 0 = ?PQ:plen(1, Q4), - {_, Q5} = ?PQ:out(Q4), - 1 = ?PQ:plen(2, Q5), - {_, Q6} = ?PQ:out(Q5), - 0 = ?PQ:plen(2, Q6), - 1 = ?PQ:len(Q6), - {_, Q7} = ?PQ:out(Q6), - 0 = ?PQ:len(Q7). - -priority_queue_out2(_) -> - Els = [a, {b, 1}, {c, 1}, {d, 2}, {e, 2}, {f, 2}], - Q = ?PQ:new(), - Q0 = lists:foldl( - fun({El, P}, Acc) -> - ?PQ:in(El, P, Acc); - (El, Acc) -> - ?PQ:in(El, Acc) - end, Q, Els), - {Val, Q1} = ?PQ:out(Q0), - {value, d} = Val, - {Val1, Q2} = ?PQ:out(2, Q1), - {value, e} = Val1, - {Val2, Q3} = ?PQ:out(1, Q2), - {value, b} = Val2, - {Val3, Q4} = ?PQ:out(Q3), - {value, f} = Val3, - {Val4, Q5} = ?PQ:out(Q4), - {value, c} = Val4, - {Val5, Q6} = ?PQ:out(Q5), - {value, a} = Val5, - {empty, _Q7} = ?PQ:out(Q6). - -%%-------------------------------------------------------------------- -%% emqx_time -%%-------------------------------------------------------------------- - -time_now_to_(_) -> - emqx_time:seed(), - emqx_time:now_secs(), - emqx_time:now_ms(). - -%%-------------------------------------------------------------------- -%% emqx_node -%%-------------------------------------------------------------------- - -node_is_aliving(_) -> - io:format("Node: ~p~n", [node()]), - true = ekka_node:is_aliving(node()), - false = ekka_node:is_aliving('x@127.0.0.1'). - -node_parse_name(_) -> - 'a@127.0.0.1' = ekka_node:parse_name("a@127.0.0.1"), - 'b@127.0.0.1' = ekka_node:parse_name("b"). - -%%-------------------------------------------------------------------- -%% base62 encode decode -%%-------------------------------------------------------------------- - -base62_encode(_) -> - <<"10">> = ?BASE62:decode(?BASE62:encode(<<"10">>)), - <<"100">> = ?BASE62:decode(?BASE62:encode(<<"100">>)), - <<"9999">> = ?BASE62:decode(?BASE62:encode(<<"9999">>)), - <<"65535">> = ?BASE62:decode(?BASE62:encode(<<"65535">>)), - <> = emqx_guid:gen(), - <> = emqx_guid:gen(), - X = ?BASE62:decode(?BASE62:encode(X), integer), - Y = ?BASE62:decode(?BASE62:encode(Y), integer), - <<"helloworld">> = ?BASE62:decode(?BASE62:encode("helloworld")), - "helloworld" = ?BASE62:decode(?BASE62:encode("helloworld", string), string). diff --git a/test/emqx_listeners_SUITE.erl b/test/emqx_listeners_SUITE.erl index cab8707d5..1cba68b74 100644 --- a/test/emqx_listeners_SUITE.erl +++ b/test/emqx_listeners_SUITE.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,22 +12,17 @@ %% 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_listeners_SUITE). -compile(export_all). -compile(nowarn_export_all). --include_lib("eunit/include/eunit.hrl"). - --include_lib("common_test/include/ct.hrl"). - -include("emqx.hrl"). -include("emqx_mqtt.hrl"). -all() -> - [start_stop_listeners, - restart_listeners]. +all() -> emqx_ct:all(?MODULE). init_per_suite(Config) -> NewConfig = generate_config(), @@ -39,11 +35,11 @@ end_per_suite(_Config) -> application:stop(esockd), application:stop(cowboy). -start_stop_listeners(_) -> +t_start_stop_listeners(_) -> ok = emqx_listeners:start(), ok = emqx_listeners:stop(). -restart_listeners(_) -> +t_restart_listeners(_) -> ok = emqx_listeners:start(), ok = emqx_listeners:stop(), ok = emqx_listeners:restart(), @@ -93,3 +89,4 @@ get_base_dir(Module) -> get_base_dir() -> get_base_dir(?MODULE). + diff --git a/test/emqx_message_SUITE.erl b/test/emqx_message_SUITE.erl index 79e34bfb5..9c699b65d 100644 --- a/test/emqx_message_SUITE.erl +++ b/test/emqx_message_SUITE.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,10 +12,13 @@ %% 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_message_SUITE). --include("emqx.hrl"). +-compile(export_all). +-compile(nowarn_export_all). + -include("emqx_mqtt.hrl"). -include_lib("eunit/include/eunit.hrl"). @@ -30,17 +34,22 @@ , suite/0 ]). +all() -> emqx_ct:all(?MODULE). + +suite() -> + [{ct_hooks, [cth_surefire]}, {timetrap, {seconds, 30}}]. + t_make(_) -> Msg = emqx_message:make(<<"topic">>, <<"payload">>), - ?assertEqual(0, emqx_message:qos(Msg)), + ?assertEqual(?QOS_0, emqx_message:qos(Msg)), ?assertEqual(undefined, emqx_message:from(Msg)), ?assertEqual(<<"payload">>, emqx_message:payload(Msg)), Msg1 = emqx_message:make(<<"clientid">>, <<"topic">>, <<"payload">>), - ?assertEqual(0, emqx_message:qos(Msg1)), + ?assertEqual(?QOS_0, emqx_message:qos(Msg1)), ?assertEqual(<<"topic">>, emqx_message:topic(Msg1)), Msg2 = emqx_message:make(<<"clientid">>, ?QOS_2, <<"topic">>, <<"payload">>), ?assert(is_binary(emqx_message:id(Msg2))), - ?assertEqual(2, emqx_message:qos(Msg2)), + ?assertEqual(?QOS_2, emqx_message:qos(Msg2)), ?assertEqual(<<"clientid">>, emqx_message:from(Msg2)), ?assertEqual(<<"topic">>, emqx_message:topic(Msg2)), ?assertEqual(<<"payload">>, emqx_message:payload(Msg2)). @@ -84,21 +93,14 @@ t_expired(_) -> t_to_map(_) -> Msg = emqx_message:make(<<"clientid">>, ?QOS_1, <<"topic">>, <<"payload">>), - List = [{id, Msg#message.id}, + List = [{id, emqx_message:id(Msg)}, {qos, ?QOS_1}, {from, <<"clientid">>}, {flags, #{dup => false}}, {headers, #{}}, {topic, <<"topic">>}, {payload, <<"payload">>}, - {timestamp, Msg#message.timestamp}], + {timestamp, emqx_message:timestamp(Msg)}], ?assertEqual(List, emqx_message:to_list(Msg)), ?assertEqual(maps:from_list(List), emqx_message:to_map(Msg)). -all() -> - IsTestCase = fun("t_" ++ _) -> true; (_) -> false end, - [F || {F, _A} <- module_info(exports), IsTestCase(atom_to_list(F))]. - -suite() -> - [{ct_hooks, [cth_surefire]}, {timetrap, {seconds, 30}}]. - diff --git a/test/emqx_metrics_SUITE.erl b/test/emqx_metrics_SUITE.erl index b100d44e5..cc7db5ad2 100644 --- a/test/emqx_metrics_SUITE.erl +++ b/test/emqx_metrics_SUITE.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_metrics_SUITE). @@ -20,55 +22,64 @@ -include("emqx_mqtt.hrl"). -include_lib("eunit/include/eunit.hrl"). -all() -> [t_inc_dec, t_inc_recv, t_inc_sent, t_trans]. +all() -> emqx_ct:all(?MODULE). t_inc_dec(_) -> - {ok, _} = emqx_metrics:start_link(), - ?assertEqual(0, emqx_metrics:val('bytes.received')), - ?assertEqual(0, emqx_metrics:val('messages.retained')), - ok = emqx_metrics:inc('bytes.received'), - ok = emqx_metrics:inc('bytes.received', 2), - ok = emqx_metrics:inc('bytes.received', 2), - ?assertEqual(5, emqx_metrics:val('bytes.received')), - ok = emqx_metrics:inc('messages.retained', 2), - ok = emqx_metrics:inc('messages.retained', 2), - ?assertEqual(4, emqx_metrics:val('messages.retained')), - ok = emqx_metrics:dec('messages.retained'), - ok = emqx_metrics:dec('messages.retained', 1), - ?assertEqual(2, emqx_metrics:val('messages.retained')), - ok = emqx_metrics:set('messages.retained', 3), - ?assertEqual(3, emqx_metrics:val('messages.retained')), - ok = emqx_metrics:stop(). + with_metrics_server( + fun() -> + ?assertEqual(0, emqx_metrics:val('bytes.received')), + ?assertEqual(0, emqx_metrics:val('messages.retained')), + ok = emqx_metrics:inc('bytes.received'), + ok = emqx_metrics:inc('bytes.received', 2), + ok = emqx_metrics:inc('bytes.received', 2), + ?assertEqual(5, emqx_metrics:val('bytes.received')), + ok = emqx_metrics:inc('messages.retained', 2), + ok = emqx_metrics:inc('messages.retained', 2), + ?assertEqual(4, emqx_metrics:val('messages.retained')), + ok = emqx_metrics:dec('messages.retained'), + ok = emqx_metrics:dec('messages.retained', 1), + ?assertEqual(2, emqx_metrics:val('messages.retained')), + ok = emqx_metrics:set('messages.retained', 3), + ?assertEqual(3, emqx_metrics:val('messages.retained')) + end). t_inc_recv(_) -> - {ok, _} = emqx_metrics:start_link(), - ok = emqx_metrics:inc_recv(?PACKET(?CONNECT)), - ?assertEqual(1, emqx_metrics:val('packets.received')), - ?assertEqual(1, emqx_metrics:val('packets.connect.received')), - ok = emqx_metrics:stop(). + with_metrics_server( + fun() -> + ok = emqx_metrics:inc_recv(?PACKET(?CONNECT)), + ?assertEqual(1, emqx_metrics:val('packets.received')), + ?assertEqual(1, emqx_metrics:val('packets.connect.received')) + end). t_inc_sent(_) -> - {ok, _} = emqx_metrics:start_link(), - ok = emqx_metrics:inc_sent(?CONNACK_PACKET(0)), - ?assertEqual(1, emqx_metrics:val('packets.sent')), - ?assertEqual(1, emqx_metrics:val('packets.connack.sent')), - ok = emqx_metrics:stop(). + with_metrics_server( + fun() -> + ok = emqx_metrics:inc_sent(?CONNACK_PACKET(0)), + ?assertEqual(1, emqx_metrics:val('packets.sent')), + ?assertEqual(1, emqx_metrics:val('packets.connack.sent')) + end). t_trans(_) -> + with_metrics_server( + fun() -> + ok = emqx_metrics:trans(inc, 'bytes.received'), + ok = emqx_metrics:trans(inc, 'bytes.received', 2), + ?assertEqual(0, emqx_metrics:val('bytes.received')), + ok = emqx_metrics:trans(inc, 'messages.retained', 2), + ok = emqx_metrics:trans(inc, 'messages.retained', 2), + ?assertEqual(0, emqx_metrics:val('messages.retained')), + ok = emqx_metrics:commit(), + ?assertEqual(3, emqx_metrics:val('bytes.received')), + ?assertEqual(4, emqx_metrics:val('messages.retained')), + ok = emqx_metrics:trans(dec, 'messages.retained'), + ok = emqx_metrics:trans(dec, 'messages.retained', 1), + ?assertEqual(4, emqx_metrics:val('messages.retained')), + ok = emqx_metrics:commit(), + ?assertEqual(2, emqx_metrics:val('messages.retained')) + end). + +with_metrics_server(Fun) -> {ok, _} = emqx_metrics:start_link(), - ok = emqx_metrics:trans(inc, 'bytes.received'), - ok = emqx_metrics:trans(inc, 'bytes.received', 2), - ?assertEqual(0, emqx_metrics:val('bytes.received')), - ok = emqx_metrics:trans(inc, 'messages.retained', 2), - ok = emqx_metrics:trans(inc, 'messages.retained', 2), - ?assertEqual(0, emqx_metrics:val('messages.retained')), - ok = emqx_metrics:commit(), - ?assertEqual(3, emqx_metrics:val('bytes.received')), - ?assertEqual(4, emqx_metrics:val('messages.retained')), - ok = emqx_metrics:trans(dec, 'messages.retained'), - ok = emqx_metrics:trans(dec, 'messages.retained', 1), - ?assertEqual(4, emqx_metrics:val('messages.retained')), - ok = emqx_metrics:commit(), - ?assertEqual(2, emqx_metrics:val('messages.retained')), + _ = Fun(), ok = emqx_metrics:stop(). diff --git a/test/emqx_misc_tests.erl b/test/emqx_misc_SUITE.erl similarity index 69% rename from test/emqx_misc_tests.erl rename to test/emqx_misc_SUITE.erl index 038180b5b..c9a3929b6 100644 --- a/test/emqx_misc_tests.erl +++ b/test/emqx_misc_SUITE.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,8 +12,13 @@ %% 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_misc_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). --module(emqx_misc_tests). -include_lib("eunit/include/eunit.hrl"). -define(SOCKOPTS, [binary, @@ -21,8 +27,9 @@ {backlog, 512}, {nodelay, true}]). +all() -> emqx_ct:all(?MODULE). -t_merge_opts_test() -> +t_merge_opts() -> Opts = emqx_misc:merge_opts(?SOCKOPTS, [raw, binary, {backlog, 1024}, @@ -39,33 +46,47 @@ t_merge_opts_test() -> {packet, raw}, {reuseaddr, true}] = lists:sort(Opts). -timer_cancel_flush_test() -> +t_timer_cancel_flush() -> Timer = emqx_misc:start_timer(0, foo), ok = emqx_misc:cancel_timer(Timer), - receive {timeout, Timer, foo} -> error(unexpected) + receive + {timeout, Timer, foo} -> + error(unexpected) after 0 -> ok end. -shutdown_disabled_test() -> +t_shutdown_disabled() -> ok = drain(), self() ! foo, - ?assertEqual(continue, conn_proc_mng_policy(0)), + ?assertEqual(continue, emqx_misc:conn_proc_mng_policy(0)), receive foo -> ok end, - ?assertEqual(hibernate, conn_proc_mng_policy(0)). + ?assertEqual(hibernate, emqx_misc:conn_proc_mng_policy(0)). -message_queue_too_long_test() -> +t_message_queue_too_long() -> ok = drain(), self() ! foo, self() ! bar, ?assertEqual({shutdown, message_queue_too_long}, - conn_proc_mng_policy(1)), + emqx_misc:conn_proc_mng_policy(1)), receive foo -> ok end, - ?assertEqual(continue, conn_proc_mng_policy(1)), + ?assertEqual(continue, emqx_misc:conn_proc_mng_policy(1)), receive bar -> ok end. -conn_proc_mng_policy(L) -> +t_conn_proc_mng_policy(L) -> emqx_misc:conn_proc_mng_policy(#{message_queue_len => L}). +t_proc_name(_) -> + 'TODO'. + +t_proc_stats(_) -> + 'TODO'. + +t_drain_deliver(_) -> + 'TODO'. + +t_drain_down(_) -> + 'TODO'. + %% drain self() msg queue for deterministic test behavior drain() -> _ = drain([]), % maybe log diff --git a/test/emqx_mock_client.erl b/test/emqx_mock_client.erl deleted file mode 100644 index 7de80ad4a..000000000 --- a/test/emqx_mock_client.erl +++ /dev/null @@ -1,99 +0,0 @@ -%% 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_mock_client). - --behaviour(gen_server). - --export([start_link/1, open_session/3, open_session/4, - close_session/1, stop/1, get_last_message/1]). - --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). - --record(state, {clean_start, client_id, client_pid, last_msg, session_pid}). - -start_link(ClientId) -> - gen_server:start_link(?MODULE, [ClientId], []). - -open_session(ClientPid, ClientId, Zone) -> - open_session(ClientPid, ClientId, Zone, _Attrs = #{}). - -open_session(ClientPid, ClientId, Zone, Attrs0) -> - Attrs1 = default_session_attributes(Zone, ClientId, ClientPid), - Attrs = maps:merge(Attrs1, Attrs0), - gen_server:call(ClientPid, {start_session, ClientPid, ClientId, Attrs}). - -%% close session and terminate the client itself -close_session(ClientPid) -> - gen_server:call(ClientPid, stop_session, infinity). - -stop(CPid) -> - gen_server:call(CPid, stop, infinity). - -get_last_message(Pid) -> - gen_server:call(Pid, get_last_message, infinity). - -init([ClientId]) -> - erlang:process_flag(trap_exit, true), - {ok, #state{clean_start = true, - client_id = ClientId, - last_msg = undefined - } - }. - -handle_call({start_session, ClientPid, ClientId, Attrs}, _From, State) -> - {ok, SessPid} = emqx_sm:open_session(Attrs), - {reply, {ok, SessPid}, - State#state{clean_start = true, - client_id = ClientId, - client_pid = ClientPid, - session_pid = SessPid - }}; -handle_call(stop_session, _From, #state{session_pid = Pid} = State) -> - is_pid(Pid) andalso is_process_alive(Pid) andalso emqx_sm:close_session(Pid), - {stop, normal, ok, State#state{session_pid = undefined}}; -handle_call(get_last_message, _From, #state{last_msg = Msg} = State) -> - {reply, Msg, State}; -handle_call(stop, _From, State) -> - {stop, normal, ok, State}; -handle_call(_Request, _From, State) -> - {reply, ok, State}. - -handle_cast(_Msg, State) -> - {noreply, State}. - -handle_info({deliver, Msg}, State) -> - {noreply, State#state{last_msg = Msg}}; -handle_info(_Info, State) -> - {noreply, State}. - -terminate(_Reason, _State) -> - ok. - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -default_session_attributes(Zone, ClientId, ClientPid) -> - #{zone => Zone, - client_id => ClientId, - conn_pid => ClientPid, - clean_start => true, - username => undefined, - expiry_interval => 0, - max_inflight => 0, - topic_alias_maximum => 0, - will_msg => undefined - }. - diff --git a/test/emqx_mod_SUITE.erl b/test/emqx_mod_SUITE.erl deleted file mode 100644 index 39d9abc21..000000000 --- a/test/emqx_mod_SUITE.erl +++ /dev/null @@ -1,25 +0,0 @@ -%% 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_mod_SUITE). - --compile(export_all). --compile(nowarn_export_all). - --include("emqx.hrl"). - -all() -> [mod_subscription_rep]. - -mod_subscription_rep(_) -> ok. - diff --git a/test/emqx_mod_rewrite_SUITE.erl b/test/emqx_mod_rewrite_SUITE.erl new file mode 100644 index 000000000..675918286 --- /dev/null +++ b/test/emqx_mod_rewrite_SUITE.erl @@ -0,0 +1,81 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_mod_rewrite_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-import(emqx_mod_rewrite, + [ rewrite_subscribe/4 + , rewrite_unsubscribe/4 + , rewrite_publish/2 + ]). + +-include_lib("emqx.hrl"). +-include_lib("eunit/include/eunit.hrl"). + +-define(TEST_RULES, [<<"x/# ^x/y/(.+)$ z/y/$1">>, + <<"y/+/z/# ^y/(.+)/z/(.+)$ y/z/$2">> + ]). + +all() -> emqx_ct:all(?MODULE). + +%%-------------------------------------------------------------------- +%% Test cases +%%-------------------------------------------------------------------- + +t_rewrite_subscribe(_) -> + ?assertEqual({ok, [{<<"test">>, #{}}]}, + rewrite(subscribe, [{<<"test">>, #{}}])), + ?assertEqual({ok, [{<<"z/y/test">>, #{}}]}, + rewrite(subscribe, [{<<"x/y/test">>, #{}}])), + ?assertEqual({ok, [{<<"y/z/test_topic">>, #{}}]}, + rewrite(subscribe, [{<<"y/test/z/test_topic">>, #{}}])). + +t_rewrite_unsubscribe(_) -> + ?assertEqual({ok, [{<<"test">>, #{}}]}, + rewrite(unsubscribe, [{<<"test">>, #{}}])), + ?assertEqual({ok, [{<<"z/y/test">>, #{}}]}, + rewrite(unsubscribe, [{<<"x/y/test">>, #{}}])), + ?assertEqual({ok, [{<<"y/z/test_topic">>, #{}}]}, + rewrite(unsubscribe, [{<<"y/test/z/test_topic">>, #{}}])). + +t_rewrite_publish(_) -> + ?assertMatch({ok, #message{topic = <<"test">>}}, + rewrite(publish, #message{topic = <<"test">>})), + ?assertMatch({ok, #message{topic = <<"z/y/test">>}}, + rewrite(publish, #message{topic = <<"x/y/test">>})), + ?assertMatch({ok, #message{topic = <<"y/z/test_topic">>}}, + rewrite(publish, #message{topic = <<"y/test/z/test_topic">>})). + +%%-------------------------------------------------------------------- +%% Helper functions +%%-------------------------------------------------------------------- + +rewrite(subscribe, TopicFilters) -> + rewrite_subscribe(#{}, #{}, TopicFilters, rules()); +rewrite(unsubscribe, TopicFilters) -> + rewrite_unsubscribe(#{}, #{}, TopicFilters, rules()); +rewrite(publish, Msg) -> rewrite_publish(Msg, rules()). + +rules() -> + [begin + [Topic, Re, Dest] = string:split(Rule, " ", all), + {ok, MP} = re:compile(Re), + {rewrite, Topic, MP, Dest} + end || Rule <- ?TEST_RULES]. + diff --git a/test/emqx_mod_rewrite_tests.erl b/test/emqx_mod_rewrite_tests.erl deleted file mode 100644 index bc41977c3..000000000 --- a/test/emqx_mod_rewrite_tests.erl +++ /dev/null @@ -1,63 +0,0 @@ -%% 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_mod_rewrite_tests). - --include_lib("emqx.hrl"). --include_lib("eunit/include/eunit.hrl"). - - -rules() -> - Rawrules1 = "x/# ^x/y/(.+)$ z/y/$1", - Rawrules2 = "y/+/z/# ^y/(.+)/z/(.+)$ y/z/$2", - Rawrules = [Rawrules1, Rawrules2], - Rules = lists:map(fun(Rule) -> - [Topic, Re, Dest] = string:tokens(Rule, " "), - {rewrite, - list_to_binary(Topic), - list_to_binary(Re), - list_to_binary(Dest)} - end, Rawrules), - lists:map(fun({rewrite, Topic, Re, Dest}) -> - {ok, MP} = re:compile(Re), - {rewrite, Topic, MP, Dest} - end, Rules). - -rewrite_subscribe_test() -> - Rules = rules(), - io:format("Rules: ~p",[Rules]), - ?assertEqual({ok, [{<<"test">>, opts}]}, - emqx_mod_rewrite:rewrite_subscribe(credentials, [{<<"test">>, opts}], Rules)), - ?assertEqual({ok, [{<<"z/y/test">>, opts}]}, - emqx_mod_rewrite:rewrite_subscribe(credentials, [{<<"x/y/test">>, opts}], Rules)), - ?assertEqual({ok, [{<<"y/z/test_topic">>, opts}]}, - emqx_mod_rewrite:rewrite_subscribe(credentials, [{<<"y/test/z/test_topic">>, opts}], Rules)). - -rewrite_unsubscribe_test() -> - Rules = rules(), - ?assertEqual({ok, [{<<"test">>, opts}]}, - emqx_mod_rewrite:rewrite_subscribe(credentials, [{<<"test">>, opts}], Rules)), - ?assertEqual({ok, [{<<"z/y/test">>, opts}]}, - emqx_mod_rewrite:rewrite_subscribe(credentials, [{<<"x/y/test">>, opts}], Rules)), - ?assertEqual({ok, [{<<"y/z/test_topic">>, opts}]}, - emqx_mod_rewrite:rewrite_subscribe(credentials, [{<<"y/test/z/test_topic">>, opts}], Rules)). - -rewrite_publish_test() -> - Rules = rules(), - ?assertMatch({ok, #message{topic = <<"test">>}}, - emqx_mod_rewrite:rewrite_publish(#message{topic = <<"test">>}, Rules)), - ?assertMatch({ok, #message{topic = <<"z/y/test">>}}, - emqx_mod_rewrite:rewrite_publish(#message{topic = <<"x/y/test">>}, Rules)), - ?assertMatch({ok, #message{topic = <<"y/z/test_topic">>}}, - emqx_mod_rewrite:rewrite_publish(#message{topic = <<"y/test/z/test_topic">>}, Rules)). diff --git a/test/emqx_mod_sup_SUITE.erl b/test/emqx_mod_sup_SUITE.erl deleted file mode 100644 index 85fa6af78..000000000 --- a/test/emqx_mod_sup_SUITE.erl +++ /dev/null @@ -1,43 +0,0 @@ -%% 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_mod_sup_SUITE). - --compile(export_all). --compile(nowarn_export_all). - --include("emqx.hrl"). - -all() -> [t_child_all]. - -start_link() -> - Pid = spawn_link(?MODULE, echo, [0]), - {ok, Pid}. - -echo(State) -> - receive - {From, Req} -> - ct:pal("======from:~p, req:~p", [From, Req]), - From ! Req, - echo(State) - end. - -t_child_all(_) -> - {ok, Pid} = emqx_mod_sup:start_link(), - {ok, Child} = emqx_mod_sup:start_child(?MODULE, worker), - timer:sleep(10), - Child ! {self(), hi}, - receive hi -> ok after 100 -> ct:fail({timeout, wait_echo}) end, - ok = emqx_mod_sup:stop_child(?MODULE), - exit(Pid, normal). diff --git a/test/emqx_mountpoint_SUITE.erl b/test/emqx_mountpoint_SUITE.erl index 8db5805b0..bb1f40db4 100644 --- a/test/emqx_mountpoint_SUITE.erl +++ b/test/emqx_mountpoint_SUITE.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,26 +12,56 @@ %% 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_mountpoint_SUITE). -compile(export_all). -compile(nowarn_export_all). --include("emqx.hrl"). --include("emqx_mqtt.hrl"). +-import(emqx_mountpoint, + [ mount/2 + , unmount/2 + , replvar/2 + ]). +-include("emqx.hrl"). -include_lib("eunit/include/eunit.hrl"). -all() -> [t_mount_unmount, t_replvar]. +all() -> emqx_ct:all(?MODULE). -t_mount_unmount(_) -> +t_mount(_) -> Msg = emqx_message:make(<<"clientid">>, <<"topic">>, <<"payload">>), - Msg2 = emqx_mountpoint:mount(<<"mount">>, Msg), - ?assertEqual(<<"mounttopic">>, Msg2#message.topic), - TopicFilter = [{<<"mounttopic">>, #{qos => ?QOS_2}}], - TopicFilter = emqx_mountpoint:mount(<<"mount">>, [{<<"topic">>, #{qos => ?QOS_2}}]), - Msg = emqx_mountpoint:unmount(<<"mount">>, Msg2). + TopicFilters = [{<<"topic">>, #{qos => 2}}], + ?assertEqual(<<"topic">>, mount(undefined, <<"topic">>)), + ?assertEqual(Msg, mount(undefined, Msg)), + ?assertEqual(TopicFilters, mount(undefined, TopicFilters)), + ?assertEqual(<<"device/1/topic">>, + mount(<<"device/1/">>, <<"topic">>)), + ?assertEqual(Msg#message{topic = <<"device/1/topic">>}, + mount(<<"device/1/">>, Msg)), + ?assertEqual([{<<"device/1/topic">>, #{qos => 2}}], + mount(<<"device/1/">>, TopicFilters)). + +t_unmount(_) -> + Msg = emqx_message:make(<<"clientid">>, <<"device/1/topic">>, <<"payload">>), + ?assertEqual(<<"topic">>, unmount(undefined, <<"topic">>)), + ?assertEqual(Msg, unmount(undefined, Msg)), + ?assertEqual(<<"topic">>, unmount(<<"device/1/">>, <<"device/1/topic">>)), + ?assertEqual(Msg#message{topic = <<"topic">>}, unmount(<<"device/1/">>, Msg)), + ?assertEqual(<<"device/1/topic">>, unmount(<<"device/2/">>, <<"device/1/topic">>)), + ?assertEqual(Msg#message{topic = <<"device/1/topic">>}, unmount(<<"device/2/">>, Msg)). t_replvar(_) -> - <<"mount/test/clientid">> = emqx_mountpoint:replvar(<<"mount/%u/%c">>, #{client_id => <<"clientid">>, username => <<"test">>}). + ?assertEqual(undefined, replvar(undefined, #{})), + ?assertEqual(<<"mount/user/clientid/">>, + replvar(<<"mount/%u/%c/">>, + #{client_id => <<"clientid">>, + username => <<"user">> + })), + ?assertEqual(<<"mount/%u/clientid/">>, + replvar(<<"mount/%u/%c/">>, + #{client_id => <<"clientid">>, + username => undefined + })). + diff --git a/test/emqx_mqtt_caps_SUITE.erl b/test/emqx_mqtt_caps_SUITE.erl index e07158e72..c9cc0b073 100644 --- a/test/emqx_mqtt_caps_SUITE.erl +++ b/test/emqx_mqtt_caps_SUITE.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,121 +12,56 @@ %% 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_mqtt_caps_SUITE). --include_lib("eunit/include/eunit.hrl"). - --include("emqx.hrl"). --include("emqx_mqtt.hrl"). - -%% CT -compile(export_all). -compile(nowarn_export_all). -all() -> [t_get_set_caps, t_check_pub, t_check_sub]. +-include("emqx_mqtt.hrl"). +-include_lib("eunit/include/eunit.hrl"). -t_get_set_caps(_) -> - {ok, _} = emqx_zone:start_link(), - Caps = #{ - max_packet_size => ?MAX_PACKET_SIZE, - max_clientid_len => ?MAX_CLIENTID_LEN, - max_topic_alias => 0, - max_topic_levels => 0, - max_qos_allowed => ?QOS_2, - mqtt_retain_available => true, - mqtt_shared_subscription => true, - mqtt_wildcard_subscription => true - }, - Caps2 = Caps#{max_packet_size => 1048576}, - case emqx_mqtt_caps:get_caps(zone) of - Caps -> ok; - Caps2 -> ok - end, - PubCaps = #{ - max_qos_allowed => ?QOS_2, - mqtt_retain_available => true, - max_topic_alias => 0 - }, - PubCaps = emqx_mqtt_caps:get_caps(zone, publish), - NewPubCaps = PubCaps#{max_qos_allowed => ?QOS_1}, - emqx_zone:set_env(zone, '$mqtt_pub_caps', NewPubCaps), - timer:sleep(100), - NewPubCaps = emqx_mqtt_caps:get_caps(zone, publish), - SubCaps = #{ - max_topic_levels => 0, - max_qos_allowed => ?QOS_2, - mqtt_shared_subscription => true, - mqtt_wildcard_subscription => true - }, - SubCaps = emqx_mqtt_caps:get_caps(zone, subscribe), - emqx_zone:stop(). +all() -> emqx_ct:all(?MODULE). t_check_pub(_) -> - {ok, _} = emqx_zone:start_link(), - PubCaps = #{ - max_qos_allowed => ?QOS_1, - mqtt_retain_available => false, - max_topic_alias => 4 - }, - emqx_zone:set_env(zone, '$mqtt_pub_caps', PubCaps), - timer:sleep(100), - ct:log("~p", [emqx_mqtt_caps:get_caps(zone, publish)]), - BadPubProps1 = #{ - qos => ?QOS_2, - retain => false - }, - {error, ?RC_QOS_NOT_SUPPORTED} = emqx_mqtt_caps:check_pub(zone, BadPubProps1), - BadPubProps2 = #{ - qos => ?QOS_1, - retain => true - }, - {error, ?RC_RETAIN_NOT_SUPPORTED} = emqx_mqtt_caps:check_pub(zone, BadPubProps2), - BadPubProps3 = #{ - qos => ?QOS_1, - retain => false, - topic_alias => 5 - }, - {error, ?RC_TOPIC_ALIAS_INVALID} = emqx_mqtt_caps:check_pub(zone, BadPubProps3), - PubProps = #{ - qos => ?QOS_1, - retain => false - }, - ok = emqx_mqtt_caps:check_pub(zone, PubProps), - emqx_zone:stop(). + PubCaps = #{max_qos_allowed => ?QOS_1, + retain_available => false, + max_topic_alias => 4 + }, + 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 + }), + 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(_) -> - {ok, _} = emqx_zone:start_link(), - - Opts = #{qos => ?QOS_2, share => true, rc => 0}, - Caps = #{ - max_topic_levels => 0, - max_qos_allowed => ?QOS_2, - mqtt_shared_subscription => true, - mqtt_wildcard_subscription => true - }, - - ok = do_check_sub([{<<"client/stat">>, Opts}], [{<<"client/stat">>, Opts}]), - ok = do_check_sub(Caps#{max_qos_allowed => ?QOS_1}, [{<<"client/stat">>, Opts}], [{<<"client/stat">>, Opts#{qos => ?QOS_1}}]), - ok = do_check_sub(Caps#{max_topic_levels => 1}, - [{<<"client/stat">>, Opts}], - [{<<"client/stat">>, Opts#{rc => ?RC_TOPIC_FILTER_INVALID}}]), - ok = do_check_sub(Caps#{mqtt_shared_subscription => false}, - [{<<"client/stat">>, Opts}], - [{<<"client/stat">>, Opts#{rc => ?RC_SHARED_SUBSCRIPTIONS_NOT_SUPPORTED}}]), - - ok = do_check_sub(Caps#{mqtt_wildcard_subscription => false}, - [{<<"vlient/+/dsofi">>, Opts}], - [{<<"vlient/+/dsofi">>, Opts#{rc => ?RC_WILDCARD_SUBSCRIPTIONS_NOT_SUPPORTED}}]), - emqx_zone:stop(). - - - -do_check_sub(TopicFilters, Topics) -> - {ok, Topics} = emqx_mqtt_caps:check_sub(zone, TopicFilters), - ok. -do_check_sub(Caps, TopicFilters, Topics) -> - emqx_zone:set_env(zone, '$mqtt_sub_caps', Caps), - timer:sleep(100), - {_, Topics} = emqx_mqtt_caps:check_sub(zone, TopicFilters), - ok. + SubOpts = #{rh => 0, + rap => 0, + nl => 0, + qos => ?QOS_2 + }, + SubCaps = #{max_topic_levels => 2, + max_qos_allowed => ?QOS_2, + shared_subscription => false, + wildcard_subscription => false + }, + ok = emqx_zone:set_env(zone, '$mqtt_sub_caps', SubCaps), + ok = emqx_mqtt_caps:check_sub(zone, <<"topic">>, SubOpts), + ?assertEqual({error, ?RC_TOPIC_FILTER_INVALID}, + emqx_mqtt_caps:check_sub(zone, <<"a/b/c/d">>, SubOpts)), + ?assertEqual({error, ?RC_WILDCARD_SUBSCRIPTIONS_NOT_SUPPORTED}, + emqx_mqtt_caps:check_sub(zone, <<"+/#">>, SubOpts)), + ?assertEqual({error, ?RC_SHARED_SUBSCRIPTIONS_NOT_SUPPORTED}, + emqx_mqtt_caps:check_sub(zone, <<"topic">>, SubOpts#{share => true})), + true = emqx_zone:unset_env(zone, '$mqtt_sub_caps'). diff --git a/test/emqx_mqtt_packet_SUITE.erl b/test/emqx_mqtt_packet_SUITE.erl deleted file mode 100644 index e189ed69d..000000000 --- a/test/emqx_mqtt_packet_SUITE.erl +++ /dev/null @@ -1,115 +0,0 @@ -%% 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_mqtt_packet_SUITE). - --compile(export_all). --compile(nowarn_export_all). - --import(emqx_frame, [serialize/1]). - --include("emqx_mqtt.hrl"). - --include_lib("eunit/include/eunit.hrl"). - --define(INVALID_RESERVED, 1). - --define(CONNECT_INVALID_PACKET(Var), - #mqtt_packet{header = #mqtt_packet_header{type = ?INVALID_RESERVED}, - variable = Var}). - --define(CASE1_PROTOCOL_NAME, ?CONNECT_PACKET(#mqtt_packet_connect{ - proto_name = <<"MQTC">>, - client_id = <<"mqtt_protocol_name">>, - username = <<"admin">>, - password = <<"public">>})). - --define(CASE2_PROTOCAL_VER, ?CONNECT_PACKET(#mqtt_packet_connect{ - client_id = <<"mqtt_client">>, - proto_ver = 6, - username = <<"admin">>, - password = <<"public">>})). - --define(CASE3_PROTOCAL_INVALID_RESERVED, ?CONNECT_INVALID_PACKET(#mqtt_packet_connect{ - client_id = <<"mqtt_client">>, - proto_ver = 5, - username = <<"admin">>, - password = <<"public">>})). - --define(PROTOCOL5, ?CONNECT_PACKET(#mqtt_packet_connect{ - proto_ver = 5, - keepalive = 60, - properties = #{'Message-Expiry-Interval' => 3600}, - client_id = <<"mqtt_client">>, - will_topic = <<"will_tipic">>, - will_payload = <<"will message">>, - username = <<"admin">>, - password = <<"public">>})). - - - -all() -> [{group, connect}]. - -groups() -> [{connect, [sequence], - [case1_protocol_name, - case2_protocol_ver%, - %TOTO case3_invalid_reserved - ]}]. - -init_per_suite(Config) -> - emqx_ct_helpers:start_apps([]), - Config. - -end_per_suite(_Config) -> - emqx_ct_helpers:stop_apps([]). - -init_per_group(_Group, Config) -> - Config. - -end_per_group(_Group, _Config) -> - ok. - -case1_protocol_name(_) -> - {ok, Sock} = emqx_client_sock:connect({127,0,0,1}, 1883, [binary, {packet, raw}, {active, false}], 3000), - MqttPacket = serialize(?CASE1_PROTOCOL_NAME), - emqx_client_sock:send(Sock, MqttPacket), - {ok, Data} = gen_tcp:recv(Sock, 0), - {ok, ?CONNACK_PACKET(?CONNACK_PROTO_VER), <<>>, _} = raw_recv_pase(Data), - Disconnect = gen_tcp:recv(Sock, 0), - ?assertEqual({error, closed}, Disconnect). - -case2_protocol_ver(_) -> - {ok, Sock} = emqx_client_sock:connect({127,0,0,1}, 1883, [binary, {packet, raw}, {active, false}], 3000), - Packet = serialize(?CASE2_PROTOCAL_VER), - emqx_client_sock:send(Sock, Packet), - {ok, Data} = gen_tcp:recv(Sock, 0), - %% case1 Unacceptable protocol version - {ok, ?CONNACK_PACKET(?CONNACK_PROTO_VER), <<>>, _} = raw_recv_pase(Data), - Disconnect = gen_tcp:recv(Sock, 0), - ?assertEqual({error, closed}, Disconnect). - -case3_invalid_reserved(_) -> - {ok, Sock} = emqx_client_sock:connect({127,0,0,1}, 1883, [binary, {packet, raw}, {active, false}], 3000), - Packet = serialize(?CASE3_PROTOCAL_INVALID_RESERVED), - emqx_client_sock:send(Sock, Packet), - {ok, Data} = gen_tcp:recv(Sock, 0), - %% case1 Unacceptable protocol version - ct:log("Data:~p~n", [raw_recv_pase(Data)]), - {ok, ?CONNACK_PACKET(?CONNACK_PROTO_VER), _} = raw_recv_pase(Data), - Disconnect = gen_tcp:recv(Sock, 0), - ?assertEqual({error, closed}, Disconnect). - -raw_recv_pase(P) -> - emqx_frame:parse(P, {none, #{max_packet_size => ?MAX_PACKET_SIZE, - version => ?MQTT_PROTO_V4} }). diff --git a/test/emqx_mqtt_props_SUITE.erl b/test/emqx_mqtt_props_SUITE.erl index 09a2611f7..b1a1b04b4 100644 --- a/test/emqx_mqtt_props_SUITE.erl +++ b/test/emqx_mqtt_props_SUITE.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_mqtt_props_SUITE). @@ -19,9 +21,21 @@ -include("emqx_mqtt.hrl"). -all() -> [t_mqtt_properties_all]. +all() -> emqx_ct:all(?MODULE). -t_mqtt_properties_all(_) -> +t_id(_) -> + 'TODO'. + +t_name(_) -> + 'TODO'. + +t_filter(_) -> + 'TODO'. + +t_validate(_) -> + 'TODO'. + +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}). diff --git a/test/emqx_mqueue_SUITE.erl b/test/emqx_mqueue_SUITE.erl index 8a97ec108..cd15b1b8e 100644 --- a/test/emqx_mqueue_SUITE.erl +++ b/test/emqx_mqueue_SUITE.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_mqueue_SUITE). @@ -24,8 +26,7 @@ -define(Q, emqx_mqueue). -all() -> [t_in, t_in_qos0, t_out, t_simple_mqueue, t_infinity_simple_mqueue, - t_priority_mqueue, t_infinity_priority_mqueue]. +all() -> emqx_ct:all(?MODULE). t_in(_) -> Opts = #{max_len => 5, store_qos0 => true}, @@ -128,15 +129,18 @@ t_infinity_priority_mqueue(_) -> ?assertEqual(510, ?Q:len(Qx)), ?assertEqual([{len, 510}, {max_len, 0}, {dropped, 0}], ?Q:stats(Qx)). -t_priority_mqueue2(_) -> - Opts = #{max_length => 2, store_qos0 => false}, - Q = ?Q:init("priority_queue2_test", Opts), +%%TODO: fixme later +t_length_priority_mqueue(_) -> + Opts = #{max_len => 2, + store_qos0 => false + }, + Q = ?Q:init(Opts), 2 = ?Q:max_len(Q), {_, Q1} = ?Q:in(#message{topic = <<"x">>, qos = 1, payload = <<1>>}, Q), {_, Q2} = ?Q:in(#message{topic = <<"x">>, qos = 1, payload = <<2>>}, Q1), {_, Q3} = ?Q:in(#message{topic = <<"y">>, qos = 1, payload = <<3>>}, Q2), {_, Q4} = ?Q:in(#message{topic = <<"y">>, qos = 1, payload = <<4>>}, Q3), - 4 = ?Q:len(Q4), + ?assertEqual(2, ?Q:len(Q4)), {{value, _Val}, Q5} = ?Q:out(Q4), - 3 = ?Q:len(Q5). + ?assertEqual(1, ?Q:len(Q5)). diff --git a/test/emqx_net_SUITE.erl b/test/emqx_net_SUITE.erl index 47695043b..439ac6c70 100644 --- a/test/emqx_net_SUITE.erl +++ b/test/emqx_net_SUITE.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and %% limitations under the License. +%%-------------------------------------------------------------------- -module(emqx_net_SUITE). diff --git a/test/emqx_os_mon_SUITE.erl b/test/emqx_os_mon_SUITE.erl index 67a3959a5..7d816000f 100644 --- a/test/emqx_os_mon_SUITE.erl +++ b/test/emqx_os_mon_SUITE.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_os_mon_SUITE). @@ -19,9 +21,7 @@ -include_lib("eunit/include/eunit.hrl"). --include_lib("common_test/include/ct.hrl"). - -all() -> [t_api]. +all() -> emqx_ct:all(?MODULE). init_per_suite(Config) -> application:ensure_all_started(os_mon), @@ -54,3 +54,4 @@ t_api(_) -> % timer:sleep(3000), % ?assertEqual(false, lists:keymember(cpu_high_watermark, 1, alarm_handler:get_alarms())), ok. + diff --git a/test/emqx_packet_SUITE.erl b/test/emqx_packet_SUITE.erl index 07fb6c58d..b334093b8 100644 --- a/test/emqx_packet_SUITE.erl +++ b/test/emqx_packet_SUITE.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,7 +12,7 @@ %% 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_packet_SUITE). @@ -19,35 +20,28 @@ -compile(nowarn_export_all). -include("emqx.hrl"). - -include("emqx_mqtt.hrl"). -include_lib("eunit/include/eunit.hrl"). -all() -> - [ - packet_proto_name, - packet_type_name, - packet_validate, - packet_message, - packet_format, - packet_will_msg - ]. +all() -> emqx_ct:all(?MODULE). -packet_proto_name(_) -> +t_proto_name(_) -> ?assertEqual(<<"MQIsdp">>, emqx_packet:protocol_name(3)), ?assertEqual(<<"MQTT">>, emqx_packet:protocol_name(4)), ?assertEqual(<<"MQTT">>, emqx_packet:protocol_name(5)). -packet_type_name(_) -> - ?assertEqual('CONNECT', emqx_packet:type_name(?CONNECT)), +t_type_name(_) -> + ?assertEqual('CONNECT', emqx_packet:type_name(?CONNECT)), ?assertEqual('UNSUBSCRIBE', emqx_packet:type_name(?UNSUBSCRIBE)). -packet_validate(_) -> - ?assert(emqx_packet:validate(?SUBSCRIBE_PACKET(15, #{'Subscription-Identifier' => 1}, [{<<"topic">>, #{qos => ?QOS_0}}]))), +t_validate(_) -> + ?assert(emqx_packet:validate(?SUBSCRIBE_PACKET(15, #{'Subscription-Identifier' => 1}, + [{<<"topic">>, #{qos => ?QOS_0}}]))), ?assert(emqx_packet:validate(?UNSUBSCRIBE_PACKET(89, [<<"topic">>]))), ?assert(emqx_packet:validate(?CONNECT_PACKET(#mqtt_packet_connect{}))), - ?assert(emqx_packet:validate(?PUBLISH_PACKET(1, <<"topic">>, 1, #{'Response-Topic' => <<"responsetopic">>, 'Topic-Alias' => 1}, <<"payload">>))), + Props = #{'Response-Topic' => <<"responsetopic">>, 'Topic-Alias' => 1}, + ?assert(emqx_packet:validate(?PUBLISH_PACKET(?QOS_1, <<"topic">>, 1, Props, <<"payload">>))), ?assert(emqx_packet:validate(?CONNECT_PACKET(#mqtt_packet_connect{properties = #{'Receive-Maximum' => 1}}))), ?assertError(subscription_identifier_invalid, emqx_packet:validate( @@ -88,7 +82,7 @@ packet_validate(_) -> properties = #{'Receive-Maximum' => 0}}))). -packet_message(_) -> +t_from_to_message(_) -> Pkt = #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, qos = ?QOS_0, retain = false, @@ -110,7 +104,7 @@ packet_message(_) -> Msg5 = Msg4#message{timestamp = Msg3#message.timestamp, id = Msg3#message.id}, Msg5 = Msg3. -packet_format(_) -> +t_packet_format(_) -> io:format("~s", [emqx_packet:format(?CONNECT_PACKET(#mqtt_packet_connect{}))]), io:format("~s", [emqx_packet:format(?CONNACK_PACKET(?CONNACK_SERVER))]), io:format("~s", [emqx_packet:format(?PUBLISH_PACKET(?QOS_1, 1))]), @@ -122,15 +116,17 @@ packet_format(_) -> io:format("~s", [emqx_packet:format(?UNSUBSCRIBE_PACKET(89, [<<"t">>, <<"t2">>]))]), io:format("~s", [emqx_packet:format(?UNSUBACK_PACKET(90))]). -packet_will_msg(_) -> - Pkt = #mqtt_packet_connect{ will_flag = true, - client_id = <<"clientid">>, - username = "test", - will_retain = true, - will_qos = ?QOS_2, - will_topic = <<"topic">>, - will_props = #{}, - will_payload = <<"payload">>}, +t_will_msg(_) -> + Pkt = #mqtt_packet_connect{will_flag = true, + client_id = <<"clientid">>, + username = "test", + will_retain = true, + will_qos = ?QOS_2, + will_topic = <<"topic">>, + will_props = #{}, + will_payload = <<"payload">> + }, Msg = emqx_packet:will_msg(Pkt), ?assertEqual(<<"clientid">>, Msg#message.from), ?assertEqual(<<"topic">>, Msg#message.topic). + diff --git a/test/emqx_pd_SUITE.erl b/test/emqx_pd_SUITE.erl index 80b203392..7c08d7f18 100644 --- a/test/emqx_pd_SUITE.erl +++ b/test/emqx_pd_SUITE.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_pd_SUITE). @@ -19,9 +21,9 @@ -include_lib("eunit/include/eunit.hrl"). -all() -> [update_counter]. +all() -> emqx_ct:all(?MODULE). -update_counter(_) -> +t_update_counter(_) -> ?assertEqual(undefined, emqx_pd:update_counter(bytes, 1)), ?assertEqual(1, emqx_pd:update_counter(bytes, 1)), ?assertEqual(2, emqx_pd:update_counter(bytes, 1)), diff --git a/test/emqx_pmon_SUITE.erl b/test/emqx_pmon_SUITE.erl index 6b2170282..17c56729e 100644 --- a/test/emqx_pmon_SUITE.erl +++ b/test/emqx_pmon_SUITE.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_pmon_SUITE). @@ -19,22 +21,24 @@ -include_lib("eunit/include/eunit.hrl"). -all() -> - [t_monitor, t_find, t_erase]. +all() -> emqx_ct:all(?MODULE). t_monitor(_) -> PMon = emqx_pmon:new(), PMon1 = emqx_pmon:monitor(self(), PMon), ?assertEqual(1, emqx_pmon:count(PMon1)), PMon2 = emqx_pmon:demonitor(self(), PMon1), + PMon2 = emqx_pmon:demonitor(self(), PMon2), ?assertEqual(0, emqx_pmon:count(PMon2)). t_find(_) -> PMon = emqx_pmon:new(), PMon1 = emqx_pmon:monitor(self(), val, PMon), + PMon1 = emqx_pmon:monitor(self(), val, PMon1), ?assertEqual(1, emqx_pmon:count(PMon1)), ?assertEqual({ok, val}, emqx_pmon:find(self(), PMon1)), PMon2 = emqx_pmon:erase(self(), PMon1), + PMon2 = emqx_pmon:erase(self(), PMon1), ?assertEqual(error, emqx_pmon:find(self(), PMon2)). t_erase(_) -> @@ -43,6 +47,7 @@ t_erase(_) -> PMon2 = emqx_pmon:erase(self(), PMon1), ?assertEqual(0, emqx_pmon:count(PMon2)), {Items, PMon3} = emqx_pmon:erase_all([self()], PMon1), + {[], PMon3} = emqx_pmon:erase_all([self()], PMon3), ?assertEqual([{self(), val}], Items), ?assertEqual(0, emqx_pmon:count(PMon3)). diff --git a/test/emqx_pool_SUITE.erl b/test/emqx_pool_SUITE.erl index 752e41a12..8ec9d6941 100644 --- a/test/emqx_pool_SUITE.erl +++ b/test/emqx_pool_SUITE.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,26 +12,30 @@ %% 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_pool_SUITE). -compile(export_all). -compile(nowarn_export_all). --include("emqx_mqtt.hrl"). -include_lib("eunit/include/eunit.hrl"). all() -> - [ - {group, submit_case}, - {group, async_submit_case}, + [{group, submit}, + {group, async_submit}, t_unexpected ]. groups() -> - [ - {submit_case, [sequence], [submit_mfa, submit_fa]}, - {async_submit_case, [sequence], [async_submit_mfa, async_submit_crash]} + [{submit, [sequence], + [t_submit_mfa, + t_submit_fa + ]}, + {async_submit, [sequence], + [t_async_submit_mfa, + t_async_submit_crash + ]} ]. init_per_suite(Config) -> @@ -46,22 +51,28 @@ init_per_testcase(_, Config) -> end_per_testcase(_, Config) -> Sup = proplists:get_value(pool_sup, Config), + %% ??? exit(Sup, normal). -submit_mfa(_Config) -> +t_submit_mfa(_Config) -> Result = emqx_pool:submit({?MODULE, test_mfa, []}), ?assertEqual(15, Result). -submit_fa(_Config) -> - Fun = fun(X) -> case X rem 2 of 0 -> {true, X div 2}; _ -> false end end, +t_submit_fa(_Config) -> + Fun = fun(X) -> + case X rem 2 of + 0 -> {true, X div 2}; + _ -> false + end + end, Result = emqx_pool:submit(Fun, [2]), ?assertEqual({true, 1}, Result). -async_submit_mfa(_Config) -> +t_async_submit_mfa(_Config) -> emqx_pool:async_submit({?MODULE, test_mfa, []}), emqx_pool:async_submit(fun ?MODULE:test_mfa/0, []). -async_submit_crash(_) -> +t_async_submit_crash(_) -> emqx_pool:async_submit(fun() -> error(unexpected_error) end). t_unexpected(_) -> diff --git a/test/emqx_pqueue_SUITE.erl b/test/emqx_pqueue_SUITE.erl index 31d3fefd3..3960f7e26 100644 --- a/test/emqx_pqueue_SUITE.erl +++ b/test/emqx_pqueue_SUITE.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,18 +12,19 @@ %% 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_pqueue_SUITE). --include("emqx_mqtt.hrl"). --include_lib("eunit/include/eunit.hrl"). - -compile(export_all). -compile(nowarn_export_all). --define(PQ, emqx_pqueue). +-include_lib("eunit/include/eunit.hrl"). -all() -> [t_priority_queue_plen, t_priority_queue_out2, t_priority_queues]. +-define(PQ, emqx_pqueue). +-define(SUITE, ?MODULE). + +all() -> emqx_ct:all(?SUITE). t_priority_queue_plen(_) -> Q = ?PQ:new(), @@ -85,7 +87,7 @@ t_priority_queues(_) -> [{1, c}, {1, d}, {0, a}, {0, b}] = ?PQ:to_list(PQueue4), PQueue4 = ?PQ:from_list([{1, c}, {1, d}, {0, a}, {0, b}]), - + empty = ?PQ:highest(?PQ:new()), 0 = ?PQ:highest(PQueue1), 1 = ?PQ:highest(PQueue4), @@ -120,4 +122,3 @@ t_priority_queues(_) -> {pqueue,[{-1,{queue,[f],[d,f,d],4}}, {0,{queue,[b],[a,b,a],4}}]} = ?PQ:join(PQueue8, PQueue8). - diff --git a/test/emqx_protocol_SUITE.erl b/test/emqx_protocol_SUITE.erl index 17c55f517..4edcbe3f7 100644 --- a/test/emqx_protocol_SUITE.erl +++ b/test/emqx_protocol_SUITE.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,601 +12,276 @@ %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and %% limitations under the License. +%%-------------------------------------------------------------------- -module(emqx_protocol_SUITE). -compile(export_all). -compile(nowarn_export_all). --include_lib("eunit/include/eunit.hrl"). - --include_lib("common_test/include/ct.hrl"). +-import(emqx_protocol, + [ handle_in/2 + , handle_out/2 + ]). +-include("emqx.hrl"). -include("emqx_mqtt.hrl"). --define(TOPICS, [<<"TopicA">>, <<"TopicA/B">>, <<"Topic/C">>, <<"TopicA/C">>, - <<"/TopicA">>]). +-include_lib("eunit/include/eunit.hrl"). --define(CLIENT, ?CONNECT_PACKET(#mqtt_packet_connect{ - client_id = <<"mqtt_client">>, - username = <<"emqx">>, - password = <<"public">>})). - -all() -> - [{group, mqtt_common}, - {group, mqttv4}, - {group, mqttv5}, - {group, acl}, - {group, frame_partial}]. - -groups() -> - [{mqtt_common, [sequence], - [will_topic_check, - will_acl_check]}, - {mqttv4, [sequence], - [connect_v4, - subscribe_v4]}, - {mqttv5, [sequence], - [connect_v5, - subscribe_v5]}, - {acl, [sequence], - [acl_deny_action_ct]}, - {frame_partial, [sequence], - [handle_followed_packet]}]. +all() -> emqx_ct:all(?MODULE). init_per_suite(Config) -> - emqx_ct_helpers:start_apps([], fun set_special_configs/1), - MqttCaps = maps:from_list(emqx_mqtt_caps:default_caps()), - emqx_zone:set_env(external, '$mqtt_caps', MqttCaps#{max_topic_alias => 20}), + emqx_ct_helpers:start_apps([]), Config. end_per_suite(_Config) -> emqx_ct_helpers:stop_apps([]). -batch_connect(NumberOfConnections) -> - batch_connect([], NumberOfConnections). - -batch_connect(Socks, 0) -> - Socks; -batch_connect(Socks, NumberOfConnections) -> - {ok, Sock} = emqx_client_sock:connect({127, 0, 0, 1}, 1883, - [binary, {packet, raw}, {active, false}], - 3000), - batch_connect([Sock | Socks], NumberOfConnections - 1). - -with_connection(DoFun, NumberOfConnections) -> - Socks = batch_connect(NumberOfConnections), - try - DoFun(Socks) - after - lists:foreach(fun(Sock) -> - emqx_client_sock:close(Sock) - end, Socks) - end. - -with_connection(DoFun) -> - with_connection(DoFun, 1). - -handle_followed_packet(_Config) -> - ConnPkt = <<16,12,0,4,77,81,84,84,4,2,0,60,0,0>>, - PartialPkt1 = <<50,182,1,0,4,116,101,115,116,0,1,48,48,48,48,48,48,48,48,48,48,48,48,48, - 48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48, - 48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48, - 48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48, - 48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48>>, - PartialPkt2 = <<48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48, - 48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48, - 48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48>>, - - %% This is a PUBLISH message (Qos=1) - PubPkt = <>, - ComplexPkt = <>, - - AssertConnAck = fun(R) -> ?assertEqual({ok, <<32,2,0,0>>}, R) end, - AssertPubAck = fun(R) -> ?assertEqual({ok, <<64,2,0,1>>}, R) end, - - {ok, Sock} = gen_tcp:connect("127.0.0.1", 1883, [{active, false}, binary]), - - %% CONNECT - ok = gen_tcp:send(Sock, ConnPkt), - AssertConnAck(gen_tcp:recv(Sock, 4, 500)), - - %% Once Publish - ok = gen_tcp:send(Sock, PubPkt), - AssertPubAck(gen_tcp:recv(Sock, 4, 500)), - - %% Complex Packet - ok = gen_tcp:send(Sock, ComplexPkt), - AssertPubAck(gen_tcp:recv(Sock, 4, 500)), - AssertPubAck(gen_tcp:recv(Sock, 4, 500)), - AssertPubAck(gen_tcp:recv(Sock, 4, 500)), - - ok = gen_tcp:send(Sock, PartialPkt2), - AssertPubAck(gen_tcp:recv(Sock, 4, 500)), - gen_tcp:close(Sock). - -connect_v4(_) -> - with_connection(fun([Sock]) -> - emqx_client_sock:send(Sock, raw_send_serialize(?PACKET(?PUBLISH))), - {error, closed} =gen_tcp:recv(Sock, 0) - end), - with_connection(fun([Sock]) -> - ConnectPacket = raw_send_serialize(?CONNECT_PACKET - (#mqtt_packet_connect{ - client_id = <<"mqttv4_client">>, - username = <<"admin">>, - password = <<"public">>, - proto_ver = ?MQTT_PROTO_V4 - })), - emqx_client_sock:send(Sock, ConnectPacket), - {ok, Data} = gen_tcp:recv(Sock, 0), - {ok, ?CONNACK_PACKET(?CONNACK_ACCEPT), <<>>, _} = raw_recv_parse(Data, ?MQTT_PROTO_V4), - - emqx_client_sock:send(Sock, ConnectPacket), - {error, closed} = gen_tcp:recv(Sock, 0) - end), - ok. - - -connect_v5(_) -> - with_connection(fun([Sock]) -> - emqx_client_sock:send(Sock, - raw_send_serialize( - ?CONNECT_PACKET(#mqtt_packet_connect{ - proto_ver = ?MQTT_PROTO_V5, - properties = - #{'Request-Response-Information' => -1}}))), - {ok, Data} = gen_tcp:recv(Sock, 0), - {ok, ?DISCONNECT_PACKET(?RC_PROTOCOL_ERROR), <<>>, _} = raw_recv_parse(Data, ?MQTT_PROTO_V5) - end), - - with_connection(fun([Sock]) -> - emqx_client_sock:send(Sock, - raw_send_serialize( - ?CONNECT_PACKET( - #mqtt_packet_connect{ - proto_ver = ?MQTT_PROTO_V5, - properties = - #{'Request-Problem-Information' => 2}}))), - {ok, Data} = gen_tcp:recv(Sock, 0), - {ok, ?DISCONNECT_PACKET(?RC_PROTOCOL_ERROR), <<>>, _} = raw_recv_parse(Data, ?MQTT_PROTO_V5) - end), - - with_connection(fun([Sock]) -> - emqx_client_sock:send(Sock, - raw_send_serialize( - ?CONNECT_PACKET( - #mqtt_packet_connect{ - proto_ver = ?MQTT_PROTO_V5, - properties = - #{'Request-Response-Information' => 1}}) - )), - {ok, Data} = gen_tcp:recv(Sock, 0), - {ok, ?CONNACK_PACKET(?RC_SUCCESS, 0, Props), <<>>, _} = - raw_recv_parse(Data, ?MQTT_PROTO_V5), - ?assertNot(maps:is_key('Response-Information', Props)) - end), - - % topic alias = 0 - with_connection(fun([Sock]) -> - - %% ct:log("emqx_protocol: ~p~n", [emqx_zone:get_zone(external, max_topic_alias)]), - emqx_client_sock:send(Sock, - raw_send_serialize( - ?CONNECT_PACKET( - #mqtt_packet_connect{ - client_id = "hello", - proto_ver = ?MQTT_PROTO_V5, - properties = - #{'Topic-Alias-Maximum' => 10}}), - #{version => ?MQTT_PROTO_V5} - )), - {ok, Data} = gen_tcp:recv(Sock, 0), - {ok, ?CONNACK_PACKET(?RC_SUCCESS, 0, - #{'Topic-Alias-Maximum' := 20}), <<>>, _} = - raw_recv_parse(Data, ?MQTT_PROTO_V5), - emqx_client_sock:send(Sock, - raw_send_serialize( - ?PUBLISH_PACKET(?QOS_1, <<"TopicA">>, 1, #{'Topic-Alias' => 0}, <<"hello">>), - #{version => ?MQTT_PROTO_V5} - )), - - {ok, Data2} = gen_tcp:recv(Sock, 0), - {ok, ?DISCONNECT_PACKET(?RC_TOPIC_ALIAS_INVALID), <<>>, _} = raw_recv_parse(Data2, ?MQTT_PROTO_V5) - end), - - % topic alias maximum - with_connection(fun([Sock]) -> - emqx_client_sock:send(Sock, - raw_send_serialize( - ?CONNECT_PACKET( - #mqtt_packet_connect{ - proto_ver = ?MQTT_PROTO_V5, - properties = - #{'Topic-Alias-Maximum' => 10}}), - #{version => ?MQTT_PROTO_V5} - )), - {ok, Data} = gen_tcp:recv(Sock, 0), - {ok, ?CONNACK_PACKET(?RC_SUCCESS, 0, - #{'Topic-Alias-Maximum' := 20}), <<>>, _} = - raw_recv_parse(Data, ?MQTT_PROTO_V5), - - emqx_client_sock:send(Sock, raw_send_serialize(?SUBSCRIBE_PACKET(1, [{<<"TopicA">>, #{rh => 1, - qos => ?QOS_2, - rap => 0, - nl => 0, - rc => 0}}]), #{version => ?MQTT_PROTO_V5})), - - {ok, Data2} = gen_tcp:recv(Sock, 0), - {ok, ?SUBACK_PACKET(1, #{}, [2]), <<>>, _} = raw_recv_parse(Data2, ?MQTT_PROTO_V5), - - emqx_client_sock:send(Sock, - raw_send_serialize( - ?PUBLISH_PACKET(?QOS_1, <<"TopicA">>, 1, #{'Topic-Alias' => 15}, <<"hello">>), - #{version => ?MQTT_PROTO_V5} - )), - - {ok, Data3} = gen_tcp:recv(Sock, 6), - - {ok, ?PUBACK_PACKET(1, 0), <<>>, _} = raw_recv_parse(Data3, ?MQTT_PROTO_V5), - - {ok, Data4} = gen_tcp:recv(Sock, 0), - - {ok, ?PUBLISH_PACKET(?QOS_1, <<"TopicA">>, _, <<"hello">>), <<>>, _} = raw_recv_parse(Data4, ?MQTT_PROTO_V5), - - emqx_client_sock:send(Sock, - raw_send_serialize( - ?PUBLISH_PACKET(?QOS_1, <<"TopicA">>, 2, #{'Topic-Alias' => 21}, <<"hello">>), - #{version => ?MQTT_PROTO_V5} - )), - - {ok, Data5} = gen_tcp:recv(Sock, 0), - {ok, ?DISCONNECT_PACKET(?RC_TOPIC_ALIAS_INVALID), <<>>, _} = raw_recv_parse(Data5, ?MQTT_PROTO_V5) - end), - - % test clean start - with_connection(fun([Sock]) -> - emqx_client_sock:send(Sock, - raw_send_serialize( - ?CONNECT_PACKET( - #mqtt_packet_connect{ - proto_ver = ?MQTT_PROTO_V5, - clean_start = true, - client_id = <<"myclient">>, - properties = - #{'Session-Expiry-Interval' => 10}}) - )), - {ok, Data} = gen_tcp:recv(Sock, 0), - {ok, ?CONNACK_PACKET(?RC_SUCCESS, 0), <<>>, _} = raw_recv_parse(Data, ?MQTT_PROTO_V5), - emqx_client_sock:send(Sock, raw_send_serialize( - ?DISCONNECT_PACKET(?RC_SUCCESS) - )) - end), - - timer:sleep(1000), - - with_connection(fun([Sock]) -> - emqx_client_sock:send(Sock, - raw_send_serialize( - ?CONNECT_PACKET( - #mqtt_packet_connect{ - proto_ver = ?MQTT_PROTO_V5, - clean_start = false, - client_id = <<"myclient">>, - properties = - #{'Session-Expiry-Interval' => 10}}) - )), - {ok, Data} = gen_tcp:recv(Sock, 0), - {ok, ?CONNACK_PACKET(?RC_SUCCESS, 1), <<>>, _} = raw_recv_parse(Data, ?MQTT_PROTO_V5) - end), - - % test will message publish and cancel - with_connection(fun([Sock]) -> - emqx_client_sock:send(Sock, - raw_send_serialize( - ?CONNECT_PACKET( - #mqtt_packet_connect{ - proto_ver = ?MQTT_PROTO_V5, - clean_start = true, - client_id = <<"myclient">>, - will_flag = true, - will_qos = ?QOS_1, - will_retain = false, - will_props = #{'Will-Delay-Interval' => 5}, - will_topic = <<"TopicA">>, - will_payload = <<"will message">>, - properties = #{'Session-Expiry-Interval' => 0} - } - ) - ) - ), - {ok, Data} = gen_tcp:recv(Sock, 0), - {ok, ?CONNACK_PACKET(?RC_SUCCESS, 0), <<>>, _} = raw_recv_parse(Data, ?MQTT_PROTO_V5), - - {ok, Sock2} = emqx_client_sock:connect({127, 0, 0, 1}, 1883, - [binary, {packet, raw}, - {active, false}], 3000), - - do_connect(Sock2, ?MQTT_PROTO_V5), - - emqx_client_sock:send(Sock2, raw_send_serialize(?SUBSCRIBE_PACKET(1, [{<<"TopicA">>, #{rh => 1, - qos => ?QOS_2, - rap => 0, - nl => 0, - rc => 0}}]), #{version => ?MQTT_PROTO_V5})), - - {ok, SubData} = gen_tcp:recv(Sock2, 0), - {ok, ?SUBACK_PACKET(1, #{}, [2]), <<>>, _} = raw_recv_parse(SubData, ?MQTT_PROTO_V5), - - emqx_client_sock:send(Sock, raw_send_serialize( - ?DISCONNECT_PACKET(?RC_SUCCESS))), - - {error, timeout} = gen_tcp:recv(Sock2, 0, 2000), - - % session resumed - {ok, Sock3} = emqx_client_sock:connect({127, 0, 0, 1}, 1883, - [binary, {packet, raw}, - {active, false}], 3000), - - emqx_client_sock:send(Sock3, - raw_send_serialize( - ?CONNECT_PACKET( - #mqtt_packet_connect{ - proto_ver = ?MQTT_PROTO_V5, - clean_start = false, - client_id = <<"myclient">>, - will_flag = true, - will_qos = ?QOS_1, - will_retain = false, - will_props = #{'Will-Delay-Interval' => 5}, - will_topic = <<"TopicA">>, - will_payload = <<"will message 2">>, - properties = #{'Session-Expiry-Interval' => 3} - } - ), - #{version => ?MQTT_PROTO_V5} - ) - ), - {ok, Data3} = gen_tcp:recv(Sock3, 0), - {ok, ?CONNACK_PACKET(?RC_SUCCESS, 0), <<>>, _} = raw_recv_parse(Data3, ?MQTT_PROTO_V5), - - emqx_client_sock:send(Sock3, raw_send_serialize( - ?DISCONNECT_PACKET(?RC_DISCONNECT_WITH_WILL_MESSAGE), - #{version => ?MQTT_PROTO_V5} - ) - ), - - {ok, WillData} = gen_tcp:recv(Sock2, 0, 5000), - {ok, ?PUBLISH_PACKET(?QOS_1, <<"TopicA">>, _, <<"will message 2">>), <<>>, _} - = raw_recv_parse(WillData, ?MQTT_PROTO_V5) - end), - - % duplicate client id - with_connection(fun([Sock, Sock1]) -> - emqx_zone:set_env(external, use_username_as_clientid, true), - emqx_client_sock:send(Sock, - raw_send_serialize( - ?CONNECT_PACKET( - #mqtt_packet_connect{ - proto_ver = ?MQTT_PROTO_V5, - clean_start = true, - client_id = <<"myclient">>, - properties = - #{'Session-Expiry-Interval' => 10}}) - )), - {ok, Data} = gen_tcp:recv(Sock, 0), - {ok, ?CONNACK_PACKET(?RC_SUCCESS, 0), <<>>, _} = raw_recv_parse(Data, ?MQTT_PROTO_V5), - - emqx_client_sock:send(Sock1, - raw_send_serialize( - ?CONNECT_PACKET( - #mqtt_packet_connect{ - proto_ver = ?MQTT_PROTO_V5, - clean_start = false, - client_id = <<"myclient">>, - username = <<"admin">>, - password = <<"public">>, - properties = - #{'Session-Expiry-Interval' => 10}}) - )), - {ok, Data1} = gen_tcp:recv(Sock1, 0), - {ok, ?CONNACK_PACKET(?RC_SUCCESS, 0), <<>>, _} = raw_recv_parse(Data1, ?MQTT_PROTO_V5), - - emqx_client_sock:send(Sock, raw_send_serialize(?SUBSCRIBE_PACKET(1, [{<<"TopicA">>, #{rh => 1, - qos => ?QOS_2, - rap => 0, - nl => 0, - rc => 0}}]), - #{version => ?MQTT_PROTO_V5})), - - {ok, SubData} = gen_tcp:recv(Sock, 0), - {ok, ?SUBACK_PACKET(1, #{}, [2]), <<>>, _} = raw_recv_parse(SubData, ?MQTT_PROTO_V5), - - emqx_client_sock:send(Sock1, raw_send_serialize(?SUBSCRIBE_PACKET(1, [{<<"TopicA">>, #{rh => 1, - qos => ?QOS_2, - rap => 0, - nl => 0, - rc => 0}}]), - #{version => ?MQTT_PROTO_V5})), - - {ok, SubData1} = gen_tcp:recv(Sock1, 0), - {ok, ?SUBACK_PACKET(1, #{}, [2]), <<>>, _} = raw_recv_parse(SubData1, ?MQTT_PROTO_V5) - end, 2), - - ok. - -do_connect(Sock, ProtoVer) -> - emqx_client_sock:send(Sock, raw_send_serialize( - ?CONNECT_PACKET( - #mqtt_packet_connect{ - client_id = <<"mqtt_client">>, - proto_ver = ProtoVer - }))), - {ok, Data} = gen_tcp:recv(Sock, 0), - {ok, ?CONNACK_PACKET(?CONNACK_ACCEPT), <<>>, _} = raw_recv_parse(Data, ProtoVer). - -subscribe_v4(_) -> - with_connection(fun([Sock]) -> - do_connect(Sock, ?MQTT_PROTO_V4), - SubPacket = raw_send_serialize( - ?SUBSCRIBE_PACKET(15, - [{<<"topic">>, #{rh => 1, - qos => ?QOS_2, - rap => 0, - nl => 0, - rc => 0}}])), - emqx_client_sock:send(Sock, SubPacket), - {ok, Data} = gen_tcp:recv(Sock, 0), - {ok, ?SUBACK_PACKET(15, _), <<>>, _} = raw_recv_parse(Data, ?MQTT_PROTO_V4) - end), - ok. - -subscribe_v5(_) -> - with_connection(fun([Sock]) -> - do_connect(Sock, ?MQTT_PROTO_V5), - SubPacket = raw_send_serialize(?SUBSCRIBE_PACKET(15, #{'Subscription-Identifier' => -1},[]), - #{version => ?MQTT_PROTO_V5}), - emqx_client_sock:send(Sock, SubPacket), - {ok, DisConnData} = gen_tcp:recv(Sock, 0), - {ok, ?DISCONNECT_PACKET(?RC_TOPIC_FILTER_INVALID), <<>>, _} = - raw_recv_parse(DisConnData, ?MQTT_PROTO_V5) - end), - with_connection(fun([Sock]) -> - do_connect(Sock, ?MQTT_PROTO_V5), - SubPacket = raw_send_serialize( - ?SUBSCRIBE_PACKET(0, #{}, [{<<"TopicQos0">>, - #{rh => 1, qos => ?QOS_2, - rap => 0, nl => 0, - rc => 0}}]), - #{version => ?MQTT_PROTO_V5}), - emqx_client_sock:send(Sock, SubPacket), - {ok, DisConnData} = gen_tcp:recv(Sock, 0), - ?assertMatch( - {ok, ?DISCONNECT_PACKET(?RC_MALFORMED_PACKET), <<>>, _}, - raw_recv_parse(DisConnData, ?MQTT_PROTO_V5)) - end), - with_connection(fun([Sock]) -> - do_connect(Sock, ?MQTT_PROTO_V5), - SubPacket = raw_send_serialize( - ?SUBSCRIBE_PACKET(1, #{'Subscription-Identifier' => 0}, - [{<<"TopicQos0">>, - #{rh => 1, qos => ?QOS_2, - rap => 0, nl => 0, - rc => 0}}]), - #{version => ?MQTT_PROTO_V5}), - emqx_client_sock:send(Sock, SubPacket), - {ok, DisConnData} = gen_tcp:recv(Sock, 0), - ?assertMatch( - {ok, ?DISCONNECT_PACKET(?RC_SUBSCRIPTION_IDENTIFIERS_NOT_SUPPORTED), <<>>, _}, - raw_recv_parse(DisConnData, ?MQTT_PROTO_V5)) - end), - with_connection(fun([Sock]) -> - do_connect(Sock, ?MQTT_PROTO_V5), - SubPacket = raw_send_serialize( - ?SUBSCRIBE_PACKET(1, #{'Subscription-Identifier' => 1}, - [{<<"TopicQos0">>, - #{rh => 1, qos => ?QOS_2, - rap => 0, nl => 0, - rc => 0}}]), - #{version => ?MQTT_PROTO_V5}), - emqx_client_sock:send(Sock, SubPacket), - {ok, SubData} = gen_tcp:recv(Sock, 0), - {ok, ?SUBACK_PACKET(1, #{}, [2]), <<>>, _} - = raw_recv_parse(SubData, ?MQTT_PROTO_V5) - end), - ok. - -publish_v4(_) -> - ok. - -publish_v5(_) -> - ok. - -raw_send_serialize(Packet) -> - emqx_frame:serialize(Packet). - -raw_send_serialize(Packet, Opts) -> - emqx_frame:serialize(Packet, Opts). - -raw_recv_parse(Bin, ProtoVer) -> - emqx_frame:parse(Bin, emqx_frame:initial_parse_state(#{version => ProtoVer})). - -acl_deny_action_ct(_) -> - emqx_zone:set_env(external, acl_deny_action, disconnect), - process_flag(trap_exit, true), - [acl_deny_do_disconnect(subscribe, QoS, <<"acl_deny_action">>) || QoS <- lists:seq(0, 2)], - [acl_deny_do_disconnect(publish, QoS, <<"acl_deny_action">>) || QoS <- lists:seq(0, 2)], - emqx_zone:set_env(external, acl_deny_action, ignore), - ok. - -will_topic_check(_) -> - {ok, Client} = emqx_client:start_link([{username, <<"emqx">>}, - {will_flag, true}, - {will_topic, <<"aaa">>}, - {will_payload, <<"I have died">>}, - {will_qos, 0}]), - {ok, _} = emqx_client:connect(Client), - - {ok, T} = emqx_client:start_link([{client_id, <<"client">>}]), - emqx_client:connect(T), - emqx_client:subscribe(T, <<"aaa">>), - ct:sleep(200), - - emqx_client:stop(Client), - ct:sleep(100), - false = is_process_alive(Client), - emqx_ct_helpers:wait_mqtt_payload(<<"I have died">>), - emqx_client:stop(T). - -will_acl_check(_) -> - %% The connection will be rejected if publishing of the will message is not allowed by - %% ACL rules - process_flag(trap_exit, true), - {ok, Client} = emqx_client:start_link([{username, <<"pub_deny">>}, - {will_flag, true}, - {will_topic, <<"pub_deny">>}, - {will_payload, <<"I have died">>}, - {will_qos, 0}]), - ?assertMatch({error,{_,_}}, emqx_client:connect(Client)). - -acl_deny_do_disconnect(publish, QoS, Topic) -> - process_flag(trap_exit, true), - {ok, Client} = emqx_client:start_link([{username, <<"emqx">>}]), - {ok, _} = emqx_client:connect(Client), - emqx_client:publish(Client, Topic, <<"test">>, QoS), - receive - {disconnected, shutdown, tcp_closed} -> - ct:pal(info, "[OK] after publish, client got disconnected: tcp_closed", []); - {'EXIT', Client, {shutdown,tcp_closed}} -> - ct:pal(info, "[OK] after publish, received exit: {shutdown,tcp_closed}"), - false = is_process_alive(Client); - {'EXIT', Client, Reason} -> - ct:pal(info, "[OK] after publish, client got disconnected: ~p", [Reason]) - after 1000 -> ct:fail({timeout, wait_tcp_closed}) - end; - -acl_deny_do_disconnect(subscribe, QoS, Topic) -> - process_flag(trap_exit, true), - {ok, Client} = emqx_client:start_link([{username, <<"emqx">>}]), - {ok, _} = emqx_client:connect(Client), - {ok, _, [128]} = emqx_client:subscribe(Client, Topic, QoS), - receive - {disconnected, shutdown, tcp_closed} -> - ct:pal(info, "[OK] after subscribe, client got disconnected: tcp_closed", []); - {'EXIT', Client, {shutdown,tcp_closed}} -> - ct:pal(info, "[OK] after subscribe, received exit: {shutdown,tcp_closed}"), - false = is_process_alive(Client); - {'EXIT', Client, Reason} -> - ct:pal(info, "[OK] after subscribe, client got disconnected: ~p", [Reason]) - after 1000 -> ct:fail({timeout, wait_tcp_closed}) - end. - -set_special_configs(emqx) -> - application:set_env(emqx, enable_acl_cache, false), - application:set_env(emqx, plugins_loaded_file, - emqx_ct_helpers:deps_path(emqx, "test/emqx_SUITE_data/loaded_plugins")), - application:set_env(emqx, acl_deny_action, disconnect), - application:set_env(emqx, acl_file, - emqx_ct_helpers:deps_path(emqx, "test/emqx_access_SUITE_data/acl_deny_action.conf")); -set_special_configs(_App) -> - ok. +%%-------------------------------------------------------------------- +%% Test cases for handle_in +%%-------------------------------------------------------------------- + +t_handle_connect(_) -> + ConnPkt = #mqtt_packet_connect{ + proto_name = <<"MQTT">>, + proto_ver = ?MQTT_PROTO_V4, + is_bridge = false, + clean_start = true, + keepalive = 30, + properties = #{}, + client_id = <<"clientid">>, + username = <<"username">>, + password = <<"passwd">> + }, + with_proto( + fun(PState) -> + {ok, ?CONNACK_PACKET(?RC_SUCCESS), PState1} + = handle_in(?CONNECT_PACKET(ConnPkt), PState), + Client = emqx_protocol:info(client, PState1), + ?assertEqual(<<"clientid">>, maps:get(client_id, Client)), + ?assertEqual(<<"username">>, maps:get(username, Client)) + end). + +t_handle_publish_qos0(_) -> + with_proto( + fun(PState) -> + Publish = ?PUBLISH_PACKET(?QOS_0, <<"topic">>, undefined, <<"payload">>), + {ok, PState} = handle_in(Publish, PState) + end). + +t_handle_publish_qos1(_) -> + with_proto( + fun(PState) -> + Publish = ?PUBLISH_PACKET(?QOS_1, <<"topic">>, 1, <<"payload">>), + {ok, ?PUBACK_PACKET(1, RC), _} = handle_in(Publish, PState), + ?assert((RC == ?RC_SUCCESS) orelse (RC == ?RC_NO_MATCHING_SUBSCRIBERS)) + end). + +t_handle_publish_qos2(_) -> + with_proto( + fun(PState) -> + Publish1 = ?PUBLISH_PACKET(?QOS_2, <<"topic">>, 1, <<"payload">>), + {ok, ?PUBREC_PACKET(1, RC), PState1} = handle_in(Publish1, PState), + Publish2 = ?PUBLISH_PACKET(?QOS_2, <<"topic">>, 2, <<"payload">>), + {ok, ?PUBREC_PACKET(2, RC), PState2} = handle_in(Publish2, PState1), + ?assert((RC == ?RC_SUCCESS) orelse (RC == ?RC_NO_MATCHING_SUBSCRIBERS)), + Session = emqx_protocol:info(session, PState2), + ?assertEqual(2, emqx_session:info(awaiting_rel, Session)) + end). + +t_handle_puback(_) -> + with_proto( + fun(PState) -> + {ok, PState} = handle_in(?PUBACK_PACKET(1, ?RC_SUCCESS), PState) + end). + +t_handle_pubrec(_) -> + with_proto( + fun(PState) -> + {ok, ?PUBREL_PACKET(1, ?RC_PACKET_IDENTIFIER_NOT_FOUND), PState} + = handle_in(?PUBREC_PACKET(1, ?RC_SUCCESS), PState) + end). + +t_handle_pubrel(_) -> + with_proto( + fun(PState) -> + {ok, ?PUBCOMP_PACKET(1, ?RC_PACKET_IDENTIFIER_NOT_FOUND), PState} + = handle_in(?PUBREL_PACKET(1, ?RC_SUCCESS), PState) + end). + +t_handle_pubcomp(_) -> + with_proto( + fun(PState) -> + {ok, PState} = handle_in(?PUBCOMP_PACKET(1, ?RC_SUCCESS), PState) + end). + +t_handle_subscribe(_) -> + with_proto( + fun(PState) -> + TopicFilters = [{<<"+">>, ?DEFAULT_SUBOPTS}], + {ok, ?SUBACK_PACKET(10, [?QOS_0]), PState1} + = handle_in(?SUBSCRIBE_PACKET(10, #{}, TopicFilters), PState), + Session = emqx_protocol:info(session, PState1), + ?assertEqual(maps:from_list(TopicFilters), + emqx_session:info(subscriptions, Session)) + + end). + +t_handle_unsubscribe(_) -> + with_proto( + fun(PState) -> + {ok, ?UNSUBACK_PACKET(11), PState} + = handle_in(?UNSUBSCRIBE_PACKET(11, #{}, [<<"+">>]), PState) + end). + +t_handle_pingreq(_) -> + with_proto( + fun(PState) -> + {ok, ?PACKET(?PINGRESP), PState} = handle_in(?PACKET(?PINGREQ), PState) + end). + +t_handle_disconnect(_) -> + with_proto( + fun(PState) -> + {stop, normal, PState1} = handle_in(?DISCONNECT_PACKET(?RC_SUCCESS), PState), + ?assertEqual(undefined, emqx_protocol:info(will_msg, PState1)) + end). + +t_handle_auth(_) -> + with_proto( + fun(PState) -> + {ok, PState} = handle_in(?AUTH_PACKET(), PState) + end). + +%%-------------------------------------------------------------------- +%% Test cases for handle_deliver +%%-------------------------------------------------------------------- + +t_handle_deliver(_) -> + with_proto( + fun(PState) -> + TopicFilters = [{<<"+">>, ?DEFAULT_SUBOPTS#{qos => ?QOS_2}}], + {ok, ?SUBACK_PACKET(1, [?QOS_2]), PState1} + = handle_in(?SUBSCRIBE_PACKET(1, #{}, TopicFilters), PState), + Msg0 = emqx_message:make(<<"clientx">>, ?QOS_0, <<"t0">>, <<"qos0">>), + Msg1 = emqx_message:make(<<"clientx">>, ?QOS_1, <<"t1">>, <<"qos1">>), + Delivers = [{deliver, <<"+">>, Msg0}, + {deliver, <<"+">>, Msg1}], + {ok, Packets, _PState2} = emqx_protocol:handle_deliver(Delivers, PState1), + ?assertMatch([?PUBLISH_PACKET(?QOS_0, <<"t0">>, undefined, <<"qos0">>), + ?PUBLISH_PACKET(?QOS_1, <<"t1">>, 1, <<"qos1">>) + ], Packets) + end). + +%%-------------------------------------------------------------------- +%% Test cases for handle_out +%%-------------------------------------------------------------------- + +t_handle_conack(_) -> + with_proto( + fun(PState) -> + {ok, ?CONNACK_PACKET(?RC_SUCCESS, SP, _), _} + = handle_out({connack, ?RC_SUCCESS, 0}, PState), + {error, unauthorized_client, ?CONNACK_PACKET(5), _} + = handle_out({connack, ?RC_NOT_AUTHORIZED}, PState) + end). + +t_handle_out_publish(_) -> + with_proto( + fun(PState) -> + Pub0 = {publish, undefined, emqx_message:make(<<"t">>, <<"qos0">>)}, + Pub1 = {publish, 1, emqx_message:make(<<"c">>, ?QOS_1, <<"t">>, <<"qos1">>)}, + {ok, ?PUBLISH_PACKET(?QOS_0), PState} = handle_out(Pub0, PState), + {ok, ?PUBLISH_PACKET(?QOS_1), PState} = handle_out(Pub1, PState), + {ok, Packets, PState} = handle_out({publish, [Pub0, Pub1]}, PState), + ?assertEqual(2, length(Packets)) + end). + +t_handle_out_puback(_) -> + with_proto( + fun(PState) -> + {ok, PState} = handle_out({puberr, ?RC_NOT_AUTHORIZED}, PState), + {ok, ?PUBACK_PACKET(1, ?RC_SUCCESS), PState} + = handle_out({puback, 1, ?RC_SUCCESS}, PState) + end). + +t_handle_out_pubrec(_) -> + with_proto( + fun(PState) -> + {ok, ?PUBREC_PACKET(4, ?RC_SUCCESS), PState} + = handle_out({pubrec, 4, ?RC_SUCCESS}, PState) + end). + +t_handle_out_pubrel(_) -> + with_proto( + fun(PState) -> + {ok, ?PUBREL_PACKET(2), PState} = handle_out({pubrel, 2}, PState), + {ok, ?PUBREL_PACKET(3, ?RC_SUCCESS), PState} + = handle_out({pubrel, 3, ?RC_SUCCESS}, PState) + end). + +t_handle_out_pubcomp(_) -> + with_proto( + fun(PState) -> + {ok, ?PUBCOMP_PACKET(5, ?RC_SUCCESS), PState} + = handle_out({pubcomp, 5, ?RC_SUCCESS}, PState) + end). + +t_handle_out_suback(_) -> + with_proto( + fun(PState) -> + {ok, ?SUBACK_PACKET(1, [?QOS_2]), PState} + = handle_out({suback, 1, [?QOS_2]}, PState) + end). + +t_handle_out_unsuback(_) -> + with_proto( + fun(PState) -> + {ok, ?UNSUBACK_PACKET(1), PState} = handle_out({unsuback, 1, [?RC_SUCCESS]}, PState) + end). + +t_handle_out_disconnect(_) -> + with_proto( + fun(PState) -> + handle_out({disconnect, 0}, PState) + end). + +%%-------------------------------------------------------------------- +%% Test cases for handle_timeout +%%-------------------------------------------------------------------- + +t_handle_timeout(_) -> + with_proto( + fun(PState) -> + 'TODO' + end). + +%%-------------------------------------------------------------------- +%% Test cases for terminate +%%-------------------------------------------------------------------- + +t_terminate(_) -> + with_proto( + fun(PState) -> + 'TODO' + end). + +%%-------------------------------------------------------------------- +%% Helper functions +%%-------------------------------------------------------------------- + +with_proto(Fun) -> + ConnInfo = #{peername => {{127,0,0,1}, 3456}, + sockname => {{127,0,0,1}, 1883}, + client_id => <<"clientid">>, + username => <<"username">> + }, + Options = [{zone, testing}], + PState = emqx_protocol:init(ConnInfo, Options), + Session = emqx_session:init(false, #{zone => testing}, + #{max_inflight => 100, + expiry_interval => 0 + }), + Fun(emqx_protocol:set(session, Session, PState)). + diff --git a/test/emqx_protocol_tests.erl b/test/emqx_protocol_tests.erl deleted file mode 100644 index acadfe79d..000000000 --- a/test/emqx_protocol_tests.erl +++ /dev/null @@ -1,30 +0,0 @@ -%% 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_protocol_tests). - --include_lib("eunit/include/eunit.hrl"). - -set_property_test() -> - ?assertEqual(#{test => test_property}, emqx_protocol:set_property(test, test_property, undefined)), - TestMap = #{test => test_property}, - ?assertEqual(#{test => test_property, test1 => test_property2}, - emqx_protocol:set_property(test1, test_property2, TestMap)), - ok. - -init_username_test() -> - ?assertEqual(<<"Peercert">>, - emqx_protocol:init_username(<<"Peercert">>, [{peer_cert_as_username, crt}])), - ?assertEqual(undefined, - emqx_protocol:init_username(undefined, [{peer_cert_as_username, undefined}])). diff --git a/test/emqx_reason_codes_tests.erl b/test/emqx_reason_codes_SUITE.erl similarity index 94% rename from test/emqx_reason_codes_tests.erl rename to test/emqx_reason_codes_SUITE.erl index bf38a451e..c0f1c0e74 100644 --- a/test/emqx_reason_codes_tests.erl +++ b/test/emqx_reason_codes_SUITE.erl @@ -1,6 +1,7 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% Copyright (c) 2019 EMQ Technologies Co., Ltd. All Rights Reserved. %% -%% Licensed under the Apache License, Version 2.0 (the "License") +%% 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 %% @@ -11,12 +12,15 @@ %% 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_reason_codes_tests). +-module(emqx_reason_codes_SUITE). --include_lib("eunit/include/eunit.hrl"). +-compile(export_all). +-compile(nowarn_export_all). -include("emqx_mqtt.hrl"). +-include_lib("eunit/include/eunit.hrl"). -import(lists, [seq/2, zip/2, foreach/2]). @@ -86,25 +90,27 @@ ?CONNACK_AUTH, ?CONNACK_SERVER, ?CONNACK_SERVER, ?CONNACK_SERVER, ?CONNACK_SERVER]). -mqttv4_name_test() -> +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). -mqttv5_name_test() -> +t_mqttv5_name(_) -> (((codes_test(?MQTT_PROTO_V5)) (?MQTTV5_CODES)) (?MQTTV5_CODE_NAMES)) (fun emqx_reason_codes:name/2). -text_test() -> +t_text(_) -> (((codes_test(?MQTT_PROTO_V5)) (?MQTTV5_CODES)) (?MQTTV5_TXT)) (fun emqx_reason_codes:text/1). -compat_test() -> +t_compat(_) -> (((codes_test(connack)) (?COMPAT_CODES_V5)) (?COMPAT_CODES_V4)) diff --git a/test/emqx_request_handler.erl b/test/emqx_request_handler.erl deleted file mode 100644 index d80288023..000000000 --- a/test/emqx_request_handler.erl +++ /dev/null @@ -1,102 +0,0 @@ -%% 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. - -%% @doc This module implements a request handler based on emqx_client. -%% A request handler is a MQTT client which subscribes to a request topic, -%% processes the requests then send response to another topic which is -%% subscribed by the request sender. -%% This code is in test directory because request and response are pure -%% client-side behaviours. - --module(emqx_request_handler). - --export([start_link/4, stop/1]). - --include("emqx_client.hrl"). - --type qos() :: emqx_mqtt_types:qos_name() | emqx_mqtt_types:qos(). --type topic() :: emqx_topic:topic(). --type handler() :: fun((CorrData :: binary(), ReqPayload :: binary()) -> RspPayload :: binary()). - --spec start_link(topic(), qos(), handler(), emqx_client:options()) -> - {ok, pid()} | {error, any()}. -start_link(RequestTopic, QoS, RequestHandler, Options0) -> - Parent = self(), - MsgHandler = make_msg_handler(RequestHandler, Parent), - Options = [{msg_handler, MsgHandler} | Options0], - case emqx_client:start_link(Options) of - {ok, Pid} -> - {ok, _} = emqx_client:connect(Pid), - try subscribe(Pid, RequestTopic, QoS) of - ok -> {ok, Pid}; - {error, _} = Error -> Error - catch - C : E : S -> - emqx_client:stop(Pid), - {error, {C, E, S}} - end; - {error, _} = Error -> Error - end. - -stop(Pid) -> - emqx_client:disconnect(Pid). - -make_msg_handler(RequestHandler, Parent) -> - #{publish => fun(Msg) -> handle_msg(Msg, RequestHandler, Parent) end, - puback => fun(_Ack) -> ok end, - disconnected => fun(_Reason) -> ok end - }. - -handle_msg(ReqMsg, RequestHandler, Parent) -> - #{qos := QoS, properties := Props, payload := ReqPayload} = ReqMsg, - case maps:find('Response-Topic', Props) of - {ok, RspTopic} when RspTopic =/= <<>> -> - CorrData = maps:get('Correlation-Data', Props), - RspProps = maps:without(['Response-Topic'], Props), - RspPayload = RequestHandler(CorrData, ReqPayload), - RspMsg = #mqtt_msg{qos = QoS, - topic = RspTopic, - props = RspProps, - payload = RspPayload - }, - emqx_logger:debug("~p sending response msg to topic ~s with~n" - "corr-data=~p~npayload=~p", - [?MODULE, RspTopic, CorrData, RspPayload]), - ok = send_response(RspMsg); - _ -> - Parent ! {discarded, ReqPayload}, - ok - end. - -send_response(Msg) -> - %% This function is evaluated by emqx_client itself. - %% hence delegate to another temp process for the loopback gen_statem call. - Client = self(), - _ = spawn_link(fun() -> - case emqx_client:publish(Client, Msg) of - ok -> ok; - {ok, _} -> ok; - {error, Reason} -> exit({failed_to_publish_response, Reason}) - end - end), - ok. - -subscribe(Client, Topic, QoS) -> - {ok, _Props, _QoS} = - emqx_client:subscribe(Client, [{Topic, [{rh, 2}, {rap, false}, - {nl, true}, {qos, QoS}]}]), - ok. - - - diff --git a/test/emqx_request_response_SUITE.erl b/test/emqx_request_response_SUITE.erl deleted file mode 100644 index 373619f1d..000000000 --- a/test/emqx_request_response_SUITE.erl +++ /dev/null @@ -1,69 +0,0 @@ -%% 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_response_SUITE). - --compile(export_all). --compile(nowarn_export_all). - --include("emqx_mqtt.hrl"). --include_lib("eunit/include/eunit.hrl"). --include_lib("common_test/include/ct.hrl"). - -init_per_suite(Config) -> - emqx_ct_helpers:start_apps([]), - Config. - -end_per_suite(_Config) -> - emqx_ct_helpers:stop_apps([]). - -all() -> - [request_response]. - -request_response(_Config) -> - request_response_per_qos(?QOS_0), - request_response_per_qos(?QOS_1), - request_response_per_qos(?QOS_2). - -request_response_per_qos(QoS) -> - ReqTopic = <<"request_topic">>, - RspTopic = <<"response_topic">>, - {ok, Requester} = emqx_request_sender:start_link(RspTopic, QoS, - [{proto_ver, v5}, - {client_id, <<"requester">>}, - {properties, #{ 'Request-Response-Information' => 1}}]), - %% This is a square service - Square = fun(_CorrData, ReqBin) -> - I = b2i(ReqBin), - i2b(I * I) - end, - {ok, Responser} = emqx_request_handler:start_link(ReqTopic, QoS, Square, - [{proto_ver, v5}, - {client_id, <<"responser">>} - ]), - ok = emqx_request_sender:send(Requester, ReqTopic, RspTopic, <<"corr-1">>, <<"2">>, QoS), - receive - {response, <<"corr-1">>, <<"4">>} -> - ok; - Other -> - erlang:error({unexpected, Other}) - after - 100 -> - erlang:error(timeout) - end, - ok = emqx_request_sender:stop(Requester), - ok = emqx_request_handler:stop(Responser). - -b2i(B) -> binary_to_integer(B). -i2b(I) -> integer_to_binary(I). diff --git a/test/emqx_request_sender.erl b/test/emqx_request_sender.erl deleted file mode 100644 index e01db96c0..000000000 --- a/test/emqx_request_sender.erl +++ /dev/null @@ -1,82 +0,0 @@ -%% 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. - -%% @doc This module implements a request sender based on emqx_client. -%% A request sender is a MQTT client which sends messages to a request -%% topic, and subscribes to another topic for responses. -%% This code is in test directory because request and response are pure -%% client-side behaviours. - --module(emqx_request_sender). - --export([start_link/3, stop/1, send/6]). - --include("emqx_client.hrl"). - -start_link(ResponseTopic, QoS, Options0) -> - Parent = self(), - MsgHandler = make_msg_handler(Parent), - Options = [{msg_handler, MsgHandler} | Options0], - case emqx_client:start_link(Options) of - {ok, Pid} -> - {ok, _} = emqx_client:connect(Pid), - try subscribe(Pid, ResponseTopic, QoS) of - ok -> {ok, Pid}; - {error, _} = Error -> Error - catch - C : E : S -> - emqx_client:stop(Pid), - {error, {C, E, S}} - end; - {error, _} = Error -> Error - end. - -%% @doc Send a message to request topic with correlation-data `CorrData'. -%% Response should be delivered as a `{response, CorrData, Payload}' -send(Client, ReqTopic, RspTopic, CorrData, Payload, QoS) -> - Props = #{'Response-Topic' => RspTopic, - 'Correlation-Data' => CorrData - }, - Msg = #mqtt_msg{qos = QoS, - topic = ReqTopic, - props = Props, - payload = Payload - }, - case emqx_client:publish(Client, Msg) of - ok -> ok; %% QoS = 0 - {ok, _} -> ok; - {error, _} = E -> E - end. - -stop(Pid) -> - emqx_client:disconnect(Pid). - -subscribe(Client, Topic, QoS) -> - case emqx_client:subscribe(Client, Topic, QoS) of - {ok, _, _} -> ok; - {error, _} = Error -> Error - end. - -make_msg_handler(Parent) -> - #{publish => fun(Msg) -> handle_msg(Msg, Parent) end, - puback => fun(_Ack) -> ok end, - disconnected => fun(_Reason) -> ok end - }. - -handle_msg(Msg, Parent) -> - #{properties := Props, payload := Payload} = Msg, - CorrData = maps:get('Correlation-Data', Props), - Parent ! {response, CorrData, Payload}, - ok. - diff --git a/test/emqx_router_SUITE.erl b/test/emqx_router_SUITE.erl index 4fed02835..711f1b1f9 100644 --- a/test/emqx_router_SUITE.erl +++ b/test/emqx_router_SUITE.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,29 +12,19 @@ %% 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_router_SUITE). --include("emqx.hrl"). --include_lib("eunit/include/eunit.hrl"). - -compile(export_all). -compile(nowarn_export_all). +-include("emqx.hrl"). +-include_lib("eunit/include/eunit.hrl"). + -define(R, emqx_router). -all() -> - [{group, route}]. - -groups() -> - [{route, [sequence], - [t_mnesia, - t_add_delete, - t_do_add_delete, - t_match_routes, - t_print_routes, - t_has_routes, - t_unexpected]}]. +all() -> emqx_ct:all(?MODULE). init_per_suite(Config) -> emqx_ct_helpers:start_apps([]), @@ -105,4 +96,6 @@ t_unexpected(_) -> Router ! bad_info. clear_tables() -> - lists:foreach(fun mnesia:clear_table/1, [emqx_route, emqx_trie, emqx_trie_node]). + lists:foreach(fun mnesia:clear_table/1, + [emqx_route, emqx_trie, emqx_trie_node]). + diff --git a/test/emqx_rpc_SUITE.erl b/test/emqx_rpc_SUITE.erl deleted file mode 100644 index 41c71b1bc..000000000 --- a/test/emqx_rpc_SUITE.erl +++ /dev/null @@ -1,36 +0,0 @@ -%% 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_rpc_SUITE). - --include("emqx.hrl"). --include_lib("eunit/include/eunit.hrl"). - --compile(export_all). --compile(nowarn_export_all). --define(MASTER, 'emqxct@127.0.0.1'). - -all() -> [t_rpc]. - -init_per_suite(Config) -> - emqx_ct_helpers:start_apps([]), - Config. - -end_per_suite(_Config) -> - emqx_ct_helpers:stop_apps([]). - -t_rpc(_) -> - 60000 = emqx_rpc:call(?MASTER, timer, seconds, [60]), - {badrpc, _} = emqx_rpc:call(?MASTER, os, test, []), - {_, []} = emqx_rpc:multicall([?MASTER, ?MASTER], os, timestamp, []). diff --git a/test/emqx_sequence_SUITE.erl b/test/emqx_sequence_SUITE.erl index 7b95c0c67..a5c49f2f6 100644 --- a/test/emqx_sequence_SUITE.erl +++ b/test/emqx_sequence_SUITE.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_sequence_SUITE). @@ -19,20 +21,29 @@ -include_lib("eunit/include/eunit.hrl"). --import(emqx_sequence, [nextval/2, reclaim/2]). +-import(emqx_sequence, + [ nextval/2 + , currval/2 + , reclaim/2 + ]). -all() -> - [sequence_generate]. +all() -> emqx_ct:all(?MODULE). -sequence_generate(_) -> +t_generate(_) -> ok = emqx_sequence:create(seqtab), + ?assertEqual(0, currval(seqtab, key)), ?assertEqual(1, nextval(seqtab, key)), + ?assertEqual(1, currval(seqtab, key)), ?assertEqual(2, nextval(seqtab, key)), + ?assertEqual(2, currval(seqtab, key)), ?assertEqual(3, nextval(seqtab, key)), ?assertEqual(2, reclaim(seqtab, key)), ?assertEqual(1, reclaim(seqtab, key)), ?assertEqual(0, reclaim(seqtab, key)), - ?assertEqual(false, ets:member(seqtab, key)), ?assertEqual(1, nextval(seqtab, key)), - ?assert(emqx_sequence:delete(seqtab)). + ?assertEqual(0, reclaim(seqtab, key)), + ?assertEqual(0, reclaim(seqtab, key)), + ?assertEqual(false, ets:member(seqtab, key)), + ?assert(emqx_sequence:delete(seqtab)), + ?assertNot(emqx_sequence:delete(seqtab)). diff --git a/test/emqx_session_SUITE.erl b/test/emqx_session_SUITE.erl index 1bab8b216..8736d69b7 100644 --- a/test/emqx_session_SUITE.erl +++ b/test/emqx_session_SUITE.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_session_SUITE). @@ -19,9 +21,7 @@ -include_lib("eunit/include/eunit.hrl"). --include_lib("common_test/include/ct.hrl"). - -all() -> [ignore_loop, t_session_all]. +all() -> emqx_ct:all(?MODULE). init_per_suite(Config) -> emqx_ct_helpers:start_apps([]), @@ -30,6 +30,42 @@ init_per_suite(Config) -> end_per_suite(_Config) -> emqx_ct_helpers:stop_apps([]). +t_info(_) -> + 'TODO'. + +t_attrs(_) -> + 'TODO'. + +t_stats(_) -> + 'TODO'. + +t_subscribe(_) -> + 'TODO'. + +t_unsubscribe(_) -> + 'TODO'. + +t_publish(_) -> + 'TODO'. + +t_puback(_) -> + 'TODO'. + +t_pubrec(_) -> + 'TODO'. + +t_pubrel(_) -> + 'TODO'. + +t_pubcomp(_) -> + 'TODO'. + +t_deliver(_) -> + 'TODO'. + +t_timeout(_) -> + 'TODO'. + ignore_loop(_Config) -> emqx_zone:set_env(external, ignore_loop_deliver, true), {ok, Client} = emqx_client:start_link(), @@ -43,7 +79,7 @@ ignore_loop(_Config) -> ok = emqx_client:disconnect(Client), emqx_zone:set_env(external, ignore_loop_deliver, false). -t_session_all(_) -> +session_all(_) -> emqx_zone:set_env(internal, idle_timeout, 1000), ClientId = <<"ClientId">>, {ok, ConnPid} = emqx_mock_client:start_link(ClientId), @@ -66,3 +102,4 @@ t_session_all(_) -> timer:sleep(200), [] = emqx:subscriptions(SPid), emqx_mock_client:close_session(ConnPid). + diff --git a/test/emqx_shared_sub_SUITE.erl b/test/emqx_shared_sub_SUITE.erl index ae67cde69..62b7c441b 100644 --- a/test/emqx_shared_sub_SUITE.erl +++ b/test/emqx_shared_sub_SUITE.erl @@ -1,5 +1,5 @@ - -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -12,32 +12,24 @@ %% 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_shared_sub_SUITE). --export([all/0, init_per_suite/1, end_per_suite/1]). --export([t_random_basic/1, - t_random/1, - t_round_robin/1, - t_sticky/1, - t_hash/1, - t_not_so_sticky/1, - t_no_connection_nack/1 - ]). +-compile(export_all). +-compile(nowarn_export_all). -include("emqx.hrl"). -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). --define(wait(For, Timeout), emqx_ct_helpers:wait_for(?FUNCTION_NAME, ?LINE, fun() -> For end, Timeout)). +-define(SUITE, ?MODULE). -all() -> [t_random_basic, - t_random, - t_round_robin, - t_sticky, - t_hash, - t_not_so_sticky, - t_no_connection_nack]. +-define(wait(For, Timeout), + emqx_ct_helpers:wait_for( + ?FUNCTION_NAME, ?LINE, fun() -> For end, Timeout)). + +all() -> emqx_ct:all(?SUITE). init_per_suite(Config) -> emqx_ct_helpers:start_apps([]), @@ -49,21 +41,23 @@ end_per_suite(_Config) -> t_random_basic(_) -> ok = ensure_config(random), 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, <<"foo">>, <<"hello">>), - emqx_session:subscribe(SPid, [{<<"foo">>, #{qos => 2, share => <<"group1">>}}]), + Topic = <<"foo">>, + Payload = <<"hello">>, + emqx:subscribe(Topic, #{qos => 2, share => <<"group1">>}), + MsgQoS2 = emqx_message:make(ClientId, 2, Topic, Payload), %% wait for the subscription to show up - ?wait(subscribed(<<"group1">>, <<"foo">>, SPid), 1000), - PacketId = 1, - emqx_session:publish(SPid, PacketId, Message1), - ?wait(case emqx_mock_client:get_last_message(ConnPid) of - [{publish, 1, _}] -> true; - Other -> Other - end, 1000), - emqx_session:pubrec(SPid, PacketId, reasoncode), - emqx_session:pubcomp(SPid, PacketId, reasoncode), - emqx_mock_client:close_session(ConnPid), + ct:sleep(200), + ?assertEqual(true, subscribed(<<"group1">>, Topic, self())), + emqx:publish(MsgQoS2), + receive + {deliver, Topic0, #message{from = ClientId0, + payload = Payload0}} = M-> + ct:pal("==== received: ~p", [M]), + ?assertEqual(Topic, Topic0), + ?assertEqual(ClientId, ClientId0), + ?assertEqual(Payload, Payload0) + after 1000 -> ct:fail(waiting_basic_failed) + end, ok. %% Start two subscribers share subscribe to "$share/g1/foo/bar" @@ -82,69 +76,61 @@ t_no_connection_nack(_) -> QoS = 1, Group = <<"g1">>, Topic = <<"foo/bar">>, - {ok, PubConnPid} = emqx_mock_client:start_link(Publisher), - {ok, SubConnPid1} = emqx_mock_client:start_link(Subscriber1), - {ok, SubConnPid2} = emqx_mock_client:start_link(Subscriber2), - %% allow session to persist after connection shutdown - Attrs = #{expiry_interval => timer:seconds(30)}, - {ok, P_Pid} = emqx_mock_client:open_session(PubConnPid, Publisher, internal, Attrs), - {ok, SPid1} = emqx_mock_client:open_session(SubConnPid1, Subscriber1, internal, Attrs), - {ok, SPid2} = emqx_mock_client:open_session(SubConnPid2, Subscriber2, internal, Attrs), - emqx_session:subscribe(SPid1, [{Topic, #{qos => QoS, share => Group}}]), - emqx_session:subscribe(SPid2, [{Topic, #{qos => QoS, share => Group}}]), + ShareTopic = <<"$share/", Group/binary, $/, Topic/binary>>, + + ExpProp = [{properties, #{'Session-Expiry-Interval' => timer:seconds(30)}}], + {ok, SubConnPid1} = emqtt:start_link([{client_id, Subscriber1}] ++ ExpProp), + {ok, _Props} = emqtt:connect(SubConnPid1), + {ok, SubConnPid2} = emqtt:start_link([{client_id, Subscriber2}] ++ ExpProp), + {ok, _Props} = emqtt:connect(SubConnPid2), + emqtt:subscribe(SubConnPid1, ShareTopic, QoS), + emqtt:subscribe(SubConnPid1, ShareTopic, QoS), + %% wait for the subscriptions to show up - ?wait(subscribed(Group, Topic, SPid1), 1000), - ?wait(subscribed(Group, Topic, SPid2), 1000), - MkPayload = fun(PacketId) -> iolist_to_binary(["hello-", integer_to_list(PacketId)]) end, - SendF = fun(PacketId) -> emqx_session:publish(P_Pid, PacketId, emqx_message:make(Publisher, QoS, Topic, MkPayload(PacketId))) end, + ct:sleep(200), + MkPayload = fun(PacketId) -> + iolist_to_binary(["hello-", integer_to_list(PacketId)]) + end, + SendF = fun(PacketId) -> + M = emqx_message:make(Publisher, QoS, Topic, MkPayload(PacketId)), + emqx:publish(M#message{id = PacketId}) + end, SendF(1), - Ref = make_ref(), - CasePid = self(), - Received = - fun(PacketId, ConnPid) -> - Payload = MkPayload(PacketId), - case emqx_mock_client:get_last_message(ConnPid) of - [{publish, _, #message{payload = Payload}}] -> - CasePid ! {Ref, PacketId, ConnPid}, - true; - _Other -> - false - end - end, - ?wait(Received(1, SubConnPid1) orelse Received(1, SubConnPid2), 1000), + timer:sleep(200), %% This is the connection which was picked by broker to dispatch (sticky) for 1st message - ConnPid = receive {Ref, 1, Pid} -> Pid after 1000 -> error(timeout) end, + + ?assertMatch([#{packet_id := 1}], recv_msgs(1)), %% Now kill the connection, expect all following messages to be delivered to the other subscriber. - emqx_mock_client:stop(ConnPid), + %emqx_mock_client:stop(ConnPid), %% sleep then make synced calls to session processes to ensure that %% the connection pid's 'EXIT' message is propagated to the session process %% also to be sure sessions are still alive - timer:sleep(2), - _ = emqx_session:info(SPid1), - _ = emqx_session:info(SPid2), - %% Now we know what is the other still alive connection - [TheOtherConnPid] = [SubConnPid1, SubConnPid2] -- [ConnPid], - %% Send some more messages - PacketIdList = lists:seq(2, 10), - lists:foreach(fun(Id) -> - SendF(Id), - ?wait(Received(Id, TheOtherConnPid), 1000) - end, PacketIdList), - %% Now close the 2nd (last connection) - emqx_mock_client:stop(TheOtherConnPid), - timer:sleep(2), - %% both sessions should have conn_pid = undefined - ?assertEqual({conn_pid, undefined}, lists:keyfind(conn_pid, 1, emqx_session:info(SPid1))), - ?assertEqual({conn_pid, undefined}, lists:keyfind(conn_pid, 1, emqx_session:info(SPid2))), - %% send more messages, but all should be queued in session state - lists:foreach(fun(Id) -> SendF(Id) end, PacketIdList), - {_, L1} = lists:keyfind(mqueue_len, 1, emqx_session:info(SPid1)), - {_, L2} = lists:keyfind(mqueue_len, 1, emqx_session:info(SPid2)), - ?assertEqual(length(PacketIdList), L1 + L2), - %% clean up - emqx_mock_client:close_session(PubConnPid), - emqx_sm:close_session(SPid1), - emqx_sm:close_session(SPid2), + % timer:sleep(2), + % _ = emqx_session:info(SPid1), + % _ = emqx_session:info(SPid2), + % %% Now we know what is the other still alive connection + % [TheOtherConnPid] = [SubConnPid1, SubConnPid2] -- [ConnPid], + % %% Send some more messages + % PacketIdList = lists:seq(2, 10), + % lists:foreach(fun(Id) -> + % SendF(Id), + % ?wait(Received(Id, TheOtherConnPid), 1000) + % end, PacketIdList), + % %% Now close the 2nd (last connection) + % emqx_mock_client:stop(TheOtherConnPid), + % timer:sleep(2), + % %% both sessions should have conn_pid = undefined + % ?assertEqual({conn_pid, undefined}, lists:keyfind(conn_pid, 1, emqx_session:info(SPid1))), + % ?assertEqual({conn_pid, undefined}, lists:keyfind(conn_pid, 1, emqx_session:info(SPid2))), + % %% send more messages, but all should be queued in session state + % lists:foreach(fun(Id) -> SendF(Id) end, PacketIdList), + % {_, L1} = lists:keyfind(mqueue_len, 1, emqx_session:info(SPid1)), + % {_, L2} = lists:keyfind(mqueue_len, 1, emqx_session:info(SPid2)), + % ?assertEqual(length(PacketIdList), L1 + L2), + % %% clean up + % emqx_mock_client:close_session(PubConnPid), + % emqx_sm:close_session(SPid1), + % emqx_sm:close_session(SPid2), ok. t_random(_) -> @@ -164,31 +150,24 @@ t_not_so_sticky(_) -> ok = ensure_config(sticky), ClientId1 = <<"ClientId1">>, ClientId2 = <<"ClientId2">>, - {ok, ConnPid1} = emqx_mock_client:start_link(ClientId1), - {ok, ConnPid2} = emqx_mock_client:start_link(ClientId2), - {ok, SPid1} = emqx_mock_client:open_session(ConnPid1, ClientId1, internal), - {ok, SPid2} = emqx_mock_client:open_session(ConnPid2, ClientId2, internal), - Message1 = emqx_message:make(ClientId1, 0, <<"foo/bar">>, <<"hello1">>), - Message2 = emqx_message:make(ClientId1, 0, <<"foo/bar">>, <<"hello2">>), - emqx_session:subscribe(SPid1, [{<<"foo/bar">>, #{qos => 0, share => <<"group1">>}}]), - %% wait for the subscription to show up - ?wait(subscribed(<<"group1">>, <<"foo/bar">>, SPid1), 1000), - emqx_session:publish(SPid1, 1, Message1), - ?wait(case emqx_mock_client:get_last_message(ConnPid1) of - [{publish, _, #message{payload = <<"hello1">>}}] -> true; - Other -> Other - end, 1000), - emqx_mock_client:close_session(ConnPid1), - ?wait(not subscribed(<<"group1">>, <<"foo/bar">>, SPid1), 1000), - emqx_session:subscribe(SPid2, [{<<"foo/#">>, #{qos => 0, share => <<"group1">>}}]), - ?wait(subscribed(<<"group1">>, <<"foo/#">>, SPid2), 1000), - emqx_session:publish(SPid2, 2, Message2), - ?wait(case emqx_mock_client:get_last_message(ConnPid2) of - [{publish, _, #message{payload = <<"hello2">>}}] -> true; - Other -> Other - end, 1000), - emqx_mock_client:close_session(ConnPid2), - ?wait(not subscribed(<<"group1">>, <<"foo/#">>, SPid2), 1000), + {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), + + emqx_client:subscribe(C1, {<<"$share/group1/foo/bar">>, 0}), + timer:sleep(50), + emqx_client:publish(C2, <<"foo/bar">>, <<"hello1">>), + ?assertMatch([#{payload := <<"hello1">>}], recv_msgs(1)), + + emqx_client:unsubscribe(C1, <<"$share/group1/foo/bar">>), + timer:sleep(50), + emqx_client:subscribe(C1, {<<"$share/group1/foo/#">>, 0}), + timer:sleep(50), + emqx_client:publish(C2, <<"foo/bar">>, <<"hello2">>), + ?assertMatch([#{payload := <<"hello2">>}], recv_msgs(1)), + emqx_client:disconnect(C1), + emqx_client:disconnect(C2), ok. test_two_messages(Strategy) -> @@ -199,18 +178,17 @@ test_two_messages(Strategy, WithAck) -> Topic = <<"foo/bar">>, ClientId1 = <<"ClientId1">>, ClientId2 = <<"ClientId2">>, - {ok, ConnPid1} = emqx_mock_client:start_link(ClientId1), - {ok, ConnPid2} = emqx_mock_client:start_link(ClientId2), - {ok, SPid1} = emqx_mock_client:open_session(ConnPid1, ClientId1, internal), - {ok, SPid2} = emqx_mock_client:open_session(ConnPid2, ClientId2, internal), + {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), + Message1 = emqx_message:make(ClientId1, 0, Topic, <<"hello1">>), Message2 = emqx_message:make(ClientId1, 0, Topic, <<"hello2">>), - emqx_session:subscribe(SPid1, [{Topic, #{qos => 0, share => <<"group1">>}}]), - emqx_session:subscribe(SPid2, [{Topic, #{qos => 0, share => <<"group1">>}}]), - %% wait for the subscription to show up - ?wait(subscribed(<<"group1">>, Topic, SPid1) andalso - subscribed(<<"group1">>, Topic, SPid2), 1000), - emqx_broker:publish(Message1), + emqx_client:subscribe(ConnPid1, {<<"$share/group1/foo/bar">>, 0}), + emqx_client:subscribe(ConnPid2, {<<"$share/group1/foo/bar">>, 0}), + ct:sleep(100), + emqx:publish(Message1), Me = self(), WaitF = fun(ExpectedPayload) -> case last_message(ExpectedPayload, [ConnPid1, ConnPid2]) of @@ -221,10 +199,10 @@ test_two_messages(Strategy, WithAck) -> Other end end, - ?wait(WaitF(<<"hello1">>), 2000), + WaitF(<<"hello1">>), UsedSubPid1 = receive {subscriber, P1} -> P1 end, emqx_broker:publish(Message2), - ?wait(WaitF(<<"hello2">>), 2000), + WaitF(<<"hello2">>), UsedSubPid2 = receive {subscriber, P2} -> P2 end, case Strategy of sticky -> ?assert(UsedSubPid1 =:= UsedSubPid2); @@ -232,20 +210,22 @@ test_two_messages(Strategy, WithAck) -> hash -> ?assert(UsedSubPid1 =:= UsedSubPid2); _ -> ok end, - emqx_mock_client:close_session(ConnPid1), - emqx_mock_client:close_session(ConnPid2), + emqx_client:stop(ConnPid1), + emqx_client:stop(ConnPid2), ok. -last_message(_ExpectedPayload, []) -> <<"not yet?">>; -last_message(ExpectedPayload, [Pid | Pids]) -> - case emqx_mock_client:get_last_message(Pid) of - [{publish, _, #message{payload = ExpectedPayload}}] -> {true, Pid}; - _Other -> last_message(ExpectedPayload, Pids) +last_message(ExpectedPayload, Pids) -> + receive + {publish, #{client_pid := Pid, payload := ExpectedPayload}} -> + ct:pal("~p ====== ~p", [Pids, Pid]), + {true, Pid} + after 100 -> + <<"not yet?">> end. -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- %% help functions -%%------------------------------------------------------------------------------ +%%-------------------------------------------------------------------- ensure_config(Strategy) -> ensure_config(Strategy, _AckEnabled = true). @@ -257,3 +237,17 @@ ensure_config(Strategy, AckEnabled) -> subscribed(Group, Topic, Pid) -> lists:member(Pid, emqx_shared_sub:subscribers(Group, Topic)). + +recv_msgs(Count) -> + recv_msgs(Count, []). + +recv_msgs(0, Msgs) -> + Msgs; +recv_msgs(Count, Msgs) -> + receive + {publish, Msg} -> + recv_msgs(Count-1, [Msg|Msgs]); + _Other -> recv_msgs(Count, Msgs) %%TODO:: remove the branch? + after 100 -> + Msgs + end. diff --git a/test/emqx_sm_SUITE.erl b/test/emqx_sm_SUITE.erl deleted file mode 100644 index 0c0a398b3..000000000 --- a/test/emqx_sm_SUITE.erl +++ /dev/null @@ -1,113 +0,0 @@ -%% 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_sm_SUITE). - --include("emqx.hrl"). --include_lib("eunit/include/eunit.hrl"). --include_lib("common_test/include/ct.hrl"). - --compile(export_all). --compile(nowarn_export_all). - --define(ATTRS, #{clean_start => true, - client_id => <<"client">>, - zone => internal, - username => <<"emqx">>, - expiry_interval => 0, - max_inflight => 0, - topic_alias_maximum => 0, - will_msg => undefined}). - -all() -> [{group, registry}, {group, ets}]. - -groups() -> - Cases = - [ t_resume_session, - t_discard_session, - t_register_unregister_session, - t_get_set_session_attrs, - t_get_set_session_stats, - t_lookup_session_pids], - [ {registry, [non_parallel_tests], Cases}, - {ets, [non_parallel_tests], Cases}]. - -init_per_suite(Config) -> - Config. - -end_per_suite(_Config) -> - ok. - -init_per_group(registry, Config) -> - emqx_ct_helpers:start_apps([], fun enable_session_registry/1), - Config; -init_per_group(ets, Config) -> - emqx_ct_helpers:start_apps([], fun disable_session_registry/1), - Config. - -end_per_group(_, _Config) -> - emqx_ct_helpers:stop_apps([]). - -init_per_testcase(_All, Config) -> - {ok, SPid} = emqx_sm:open_session(?ATTRS#{conn_pid => self()}), - [{session_pid, SPid}|Config]. - -end_per_testcase(_All, Config) -> - emqx_sm:close_session(?config(session_pid, Config)), - receive - {shutdown, normal} -> ok - after 500 -> ct:fail({timeout, wait_session_shutdown}) - end. - -enable_session_registry(_) -> - application:set_env(emqx, enable_session_registry, true), - ok. - -disable_session_registry(_) -> - application:set_env(emqx, enable_session_registry, false), - ok. - -t_resume_session(Config) -> - ?assertEqual({ok, ?config(session_pid, Config)}, emqx_sm:resume_session(<<"client">>, ?ATTRS#{conn_pid => self()})). - -t_discard_session(_) -> - ?assertEqual(ok, emqx_sm:discard_session(<<"client1">>)). - -t_register_unregister_session(_) -> - Pid = self(), - ?assertEqual(ok, emqx_sm:register_session(<<"client">>)), - ?assertEqual(ok, emqx_sm:register_session(<<"client">>, Pid)), - ?assertEqual(ok, emqx_sm:unregister_session(<<"client">>)), - ?assertEqual(ok, emqx_sm:unregister_session(<<"client">>), Pid). - -t_get_set_session_attrs(Config) -> - SPid = ?config(session_pid, Config), - ClientPid0 = spawn(fun() -> receive _ -> ok end end), - ?assertEqual(true, emqx_sm:set_session_attrs(<<"client">>, [?ATTRS#{conn_pid => ClientPid0}])), - ?assertEqual(true, emqx_sm:set_session_attrs(<<"client">>, SPid, [?ATTRS#{conn_pid => ClientPid0}])), - [SAttr0] = emqx_sm:get_session_attrs(<<"client">>, SPid), - ?assertEqual(ClientPid0, maps:get(conn_pid, SAttr0)), - ?assertEqual(true, emqx_sm:set_session_attrs(<<"client">>, SPid, [?ATTRS#{conn_pid => self()}])), - [SAttr1] = emqx_sm:get_session_attrs(<<"client">>, SPid), - ?assertEqual(self(), maps:get(conn_pid, SAttr1)). - -t_get_set_session_stats(Config) -> - SPid = ?config(session_pid, Config), - ?assertEqual(true, emqx_sm:set_session_stats(<<"client">>, [{inflight, 10}])), - ?assertEqual(true, emqx_sm:set_session_stats(<<"client">>, SPid, [{inflight, 10}])), - ?assertEqual([{inflight, 10}], emqx_sm:get_session_stats(<<"client">>, SPid)). - -t_lookup_session_pids(Config) -> - SPid = ?config(session_pid, Config), - ?assertEqual([SPid], emqx_sm:lookup_session_pids(<<"client">>)). diff --git a/test/emqx_stats_tests.erl b/test/emqx_stats_SUITE.erl similarity index 74% rename from test/emqx_stats_tests.erl rename to test/emqx_stats_SUITE.erl index c88f3c831..b2c0b3524 100644 --- a/test/emqx_stats_tests.erl +++ b/test/emqx_stats_SUITE.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,37 +12,43 @@ %% 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_stats_tests). +-module(emqx_stats_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). -include_lib("eunit/include/eunit.hrl"). -get_state_test() -> +all() -> emqx_ct:all(?MODULE). + +t_get_state(_) -> with_proc(fun() -> SetConnsCount = emqx_stats:statsfun('connections.count'), SetConnsCount(1), - 1 = emqx_stats:getstat('connections.count'), + ?assertEqual(1, emqx_stats:getstat('connections.count')), emqx_stats:setstat('connections.count', 2), - 2 = emqx_stats:getstat('connections.count'), + ?assertEqual(2, emqx_stats:getstat('connections.count')), emqx_stats:setstat('connections.count', 'connections.max', 3), timer:sleep(100), - 3 = emqx_stats:getstat('connections.count'), - 3 = emqx_stats:getstat('connections.max'), + ?assertEqual(3, emqx_stats:getstat('connections.count')), + ?assertEqual(3, emqx_stats:getstat('connections.max')), emqx_stats:setstat('connections.count', 'connections.max', 2), timer:sleep(100), - 2 = emqx_stats:getstat('connections.count'), - 3 = emqx_stats:getstat('connections.max'), + ?assertEqual(2, emqx_stats:getstat('connections.count')), + ?assertEqual(3, emqx_stats:getstat('connections.max')), SetConns = emqx_stats:statsfun('connections.count', 'connections.max'), SetConns(4), timer:sleep(100), - 4 = emqx_stats:getstat('connections.count'), - 4 = emqx_stats:getstat('connections.max'), + ?assertEqual(4, emqx_stats:getstat('connections.count')), + ?assertEqual(4, emqx_stats:getstat('connections.max')), Conns = emqx_stats:getstats(), - 4 = proplists:get_value('connections.count', Conns), - 4 = proplists:get_value('connections.max', Conns) + ?assertEqual(4, proplists:get_value('connections.count', Conns)), + ?assertEqual(4, proplists:get_value('connections.max', Conns)) end). -update_interval_test() -> +t_update_interval(_) -> TickMs = 200, with_proc(fun() -> SleepMs = TickMs * 2 + TickMs div 2, %% sleep for 2.5 ticks @@ -52,7 +59,7 @@ update_interval_test() -> ?assertEqual(1, emqx_stats:getstat('connections.count')) end, TickMs). -helper_test_() -> +t_helper(_) -> TickMs = 200, TestF = fun(CbModule, CbFun) -> @@ -98,4 +105,3 @@ with_stop(F) -> after ok = emqx_stats:stop() end. - diff --git a/test/emqx_sys_mon_SUITE.erl b/test/emqx_sys_mon_SUITE.erl index 86e7bd6a8..ef54ea630 100644 --- a/test/emqx_sys_mon_SUITE.erl +++ b/test/emqx_sys_mon_SUITE.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,27 +12,34 @@ %% 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_mon_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"). +-define(SYSMON, emqx_sys_mon). --include("emqx_mqtt.hrl"). - --define(SYSMONPID, emqx_sys_mon). --define(INPUTINFO, [{self(), long_gc, concat_str("long_gc warning: pid = ~p, info: ~p", self(), "hello"), "hello"}, - {self(), long_schedule, concat_str("long_schedule warning: pid = ~p, info: ~p", self(), "hello"), "hello"}, - {self(), busy_port, concat_str("busy_port warning: suspid = ~p, port = ~p", self(), list_to_port("#Port<0.4>")), list_to_port("#Port<0.4>")}, - {self(), busy_dist_port, concat_str("busy_dist_port warning: suspid = ~p, port = ~p", self(), list_to_port("#Port<0.4>")),list_to_port("#Port<0.4>")}, - {list_to_port("#Port<0.4>"), long_schedule, concat_str("long_schedule warning: port = ~p, info: ~p", list_to_port("#Port<0.4>"), "hello"), "hello"} +-define(INPUTINFO, [{self(), long_gc, + concat_str("long_gc warning: pid = ~p, info: ~p", self(), "hello"), "hello"}, + {self(), long_schedule, + concat_str("long_schedule warning: pid = ~p, info: ~p", self(), "hello"), "hello"}, + {self(), busy_port, + concat_str("busy_port warning: suspid = ~p, port = ~p", + self(), list_to_port("#Port<0.4>")), list_to_port("#Port<0.4>")}, + {self(), busy_dist_port, + concat_str("busy_dist_port warning: suspid = ~p, port = ~p", + self(), list_to_port("#Port<0.4>")),list_to_port("#Port<0.4>")}, + {list_to_port("#Port<0.4>"), long_schedule, + concat_str("long_schedule warning: port = ~p, info: ~p", + list_to_port("#Port<0.4>"), "hello"), "hello"} ]). -all() -> [t_sys_mon]. +all() -> emqx_ct:all(?MODULE). init_per_suite(Config) -> emqx_ct_helpers:start_apps([]), @@ -41,16 +49,17 @@ end_per_suite(_Config) -> emqx_ct_helpers:stop_apps([]). t_sys_mon(_Config) -> - lists:foreach(fun({PidOrPort, SysMonName,ValidateInfo, InfoOrPort}) -> - validate_sys_mon_info(PidOrPort, SysMonName,ValidateInfo, InfoOrPort) - end, ?INPUTINFO). + lists:foreach( + fun({PidOrPort, SysMonName,ValidateInfo, InfoOrPort}) -> + validate_sys_mon_info(PidOrPort, SysMonName,ValidateInfo, InfoOrPort) + 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), timer:sleep(100), - ?SYSMONPID ! {monitor, PidOrPort, SysMonName, InfoOrPort}, + ?SYSMON ! {monitor, PidOrPort, SysMonName, InfoOrPort}, receive {publish, #{payload := Info}} -> ?assertEqual(ValidateInfo, binary_to_list(Info)), diff --git a/test/emqx_tables_SUITE.erl b/test/emqx_tables_SUITE.erl index c028d3681..106001f62 100644 --- a/test/emqx_tables_SUITE.erl +++ b/test/emqx_tables_SUITE.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,18 +12,34 @@ %% 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_tables_SUITE). -compile(export_all). -compile(nowarn_export_all). -all() -> [t_new]. +-include_lib("eunit/include/eunit.hrl"). + +-define(TAB, ?MODULE). + +all() -> emqx_ct:all(?MODULE). t_new(_) -> - ok = emqx_tables:new(test_table, [{read_concurrency, true}]), - ets:insert(test_table, {key, 100}), - ok = emqx_tables:new(test_table, [{read_concurrency, true}]), - 100 = ets:lookup_element(test_table, key, 2), - ok = emqx_tables:delete(test_table), - ok = emqx_tables:delete(test_table). + ok = emqx_tables:new(?TAB), + ok = emqx_tables:new(?TAB, [{read_concurrency, true}]), + ?assertEqual(?TAB, ets:info(?TAB, name)). + +t_lookup_value(_) -> + ok = emqx_tables:new(?TAB, []), + true = ets:insert(?TAB, {key, val}), + ?assertEqual(val, emqx_tables:lookup_value(?TAB, key)), + ?assertEqual(undefined, emqx_tables:lookup_value(?TAB, badkey)). + +t_delete(_) -> + ok = emqx_tables:new(?TAB, []), + ?assertEqual(?TAB, ets:info(?TAB, name)), + ok = emqx_tables:delete(?TAB), + ok = emqx_tables:delete(?TAB), + ?assertEqual(undefined, ets:info(?TAB, name)). + diff --git a/test/emqx_time_SUITE.erl b/test/emqx_time_SUITE.erl index 4c8886f9c..e190c7f78 100644 --- a/test/emqx_time_SUITE.erl +++ b/test/emqx_time_SUITE.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,17 +12,23 @@ %% 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_time_SUITE). --include_lib("eunit/include/eunit.hrl"). - -compile(export_all). -compile(nowarn_export_all). -all() -> [t_time_now_to]. +-include_lib("eunit/include/eunit.hrl"). + +all() -> emqx_ct:all(?MODULE). + +t_seed(_) -> + ?assert(is_tuple(emqx_time:seed())). + +t_now_secs(_) -> + ?assert(emqx_time:now_secs() =< emqx_time:now_secs(os:timestamp())). + +t_now_ms(_) -> + ?assert(emqx_time:now_ms() =< emqx_time:now_ms(os:timestamp())). -t_time_now_to(_) -> - emqx_time:seed(), - emqx_time:now_secs(), - emqx_time:now_ms(). diff --git a/test/emqx_topic_SUITE.erl b/test/emqx_topic_SUITE.erl index 0a51598ee..b0776aa69 100644 --- a/test/emqx_topic_SUITE.erl +++ b/test/emqx_topic_SUITE.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,47 +12,33 @@ %% 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_topic_SUITE). --include_lib("eunit/include/eunit.hrl"). - -%% CT -compile(export_all). -compile(nowarn_export_all). +-include_lib("eunit/include/eunit.hrl"). +-include_lib("emqx_ct_helpers/include/emqx_ct.hrl"). + -import(emqx_topic, - [wildcard/1, - match/2, - validate/1, - triples/1, - join/1, - words/1, - systop/1, - feed_var/3, - parse/1 + [ wildcard/1 + , match/2 + , validate/1 + , triples/1 + , prepend/2 + , join/1 + , words/1 + , systop/1 + , feed_var/3 + , parse/1 + , parse/2 ]). --define(N, 10000). +-define(N, 100000). -all() -> - [t_wildcard, - t_match, t_match2, t_match3, - t_validate, - t_triples, - t_join, - t_levels, - t_tokens, - t_words, - t_systop, - t_feed_var, - t_sys_match, - 't_#_match', - t_sigle_level_validate, - t_sigle_level_match, - t_match_perf, - t_triples_perf, - t_parse]. +all() -> emqx_ct:all(?MODULE). t_wildcard(_) -> true = wildcard(<<"a/b/#">>), @@ -59,7 +46,7 @@ t_wildcard(_) -> false = wildcard(<<"">>), false = wildcard(<<"a/b/c">>). -t_match(_) -> +t_match1(_) -> true = match(<<"a/b/c">>, <<"a/b/+">>), true = match(<<"a/b/c">>, <<"a/#">>), true = match(<<"abcd/ef/g">>, <<"#">>), @@ -130,74 +117,74 @@ t_match_perf(_) -> Name = <<"/abkc/19383/192939/akakdkkdkak/xxxyyuya/akakak">>, Filter = <<"/abkc/19383/+/akakdkkdkak/#">>, true = match(Name, Filter), - {Time, _} = timer:tc(fun() -> - [match(Name, Filter) || _I <- lists:seq(1, ?N)] - end), - io:format("Time for match: ~p(micro)", [Time/?N]). + ok = bench('match/2', fun emqx_topic:match/2, [Name, Filter]). t_validate(_) -> - true = validate({name, <<"abc/de/f">>}), - true = validate({filter, <<"abc/+/f">>}), - true = validate({filter, <<"abc/#">>}), - true = validate({filter, <<"x">>}), - true = validate({name, <<"x//y">>}), - true = validate({filter, <<"sport/tennis/#">>}), - catch validate({name, <<>>}), - catch validate({name, long_topic()}), - catch validate({name, <<"abc/#">>}), - catch validate({filter, <<"abc/#/1">>}), - catch validate({filter, <<"abc/#xzy/+">>}), - catch validate({filter, <<"abc/xzy/+9827">>}), - catch validate({filter, <<"sport/tennis#">>}), - catch validate({filter, <<"sport/tennis/#/ranking">>}), - ok. + true = validate(<<"a/+/#">>), + true = validate(<<"a/b/c/d">>), + true = validate({name, <<"abc/de/f">>}), + true = validate({filter, <<"abc/+/f">>}), + true = validate({filter, <<"abc/#">>}), + true = validate({filter, <<"x">>}), + true = validate({name, <<"x//y">>}), + true = validate({filter, <<"sport/tennis/#">>}), + ok = ?catch_error(empty_topic, validate({name, <<>>})), + ok = ?catch_error(topic_name_error, validate({name, <<"abc/#">>})), + ok = ?catch_error(topic_too_long, validate({name, long_topic()})), + ok = ?catch_error('topic_invalid_#', validate({filter, <<"abc/#/1">>})), + ok = ?catch_error(topic_invalid_char, validate({filter, <<"abc/#xzy/+">>})), + ok = ?catch_error(topic_invalid_char, validate({filter, <<"abc/xzy/+9827">>})), + ok = ?catch_error(topic_invalid_char, validate({filter, <<"sport/tennis#">>})), + ok = ?catch_error('topic_invalid_#', validate({filter, <<"sport/tennis/#/ranking">>})). t_sigle_level_validate(_) -> - true = validate({filter, <<"+">>}), - true = validate({filter, <<"+/tennis/#">>}), - true = validate({filter, <<"sport/+/player1">>}), - catch validate({filter, <<"sport+">>}), - ok. + true = validate({filter, <<"+">>}), + true = validate({filter, <<"+/tennis/#">>}), + true = validate({filter, <<"sport/+/player1">>}), + ok = ?catch_error(topic_invalid_char, validate({filter, <<"sport+">>})). t_triples(_) -> Triples = [{root,<<"a">>,<<"a">>}, {<<"a">>,<<"b">>,<<"a/b">>}, {<<"a/b">>,<<"c">>,<<"a/b/c">>}], - Triples = triples(<<"a/b/c">>). + ?assertEqual(Triples, triples(<<"a/b/c">>)). t_triples_perf(_) -> Topic = <<"/abkc/19383/192939/akakdkkdkak/xxxyyuya/akakak">>, - {Time, _} = timer:tc(fun() -> - [triples(Topic) || _I <- lists:seq(1, ?N)] - end), - io:format("Time for triples: ~p(micro)", [Time/?N]). + ok = bench('triples/1', fun emqx_topic:triples/1, [Topic]). + +t_prepend(_) -> + ?assertEqual(<<"a/b/c">>, prepend(root, <<"a/b/c">>)), + ?assertEqual(<<"ab">>, prepend(undefined, <<"ab">>)), + ?assertEqual(<<"a/b">>, prepend(<<>>, <<"a/b">>)), + ?assertEqual(<<"x/a/b">>, prepend("x/", <<"a/b">>)), + ?assertEqual(<<"x/y/a/b">>, prepend(<<"x/y">>, <<"a/b">>)), + ?assertEqual(<<"+/a/b">>, prepend('+', <<"a/b">>)). t_levels(_) -> + ?assertEqual(3, emqx_topic:levels(<<"a/+/#">>)), ?assertEqual(4, emqx_topic:levels(<<"a/b/c/d">>)). t_tokens(_) -> - ?assertEqual([<<"a">>, <<"b">>, <<"+">>, <<"#">>], emqx_topic:tokens(<<"a/b/+/#">>)). + ?assertEqual([<<"a">>, <<"b">>, <<"+">>, <<"#">>], + emqx_topic:tokens(<<"a/b/+/#">>)). t_words(_) -> - ['', <<"a">>, '+', '#'] = words(<<"/a/+/#">>), - ['', <<"abkc">>, <<"19383">>, '+', <<"akakdkkdkak">>, '#'] = words(<<"/abkc/19383/+/akakdkkdkak/#">>), - {Time, _} = timer:tc(fun() -> - [words(<<"/abkc/19383/+/akakdkkdkak/#">>) || _I <- lists:seq(1, ?N)] - end), - io:format("Time for words: ~p(micro)", [Time/?N]), - {Time2, _} = timer:tc(fun() -> - [binary:split(<<"/abkc/19383/+/akakdkkdkak/#">>, <<"/">>, [global]) || _I <- lists:seq(1, ?N)] - end), - io:format("Time for binary:split: ~p(micro)", [Time2/?N]). + Topic = <<"/abkc/19383/+/akakdkkdkak/#">>, + ?assertEqual(['', <<"a">>, '+', '#'], words(<<"/a/+/#">>)), + ?assertEqual(['', <<"abkc">>, <<"19383">>, '+', <<"akakdkkdkak">>, '#'], words(Topic)), + ok = bench('words/1', fun emqx_topic:words/1, [Topic]), + BSplit = fun(Bin) -> binary:split(Bin, <<"/">>, [global]) end, + ok = bench('binary:split/3', BSplit, [Topic]). t_join(_) -> - <<>> = join([]), - <<"x">> = join([<<"x">>]), - <<"#">> = join(['#']), - <<"+//#">> = join(['+', '', '#']), - <<"x/y/z/+">> = join([<<"x">>, <<"y">>, <<"z">>, '+']), - <<"/ab/cd/ef/">> = join(words(<<"/ab/cd/ef/">>)), - <<"ab/+/#">> = join(words(<<"ab/+/#">>)). + ?assertEqual(<<>>, join([])), + ?assertEqual(<<"x">>, join([<<"x">>])), + ?assertEqual(<<"#">>, join(['#'])), + ?assertEqual(<<"+//#">>, join(['+', '', '#'])), + ?assertEqual(<<"x/y/z/+">>, join([<<"x">>, <<"y">>, <<"z">>, '+'])), + ?assertEqual(<<"/ab/cd/ef/">>, join(words(<<"/ab/cd/ef/">>))), + ?assertEqual(<<"ab/+/#">>, join(words(<<"ab/+/#">>))). t_systop(_) -> SysTop1 = iolist_to_binary(["$SYS/brokers/", atom_to_list(node()), "/xyz"]), @@ -217,11 +204,29 @@ long_topic() -> iolist_to_binary([[integer_to_list(I), "/"] || I <- lists:seq(0, 10000)]). t_parse(_) -> + ok = ?catch_error({invalid_topic_filter, <<"$queue/t">>}, + parse(<<"$queue/t">>, #{share => <<"g">>})), + ok = ?catch_error({invalid_topic_filter, <<"$share/g/t">>}, + parse(<<"$share/g/t">>, #{share => <<"g">>})), + ok = ?catch_error({invalid_topic_filter, <<"$share/t">>}, + parse(<<"$share/t">>)), + ok = ?catch_error({invalid_topic_filter, <<"$share/+/t">>}, + parse(<<"$share/+/t">>)), ?assertEqual({<<"a/b/+/#">>, #{}}, parse(<<"a/b/+/#">>)), - ?assertEqual({<<"topic">>, #{ share => <<"$queue">> }}, parse(<<"$queue/topic">>)), - ?assertEqual({<<"topic">>, #{ share => <<"group">>}}, parse(<<"$share/group/topic">>)), + ?assertEqual({<<"a/b/+/#">>, #{qos => 1}}, parse({<<"a/b/+/#">>, #{qos => 1}})), + ?assertEqual({<<"topic">>, #{share => <<"$queue">>}}, parse(<<"$queue/topic">>)), + ?assertEqual({<<"topic">>, #{share => <<"group">>}}, parse(<<"$share/group/topic">>)), + %% The '$local' and '$fastlane' topics have been deprecated. ?assertEqual({<<"$local/topic">>, #{}}, parse(<<"$local/topic">>)), ?assertEqual({<<"$local/$queue/topic">>, #{}}, parse(<<"$local/$queue/topic">>)), ?assertEqual({<<"$local/$share/group/a/b/c">>, #{}}, parse(<<"$local/$share/group/a/b/c">>)), ?assertEqual({<<"$fastlane/topic">>, #{}}, parse(<<"$fastlane/topic">>)). +bench(Case, Fun, Args) -> + {Time, ok} = timer:tc(fun lists:foreach/2, + [fun(_) -> apply(Fun, Args) end, + lists:seq(1, ?N) + ]), + ct:pal("Time consumed by ~s: ~.3f(us)~nCall ~s per second: ~w", + [Case, Time/?N, Case, (?N * 1000000) div Time]). + diff --git a/test/emqx_tracer_SUITE.erl b/test/emqx_tracer_SUITE.erl index 8d516e94d..87f6d73f2 100644 --- a/test/emqx_tracer_SUITE.erl +++ b/test/emqx_tracer_SUITE.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_tracer_SUITE). @@ -21,7 +23,7 @@ -include_lib("common_test/include/ct.hrl"). -all() -> [start_traces]. +all() -> [t_start_traces]. init_per_suite(Config) -> emqx_ct_helpers:start_apps([]), @@ -30,7 +32,7 @@ init_per_suite(Config) -> end_per_suite(_Config) -> emqx_ct_helpers:stop_apps([]). -start_traces(_Config) -> +t_start_traces(_Config) -> {ok, T} = emqx_client:start_link([{host, "localhost"}, {client_id, <<"client">>}, {username, <<"testuser">>}, @@ -43,7 +45,7 @@ start_traces(_Config) -> emqx_logger:set_log_level(debug), ok = emqx_tracer:start_trace({client_id, <<"client">>}, debug, "tmp/client.log"), ok = emqx_tracer:start_trace({client_id, <<"client2">>}, all, "tmp/client2.log"), - {error, invalid_log_level} = emqx_tracer:start_trace({client_id, <<"client3">>}, bad_level, "tmp/client3.log"), + {error, {invalid_log_level, bad_level}} = emqx_tracer:start_trace({client_id, <<"client3">>}, bad_level, "tmp/client3.log"), ok = emqx_tracer:start_trace({topic, <<"a/#">>}, all, "tmp/topic_trace.log"), ct:sleep(100), @@ -53,9 +55,9 @@ start_traces(_Config) -> ?assert(filelib:is_regular("tmp/topic_trace.log")), %% Get current traces - ?assertEqual([{{client_id,<<"client">>},{debug,"tmp/client.log"}}, - {{client_id,<<"client2">>},{all,"tmp/client2.log"}}, - {{topic,<<"a/#">>},{all,"tmp/topic_trace.log"}}], emqx_tracer:lookup_traces()), + ?assertEqual([{{client_id,"client"},{debug,"tmp/client.log"}}, + {{client_id,"client2"},{debug,"tmp/client2.log"}}, + {{topic,"a/#"},{debug,"tmp/topic_trace.log"}}], emqx_tracer:lookup_traces()), %% set the overall log level to debug emqx_logger:set_log_level(debug), diff --git a/test/emqx_trie_SUITE.erl b/test/emqx_trie_SUITE.erl index ad92cb313..1414c9027 100644 --- a/test/emqx_trie_SUITE.erl +++ b/test/emqx_trie_SUITE.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,6 +12,7 @@ %% 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_trie_SUITE). @@ -23,8 +25,7 @@ -define(TRIE, emqx_trie). -define(TRIE_TABS, [emqx_trie, emqx_trie_node]). -all() -> - [t_mnesia, t_insert, t_match, t_match2, t_match3, t_empty, t_delete, t_delete2, t_delete3]. +all() -> emqx_ct:all(?MODULE). init_per_suite(Config) -> application:load(emqx), @@ -49,7 +50,8 @@ t_insert(_) -> TN = #trie_node{node_id = <<"sensor">>, edge_count = 3, topic = <<"sensor">>, - flags = undefined}, + flags = undefined + }, Fun = fun() -> ?TRIE:insert(<<"sensor/1/metric/2">>), ?TRIE:insert(<<"sensor/+/#">>), diff --git a/test/emqx_vm_SUITE.erl b/test/emqx_vm_SUITE.erl index 650f792ab..c4532ed26 100644 --- a/test/emqx_vm_SUITE.erl +++ b/test/emqx_vm_SUITE.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,13 +12,14 @@ %% 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_vm_SUITE). -compile(export_all). -compile(nowarn_export_all). --include_lib("common_test/include/ct.hrl"). +-include_lib("eunit/include/eunit.hrl"). -define(SYSTEM_INFO, [allocated_areas, allocator, @@ -92,82 +94,79 @@ min_heap_size]). %fullsweep_after]). +all() -> emqx_ct:all(?MODULE). +t_load(_Config) -> + ?assertMatch([{load1, _}, + {load5, _}, + {load15, _} + ], emqx_vm:loads()). -all() -> - [load, systeminfo, mem_info, process_list, process_info, process_gc, - get_ets_list, get_ets_info, get_ets_object, get_port_types, get_port_info, - scheduler_usage, get_memory, microsecs, schedulers, get_process_group_leader_info, - get_process_limit]. - -load(_Config) -> - Loads = emqx_vm:loads(), - [{load1, _}, {load5, _}, {load15, _}] = Loads. - -systeminfo(_Config) -> +t_systeminfo(_Config) -> Keys = [Key || {Key, _} <- emqx_vm:get_system_info()], ?SYSTEM_INFO = Keys. -mem_info(_Config) -> +t_mem_info(_Config) -> application:ensure_all_started(os_mon), MemInfo = emqx_vm:mem_info(), [{total_memory, _}, {used_memory, _}]= MemInfo, application:stop(os_mon). -process_list(_Config) -> +t_process_list(_Config) -> Pid = self(), ProcessInfo = emqx_vm:get_process_list(), true = lists:member({pid, Pid}, lists:concat(ProcessInfo)). -process_info(_Config) -> +t_process_info(_Config) -> ProcessInfos = emqx_vm:get_process_info(), ProcessInfo = lists:last(ProcessInfos), Keys = [K || {K, _V}<- ProcessInfo], ?PROCESS_INFO = Keys. -process_gc(_Config) -> +t_process_gc(_Config) -> ProcessGcs = emqx_vm:get_process_gc(), ProcessGc = lists:last(ProcessGcs), Keys = [K || {K, _V}<- ProcessGc], ?PROCESS_GC = Keys. - -get_ets_list(_Config) -> + +t_get_ets_list(_Config) -> ets:new(test, [named_table]), Ets = emqx_vm:get_ets_list(), true = lists:member(test, Ets). -get_ets_info(_Config) -> +t_get_ets_info(_Config) -> ets:new(test, [named_table]), [] = emqx_vm:get_ets_info(test1), EtsInfo = emqx_vm:get_ets_info(test), test = proplists:get_value(name, EtsInfo). -get_ets_object(_Config) -> +t_get_ets_object(_Config) -> ets:new(test, [named_table]), ets:insert(test, {k, v}), [{k, v}] = emqx_vm:get_ets_object(test). -get_port_types(_Config) -> +t_get_port_types(_Config) -> emqx_vm:get_port_types(). -get_port_info(_Config) -> +t_get_port_info(_Config) -> emqx_vm:get_port_info(). -scheduler_usage(_Config) -> +t_scheduler_usage(_Config) -> emqx_vm:scheduler_usage(5000). -get_memory(_Config) -> +t_get_memory(_Config) -> emqx_vm:get_memory(). - -microsecs(_Config) -> + +t_microsecs(_Config) -> emqx_vm:microsecs(). -schedulers(_Config) -> +t_schedulers(_Config) -> emqx_vm:schedulers(). -get_process_group_leader_info(_Config) -> +t_get_process_group_leader_info(_Config) -> emqx_vm:get_process_group_leader_info(self()). -get_process_limit(_Config) -> +t_get_process_limit(_Config) -> emqx_vm:get_process_limit(). + diff --git a/test/emqx_vm_mon_SUITE.erl b/test/emqx_vm_mon_SUITE.erl index 3718e3626..438974ade 100644 --- a/test/emqx_vm_mon_SUITE.erl +++ b/test/emqx_vm_mon_SUITE.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2013-2013-2019 EMQ Enterprise, Inc. (http://emqtt.io) +%% 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. @@ -21,8 +21,6 @@ -include_lib("eunit/include/eunit.hrl"). --include_lib("common_test/include/ct.hrl"). - -define(WAIT(PATTERN, TIMEOUT), receive PATTERN -> @@ -32,7 +30,7 @@ error(timeout) end). -all() -> [t_api]. +all() -> emqx_ct:all(?MODULE). init_per_suite(Config) -> application:ensure_all_started(sasl), @@ -75,3 +73,4 @@ t_api(_) -> after meck:unload(alarm_handler) end. + diff --git a/test/emqx_ws_channel_SUITE.erl b/test/emqx_ws_channel_SUITE.erl index 0ce31b464..f634e633e 100644 --- a/test/emqx_ws_channel_SUITE.erl +++ b/test/emqx_ws_channel_SUITE.erl @@ -19,33 +19,9 @@ -compile(export_all). -compile(nowarn_export_all). --include("emqx_mqtt.hrl"). -include_lib("eunit/include/eunit.hrl"). --include_lib("common_test/include/ct.hrl"). --define(CLIENT, ?CONNECT_PACKET(#mqtt_packet_connect{ - client_id = <<"mqtt_client">>, - username = <<"admin">>, - password = <<"public">>})). - --define(WILL_TOPIC, <<"test/websocket/will">>). - --define(WILL_CLIENT, ?CONNECT_PACKET(#mqtt_packet_connect{ - client_id = <<"mqtt_client">>, - username = <<"admin">>, - password = <<"public">>, - will_flag = true, - will_qos = ?QOS_1, - will_topic = ?WILL_TOPIC, - will_payload = <<"payload">> - })). - -all() -> - [ t_ws_connect_api - , t_ws_auth_failure - , t_ws_other_type_frame - , t_ws_will - ]. +all() -> emqx_ct:all(?MODULE). init_per_suite(Config) -> emqx_ct_helpers:start_apps([]), @@ -54,88 +30,27 @@ init_per_suite(Config) -> end_per_suite(_Config) -> emqx_ct_helpers:stop_apps([]). -t_ws_will(_Config) -> - {ok, ClientPid} = emqx_client:start_link(), - {ok, _} = emqx_client:connect(ClientPid), - {ok, _, [1]} = emqx_client:subscribe(ClientPid, ?WILL_TOPIC, qos1), - WS = rfc6455_client:new("ws://127.0.0.1:8083" ++ "/mqtt", self()), - {ok, _} = rfc6455_client:open(WS), - Packet = raw_send_serialize(?WILL_CLIENT), - ok = rfc6455_client:send_binary(WS, Packet), - {binary, Bin} = rfc6455_client:recv(WS), - Connack = ?CONNACK_PACKET(?CONNACK_ACCEPT), - {ok, Connack, <<>>, _} = raw_recv_pase(Bin), - exit(WS, abnomal), - ?assertEqual(1, length(emqx_client_SUITE:receive_messages(1))), - ok = emqx_client:disconnect(ClientPid), - ok. +t_basic(_) -> + Topic = <<"TopicA">>, + {ok, C} = emqtt:start_link([{host, "127.0.0.1"}, {port, 8083}]), + {ok, _} = emqtt:ws_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). -t_ws_auth_failure(_Config) -> - application:set_env(emqx, allow_anonymous, false), - WS = rfc6455_client:new("ws://127.0.0.1:8083" ++ "/mqtt", self()), - {ok, _} = rfc6455_client:open(WS), - Packet = raw_send_serialize(?CLIENT), - ok = rfc6455_client:send_binary(WS, Packet), - {binary, CONNACK} = rfc6455_client:recv(WS), - {ok, ?CONNACK_PACKET(?CONNACK_AUTH), <<>>, _} = raw_recv_pase(CONNACK), - application:set_env(emqx, allow_anonymous, true), - ok. +recv_msgs(Count) -> + recv_msgs(Count, []). -t_ws_connect_api(_Config) -> - WS = rfc6455_client:new("ws://127.0.0.1:8083" ++ "/mqtt", self()), - {ok, _} = rfc6455_client:open(WS), - ok = rfc6455_client:send_binary(WS, raw_send_serialize(?CLIENT)), - {binary, Bin} = rfc6455_client:recv(WS), - Connack = ?CONNACK_PACKET(?CONNACK_ACCEPT), - {ok, Connack, <<>>, _} = raw_recv_pase(Bin), - Pid = emqx_cm:lookup_conn_pid(<<"mqtt_client">>), - ConnInfo = emqx_ws_channel:info(Pid), - ok = t_info(ConnInfo), - ConnAttrs = emqx_ws_channel:attrs(Pid), - ok = t_attrs(ConnAttrs), - ConnStats = emqx_ws_channel:stats(Pid), - ok = t_stats(ConnStats), - SessionPid = emqx_ws_channel:session(Pid), - true = is_pid(SessionPid), - ok = emqx_ws_channel:kick(Pid), - {close, _} = rfc6455_client:close(WS), - ok. - -t_ws_other_type_frame(_Config) -> - WS = rfc6455_client:new("ws://127.0.0.1:8083" ++ "/mqtt", self()), - {ok, _} = rfc6455_client:open(WS), - ok = rfc6455_client:send_binary(WS, raw_send_serialize(?CLIENT)), - {binary, Bin} = rfc6455_client:recv(WS), - Connack = ?CONNACK_PACKET(?CONNACK_ACCEPT), - {ok, Connack, <<>>, _} = raw_recv_pase(Bin), - rfc6455_client:send(WS, <<"testdata">>), - timer:sleep(1000), - ?assertEqual(undefined, erlang:process_info(WS)), - ok. - -raw_send_serialize(Packet) -> - emqx_frame:serialize(Packet). - -raw_recv_pase(Packet) -> - emqx_frame:parse(Packet). - -t_info(InfoData) -> - ?assertEqual(websocket, maps:get(socktype, InfoData)), - ?assertEqual(running, maps:get(conn_state, InfoData)), - ?assertEqual(<<"mqtt_client">>, maps:get(client_id, InfoData)), - ?assertEqual(<<"admin">>, maps:get(username, InfoData)), - ?assertEqual(<<"MQTT">>, maps:get(proto_name, InfoData)). - -t_attrs(AttrsData) -> - ?assertEqual(<<"mqtt_client">>, maps:get(client_id, AttrsData)), - ?assertEqual(emqx_ws_channel, maps:get(conn_mod, AttrsData)), - ?assertEqual(<<"admin">>, maps:get(username, AttrsData)). - -t_stats(StatsData) -> - ?assertEqual(true, proplists:get_value(recv_oct, StatsData) >= 0), - ?assertEqual(true, proplists:get_value(mailbox_len, StatsData) >= 0), - ?assertEqual(true, proplists:get_value(heap_size, StatsData) >= 0), - ?assertEqual(true, proplists:get_value(reductions, StatsData) >=0), - ?assertEqual(true, proplists:get_value(recv_pkt, StatsData) =:=1), - ?assertEqual(true, proplists:get_value(recv_msg, StatsData) >=0), - ?assertEqual(true, proplists:get_value(send_pkt, StatsData) =:=1). +recv_msgs(0, Msgs) -> + Msgs; +recv_msgs(Count, Msgs) -> + receive + {publish, Msg} -> + recv_msgs(Count-1, [Msg|Msgs]) + after 100 -> + Msgs + end. diff --git a/test/emqx_zone_SUITE.erl b/test/emqx_zone_SUITE.erl index 983e542f3..c9502d0a7 100644 --- a/test/emqx_zone_SUITE.erl +++ b/test/emqx_zone_SUITE.erl @@ -1,4 +1,5 @@ -%% Copyright (c) 2013-2019 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- +%% 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. @@ -11,26 +12,38 @@ %% 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_zone_SUITE). -compile(export_all). -compile(nowarn_export_all). --include("emqx_mqtt.hrl"). -include_lib("eunit/include/eunit.hrl"). -all() -> [t_set_get_env]. +-define(OPTS, [{enable_acl, true}, + {enable_banned, false} + ]). + +all() -> emqx_ct:all(?MODULE). t_set_get_env(_) -> - application:set_env(emqx, zones, [{china, [{language, chinese}]}]), + _ = application:load(emqx), + application:set_env(emqx, zones, [{external, ?OPTS}]), {ok, _} = emqx_zone:start_link(), - chinese = emqx_zone:get_env(china, language), - cn470 = emqx_zone:get_env(china, ism_band, cn470), - undefined = emqx_zone:get_env(undefined, delay), - 500 = emqx_zone:get_env(undefined, delay, 500), - application:set_env(emqx, zones, [{zone1, [{key, val}]}]), - ?assertEqual(undefined, emqx_zone:get_env(zone1, key)), - emqx_zone:force_reload(), - ?assertEqual(val, emqx_zone:get_env(zone1, key)), + ?assert(emqx_zone:get_env(external, enable_acl)), + ?assertNot(emqx_zone:get_env(external, enable_banned)), + ?assertEqual(defval, emqx_zone:get_env(extenal, key, defval)), + ?assertEqual(undefined, emqx_zone:get_env(external, key)), + ?assertEqual(undefined, emqx_zone:get_env(internal, key)), + ?assertEqual(def, emqx_zone:get_env(internal, key, def)), emqx_zone:stop(). + +t_force_reload(_) -> + {ok, _} = emqx_zone:start_link(), + application:set_env(emqx, zones, [{zone, [{key, val}]}]), + ?assertEqual(undefined, emqx_zone:get_env(zone, key)), + ok = emqx_zone:force_reload(), + ?assertEqual(val, emqx_zone:get_env(zone, key)), + emqx_zone:stop(). + diff --git a/test/prop_base62.erl b/test/prop_base62.erl new file mode 100644 index 000000000..a660012db --- /dev/null +++ b/test/prop_base62.erl @@ -0,0 +1,51 @@ +-module(prop_base62). +-include_lib("proper/include/proper.hrl"). + +%%%%%%%%%%%%%%%%%% +%%% Properties %%% +%%%%%%%%%%%%%%%%%% +prop_symmetric() -> + ?FORALL(Data, raw_data(), + begin + Encoded = emqx_base62:encode(Data), + to_binary(Data) =:= emqx_base62:decode(Encoded) + end). + +prop_size() -> + ?FORALL(Data, binary(), + begin + Encoded = emqx_base62:encode(Data), + base62_size(Data, Encoded) + end). + +%%%%%%%%%%%%%%% +%%% Helpers %%% +%%%%%%%%%%%%%%% +to_binary(Data) when is_list(Data) -> + unicode:characters_to_binary(Data); +to_binary(Data) when is_integer(Data) -> + integer_to_binary(Data); +to_binary(Data) when is_binary(Data) -> + Data. + +base62_size(Data, Encoded) -> + DataSize = erlang:size(Data), + EncodedSize = erlang:size(Encoded), + case (DataSize * 8 rem 6) of + 0 -> + %% Due to the particularity of base 62, 3 bytes data maybe encoded + %% as 4 bytes data or 5 bytes data, the encode size maybe in the + %% range between DataSize*4/3 and DataSize*8/3 + RangeStart = DataSize div 3 * 4, + RangeEnd = DataSize div 3 * 8, + EncodedSize >= RangeStart andalso EncodedSize =< RangeEnd; + _Rem -> + RangeStart = DataSize * 8 div 6 + 1, + RangeEnd = DataSize * 8 div 6 * 2 + 1, + EncodedSize >= RangeStart andalso EncodedSize =< RangeEnd + end. + +%%%%%%%%%%%%%%%%%% +%%% Generators %%% +%%%%%%%%%%%%%%%%%% +raw_data() -> oneof([integer(), string(), binary()]). diff --git a/test/prop_emqx_session.erl b/test/prop_emqx_session.erl new file mode 100644 index 000000000..5e137ee12 --- /dev/null +++ b/test/prop_emqx_session.erl @@ -0,0 +1,327 @@ +%%-------------------------------------------------------------------- +%% 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. diff --git a/test/rfc6455_client.erl b/test/rfc6455_client.erl deleted file mode 100644 index 18b094a76..000000000 --- a/test/rfc6455_client.erl +++ /dev/null @@ -1,251 +0,0 @@ -%% The contents of this file are subject to the Mozilla Public License -%% Version 1.1 (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.mozilla.org/MPL/ -%% -%% Software distributed under the License is distributed on an "AS IS" -%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the -%% License for the specific language governing rights and limitations -%% under the License. -%% -%% The Original Code is RabbitMQ Management Console. -%% -%% The Initial Developer of the Original Code is GoPivotal, Inc. -%% Copyright (c) 2012-2016 Pivotal Software, Inc. All rights reserved. -%% - --module(rfc6455_client). - --export([new/2, open/1, recv/1, send/2, send_binary/2, close/1, close/2]). - --record(state, {host, port, addr, path, ppid, socket, data, phase}). - -%% -------------------------------------------------------------------------- - -new(WsUrl, PPid) -> - crypto:start(), - "ws://" ++ Rest = WsUrl, - [Addr, Path] = split("/", Rest, 1), - [Host, MaybePort] = split(":", Addr, 1, empty), - Port = case MaybePort of - empty -> 80; - V -> {I, ""} = string:to_integer(V), I - end, - State = #state{host = Host, - port = Port, - addr = Addr, - path = "/" ++ Path, - ppid = PPid}, - spawn(fun() -> - start_conn(State) - end). - -open(WS) -> - receive - {rfc6455, open, WS, Opts} -> - {ok, Opts}; - {rfc6455, close, WS, R} -> - {close, R} - end. - -recv(WS) -> - receive - {rfc6455, recv, WS, Payload} -> - {ok, Payload}; - {rfc6455, recv_binary, WS, Payload} -> - {binary, Payload}; - {rfc6455, close, WS, R} -> - {close, R} - end. - -send(WS, IoData) -> - WS ! {send, IoData}, - ok. - -send_binary(WS, IoData) -> - WS ! {send_binary, IoData}, - ok. - -close(WS) -> - close(WS, {1000, ""}). - -close(WS, WsReason) -> - WS ! {close, WsReason}, - receive - {rfc6455, close, WS, R} -> - {close, R} - end. - - -%% -------------------------------------------------------------------------- - -start_conn(State) -> - {ok, Socket} = gen_tcp:connect(State#state.host, State#state.port, - [binary, - {packet, 0}]), - Key = base64:encode_to_string(crypto:strong_rand_bytes(16)), - gen_tcp:send(Socket, - "GET " ++ State#state.path ++ " HTTP/1.1\r\n" ++ - "Host: " ++ State#state.addr ++ "\r\n" ++ - "Upgrade: websocket\r\n" ++ - "Connection: Upgrade\r\n" ++ - "Sec-WebSocket-Key: " ++ Key ++ "\r\n" ++ - "Origin: null\r\n" ++ - "Sec-WebSocket-Protocol: mqtt\r\n" ++ - "Sec-WebSocket-Version: 13\r\n\r\n"), - - loop(State#state{socket = Socket, - data = <<>>, - phase = opening}). - -do_recv(State = #state{phase = opening, ppid = PPid, data = Data}) -> - case split("\r\n\r\n", binary_to_list(Data), 1, empty) of - [_Http, empty] -> State; - [Http, Data1] -> - %% TODO: don't ignore http response data, verify key - PPid ! {rfc6455, open, self(), [{http_response, Http}]}, - State#state{phase = open, - data = Data1} - end; -do_recv(State = #state{phase = Phase, data = Data, socket = Socket, ppid = PPid}) - when Phase =:= open orelse Phase =:= closing -> - R = case Data of - <> - when L < 126 -> - {F, O, Payload, Rest}; - - <> -> - {F, O, Payload, Rest}; - - <> -> - {F, O, Payload, Rest}; - - <<_:1, _:3, _:4, 1:1, _/binary>> -> - %% According o rfc6455 5.1 the server must not mask any frames. - die(Socket, PPid, {1006, "Protocol error"}, normal); - _ -> - moredata - end, - case R of - moredata -> - State; - _ -> do_recv2(State, R) - end. - -do_recv2(State = #state{phase = Phase, socket = Socket, ppid = PPid}, R) -> - case R of - {1, 1, Payload, Rest} -> - PPid ! {rfc6455, recv, self(), Payload}, - State#state{data = Rest}; - {1, 2, Payload, Rest} -> - PPid ! {rfc6455, recv_binary, self(), Payload}, - State#state{data = Rest}; - {1, 8, Payload, _Rest} -> - WsReason = case Payload of - <> -> {WC, WR}; - <<>> -> {1005, "No status received"} - end, - case Phase of - open -> %% echo - do_close(State, WsReason), - gen_tcp:close(Socket); - closing -> - ok - end, - die(Socket, PPid, WsReason, normal); - {_, _, _, _Rest2} -> - io:format("Unknown frame type~n"), - die(Socket, PPid, {1006, "Unknown frame type"}, normal) - end. - -encode_frame(F, O, Payload) -> - Mask = crypto:strong_rand_bytes(4), - MaskedPayload = apply_mask(Mask, iolist_to_binary(Payload)), - - L = byte_size(MaskedPayload), - IoData = case L of - _ when L < 126 -> - [<>, Mask, MaskedPayload]; - _ when L < 65536 -> - [<>, Mask, MaskedPayload]; - _ -> - [<>, Mask, MaskedPayload] - end, - iolist_to_binary(IoData). - -do_send(State = #state{socket = Socket}, Payload) -> - gen_tcp:send(Socket, encode_frame(1, 1, Payload)), - State. - -do_send_binary(State = #state{socket = Socket}, Payload) -> - gen_tcp:send(Socket, encode_frame(1, 2, Payload)), - State. - -do_close(State = #state{socket = Socket}, {Code, Reason}) -> - Payload = iolist_to_binary([<>, Reason]), - gen_tcp:send(Socket, encode_frame(1, 8, Payload)), - State#state{phase = closing}. - -loop(State = #state{socket = Socket, ppid = PPid, data = Data, - phase = Phase}) -> - receive - {tcp, Socket, Bin} -> - State1 = State#state{data = iolist_to_binary([Data, Bin])}, - loop(do_recv(State1)); - {send, Payload} when Phase == open -> - loop(do_send(State, Payload)); - {send_binary, Payload} when Phase == open -> - loop(do_send_binary(State, Payload)); - {tcp_closed, Socket} -> - die(Socket, PPid, {1006, "Connection closed abnormally"}, normal); - {close, WsReason} when Phase == open -> - loop(do_close(State, WsReason)) - end. - - -die(Socket, PPid, WsReason, Reason) -> - gen_tcp:shutdown(Socket, read_write), - PPid ! {rfc6455, close, self(), WsReason}, - exit(Reason). - - -%% -------------------------------------------------------------------------- - -split(SubStr, Str, Limit) -> - split(SubStr, Str, Limit, ""). - -split(SubStr, Str, Limit, Default) -> - Acc = split(SubStr, Str, Limit, [], Default), - lists:reverse(Acc). -split(_SubStr, Str, 0, Acc, _Default) -> [Str | Acc]; -split(SubStr, Str, Limit, Acc, Default) -> - {L, R} = case string:str(Str, SubStr) of - 0 -> {Str, Default}; - I -> {string:substr(Str, 1, I-1), - string:substr(Str, I+length(SubStr))} - end, - split(SubStr, R, Limit-1, [L | Acc], Default). - - -apply_mask(Mask, Data) when is_number(Mask) -> - apply_mask(<>, Data); - -apply_mask(<<0:32>>, Data) -> - Data; -apply_mask(Mask, Data) -> - iolist_to_binary(lists:reverse(apply_mask2(Mask, Data, []))). - -apply_mask2(M = <>, <>, Acc) -> - T = Data bxor Mask, - apply_mask2(M, Rest, [<> | Acc]); -apply_mask2(<>, <>, Acc) -> - T = Data bxor Mask, - [<> | Acc]; -apply_mask2(<>, <>, Acc) -> - T = Data bxor Mask, - [<> | Acc]; -apply_mask2(<>, <>, Acc) -> - T = Data bxor Mask, - [<> | Acc]; -apply_mask2(_, <<>>, Acc) -> - Acc. diff --git a/test/ws_client.erl b/test/ws_client.erl deleted file mode 100644 index 39f01467a..000000000 --- a/test/ws_client.erl +++ /dev/null @@ -1,75 +0,0 @@ --module(ws_client). - --export([ - start_link/0, - start_link/1, - send_binary/2, - send_ping/2, - recv/2, - recv/1, - stop/1 - ]). - --export([ - init/2, - websocket_handle/3, - websocket_info/3, - websocket_terminate/3 - ]). - --record(state, { - buffer = [] :: list(), - waiting = undefined :: undefined | pid() - }). - -start_link() -> - start_link("ws://localhost:8083/mqtt"). - -start_link(Url) -> - websocket_client:start_link(Url, ?MODULE, [], [{extra_headers, [{"Sec-Websocket-Protocol", "mqtt"}]}]). - -stop(Pid) -> - Pid ! stop. - -send_binary(Pid, Msg) -> - websocket_client:cast(Pid, {binary, Msg}). - -send_ping(Pid, Msg) -> - websocket_client:cast(Pid, {ping, Msg}). - -recv(Pid) -> - recv(Pid, 5000). - -recv(Pid, Timeout) -> - Pid ! {recv, self()}, - receive - M -> M - after - Timeout -> error - end. - -init(_, _WSReq) -> - {ok, #state{}}. - -websocket_handle(Frame, _, State = #state{waiting = undefined, buffer = Buffer}) -> - logger:info("Client received frame~p", [Frame]), - {ok, State#state{buffer = [Frame|Buffer]}}; -websocket_handle(Frame, _, State = #state{waiting = From}) -> - logger:info("Client received frame~p", [Frame]), - From ! Frame, - {ok, State#state{waiting = undefined}}. - -websocket_info({send_text, Text}, WSReq, State) -> - websocket_client:send({text, Text}, WSReq), - {ok, State}; -websocket_info({recv, From}, _, State = #state{buffer = []}) -> - {ok, State#state{waiting = From}}; -websocket_info({recv, From}, _, State = #state{buffer = [Top|Rest]}) -> - From ! Top, - {ok, State#state{buffer = Rest}}; -websocket_info(stop, _, State) -> - {close, <<>>, State}. - -websocket_terminate(Close, _, State) -> - io:format("Websocket closed with frame ~p and state ~p", [Close, State]), - ok.